matrix-nodejs-homeserver 0.0.0-stage → 0.3.1
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/LICENSE +75 -0
- package/README.md +479 -2
- package/dist/adapters/auth.d.ts +50 -0
- package/dist/adapters/auth.js +18 -0
- package/dist/adapters/cluster.d.ts +55 -0
- package/dist/adapters/cluster.js +249 -0
- package/dist/adapters/media.d.ts +50 -0
- package/dist/adapters/media.js +63 -0
- package/dist/adapters/s3.d.ts +33 -0
- package/dist/adapters/s3.js +95 -0
- package/dist/api/account.js +594 -0
- package/dist/api/admin.js +267 -0
- package/dist/api/appservice.js +45 -0
- package/dist/api/backup.js +280 -0
- package/dist/api/federation.js +826 -0
- package/dist/api/filters.js +88 -0
- package/dist/api/index.js +47 -0
- package/dist/api/legacy.js +183 -0
- package/dist/api/media.js +271 -0
- package/dist/api/misc.js +95 -0
- package/dist/api/preview.js +201 -0
- package/dist/api/profile.js +176 -0
- package/dist/api/push.js +202 -0
- package/dist/api/relationships.js +270 -0
- package/dist/api/rooms.js +1487 -0
- package/dist/api/rtc.d.ts +23 -0
- package/dist/api/rtc.js +66 -0
- package/dist/api/search.js +161 -0
- package/dist/api/slidingsync.js +474 -0
- package/dist/api/spaces.js +177 -0
- package/dist/api/sync.js +609 -0
- package/dist/api/threadsubs.js +140 -0
- package/dist/api/threepid.js +155 -0
- package/dist/api/uia.js +127 -0
- package/dist/api/user.js +329 -0
- package/dist/core/admin.d.ts +84 -0
- package/dist/core/admin.js +308 -0
- package/dist/core/adminroom.d.ts +49 -0
- package/dist/core/adminroom.js +483 -0
- package/dist/core/appservices.d.ts +74 -0
- package/dist/core/appservices.js +369 -0
- package/dist/core/audit.d.ts +50 -0
- package/dist/core/audit.js +86 -0
- package/dist/core/auth.d.ts +48 -0
- package/dist/core/auth.js +237 -0
- package/dist/core/authrules.d.ts +4 -0
- package/dist/core/authrules.js +348 -0
- package/dist/core/cluster.d.ts +66 -0
- package/dist/core/cluster.js +191 -0
- package/dist/core/crypto.d.ts +10 -0
- package/dist/core/crypto.js +100 -0
- package/dist/core/delayed.d.ts +39 -0
- package/dist/core/delayed.js +160 -0
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +45 -0
- package/dist/core/events.d.ts +32 -0
- package/dist/core/events.js +186 -0
- package/dist/core/glob.js +66 -0
- package/dist/core/identity.d.ts +24 -0
- package/dist/core/identity.js +96 -0
- package/dist/core/ids.js +36 -0
- package/dist/core/invitefilter.js +36 -0
- package/dist/core/json.js +16 -0
- package/dist/core/keys.d.ts +36 -0
- package/dist/core/keys.js +450 -0
- package/dist/core/m.d.ts +209 -0
- package/dist/core/m.js +182 -0
- package/dist/core/notifier.d.ts +17 -0
- package/dist/core/notifier.js +54 -0
- package/dist/core/oauth.d.ts +38 -0
- package/dist/core/oauth.js +108 -0
- package/dist/core/partialstate.d.ts +32 -0
- package/dist/core/partialstate.js +198 -0
- package/dist/core/phone.js +268 -0
- package/dist/core/policy.d.ts +29 -0
- package/dist/core/policy.js +97 -0
- package/dist/core/profile.js +39 -0
- package/dist/core/push.d.ts +46 -0
- package/dist/core/push.js +307 -0
- package/dist/core/pushrules.js +229 -0
- package/dist/core/ratelimit.d.ts +23 -0
- package/dist/core/ratelimit.js +77 -0
- package/dist/core/rooms.d.ts +215 -0
- package/dist/core/rooms.js +1731 -0
- package/dist/core/settings.d.ts +11 -0
- package/dist/core/settings.js +29 -0
- package/dist/core/sso.d.ts +46 -0
- package/dist/core/sso.js +165 -0
- package/dist/core/stateres.d.ts +4 -0
- package/dist/core/stateres.js +209 -0
- package/dist/core/threepid.d.ts +80 -0
- package/dist/core/threepid.js +275 -0
- package/dist/core/typing.d.ts +16 -0
- package/dist/core/typing.js +60 -0
- package/dist/federation/client.d.ts +51 -0
- package/dist/federation/client.js +279 -0
- package/dist/federation/index.d.ts +153 -0
- package/dist/federation/index.js +1059 -0
- package/dist/federation/keyring.d.ts +26 -0
- package/dist/federation/keyring.js +122 -0
- package/dist/federation/sender.d.ts +26 -0
- package/dist/federation/sender.js +164 -0
- package/dist/homeserver.d.ts +224 -0
- package/dist/homeserver.js +302 -0
- package/dist/http/adapters.d.ts +40 -0
- package/dist/http/adapters.js +130 -0
- package/dist/http/handler.d.ts +8 -0
- package/dist/http/handler.js +181 -0
- package/dist/http/input.js +24 -0
- package/dist/http/procedure.js +25 -0
- package/dist/http/router.d.ts +43 -0
- package/dist/http/router.js +56 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +15 -0
- package/dist/storage/memory.d.ts +5 -0
- package/dist/storage/memory.js +122 -0
- package/dist/storage/schema.d.ts +4 -0
- package/dist/storage/schema.js +664 -0
- package/dist/storage/sql.d.ts +72 -0
- package/dist/storage/sql.js +274 -0
- package/dist/storage/types.d.ts +55 -0
- package/dist/vendor/zod/LICENSE +21 -0
- package/dist/vendor/zod/v4/classic/errors.js +54 -0
- package/dist/vendor/zod/v4/classic/parse.js +17 -0
- package/dist/vendor/zod/v4/classic/schemas.js +865 -0
- package/dist/vendor/zod/v4/core/api.js +512 -0
- package/dist/vendor/zod/v4/core/checks.js +300 -0
- package/dist/vendor/zod/v4/core/core.js +109 -0
- package/dist/vendor/zod/v4/core/doc.js +35 -0
- package/dist/vendor/zod/v4/core/errors.js +118 -0
- package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
- package/dist/vendor/zod/v4/core/memoizer.js +255 -0
- package/dist/vendor/zod/v4/core/parse.js +206 -0
- package/dist/vendor/zod/v4/core/regexes.js +73 -0
- package/dist/vendor/zod/v4/core/registries.js +48 -0
- package/dist/vendor/zod/v4/core/schemas.js +1378 -0
- package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
- package/dist/vendor/zod/v4/core/util.js +537 -0
- package/dist/vendor/zod/v4/core/versions.js +8 -0
- package/dist/vendor/zod/v4/locales/en.js +110 -0
- package/package.json +65 -4
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { randomString } from "./crypto.js";
|
|
2
|
+
//#region src/core/cluster.ts
|
|
3
|
+
/** In-process cluster adapter: a single homeserver instance. */
|
|
4
|
+
function localCluster() {
|
|
5
|
+
const sequences = /* @__PURE__ */ new Map();
|
|
6
|
+
const locks = /* @__PURE__ */ new Map();
|
|
7
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
8
|
+
const kv = /* @__PURE__ */ new Map();
|
|
9
|
+
const live = (key) => {
|
|
10
|
+
const e = kv.get(key);
|
|
11
|
+
if (!e) return void 0;
|
|
12
|
+
if (e.expires && e.expires < Date.now()) {
|
|
13
|
+
kv.delete(key);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
return e;
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
async nextId(seq) {
|
|
20
|
+
const v = (sequences.get(seq) ?? 0) + 1;
|
|
21
|
+
sequences.set(seq, v);
|
|
22
|
+
return v;
|
|
23
|
+
},
|
|
24
|
+
async currentId(seq) {
|
|
25
|
+
return sequences.get(seq) ?? 0;
|
|
26
|
+
},
|
|
27
|
+
async advance(seq, value) {
|
|
28
|
+
if (value > (sequences.get(seq) ?? 0)) sequences.set(seq, value);
|
|
29
|
+
},
|
|
30
|
+
async lock(key, fn) {
|
|
31
|
+
const run = (locks.get(key) ?? Promise.resolve()).then(fn, fn);
|
|
32
|
+
const settled = run.catch(() => {});
|
|
33
|
+
locks.set(key, settled);
|
|
34
|
+
try {
|
|
35
|
+
return await run;
|
|
36
|
+
} finally {
|
|
37
|
+
if (locks.get(key) === settled) locks.delete(key);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async publish(channel, message) {
|
|
41
|
+
for (const h of handlers.get(channel) ?? []) queueMicrotask(() => h(message));
|
|
42
|
+
},
|
|
43
|
+
subscribe(channel, handler) {
|
|
44
|
+
let set = handlers.get(channel);
|
|
45
|
+
if (!set) handlers.set(channel, set = /* @__PURE__ */ new Set());
|
|
46
|
+
set.add(handler);
|
|
47
|
+
},
|
|
48
|
+
async get(key) {
|
|
49
|
+
return live(key)?.value ?? null;
|
|
50
|
+
},
|
|
51
|
+
async set(key, value, ttlMs) {
|
|
52
|
+
kv.set(key, {
|
|
53
|
+
value,
|
|
54
|
+
expires: ttlMs ? Date.now() + ttlMs : 0
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
async setIfAbsent(key, value, ttlMs) {
|
|
58
|
+
if (live(key)) return false;
|
|
59
|
+
kv.set(key, {
|
|
60
|
+
value,
|
|
61
|
+
expires: ttlMs ? Date.now() + ttlMs : 0
|
|
62
|
+
});
|
|
63
|
+
if (kv.size > 2e5) {
|
|
64
|
+
for (const k of kv.keys()) if (!live(k)) kv.delete(k);
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
},
|
|
68
|
+
async delete(key) {
|
|
69
|
+
kv.delete(key);
|
|
70
|
+
},
|
|
71
|
+
async entries(prefix) {
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const k of [...kv.keys()]) if (k.startsWith(prefix)) {
|
|
74
|
+
const e = live(k);
|
|
75
|
+
if (e) out.push([k, e.value]);
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const WRITER_TTL_MS = 3e4;
|
|
82
|
+
/**
|
|
83
|
+
* Stream positions with multiple writers. Positions are allocated from a global
|
|
84
|
+
* sequence; readers only see positions up to which every writer has finished, so
|
|
85
|
+
* an event committed late with a lower position can never be skipped by /sync.
|
|
86
|
+
*
|
|
87
|
+
* Each writer publishes `{ busy, pos }`: when busy, all of its positions ≤ pos are
|
|
88
|
+
* persisted. Readers use min(global, busy writers' pos).
|
|
89
|
+
*/
|
|
90
|
+
var StreamManager = class {
|
|
91
|
+
cluster;
|
|
92
|
+
instanceId;
|
|
93
|
+
inflight = /* @__PURE__ */ new Set();
|
|
94
|
+
published = {
|
|
95
|
+
busy: false,
|
|
96
|
+
pos: 0
|
|
97
|
+
};
|
|
98
|
+
transition = Promise.resolve();
|
|
99
|
+
heartbeat;
|
|
100
|
+
positionCache = null;
|
|
101
|
+
constructor(cluster, instanceId) {
|
|
102
|
+
this.cluster = cluster;
|
|
103
|
+
this.instanceId = instanceId;
|
|
104
|
+
this.heartbeat = setInterval(() => void this.publishState().catch(() => {}), WRITER_TTL_MS / 3);
|
|
105
|
+
this.heartbeat.unref?.();
|
|
106
|
+
}
|
|
107
|
+
key() {
|
|
108
|
+
return `writer:${this.instanceId}`;
|
|
109
|
+
}
|
|
110
|
+
async publishState() {
|
|
111
|
+
await this.cluster.set(this.key(), this.published, WRITER_TTL_MS);
|
|
112
|
+
}
|
|
113
|
+
/** Allocate a position. The caller must call `done()` once its writes are committed. */
|
|
114
|
+
async allocate() {
|
|
115
|
+
await (this.transition = this.transition.then(async () => {
|
|
116
|
+
if (!this.published.busy) {
|
|
117
|
+
this.published = {
|
|
118
|
+
busy: true,
|
|
119
|
+
pos: await this.cluster.currentId("stream")
|
|
120
|
+
};
|
|
121
|
+
await this.publishState();
|
|
122
|
+
}
|
|
123
|
+
}));
|
|
124
|
+
const id = await this.cluster.nextId("stream");
|
|
125
|
+
this.inflight.add(id);
|
|
126
|
+
let finished = false;
|
|
127
|
+
const timer = setTimeout(() => void lease.done(), 6e4);
|
|
128
|
+
timer.unref?.();
|
|
129
|
+
const lease = {
|
|
130
|
+
id,
|
|
131
|
+
done: async () => {
|
|
132
|
+
if (finished) return;
|
|
133
|
+
finished = true;
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
this.inflight.delete(id);
|
|
136
|
+
await (this.transition = this.transition.then(async () => {
|
|
137
|
+
this.published = this.inflight.size ? {
|
|
138
|
+
busy: true,
|
|
139
|
+
pos: Math.min(...this.inflight) - 1
|
|
140
|
+
} : {
|
|
141
|
+
busy: false,
|
|
142
|
+
pos: await this.cluster.currentId("stream")
|
|
143
|
+
};
|
|
144
|
+
await this.publishState();
|
|
145
|
+
}));
|
|
146
|
+
this.positionCache = null;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
return lease;
|
|
150
|
+
}
|
|
151
|
+
/** Allocate a position, run `fn` with it, then complete it. */
|
|
152
|
+
async write(fn) {
|
|
153
|
+
const lease = await this.allocate();
|
|
154
|
+
try {
|
|
155
|
+
return await fn(lease.id);
|
|
156
|
+
} finally {
|
|
157
|
+
await lease.done();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** A fresh decreasing position for backfilled (historical) events. */
|
|
161
|
+
async backfillId() {
|
|
162
|
+
return -await this.cluster.nextId("backfill");
|
|
163
|
+
}
|
|
164
|
+
/** The highest position below which everything is persisted, cluster-wide. */
|
|
165
|
+
async position() {
|
|
166
|
+
if (this.positionCache && Date.now() - this.positionCache.at < 20) return this.positionCache.value;
|
|
167
|
+
let pos = await this.cluster.currentId("stream");
|
|
168
|
+
for (const [, w] of await this.cluster.entries("writer:")) if (w?.busy) pos = Math.min(pos, w.pos);
|
|
169
|
+
if (this.inflight.size) pos = Math.min(pos, Math.min(...this.inflight) - 1);
|
|
170
|
+
this.positionCache = {
|
|
171
|
+
value: pos,
|
|
172
|
+
at: Date.now()
|
|
173
|
+
};
|
|
174
|
+
return pos;
|
|
175
|
+
}
|
|
176
|
+
async advanceTo(value) {
|
|
177
|
+
await this.cluster.advance("stream", value);
|
|
178
|
+
}
|
|
179
|
+
async lowerBackfillTo(value) {
|
|
180
|
+
if (value < 0) await this.cluster.advance("backfill", -value);
|
|
181
|
+
}
|
|
182
|
+
async stop() {
|
|
183
|
+
clearInterval(this.heartbeat);
|
|
184
|
+
await this.cluster.delete(this.key()).catch(() => {});
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
function newInstanceId() {
|
|
188
|
+
return `${process.pid}-${randomString(8)}`;
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
export { StreamManager, localCluster, newInstanceId };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { KeyObject } from "node:crypto";
|
|
2
|
+
//#region src/core/crypto.d.ts
|
|
3
|
+
export interface SigningKey {
|
|
4
|
+
keyId: string;
|
|
5
|
+
privateKey: KeyObject;
|
|
6
|
+
publicKeyBase64: string;
|
|
7
|
+
/** Unpadded base64 of the 32-byte ed25519 seed, for persistence. */
|
|
8
|
+
seed: string;
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { canonicalJson } from "./json.js";
|
|
2
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, randomInt, scrypt, sign, timingSafeEqual, verify } from "node:crypto";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
//#region src/core/crypto.ts
|
|
5
|
+
const scryptAsync = promisify(scrypt);
|
|
6
|
+
function unpaddedBase64(buf) {
|
|
7
|
+
return Buffer.from(buf).toString("base64").replace(/=+$/, "");
|
|
8
|
+
}
|
|
9
|
+
function urlSafeBase64(buf) {
|
|
10
|
+
return unpaddedBase64(buf).replace(/\+/g, "-").replace(/\//g, "_");
|
|
11
|
+
}
|
|
12
|
+
function randomString(length) {
|
|
13
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
14
|
+
let out = "";
|
|
15
|
+
for (let i = 0; i < length; i++) out += alphabet[randomInt(52)];
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
function randomToken(bytes = 32) {
|
|
19
|
+
return urlSafeBase64(randomBytes(bytes));
|
|
20
|
+
}
|
|
21
|
+
function sha256(data) {
|
|
22
|
+
return createHash("sha256").update(data).digest();
|
|
23
|
+
}
|
|
24
|
+
async function hashPassword(password) {
|
|
25
|
+
const salt = randomBytes(16);
|
|
26
|
+
const hash = await scryptAsync(password.normalize("NFKC"), salt, 64);
|
|
27
|
+
return `scrypt$${salt.toString("base64")}$${hash.toString("base64")}`;
|
|
28
|
+
}
|
|
29
|
+
async function verifyPassword(password, stored) {
|
|
30
|
+
const [scheme, saltB64, hashB64] = stored.split("$");
|
|
31
|
+
if (scheme !== "scrypt" || !saltB64 || !hashB64) return false;
|
|
32
|
+
const expected = Buffer.from(hashB64, "base64");
|
|
33
|
+
const actual = await scryptAsync(password.normalize("NFKC"), Buffer.from(saltB64, "base64"), expected.length);
|
|
34
|
+
return timingSafeEqual(expected, actual);
|
|
35
|
+
}
|
|
36
|
+
const PKCS8_ED25519_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
37
|
+
function loadSigningKey(keyId, seed) {
|
|
38
|
+
let privateKey;
|
|
39
|
+
if (seed) {
|
|
40
|
+
const raw = Buffer.from(seed, "base64");
|
|
41
|
+
privateKey = createPrivateKey({
|
|
42
|
+
key: Buffer.concat([PKCS8_ED25519_PREFIX, raw]),
|
|
43
|
+
format: "der",
|
|
44
|
+
type: "pkcs8"
|
|
45
|
+
});
|
|
46
|
+
} else privateKey = generateKeyPairSync("ed25519").privateKey;
|
|
47
|
+
const der = privateKey.export({
|
|
48
|
+
format: "der",
|
|
49
|
+
type: "pkcs8"
|
|
50
|
+
});
|
|
51
|
+
const rawSeed = der.subarray(der.length - 32);
|
|
52
|
+
const spki = createPublicKey(privateKey).export({
|
|
53
|
+
format: "der",
|
|
54
|
+
type: "spki"
|
|
55
|
+
});
|
|
56
|
+
const rawPub = spki.subarray(spki.length - 32);
|
|
57
|
+
return {
|
|
58
|
+
keyId,
|
|
59
|
+
privateKey,
|
|
60
|
+
publicKeyBase64: unpaddedBase64(rawPub),
|
|
61
|
+
seed: unpaddedBase64(rawSeed)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Sign a JSON object in place according to the Matrix signing algorithm. */
|
|
65
|
+
function signJson(obj, serverName, key) {
|
|
66
|
+
const { signatures, unsigned, ...rest } = obj;
|
|
67
|
+
const sig = unpaddedBase64(sign(null, Buffer.from(canonicalJson(rest)), key.privateKey));
|
|
68
|
+
const merged = { ...signatures };
|
|
69
|
+
merged[serverName] = {
|
|
70
|
+
...merged[serverName],
|
|
71
|
+
[`ed25519:${key.keyId}`]: sig
|
|
72
|
+
};
|
|
73
|
+
obj.signatures = merged;
|
|
74
|
+
return obj;
|
|
75
|
+
}
|
|
76
|
+
const SPKI_ED25519_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
77
|
+
/** Decode unpadded (standard or URL-safe) base64. */
|
|
78
|
+
function decodeBase64(s) {
|
|
79
|
+
return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
80
|
+
}
|
|
81
|
+
function publicKeyFromBase64(key) {
|
|
82
|
+
return createPublicKey({
|
|
83
|
+
key: Buffer.concat([SPKI_ED25519_PREFIX, decodeBase64(key)]),
|
|
84
|
+
format: "der",
|
|
85
|
+
type: "spki"
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** Verify one ed25519 signature on a JSON object (signatures/unsigned excluded). */
|
|
89
|
+
function verifyJsonSignature(obj, serverName, keyId, publicKey) {
|
|
90
|
+
const sig = obj.signatures?.[serverName]?.[keyId];
|
|
91
|
+
if (typeof sig !== "string") return false;
|
|
92
|
+
const { signatures, unsigned, ...rest } = obj;
|
|
93
|
+
try {
|
|
94
|
+
return verify(null, Buffer.from(canonicalJson(rest)), publicKey, decodeBase64(sig));
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
export { decodeBase64, hashPassword, loadSigningKey, publicKeyFromBase64, randomString, randomToken, sha256, signJson, unpaddedBase64, urlSafeBase64, verifyJsonSignature, verifyPassword };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Homeserver } from "../homeserver.js";
|
|
2
|
+
//#region src/core/delayed.d.ts
|
|
3
|
+
export interface DelayedEventsOptions {
|
|
4
|
+
/** Maximum delay a client may request, in ms. Default: 24 hours. */
|
|
5
|
+
maxDelayMs?: number;
|
|
6
|
+
/** Maximum pending delayed events per user. Default: 100. */
|
|
7
|
+
maxPerUser?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Delayed events (MSC4140), used by MatrixRTC so that a call membership is removed
|
|
11
|
+
* when a client disappears. Stored in the database; any instance may send a due
|
|
12
|
+
* event, and deleting the row first guarantees it is sent exactly once.
|
|
13
|
+
*/
|
|
14
|
+
export declare class DelayedEventService {
|
|
15
|
+
private hs;
|
|
16
|
+
private timer;
|
|
17
|
+
readonly maxDelayMs: number;
|
|
18
|
+
readonly maxPerUser: number;
|
|
19
|
+
constructor(hs: Homeserver, options?: DelayedEventsOptions);
|
|
20
|
+
start(): void;
|
|
21
|
+
stop(): void;
|
|
22
|
+
schedule(opts: {
|
|
23
|
+
userId: string;
|
|
24
|
+
deviceId: string;
|
|
25
|
+
roomId: string;
|
|
26
|
+
type: string;
|
|
27
|
+
stateKey?: string;
|
|
28
|
+
content: Record<string, any>;
|
|
29
|
+
delayMs: number;
|
|
30
|
+
txnId?: string;
|
|
31
|
+
}): Promise<string>;
|
|
32
|
+
list(userId: string): Promise<Record<string, any>[]>;
|
|
33
|
+
update(userId: string, delayId: string, action: string): Promise<void>;
|
|
34
|
+
/** Cancel pending delayed state events for a key overwritten by someone else (MSC4140). */
|
|
35
|
+
onStateEvent(roomId: string, type: string, stateKey: string, sender: string): Promise<void>;
|
|
36
|
+
private tick;
|
|
37
|
+
private send;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { randomString } from "./crypto.js";
|
|
2
|
+
import { UNSTABLE } from "./m.js";
|
|
3
|
+
import { MatrixError, errors } from "./errors.js";
|
|
4
|
+
//#region src/core/delayed.ts
|
|
5
|
+
/**
|
|
6
|
+
* Delayed events (MSC4140), used by MatrixRTC so that a call membership is removed
|
|
7
|
+
* when a client disappears. Stored in the database; any instance may send a due
|
|
8
|
+
* event, and deleting the row first guarantees it is sent exactly once.
|
|
9
|
+
*/
|
|
10
|
+
var DelayedEventService = class {
|
|
11
|
+
hs;
|
|
12
|
+
timer = null;
|
|
13
|
+
maxDelayMs;
|
|
14
|
+
maxPerUser;
|
|
15
|
+
constructor(hs, options = {}) {
|
|
16
|
+
this.hs = hs;
|
|
17
|
+
this.maxDelayMs = options.maxDelayMs ?? 864e5;
|
|
18
|
+
this.maxPerUser = options.maxPerUser ?? 100;
|
|
19
|
+
}
|
|
20
|
+
start() {
|
|
21
|
+
if (this.timer) return;
|
|
22
|
+
this.timer = setInterval(() => void this.tick().catch((err) => this.hs.logger.warn("Delayed events failed", err)), 250);
|
|
23
|
+
this.timer.unref?.();
|
|
24
|
+
}
|
|
25
|
+
stop() {
|
|
26
|
+
if (this.timer) clearInterval(this.timer);
|
|
27
|
+
this.timer = null;
|
|
28
|
+
}
|
|
29
|
+
async schedule(opts) {
|
|
30
|
+
if (!Number.isInteger(opts.delayMs) || opts.delayMs < 0) throw errors.invalidParam("Invalid delay");
|
|
31
|
+
if (opts.delayMs > this.maxDelayMs) throw new MatrixError(400, "M_MAX_DELAY_EXCEEDED", "Delay is too long", { [UNSTABLE.ORG.MATRIX.MSC4140.MAX_DELAY]: this.maxDelayMs });
|
|
32
|
+
const txnKey = opts.txnId ? `${opts.userId}|${opts.deviceId}|${opts.roomId}|${opts.txnId}` : null;
|
|
33
|
+
if (txnKey) {
|
|
34
|
+
const existing = await this.hs.db.findOne("delayed_events", [{
|
|
35
|
+
field: "txn_key",
|
|
36
|
+
value: txnKey
|
|
37
|
+
}]);
|
|
38
|
+
if (existing) return existing.delay_id;
|
|
39
|
+
}
|
|
40
|
+
if ((await this.hs.db.findMany("delayed_events", { where: [{
|
|
41
|
+
field: "user_id",
|
|
42
|
+
value: opts.userId
|
|
43
|
+
}] })).length >= this.maxPerUser) throw new MatrixError(400, "M_LIMIT_EXCEEDED", "Too many delayed events");
|
|
44
|
+
await this.hs.rooms.requireJoined(opts.roomId, opts.userId);
|
|
45
|
+
const delayId = `syd_${randomString(20)}`;
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
await this.hs.db.create("delayed_events", {
|
|
48
|
+
delay_id: delayId,
|
|
49
|
+
user_id: opts.userId,
|
|
50
|
+
device_id: opts.deviceId,
|
|
51
|
+
room_id: opts.roomId,
|
|
52
|
+
event_type: opts.type,
|
|
53
|
+
state_key: opts.stateKey ?? null,
|
|
54
|
+
content: opts.content,
|
|
55
|
+
delay_ms: opts.delayMs,
|
|
56
|
+
send_at: now + opts.delayMs,
|
|
57
|
+
running_since: now,
|
|
58
|
+
txn_key: txnKey
|
|
59
|
+
});
|
|
60
|
+
return delayId;
|
|
61
|
+
}
|
|
62
|
+
async list(userId) {
|
|
63
|
+
return (await this.hs.db.findMany("delayed_events", {
|
|
64
|
+
where: [{
|
|
65
|
+
field: "user_id",
|
|
66
|
+
value: userId
|
|
67
|
+
}],
|
|
68
|
+
sortBy: {
|
|
69
|
+
field: "send_at",
|
|
70
|
+
direction: "asc"
|
|
71
|
+
}
|
|
72
|
+
})).map((r) => ({
|
|
73
|
+
delay_id: r.delay_id,
|
|
74
|
+
room_id: r.room_id,
|
|
75
|
+
type: r.event_type,
|
|
76
|
+
...r.state_key !== null ? { state_key: r.state_key } : {},
|
|
77
|
+
delay: r.delay_ms,
|
|
78
|
+
running_since: r.running_since,
|
|
79
|
+
content: r.content
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
async update(userId, delayId, action) {
|
|
83
|
+
if (![
|
|
84
|
+
"cancel",
|
|
85
|
+
"restart",
|
|
86
|
+
"send"
|
|
87
|
+
].includes(action)) throw errors.unrecognized("Unknown action");
|
|
88
|
+
const where = [{
|
|
89
|
+
field: "delay_id",
|
|
90
|
+
value: delayId
|
|
91
|
+
}, {
|
|
92
|
+
field: "user_id",
|
|
93
|
+
value: userId
|
|
94
|
+
}];
|
|
95
|
+
const row = await this.hs.db.findOne("delayed_events", where);
|
|
96
|
+
if (!row) throw errors.notFound("Unknown delayed event");
|
|
97
|
+
if (action === "cancel") await this.hs.db.delete("delayed_events", where);
|
|
98
|
+
else if (action === "restart") {
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
await this.hs.db.update("delayed_events", where, {
|
|
101
|
+
send_at: now + row.delay_ms,
|
|
102
|
+
running_since: now
|
|
103
|
+
});
|
|
104
|
+
} else if (await this.hs.db.delete("delayed_events", where)) await this.send(row);
|
|
105
|
+
}
|
|
106
|
+
/** Cancel pending delayed state events for a key overwritten by someone else (MSC4140). */
|
|
107
|
+
async onStateEvent(roomId, type, stateKey, sender) {
|
|
108
|
+
const rows = await this.hs.db.findMany("delayed_events", { where: [
|
|
109
|
+
{
|
|
110
|
+
field: "room_id",
|
|
111
|
+
value: roomId
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
field: "event_type",
|
|
115
|
+
value: type
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
field: "state_key",
|
|
119
|
+
value: stateKey
|
|
120
|
+
}
|
|
121
|
+
] });
|
|
122
|
+
for (const r of rows) if (r.user_id !== sender) await this.hs.db.delete("delayed_events", [{
|
|
123
|
+
field: "delay_id",
|
|
124
|
+
value: r.delay_id
|
|
125
|
+
}]);
|
|
126
|
+
}
|
|
127
|
+
async tick() {
|
|
128
|
+
const due = await this.hs.db.findMany("delayed_events", {
|
|
129
|
+
where: [{
|
|
130
|
+
field: "send_at",
|
|
131
|
+
operator: "lte",
|
|
132
|
+
value: Date.now()
|
|
133
|
+
}],
|
|
134
|
+
sortBy: {
|
|
135
|
+
field: "send_at",
|
|
136
|
+
direction: "asc"
|
|
137
|
+
},
|
|
138
|
+
limit: 100
|
|
139
|
+
});
|
|
140
|
+
for (const row of due) {
|
|
141
|
+
if (!await this.hs.db.delete("delayed_events", [{
|
|
142
|
+
field: "delay_id",
|
|
143
|
+
value: row.delay_id
|
|
144
|
+
}, {
|
|
145
|
+
field: "send_at",
|
|
146
|
+
value: row.send_at
|
|
147
|
+
}])) continue;
|
|
148
|
+
await this.send(row).catch((err) => this.hs.logger.warn("Sending delayed event failed", row.delay_id, err));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async send(row) {
|
|
152
|
+
await this.hs.rooms.sendEvent(row.room_id, row.user_id, {
|
|
153
|
+
type: row.event_type,
|
|
154
|
+
content: row.content,
|
|
155
|
+
...row.state_key !== null ? { state_key: row.state_key } : {}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
//#endregion
|
|
160
|
+
export { DelayedEventService };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/core/errors.d.ts
|
|
2
|
+
export declare class MatrixError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly errcode: string;
|
|
5
|
+
readonly extra: Record<string, unknown>;
|
|
6
|
+
constructor(status: number, errcode: string, message: string, extra?: Record<string, unknown>);
|
|
7
|
+
toJSON(): Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/core/errors.ts
|
|
2
|
+
var MatrixError = class extends Error {
|
|
3
|
+
status;
|
|
4
|
+
errcode;
|
|
5
|
+
extra;
|
|
6
|
+
constructor(status, errcode, message, extra = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.errcode = errcode;
|
|
10
|
+
this.extra = extra;
|
|
11
|
+
}
|
|
12
|
+
toJSON() {
|
|
13
|
+
return {
|
|
14
|
+
errcode: this.errcode,
|
|
15
|
+
error: this.message,
|
|
16
|
+
...this.extra
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const errors = {
|
|
21
|
+
forbidden: (msg = "Forbidden") => new MatrixError(403, "M_FORBIDDEN", msg),
|
|
22
|
+
notFound: (msg = "Not found") => new MatrixError(404, "M_NOT_FOUND", msg),
|
|
23
|
+
unrecognized: (msg = "Unrecognized request") => new MatrixError(404, "M_UNRECOGNIZED", msg),
|
|
24
|
+
badJson: (msg = "Bad JSON") => new MatrixError(400, "M_BAD_JSON", msg),
|
|
25
|
+
notJson: (msg = "Content not JSON") => new MatrixError(400, "M_NOT_JSON", msg),
|
|
26
|
+
missingParam: (msg = "Missing parameter") => new MatrixError(400, "M_MISSING_PARAM", msg),
|
|
27
|
+
invalidParam: (msg = "Invalid parameter") => new MatrixError(400, "M_INVALID_PARAM", msg),
|
|
28
|
+
unknownToken: (msg = "Unknown access token", softLogout = false) => new MatrixError(401, "M_UNKNOWN_TOKEN", msg, { soft_logout: softLogout }),
|
|
29
|
+
userLocked: (msg = "This account has been locked") => new MatrixError(401, "M_USER_LOCKED", msg, { soft_logout: true }),
|
|
30
|
+
userSuspended: (msg = "You cannot perform this action while suspended.") => new MatrixError(403, "M_USER_SUSPENDED", msg),
|
|
31
|
+
missingToken: () => new MatrixError(401, "M_MISSING_TOKEN", "Missing access token"),
|
|
32
|
+
userInUse: () => new MatrixError(400, "M_USER_IN_USE", "User ID already taken"),
|
|
33
|
+
invalidUsername: (msg = "Invalid username") => new MatrixError(400, "M_INVALID_USERNAME", msg),
|
|
34
|
+
roomInUse: () => new MatrixError(400, "M_ROOM_IN_USE", "Room alias already taken"),
|
|
35
|
+
tooLarge: (msg = "Too large") => new MatrixError(413, "M_TOO_LARGE", msg),
|
|
36
|
+
unsupportedRoomVersion: () => new MatrixError(400, "M_UNSUPPORTED_ROOM_VERSION", "Unsupported room version"),
|
|
37
|
+
unknown: (msg = "Unknown error") => new MatrixError(400, "M_UNKNOWN", msg),
|
|
38
|
+
limitExceeded: () => new MatrixError(429, "M_LIMIT_EXCEEDED", "Too many requests"),
|
|
39
|
+
userDeactivated: () => new MatrixError(403, "M_USER_DEACTIVATED", "User is deactivated"),
|
|
40
|
+
invalidRoomState: (msg = "Invalid room state") => new MatrixError(400, "M_INVALID_ROOM_STATE", msg),
|
|
41
|
+
exclusive: (msg = "Exclusive namespace") => new MatrixError(400, "M_EXCLUSIVE", msg),
|
|
42
|
+
threepidDenied: () => new MatrixError(403, "M_THREEPID_DENIED", "Third party identifiers are not supported")
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { MatrixError, errors };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/core/events.d.ts
|
|
2
|
+
export interface PDU {
|
|
3
|
+
event_id?: string;
|
|
4
|
+
room_id: string;
|
|
5
|
+
sender: string;
|
|
6
|
+
type: string;
|
|
7
|
+
state_key?: string;
|
|
8
|
+
content: Record<string, any>;
|
|
9
|
+
origin_server_ts: number;
|
|
10
|
+
depth: number;
|
|
11
|
+
prev_events: any[];
|
|
12
|
+
auth_events: any[];
|
|
13
|
+
redacts?: string;
|
|
14
|
+
hashes?: {
|
|
15
|
+
sha256: string;
|
|
16
|
+
};
|
|
17
|
+
signatures?: Record<string, Record<string, string>>;
|
|
18
|
+
unsigned?: Record<string, any>;
|
|
19
|
+
origin?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ClientEvent {
|
|
22
|
+
event_id: string;
|
|
23
|
+
room_id?: string;
|
|
24
|
+
sender: string;
|
|
25
|
+
type: string;
|
|
26
|
+
state_key?: string;
|
|
27
|
+
content: Record<string, any>;
|
|
28
|
+
origin_server_ts: number;
|
|
29
|
+
unsigned?: Record<string, any>;
|
|
30
|
+
redacts?: string;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|