matrix-nodejs-homeserver 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +75 -0
- package/README.md +457 -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 +560 -0
- package/dist/api/admin.js +251 -0
- package/dist/api/appservice.js +33 -0
- package/dist/api/backup.js +269 -0
- package/dist/api/federation.js +792 -0
- package/dist/api/filters.js +87 -0
- package/dist/api/index.js +47 -0
- package/dist/api/legacy.js +170 -0
- package/dist/api/media.js +255 -0
- package/dist/api/misc.js +86 -0
- package/dist/api/preview.js +200 -0
- package/dist/api/profile.js +165 -0
- package/dist/api/push.js +187 -0
- package/dist/api/relationships.js +264 -0
- package/dist/api/rooms.js +1364 -0
- package/dist/api/rtc.d.ts +24 -0
- package/dist/api/rtc.js +52 -0
- package/dist/api/search.js +155 -0
- package/dist/api/slidingsync.js +464 -0
- package/dist/api/spaces.js +172 -0
- package/dist/api/sync.js +600 -0
- package/dist/api/threadsubs.js +129 -0
- package/dist/api/threepid.js +122 -0
- package/dist/api/uia.js +126 -0
- package/dist/api/user.js +286 -0
- package/dist/core/admin.d.ts +85 -0
- package/dist/core/admin.js +307 -0
- package/dist/core/adminroom.d.ts +49 -0
- package/dist/core/adminroom.js +482 -0
- package/dist/core/appservices.d.ts +73 -0
- package/dist/core/appservices.js +368 -0
- package/dist/core/audit.d.ts +51 -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 +347 -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 +159 -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 +185 -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 +35 -0
- package/dist/core/json.js +16 -0
- package/dist/core/keys.d.ts +36 -0
- package/dist/core/keys.js +449 -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 +197 -0
- package/dist/core/phone.js +268 -0
- package/dist/core/policy.d.ts +29 -0
- package/dist/core/policy.js +96 -0
- package/dist/core/profile.js +39 -0
- package/dist/core/push.d.ts +47 -0
- package/dist/core/push.js +306 -0
- package/dist/core/pushrules.d.ts +1 -0
- package/dist/core/pushrules.js +228 -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 +1729 -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 +208 -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 +278 -0
- package/dist/federation/index.d.ts +153 -0
- package/dist/federation/index.js +1058 -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 +223 -0
- package/dist/homeserver.js +301 -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 +202 -0
- package/dist/http/input.js +24 -0
- package/dist/http/router.d.ts +60 -0
- package/dist/http/router.js +76 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +14 -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/package.json +67 -4
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { isObject } from "./json.js";
|
|
2
|
+
import { errors } from "./errors.js";
|
|
3
|
+
//#region src/core/keys.ts
|
|
4
|
+
/** End-to-end encryption key storage (device keys, one-time keys, cross-signing). */
|
|
5
|
+
var KeyService = class {
|
|
6
|
+
hs;
|
|
7
|
+
constructor(hs) {
|
|
8
|
+
this.hs = hs;
|
|
9
|
+
}
|
|
10
|
+
/** A local user's devices or device keys changed: update local clients and remote servers. */
|
|
11
|
+
async deviceChanged(userId, deletedDeviceId) {
|
|
12
|
+
await this.markDeviceListChanged(userId);
|
|
13
|
+
await this.hs.federation.onDeviceListChange(userId, deletedDeviceId).catch((err) => this.hs.logger.warn("Device list EDU failed", err));
|
|
14
|
+
}
|
|
15
|
+
async markDeviceListChanged(userId, notify = true) {
|
|
16
|
+
await this.hs.streams.write((stream) => this.hs.db.upsert("device_list_changes", {
|
|
17
|
+
user_id: userId,
|
|
18
|
+
stream
|
|
19
|
+
}));
|
|
20
|
+
if (notify) this.hs.notifier.notify(await this.hs.rooms.usersSharingRooms(userId));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A partial-state room got its full state (MSC3902): device list updates received meanwhile for
|
|
24
|
+
* users we could not place count as changes now, if those users turned out to be members.
|
|
25
|
+
*/
|
|
26
|
+
async onPartialStateComplete(roomId, pendingUsers) {
|
|
27
|
+
const members = new Set(await this.hs.rooms.joinedMembers(roomId));
|
|
28
|
+
for (const userId of pendingUsers) if (members.has(userId)) await this.markDeviceListChanged(userId, false);
|
|
29
|
+
this.hs.notifier.notify([...members].filter((u) => this.hs.isLocalUser(u)));
|
|
30
|
+
}
|
|
31
|
+
async upload(userId, deviceId, body) {
|
|
32
|
+
if (body.device_keys !== void 0) {
|
|
33
|
+
const dk = body.device_keys;
|
|
34
|
+
if (!isObject(dk) || dk.user_id !== userId || dk.device_id !== deviceId) throw errors.invalidParam("device_keys do not match this device");
|
|
35
|
+
if (!Array.isArray(dk.algorithms) || !isObject(dk.keys) || !isObject(dk.signatures)) throw errors.badJson("device_keys must contain algorithms, keys and signatures");
|
|
36
|
+
const existing = await this.hs.db.findOne("device_keys", [{
|
|
37
|
+
field: "user_id",
|
|
38
|
+
value: userId
|
|
39
|
+
}, {
|
|
40
|
+
field: "device_id",
|
|
41
|
+
value: deviceId
|
|
42
|
+
}]);
|
|
43
|
+
if (!existing || JSON.stringify(existing.keys) !== JSON.stringify(dk)) {
|
|
44
|
+
await this.hs.streams.write((stream) => this.hs.db.upsert("device_keys", {
|
|
45
|
+
user_id: userId,
|
|
46
|
+
device_id: deviceId,
|
|
47
|
+
keys: dk,
|
|
48
|
+
stream
|
|
49
|
+
}));
|
|
50
|
+
await this.deviceChanged(userId);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const device = [{
|
|
54
|
+
field: "user_id",
|
|
55
|
+
value: userId
|
|
56
|
+
}, {
|
|
57
|
+
field: "device_id",
|
|
58
|
+
value: deviceId
|
|
59
|
+
}];
|
|
60
|
+
if (isObject(body.one_time_keys)) for (const [fullId, key] of Object.entries(body.one_time_keys)) {
|
|
61
|
+
const [algorithm] = fullId.split(":");
|
|
62
|
+
const existing = await this.hs.db.findOne("one_time_keys", [...device, {
|
|
63
|
+
field: "key_id",
|
|
64
|
+
value: fullId
|
|
65
|
+
}]);
|
|
66
|
+
if (existing) {
|
|
67
|
+
if (JSON.stringify(existing.key) !== JSON.stringify(key)) throw errors.invalidParam(`One time key ${fullId} already exists`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
await this.hs.streams.write((seq) => this.hs.db.create("one_time_keys", {
|
|
71
|
+
user_id: userId,
|
|
72
|
+
device_id: deviceId,
|
|
73
|
+
key_id: fullId,
|
|
74
|
+
algorithm,
|
|
75
|
+
key,
|
|
76
|
+
fallback: false,
|
|
77
|
+
used: false,
|
|
78
|
+
seq
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
const fallbackKeys = body.fallback_keys ?? body["org.matrix.msc2732.fallback_keys"];
|
|
82
|
+
if (isObject(fallbackKeys)) for (const [fullId, key] of Object.entries(fallbackKeys)) {
|
|
83
|
+
const [algorithm] = fullId.split(":");
|
|
84
|
+
const existing = await this.hs.db.findOne("fallback_keys", [...device, {
|
|
85
|
+
field: "algorithm",
|
|
86
|
+
value: algorithm
|
|
87
|
+
}]);
|
|
88
|
+
if (existing && existing.key_id === fullId && JSON.stringify(existing.key) === JSON.stringify(key)) continue;
|
|
89
|
+
await this.hs.db.upsert("fallback_keys", {
|
|
90
|
+
user_id: userId,
|
|
91
|
+
device_id: deviceId,
|
|
92
|
+
algorithm,
|
|
93
|
+
key_id: fullId,
|
|
94
|
+
key,
|
|
95
|
+
used: false
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return this.oneTimeKeyCounts(userId, deviceId);
|
|
99
|
+
}
|
|
100
|
+
async oneTimeKeyCounts(userId, deviceId) {
|
|
101
|
+
const rows = await this.hs.db.findMany("one_time_keys", { where: [
|
|
102
|
+
{
|
|
103
|
+
field: "user_id",
|
|
104
|
+
value: userId
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
field: "device_id",
|
|
108
|
+
value: deviceId
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
field: "fallback",
|
|
112
|
+
value: false
|
|
113
|
+
}
|
|
114
|
+
] });
|
|
115
|
+
const counts = { signed_curve25519: 0 };
|
|
116
|
+
for (const r of rows) counts[r.algorithm] = (counts[r.algorithm] ?? 0) + 1;
|
|
117
|
+
return counts;
|
|
118
|
+
}
|
|
119
|
+
async unusedFallbackKeyTypes(userId, deviceId) {
|
|
120
|
+
const rows = await this.hs.db.findMany("fallback_keys", { where: [
|
|
121
|
+
{
|
|
122
|
+
field: "user_id",
|
|
123
|
+
value: userId
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
field: "device_id",
|
|
127
|
+
value: deviceId
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
field: "used",
|
|
131
|
+
value: false
|
|
132
|
+
}
|
|
133
|
+
] });
|
|
134
|
+
return [...new Set(rows.map((r) => r.algorithm))];
|
|
135
|
+
}
|
|
136
|
+
async query(requester, deviceKeys, timeoutMs = 1e4) {
|
|
137
|
+
const result = {
|
|
138
|
+
device_keys: {},
|
|
139
|
+
failures: {},
|
|
140
|
+
master_keys: {},
|
|
141
|
+
self_signing_keys: {},
|
|
142
|
+
user_signing_keys: {}
|
|
143
|
+
};
|
|
144
|
+
const remote = {};
|
|
145
|
+
for (const [userId, devices] of Object.entries(deviceKeys ?? {})) {
|
|
146
|
+
if (!this.hs.isLocalUser(userId)) {
|
|
147
|
+
remote[userId] = devices;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const rows = await this.hs.db.findMany("device_keys", { where: [{
|
|
151
|
+
field: "user_id",
|
|
152
|
+
value: userId
|
|
153
|
+
}] });
|
|
154
|
+
const deviceRows = await this.hs.db.findMany("devices", { where: [{
|
|
155
|
+
field: "user_id",
|
|
156
|
+
value: userId
|
|
157
|
+
}] });
|
|
158
|
+
const names = new Map(deviceRows.map((d) => [d.device_id, d.display_name]));
|
|
159
|
+
const wanted = Array.isArray(devices) && devices.length ? new Set(devices) : null;
|
|
160
|
+
const out = {};
|
|
161
|
+
for (const row of rows) {
|
|
162
|
+
if (wanted && !wanted.has(row.device_id)) continue;
|
|
163
|
+
if (!names.has(row.device_id)) continue;
|
|
164
|
+
const keys = await this.withSignatures(userId, `${row.device_id}`, row.keys);
|
|
165
|
+
const name = names.get(row.device_id);
|
|
166
|
+
keys.unsigned = {
|
|
167
|
+
...keys.unsigned,
|
|
168
|
+
...name ? { device_display_name: name } : {}
|
|
169
|
+
};
|
|
170
|
+
out[row.device_id] = keys;
|
|
171
|
+
}
|
|
172
|
+
result.device_keys[userId] = out;
|
|
173
|
+
const csRows = await this.hs.db.findMany("cross_signing_keys", { where: [{
|
|
174
|
+
field: "user_id",
|
|
175
|
+
value: userId
|
|
176
|
+
}] });
|
|
177
|
+
for (const cs of csRows) {
|
|
178
|
+
if (cs.key_type === "user_signing" && userId !== requester) continue;
|
|
179
|
+
const pub = Object.values(cs.key.keys ?? {})[0];
|
|
180
|
+
result[`${cs.key_type}_keys`][userId] = await this.withSignatures(userId, pub, cs.key);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (Object.keys(remote).length && requester) {
|
|
184
|
+
const fetch = {};
|
|
185
|
+
const tracked = /* @__PURE__ */ new Set();
|
|
186
|
+
for (const [userId, devices] of Object.entries(remote)) {
|
|
187
|
+
if (!await this.isTracked(userId)) {
|
|
188
|
+
await this.dropRemoteCache(userId);
|
|
189
|
+
fetch[userId] = devices;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
tracked.add(userId);
|
|
193
|
+
const list = await this.hs.db.findOne("remote_device_lists", [{
|
|
194
|
+
field: "user_id",
|
|
195
|
+
value: userId
|
|
196
|
+
}]);
|
|
197
|
+
if (!list || list.stale) {
|
|
198
|
+
fetch[userId] = [];
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
await this.fromRemoteCache(userId, devices, result);
|
|
202
|
+
}
|
|
203
|
+
if (Object.keys(fetch).length) {
|
|
204
|
+
const { results, failures } = await this.hs.federation.queryKeys(fetch, timeoutMs);
|
|
205
|
+
Object.assign(result.failures, failures);
|
|
206
|
+
const fetched = {
|
|
207
|
+
device_keys: {},
|
|
208
|
+
master_keys: {},
|
|
209
|
+
self_signing_keys: {}
|
|
210
|
+
};
|
|
211
|
+
for (const res of results) for (const kind of [
|
|
212
|
+
"device_keys",
|
|
213
|
+
"master_keys",
|
|
214
|
+
"self_signing_keys"
|
|
215
|
+
]) for (const [userId, value] of Object.entries(isObject(res[kind]) ? res[kind] : {})) if (fetch[userId] !== void 0) fetched[kind][userId] = value;
|
|
216
|
+
for (const userId of Object.keys(fetch)) {
|
|
217
|
+
if (tracked.has(userId) && isObject(fetched.device_keys[userId])) {
|
|
218
|
+
await this.storeRemoteCache(userId, fetched.device_keys[userId], fetched.master_keys[userId], fetched.self_signing_keys[userId]);
|
|
219
|
+
await this.fromRemoteCache(userId, remote[userId], result);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
for (const kind of [
|
|
223
|
+
"device_keys",
|
|
224
|
+
"master_keys",
|
|
225
|
+
"self_signing_keys"
|
|
226
|
+
]) if (fetched[kind][userId] !== void 0) result[kind][userId] = fetched[kind][userId];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
/** Whether a remote user shares a room with a local user (by known membership). */
|
|
233
|
+
async isTracked(userId) {
|
|
234
|
+
const rows = await this.hs.db.findMany("memberships", { where: [{
|
|
235
|
+
field: "user_id",
|
|
236
|
+
value: userId
|
|
237
|
+
}, {
|
|
238
|
+
field: "membership",
|
|
239
|
+
value: "join"
|
|
240
|
+
}] });
|
|
241
|
+
for (const row of rows) if ((await this.hs.rooms.joinedMembers(row.room_id)).some((u) => this.hs.isLocalUser(u))) return true;
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
async dropRemoteCache(userId) {
|
|
245
|
+
await this.hs.db.delete("remote_devices", [{
|
|
246
|
+
field: "user_id",
|
|
247
|
+
value: userId
|
|
248
|
+
}]);
|
|
249
|
+
await this.hs.db.delete("remote_device_lists", [{
|
|
250
|
+
field: "user_id",
|
|
251
|
+
value: userId
|
|
252
|
+
}]);
|
|
253
|
+
}
|
|
254
|
+
/** A tracked user's device list changed: refetch it on the next query. */
|
|
255
|
+
async markRemoteStale(userId) {
|
|
256
|
+
if (await this.isTracked(userId)) await this.hs.db.upsert("remote_device_lists", {
|
|
257
|
+
user_id: userId,
|
|
258
|
+
stream_id: 0,
|
|
259
|
+
stale: true
|
|
260
|
+
});
|
|
261
|
+
else await this.dropRemoteCache(userId);
|
|
262
|
+
}
|
|
263
|
+
async storeRemoteCache(userId, devices, master, selfSigning) {
|
|
264
|
+
await this.hs.db.delete("remote_devices", [{
|
|
265
|
+
field: "user_id",
|
|
266
|
+
value: userId
|
|
267
|
+
}]);
|
|
268
|
+
for (const [deviceId, keys] of Object.entries(devices)) {
|
|
269
|
+
if (!isObject(keys)) continue;
|
|
270
|
+
const name = keys.unsigned?.device_display_name;
|
|
271
|
+
await this.hs.db.upsert("remote_devices", {
|
|
272
|
+
user_id: userId,
|
|
273
|
+
device_id: deviceId,
|
|
274
|
+
keys,
|
|
275
|
+
display_name: typeof name === "string" ? name : null,
|
|
276
|
+
stream_id: 0
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
for (const [type, key] of [["master", master], ["self_signing", selfSigning]]) if (isObject(key)) await this.hs.streams.write((stream) => this.hs.db.upsert("cross_signing_keys", {
|
|
280
|
+
user_id: userId,
|
|
281
|
+
key_type: type,
|
|
282
|
+
key,
|
|
283
|
+
stream
|
|
284
|
+
}));
|
|
285
|
+
await this.hs.db.upsert("remote_device_lists", {
|
|
286
|
+
user_id: userId,
|
|
287
|
+
stream_id: 0,
|
|
288
|
+
stale: false
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
async fromRemoteCache(userId, devices, result) {
|
|
292
|
+
const wanted = Array.isArray(devices) && devices.length ? new Set(devices) : null;
|
|
293
|
+
const out = {};
|
|
294
|
+
for (const row of await this.hs.db.findMany("remote_devices", { where: [{
|
|
295
|
+
field: "user_id",
|
|
296
|
+
value: userId
|
|
297
|
+
}] })) if (!wanted || wanted.has(row.device_id)) out[row.device_id] = row.keys;
|
|
298
|
+
result.device_keys[userId] = out;
|
|
299
|
+
for (const cs of await this.hs.db.findMany("cross_signing_keys", { where: [{
|
|
300
|
+
field: "user_id",
|
|
301
|
+
value: userId
|
|
302
|
+
}] })) if (cs.key_type === "master" || cs.key_type === "self_signing") result[`${cs.key_type}_keys`][userId] = cs.key;
|
|
303
|
+
}
|
|
304
|
+
async withSignatures(userId, keyId, key) {
|
|
305
|
+
const sigs = await this.hs.db.findMany("key_signatures", { where: [{
|
|
306
|
+
field: "user_id",
|
|
307
|
+
value: userId
|
|
308
|
+
}, {
|
|
309
|
+
field: "key_id",
|
|
310
|
+
value: keyId
|
|
311
|
+
}] });
|
|
312
|
+
if (sigs.length === 0) return structuredClone(key);
|
|
313
|
+
const out = structuredClone(key);
|
|
314
|
+
out.signatures ??= {};
|
|
315
|
+
for (const s of sigs) out.signatures[s.signer] = {
|
|
316
|
+
...out.signatures[s.signer],
|
|
317
|
+
[s.signer_key_id]: s.signature
|
|
318
|
+
};
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
async claim(request) {
|
|
322
|
+
const result = {};
|
|
323
|
+
for (const [userId, devices] of Object.entries(request ?? {})) for (const [deviceId, algorithm] of Object.entries(devices ?? {})) {
|
|
324
|
+
const base = [
|
|
325
|
+
{
|
|
326
|
+
field: "user_id",
|
|
327
|
+
value: userId
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
field: "device_id",
|
|
331
|
+
value: deviceId
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
field: "algorithm",
|
|
335
|
+
value: algorithm
|
|
336
|
+
}
|
|
337
|
+
];
|
|
338
|
+
let key = (await this.hs.db.findMany("one_time_keys", {
|
|
339
|
+
where: [...base, {
|
|
340
|
+
field: "fallback",
|
|
341
|
+
value: false
|
|
342
|
+
}],
|
|
343
|
+
sortBy: {
|
|
344
|
+
field: "seq",
|
|
345
|
+
direction: "asc"
|
|
346
|
+
},
|
|
347
|
+
limit: 1
|
|
348
|
+
}))[0] ?? null;
|
|
349
|
+
if (key) await this.hs.db.delete("one_time_keys", [...base, {
|
|
350
|
+
field: "key_id",
|
|
351
|
+
value: key.key_id
|
|
352
|
+
}]);
|
|
353
|
+
else {
|
|
354
|
+
key = await this.hs.db.findOne("fallback_keys", base);
|
|
355
|
+
if (key && !key.used) await this.hs.db.update("fallback_keys", base, { used: true });
|
|
356
|
+
}
|
|
357
|
+
if (!key) continue;
|
|
358
|
+
result[userId] ??= {};
|
|
359
|
+
result[userId][deviceId] = { [key.key_id]: key.key };
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
async uploadCrossSigning(userId, body) {
|
|
364
|
+
for (const type of [
|
|
365
|
+
"master",
|
|
366
|
+
"self_signing",
|
|
367
|
+
"user_signing"
|
|
368
|
+
]) {
|
|
369
|
+
const key = body[`${type}_key`];
|
|
370
|
+
if (key === void 0) continue;
|
|
371
|
+
if (!isObject(key) || key.user_id !== userId || !isObject(key.keys)) throw errors.invalidParam(`Invalid ${type}_key`);
|
|
372
|
+
await this.hs.streams.write((stream) => this.hs.db.upsert("cross_signing_keys", {
|
|
373
|
+
user_id: userId,
|
|
374
|
+
key_type: type,
|
|
375
|
+
key,
|
|
376
|
+
stream
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
await this.markDeviceListChanged(userId);
|
|
380
|
+
await this.hs.federation.onSigningKeyUpdate(userId).catch(() => {});
|
|
381
|
+
}
|
|
382
|
+
async uploadSignatures(signer, body) {
|
|
383
|
+
const failures = {};
|
|
384
|
+
const changed = /* @__PURE__ */ new Set();
|
|
385
|
+
for (const [userId, keys] of Object.entries(body ?? {})) {
|
|
386
|
+
if (!isObject(keys)) continue;
|
|
387
|
+
for (const [keyId, signed] of Object.entries(keys)) {
|
|
388
|
+
const sigs = signed?.signatures?.[signer];
|
|
389
|
+
if (!isObject(sigs)) {
|
|
390
|
+
failures[userId] = {
|
|
391
|
+
...failures[userId],
|
|
392
|
+
[keyId]: {
|
|
393
|
+
errcode: "M_INVALID_SIGNATURE",
|
|
394
|
+
error: "No signature from signer"
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
for (const [signerKeyId, signature] of Object.entries(sigs)) await this.hs.db.upsert("key_signatures", {
|
|
400
|
+
user_id: userId,
|
|
401
|
+
key_id: keyId,
|
|
402
|
+
signer,
|
|
403
|
+
signer_key_id: signerKeyId,
|
|
404
|
+
signature: String(signature)
|
|
405
|
+
});
|
|
406
|
+
changed.add(userId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
for (const u of changed) if (this.hs.isLocalUser(u)) await this.deviceChanged(u);
|
|
410
|
+
else await this.markDeviceListChanged(u);
|
|
411
|
+
if (changed.has(signer)) await this.hs.federation.onSigningKeyUpdate(signer).catch(() => {});
|
|
412
|
+
return { failures };
|
|
413
|
+
}
|
|
414
|
+
/** Users (sharing a room with `userId`) whose device lists changed after `since`. */
|
|
415
|
+
async changesSince(userId, since, upTo) {
|
|
416
|
+
const sharing = await this.hs.rooms.usersSharingRooms(userId);
|
|
417
|
+
const rows = await this.hs.db.findMany("device_list_changes", { where: [{
|
|
418
|
+
field: "stream",
|
|
419
|
+
operator: "gt",
|
|
420
|
+
value: since
|
|
421
|
+
}] });
|
|
422
|
+
const changed = /* @__PURE__ */ new Set();
|
|
423
|
+
for (const r of rows) if (r.stream <= upTo && sharing.has(r.user_id)) changed.add(r.user_id);
|
|
424
|
+
const left = /* @__PURE__ */ new Set();
|
|
425
|
+
const memberRows = await this.hs.db.findMany("memberships", { where: [{
|
|
426
|
+
field: "stream",
|
|
427
|
+
operator: "gt",
|
|
428
|
+
value: since
|
|
429
|
+
}] });
|
|
430
|
+
const myRooms = /* @__PURE__ */ new Set([...await this.hs.rooms.roomsForUser(userId), ...memberRows.filter((m) => m.user_id === userId).map((m) => m.room_id)]);
|
|
431
|
+
for (const m of memberRows) {
|
|
432
|
+
if (m.stream > upTo || !myRooms.has(m.room_id)) continue;
|
|
433
|
+
if (m.user_id === userId) {
|
|
434
|
+
const members = await this.hs.rooms.joinedMembers(m.room_id);
|
|
435
|
+
if (m.membership === "join") {
|
|
436
|
+
for (const u of members) if (u !== userId) changed.add(u);
|
|
437
|
+
} else for (const u of members) if (!sharing.has(u)) left.add(u);
|
|
438
|
+
} else if (m.membership === "join") changed.add(m.user_id);
|
|
439
|
+
else if (!sharing.has(m.user_id)) left.add(m.user_id);
|
|
440
|
+
}
|
|
441
|
+
for (const u of left) changed.delete(u);
|
|
442
|
+
return {
|
|
443
|
+
changed: [...changed],
|
|
444
|
+
left: [...left]
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
//#endregion
|
|
449
|
+
export { KeyService };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ClusterAdapter } from "./cluster.js";
|
|
2
|
+
//#region src/core/notifier.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Wakes long-polling `/sync` requests when something relevant happens. Notifications
|
|
5
|
+
* are broadcast through the cluster adapter so waiters on other instances wake too.
|
|
6
|
+
*/
|
|
7
|
+
export declare class Notifier {
|
|
8
|
+
private waiters;
|
|
9
|
+
private cluster;
|
|
10
|
+
private instanceId;
|
|
11
|
+
constructor(cluster: ClusterAdapter, instanceId: string);
|
|
12
|
+
notify(userIds: Iterable<string>): void;
|
|
13
|
+
private wakeLocal;
|
|
14
|
+
notifyAll(): void;
|
|
15
|
+
wait(userId: string, timeoutMs: number, signal?: AbortSignal): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/core/notifier.ts
|
|
2
|
+
/**
|
|
3
|
+
* Wakes long-polling `/sync` requests when something relevant happens. Notifications
|
|
4
|
+
* are broadcast through the cluster adapter so waiters on other instances wake too.
|
|
5
|
+
*/
|
|
6
|
+
var Notifier = class {
|
|
7
|
+
waiters = /* @__PURE__ */ new Map();
|
|
8
|
+
cluster;
|
|
9
|
+
instanceId;
|
|
10
|
+
constructor(cluster, instanceId) {
|
|
11
|
+
this.cluster = cluster;
|
|
12
|
+
this.instanceId = instanceId;
|
|
13
|
+
cluster.subscribe("notify", (msg) => {
|
|
14
|
+
if (msg?.from !== this.instanceId && Array.isArray(msg?.users)) this.wakeLocal(msg.users);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
notify(userIds) {
|
|
18
|
+
const users = [...userIds];
|
|
19
|
+
if (!users.length) return;
|
|
20
|
+
this.wakeLocal(users);
|
|
21
|
+
this.cluster.publish("notify", {
|
|
22
|
+
from: this.instanceId,
|
|
23
|
+
users
|
|
24
|
+
}).catch(() => {});
|
|
25
|
+
}
|
|
26
|
+
wakeLocal(userIds) {
|
|
27
|
+
for (const u of userIds) {
|
|
28
|
+
const set = this.waiters.get(u);
|
|
29
|
+
if (!set) continue;
|
|
30
|
+
this.waiters.delete(u);
|
|
31
|
+
for (const wake of set) wake();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
notifyAll() {
|
|
35
|
+
this.wakeLocal([...this.waiters.keys()]);
|
|
36
|
+
}
|
|
37
|
+
wait(userId, timeoutMs, signal) {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
let set = this.waiters.get(userId);
|
|
40
|
+
if (!set) this.waiters.set(userId, set = /* @__PURE__ */ new Set());
|
|
41
|
+
const done = () => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
set.delete(done);
|
|
44
|
+
signal?.removeEventListener("abort", done);
|
|
45
|
+
resolve();
|
|
46
|
+
};
|
|
47
|
+
const timer = setTimeout(done, timeoutMs);
|
|
48
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
49
|
+
set.add(done);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { Notifier };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Session } from "./auth.js";
|
|
2
|
+
import { Homeserver } from "../homeserver.js";
|
|
3
|
+
//#region src/core/oauth.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Delegate authentication to an external OAuth 2.0 / OpenID Connect provider
|
|
6
|
+
* (Matrix "OAuth 2.0 API", e.g. matrix-authentication-service, Keycloak, Authentik).
|
|
7
|
+
*/
|
|
8
|
+
export interface OAuthDelegationOptions {
|
|
9
|
+
/** Issuer URL of the authorization server. */
|
|
10
|
+
issuer: string;
|
|
11
|
+
/** Client credentials the homeserver uses for token introspection. */
|
|
12
|
+
clientId: string;
|
|
13
|
+
clientSecret: string;
|
|
14
|
+
/** Override the introspection endpoint instead of discovering it. */
|
|
15
|
+
introspectionEndpoint?: string;
|
|
16
|
+
/** Where users manage their account (advertised to clients). */
|
|
17
|
+
accountManagementUrl?: string;
|
|
18
|
+
/** Map introspection claims to a Matrix localpart. Default: `username`, then `sub`. */
|
|
19
|
+
localpartFromClaims?: (claims: Record<string, any>) => string | null;
|
|
20
|
+
/** Scope granting server admin rights. Default: "urn:synapse:admin:*". */
|
|
21
|
+
adminScope?: string;
|
|
22
|
+
/** How long successful introspections are cached, in ms. Default: 60 000. */
|
|
23
|
+
cacheTtlMs?: number;
|
|
24
|
+
}
|
|
25
|
+
export declare class OAuthDelegation {
|
|
26
|
+
private hs;
|
|
27
|
+
readonly options: OAuthDelegationOptions;
|
|
28
|
+
private metadata;
|
|
29
|
+
private metadataFetched;
|
|
30
|
+
private cache;
|
|
31
|
+
constructor(hs: Homeserver, options: OAuthDelegationOptions);
|
|
32
|
+
/** Authorization server metadata (RFC 8414 / OIDC discovery), cached for an hour. */
|
|
33
|
+
serverMetadata(): Promise<Record<string, any>>;
|
|
34
|
+
/** Validate an access token by introspection, provisioning the user and device on first use. */
|
|
35
|
+
authenticate(token: string): Promise<Session>;
|
|
36
|
+
forget(token: string): void;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { errors } from "./errors.js";
|
|
2
|
+
import { LOCALPART_RE } from "./ids.js";
|
|
3
|
+
//#region src/core/oauth.ts
|
|
4
|
+
const API_SCOPES = ["urn:matrix:client:api:*", "urn:matrix:org.matrix.msc2967.client:api:*"];
|
|
5
|
+
const DEVICE_SCOPE_PREFIXES = ["urn:matrix:client:device:", "urn:matrix:org.matrix.msc2967.client:device:"];
|
|
6
|
+
var OAuthDelegation = class {
|
|
7
|
+
hs;
|
|
8
|
+
options;
|
|
9
|
+
metadata = null;
|
|
10
|
+
metadataFetched = 0;
|
|
11
|
+
cache = /* @__PURE__ */ new Map();
|
|
12
|
+
constructor(hs, options) {
|
|
13
|
+
this.hs = hs;
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
/** Authorization server metadata (RFC 8414 / OIDC discovery), cached for an hour. */
|
|
17
|
+
async serverMetadata() {
|
|
18
|
+
if (this.metadata && Date.now() - this.metadataFetched < 36e5) return this.metadata;
|
|
19
|
+
const base = this.options.issuer.replace(/\/$/, "");
|
|
20
|
+
let res = await this.hs.fetch(`${base}/.well-known/openid-configuration`, { signal: AbortSignal.timeout(1e4) });
|
|
21
|
+
if (!res.ok) res = await this.hs.fetch(`${base}/.well-known/oauth-authorization-server`, { signal: AbortSignal.timeout(1e4) });
|
|
22
|
+
if (!res.ok) throw new Error(`OAuth discovery failed with ${res.status}`);
|
|
23
|
+
const meta = await res.json();
|
|
24
|
+
if (this.options.accountManagementUrl) meta.account_management_uri = this.options.accountManagementUrl;
|
|
25
|
+
this.metadata = meta;
|
|
26
|
+
this.metadataFetched = Date.now();
|
|
27
|
+
return meta;
|
|
28
|
+
}
|
|
29
|
+
/** Validate an access token by introspection, provisioning the user and device on first use. */
|
|
30
|
+
async authenticate(token) {
|
|
31
|
+
const cached = this.cache.get(token);
|
|
32
|
+
if (cached && cached.expires > Date.now()) return cached.session;
|
|
33
|
+
const endpoint = this.options.introspectionEndpoint ?? (await this.serverMetadata()).introspection_endpoint;
|
|
34
|
+
if (!endpoint) throw new Error("Authorization server has no introspection endpoint");
|
|
35
|
+
const res = await this.hs.fetch(endpoint, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: {
|
|
38
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
39
|
+
authorization: `Basic ${Buffer.from(`${encodeURIComponent(this.options.clientId)}:${encodeURIComponent(this.options.clientSecret)}`).toString("base64")}`
|
|
40
|
+
},
|
|
41
|
+
body: new URLSearchParams({
|
|
42
|
+
token,
|
|
43
|
+
token_type_hint: "access_token"
|
|
44
|
+
}),
|
|
45
|
+
signal: AbortSignal.timeout(1e4)
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok) throw errors.unknownToken("Token introspection failed");
|
|
48
|
+
const claims = await res.json();
|
|
49
|
+
if (!claims.active) throw errors.unknownToken();
|
|
50
|
+
const scopes = String(claims.scope ?? "").split(" ");
|
|
51
|
+
if (!scopes.some((s) => API_SCOPES.includes(s))) throw errors.unknownToken("Token lacks the Matrix API scope");
|
|
52
|
+
let deviceId = null;
|
|
53
|
+
for (const s of scopes) for (const p of DEVICE_SCOPE_PREFIXES) if (s.startsWith(p)) deviceId = s.slice(p.length);
|
|
54
|
+
if (!deviceId) throw errors.unknownToken("Token lacks a device scope");
|
|
55
|
+
const localpart = (this.options.localpartFromClaims?.(claims) ?? claims.username ?? claims.sub ?? "").toLowerCase();
|
|
56
|
+
if (!LOCALPART_RE.test(localpart)) throw errors.unknownToken("Token subject cannot be mapped to a Matrix user");
|
|
57
|
+
const userId = `@${localpart}:${this.hs.serverName}`;
|
|
58
|
+
let user = await this.hs.db.findOne("users", [{
|
|
59
|
+
field: "user_id",
|
|
60
|
+
value: userId
|
|
61
|
+
}]);
|
|
62
|
+
if (!user) {
|
|
63
|
+
await this.hs.admin.createUser(localpart, { displayname: claims.name ?? localpart });
|
|
64
|
+
user = await this.hs.db.findOne("users", [{
|
|
65
|
+
field: "user_id",
|
|
66
|
+
value: userId
|
|
67
|
+
}]);
|
|
68
|
+
}
|
|
69
|
+
if (user.deactivated) throw errors.userDeactivated();
|
|
70
|
+
const deviceWhere = [{
|
|
71
|
+
field: "user_id",
|
|
72
|
+
value: userId
|
|
73
|
+
}, {
|
|
74
|
+
field: "device_id",
|
|
75
|
+
value: deviceId
|
|
76
|
+
}];
|
|
77
|
+
if (!await this.hs.db.findOne("devices", deviceWhere)) {
|
|
78
|
+
await this.hs.db.create("devices", {
|
|
79
|
+
user_id: userId,
|
|
80
|
+
device_id: deviceId,
|
|
81
|
+
display_name: null,
|
|
82
|
+
last_seen_ts: Date.now(),
|
|
83
|
+
last_seen_ip: null
|
|
84
|
+
});
|
|
85
|
+
await this.hs.keys.markDeviceListChanged(userId);
|
|
86
|
+
}
|
|
87
|
+
const session = {
|
|
88
|
+
userId,
|
|
89
|
+
deviceId,
|
|
90
|
+
accessToken: token,
|
|
91
|
+
isGuest: false,
|
|
92
|
+
isAdmin: !!user.is_admin || scopes.includes(this.options.adminScope ?? "urn:synapse:admin:*")
|
|
93
|
+
};
|
|
94
|
+
const ttl = this.options.cacheTtlMs ?? 6e4;
|
|
95
|
+
const exp = typeof claims.exp === "number" ? claims.exp * 1e3 : Infinity;
|
|
96
|
+
if (this.cache.size > 5e4) this.cache.clear();
|
|
97
|
+
this.cache.set(token, {
|
|
98
|
+
session,
|
|
99
|
+
expires: Math.min(Date.now() + ttl, exp)
|
|
100
|
+
});
|
|
101
|
+
return session;
|
|
102
|
+
}
|
|
103
|
+
forget(token) {
|
|
104
|
+
this.cache.delete(token);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
//#endregion
|
|
108
|
+
export { OAuthDelegation };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Homeserver } from "../homeserver.js";
|
|
2
|
+
//#region src/core/partialstate.d.ts
|
|
3
|
+
export declare class PartialStateService {
|
|
4
|
+
private readonly hs;
|
|
5
|
+
private readonly partial;
|
|
6
|
+
private readonly completed;
|
|
7
|
+
private readonly running;
|
|
8
|
+
private readonly completedStreams;
|
|
9
|
+
private stopped;
|
|
10
|
+
constructor(hs: Homeserver);
|
|
11
|
+
isPartial(roomId: string): Promise<boolean>;
|
|
12
|
+
/** Servers the joining server told us were in the room (used as destinations and resync sources). */
|
|
13
|
+
servers(roomId: string): Promise<string[]>;
|
|
14
|
+
/** Stream position at which the room got its full state (null if it never was partial). */
|
|
15
|
+
completedAt(roomId: string): Promise<number | null>;
|
|
16
|
+
/**
|
|
17
|
+
* A device list update for a user we do not share a (known) room with: if they may be a member
|
|
18
|
+
* of a partial-state room hosted by the sender, remember them for when the state is complete.
|
|
19
|
+
*/
|
|
20
|
+
notePendingDeviceUpdate(userId: string, origin: string): Promise<void>;
|
|
21
|
+
markPartial(roomId: string, joinEventId: string, origin: string, servers: string[]): Promise<void>;
|
|
22
|
+
/** Resolve once the room has its full state (or the signal aborts / the server closes). */
|
|
23
|
+
waitForFullState(roomId: string, signal?: AbortSignal): Promise<void>;
|
|
24
|
+
/** Resume resyncs of rooms left partial (e.g. by a restart). */
|
|
25
|
+
resume(): Promise<void>;
|
|
26
|
+
stop(): void;
|
|
27
|
+
startResync(roomId: string): void;
|
|
28
|
+
private resync;
|
|
29
|
+
private tryResync;
|
|
30
|
+
private finish;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|