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,826 @@
1
+ import { isObject } from "../core/json.js";
2
+ import { randomToken, signJson, verifyJsonSignature } from "../core/crypto.js";
3
+ import { M } from "../core/m.js";
4
+ import { SUPPORTED_ROOM_VERSIONS, eventIdOf, redact, roomVersionFeatures, splitStateKey, stateKey } from "../core/events.js";
5
+ import { MatrixError, errors } from "../core/errors.js";
6
+ import { isValidUserId } from "../core/ids.js";
7
+ import { authEventKeys, checkAuth, getState, membershipOf, powerLevels, userLevel } from "../core/authrules.js";
8
+ import { inviteDecision } from "../core/invitefilter.js";
9
+ import { parseXMatrix } from "../federation/client.js";
10
+ import { domainOf, serverAllowedByAcl } from "../federation/index.js";
11
+ import { array, looseObject, record, string, unknown } from "../vendor/zod/v4/classic/schemas.js";
12
+ import { body, lenient } from "../http/input.js";
13
+ import { procedure } from "../http/procedure.js";
14
+ import { roomDetails } from "./spaces.js";
15
+ import { fullProfile } from "../core/profile.js";
16
+ import { relationshipsForServer } from "./relationships.js";
17
+ import { canonicalJsonResponse } from "../http/handler.js";
18
+ //#region src/api/federation.ts
19
+ const objectMap = lenient(record(string(), unknown()));
20
+ const keyQueryBody = body({ server_keys: objectMap });
21
+ const missingEventsBody = body({
22
+ earliest_events: lenient(array(string())),
23
+ latest_events: lenient(array(string())),
24
+ limit: unknown().optional(),
25
+ min_depth: unknown().optional()
26
+ });
27
+ const userKeysQueryBody = body({ device_keys: objectMap });
28
+ const userKeysClaimBody = body({ one_time_keys: objectMap });
29
+ const publicRoomsBody = body({
30
+ limit: unknown().optional(),
31
+ filter: lenient(looseObject({ generic_search_term: lenient(string()) }))
32
+ });
33
+ const F = "/_matrix/federation";
34
+ /** Authenticate a server-server request; returns the origin server name. */
35
+ async function authenticate(ctx) {
36
+ const hs = ctx.hs;
37
+ if (!hs.federation.enabled) throw errors.unrecognized("Federation is disabled");
38
+ const header = ctx.request.headers.get("authorization") ?? "";
39
+ const auth = parseXMatrix(header.split(/,\s*(?=X-Matrix)/i)[0]);
40
+ if (!auth) throw new MatrixError(401, "M_UNAUTHORIZED", "Missing or invalid X-Matrix authorization");
41
+ if (auth.destination && auth.destination !== hs.serverName) throw new MatrixError(401, "M_UNAUTHORIZED", "Wrong destination");
42
+ if (!hs.federation.serverAllowed(auth.origin)) throw errors.forbidden("Federation with this server is not allowed");
43
+ const bytes = await ctx.bytes();
44
+ const toSign = {
45
+ method: ctx.method,
46
+ uri: ctx.url.pathname + ctx.url.search,
47
+ origin: auth.origin,
48
+ destination: hs.serverName
49
+ };
50
+ if (bytes.length) toSign.content = await ctx.json();
51
+ const key = await hs.federation.keyring.getKey(auth.origin, auth.key);
52
+ if (!key || !verifyJsonSignature({
53
+ ...toSign,
54
+ signatures: { [auth.origin]: { [auth.key]: auth.sig } }
55
+ }, auth.origin, auth.key, key)) throw new MatrixError(401, "M_UNAUTHORIZED", "Invalid signature");
56
+ return auth.origin;
57
+ }
58
+ async function roomOr404(hs, roomId) {
59
+ const version = await hs.rooms.roomVersion(roomId);
60
+ if (!version) throw errors.notFound("Unknown room");
61
+ return version;
62
+ }
63
+ async function checkAcl(hs, roomId, origin) {
64
+ const state = await hs.rooms.currentState(roomId);
65
+ if (!serverAllowedByAcl(state, origin)) throw errors.forbidden("Server is denied by the room's ACL");
66
+ return state;
67
+ }
68
+ /** Whether `server` may see an event, per history visibility. */
69
+ async function serverCanSee(hs, server, ev) {
70
+ const ids = await hs.rooms.snapshotIds(ev.snapshot);
71
+ const hvId = ids[stateKey(M.ROOM.HISTORY_VISIBILITY, "")];
72
+ const hv = hvId ? (await hs.rooms.getPdu(hvId))?.content?.history_visibility ?? "shared" : "shared";
73
+ if (hv === "world_readable" || hv === "shared") return true;
74
+ for (const [key, id] of Object.entries(ids)) {
75
+ const [type, userId] = splitStateKey(key);
76
+ if (type !== M.ROOM.MEMBER || domainOf(userId) !== server) continue;
77
+ const m = (await hs.rooms.getPdu(id))?.content?.membership;
78
+ if (m === "join" || hv === "invited" && m === "invite") return true;
79
+ }
80
+ return false;
81
+ }
82
+ /** While our state is partial (MSC3902) we cannot vouch for joins or knocks through us. */
83
+ async function rejectIfPartial(hs, roomId) {
84
+ if (await hs.partialState.isPartial(roomId)) throw errors.notFound("This server does not yet have the full state of the room");
85
+ }
86
+ async function authChainOf(hs, ids) {
87
+ const seen = /* @__PURE__ */ new Set();
88
+ const out = [];
89
+ let frontier = ids;
90
+ while (frontier.length) {
91
+ const next = [];
92
+ const pdus = await hs.rooms.getPdus(frontier);
93
+ for (const id of frontier) {
94
+ const pdu = pdus.get(id);
95
+ if (!pdu) continue;
96
+ for (const a of (pdu.auth_events ?? []).map(eventIdOf)) {
97
+ if (seen.has(a)) continue;
98
+ seen.add(a);
99
+ next.push(a);
100
+ }
101
+ }
102
+ const loaded = await hs.rooms.getPdus(next);
103
+ for (const id of next) {
104
+ const p = loaded.get(id);
105
+ if (p) out.push(p);
106
+ }
107
+ frontier = next;
108
+ }
109
+ return out;
110
+ }
111
+ /** State before an event, as event IDs. */
112
+ async function stateBeforeEvent(hs, roomId, version, eventId) {
113
+ const pdu = await hs.rooms.getPdu(eventId);
114
+ if (!pdu || pdu.room_id !== roomId) throw errors.notFound("Unknown event");
115
+ return (await hs.rooms.stateBefore(roomId, version, (pdu.prev_events ?? []).map(eventIdOf))).ids;
116
+ }
117
+ /** Build a membership template for make_join / make_leave / make_knock. */
118
+ async function makeMembership(hs, roomId, version, userId, membership, content = {}) {
119
+ const state = await hs.rooms.currentState(roomId);
120
+ const extremities = await hs.rooms.extremities(roomId);
121
+ const prevs = [...(await hs.rooms.getPdus(extremities)).values()].sort((a, b) => b.depth - a.depth).slice(0, 10);
122
+ const f = roomVersionFeatures(version);
123
+ const draft = {
124
+ type: M.ROOM.MEMBER,
125
+ sender: userId,
126
+ state_key: userId,
127
+ content: {
128
+ membership,
129
+ ...content
130
+ }
131
+ };
132
+ const authIds = authEventKeys(draft, version).map((k) => state.get(k)?.event_id).filter((x) => !!x);
133
+ const ref = (id, pdu) => f.legacyEventIds ? [id, pdu?.hashes ?? {}] : id;
134
+ const authPdus = await hs.rooms.getPdus(authIds);
135
+ const event = {
136
+ type: M.ROOM.MEMBER,
137
+ room_id: roomId,
138
+ sender: userId,
139
+ state_key: userId,
140
+ content: draft.content,
141
+ origin_server_ts: Date.now(),
142
+ depth: Math.min(prevs.reduce((m, p) => Math.max(m, p.depth), 0) + 1, Number.MAX_SAFE_INTEGER),
143
+ prev_events: prevs.map((p) => ref(p.event_id, p)),
144
+ auth_events: authIds.map((id) => ref(id, authPdus.get(id)))
145
+ };
146
+ if (f.legacyEventIds) event.origin = domainOf(userId);
147
+ const authState = new Map(state);
148
+ try {
149
+ checkAuth(event, authState, version, { joinAuthorisedSignatureValid: true });
150
+ } catch (err) {
151
+ throw err instanceof MatrixError ? err : errors.forbidden(String(err));
152
+ }
153
+ return event;
154
+ }
155
+ /** For restricted rooms: a local user who can authorise a remote join, or why not. */
156
+ async function restrictedAuthoriser(hs, state, version, userId) {
157
+ const joinRules = getState(state, M.ROOM.JOIN_RULES)?.content;
158
+ const rule = joinRules?.join_rule;
159
+ const f = roomVersionFeatures(version);
160
+ if (!(rule === "restricted" && f.restricted || rule === "knock_restricted" && f.knockRestricted)) return null;
161
+ const current = membershipOf(state, userId);
162
+ if (current === "join" || current === "invite") return null;
163
+ let allowed = false;
164
+ let unverifiable = false;
165
+ for (const allow of Array.isArray(joinRules?.allow) ? joinRules.allow : []) {
166
+ if (allow?.type !== M.ROOM_MEMBERSHIP || typeof allow.room_id !== "string") continue;
167
+ if (!await hs.rooms.serverIsJoined(allow.room_id)) {
168
+ unverifiable = true;
169
+ continue;
170
+ }
171
+ if (await hs.rooms.membership(allow.room_id, userId) === "join") {
172
+ allowed = true;
173
+ break;
174
+ }
175
+ }
176
+ if (!allowed) {
177
+ if (unverifiable) throw new MatrixError(400, "M_UNABLE_TO_AUTHORISE_JOIN", "Unable to verify the join conditions for this user");
178
+ throw errors.forbidden("You do not satisfy the room's join conditions");
179
+ }
180
+ const pl = powerLevels(state, version);
181
+ for (const [key, ev] of state) {
182
+ const [type, member] = splitStateKey(key);
183
+ if (type === M.ROOM.MEMBER && hs.isLocalUser(member) && ev.content?.membership === "join" && userLevel(pl, member) >= pl.invite) return member;
184
+ }
185
+ throw new MatrixError(400, "M_UNABLE_TO_GRANT_JOIN", "No local user can authorise this join");
186
+ }
187
+ /** Shared send_join / send_leave / send_knock processing. */
188
+ /** Send the invite an m.room.third_party_invite stands for, on behalf of its (local) sender. */
189
+ async function exchangeThirdPartyInvite(hs, roomId, sender, invitee, signed) {
190
+ const token = signed.token;
191
+ if (typeof token !== "string") throw errors.badJson("signed.token is required");
192
+ const pending = getState(await hs.rooms.currentState(roomId), M.ROOM.THIRD_PARTY_INVITE, token);
193
+ if (!pending) throw errors.forbidden("No such third-party invite");
194
+ if (pending.sender !== sender) throw errors.forbidden("The third-party invite was sent by someone else");
195
+ await hs.rooms.invite(roomId, sender, invitee, { thirdPartyInvite: {
196
+ display_name: pending.content?.display_name ?? "",
197
+ signed
198
+ } });
199
+ }
200
+ async function sendMembership(ctx, origin, membership) {
201
+ const hs = ctx.hs;
202
+ const roomId = ctx.params.roomId;
203
+ const version = await roomOr404(hs, roomId);
204
+ await checkAcl(hs, roomId, origin);
205
+ const raw = await ctx.json();
206
+ if (raw.type !== M.ROOM.MEMBER || raw.content?.membership !== membership || raw.state_key !== raw.sender) throw errors.badJson(`Expected a ${membership} event`);
207
+ if (domainOf(raw.sender) !== origin) throw errors.forbidden("Sender does not belong to the origin server");
208
+ let { pdu, joinAuthorisedSignatureValid } = await hs.federation.verifyPdu(raw, version, roomId);
209
+ if (pdu.event_id !== ctx.params.eventId) throw errors.badJson("Event ID mismatch");
210
+ if (membership === "knock" && !roomVersionFeatures(version).knock) throw new MatrixError(400, "M_INCOMPATIBLE_ROOM_VERSION", "Knocking is not supported", { room_version: version });
211
+ const via = pdu.content?.join_authorised_via_users_server;
212
+ if (membership === "join" && typeof via === "string" && hs.isLocalUser(via)) {
213
+ const redacted = signJson(redact({ ...raw }, version), hs.serverName, hs.signingKey);
214
+ pdu = {
215
+ ...pdu,
216
+ signatures: {
217
+ ...pdu.signatures,
218
+ [hs.serverName]: redacted.signatures[hs.serverName]
219
+ }
220
+ };
221
+ raw.signatures = pdu.signatures;
222
+ joinAuthorisedSignatureValid = true;
223
+ }
224
+ const stateBefore = await hs.rooms.withLock(roomId, async () => {
225
+ const before = await hs.rooms.currentStateIds(roomId);
226
+ const result = await hs.rooms.persistEvent(roomId, version, pdu, { joinAuthorisedSignatureValid });
227
+ if (result.status === "rejected") throw errors.forbidden(result.reason ?? "Event not allowed");
228
+ return before;
229
+ });
230
+ const others = hs.federation.serversInRoom(await hs.rooms.currentState(roomId));
231
+ others.delete(origin);
232
+ if (others.size) await hs.federation.sender.sendPdu(others, hs.federation.toFederationPdu(pdu, version));
233
+ return {
234
+ pdu,
235
+ stateBefore,
236
+ version
237
+ };
238
+ }
239
+ function registerFederationRoutes(r) {
240
+ const fed = (method, path, def) => {
241
+ r.add(method, `${F}${path}`, procedure({
242
+ input: def.input,
243
+ guard: async (ctx) => ({ origin: await authenticate(ctx) }),
244
+ handler: (ctx) => def.handler(ctx, ctx.origin)
245
+ }));
246
+ };
247
+ const notary = async (ctx, servers) => {
248
+ const out = [];
249
+ for (const server of servers) {
250
+ if (server === ctx.hs.serverName) {
251
+ const key = ctx.hs.signingKey;
252
+ out.push(signJson({
253
+ server_name: ctx.hs.serverName,
254
+ valid_until_ts: Date.now() + 864e5,
255
+ verify_keys: { [`ed25519:${key.keyId}`]: { key: key.publicKeyBase64 } },
256
+ old_verify_keys: {}
257
+ }, ctx.hs.serverName, key));
258
+ continue;
259
+ }
260
+ try {
261
+ const fresh = await ctx.hs.federation.client.request(server, "GET", "/_matrix/key/v2/server", {
262
+ signed: false,
263
+ timeoutMs: 1e4
264
+ });
265
+ await ctx.hs.federation.keyring.ingestServerKeys(server, fresh);
266
+ if (fresh.server_name === server) out.push(signJson(fresh, ctx.hs.serverName, ctx.hs.signingKey));
267
+ } catch {}
268
+ }
269
+ return { server_keys: out };
270
+ };
271
+ const requireFederation = (ctx) => {
272
+ if (!ctx.hs.federation.enabled) throw errors.unrecognized("Federation is disabled");
273
+ };
274
+ r.post("/_matrix/key/v2/query", procedure({
275
+ input: keyQueryBody,
276
+ handler: async (ctx) => {
277
+ requireFederation(ctx);
278
+ return canonicalJsonResponse(await notary(ctx, Object.keys(ctx.input.server_keys ?? {})));
279
+ }
280
+ }));
281
+ r.get("/_matrix/key/v2/query/:serverName", procedure({ handler: async (ctx) => {
282
+ requireFederation(ctx);
283
+ return canonicalJsonResponse(await notary(ctx, [ctx.params.serverName]));
284
+ } }));
285
+ fed("PUT", "/v1/send/:txnId", { handler: async (ctx, origin) => {
286
+ const body = await ctx.json();
287
+ if (body.origin !== void 0 && body.origin !== origin) throw errors.forbidden("Transaction origin mismatch");
288
+ return ctx.hs.federation.receiveTransaction(origin, ctx.params.txnId, body);
289
+ } });
290
+ fed("GET", "/v1/make_join/:roomId/:userId", { handler: async (ctx, origin) => {
291
+ const hs = ctx.hs;
292
+ const { roomId, userId } = ctx.params;
293
+ if (domainOf(userId) !== origin) throw errors.forbidden("User does not belong to the origin server");
294
+ const version = await roomOr404(hs, roomId);
295
+ await rejectIfPartial(hs, roomId);
296
+ const supported = ctx.query.getAll("ver");
297
+ if (!(supported.length ? supported : ["1"]).includes(version)) throw new MatrixError(400, "M_INCOMPATIBLE_ROOM_VERSION", "Your server does not support this room version", { room_version: version });
298
+ const state = await checkAcl(hs, roomId, origin);
299
+ if (FederationJoinGuard.noLocalMembers(hs, state)) throw errors.notFound("This server is not in the room");
300
+ const via = await restrictedAuthoriser(hs, state, version, userId);
301
+ return {
302
+ event: await makeMembership(hs, roomId, version, userId, "join", via ? { join_authorised_via_users_server: via } : {}),
303
+ room_version: version
304
+ };
305
+ } });
306
+ const sendJoin = async (ctx, origin) => {
307
+ const hs = ctx.hs;
308
+ await rejectIfPartial(hs, ctx.params.roomId);
309
+ const { pdu, stateBefore, version } = await sendMembership(ctx, origin, "join");
310
+ const statePdus = [...(await hs.rooms.getPdus(Object.values(stateBefore))).values()];
311
+ const chain = await authChainOf(hs, [...Object.values(stateBefore), pdu.event_id]);
312
+ const toWire = (p) => hs.federation.toFederationPdu(p, version);
313
+ const servers = [.../* @__PURE__ */ new Set([hs.serverName, ...hs.federation.serversInRoom(await hs.rooms.currentState(ctx.params.roomId))])];
314
+ return {
315
+ origin: hs.serverName,
316
+ event: toWire(pdu),
317
+ state: statePdus.map(toWire),
318
+ auth_chain: chain.map(toWire),
319
+ members_omitted: false,
320
+ servers_in_room: servers
321
+ };
322
+ };
323
+ fed("PUT", "/v2/send_join/:roomId/:eventId", { handler: sendJoin });
324
+ fed("PUT", "/v1/send_join/:roomId/:eventId", { handler: async (ctx, origin) => [200, await sendJoin(ctx, origin)] });
325
+ fed("GET", "/v1/make_leave/:roomId/:userId", { handler: async (ctx, origin) => {
326
+ const { roomId, userId } = ctx.params;
327
+ if (domainOf(userId) !== origin) throw errors.forbidden("User does not belong to the origin server");
328
+ const version = await roomOr404(ctx.hs, roomId);
329
+ await checkAcl(ctx.hs, roomId, origin);
330
+ return {
331
+ event: await makeMembership(ctx.hs, roomId, version, userId, "leave"),
332
+ room_version: version
333
+ };
334
+ } });
335
+ fed("PUT", "/v2/send_leave/:roomId/:eventId", { handler: async (ctx, origin) => {
336
+ await sendMembership(ctx, origin, "leave");
337
+ return {};
338
+ } });
339
+ fed("PUT", "/v1/send_leave/:roomId/:eventId", { handler: async (ctx, origin) => {
340
+ await sendMembership(ctx, origin, "leave");
341
+ return [200, {}];
342
+ } });
343
+ fed("GET", "/v1/make_knock/:roomId/:userId", { handler: async (ctx, origin) => {
344
+ const { roomId, userId } = ctx.params;
345
+ if (domainOf(userId) !== origin) throw errors.forbidden("User does not belong to the origin server");
346
+ const version = await roomOr404(ctx.hs, roomId);
347
+ await rejectIfPartial(ctx.hs, roomId);
348
+ const supported = ctx.query.getAll("ver");
349
+ if (!(supported.length ? supported : ["1"]).includes(version) || !roomVersionFeatures(version).knock) throw new MatrixError(400, "M_INCOMPATIBLE_ROOM_VERSION", "Your server does not support this room version", { room_version: version });
350
+ await checkAcl(ctx.hs, roomId, origin);
351
+ return {
352
+ event: await makeMembership(ctx.hs, roomId, version, userId, "knock"),
353
+ room_version: version
354
+ };
355
+ } });
356
+ fed("PUT", "/v1/send_knock/:roomId/:eventId", { handler: async (ctx, origin) => {
357
+ await rejectIfPartial(ctx.hs, ctx.params.roomId);
358
+ await sendMembership(ctx, origin, "knock");
359
+ return { knock_room_state: ctx.hs.rooms.strippedState(await ctx.hs.rooms.currentState(ctx.params.roomId)) };
360
+ } });
361
+ const invite = async (ctx, origin, v2) => {
362
+ const hs = ctx.hs;
363
+ const body = await ctx.json();
364
+ const raw = v2 ? body.event : body;
365
+ const version = v2 ? String(body.room_version ?? "1") : "1";
366
+ if (!SUPPORTED_ROOM_VERSIONS.includes(version)) throw new MatrixError(400, "M_INCOMPATIBLE_ROOM_VERSION", "Unsupported room version", { room_version: version });
367
+ if (!isObject(raw) || raw.type !== M.ROOM.MEMBER || raw.content?.membership !== "invite" || typeof raw.state_key !== "string") throw errors.badJson("Expected an invite event");
368
+ if (raw.room_id !== ctx.params.roomId) throw errors.badJson("Room ID mismatch");
369
+ if (domainOf(raw.sender) !== origin) throw errors.forbidden("Sender does not belong to the origin server");
370
+ const target = raw.state_key;
371
+ if (!hs.isLocalUser(target)) throw errors.forbidden("User is not local");
372
+ const user = await hs.db.findOne("users", [{
373
+ field: "user_id",
374
+ value: target
375
+ }]);
376
+ if (!user || user.deactivated) throw errors.notFound("Unknown user");
377
+ if (await hs.rooms.exists(raw.room_id)) await checkAcl(hs, raw.room_id, origin);
378
+ const { pdu } = await hs.federation.verifyPdu(raw, version, raw.room_id);
379
+ if (pdu.event_id !== ctx.params.eventId) throw errors.badJson("Event ID mismatch");
380
+ if (await inviteDecision(hs, target, raw.sender) === "block") throw errors.forbidden("This user does not accept invites from you");
381
+ const ignored = await hs.db.findOne("account_data", [
382
+ {
383
+ field: "user_id",
384
+ value: target
385
+ },
386
+ {
387
+ field: "room_id",
388
+ value: ""
389
+ },
390
+ {
391
+ field: "type",
392
+ value: M.IGNORED_USER_LIST
393
+ }
394
+ ]);
395
+ const signedRedacted = signJson(redact({ ...raw }, version), hs.serverName, hs.signingKey);
396
+ const signatures = {
397
+ ...pdu.signatures,
398
+ [hs.serverName]: signedRedacted.signatures[hs.serverName]
399
+ };
400
+ const inviteState = Array.isArray(body.invite_room_state) ? body.invite_room_state : Array.isArray(raw.unsigned?.invite_room_state) ? raw.unsigned.invite_room_state : [];
401
+ const createInState = inviteState.find((e) => e?.type === M.ROOM.CREATE);
402
+ if (v2 && !createInState && roomVersionFeatures(version).v12) throw errors.missingParam("invite_room_state must include the m.room.create event");
403
+ if (createInState && roomVersionFeatures(version).v12 && createInState.event_id === void 0) try {
404
+ if (hs.federation.computeEventId(createInState, version) !== `$${raw.room_id.slice(1)}`) throw new Error("mismatch");
405
+ } catch {
406
+ throw errors.invalidParam("invite_room_state contains a create event for another room");
407
+ }
408
+ const stored = {
409
+ ...pdu,
410
+ signatures,
411
+ unsigned: {
412
+ ...pdu.unsigned,
413
+ invite_room_state: inviteState
414
+ }
415
+ };
416
+ if (!ignored?.content?.ignored_users?.[raw.sender]) await hs.rooms.recordRemoteMembership(stored, version);
417
+ const wire = hs.federation.toFederationPdu({
418
+ ...pdu,
419
+ signatures
420
+ }, version);
421
+ return v2 ? { event: wire } : [200, { event: wire }];
422
+ };
423
+ fed("PUT", "/v2/invite/:roomId/:eventId", { handler: (ctx, origin) => invite(ctx, origin, true) });
424
+ fed("PUT", "/v1/invite/:roomId/:eventId", { handler: (ctx, origin) => invite(ctx, origin, false) });
425
+ fed("GET", "/v1/event/:eventId", { handler: async (ctx, origin) => {
426
+ const hs = ctx.hs;
427
+ const ev = await hs.rooms.getEvent(ctx.params.eventId);
428
+ const pdu = ev?.pdu ?? await hs.rooms.getPdu(ctx.params.eventId);
429
+ if (!pdu) throw errors.notFound("Unknown event");
430
+ const version = await roomOr404(hs, pdu.room_id);
431
+ await checkAcl(hs, pdu.room_id, origin);
432
+ if (ev && !await serverCanSee(hs, origin, ev)) throw errors.forbidden("You may not see this event");
433
+ return {
434
+ origin: hs.serverName,
435
+ origin_server_ts: Date.now(),
436
+ pdus: [hs.federation.toFederationPdu(pdu, version)]
437
+ };
438
+ } });
439
+ const stateHandler = (idsOnly) => async (ctx, origin) => {
440
+ const hs = ctx.hs;
441
+ const roomId = ctx.params.roomId;
442
+ const version = await roomOr404(hs, roomId);
443
+ if (await hs.partialState.isPartial(roomId)) throw errors.forbidden("This server only has partial state for the room");
444
+ const state = await checkAcl(hs, roomId, origin);
445
+ if (!FederationJoinGuard.serverInRoom(state, origin)) throw errors.forbidden("Your server is not in the room");
446
+ const eventId = ctx.query.get("event_id");
447
+ if (!eventId) throw errors.missingParam("Missing event_id");
448
+ const before = await stateBeforeEvent(hs, roomId, version, eventId);
449
+ const chain = await authChainOf(hs, Object.values(before));
450
+ if (idsOnly) return {
451
+ pdu_ids: Object.values(before),
452
+ auth_chain_ids: chain.map((p) => p.event_id)
453
+ };
454
+ return {
455
+ pdus: [...(await hs.rooms.getPdus(Object.values(before))).values()].map((p) => hs.federation.toFederationPdu(p, version)),
456
+ auth_chain: chain.map((p) => hs.federation.toFederationPdu(p, version))
457
+ };
458
+ };
459
+ fed("GET", "/v1/state/:roomId", { handler: stateHandler(false) });
460
+ fed("GET", "/v1/state_ids/:roomId", { handler: stateHandler(true) });
461
+ fed("GET", "/v1/event_auth/:roomId/:eventId", { handler: async (ctx, origin) => {
462
+ const hs = ctx.hs;
463
+ const version = await roomOr404(hs, ctx.params.roomId);
464
+ await checkAcl(hs, ctx.params.roomId, origin);
465
+ return { auth_chain: (await authChainOf(hs, [ctx.params.eventId])).map((p) => hs.federation.toFederationPdu(p, version)) };
466
+ } });
467
+ fed("GET", "/v1/backfill/:roomId", { handler: async (ctx, origin) => {
468
+ const hs = ctx.hs;
469
+ const roomId = ctx.params.roomId;
470
+ const version = await roomOr404(hs, roomId);
471
+ const state = await checkAcl(hs, roomId, origin);
472
+ if (!FederationJoinGuard.serverInRoom(state, origin)) throw errors.forbidden("Your server is not in the room");
473
+ const limit = Math.min(Math.max(Number(ctx.query.get("limit") ?? 10) || 10, 1), 100);
474
+ const out = [];
475
+ const seen = /* @__PURE__ */ new Set();
476
+ let frontier = ctx.query.getAll("v");
477
+ while (frontier.length && out.length < limit) {
478
+ const next = [];
479
+ for (const id of frontier) {
480
+ if (seen.has(id) || out.length >= limit) continue;
481
+ seen.add(id);
482
+ const ev = await hs.rooms.getEvent(id);
483
+ if (!ev || ev.pdu.room_id !== roomId) continue;
484
+ out.push(await serverCanSee(hs, origin, ev) ? ev.pdu : redact(ev.pdu, version));
485
+ next.push(...(ev.pdu.prev_events ?? []).map(eventIdOf));
486
+ }
487
+ frontier = next;
488
+ }
489
+ return {
490
+ origin: hs.serverName,
491
+ origin_server_ts: Date.now(),
492
+ pdus: out.map((p) => hs.federation.toFederationPdu(p, version))
493
+ };
494
+ } });
495
+ fed("POST", "/v1/get_missing_events/:roomId", {
496
+ input: missingEventsBody,
497
+ handler: async (ctx, origin) => {
498
+ const hs = ctx.hs;
499
+ const roomId = ctx.params.roomId;
500
+ const version = await roomOr404(hs, roomId);
501
+ const state = await checkAcl(hs, roomId, origin);
502
+ if (!FederationJoinGuard.serverInRoom(state, origin)) throw errors.forbidden("Your server is not in the room");
503
+ const body = ctx.input;
504
+ const earliest = new Set(body.earliest_events ?? []);
505
+ const limit = Math.min(Number(body.limit ?? 10) || 10, 100);
506
+ const minDepth = Number(body.min_depth ?? 0) || 0;
507
+ const out = [];
508
+ const seen = new Set(body.latest_events ?? []);
509
+ let frontier = [];
510
+ for (const id of seen) {
511
+ const ev = await hs.rooms.getEvent(id);
512
+ if (ev) frontier.push(...(ev.pdu.prev_events ?? []).map(eventIdOf));
513
+ }
514
+ while (frontier.length && out.length < limit) {
515
+ const next = [];
516
+ for (const id of frontier) {
517
+ if (seen.has(id) || earliest.has(id) || out.length >= limit) continue;
518
+ seen.add(id);
519
+ const ev = await hs.rooms.getEvent(id);
520
+ if (!ev || ev.pdu.depth < minDepth) continue;
521
+ out.push(await serverCanSee(hs, origin, ev) ? ev.pdu : redact(ev.pdu, version));
522
+ next.push(...(ev.pdu.prev_events ?? []).map(eventIdOf));
523
+ }
524
+ frontier = next;
525
+ }
526
+ out.sort((a, b) => a.depth - b.depth);
527
+ return { events: out.map((p) => hs.federation.toFederationPdu(p, version)) };
528
+ }
529
+ });
530
+ fed("GET", "/v1/timestamp_to_event/:roomId", { handler: async (ctx, origin) => {
531
+ const hs = ctx.hs;
532
+ const roomId = ctx.params.roomId;
533
+ await roomOr404(hs, roomId);
534
+ await checkAcl(hs, roomId, origin);
535
+ const ts = Number(ctx.query.get("ts"));
536
+ const dir = ctx.query.get("dir");
537
+ if (!Number.isFinite(ts) || dir !== "f" && dir !== "b") throw errors.invalidParam("Invalid ts or dir");
538
+ const rows = await hs.db.findMany("events", {
539
+ where: [{
540
+ field: "room_id",
541
+ value: roomId
542
+ }],
543
+ sortBy: {
544
+ field: "stream",
545
+ direction: dir === "f" ? "asc" : "desc"
546
+ }
547
+ });
548
+ for (const row of rows) {
549
+ const t = row.json.origin_server_ts;
550
+ if (dir === "f" ? t >= ts : t <= ts) return {
551
+ event_id: row.event_id,
552
+ origin_server_ts: t
553
+ };
554
+ }
555
+ throw errors.notFound("No event found");
556
+ } });
557
+ fed("GET", "/v1/query/directory", { handler: async (ctx) => {
558
+ const alias = ctx.query.get("room_alias");
559
+ if (!alias) throw errors.missingParam("Missing room_alias");
560
+ const row = await ctx.hs.db.findOne("room_aliases", [{
561
+ field: "alias",
562
+ value: alias
563
+ }]);
564
+ if (!row) throw errors.notFound("Room alias not found");
565
+ const servers = [ctx.hs.serverName, ...ctx.hs.federation.serversInRoom(await ctx.hs.rooms.currentState(row.room_id))];
566
+ return {
567
+ room_id: row.room_id,
568
+ servers: [...new Set(servers)]
569
+ };
570
+ } });
571
+ fed("GET", "/v1/query/profile", { handler: async (ctx) => {
572
+ const userId = ctx.query.get("user_id");
573
+ if (!userId) throw errors.missingParam("Missing user_id");
574
+ if (!isValidUserId(userId)) throw errors.invalidParam("Invalid user ID");
575
+ if (!ctx.hs.isLocalUser(userId)) throw errors.notFound("Unknown user");
576
+ const profile = await fullProfile(ctx.hs, userId);
577
+ if (!profile) throw errors.notFound("Unknown user");
578
+ const field = ctx.query.get("field");
579
+ if (!field) return profile;
580
+ if (profile[field] === void 0) throw errors.notFound("Profile field not set");
581
+ return { [field]: profile[field] };
582
+ } });
583
+ fed("POST", "/unstable/event_relationships", {
584
+ input: body({}),
585
+ handler: async (ctx, origin) => relationshipsForServer(ctx.hs, origin, ctx.input, (ids) => authChainOf(ctx.hs, ids))
586
+ });
587
+ fed("POST", "/v1/user/keys/query", {
588
+ input: userKeysQueryBody,
589
+ handler: async (ctx) => {
590
+ const local = {};
591
+ for (const [userId, devices] of Object.entries(ctx.input.device_keys ?? {})) if (ctx.hs.isLocalUser(userId)) local[userId] = Array.isArray(devices) ? devices : [];
592
+ const res = await ctx.hs.keys.query("", local);
593
+ return {
594
+ device_keys: res.device_keys,
595
+ master_keys: res.master_keys,
596
+ self_signing_keys: res.self_signing_keys
597
+ };
598
+ }
599
+ });
600
+ fed("POST", "/v1/user/keys/claim", {
601
+ input: userKeysClaimBody,
602
+ handler: async (ctx) => {
603
+ const local = {};
604
+ for (const [userId, devices] of Object.entries(ctx.input.one_time_keys ?? {})) if (ctx.hs.isLocalUser(userId) && isObject(devices)) local[userId] = devices;
605
+ return { one_time_keys: await ctx.hs.keys.claim(local) };
606
+ }
607
+ });
608
+ fed("GET", "/v1/user/devices/:userId", { handler: async (ctx) => {
609
+ const hs = ctx.hs;
610
+ const userId = ctx.params.userId;
611
+ if (!hs.isLocalUser(userId)) throw errors.forbidden("User is not local");
612
+ const devices = await hs.db.findMany("devices", { where: [{
613
+ field: "user_id",
614
+ value: userId
615
+ }] });
616
+ const keys = await hs.keys.query("", { [userId]: [] });
617
+ const out = {
618
+ user_id: userId,
619
+ stream_id: (await hs.db.findOne("device_list_changes", [{
620
+ field: "user_id",
621
+ value: userId
622
+ }]))?.stream ?? 0,
623
+ devices: devices.map((d) => ({
624
+ device_id: d.device_id,
625
+ keys: keys.device_keys[userId]?.[d.device_id],
626
+ ...d.display_name ? { device_display_name: d.display_name } : {}
627
+ }))
628
+ };
629
+ if (keys.master_keys[userId]) out.master_key = keys.master_keys[userId];
630
+ if (keys.self_signing_keys[userId]) out.self_signing_key = keys.self_signing_keys[userId];
631
+ return out;
632
+ } });
633
+ const publicRooms = async (ctx) => {
634
+ const hs = ctx.hs;
635
+ const body = (ctx.method === "POST" ? ctx.input : void 0) ?? {};
636
+ const limit = Number(body.limit ?? ctx.query.get("limit") ?? 0) || void 0;
637
+ const rooms = await hs.db.findMany("rooms", { where: [{
638
+ field: "is_public",
639
+ value: true
640
+ }] });
641
+ let chunk = await Promise.all(rooms.map((r) => roomDetails(hs, r.room_id)));
642
+ const term = isObject(body.filter) && typeof body.filter.generic_search_term === "string" ? body.filter.generic_search_term.toLowerCase() : null;
643
+ if (term) chunk = chunk.filter((c) => [
644
+ c.name,
645
+ c.topic,
646
+ c.canonical_alias
647
+ ].some((v) => typeof v === "string" && v.toLowerCase().includes(term)));
648
+ chunk.sort((a, b) => b.num_joined_members - a.num_joined_members);
649
+ const since = Number.parseInt(String(body.since ?? ctx.query.get("since") ?? "0").replace(/^o/, ""), 10) || 0;
650
+ const out = {
651
+ chunk: limit ? chunk.slice(since, since + limit) : chunk.slice(since),
652
+ total_room_count_estimate: chunk.length
653
+ };
654
+ if (limit && since + limit < chunk.length) out.next_batch = `o${since + limit}`;
655
+ return out;
656
+ };
657
+ fed("GET", "/v1/publicRooms", { handler: (ctx) => publicRooms(ctx) });
658
+ fed("POST", "/v1/publicRooms", {
659
+ input: publicRoomsBody,
660
+ handler: (ctx) => publicRooms(ctx)
661
+ });
662
+ fed("GET", "/v1/hierarchy/:roomId", { handler: async (ctx, origin) => {
663
+ const hs = ctx.hs;
664
+ const roomId = ctx.params.roomId;
665
+ await roomOr404(hs, roomId);
666
+ const state = await checkAcl(hs, roomId, origin);
667
+ const suggestedOnly = ctx.query.get("suggested_only") === "true";
668
+ const summary = async (id, s) => {
669
+ const details = await roomDetails(hs, id);
670
+ const jr = getState(s, M.ROOM.JOIN_RULES)?.content;
671
+ if (["restricted", "knock_restricted"].includes(jr?.join_rule)) details.allowed_room_ids = (Array.isArray(jr?.allow) ? jr.allow : []).filter((a) => a?.type === M.ROOM_MEMBERSHIP).map((a) => a.room_id);
672
+ return details;
673
+ };
674
+ const accessible = async (s) => {
675
+ const jr = getState(s, M.ROOM.JOIN_RULES)?.content;
676
+ const rule = jr?.join_rule ?? "invite";
677
+ const hv = getState(s, M.ROOM.HISTORY_VISIBILITY)?.content?.history_visibility;
678
+ if ([
679
+ "public",
680
+ "knock",
681
+ "knock_restricted"
682
+ ].includes(rule) || hv === "world_readable" || FederationJoinGuard.serverInRoom(s, origin)) return true;
683
+ if (rule !== "restricted") return false;
684
+ for (const allow of Array.isArray(jr?.allow) ? jr.allow : []) {
685
+ if (allow?.type !== M.ROOM_MEMBERSHIP || typeof allow.room_id !== "string" || !await hs.rooms.exists(allow.room_id)) continue;
686
+ if (FederationJoinGuard.serverInRoom(await hs.rooms.currentState(allow.room_id), origin)) return true;
687
+ }
688
+ return false;
689
+ };
690
+ if (!await accessible(state)) throw errors.notFound("Room is not accessible");
691
+ const children = [];
692
+ const childrenState = [];
693
+ const inaccessible = [];
694
+ for (const [key, ev] of state) {
695
+ const [type, childId] = splitStateKey(key);
696
+ if (type !== M.SPACE.CHILD || !Array.isArray(ev.content?.via) || !ev.content.via.length) continue;
697
+ if (suggestedOnly && ev.content.suggested !== true) continue;
698
+ childrenState.push({
699
+ type,
700
+ state_key: childId,
701
+ sender: ev.sender,
702
+ content: ev.content,
703
+ origin_server_ts: ev.origin_server_ts
704
+ });
705
+ if (!await hs.rooms.exists(childId)) continue;
706
+ const childState = await hs.rooms.currentState(childId);
707
+ if (!await accessible(childState)) {
708
+ inaccessible.push(childId);
709
+ continue;
710
+ }
711
+ const childSummary = await summary(childId, childState);
712
+ childSummary.children_state = [];
713
+ children.push(childSummary);
714
+ }
715
+ const room = await summary(roomId, state);
716
+ room.children_state = childrenState;
717
+ return {
718
+ room,
719
+ children,
720
+ inaccessible_children: inaccessible
721
+ };
722
+ } });
723
+ const mediaHandler = async (ctx) => {
724
+ const hs = ctx.hs;
725
+ const meta = await hs.db.findOne("media", [{
726
+ field: "media_id",
727
+ value: ctx.params.mediaId
728
+ }]);
729
+ if (!meta || meta.pending) throw errors.notFound("Media not found");
730
+ const data = await hs.media.get(ctx.params.mediaId);
731
+ if (!data) throw errors.notFound("Media not found");
732
+ const boundary = randomToken(16);
733
+ const head = `--${boundary}\r\nContent-Type: application/json\r\n\r\n{}\r\n--${boundary}\r\nContent-Type: ${meta.content_type ?? "application/octet-stream"}\r\n` + (meta.filename ? `Content-Disposition: attachment; filename*=utf-8''${encodeURIComponent(meta.filename)}\r\n` : "") + "\r\n";
734
+ const body = Buffer.concat([
735
+ Buffer.from(head),
736
+ Buffer.from(data),
737
+ Buffer.from(`\r\n--${boundary}--\r\n`)
738
+ ]);
739
+ return new Response(body, {
740
+ status: 200,
741
+ headers: { "content-type": `multipart/mixed; boundary=${boundary}` }
742
+ });
743
+ };
744
+ fed("GET", "/v1/media/download/:mediaId", { handler: (ctx) => mediaHandler(ctx) });
745
+ fed("GET", "/v1/media/thumbnail/:mediaId", { handler: (ctx) => mediaHandler(ctx) });
746
+ r.get(`${F}/v1/openid/userinfo`, procedure({ handler: async (ctx) => {
747
+ const token = ctx.query.get("access_token");
748
+ const userId = token ? await ctx.hs.cluster.get(`openid:${token}`) : null;
749
+ if (!userId) throw new MatrixError(401, "M_UNKNOWN_TOKEN", "Invalid OpenID token");
750
+ return { sub: userId };
751
+ } }));
752
+ r.client("POST", "/user/:userId/openid/request_token", procedure({
753
+ auth: "user",
754
+ handler: async (ctx) => {
755
+ const s = ctx.session;
756
+ if (ctx.params.userId !== s.userId) throw errors.forbidden("Cannot request tokens for other users");
757
+ const token = randomToken();
758
+ await ctx.hs.cluster.set(`openid:${token}`, s.userId, 36e5);
759
+ return {
760
+ access_token: token,
761
+ token_type: "Bearer",
762
+ matrix_server_name: ctx.hs.serverName,
763
+ expires_in: 3600
764
+ };
765
+ }
766
+ }));
767
+ fed("PUT", "/v1/exchange_third_party_invite/:roomId", { handler: async (ctx, origin) => {
768
+ const hs = ctx.hs;
769
+ const roomId = ctx.params.roomId;
770
+ const event = await ctx.json();
771
+ const signed = event.content?.third_party_invite?.signed;
772
+ if (event.type !== M.ROOM.MEMBER || event.room_id !== roomId || event.content?.membership !== "invite" || !isObject(signed)) throw errors.badJson("Expected an m.room.member invite with third_party_invite");
773
+ if (typeof event.sender !== "string" || !hs.isLocalUser(event.sender)) throw errors.forbidden("The sender is not a user of this server");
774
+ if (typeof event.state_key !== "string" || domainOf(event.state_key) !== origin || signed.mxid !== event.state_key) throw errors.forbidden("The invitee does not belong to the requesting server");
775
+ await roomOr404(hs, roomId);
776
+ await checkAcl(hs, roomId, origin);
777
+ await exchangeThirdPartyInvite(hs, roomId, event.sender, event.state_key, signed);
778
+ return {};
779
+ } });
780
+ r.put(`${F}/v1/3pid/onbind`, procedure({ handler: async (ctx) => {
781
+ const hs = ctx.hs;
782
+ const body = await ctx.json();
783
+ const invites = Array.isArray(body.invites) ? body.invites : [];
784
+ for (const invite of invites) {
785
+ const signed = invite?.signed;
786
+ if (!isObject(signed) || typeof signed.mxid !== "string" || !hs.isLocalUser(signed.mxid)) continue;
787
+ if (typeof invite.room_id !== "string" || typeof invite.sender !== "string") continue;
788
+ const event = {
789
+ type: M.ROOM.MEMBER,
790
+ room_id: invite.room_id,
791
+ sender: invite.sender,
792
+ state_key: signed.mxid,
793
+ content: {
794
+ membership: "invite",
795
+ third_party_invite: { signed }
796
+ }
797
+ };
798
+ try {
799
+ if (hs.isLocalUser(invite.sender)) await exchangeThirdPartyInvite(hs, invite.room_id, invite.sender, signed.mxid, signed);
800
+ else await hs.federation.client.request(domainOf(invite.sender), "PUT", `/_matrix/federation/v1/exchange_third_party_invite/${encodeURIComponent(invite.room_id)}`, { body: event });
801
+ } catch (err) {
802
+ hs.logger.warn("Could not exchange third-party invite to", invite.room_id, "for", signed.mxid, err);
803
+ }
804
+ }
805
+ return {};
806
+ } }));
807
+ }
808
+ /** Helpers for "is a server in the room" checks. */
809
+ const FederationJoinGuard = {
810
+ serverInRoom(state, server) {
811
+ for (const [key, ev] of state) {
812
+ const [type, userId] = splitStateKey(key);
813
+ if (type === M.ROOM.MEMBER && domainOf(userId) === server && ev.content?.membership === "join") return true;
814
+ }
815
+ return false;
816
+ },
817
+ noLocalMembers(hs, state) {
818
+ for (const [key, ev] of state) {
819
+ const [type, userId] = splitStateKey(key);
820
+ if (type === M.ROOM.MEMBER && hs.isLocalUser(userId) && ev.content?.membership === "join") return false;
821
+ }
822
+ return true;
823
+ }
824
+ };
825
+ //#endregion
826
+ export { registerFederationRoutes };