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.
Files changed (141) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +479 -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 +594 -0
  12. package/dist/api/admin.js +267 -0
  13. package/dist/api/appservice.js +45 -0
  14. package/dist/api/backup.js +280 -0
  15. package/dist/api/federation.js +826 -0
  16. package/dist/api/filters.js +88 -0
  17. package/dist/api/index.js +47 -0
  18. package/dist/api/legacy.js +183 -0
  19. package/dist/api/media.js +271 -0
  20. package/dist/api/misc.js +95 -0
  21. package/dist/api/preview.js +201 -0
  22. package/dist/api/profile.js +176 -0
  23. package/dist/api/push.js +202 -0
  24. package/dist/api/relationships.js +270 -0
  25. package/dist/api/rooms.js +1487 -0
  26. package/dist/api/rtc.d.ts +23 -0
  27. package/dist/api/rtc.js +66 -0
  28. package/dist/api/search.js +161 -0
  29. package/dist/api/slidingsync.js +474 -0
  30. package/dist/api/spaces.js +177 -0
  31. package/dist/api/sync.js +609 -0
  32. package/dist/api/threadsubs.js +140 -0
  33. package/dist/api/threepid.js +155 -0
  34. package/dist/api/uia.js +127 -0
  35. package/dist/api/user.js +329 -0
  36. package/dist/core/admin.d.ts +84 -0
  37. package/dist/core/admin.js +308 -0
  38. package/dist/core/adminroom.d.ts +49 -0
  39. package/dist/core/adminroom.js +483 -0
  40. package/dist/core/appservices.d.ts +74 -0
  41. package/dist/core/appservices.js +369 -0
  42. package/dist/core/audit.d.ts +50 -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 +348 -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 +160 -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 +186 -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 +36 -0
  63. package/dist/core/json.js +16 -0
  64. package/dist/core/keys.d.ts +36 -0
  65. package/dist/core/keys.js +450 -0
  66. package/dist/core/m.d.ts +209 -0
  67. package/dist/core/m.js +182 -0
  68. package/dist/core/notifier.d.ts +17 -0
  69. package/dist/core/notifier.js +54 -0
  70. package/dist/core/oauth.d.ts +38 -0
  71. package/dist/core/oauth.js +108 -0
  72. package/dist/core/partialstate.d.ts +32 -0
  73. package/dist/core/partialstate.js +198 -0
  74. package/dist/core/phone.js +268 -0
  75. package/dist/core/policy.d.ts +29 -0
  76. package/dist/core/policy.js +97 -0
  77. package/dist/core/profile.js +39 -0
  78. package/dist/core/push.d.ts +46 -0
  79. package/dist/core/push.js +307 -0
  80. package/dist/core/pushrules.js +229 -0
  81. package/dist/core/ratelimit.d.ts +23 -0
  82. package/dist/core/ratelimit.js +77 -0
  83. package/dist/core/rooms.d.ts +215 -0
  84. package/dist/core/rooms.js +1731 -0
  85. package/dist/core/settings.d.ts +11 -0
  86. package/dist/core/settings.js +29 -0
  87. package/dist/core/sso.d.ts +46 -0
  88. package/dist/core/sso.js +165 -0
  89. package/dist/core/stateres.d.ts +4 -0
  90. package/dist/core/stateres.js +209 -0
  91. package/dist/core/threepid.d.ts +80 -0
  92. package/dist/core/threepid.js +275 -0
  93. package/dist/core/typing.d.ts +16 -0
  94. package/dist/core/typing.js +60 -0
  95. package/dist/federation/client.d.ts +51 -0
  96. package/dist/federation/client.js +279 -0
  97. package/dist/federation/index.d.ts +153 -0
  98. package/dist/federation/index.js +1059 -0
  99. package/dist/federation/keyring.d.ts +26 -0
  100. package/dist/federation/keyring.js +122 -0
  101. package/dist/federation/sender.d.ts +26 -0
  102. package/dist/federation/sender.js +164 -0
  103. package/dist/homeserver.d.ts +224 -0
  104. package/dist/homeserver.js +302 -0
  105. package/dist/http/adapters.d.ts +40 -0
  106. package/dist/http/adapters.js +130 -0
  107. package/dist/http/handler.d.ts +8 -0
  108. package/dist/http/handler.js +181 -0
  109. package/dist/http/input.js +24 -0
  110. package/dist/http/procedure.js +25 -0
  111. package/dist/http/router.d.ts +43 -0
  112. package/dist/http/router.js +56 -0
  113. package/dist/index.d.ts +26 -0
  114. package/dist/index.js +15 -0
  115. package/dist/storage/memory.d.ts +5 -0
  116. package/dist/storage/memory.js +122 -0
  117. package/dist/storage/schema.d.ts +4 -0
  118. package/dist/storage/schema.js +664 -0
  119. package/dist/storage/sql.d.ts +72 -0
  120. package/dist/storage/sql.js +274 -0
  121. package/dist/storage/types.d.ts +55 -0
  122. package/dist/vendor/zod/LICENSE +21 -0
  123. package/dist/vendor/zod/v4/classic/errors.js +54 -0
  124. package/dist/vendor/zod/v4/classic/parse.js +17 -0
  125. package/dist/vendor/zod/v4/classic/schemas.js +865 -0
  126. package/dist/vendor/zod/v4/core/api.js +512 -0
  127. package/dist/vendor/zod/v4/core/checks.js +300 -0
  128. package/dist/vendor/zod/v4/core/core.js +109 -0
  129. package/dist/vendor/zod/v4/core/doc.js +35 -0
  130. package/dist/vendor/zod/v4/core/errors.js +118 -0
  131. package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
  132. package/dist/vendor/zod/v4/core/memoizer.js +255 -0
  133. package/dist/vendor/zod/v4/core/parse.js +206 -0
  134. package/dist/vendor/zod/v4/core/regexes.js +73 -0
  135. package/dist/vendor/zod/v4/core/registries.js +48 -0
  136. package/dist/vendor/zod/v4/core/schemas.js +1378 -0
  137. package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
  138. package/dist/vendor/zod/v4/core/util.js +537 -0
  139. package/dist/vendor/zod/v4/core/versions.js +8 -0
  140. package/dist/vendor/zod/v4/locales/en.js +110 -0
  141. package/package.json +65 -4
@@ -0,0 +1,1059 @@
1
+ import { isObject } from "../core/json.js";
2
+ import { unpaddedBase64, urlSafeBase64 } from "../core/crypto.js";
3
+ import { M } from "../core/m.js";
4
+ import { SUPPORTED_ROOM_VERSIONS, computeContentHash, computeReferenceHash, eventIdOf, finalisePdu, redact, roomVersionFeatures, splitStateKey, stateKey, stripState } from "../core/events.js";
5
+ import { MatrixError, errors } from "../core/errors.js";
6
+ import { parseId } from "../core/ids.js";
7
+ import { authStateFromAuthEvents, checkAuth, getState, membershipOf } from "../core/authrules.js";
8
+ import { globMatch } from "../core/glob.js";
9
+ import { resolveState } from "../core/stateres.js";
10
+ import { MissingStateError } from "../core/rooms.js";
11
+ import { FederationClient } from "./client.js";
12
+ import { KeyRing } from "./keyring.js";
13
+ import { TransactionSender } from "./sender.js";
14
+ //#region src/federation/index.ts
15
+ const domainOf = (id) => id.slice(id.indexOf(":") + 1);
16
+ /** Server ACL check (m.room.server_acl). */
17
+ function serverAllowedByAcl(state, serverName) {
18
+ const acl = getState(state, M.ROOM.SERVER_ACL)?.content;
19
+ if (!isObject(acl)) return true;
20
+ const host = serverName.replace(/:\d+$/, "").replace(/^\[|\]$/g, "");
21
+ if (acl.allow_ip_literals === false && (/^\d+\.\d+\.\d+\.\d+$/.test(host) || host.includes(":"))) return false;
22
+ const matches = (p) => typeof p === "string" && globMatch(p, host, { caseInsensitive: true });
23
+ if (Array.isArray(acl.deny) && acl.deny.some(matches)) return false;
24
+ if (!Array.isArray(acl.allow)) return false;
25
+ return acl.allow.some(matches);
26
+ }
27
+ /** Server-server API: verification, inbound processing and outbound requests. */
28
+ var FederationService = class {
29
+ hs;
30
+ enabled;
31
+ fasterJoins;
32
+ client;
33
+ keyring;
34
+ sender;
35
+ options;
36
+ seenTxns = /* @__PURE__ */ new Map();
37
+ deviceStreams = /* @__PURE__ */ new Map();
38
+ constructor(hs, options = {}) {
39
+ this.hs = hs;
40
+ this.options = options;
41
+ this.enabled = options.enabled === true;
42
+ this.fasterJoins = options.fasterJoins !== false;
43
+ this.client = new FederationClient(hs, { rejectUnauthorized: options.verifyTls !== false });
44
+ this.keyring = new KeyRing(hs);
45
+ this.sender = new TransactionSender(hs);
46
+ }
47
+ serverAllowed(serverName) {
48
+ if (this.options.blockedServers?.includes(serverName)) return false;
49
+ if (this.options.allowedServers && !this.options.allowedServers.includes(serverName)) return false;
50
+ return true;
51
+ }
52
+ requireEnabled() {
53
+ if (!this.enabled) throw errors.forbidden("Federation is disabled");
54
+ }
55
+ /** Convert a stored event into its federation (wire) format. */
56
+ toFederationPdu(pdu, version) {
57
+ const { unsigned, ...rest } = pdu;
58
+ const out = { ...rest };
59
+ const f = roomVersionFeatures(version);
60
+ if (!f.legacyEventIds) delete out.event_id;
61
+ if (f.v12 && out.type === M.ROOM.CREATE) delete out.room_id;
62
+ return out;
63
+ }
64
+ computeEventId(raw, version) {
65
+ const f = roomVersionFeatures(version);
66
+ if (f.legacyEventIds) return raw.event_id;
67
+ const hash = computeReferenceHash(raw, version);
68
+ return `$${f.urlSafeEventIds ? urlSafeBase64(hash) : unpaddedBase64(hash)}`;
69
+ }
70
+ /**
71
+ * Verify an incoming PDU: event ID, signatures and content hash. Events with a bad
72
+ * content hash are redacted (as the spec requires) rather than rejected.
73
+ */
74
+ async verifyPdu(raw, version, roomId) {
75
+ if (!isObject(raw) || typeof raw.type !== "string" || typeof raw.sender !== "string" || !isObject(raw.content)) throw errors.badJson("Malformed PDU");
76
+ const f = roomVersionFeatures(version);
77
+ if (!f.v12 || raw.type !== M.ROOM.CREATE) {
78
+ if (raw.room_id !== roomId) throw errors.badJson("PDU room_id mismatch");
79
+ }
80
+ const eventId = this.computeEventId(raw, version);
81
+ if (typeof eventId !== "string") throw errors.badJson("Missing event ID");
82
+ const redacted = redact(raw, version);
83
+ const senderDomain = domainOf(raw.sender);
84
+ if (!await this.keyring.verifySignedBy(redacted, senderDomain, raw.origin_server_ts)) throw errors.forbidden(`Event ${eventId} is not signed by ${senderDomain}`);
85
+ if (f.legacyEventIds && domainOf(eventId) !== senderDomain && !await this.keyring.verifySignedBy(redacted, domainOf(eventId))) throw errors.forbidden(`Event ${eventId} is not signed by its origin`);
86
+ let joinAuthorisedSignatureValid;
87
+ const via = raw.content?.join_authorised_via_users_server;
88
+ if (f.restricted && raw.type === M.ROOM.MEMBER && raw.content?.membership === "join" && typeof via === "string" && via.includes(":")) joinAuthorisedSignatureValid = await this.keyring.verifySignedBy(redacted, domainOf(via));
89
+ let pdu = {
90
+ ...raw,
91
+ event_id: eventId
92
+ };
93
+ if (raw.hashes?.sha256 !== computeContentHash(raw)) pdu = {
94
+ ...redact(raw, version),
95
+ event_id: eventId
96
+ };
97
+ if (f.v12 && pdu.type === M.ROOM.CREATE) pdu.room_id = roomId;
98
+ return {
99
+ pdu,
100
+ joinAuthorisedSignatureValid
101
+ };
102
+ }
103
+ /** Servers with joined members in a room (excluding ourselves). */
104
+ serversInRoom(state, applyAcl = true) {
105
+ const servers = /* @__PURE__ */ new Set();
106
+ for (const [key, ev] of state) {
107
+ const [type, userId] = splitStateKey(key);
108
+ if (type === M.ROOM.MEMBER && ev.content?.membership === "join") servers.add(domainOf(userId));
109
+ }
110
+ servers.delete(this.hs.serverName);
111
+ for (const s of servers) if (!this.serverAllowed(s) || applyAcl && !serverAllowedByAcl(state, s)) servers.delete(s);
112
+ return servers;
113
+ }
114
+ /**
115
+ * Servers to send a room's traffic to: joined servers per the state, plus, while the room's
116
+ * state is partial (MSC3902), the servers the resident server said were in the room.
117
+ */
118
+ async roomDestinations(roomId, state, applyAcl = true) {
119
+ const destinations = this.serversInRoom(state, applyAcl);
120
+ for (const s of await this.hs.partialState.servers(roomId)) if (s !== this.hs.serverName && this.serverAllowed(s) && (!applyAcl || serverAllowedByAcl(state, s))) destinations.add(s);
121
+ return destinations;
122
+ }
123
+ /** Send a newly created local event to every server in the room. */
124
+ onLocalEvent(pdu, state) {
125
+ if (!this.enabled) return;
126
+ (async () => {
127
+ const destinations = await this.roomDestinations(pdu.room_id, state, pdu.type !== M.ROOM.SERVER_ACL);
128
+ if (pdu.type === M.ROOM.MEMBER && typeof pdu.state_key === "string" && !this.hs.isLocalUser(pdu.state_key)) destinations.add(domainOf(pdu.state_key));
129
+ if (!destinations.size) return;
130
+ const version = await this.hs.rooms.roomVersion(pdu.room_id) ?? "10";
131
+ await this.sender.sendPdu(destinations, this.toFederationPdu(pdu, version));
132
+ })().catch((err) => this.hs.logger.warn("Failed to queue PDU", err));
133
+ }
134
+ /** Send an EDU to every server in the given rooms. */
135
+ async sendEduToRooms(roomIds, eduType, content, durable = true) {
136
+ if (!this.enabled) return;
137
+ const destinations = /* @__PURE__ */ new Set();
138
+ for (const roomId of roomIds) {
139
+ if (!await this.hs.rooms.exists(roomId)) continue;
140
+ for (const s of await this.roomDestinations(roomId, await this.hs.rooms.currentState(roomId))) destinations.add(s);
141
+ }
142
+ if (destinations.size) await this.sender.sendEdu(destinations, eduType, content, durable);
143
+ }
144
+ /** Servers sharing a room with a local user. */
145
+ async serversSharingRooms(userId) {
146
+ const out = /* @__PURE__ */ new Set();
147
+ for (const roomId of await this.hs.rooms.roomsForUser(userId)) for (const s of await this.roomDestinations(roomId, await this.hs.rooms.currentState(roomId))) out.add(s);
148
+ return out;
149
+ }
150
+ async onTyping(roomId, userId, typing) {
151
+ if (this.hs.isLocalUser(userId)) await this.sendEduToRooms([roomId], M.TYPING, {
152
+ room_id: roomId,
153
+ user_id: userId,
154
+ typing
155
+ }, false);
156
+ }
157
+ async onReceipt(roomId, userId, receiptType, eventId, ts, threadId) {
158
+ if (!this.hs.isLocalUser(userId) || receiptType === M.READ.PRIVATE) return;
159
+ const data = { ts };
160
+ if (threadId) data.thread_id = threadId;
161
+ await this.sendEduToRooms([roomId], M.RECEIPT, { [roomId]: { [receiptType]: { [userId]: {
162
+ event_ids: [eventId],
163
+ data
164
+ } } } });
165
+ }
166
+ async onPresence(userId, presence, statusMsg) {
167
+ if (!this.enabled || !this.hs.isLocalUser(userId)) return;
168
+ const servers = await this.serversSharingRooms(userId);
169
+ if (!servers.size) return;
170
+ await this.sender.sendEdu(servers, M.PRESENCE, { push: [{
171
+ user_id: userId,
172
+ presence,
173
+ last_active_ago: 0,
174
+ currently_active: presence === "online",
175
+ ...statusMsg ? { status_msg: statusMsg } : {}
176
+ }] }, false);
177
+ }
178
+ /** Tell servers sharing rooms with a local user that their device list changed. */
179
+ async onDeviceListChange(userId, deletedDeviceId, to) {
180
+ if (!this.enabled || !this.hs.isLocalUser(userId)) return;
181
+ const servers = to ?? await this.serversSharingRooms(userId);
182
+ if (!servers.size) return;
183
+ const devices = await this.hs.db.findMany("devices", { where: [{
184
+ field: "user_id",
185
+ value: userId
186
+ }] });
187
+ const updates = devices.map((d) => ({
188
+ deviceId: d.device_id,
189
+ deleted: false
190
+ }));
191
+ if (deletedDeviceId) updates.push({
192
+ deviceId: deletedDeviceId,
193
+ deleted: true
194
+ });
195
+ for (const u of updates) {
196
+ const prev = this.deviceStreams.get(userId) ?? (await this.hs.db.findOne("device_list_changes", [{
197
+ field: "user_id",
198
+ value: userId
199
+ }]))?.stream ?? 0;
200
+ const streamId = Math.max(await this.hs.streams.write(async (id) => id), prev + 1);
201
+ this.deviceStreams.set(userId, streamId);
202
+ const content = {
203
+ user_id: userId,
204
+ device_id: u.deviceId,
205
+ stream_id: streamId,
206
+ prev_id: prev ? [prev] : []
207
+ };
208
+ if (u.deleted) content.deleted = true;
209
+ else {
210
+ const keys = await this.hs.db.findOne("device_keys", [{
211
+ field: "user_id",
212
+ value: userId
213
+ }, {
214
+ field: "device_id",
215
+ value: u.deviceId
216
+ }]);
217
+ if (keys) content.keys = keys.keys;
218
+ const d = devices.find((x) => x.device_id === u.deviceId);
219
+ if (d?.display_name) content.device_display_name = d.display_name;
220
+ }
221
+ await this.sender.sendEdu(servers, M.DEVICE_LIST_UPDATE, content);
222
+ }
223
+ }
224
+ async onSigningKeyUpdate(userId) {
225
+ if (!this.enabled || !this.hs.isLocalUser(userId)) return;
226
+ const servers = await this.serversSharingRooms(userId);
227
+ if (!servers.size) return;
228
+ const rows = await this.hs.db.findMany("cross_signing_keys", { where: [{
229
+ field: "user_id",
230
+ value: userId
231
+ }] });
232
+ const content = { user_id: userId };
233
+ for (const r of rows) if (r.key_type !== "user_signing") content[`${r.key_type}_key`] = r.key;
234
+ await this.sender.sendEdu(servers, M.SIGNING_KEY_UPDATE, content);
235
+ }
236
+ /** Deliver to-device messages addressed to remote users. */
237
+ async sendToDevice(sender, type, messages, messageId) {
238
+ if (!this.enabled) return;
239
+ const byServer = /* @__PURE__ */ new Map();
240
+ for (const [userId, devices] of Object.entries(messages)) {
241
+ const server = domainOf(userId);
242
+ if (server === this.hs.serverName || !this.serverAllowed(server)) continue;
243
+ (byServer.get(server) ?? byServer.set(server, {}).get(server))[userId] = devices;
244
+ }
245
+ for (const [server, msgs] of byServer) await this.sender.sendEdu([server], M.DIRECT_TO_DEVICE, {
246
+ sender,
247
+ type,
248
+ message_id: `${messageId}-${server}`,
249
+ messages: msgs
250
+ });
251
+ }
252
+ async receiveTransaction(origin, txnId, body) {
253
+ const key = `fedtxn:${origin}|${txnId}`;
254
+ this.sender.wake(origin);
255
+ const local = this.seenTxns.get(key);
256
+ if (local) return local;
257
+ const done = await this.hs.cluster.get(key);
258
+ if (done) return done;
259
+ const p = this.hs.cluster.lock(key, async () => {
260
+ const again = await this.hs.cluster.get(key);
261
+ if (again) return again;
262
+ const result = await this.processTransaction(origin, body);
263
+ await this.hs.cluster.set(key, result, 864e5);
264
+ return result;
265
+ });
266
+ this.seenTxns.set(key, p);
267
+ if (this.seenTxns.size > 1e3) this.seenTxns.delete(this.seenTxns.keys().next().value);
268
+ return p;
269
+ }
270
+ async processTransaction(origin, body) {
271
+ const pdus = Array.isArray(body.pdus) ? body.pdus.slice(0, 50) : [];
272
+ const edus = Array.isArray(body.edus) ? body.edus.slice(0, 100) : [];
273
+ const results = {};
274
+ for (const raw of pdus) {
275
+ const roomId = typeof raw?.room_id === "string" ? raw.room_id : null;
276
+ let eventId = "unknown";
277
+ try {
278
+ if (!roomId) throw errors.badJson("PDU without room_id");
279
+ const version = await this.hs.rooms.roomVersion(roomId);
280
+ if (!version) {
281
+ eventId = await this.handleOutOfRoomMembership(origin, roomId, raw);
282
+ results[eventId] = {};
283
+ continue;
284
+ }
285
+ eventId = this.computeEventId(raw, version);
286
+ await this.hs.rooms.withLock(roomId, () => this.handlePdu(origin, roomId, version, raw));
287
+ results[eventId] = {};
288
+ } catch (err) {
289
+ results[eventId] = { error: err instanceof Error ? err.message : String(err) };
290
+ this.hs.logger.debug("PDU from", origin, "failed", err);
291
+ }
292
+ }
293
+ for (const edu of edus) try {
294
+ await this.handleEdu(origin, edu);
295
+ } catch (err) {
296
+ this.hs.logger.debug("EDU from", origin, "failed", err);
297
+ }
298
+ return { pdus: results };
299
+ }
300
+ /**
301
+ * A PDU for a room we are not in can still matter: a local user's invite being
302
+ * rescinded or their knock being accepted/rejected.
303
+ */
304
+ async handleOutOfRoomMembership(origin, roomId, raw) {
305
+ if (raw?.type !== M.ROOM.MEMBER || typeof raw.state_key !== "string" || !this.hs.isLocalUser(raw.state_key)) throw errors.notFound("Unknown room");
306
+ const row = await this.hs.db.findOne("memberships", [{
307
+ field: "room_id",
308
+ value: roomId
309
+ }, {
310
+ field: "user_id",
311
+ value: raw.state_key
312
+ }]);
313
+ if (!row) throw errors.notFound("Unknown room");
314
+ const previous = await this.hs.rooms.getPdu(row.event_id);
315
+ const create = (previous?.unsigned?.invite_room_state ?? previous?.unsigned?.knock_room_state ?? []).find((e) => e?.type === M.ROOM.CREATE);
316
+ const version = typeof create?.content?.room_version === "string" ? create.content.room_version : "1";
317
+ const { pdu } = await this.verifyPdu(raw, version, roomId);
318
+ if (domainOf(pdu.sender) !== origin && domainOf(pdu.sender) !== domainOf(raw.state_key)) throw errors.forbidden("Unexpected sender");
319
+ const membership = pdu.content?.membership;
320
+ if (row.membership === "invite" && (membership === "leave" || membership === "ban") && previous && pdu.sender !== previous.sender && pdu.sender !== raw.state_key) return pdu.event_id;
321
+ if (membership === "invite" || membership === "join") pdu.unsigned = {
322
+ ...pdu.unsigned,
323
+ invite_room_state: previous?.unsigned?.knock_room_state ?? previous?.unsigned?.invite_room_state ?? []
324
+ };
325
+ if (membership !== "join") await this.hs.rooms.recordRemoteMembership(pdu, version);
326
+ return pdu.event_id;
327
+ }
328
+ /** Process one incoming PDU (caller holds the room lock). */
329
+ async handlePdu(origin, roomId, version, raw, depth = 0) {
330
+ if (!serverAllowedByAcl(await this.hs.rooms.currentState(roomId), origin)) throw errors.forbidden("Server is denied by the room's ACL");
331
+ const { pdu, joinAuthorisedSignatureValid } = await this.verifyPdu(raw, version, roomId);
332
+ if (await this.hs.rooms.getEvent(pdu.event_id)) return;
333
+ for (const id of (pdu.auth_events ?? []).map(eventIdOf)) if ((await this.hs.rooms.getOutlier(id))?.rejected) {
334
+ await this.hs.db.upsert("outliers", {
335
+ event_id: pdu.event_id,
336
+ room_id: roomId,
337
+ json: pdu,
338
+ state_snapshot: null,
339
+ soft_failed: false,
340
+ rejected: `Auth event ${id} was rejected`
341
+ });
342
+ return;
343
+ }
344
+ let stateBefore;
345
+ const prevIds = (pdu.prev_events ?? []).map(eventIdOf);
346
+ const unknownPrevs = async () => {
347
+ const out = [];
348
+ for (const id of prevIds) {
349
+ if (await this.hs.rooms.snapshotOf(id) !== null) continue;
350
+ if ((await this.hs.rooms.getOutlier(id))?.rejected) continue;
351
+ out.push(id);
352
+ }
353
+ return out;
354
+ };
355
+ let missing = await unknownPrevs();
356
+ if (missing.length && depth < 3) {
357
+ if (!await this.fetchMissingPrevEvents(origin, roomId, version, pdu)) return;
358
+ missing = await unknownPrevs();
359
+ }
360
+ if (missing.length && depth === 0) throw errors.forbidden("Your server isn't divulging details about prev_events referenced in this event.");
361
+ if (missing.length) stateBefore = await this.stateFromPrevs(origin, roomId, version, prevIds);
362
+ await this.fetchMissingAuthEvents(origin, roomId, version, pdu);
363
+ for (const id of (pdu.auth_events ?? []).map(eventIdOf)) {
364
+ const outlier = await this.hs.rooms.getOutlier(id);
365
+ if (outlier?.rejected || !await this.hs.rooms.knowsEvent(id)) {
366
+ await this.hs.db.upsert("outliers", {
367
+ event_id: pdu.event_id,
368
+ room_id: roomId,
369
+ json: pdu,
370
+ state_snapshot: null,
371
+ soft_failed: false,
372
+ rejected: outlier?.rejected ? `Auth event ${id} was rejected` : `Auth event ${id} is unavailable`
373
+ });
374
+ return;
375
+ }
376
+ }
377
+ try {
378
+ const result = await this.hs.rooms.persistEvent(roomId, version, pdu, {
379
+ joinAuthorisedSignatureValid,
380
+ stateBefore,
381
+ gap: !!stateBefore
382
+ });
383
+ if (result.status === "rejected") this.hs.logger.debug("Rejected", pdu.event_id, result.reason);
384
+ } catch (err) {
385
+ if (err instanceof MissingStateError && !stateBefore && depth > 0) {
386
+ const sb = await this.stateFromPrevs(origin, roomId, version, prevIds);
387
+ await this.hs.rooms.persistEvent(roomId, version, pdu, {
388
+ joinAuthorisedSignatureValid,
389
+ stateBefore: sb,
390
+ gap: true
391
+ });
392
+ } else throw err;
393
+ }
394
+ }
395
+ /**
396
+ * State before an event whose predecessors we lack: for each unknown prev event,
397
+ * fetch the state at it (/state_ids) plus the event itself, then resolve all
398
+ * predecessors' states. Throws if a required event cannot be fetched.
399
+ */
400
+ async stateFromPrevs(origin, roomId, version, prevIds, opts = {}) {
401
+ const sets = [];
402
+ for (const prev of prevIds) {
403
+ const snap = opts.remote ? null : await this.hs.rooms.snapshotOf(prev);
404
+ if (snap !== null) {
405
+ sets.push(await this.hs.rooms.snapshotIds(snap));
406
+ continue;
407
+ }
408
+ if ((await this.hs.rooms.getOutlier(prev))?.rejected) continue;
409
+ const before = await this.fetchStateBefore(origin, roomId, version, prev);
410
+ if (!await this.hs.rooms.getPdu(prev)) await this.fetchEvent(origin, roomId, version, prev);
411
+ const prevPdu = await this.hs.rooms.getPdu(prev);
412
+ if (!prevPdu) throw errors.notFound(`Could not fetch prev event ${prev}`);
413
+ sets.push(prevPdu.state_key !== void 0 ? {
414
+ ...before,
415
+ [stateKey(prevPdu.type, prevPdu.state_key)]: prev
416
+ } : before);
417
+ }
418
+ if (!sets.length) throw errors.forbidden("All predecessors were rejected");
419
+ if (sets.length === 1) return sets[0];
420
+ const create = roomVersionFeatures(version).v12 ? await this.hs.rooms.getPdu(`$${roomId.slice(1)}`) ?? void 0 : void 0;
421
+ return resolveState(sets, version, (ids) => this.hs.rooms.getPdus(ids), create);
422
+ }
423
+ async fetchMissingAuthEvents(origin, roomId, version, pdu) {
424
+ const authIds = (pdu.auth_events ?? []).map(eventIdOf);
425
+ const missing = [];
426
+ for (const id of authIds) if (!await this.hs.rooms.knowsEvent(id)) missing.push(id);
427
+ if (!missing.length) return;
428
+ try {
429
+ const res = await this.client.request(origin, "GET", `/_matrix/federation/v1/event_auth/${encodeURIComponent(roomId)}/${encodeURIComponent(pdu.event_id)}`);
430
+ await this.storeOutliers(roomId, version, Array.isArray(res.auth_chain) ? res.auth_chain : []);
431
+ } catch {
432
+ const seen = /* @__PURE__ */ new Set();
433
+ const raws = [];
434
+ let queue = missing;
435
+ while (queue.length && seen.size < 100) {
436
+ const next = [];
437
+ for (const id of queue) {
438
+ if (seen.has(id) || await this.hs.rooms.knowsEvent(id)) continue;
439
+ seen.add(id);
440
+ const raw = await this.fetchRawEvent(origin, id).catch(() => null);
441
+ if (!raw) continue;
442
+ raws.push(raw);
443
+ for (const a of (raw.auth_events ?? []).map(eventIdOf)) next.push(a);
444
+ }
445
+ queue = next;
446
+ }
447
+ await this.storeOutliers(roomId, version, raws);
448
+ }
449
+ }
450
+ /** Fill a gap with /get_missing_events. Returns false when the response contained invalid events. */
451
+ async fetchMissingPrevEvents(origin, roomId, version, pdu) {
452
+ const earliest = await this.hs.rooms.extremities(roomId);
453
+ let res;
454
+ try {
455
+ res = await this.client.request(origin, "POST", `/_matrix/federation/v1/get_missing_events/${encodeURIComponent(roomId)}`, { body: {
456
+ earliest_events: earliest,
457
+ latest_events: [pdu.event_id],
458
+ limit: 10,
459
+ min_depth: 0
460
+ } });
461
+ } catch {
462
+ return true;
463
+ }
464
+ const events = (Array.isArray(res.events) ? res.events : []).filter(isObject);
465
+ for (const ev of events) try {
466
+ this.computeEventId(ev, version);
467
+ } catch {
468
+ return false;
469
+ }
470
+ events.sort((a, b) => (a.depth ?? 0) - (b.depth ?? 0));
471
+ const failed = /* @__PURE__ */ new Set();
472
+ for (const ev of events) try {
473
+ await this.handlePdu(origin, roomId, version, ev, 3);
474
+ } catch (err) {
475
+ failed.add(this.computeEventId(ev, version));
476
+ this.hs.logger.debug("Missing event failed", err);
477
+ }
478
+ return !(pdu.prev_events ?? []).map(eventIdOf).some((id) => failed.has(id));
479
+ }
480
+ /** Fetch one event from a remote server and store it as an outlier. */
481
+ async fetchEvent(origin, roomId, version, eventId) {
482
+ const raw = await this.fetchRawEvent(origin, eventId);
483
+ if (raw) await this.storeOutliers(roomId, version, [raw]);
484
+ }
485
+ async fetchRawEvent(origin, eventId) {
486
+ const r = await this.client.request(origin, "GET", `/_matrix/federation/v1/event/${encodeURIComponent(eventId)}`);
487
+ return Array.isArray(r.pdus) && isObject(r.pdus[0]) ? r.pdus[0] : null;
488
+ }
489
+ /**
490
+ * Fetch the state before an event from a remote server. Uses /state_ids and
491
+ * fetches only unknown events (falling back to /state), storing them as outliers.
492
+ */
493
+ async fetchStateBefore(origin, roomId, version, eventId) {
494
+ let stateIds;
495
+ let res = null;
496
+ const timeoutMs = 3e5;
497
+ try {
498
+ res = await this.client.request(origin, "GET", `/_matrix/federation/v1/state_ids/${encodeURIComponent(roomId)}`, {
499
+ query: { event_id: eventId },
500
+ timeoutMs
501
+ });
502
+ } catch (err) {
503
+ this.hs.logger.debug("/state_ids failed, falling back to /state", origin, err);
504
+ }
505
+ if (res && (!Array.isArray(res.pdu_ids) || !res.pdu_ids.length)) throw errors.notFound("Remote server returned no state");
506
+ if (res) {
507
+ stateIds = res.pdu_ids;
508
+ const wanted = [...Array.isArray(res.auth_chain_ids) ? res.auth_chain_ids : [], ...stateIds];
509
+ const missing = [];
510
+ for (const id of wanted) if (typeof id === "string" && !await this.hs.rooms.knowsEvent(id)) missing.push(id);
511
+ const raws = [];
512
+ for (let i = 0; i < missing.length; i += 20) {
513
+ const batch = await Promise.all(missing.slice(i, i + 20).map((id) => this.fetchRawEvent(origin, id).catch(() => null)));
514
+ for (const raw of batch) if (raw) raws.push(raw);
515
+ }
516
+ await this.storeOutliers(roomId, version, raws);
517
+ for (const id of stateIds) if (!await this.hs.rooms.knowsEvent(id)) throw errors.notFound(`Could not fetch state event ${id}`);
518
+ } else {
519
+ const res = await this.client.request(origin, "GET", `/_matrix/federation/v1/state/${encodeURIComponent(roomId)}`, {
520
+ query: { event_id: eventId },
521
+ timeoutMs
522
+ });
523
+ const pdus = Array.isArray(res.pdus) ? res.pdus : [];
524
+ if (!pdus.length) throw errors.notFound("Remote server returned no state");
525
+ await this.storeOutliers(roomId, version, [...Array.isArray(res.auth_chain) ? res.auth_chain : [], ...pdus]);
526
+ stateIds = pdus.filter(isObject).map((raw) => this.computeEventId(raw, version));
527
+ }
528
+ const ids = {};
529
+ const pdus = await this.hs.rooms.getPdus(stateIds);
530
+ for (const id of stateIds) {
531
+ const pdu = pdus.get(id);
532
+ if (pdu && pdu.state_key !== void 0) ids[stateKey(pdu.type, pdu.state_key)] = id;
533
+ }
534
+ return ids;
535
+ }
536
+ /** Verify and store events that are not (yet) part of the timeline. */
537
+ async storeOutliers(roomId, version, raws) {
538
+ const verified = [];
539
+ for (const raw of raws) {
540
+ if (!isObject(raw)) continue;
541
+ try {
542
+ const { pdu } = await this.verifyPdu(raw, version, roomId);
543
+ if (!await this.hs.rooms.knowsEvent(pdu.event_id)) verified.push(pdu);
544
+ } catch (err) {
545
+ this.hs.logger.debug("Dropping invalid outlier", err);
546
+ }
547
+ }
548
+ verified.sort((a, b) => a.depth - b.depth);
549
+ const seen = /* @__PURE__ */ new Set();
550
+ for (const pdu of verified) {
551
+ if (seen.has(pdu.event_id)) continue;
552
+ seen.add(pdu.event_id);
553
+ const rejected = await this.checkOutlierAuth(roomId, version, pdu);
554
+ await this.hs.db.upsert("outliers", {
555
+ event_id: pdu.event_id,
556
+ room_id: roomId,
557
+ json: pdu,
558
+ state_snapshot: null,
559
+ rejected,
560
+ soft_failed: false
561
+ });
562
+ }
563
+ }
564
+ /** Check an outlier against its own auth events. Returns a rejection reason, or null. */
565
+ async checkOutlierAuth(roomId, version, pdu) {
566
+ const authIds = (pdu.auth_events ?? []).map(eventIdOf);
567
+ const authPdus = [];
568
+ for (const id of authIds) {
569
+ if ((await this.hs.rooms.getOutlier(id))?.rejected) return `Auth event ${id} was rejected`;
570
+ const p = await this.hs.rooms.getPdu(id);
571
+ if (!p) return `Auth event ${id} is unavailable`;
572
+ authPdus.push(p);
573
+ }
574
+ try {
575
+ const create = roomVersionFeatures(version).v12 && pdu.type !== M.ROOM.CREATE ? await this.hs.rooms.getPdu(`$${roomId.slice(1)}`) ?? void 0 : void 0;
576
+ checkAuth(pdu, authStateFromAuthEvents(pdu, authPdus, version, create), version);
577
+ return null;
578
+ } catch (err) {
579
+ return err instanceof Error ? err.message : String(err);
580
+ }
581
+ }
582
+ /**
583
+ * Whether a remote user's ephemeral activity (typing, receipts) in a room is accepted: they are
584
+ * joined, or the room's state is partial (MSC3902) and their server is one of the room's servers.
585
+ */
586
+ async mayActInRoom(roomId, userId, origin) {
587
+ const membership = await this.hs.rooms.membership(roomId, userId);
588
+ if (membership === "join") return true;
589
+ if (membership !== "leave" || !await this.hs.partialState.isPartial(roomId)) return false;
590
+ return (await this.hs.partialState.servers(roomId)).includes(origin);
591
+ }
592
+ async handleEdu(origin, edu) {
593
+ const content = edu?.content;
594
+ if (!isObject(content)) return;
595
+ switch (edu.edu_type) {
596
+ case M.TYPING: {
597
+ const { room_id, user_id, typing } = content;
598
+ if (typeof user_id !== "string" || domainOf(user_id) !== origin || typeof room_id !== "string") return;
599
+ if (!await this.hs.rooms.exists(room_id) || !await this.mayActInRoom(room_id, user_id, origin)) return;
600
+ if (!serverAllowedByAcl(await this.hs.rooms.currentState(room_id), origin)) return;
601
+ await this.hs.typing.setTyping(room_id, user_id, !!typing, typeof content.timeout === "number" ? content.timeout : 3e4);
602
+ return;
603
+ }
604
+ case M.RECEIPT:
605
+ for (const [roomId, byType] of Object.entries(content)) {
606
+ if (!isObject(byType) || !await this.hs.rooms.exists(roomId)) continue;
607
+ if (!serverAllowedByAcl(await this.hs.rooms.currentState(roomId), origin)) continue;
608
+ for (const [receiptType, byUser] of Object.entries(byType)) {
609
+ if (receiptType !== M.READ._ && receiptType !== M.READ.PRIVATE || !isObject(byUser)) continue;
610
+ for (const [userId, r] of Object.entries(byUser)) {
611
+ if (domainOf(userId) !== origin || !isObject(r) || !Array.isArray(r.event_ids)) continue;
612
+ if (!await this.mayActInRoom(roomId, userId, origin)) continue;
613
+ const eventId = r.event_ids[r.event_ids.length - 1];
614
+ if (typeof eventId !== "string") continue;
615
+ await this.hs.streams.write((stream) => this.hs.db.upsert("receipts", {
616
+ room_id: roomId,
617
+ user_id: userId,
618
+ receipt_type: receiptType,
619
+ thread_id: typeof r.data?.thread_id === "string" ? r.data.thread_id : "",
620
+ event_id: eventId,
621
+ ts: Number(r.data?.ts) || Date.now(),
622
+ stream
623
+ }));
624
+ }
625
+ }
626
+ this.hs.notifier.notify(await this.hs.rooms.joinedMembers(roomId));
627
+ }
628
+ return;
629
+ case M.PRESENCE:
630
+ for (const p of Array.isArray(content.push) ? content.push : []) {
631
+ if (!isObject(p) || typeof p.user_id !== "string" || domainOf(p.user_id) !== origin) continue;
632
+ if (![
633
+ "online",
634
+ "offline",
635
+ "unavailable"
636
+ ].includes(p.presence)) continue;
637
+ await this.hs.streams.write((stream) => this.hs.db.upsert("presence", {
638
+ user_id: p.user_id,
639
+ presence: p.presence,
640
+ status_msg: typeof p.status_msg === "string" ? p.status_msg : null,
641
+ last_active_ts: Date.now() - (Number(p.last_active_ago) || 0),
642
+ stream
643
+ }));
644
+ this.hs.notifier.notify(await this.hs.rooms.usersSharingRooms(p.user_id));
645
+ }
646
+ return;
647
+ case M.DEVICE_LIST_UPDATE: {
648
+ const userId = content.user_id;
649
+ if (typeof userId !== "string" || domainOf(userId) !== origin || typeof content.device_id !== "string") return;
650
+ const known = await this.hs.db.findOne("remote_device_lists", [{
651
+ field: "user_id",
652
+ value: userId
653
+ }]);
654
+ if (!await this.hs.keys.isTracked(userId)) await this.hs.partialState.notePendingDeviceUpdate(userId, origin);
655
+ await this.hs.keys.markRemoteStale(userId);
656
+ if (known || content.deleted || isObject(content.keys)) await this.hs.keys.markDeviceListChanged(userId);
657
+ return;
658
+ }
659
+ case M.SIGNING_KEY_UPDATE: {
660
+ const userId = content.user_id;
661
+ if (typeof userId !== "string" || domainOf(userId) !== origin) return;
662
+ for (const type of ["master", "self_signing"]) {
663
+ const key = content[`${type}_key`];
664
+ if (isObject(key)) await this.hs.streams.write((stream) => this.hs.db.upsert("cross_signing_keys", {
665
+ user_id: userId,
666
+ key_type: type,
667
+ key,
668
+ stream
669
+ }));
670
+ }
671
+ await this.hs.keys.markRemoteStale(userId);
672
+ await this.hs.keys.markDeviceListChanged(userId);
673
+ return;
674
+ }
675
+ case M.DIRECT_TO_DEVICE: {
676
+ const { sender, type, message_id, messages } = content;
677
+ if (typeof sender !== "string" || domainOf(sender) !== origin || typeof type !== "string" || !isObject(messages)) return;
678
+ if (!await this.hs.cluster.setIfAbsent(`fedtodevice:${origin}|${message_id}`, true, 864e5)) return;
679
+ const recipients = /* @__PURE__ */ new Set();
680
+ for (const [userId, devices] of Object.entries(messages)) {
681
+ if (!this.hs.isLocalUser(userId) || !isObject(devices)) continue;
682
+ let targets = Object.keys(devices);
683
+ if (targets.includes("*")) targets = (await this.hs.db.findMany("devices", { where: [{
684
+ field: "user_id",
685
+ value: userId
686
+ }] })).map((d) => d.device_id);
687
+ for (const deviceId of targets) await this.hs.streams.write((stream) => this.hs.db.create("to_device", {
688
+ stream,
689
+ user_id: userId,
690
+ device_id: deviceId,
691
+ sender,
692
+ type,
693
+ content: devices[deviceId] ?? devices["*"]
694
+ }));
695
+ recipients.add(userId);
696
+ }
697
+ this.hs.notifier.notify(recipients);
698
+ return;
699
+ }
700
+ }
701
+ }
702
+ /** Build a full event from a make_join/make_leave/make_knock template and sign it. */
703
+ finaliseTemplate(template, version, extraContent) {
704
+ const { event_id, hashes, signatures, unsigned, ...rest } = template;
705
+ const pdu = {
706
+ ...rest,
707
+ content: {
708
+ ...rest.content,
709
+ ...extraContent
710
+ },
711
+ origin_server_ts: Date.now()
712
+ };
713
+ if (roomVersionFeatures(version).legacyEventIds) pdu.origin = this.hs.serverName;
714
+ return finalisePdu(pdu, version, this.hs.serverName, this.hs.signingKey);
715
+ }
716
+ /** Join a room hosted on other servers. Returns the room ID. */
717
+ async joinRemoteRoom(userId, roomId, servers, content) {
718
+ this.requireEnabled();
719
+ let lastError = errors.notFound("No server could be reached to join the room");
720
+ for (const server of [...new Set(servers)].filter((s) => s !== this.hs.serverName && this.serverAllowed(s))) try {
721
+ const tmpl = await this.client.request(server, "GET", `/_matrix/federation/v1/make_join/${encodeURIComponent(roomId)}/${encodeURIComponent(userId)}`, { query: { ver: SUPPORTED_ROOM_VERSIONS } });
722
+ const version = typeof tmpl.room_version === "string" ? tmpl.room_version : "1";
723
+ if (!SUPPORTED_ROOM_VERSIONS.includes(version)) throw errors.unsupportedRoomVersion();
724
+ if (!isObject(tmpl.event) || tmpl.event.content?.membership !== "join" || tmpl.event.state_key !== userId || tmpl.event.sender !== userId) throw errors.badJson("Invalid make_join template");
725
+ const joinPdu = this.finaliseTemplate(tmpl.event, version, content);
726
+ const res = await this.client.request(server, "PUT", `/_matrix/federation/v2/send_join/${encodeURIComponent(roomId)}/${encodeURIComponent(joinPdu.event_id)}`, {
727
+ body: this.toFederationPdu(joinPdu, version),
728
+ query: this.fasterJoins ? { omit_members: "true" } : void 0,
729
+ timeoutMs: 12e4
730
+ });
731
+ if (isObject(res.event?.signatures)) joinPdu.signatures = {
732
+ ...joinPdu.signatures,
733
+ ...res.event.signatures
734
+ };
735
+ await this.completeJoin(roomId, version, joinPdu, res, server);
736
+ return roomId;
737
+ } catch (err) {
738
+ lastError = err;
739
+ this.hs.logger.debug("Joining", roomId, "via", server, "failed", err);
740
+ }
741
+ throw lastError;
742
+ }
743
+ async completeJoin(roomId, version, joinPdu, res, origin) {
744
+ const statePdus = Array.isArray(res.state) ? res.state : [];
745
+ const authChain = Array.isArray(res.auth_chain) ? res.auth_chain : [];
746
+ await this.storeOutliers(roomId, version, [...authChain, ...statePdus]);
747
+ const stateIds = {};
748
+ let create;
749
+ for (const raw of statePdus) {
750
+ if (!isObject(raw) || typeof raw.state_key !== "string") continue;
751
+ const id = this.computeEventId(raw, version);
752
+ const pdu = await this.hs.rooms.getPdu(id);
753
+ if (!pdu) continue;
754
+ stateIds[stateKey(pdu.type, pdu.state_key)] = id;
755
+ if (pdu.type === M.ROOM.CREATE) create = pdu;
756
+ }
757
+ if (!create) throw errors.badJson("send_join response lacks the create event");
758
+ if (!await this.hs.rooms.exists(roomId)) await this.hs.db.upsert("rooms", {
759
+ room_id: roomId,
760
+ room_version: version,
761
+ creator: create.sender,
762
+ is_public: false,
763
+ created_ts: Date.now()
764
+ });
765
+ if (await this.hs.rooms.serverIsJoined(roomId)) {
766
+ await this.hs.rooms.withLock(roomId, () => this.hs.rooms.persistEvent(roomId, version, joinPdu, {
767
+ local: true,
768
+ noFederate: true
769
+ }));
770
+ return;
771
+ }
772
+ let gap = false;
773
+ for (const prev of (joinPdu.prev_events ?? []).map(eventIdOf)) if (!await this.hs.rooms.getEvent(prev)) gap = true;
774
+ await this.hs.rooms.withLock(roomId, () => this.hs.rooms.persistEvent(roomId, version, joinPdu, {
775
+ local: true,
776
+ stateBefore: stateIds,
777
+ createEvent: roomVersionFeatures(version).v12 ? create : void 0,
778
+ noFederate: true,
779
+ gap
780
+ }));
781
+ if (res.members_omitted === true) {
782
+ const servers = Array.isArray(res.servers_in_room) ? res.servers_in_room.filter((s) => typeof s === "string") : [];
783
+ await this.hs.partialState.markPartial(roomId, joinPdu.event_id, origin, servers);
784
+ }
785
+ }
786
+ /** Leave (or reject an invite to) a room hosted elsewhere. */
787
+ async leaveRemoteRoom(userId, roomId, servers, reason) {
788
+ this.requireEnabled();
789
+ let lastError = errors.notFound("No server could be reached to leave the room");
790
+ for (const server of [...new Set(servers)].filter((s) => s !== this.hs.serverName)) try {
791
+ const tmpl = await this.client.request(server, "GET", `/_matrix/federation/v1/make_leave/${encodeURIComponent(roomId)}/${encodeURIComponent(userId)}`);
792
+ const version = typeof tmpl.room_version === "string" ? tmpl.room_version : "1";
793
+ const pdu = this.finaliseTemplate(tmpl.event, version, reason ? { reason } : {});
794
+ await this.client.request(server, "PUT", `/_matrix/federation/v2/send_leave/${encodeURIComponent(roomId)}/${encodeURIComponent(pdu.event_id)}`, { body: this.toFederationPdu(pdu, version) });
795
+ return pdu;
796
+ } catch (err) {
797
+ lastError = err;
798
+ }
799
+ throw lastError;
800
+ }
801
+ async knockRemoteRoom(userId, roomId, servers, reason) {
802
+ this.requireEnabled();
803
+ let lastError = errors.notFound("No server could be reached to knock on the room");
804
+ for (const server of [...new Set(servers)].filter((s) => s !== this.hs.serverName)) try {
805
+ const tmpl = await this.client.request(server, "GET", `/_matrix/federation/v1/make_knock/${encodeURIComponent(roomId)}/${encodeURIComponent(userId)}`, { query: { ver: SUPPORTED_ROOM_VERSIONS } });
806
+ const version = typeof tmpl.room_version === "string" ? tmpl.room_version : "1";
807
+ const pdu = this.finaliseTemplate(tmpl.event, version, reason ? { reason } : {});
808
+ const res = await this.client.request(server, "PUT", `/_matrix/federation/v1/send_knock/${encodeURIComponent(roomId)}/${encodeURIComponent(pdu.event_id)}`, { body: this.toFederationPdu(pdu, version) });
809
+ return {
810
+ pdu,
811
+ knockState: Array.isArray(res.knock_room_state) ? res.knock_room_state : []
812
+ };
813
+ } catch (err) {
814
+ lastError = err;
815
+ }
816
+ throw lastError;
817
+ }
818
+ /** Ask the invitee's server to sign an invite; returns the countersigned event. */
819
+ async sendInvite(pdu, version, inviteRoomState) {
820
+ this.requireEnabled();
821
+ const server = domainOf(pdu.state_key);
822
+ if (!this.serverAllowed(server)) throw errors.forbidden("Federation with this server is not allowed");
823
+ const res = await this.client.request(server, "PUT", `/_matrix/federation/v2/invite/${encodeURIComponent(pdu.room_id)}/${encodeURIComponent(pdu.event_id)}`, { body: {
824
+ room_version: version,
825
+ event: this.toFederationPdu(pdu, version),
826
+ invite_room_state: inviteRoomState
827
+ } });
828
+ if (!isObject(res.event)) throw errors.unknown("Invalid invite response");
829
+ const signed = {
830
+ ...pdu,
831
+ signatures: {
832
+ ...pdu.signatures,
833
+ ...res.event.signatures
834
+ }
835
+ };
836
+ if (!await this.keyring.verifySignedBy(redact(this.toFederationPdu(signed, version), version), server)) throw errors.forbidden("Invite was not signed by the invitee's server");
837
+ return signed;
838
+ }
839
+ async queryProfile(userId, field) {
840
+ this.requireEnabled();
841
+ return this.client.request(domainOf(userId), "GET", "/_matrix/federation/v1/query/profile", { query: {
842
+ user_id: userId,
843
+ field
844
+ } });
845
+ }
846
+ async queryAlias(alias) {
847
+ this.requireEnabled();
848
+ return this.client.request(domainOf(alias), "GET", "/_matrix/federation/v1/query/directory", { query: { room_alias: alias } });
849
+ }
850
+ async queryKeys(deviceKeys, timeoutMs = 1e4) {
851
+ const byServer = /* @__PURE__ */ new Map();
852
+ for (const [userId, devices] of Object.entries(deviceKeys)) {
853
+ const server = domainOf(userId);
854
+ if (server === this.hs.serverName) continue;
855
+ (byServer.get(server) ?? byServer.set(server, {}).get(server))[userId] = devices;
856
+ }
857
+ const results = [];
858
+ const failures = {};
859
+ await Promise.all([...byServer].map(async ([server, query]) => {
860
+ try {
861
+ if (!this.enabled || !this.serverAllowed(server)) throw new Error("Federation disabled");
862
+ results.push(await this.client.request(server, "POST", "/_matrix/federation/v1/user/keys/query", {
863
+ body: { device_keys: query },
864
+ timeoutMs
865
+ }));
866
+ } catch (err) {
867
+ failures[server] = {
868
+ status: 503,
869
+ message: err instanceof Error ? err.message : String(err)
870
+ };
871
+ }
872
+ }));
873
+ return {
874
+ results,
875
+ failures
876
+ };
877
+ }
878
+ async claimKeys(oneTimeKeys, timeoutMs = 1e4) {
879
+ const byServer = /* @__PURE__ */ new Map();
880
+ for (const [userId, devices] of Object.entries(oneTimeKeys)) {
881
+ const server = domainOf(userId);
882
+ if (server === this.hs.serverName) continue;
883
+ (byServer.get(server) ?? byServer.set(server, {}).get(server))[userId] = devices;
884
+ }
885
+ const results = {};
886
+ const failures = {};
887
+ await Promise.all([...byServer].map(async ([server, query]) => {
888
+ try {
889
+ if (!this.enabled) throw new Error("Federation disabled");
890
+ const res = await this.client.request(server, "POST", "/_matrix/federation/v1/user/keys/claim", {
891
+ body: { one_time_keys: query },
892
+ timeoutMs
893
+ });
894
+ Object.assign(results, res.one_time_keys ?? {});
895
+ } catch (err) {
896
+ failures[server] = {
897
+ status: 503,
898
+ message: err instanceof Error ? err.message : String(err)
899
+ };
900
+ }
901
+ }));
902
+ return {
903
+ results,
904
+ failures
905
+ };
906
+ }
907
+ async queryPublicRooms(server, body) {
908
+ this.requireEnabled();
909
+ return this.client.request(server, "POST", "/_matrix/federation/v1/publicRooms", { body });
910
+ }
911
+ async queryHierarchy(server, roomId, suggestedOnly) {
912
+ this.requireEnabled();
913
+ return this.client.request(server, "GET", `/_matrix/federation/v1/hierarchy/${encodeURIComponent(roomId)}`, { query: { suggested_only: String(suggestedOnly) } });
914
+ }
915
+ /**
916
+ * Fetch history preceding the oldest events we have. Returns the number of new
917
+ * events stored. Caller should not hold the room lock.
918
+ */
919
+ async backfill(roomId, fromEventIds, limit = 50) {
920
+ if (!this.enabled || !fromEventIds.length) return 0;
921
+ const version = await this.hs.rooms.roomVersion(roomId);
922
+ if (!version) return 0;
923
+ const servers = [...this.serversInRoom(await this.hs.rooms.currentState(roomId))];
924
+ for (const server of servers) try {
925
+ const res = await this.client.request(server, "GET", `/_matrix/federation/v1/backfill/${encodeURIComponent(roomId)}`, { query: {
926
+ v: fromEventIds,
927
+ limit: String(limit)
928
+ } });
929
+ const verified = [];
930
+ for (const raw of Array.isArray(res.pdus) ? res.pdus : []) try {
931
+ const { pdu } = await this.verifyPdu(raw, version, roomId);
932
+ if (!await this.hs.rooms.getEvent(pdu.event_id)) verified.push(pdu);
933
+ } catch {}
934
+ if (!verified.length) return 0;
935
+ verified.sort((a, b) => b.depth - a.depth);
936
+ const positions = /* @__PURE__ */ new Map();
937
+ for (const p of verified) positions.set(p.event_id, await this.hs.streams.backfillId());
938
+ verified.reverse();
939
+ let stored = 0;
940
+ await this.hs.rooms.withLock(roomId, async () => {
941
+ for (const pdu of verified) try {
942
+ await this.fetchMissingAuthEvents(server, roomId, version, pdu);
943
+ let stateBefore;
944
+ const prevIds = (pdu.prev_events ?? []).map(eventIdOf);
945
+ const unknown = [];
946
+ for (const id of prevIds) if (await this.hs.rooms.snapshotOf(id) === null) unknown.push(id);
947
+ if (unknown.length) stateBefore = await this.fetchStateBefore(server, roomId, version, pdu.event_id);
948
+ if ((await this.hs.rooms.persistEvent(roomId, version, pdu, {
949
+ backfill: true,
950
+ stateBefore,
951
+ stream: positions.get(pdu.event_id)
952
+ })).status === "persisted") stored++;
953
+ } catch (err) {
954
+ this.hs.logger.debug("Backfilled event failed", pdu.event_id, err);
955
+ }
956
+ });
957
+ return stored;
958
+ } catch (err) {
959
+ this.hs.logger.debug("Backfill from", server, "failed", err);
960
+ }
961
+ return 0;
962
+ }
963
+ /** Download remote media (authenticated federation endpoint, falling back to the legacy one). */
964
+ async fetchMedia(server, mediaId) {
965
+ this.requireEnabled();
966
+ const res = await this.client.requestRaw(server, "GET", `/_matrix/federation/v1/media/download/${encodeURIComponent(mediaId)}`, { timeoutMs: 6e4 });
967
+ if (res.status === 200) {
968
+ const ct = String(res.headers["content-type"] ?? "");
969
+ const m = /boundary=("?)([^";]+)\1/.exec(ct);
970
+ if (ct.startsWith("multipart/") && m) return parseMultipartMedia(res.body, m[2], this);
971
+ }
972
+ const legacy = await this.client.requestRaw(server, "GET", `/_matrix/media/v3/download/${encodeURIComponent(server)}/${encodeURIComponent(mediaId)}`, {
973
+ query: { allow_remote: "false" },
974
+ signed: false,
975
+ timeoutMs: 6e4
976
+ });
977
+ if (legacy.status !== 200) throw errors.notFound("Remote media not found");
978
+ return {
979
+ data: legacy.body,
980
+ contentType: legacy.headers["content-type"] ?? null,
981
+ filename: filenameFromDisposition(legacy.headers["content-disposition"])
982
+ };
983
+ }
984
+ /** Remote members who should receive the invitee's stripped state. */
985
+ strippedStateFor(state) {
986
+ return this.hs.rooms.strippedState(state);
987
+ }
988
+ /** Servers that may be able to help us join `roomId`. */
989
+ candidateServers(roomId, extra = []) {
990
+ const fromId = parseId(roomId, "!")?.server;
991
+ return [.../* @__PURE__ */ new Set([...extra, ...fromId ? [fromId] : []])];
992
+ }
993
+ static memberServers(state) {
994
+ const out = /* @__PURE__ */ new Set();
995
+ for (const [key, ev] of state) {
996
+ const [type, userId] = splitStateKey(key);
997
+ if (type === M.ROOM.MEMBER && ev.content?.membership === "join") out.add(domainOf(userId));
998
+ }
999
+ return [...out];
1000
+ }
1001
+ isMembershipVisibleTo(state, server) {
1002
+ for (const [key] of state) {
1003
+ const [type, userId] = splitStateKey(key);
1004
+ if (type === M.ROOM.MEMBER && domainOf(userId) === server && membershipOf(state, userId) === "join") return true;
1005
+ }
1006
+ return false;
1007
+ }
1008
+ stripped(pdu) {
1009
+ return stripState(pdu);
1010
+ }
1011
+ error(status, errcode, message) {
1012
+ return new MatrixError(status, errcode, message);
1013
+ }
1014
+ };
1015
+ function filenameFromDisposition(header) {
1016
+ if (!header) return null;
1017
+ const star = /filename\*=utf-8''([^;]+)/i.exec(header);
1018
+ if (star) return decodeURIComponent(star[1]);
1019
+ const plain = /filename="?([^";]+)"?/i.exec(header);
1020
+ return plain ? plain[1] : null;
1021
+ }
1022
+ /** Parse a federation media multipart/mixed response: a JSON part then the file (or a redirect). */
1023
+ async function parseMultipartMedia(body, boundary, fed) {
1024
+ const delimiter = Buffer.from(`--${boundary}`);
1025
+ const parts = [];
1026
+ let idx = body.indexOf(delimiter);
1027
+ while (idx !== -1) {
1028
+ const next = body.indexOf(delimiter, idx + delimiter.length);
1029
+ if (next === -1) break;
1030
+ parts.push(body.subarray(idx + delimiter.length, next));
1031
+ idx = next;
1032
+ }
1033
+ const media = parts[1];
1034
+ if (!media) throw errors.notFound("Malformed media response");
1035
+ const headerEnd = media.indexOf("\r\n\r\n");
1036
+ const headerText = media.subarray(0, headerEnd).toString("utf8");
1037
+ const data = media.subarray(headerEnd + 4, media.length - 2);
1038
+ const headers = {};
1039
+ for (const line of headerText.split("\r\n")) {
1040
+ const i = line.indexOf(":");
1041
+ if (i > 0) headers[line.slice(0, i).trim().toLowerCase()] = line.slice(i + 1).trim();
1042
+ }
1043
+ if (headers.location) {
1044
+ const res = await fetch(headers.location, { signal: AbortSignal.timeout(6e4) });
1045
+ if (!res.ok) throw errors.notFound("Remote media redirect failed");
1046
+ return {
1047
+ data: new Uint8Array(await res.arrayBuffer()),
1048
+ contentType: res.headers.get("content-type"),
1049
+ filename: filenameFromDisposition(res.headers.get("content-disposition") ?? void 0)
1050
+ };
1051
+ }
1052
+ return {
1053
+ data: new Uint8Array(data),
1054
+ contentType: headers["content-type"] ?? null,
1055
+ filename: filenameFromDisposition(headers["content-disposition"])
1056
+ };
1057
+ }
1058
+ //#endregion
1059
+ export { FederationService, domainOf, serverAllowedByAcl };