matrix-nodejs-homeserver 0.2.0 → 0.3.2

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 (99) hide show
  1. package/README.md +89 -11
  2. package/dist/adapters/cluster.d.ts +1 -26
  3. package/dist/adapters/cluster.js +50 -103
  4. package/dist/api/account.js +407 -354
  5. package/dist/api/admin.js +82 -61
  6. package/dist/api/appservice.js +37 -25
  7. package/dist/api/backup.js +73 -62
  8. package/dist/api/federation.js +172 -138
  9. package/dist/api/filters.js +4 -3
  10. package/dist/api/legacy.js +91 -78
  11. package/dist/api/media.js +71 -55
  12. package/dist/api/misc.js +46 -37
  13. package/dist/api/preview.js +37 -73
  14. package/dist/api/profile.js +108 -97
  15. package/dist/api/push.js +114 -99
  16. package/dist/api/relationships.js +64 -58
  17. package/dist/api/rooms.js +703 -580
  18. package/dist/api/rtc.d.ts +0 -1
  19. package/dist/api/rtc.js +48 -34
  20. package/dist/api/search.js +133 -127
  21. package/dist/api/slidingsync.js +41 -31
  22. package/dist/api/spaces.js +82 -77
  23. package/dist/api/sync.js +325 -135
  24. package/dist/api/threadsubs.js +72 -61
  25. package/dist/api/threepid.js +99 -66
  26. package/dist/api/uia.js +17 -16
  27. package/dist/api/user.js +221 -178
  28. package/dist/core/admin.d.ts +0 -1
  29. package/dist/core/admin.js +6 -5
  30. package/dist/core/adminroom.js +9 -8
  31. package/dist/core/appservices.d.ts +2 -1
  32. package/dist/core/appservices.js +5 -4
  33. package/dist/core/audit.d.ts +0 -1
  34. package/dist/core/authrules.js +26 -25
  35. package/dist/core/cluster.d.ts +12 -5
  36. package/dist/core/cluster.js +46 -29
  37. package/dist/core/delayed.js +2 -1
  38. package/dist/core/events.js +11 -10
  39. package/dist/core/html.js +13 -0
  40. package/dist/core/identity.js +1 -1
  41. package/dist/core/invitefilter.js +2 -1
  42. package/dist/core/json.js +10 -1
  43. package/dist/core/keys.d.ts +10 -1
  44. package/dist/core/keys.js +62 -42
  45. package/dist/core/m.d.ts +209 -0
  46. package/dist/core/m.js +182 -0
  47. package/dist/core/outbound.d.ts +16 -0
  48. package/dist/core/outbound.js +172 -0
  49. package/dist/core/partialstate.js +2 -1
  50. package/dist/core/policy.js +4 -3
  51. package/dist/core/push.d.ts +0 -1
  52. package/dist/core/push.js +41 -28
  53. package/dist/core/pushrules.js +36 -35
  54. package/dist/core/ratelimit.d.ts +1 -1
  55. package/dist/core/rooms.d.ts +10 -1
  56. package/dist/core/rooms.js +101 -74
  57. package/dist/core/semaphore.js +23 -0
  58. package/dist/core/sso.d.ts +15 -1
  59. package/dist/core/sso.js +38 -2
  60. package/dist/core/stateres.js +7 -6
  61. package/dist/core/threepid.js +1 -9
  62. package/dist/federation/client.js +9 -1
  63. package/dist/federation/index.d.ts +2 -2
  64. package/dist/federation/index.js +43 -35
  65. package/dist/homeserver.d.ts +23 -4
  66. package/dist/homeserver.js +12 -3
  67. package/dist/http/handler.js +2 -23
  68. package/dist/http/input.js +2 -2
  69. package/dist/http/procedure.js +25 -0
  70. package/dist/http/router.d.ts +9 -26
  71. package/dist/http/router.js +10 -30
  72. package/dist/index.d.ts +4 -3
  73. package/dist/index.js +3 -2
  74. package/dist/storage/batch.js +11 -0
  75. package/dist/storage/memory.js +1 -0
  76. package/dist/storage/schema.js +1 -1
  77. package/dist/storage/sql.js +20 -1
  78. package/dist/storage/types.d.ts +16 -0
  79. package/dist/vendor/zod/LICENSE +21 -0
  80. package/dist/vendor/zod/v4/classic/errors.js +54 -0
  81. package/dist/vendor/zod/v4/classic/parse.js +17 -0
  82. package/dist/vendor/zod/v4/classic/schemas.js +865 -0
  83. package/dist/vendor/zod/v4/core/api.js +512 -0
  84. package/dist/vendor/zod/v4/core/checks.js +300 -0
  85. package/dist/vendor/zod/v4/core/core.js +109 -0
  86. package/dist/vendor/zod/v4/core/doc.js +35 -0
  87. package/dist/vendor/zod/v4/core/errors.js +118 -0
  88. package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
  89. package/dist/vendor/zod/v4/core/memoizer.js +255 -0
  90. package/dist/vendor/zod/v4/core/parse.js +206 -0
  91. package/dist/vendor/zod/v4/core/regexes.js +73 -0
  92. package/dist/vendor/zod/v4/core/registries.js +48 -0
  93. package/dist/vendor/zod/v4/core/schemas.js +1378 -0
  94. package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
  95. package/dist/vendor/zod/v4/core/util.js +537 -0
  96. package/dist/vendor/zod/v4/core/versions.js +8 -0
  97. package/dist/vendor/zod/v4/locales/en.js +110 -0
  98. package/package.json +8 -6
  99. package/dist/core/pushrules.d.ts +0 -1
package/dist/core/sso.js CHANGED
@@ -3,6 +3,34 @@ import { errors } from "./errors.js";
3
3
  import { LOCALPART_RE } from "./ids.js";
4
4
  import { createHash, randomBytes } from "node:crypto";
5
5
  //#region src/core/sso.ts
6
+ const UNSAFE_SCHEMES = /* @__PURE__ */ new Set([
7
+ "javascript:",
8
+ "data:",
9
+ "vbscript:",
10
+ "file:",
11
+ "blob:",
12
+ "about:"
13
+ ]);
14
+ /** The client's redirect URL, if it is one the server may send a login token to. */
15
+ function checkRedirectUrl(raw) {
16
+ if (!raw) return null;
17
+ try {
18
+ return UNSAFE_SCHEMES.has(new URL(raw).protocol) ? null : raw;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ function clientMatches(raw, entry) {
24
+ try {
25
+ const url = new URL(raw);
26
+ const allowed = new URL(entry);
27
+ if (allowed.host) return url.protocol === allowed.protocol && url.host === allowed.host && url.pathname.startsWith(allowed.pathname);
28
+ return raw.startsWith(entry);
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+ const sameIssuer = (a, b) => typeof a === "string" && a.replace(/\/$/, "") === b.replace(/\/$/, "");
6
34
  function sanitizeLocalpart(raw) {
7
35
  return raw.toLowerCase().replace(/[^a-z0-9._=\-/+]/g, "_").slice(0, 200);
8
36
  }
@@ -15,10 +43,12 @@ function decodeJwtPayload(jwt) {
15
43
  var SsoService = class {
16
44
  hs;
17
45
  providers;
46
+ clientAllowlist;
18
47
  discovery = /* @__PURE__ */ new Map();
19
48
  constructor(hs, options) {
20
49
  this.hs = hs;
21
50
  this.providers = options.providers;
51
+ this.clientAllowlist = options.clientAllowlist ?? [];
22
52
  }
23
53
  callbackUrl(provider) {
24
54
  const base = this.hs.config.publicBaseUrl;
@@ -31,6 +61,7 @@ var SsoService = class {
31
61
  const res = await this.hs.fetch(`${provider.issuer.replace(/\/$/, "")}/.well-known/openid-configuration`, { signal: AbortSignal.timeout(1e4) });
32
62
  if (!res.ok) throw new Error(`OIDC discovery for ${provider.id} failed with ${res.status}`);
33
63
  const meta = await res.json();
64
+ if (!sameIssuer(meta.issuer, provider.issuer)) throw new Error(`OIDC discovery for ${provider.id} returned another issuer`);
34
65
  this.discovery.set(provider.id, meta);
35
66
  return meta;
36
67
  }
@@ -96,6 +127,7 @@ var SsoService = class {
96
127
  if (!res.ok) throw errors.forbidden("Code exchange failed");
97
128
  const tokens = await res.json();
98
129
  let claims = {};
130
+ if ((provider.scopes ?? ["openid"]).includes("openid") && typeof tokens.id_token !== "string") throw errors.forbidden("The provider returned no ID token");
99
131
  if (typeof tokens.id_token === "string") {
100
132
  claims = decodeJwtPayload(tokens.id_token);
101
133
  const aud = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
@@ -130,7 +162,11 @@ var SsoService = class {
130
162
  });
131
163
  const redirect = new URL(pending.redirectUrl);
132
164
  redirect.searchParams.set("loginToken", loginToken);
133
- return redirect.toString();
165
+ return {
166
+ url: redirect.toString(),
167
+ confirm: !this.clientAllowlist.some((entry) => clientMatches(pending.redirectUrl, entry)),
168
+ client: redirect.host || redirect.protocol.replace(/:$/, "")
169
+ };
134
170
  }
135
171
  async resolveUser(provider, claims) {
136
172
  const link = await this.hs.db.findOne("sso_identities", [{
@@ -162,4 +198,4 @@ var SsoService = class {
162
198
  }
163
199
  };
164
200
  //#endregion
165
- export { SsoService };
201
+ export { SsoService, checkRedirectUrl };
@@ -1,7 +1,8 @@
1
+ import { M } from "./m.js";
1
2
  import { eventIdOf, roomVersionFeatures, stateKey } from "./events.js";
2
3
  import { authEventKeys, checkAuth, powerLevels, userLevel } from "./authrules.js";
3
4
  //#region src/core/stateres.ts
4
- const isPowerEvent = (ev) => ev.type === "m.room.power_levels" && ev.state_key === "" || ev.type === "m.room.join_rules" && ev.state_key === "" || ev.type === "m.room.create" && ev.state_key === "" || ev.type === "m.room.member" && ["leave", "ban"].includes(ev.content?.membership) && ev.sender !== ev.state_key;
5
+ const isPowerEvent = (ev) => ev.type === M.ROOM.POWER_LEVELS && ev.state_key === "" || ev.type === M.ROOM.JOIN_RULES && ev.state_key === "" || ev.type === M.ROOM.CREATE && ev.state_key === "" || ev.type === M.ROOM.MEMBER && ["leave", "ban"].includes(ev.content?.membership) && ev.sender !== ev.state_key;
5
6
  const authIds = (ev) => (ev.auth_events ?? []).map(eventIdOf);
6
7
  var EventCache = class {
7
8
  events = /* @__PURE__ */ new Map();
@@ -66,7 +67,7 @@ async function resolveState(stateSets, roomVersion, source, createEvent) {
66
67
  for (const id of await cache.authChain(powerEvents)) if (full.has(id)) control.add(id);
67
68
  let resolved = await iterativeAuthChecks(cache, await reverseTopologicalPowerSort(cache, control, roomVersion, createEvent), v21 ? {} : { ...unconflicted }, roomVersion, createEvent);
68
69
  const remaining = [...full].filter((id) => !control.has(id));
69
- const mainline = await mainlineOf(cache, resolved[stateKey("m.room.power_levels", "")]);
70
+ const mainline = await mainlineOf(cache, resolved[stateKey(M.ROOM.POWER_LEVELS, "")]);
70
71
  const positions = /* @__PURE__ */ new Map();
71
72
  for (const id of remaining) positions.set(id, await mainlinePosition(cache, id, mainline));
72
73
  remaining.sort((a, b) => {
@@ -107,7 +108,7 @@ async function senderPowerLevel(cache, ev, roomVersion, createEvent) {
107
108
  const a = cache.get(id);
108
109
  if (a && a.state_key !== void 0) state.set(stateKey(a.type, a.state_key), a);
109
110
  }
110
- if (createEvent) state.set(stateKey("m.room.create", ""), createEvent);
111
+ if (createEvent) state.set(stateKey(M.ROOM.CREATE, ""), createEvent);
111
112
  const level = userLevel(powerLevels(state, roomVersion), ev.sender);
112
113
  return Number.isFinite(level) ? level : Number.MAX_SAFE_INTEGER;
113
114
  }
@@ -160,7 +161,7 @@ async function iterativeAuthChecks(cache, ids, base, roomVersion, createEvent) {
160
161
  const sev = sid ? cache.get(sid) : void 0;
161
162
  if (sev) authState.set(k, sev);
162
163
  }
163
- if (createEvent) authState.set(stateKey("m.room.create", ""), createEvent);
164
+ if (createEvent) authState.set(stateKey(M.ROOM.CREATE, ""), createEvent);
164
165
  try {
165
166
  checkAuth(ev, authState, roomVersion);
166
167
  state[stateKey(ev.type, ev.state_key)] = id;
@@ -179,7 +180,7 @@ async function mainlineOf(cache, plId) {
179
180
  await cache.load(authIds(ev));
180
181
  current = authIds(ev).find((a) => {
181
182
  const e = cache.get(a);
182
- return e?.type === "m.room.power_levels" && e.state_key === "";
183
+ return e?.type === M.ROOM.POWER_LEVELS && e.state_key === "";
183
184
  });
184
185
  }
185
186
  const positions = /* @__PURE__ */ new Map();
@@ -199,7 +200,7 @@ async function mainlinePosition(cache, id, mainline) {
199
200
  await cache.load(authIds(ev));
200
201
  current = authIds(ev).find((a) => {
201
202
  const e = cache.get(a);
202
- return e?.type === "m.room.power_levels" && e.state_key === "";
203
+ return e?.type === M.ROOM.POWER_LEVELS && e.state_key === "";
203
204
  });
204
205
  }
205
206
  return 0;
@@ -1,6 +1,7 @@
1
1
  import { randomString } from "./crypto.js";
2
2
  import { MatrixError, errors } from "./errors.js";
3
3
  import { toMsisdn } from "./phone.js";
4
+ import { escapeHtml } from "./html.js";
4
5
  import { randomInt } from "node:crypto";
5
6
  //#region src/core/threepid.ts
6
7
  const CLIENT_SECRET_RE = /^[0-9a-zA-Z.=_-]{1,255}$/;
@@ -262,14 +263,5 @@ var ThreepidService = class {
262
263
  };
263
264
  }
264
265
  };
265
- function escapeHtml(s) {
266
- return s.replace(/[&<>"']/g, (c) => ({
267
- "&": "&amp;",
268
- "<": "&lt;",
269
- ">": "&gt;",
270
- "\"": "&quot;",
271
- "'": "&#39;"
272
- })[c]);
273
- }
274
266
  //#endregion
275
267
  export { ThreepidService, canonicalAddress, isMedium };
@@ -1,6 +1,8 @@
1
1
  import { canonicalJson } from "../core/json.js";
2
2
  import { unpaddedBase64 } from "../core/crypto.js";
3
+ import { M } from "../core/m.js";
3
4
  import { MatrixError } from "../core/errors.js";
5
+ import { checkLiteral } from "../core/outbound.js";
4
6
  import { sign } from "node:crypto";
5
7
  import { resolveSrv } from "node:dns/promises";
6
8
  import { Agent, request } from "node:https";
@@ -149,7 +151,7 @@ var FederationClient = class {
149
151
  }, "GET", "/.well-known/matrix/server", void 0, {}, 5e3);
150
152
  if (res.status !== 200) return null;
151
153
  const body = JSON.parse(res.body.toString("utf8"));
152
- return typeof body["m.server"] === "string" && body["m.server"] ? body["m.server"] : null;
154
+ return typeof body[M.SERVER] === "string" && body[M.SERVER] ? body[M.SERVER] : null;
153
155
  } catch {
154
156
  return null;
155
157
  }
@@ -168,6 +170,11 @@ var FederationClient = class {
168
170
  return null;
169
171
  }
170
172
  raw(target, method, pathWithQuery, body, headers, timeoutMs, poolKey = target.hostHeader) {
173
+ try {
174
+ checkLiteral(this.hs.addressPolicy, target.host);
175
+ } catch (err) {
176
+ return Promise.reject(err);
177
+ }
171
178
  return new Promise((resolve, reject) => {
172
179
  let connectTimer = null;
173
180
  const req = request({
@@ -177,6 +184,7 @@ var FederationClient = class {
177
184
  path: pathWithQuery,
178
185
  servername: isIP(target.tlsName) ? void 0 : target.tlsName,
179
186
  agent: this.agentFor(poolKey),
187
+ lookup: this.hs.addressPolicy.lookup,
180
188
  headers: {
181
189
  host: target.hostHeader,
182
190
  ...headers,
@@ -7,7 +7,7 @@ import { TransactionSender } from "./sender.js";
7
7
  import { Homeserver } from "../homeserver.js";
8
8
  //#region src/federation/index.d.ts
9
9
  export interface FederationOptions {
10
- /** Enable federation. Default: true. */
10
+ /** Enable federation (talking to other homeservers). Default: false, so that a server only joins the network on purpose. */
11
11
  enabled?: boolean;
12
12
  /** Join remote rooms with partial state first (MSC3902 "faster joins"). Default true. */
13
13
  fasterJoins?: boolean;
@@ -136,7 +136,7 @@ export declare class FederationService {
136
136
  */
137
137
  backfill(roomId: string, fromEventIds: string[], limit?: number): Promise<number>;
138
138
  /** Download remote media (authenticated federation endpoint, falling back to the legacy one). */
139
- fetchMedia(server: string, mediaId: string): Promise<{
139
+ fetchMedia(server: string, mediaId: string, maxBytes: number): Promise<{
140
140
  data: Uint8Array;
141
141
  contentType: string | null;
142
142
  filename: string | null;
@@ -1,5 +1,6 @@
1
1
  import { isObject } from "../core/json.js";
2
2
  import { unpaddedBase64, urlSafeBase64 } from "../core/crypto.js";
3
+ import { M } from "../core/m.js";
3
4
  import { SUPPORTED_ROOM_VERSIONS, computeContentHash, computeReferenceHash, eventIdOf, finalisePdu, redact, roomVersionFeatures, splitStateKey, stateKey, stripState } from "../core/events.js";
4
5
  import { MatrixError, errors } from "../core/errors.js";
5
6
  import { parseId } from "../core/ids.js";
@@ -7,6 +8,7 @@ import { authStateFromAuthEvents, checkAuth, getState, membershipOf } from "../c
7
8
  import { globMatch } from "../core/glob.js";
8
9
  import { resolveState } from "../core/stateres.js";
9
10
  import { MissingStateError } from "../core/rooms.js";
11
+ import { publicGet } from "../core/outbound.js";
10
12
  import { FederationClient } from "./client.js";
11
13
  import { KeyRing } from "./keyring.js";
12
14
  import { TransactionSender } from "./sender.js";
@@ -14,7 +16,7 @@ import { TransactionSender } from "./sender.js";
14
16
  const domainOf = (id) => id.slice(id.indexOf(":") + 1);
15
17
  /** Server ACL check (m.room.server_acl). */
16
18
  function serverAllowedByAcl(state, serverName) {
17
- const acl = getState(state, "m.room.server_acl")?.content;
19
+ const acl = getState(state, M.ROOM.SERVER_ACL)?.content;
18
20
  if (!isObject(acl)) return true;
19
21
  const host = serverName.replace(/:\d+$/, "").replace(/^\[|\]$/g, "");
20
22
  if (acl.allow_ip_literals === false && (/^\d+\.\d+\.\d+\.\d+$/.test(host) || host.includes(":"))) return false;
@@ -37,7 +39,7 @@ var FederationService = class {
37
39
  constructor(hs, options = {}) {
38
40
  this.hs = hs;
39
41
  this.options = options;
40
- this.enabled = options.enabled !== false;
42
+ this.enabled = options.enabled === true;
41
43
  this.fasterJoins = options.fasterJoins !== false;
42
44
  this.client = new FederationClient(hs, { rejectUnauthorized: options.verifyTls !== false });
43
45
  this.keyring = new KeyRing(hs);
@@ -57,7 +59,7 @@ var FederationService = class {
57
59
  const out = { ...rest };
58
60
  const f = roomVersionFeatures(version);
59
61
  if (!f.legacyEventIds) delete out.event_id;
60
- if (f.v12 && out.type === "m.room.create") delete out.room_id;
62
+ if (f.v12 && out.type === M.ROOM.CREATE) delete out.room_id;
61
63
  return out;
62
64
  }
63
65
  computeEventId(raw, version) {
@@ -73,7 +75,7 @@ var FederationService = class {
73
75
  async verifyPdu(raw, version, roomId) {
74
76
  if (!isObject(raw) || typeof raw.type !== "string" || typeof raw.sender !== "string" || !isObject(raw.content)) throw errors.badJson("Malformed PDU");
75
77
  const f = roomVersionFeatures(version);
76
- if (!f.v12 || raw.type !== "m.room.create") {
78
+ if (!f.v12 || raw.type !== M.ROOM.CREATE) {
77
79
  if (raw.room_id !== roomId) throw errors.badJson("PDU room_id mismatch");
78
80
  }
79
81
  const eventId = this.computeEventId(raw, version);
@@ -84,7 +86,7 @@ var FederationService = class {
84
86
  if (f.legacyEventIds && domainOf(eventId) !== senderDomain && !await this.keyring.verifySignedBy(redacted, domainOf(eventId))) throw errors.forbidden(`Event ${eventId} is not signed by its origin`);
85
87
  let joinAuthorisedSignatureValid;
86
88
  const via = raw.content?.join_authorised_via_users_server;
87
- 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
+ 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));
88
90
  let pdu = {
89
91
  ...raw,
90
92
  event_id: eventId
@@ -93,7 +95,7 @@ var FederationService = class {
93
95
  ...redact(raw, version),
94
96
  event_id: eventId
95
97
  };
96
- if (f.v12 && pdu.type === "m.room.create") pdu.room_id = roomId;
98
+ if (f.v12 && pdu.type === M.ROOM.CREATE) pdu.room_id = roomId;
97
99
  return {
98
100
  pdu,
99
101
  joinAuthorisedSignatureValid
@@ -104,7 +106,7 @@ var FederationService = class {
104
106
  const servers = /* @__PURE__ */ new Set();
105
107
  for (const [key, ev] of state) {
106
108
  const [type, userId] = splitStateKey(key);
107
- if (type === "m.room.member" && ev.content?.membership === "join") servers.add(domainOf(userId));
109
+ if (type === M.ROOM.MEMBER && ev.content?.membership === "join") servers.add(domainOf(userId));
108
110
  }
109
111
  servers.delete(this.hs.serverName);
110
112
  for (const s of servers) if (!this.serverAllowed(s) || applyAcl && !serverAllowedByAcl(state, s)) servers.delete(s);
@@ -123,8 +125,8 @@ var FederationService = class {
123
125
  onLocalEvent(pdu, state) {
124
126
  if (!this.enabled) return;
125
127
  (async () => {
126
- const destinations = await this.roomDestinations(pdu.room_id, state, pdu.type !== "m.room.server_acl");
127
- if (pdu.type === "m.room.member" && typeof pdu.state_key === "string" && !this.hs.isLocalUser(pdu.state_key)) destinations.add(domainOf(pdu.state_key));
128
+ const destinations = await this.roomDestinations(pdu.room_id, state, pdu.type !== M.ROOM.SERVER_ACL);
129
+ if (pdu.type === M.ROOM.MEMBER && typeof pdu.state_key === "string" && !this.hs.isLocalUser(pdu.state_key)) destinations.add(domainOf(pdu.state_key));
128
130
  if (!destinations.size) return;
129
131
  const version = await this.hs.rooms.roomVersion(pdu.room_id) ?? "10";
130
132
  await this.sender.sendPdu(destinations, this.toFederationPdu(pdu, version));
@@ -147,17 +149,17 @@ var FederationService = class {
147
149
  return out;
148
150
  }
149
151
  async onTyping(roomId, userId, typing) {
150
- if (this.hs.isLocalUser(userId)) await this.sendEduToRooms([roomId], "m.typing", {
152
+ if (this.hs.isLocalUser(userId)) await this.sendEduToRooms([roomId], M.TYPING, {
151
153
  room_id: roomId,
152
154
  user_id: userId,
153
155
  typing
154
156
  }, false);
155
157
  }
156
158
  async onReceipt(roomId, userId, receiptType, eventId, ts, threadId) {
157
- if (!this.hs.isLocalUser(userId) || receiptType === "m.read.private") return;
159
+ if (!this.hs.isLocalUser(userId) || receiptType === M.READ.PRIVATE) return;
158
160
  const data = { ts };
159
161
  if (threadId) data.thread_id = threadId;
160
- await this.sendEduToRooms([roomId], "m.receipt", { [roomId]: { [receiptType]: { [userId]: {
162
+ await this.sendEduToRooms([roomId], M.RECEIPT, { [roomId]: { [receiptType]: { [userId]: {
161
163
  event_ids: [eventId],
162
164
  data
163
165
  } } } });
@@ -166,7 +168,7 @@ var FederationService = class {
166
168
  if (!this.enabled || !this.hs.isLocalUser(userId)) return;
167
169
  const servers = await this.serversSharingRooms(userId);
168
170
  if (!servers.size) return;
169
- await this.sender.sendEdu(servers, "m.presence", { push: [{
171
+ await this.sender.sendEdu(servers, M.PRESENCE, { push: [{
170
172
  user_id: userId,
171
173
  presence,
172
174
  last_active_ago: 0,
@@ -217,7 +219,7 @@ var FederationService = class {
217
219
  const d = devices.find((x) => x.device_id === u.deviceId);
218
220
  if (d?.display_name) content.device_display_name = d.display_name;
219
221
  }
220
- await this.sender.sendEdu(servers, "m.device_list_update", content);
222
+ await this.sender.sendEdu(servers, M.DEVICE_LIST_UPDATE, content);
221
223
  }
222
224
  }
223
225
  async onSigningKeyUpdate(userId) {
@@ -230,7 +232,7 @@ var FederationService = class {
230
232
  }] });
231
233
  const content = { user_id: userId };
232
234
  for (const r of rows) if (r.key_type !== "user_signing") content[`${r.key_type}_key`] = r.key;
233
- await this.sender.sendEdu(servers, "m.signing_key_update", content);
235
+ await this.sender.sendEdu(servers, M.SIGNING_KEY_UPDATE, content);
234
236
  }
235
237
  /** Deliver to-device messages addressed to remote users. */
236
238
  async sendToDevice(sender, type, messages, messageId) {
@@ -241,7 +243,7 @@ var FederationService = class {
241
243
  if (server === this.hs.serverName || !this.serverAllowed(server)) continue;
242
244
  (byServer.get(server) ?? byServer.set(server, {}).get(server))[userId] = devices;
243
245
  }
244
- for (const [server, msgs] of byServer) await this.sender.sendEdu([server], "m.direct_to_device", {
246
+ for (const [server, msgs] of byServer) await this.sender.sendEdu([server], M.DIRECT_TO_DEVICE, {
245
247
  sender,
246
248
  type,
247
249
  message_id: `${messageId}-${server}`,
@@ -301,7 +303,7 @@ var FederationService = class {
301
303
  * rescinded or their knock being accepted/rejected.
302
304
  */
303
305
  async handleOutOfRoomMembership(origin, roomId, raw) {
304
- if (raw?.type !== "m.room.member" || typeof raw.state_key !== "string" || !this.hs.isLocalUser(raw.state_key)) throw errors.notFound("Unknown room");
306
+ if (raw?.type !== M.ROOM.MEMBER || typeof raw.state_key !== "string" || !this.hs.isLocalUser(raw.state_key)) throw errors.notFound("Unknown room");
305
307
  const row = await this.hs.db.findOne("memberships", [{
306
308
  field: "room_id",
307
309
  value: roomId
@@ -311,7 +313,7 @@ var FederationService = class {
311
313
  }]);
312
314
  if (!row) throw errors.notFound("Unknown room");
313
315
  const previous = await this.hs.rooms.getPdu(row.event_id);
314
- const create = (previous?.unsigned?.invite_room_state ?? previous?.unsigned?.knock_room_state ?? []).find((e) => e?.type === "m.room.create");
316
+ const create = (previous?.unsigned?.invite_room_state ?? previous?.unsigned?.knock_room_state ?? []).find((e) => e?.type === M.ROOM.CREATE);
315
317
  const version = typeof create?.content?.room_version === "string" ? create.content.room_version : "1";
316
318
  const { pdu } = await this.verifyPdu(raw, version, roomId);
317
319
  if (domainOf(pdu.sender) !== origin && domainOf(pdu.sender) !== domainOf(raw.state_key)) throw errors.forbidden("Unexpected sender");
@@ -571,7 +573,7 @@ var FederationService = class {
571
573
  authPdus.push(p);
572
574
  }
573
575
  try {
574
- const create = roomVersionFeatures(version).v12 && pdu.type !== "m.room.create" ? await this.hs.rooms.getPdu(`$${roomId.slice(1)}`) ?? void 0 : void 0;
576
+ const create = roomVersionFeatures(version).v12 && pdu.type !== M.ROOM.CREATE ? await this.hs.rooms.getPdu(`$${roomId.slice(1)}`) ?? void 0 : void 0;
575
577
  checkAuth(pdu, authStateFromAuthEvents(pdu, authPdus, version, create), version);
576
578
  return null;
577
579
  } catch (err) {
@@ -592,7 +594,7 @@ var FederationService = class {
592
594
  const content = edu?.content;
593
595
  if (!isObject(content)) return;
594
596
  switch (edu.edu_type) {
595
- case "m.typing": {
597
+ case M.TYPING: {
596
598
  const { room_id, user_id, typing } = content;
597
599
  if (typeof user_id !== "string" || domainOf(user_id) !== origin || typeof room_id !== "string") return;
598
600
  if (!await this.hs.rooms.exists(room_id) || !await this.mayActInRoom(room_id, user_id, origin)) return;
@@ -600,12 +602,12 @@ var FederationService = class {
600
602
  await this.hs.typing.setTyping(room_id, user_id, !!typing, typeof content.timeout === "number" ? content.timeout : 3e4);
601
603
  return;
602
604
  }
603
- case "m.receipt":
605
+ case M.RECEIPT:
604
606
  for (const [roomId, byType] of Object.entries(content)) {
605
607
  if (!isObject(byType) || !await this.hs.rooms.exists(roomId)) continue;
606
608
  if (!serverAllowedByAcl(await this.hs.rooms.currentState(roomId), origin)) continue;
607
609
  for (const [receiptType, byUser] of Object.entries(byType)) {
608
- if (!["m.read", "m.read.private"].includes(receiptType) || !isObject(byUser)) continue;
610
+ if (receiptType !== M.READ._ && receiptType !== M.READ.PRIVATE || !isObject(byUser)) continue;
609
611
  for (const [userId, r] of Object.entries(byUser)) {
610
612
  if (domainOf(userId) !== origin || !isObject(r) || !Array.isArray(r.event_ids)) continue;
611
613
  if (!await this.mayActInRoom(roomId, userId, origin)) continue;
@@ -625,7 +627,7 @@ var FederationService = class {
625
627
  this.hs.notifier.notify(await this.hs.rooms.joinedMembers(roomId));
626
628
  }
627
629
  return;
628
- case "m.presence":
630
+ case M.PRESENCE:
629
631
  for (const p of Array.isArray(content.push) ? content.push : []) {
630
632
  if (!isObject(p) || typeof p.user_id !== "string" || domainOf(p.user_id) !== origin) continue;
631
633
  if (![
@@ -643,7 +645,7 @@ var FederationService = class {
643
645
  this.hs.notifier.notify(await this.hs.rooms.usersSharingRooms(p.user_id));
644
646
  }
645
647
  return;
646
- case "m.device_list_update": {
648
+ case M.DEVICE_LIST_UPDATE: {
647
649
  const userId = content.user_id;
648
650
  if (typeof userId !== "string" || domainOf(userId) !== origin || typeof content.device_id !== "string") return;
649
651
  const known = await this.hs.db.findOne("remote_device_lists", [{
@@ -655,7 +657,7 @@ var FederationService = class {
655
657
  if (known || content.deleted || isObject(content.keys)) await this.hs.keys.markDeviceListChanged(userId);
656
658
  return;
657
659
  }
658
- case "m.signing_key_update": {
660
+ case M.SIGNING_KEY_UPDATE: {
659
661
  const userId = content.user_id;
660
662
  if (typeof userId !== "string" || domainOf(userId) !== origin) return;
661
663
  for (const type of ["master", "self_signing"]) {
@@ -671,7 +673,7 @@ var FederationService = class {
671
673
  await this.hs.keys.markDeviceListChanged(userId);
672
674
  return;
673
675
  }
674
- case "m.direct_to_device": {
676
+ case M.DIRECT_TO_DEVICE: {
675
677
  const { sender, type, message_id, messages } = content;
676
678
  if (typeof sender !== "string" || domainOf(sender) !== origin || typeof type !== "string" || !isObject(messages)) return;
677
679
  if (!await this.hs.cluster.setIfAbsent(`fedtodevice:${origin}|${message_id}`, true, 864e5)) return;
@@ -751,7 +753,7 @@ var FederationService = class {
751
753
  const pdu = await this.hs.rooms.getPdu(id);
752
754
  if (!pdu) continue;
753
755
  stateIds[stateKey(pdu.type, pdu.state_key)] = id;
754
- if (pdu.type === "m.room.create") create = pdu;
756
+ if (pdu.type === M.ROOM.CREATE) create = pdu;
755
757
  }
756
758
  if (!create) throw errors.badJson("send_join response lacks the create event");
757
759
  if (!await this.hs.rooms.exists(roomId)) await this.hs.db.upsert("rooms", {
@@ -960,13 +962,16 @@ var FederationService = class {
960
962
  return 0;
961
963
  }
962
964
  /** Download remote media (authenticated federation endpoint, falling back to the legacy one). */
963
- async fetchMedia(server, mediaId) {
965
+ async fetchMedia(server, mediaId, maxBytes) {
964
966
  this.requireEnabled();
965
967
  const res = await this.client.requestRaw(server, "GET", `/_matrix/federation/v1/media/download/${encodeURIComponent(mediaId)}`, { timeoutMs: 6e4 });
966
968
  if (res.status === 200) {
967
969
  const ct = String(res.headers["content-type"] ?? "");
968
970
  const m = /boundary=("?)([^";]+)\1/.exec(ct);
969
- if (ct.startsWith("multipart/") && m) return parseMultipartMedia(res.body, m[2], this);
971
+ if (ct.startsWith("multipart/") && m) return parseMultipartMedia(res.body, m[2], {
972
+ maxBytes,
973
+ fetch: this.hs.publicFetch
974
+ });
970
975
  }
971
976
  const legacy = await this.client.requestRaw(server, "GET", `/_matrix/media/v3/download/${encodeURIComponent(server)}/${encodeURIComponent(mediaId)}`, {
972
977
  query: { allow_remote: "false" },
@@ -993,14 +998,14 @@ var FederationService = class {
993
998
  const out = /* @__PURE__ */ new Set();
994
999
  for (const [key, ev] of state) {
995
1000
  const [type, userId] = splitStateKey(key);
996
- if (type === "m.room.member" && ev.content?.membership === "join") out.add(domainOf(userId));
1001
+ if (type === M.ROOM.MEMBER && ev.content?.membership === "join") out.add(domainOf(userId));
997
1002
  }
998
1003
  return [...out];
999
1004
  }
1000
1005
  isMembershipVisibleTo(state, server) {
1001
1006
  for (const [key] of state) {
1002
1007
  const [type, userId] = splitStateKey(key);
1003
- if (type === "m.room.member" && domainOf(userId) === server && membershipOf(state, userId) === "join") return true;
1008
+ if (type === M.ROOM.MEMBER && domainOf(userId) === server && membershipOf(state, userId) === "join") return true;
1004
1009
  }
1005
1010
  return false;
1006
1011
  }
@@ -1019,7 +1024,7 @@ function filenameFromDisposition(header) {
1019
1024
  return plain ? plain[1] : null;
1020
1025
  }
1021
1026
  /** Parse a federation media multipart/mixed response: a JSON part then the file (or a redirect). */
1022
- async function parseMultipartMedia(body, boundary, fed) {
1027
+ async function parseMultipartMedia(body, boundary, limits) {
1023
1028
  const delimiter = Buffer.from(`--${boundary}`);
1024
1029
  const parts = [];
1025
1030
  let idx = body.indexOf(delimiter);
@@ -1040,10 +1045,13 @@ async function parseMultipartMedia(body, boundary, fed) {
1040
1045
  if (i > 0) headers[line.slice(0, i).trim().toLowerCase()] = line.slice(i + 1).trim();
1041
1046
  }
1042
1047
  if (headers.location) {
1043
- const res = await fetch(headers.location, { signal: AbortSignal.timeout(6e4) });
1044
- if (!res.ok) throw errors.notFound("Remote media redirect failed");
1048
+ const res = await publicGet(limits.fetch, headers.location, {
1049
+ maxBytes: limits.maxBytes,
1050
+ timeoutMs: 6e4
1051
+ });
1052
+ if (res.status !== 200) throw errors.notFound("Remote media redirect failed");
1045
1053
  return {
1046
- data: new Uint8Array(await res.arrayBuffer()),
1054
+ data: new Uint8Array(res.body),
1047
1055
  contentType: res.headers.get("content-type"),
1048
1056
  filename: filenameFromDisposition(res.headers.get("content-disposition") ?? void 0)
1049
1057
  };
@@ -3,12 +3,13 @@ import { SigningKey } from "./core/crypto.js";
3
3
  import { PolicyServers } from "./core/policy.js";
4
4
  import { ThreepidOptions, ThreepidService } from "./core/threepid.js";
5
5
  import { IdentityServers } from "./core/identity.js";
6
+ import { M } from "./core/m.js";
6
7
  import { AppServiceRegistration, AppServiceService } from "./core/appservices.js";
7
8
  import { AuthService } from "./core/auth.js";
9
+ import { DatabaseAdapter } from "./storage/types.js";
8
10
  import { KeyService } from "./core/keys.js";
9
11
  import { ClusterAdapter, StreamManager } from "./core/cluster.js";
10
12
  import { Notifier } from "./core/notifier.js";
11
- import { DatabaseAdapter } from "./storage/types.js";
12
13
  import { RoomService } from "./core/rooms.js";
13
14
  import { TypingService } from "./core/typing.js";
14
15
  import { PushService } from "./core/push.js";
@@ -25,6 +26,7 @@ import { SettingsService } from "./core/settings.js";
25
26
  import { AdminRoomOptions, AdminRoomService } from "./core/adminroom.js";
26
27
  import { ResolvedUserAuth, UserAuthAdapter } from "./adapters/auth.js";
27
28
  import { MediaAdapter, Thumbnailer } from "./adapters/media.js";
29
+ import { AddressPolicy } from "./core/outbound.js";
28
30
  import { EventEmitter } from "node:events";
29
31
  //#region src/homeserver.d.ts
30
32
  export interface Logger {
@@ -52,7 +54,7 @@ export interface HomeserverOptions {
52
54
  /** Contacts and page served at `/.well-known/matrix/support` (e.g. to appeal a locked account). */
53
55
  support?: {
54
56
  contacts?: {
55
- role: "m.role.admin" | "m.role.security" | (string & {});
57
+ role: typeof M.ROLE.ADMIN | typeof M.ROLE.SECURITY | (string & {});
56
58
  matrix_id?: string;
57
59
  email_address?: string;
58
60
  }[];
@@ -81,6 +83,11 @@ export interface HomeserverOptions {
81
83
  accessTokenLifetimeMs?: number;
82
84
  /** Maximum `/sync` long-poll duration in ms. Default: 30000. */
83
85
  maxSyncTimeoutMs?: number;
86
+ /**
87
+ * How many `/sync` responses one process builds at once (default 4). Requests woken together
88
+ * wait in the process instead of queueing their database reads ahead of everything else.
89
+ */
90
+ maxConcurrentSyncs?: number;
84
91
  /**
85
92
  * Enables `/preview_url`. Off by default because it makes the server fetch arbitrary URLs.
86
93
  * Private/internal addresses stay blocked unless `allowPrivateAddresses` is set.
@@ -91,6 +98,13 @@ export interface HomeserverOptions {
91
98
  logger?: Logger;
92
99
  /** Custom `fetch` for outgoing HTTP requests. */
93
100
  fetch?: typeof globalThis.fetch;
101
+ /**
102
+ * Let requests to addresses that users or remote servers choose (federation, push gateways,
103
+ * identity servers, URL previews, remote media) reach private addresses: localhost, LAN and
104
+ * other non-public ranges. `true` allows all of them, a list allows those ranges only (e.g.
105
+ * `["127.0.0.1/32"]` for a push gateway on the same machine). Default: none.
106
+ */
107
+ allowPrivateAddresses?: boolean | string[];
94
108
  /**
95
109
  * Rate limiting. `false` disables it; otherwise per-action overrides and an optional
96
110
  * shared limiter (defaults to an in-memory token bucket). Admins are never limited.
@@ -101,7 +115,7 @@ export interface HomeserverOptions {
101
115
  sso?: SsoOptions;
102
116
  /**
103
117
  * Coordination between instances. The default (`localCluster()`) supports one process;
104
- * use `postgresCluster()` / `redisCluster()` to scale horizontally.
118
+ * use `postgresCluster()` to scale horizontally.
105
119
  */
106
120
  cluster?: ClusterAdapter;
107
121
  /**
@@ -150,6 +164,7 @@ export interface ResolvedConfig {
150
164
  defaultRoomVersion: string;
151
165
  accessTokenLifetimeMs: number | null;
152
166
  maxSyncTimeoutMs: number;
167
+ maxConcurrentSyncs: number;
153
168
  urlPreviews: {
154
169
  allowPrivateAddresses: boolean;
155
170
  } | null;
@@ -188,8 +203,12 @@ export declare class Homeserver extends EventEmitter {
188
203
  readonly policyServers: PolicyServers;
189
204
  readonly threepids: ThreepidService;
190
205
  readonly identityServers: IdentityServers;
191
- /** `fetch` used for outgoing requests (push gateways, URL previews, OAuth). Overridable for testing. */
206
+ /** `fetch` for requests to addresses the operator configured (OAuth, SSO, application services). */
192
207
  readonly fetch: typeof globalThis.fetch;
208
+ /** Which addresses requests to user- or remote-chosen destinations may reach (see `allowPrivateAddresses`). */
209
+ readonly addressPolicy: AddressPolicy;
210
+ /** `fetch` for user- or remote-chosen URLs: refuses private addresses unless allowed, never follows redirects. */
211
+ readonly publicFetch: typeof globalThis.fetch;
193
212
  /** Resolves once storage is migrated and the server is ready to serve requests. */
194
213
  readonly ready: Promise<void>;
195
214
  private _signingKey;