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.
Files changed (120) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +457 -2
  3. package/dist/adapters/auth.d.ts +50 -0
  4. package/dist/adapters/auth.js +18 -0
  5. package/dist/adapters/cluster.d.ts +55 -0
  6. package/dist/adapters/cluster.js +249 -0
  7. package/dist/adapters/media.d.ts +50 -0
  8. package/dist/adapters/media.js +63 -0
  9. package/dist/adapters/s3.d.ts +33 -0
  10. package/dist/adapters/s3.js +95 -0
  11. package/dist/api/account.js +560 -0
  12. package/dist/api/admin.js +251 -0
  13. package/dist/api/appservice.js +33 -0
  14. package/dist/api/backup.js +269 -0
  15. package/dist/api/federation.js +792 -0
  16. package/dist/api/filters.js +87 -0
  17. package/dist/api/index.js +47 -0
  18. package/dist/api/legacy.js +170 -0
  19. package/dist/api/media.js +255 -0
  20. package/dist/api/misc.js +86 -0
  21. package/dist/api/preview.js +200 -0
  22. package/dist/api/profile.js +165 -0
  23. package/dist/api/push.js +187 -0
  24. package/dist/api/relationships.js +264 -0
  25. package/dist/api/rooms.js +1364 -0
  26. package/dist/api/rtc.d.ts +24 -0
  27. package/dist/api/rtc.js +52 -0
  28. package/dist/api/search.js +155 -0
  29. package/dist/api/slidingsync.js +464 -0
  30. package/dist/api/spaces.js +172 -0
  31. package/dist/api/sync.js +600 -0
  32. package/dist/api/threadsubs.js +129 -0
  33. package/dist/api/threepid.js +122 -0
  34. package/dist/api/uia.js +126 -0
  35. package/dist/api/user.js +286 -0
  36. package/dist/core/admin.d.ts +85 -0
  37. package/dist/core/admin.js +307 -0
  38. package/dist/core/adminroom.d.ts +49 -0
  39. package/dist/core/adminroom.js +482 -0
  40. package/dist/core/appservices.d.ts +73 -0
  41. package/dist/core/appservices.js +368 -0
  42. package/dist/core/audit.d.ts +51 -0
  43. package/dist/core/audit.js +86 -0
  44. package/dist/core/auth.d.ts +48 -0
  45. package/dist/core/auth.js +237 -0
  46. package/dist/core/authrules.d.ts +4 -0
  47. package/dist/core/authrules.js +347 -0
  48. package/dist/core/cluster.d.ts +66 -0
  49. package/dist/core/cluster.js +191 -0
  50. package/dist/core/crypto.d.ts +10 -0
  51. package/dist/core/crypto.js +100 -0
  52. package/dist/core/delayed.d.ts +39 -0
  53. package/dist/core/delayed.js +159 -0
  54. package/dist/core/errors.d.ts +9 -0
  55. package/dist/core/errors.js +45 -0
  56. package/dist/core/events.d.ts +32 -0
  57. package/dist/core/events.js +185 -0
  58. package/dist/core/glob.js +66 -0
  59. package/dist/core/identity.d.ts +24 -0
  60. package/dist/core/identity.js +96 -0
  61. package/dist/core/ids.js +36 -0
  62. package/dist/core/invitefilter.js +35 -0
  63. package/dist/core/json.js +16 -0
  64. package/dist/core/keys.d.ts +36 -0
  65. package/dist/core/keys.js +449 -0
  66. package/dist/core/notifier.d.ts +17 -0
  67. package/dist/core/notifier.js +54 -0
  68. package/dist/core/oauth.d.ts +38 -0
  69. package/dist/core/oauth.js +108 -0
  70. package/dist/core/partialstate.d.ts +32 -0
  71. package/dist/core/partialstate.js +197 -0
  72. package/dist/core/phone.js +268 -0
  73. package/dist/core/policy.d.ts +29 -0
  74. package/dist/core/policy.js +96 -0
  75. package/dist/core/profile.js +39 -0
  76. package/dist/core/push.d.ts +47 -0
  77. package/dist/core/push.js +306 -0
  78. package/dist/core/pushrules.d.ts +1 -0
  79. package/dist/core/pushrules.js +228 -0
  80. package/dist/core/ratelimit.d.ts +23 -0
  81. package/dist/core/ratelimit.js +77 -0
  82. package/dist/core/rooms.d.ts +215 -0
  83. package/dist/core/rooms.js +1729 -0
  84. package/dist/core/settings.d.ts +11 -0
  85. package/dist/core/settings.js +29 -0
  86. package/dist/core/sso.d.ts +46 -0
  87. package/dist/core/sso.js +165 -0
  88. package/dist/core/stateres.d.ts +4 -0
  89. package/dist/core/stateres.js +208 -0
  90. package/dist/core/threepid.d.ts +80 -0
  91. package/dist/core/threepid.js +275 -0
  92. package/dist/core/typing.d.ts +16 -0
  93. package/dist/core/typing.js +60 -0
  94. package/dist/federation/client.d.ts +51 -0
  95. package/dist/federation/client.js +278 -0
  96. package/dist/federation/index.d.ts +153 -0
  97. package/dist/federation/index.js +1058 -0
  98. package/dist/federation/keyring.d.ts +26 -0
  99. package/dist/federation/keyring.js +122 -0
  100. package/dist/federation/sender.d.ts +26 -0
  101. package/dist/federation/sender.js +164 -0
  102. package/dist/homeserver.d.ts +223 -0
  103. package/dist/homeserver.js +301 -0
  104. package/dist/http/adapters.d.ts +40 -0
  105. package/dist/http/adapters.js +130 -0
  106. package/dist/http/handler.d.ts +8 -0
  107. package/dist/http/handler.js +202 -0
  108. package/dist/http/input.js +24 -0
  109. package/dist/http/router.d.ts +60 -0
  110. package/dist/http/router.js +76 -0
  111. package/dist/index.d.ts +25 -0
  112. package/dist/index.js +14 -0
  113. package/dist/storage/memory.d.ts +5 -0
  114. package/dist/storage/memory.js +122 -0
  115. package/dist/storage/schema.d.ts +4 -0
  116. package/dist/storage/schema.js +664 -0
  117. package/dist/storage/sql.d.ts +72 -0
  118. package/dist/storage/sql.js +274 -0
  119. package/dist/storage/types.d.ts +55 -0
  120. 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