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
@@ -1,5 +1,6 @@
1
1
  import { isObject } from "./json.js";
2
2
  import { publicKeyFromBase64, verifyJsonSignature } from "./crypto.js";
3
+ import { M } from "./m.js";
3
4
  import { roomVersionFeatures, stateKey, versionNum } from "./events.js";
4
5
  import { errors } from "./errors.js";
5
6
  import { isValidUserId } from "./ids.js";
@@ -20,10 +21,10 @@ function getState(state, type, key = "") {
20
21
  return state.get(stateKey(type, key));
21
22
  }
22
23
  function membershipOf(state, userId) {
23
- return getState(state, "m.room.member", userId)?.content?.membership ?? "leave";
24
+ return getState(state, M.ROOM.MEMBER, userId)?.content?.membership ?? "leave";
24
25
  }
25
26
  function roomCreator(state, roomVersion) {
26
- const create = getState(state, "m.room.create");
27
+ const create = getState(state, M.ROOM.CREATE);
27
28
  if (!create) return void 0;
28
29
  return roomVersionFeatures(roomVersion).v11 ? create.sender : create.content.creator ?? create.sender;
29
30
  }
@@ -35,13 +36,13 @@ const toInt = (v, fallback) => {
35
36
  /** v12+: the create event's sender plus `additional_creators`. Empty for older versions. */
36
37
  function roomCreators(state, roomVersion) {
37
38
  if (!roomVersionFeatures(roomVersion).v12) return /* @__PURE__ */ new Set();
38
- const create = getState(state, "m.room.create");
39
+ const create = getState(state, M.ROOM.CREATE);
39
40
  if (!create) return /* @__PURE__ */ new Set();
40
41
  const extra = Array.isArray(create.content?.additional_creators) ? create.content.additional_creators : [];
41
42
  return /* @__PURE__ */ new Set([create.sender, ...extra.filter((u) => typeof u === "string")]);
42
43
  }
43
44
  function powerLevels(state, roomVersion) {
44
- const pl = getState(state, "m.room.power_levels")?.content;
45
+ const pl = getState(state, M.ROOM.POWER_LEVELS)?.content;
45
46
  const creators = roomCreators(state, roomVersion);
46
47
  if (!pl) {
47
48
  const creator = roomCreator(state, roomVersion);
@@ -90,21 +91,21 @@ const sameDomain = (a, b) => a.slice(a.indexOf(":") + 1) === b.slice(b.indexOf("
90
91
  const domainOf = (id) => id.slice(id.indexOf(":") + 1);
91
92
  /** The (type, state_key) pairs an event's auth_events may reference (spec "auth events selection"). */
92
93
  function authEventKeys(event, roomVersion) {
93
- if (event.type === "m.room.create") return [];
94
- const keys = [stateKey("m.room.power_levels", ""), stateKey("m.room.member", event.sender)];
95
- if (!roomVersionFeatures(roomVersion).v12) keys.unshift(stateKey("m.room.create", ""));
96
- if (event.type === "m.room.member" && typeof event.state_key === "string") {
97
- keys.push(stateKey("m.room.member", event.state_key));
94
+ if (event.type === M.ROOM.CREATE) return [];
95
+ const keys = [stateKey(M.ROOM.POWER_LEVELS, ""), stateKey(M.ROOM.MEMBER, event.sender)];
96
+ if (!roomVersionFeatures(roomVersion).v12) keys.unshift(stateKey(M.ROOM.CREATE, ""));
97
+ if (event.type === M.ROOM.MEMBER && typeof event.state_key === "string") {
98
+ keys.push(stateKey(M.ROOM.MEMBER, event.state_key));
98
99
  const membership = event.content?.membership;
99
100
  if ([
100
101
  "join",
101
102
  "invite",
102
103
  "knock"
103
- ].includes(membership)) keys.push(stateKey("m.room.join_rules", ""));
104
+ ].includes(membership)) keys.push(stateKey(M.ROOM.JOIN_RULES, ""));
104
105
  const token = event.content?.third_party_invite?.signed?.token;
105
- if (membership === "invite" && typeof token === "string") keys.push(stateKey("m.room.third_party_invite", token));
106
+ if (membership === "invite" && typeof token === "string") keys.push(stateKey(M.ROOM.THIRD_PARTY_INVITE, token));
106
107
  const via = event.content?.join_authorised_via_users_server;
107
- if (membership === "join" && typeof via === "string" && roomVersionFeatures(roomVersion).restricted) keys.push(stateKey("m.room.member", via));
108
+ if (membership === "join" && typeof via === "string" && roomVersionFeatures(roomVersion).restricted) keys.push(stateKey(M.ROOM.MEMBER, via));
108
109
  }
109
110
  return [...new Set(keys)];
110
111
  }
@@ -121,10 +122,10 @@ function authStateFromAuthEvents(event, authEvents, roomVersion, createEvent) {
121
122
  const key = stateKey(ev.type, ev.state_key);
122
123
  if (state.has(key)) throw errors.forbidden("Duplicate auth event");
123
124
  if (!allowed.has(key)) throw errors.forbidden(`Unexpected auth event ${ev.type}`);
124
- if (roomVersionFeatures(roomVersion).v12 && ev.type === "m.room.create") throw errors.forbidden("Create event must not be in auth_events");
125
+ if (roomVersionFeatures(roomVersion).v12 && ev.type === M.ROOM.CREATE) throw errors.forbidden("Create event must not be in auth_events");
125
126
  state.set(key, ev);
126
127
  }
127
- if (createEvent) state.set(stateKey("m.room.create", ""), createEvent);
128
+ if (createEvent) state.set(stateKey(M.ROOM.CREATE, ""), createEvent);
128
129
  return state;
129
130
  }
130
131
  /**
@@ -137,7 +138,7 @@ function checkAuth(event, state, roomVersion, opts = {}) {
137
138
  const deny = (msg) => {
138
139
  throw errors.forbidden(msg);
139
140
  };
140
- if (event.type === "m.room.create") {
141
+ if (event.type === M.ROOM.CREATE) {
141
142
  if (state.size > 0 || Array.isArray(event.prev_events) && event.prev_events.length > 0) deny("Create event must be the first event");
142
143
  if (!features.v12 && event.room_id && !sameDomain(event.room_id, event.sender)) deny("Room ID domain must match the creator");
143
144
  const rv = event.content?.room_version;
@@ -149,10 +150,10 @@ function checkAuth(event, state, roomVersion, opts = {}) {
149
150
  }
150
151
  return;
151
152
  }
152
- const create = getState(state, "m.room.create");
153
+ const create = getState(state, M.ROOM.CREATE);
153
154
  if (!create) deny("No create event in room");
154
- if (create.content?.["m.federate"] === false && !sameDomain(event.sender, create.sender)) deny("Room is not federated");
155
- if (n <= 5 && event.type === "m.room.aliases") {
155
+ if (create.content?.[M.FEDERATE] === false && !sameDomain(event.sender, create.sender)) deny("Room is not federated");
156
+ if (n <= 5 && event.type === M.ROOM.ALIASES) {
156
157
  if (event.state_key === void 0) deny("Aliases event lacks a state key");
157
158
  if (event.state_key !== domainOf(event.sender)) deny("Aliases state key must be the sender's domain");
158
159
  return;
@@ -160,12 +161,12 @@ function checkAuth(event, state, roomVersion, opts = {}) {
160
161
  const pl = powerLevels(state, roomVersion);
161
162
  const senderMembership = membershipOf(state, event.sender);
162
163
  const senderLevel = userLevel(pl, event.sender);
163
- if (event.type === "m.room.member") {
164
+ if (event.type === M.ROOM.MEMBER) {
164
165
  const target = event.state_key;
165
166
  const membership = event.content?.membership;
166
167
  if (typeof target !== "string" || typeof membership !== "string") deny("Member event lacks state_key or membership");
167
168
  const targetMembership = membershipOf(state, target);
168
- const joinRule = getState(state, "m.room.join_rules")?.content?.join_rule ?? "invite";
169
+ const joinRule = getState(state, M.ROOM.JOIN_RULES)?.content?.join_rule ?? "invite";
169
170
  const via = event.content?.join_authorised_via_users_server;
170
171
  switch (membership) {
171
172
  case "join": {
@@ -226,7 +227,7 @@ function checkAuth(event, state, roomVersion, opts = {}) {
226
227
  return;
227
228
  }
228
229
  if (senderMembership !== "join") deny("You are not joined to this room");
229
- if (event.type === "m.room.third_party_invite") {
230
+ if (event.type === M.ROOM.THIRD_PARTY_INVITE) {
230
231
  if (senderLevel >= pl.invite) return;
231
232
  deny("Insufficient power level to invite");
232
233
  }
@@ -238,16 +239,16 @@ function checkAuth(event, state, roomVersion, opts = {}) {
238
239
  if (!owner) deny("Malformed user-owned state key");
239
240
  if (owner !== event.sender && senderLevel <= userLevel(pl, owner)) deny("This state key belongs to another user");
240
241
  }
241
- if (event.type === "m.room.power_levels" && isState) {
242
+ if (event.type === M.ROOM.POWER_LEVELS && isState) {
242
243
  if (isObject(event.content?.users)) for (const u of Object.keys(event.content.users)) {
243
244
  if (!isValidUserId(u)) deny("Power level users must be valid user IDs");
244
245
  if (pl.creators.has(u)) deny("Room creators cannot be given a power level");
245
246
  }
246
- if (getState(state, "m.room.power_levels")) checkPowerLevelChange(event, pl, senderLevel, features.integerPowerLevels, n >= 6, deny);
247
+ if (getState(state, M.ROOM.POWER_LEVELS)) checkPowerLevelChange(event, pl, senderLevel, features.integerPowerLevels, n >= 6, deny);
247
248
  else checkPowerLevelShape(event, features.integerPowerLevels, deny);
248
249
  return;
249
250
  }
250
- if (n <= 2 && event.type === "m.room.redaction") {
251
+ if (n <= 2 && event.type === M.ROOM.REDACTION) {
251
252
  if (senderLevel >= pl.redact) return;
252
253
  const redacts = event.redacts;
253
254
  if (typeof redacts === "string" && redacts.includes(":") && domainOf(redacts) === domainOf(event.sender)) return;
@@ -258,7 +259,7 @@ function checkThirdPartyInvite(event, state) {
258
259
  const signed = event.content?.third_party_invite?.signed;
259
260
  if (!isObject(signed) || typeof signed.mxid !== "string" || typeof signed.token !== "string") return false;
260
261
  if (signed.mxid !== event.state_key) return false;
261
- const invite = getState(state, "m.room.third_party_invite", signed.token);
262
+ const invite = getState(state, M.ROOM.THIRD_PARTY_INVITE, signed.token);
262
263
  if (!invite || invite.sender !== event.sender) return false;
263
264
  const keys = [];
264
265
  if (typeof invite.content?.public_key === "string") keys.push(invite.content.public_key);
@@ -1,8 +1,8 @@
1
1
  //#region src/core/cluster.d.ts
2
2
  /**
3
3
  * Coordination between homeserver instances sharing one database. The default
4
- * (`localCluster()`) keeps everything in process; use `postgresCluster()` or
5
- * `redisCluster()` to run several instances behind a load balancer.
4
+ * (`localCluster()`) keeps everything in process; use `postgresCluster()` to run several
5
+ * instances behind a load balancer.
6
6
  */
7
7
  export interface ClusterAdapter {
8
8
  /** Allocate the next value of a cluster-wide, strictly increasing sequence. */
@@ -44,13 +44,20 @@ export declare class StreamManager {
44
44
  private cluster;
45
45
  private instanceId;
46
46
  private inflight;
47
- private published;
48
- private transition;
47
+ /** Allocations waiting for a publish that covers them. */
48
+ private reserving;
49
+ /** Allocations whose publish is done, asking the sequence for their position now. */
50
+ private allocating;
51
+ /** The position of the last published busy state: below every position still being allocated. */
52
+ private lastPos;
49
53
  private heartbeat;
50
54
  private positionCache;
55
+ private publishing;
56
+ private nextPublish;
51
57
  constructor(cluster: ClusterAdapter, instanceId: string);
52
58
  private key;
53
- private publishState;
59
+ /** Publish this writer's state as it is when the publish starts; concurrent callers share it. */
60
+ private flush;
54
61
  /** Allocate a position. The caller must call `done()` once its writes are committed. */
55
62
  allocate(): Promise<StreamLease>;
56
63
  /** Allocate a position, run `fn` with it, then complete it. */
@@ -91,38 +91,64 @@ var StreamManager = class {
91
91
  cluster;
92
92
  instanceId;
93
93
  inflight = /* @__PURE__ */ new Set();
94
- published = {
95
- busy: false,
96
- pos: 0
97
- };
98
- transition = Promise.resolve();
94
+ /** Allocations waiting for a publish that covers them. */
95
+ reserving = 0;
96
+ /** Allocations whose publish is done, asking the sequence for their position now. */
97
+ allocating = 0;
98
+ /** The position of the last published busy state: below every position still being allocated. */
99
+ lastPos = 0;
99
100
  heartbeat;
100
101
  positionCache = null;
102
+ publishing = Promise.resolve();
103
+ nextPublish = null;
101
104
  constructor(cluster, instanceId) {
102
105
  this.cluster = cluster;
103
106
  this.instanceId = instanceId;
104
- this.heartbeat = setInterval(() => void this.publishState().catch(() => {}), WRITER_TTL_MS / 3);
107
+ this.heartbeat = setInterval(() => void this.flush().catch(() => {}), WRITER_TTL_MS / 3);
105
108
  this.heartbeat.unref?.();
106
109
  }
107
110
  key() {
108
111
  return `writer:${this.instanceId}`;
109
112
  }
110
- async publishState() {
111
- await this.cluster.set(this.key(), this.published, WRITER_TTL_MS);
113
+ /** Publish this writer's state as it is when the publish starts; concurrent callers share it. */
114
+ flush() {
115
+ if (this.nextPublish) return this.nextPublish;
116
+ const run = async () => {
117
+ this.nextPublish = null;
118
+ const covered = this.reserving;
119
+ const earlier = this.allocating;
120
+ this.reserving = 0;
121
+ this.allocating += covered;
122
+ const bounds = [];
123
+ if (this.inflight.size) bounds.push(Math.min(...this.inflight) - 1);
124
+ if (earlier) bounds.push(this.lastPos);
125
+ if (covered) bounds.push(await this.cluster.currentId("stream"));
126
+ const state = bounds.length ? {
127
+ busy: true,
128
+ pos: Math.min(...bounds)
129
+ } : {
130
+ busy: false,
131
+ pos: 0
132
+ };
133
+ if (state.busy) this.lastPos = state.pos;
134
+ await this.cluster.set(this.key(), state, WRITER_TTL_MS);
135
+ };
136
+ const p = this.publishing.then(run, run);
137
+ this.nextPublish = p;
138
+ this.publishing = p.catch(() => {});
139
+ return p;
112
140
  }
113
141
  /** Allocate a position. The caller must call `done()` once its writes are committed. */
114
142
  async allocate() {
115
- await (this.transition = this.transition.then(async () => {
116
- if (!this.published.busy) {
117
- this.published = {
118
- busy: true,
119
- pos: await this.cluster.currentId("stream")
120
- };
121
- await this.publishState();
122
- }
123
- }));
124
- const id = await this.cluster.nextId("stream");
125
- this.inflight.add(id);
143
+ this.reserving++;
144
+ let id;
145
+ try {
146
+ await this.flush();
147
+ id = await this.cluster.nextId("stream");
148
+ this.inflight.add(id);
149
+ } finally {
150
+ this.allocating--;
151
+ }
126
152
  let finished = false;
127
153
  const timer = setTimeout(() => void lease.done(), 6e4);
128
154
  timer.unref?.();
@@ -133,16 +159,7 @@ var StreamManager = class {
133
159
  finished = true;
134
160
  clearTimeout(timer);
135
161
  this.inflight.delete(id);
136
- await (this.transition = this.transition.then(async () => {
137
- this.published = this.inflight.size ? {
138
- busy: true,
139
- pos: Math.min(...this.inflight) - 1
140
- } : {
141
- busy: false,
142
- pos: await this.cluster.currentId("stream")
143
- };
144
- await this.publishState();
145
- }));
162
+ await this.flush();
146
163
  this.positionCache = null;
147
164
  }
148
165
  };
@@ -1,4 +1,5 @@
1
1
  import { randomString } from "./crypto.js";
2
+ import { UNSTABLE } from "./m.js";
2
3
  import { MatrixError, errors } from "./errors.js";
3
4
  //#region src/core/delayed.ts
4
5
  /**
@@ -27,7 +28,7 @@ var DelayedEventService = class {
27
28
  }
28
29
  async schedule(opts) {
29
30
  if (!Number.isInteger(opts.delayMs) || opts.delayMs < 0) throw errors.invalidParam("Invalid delay");
30
- if (opts.delayMs > this.maxDelayMs) throw new MatrixError(400, "M_MAX_DELAY_EXCEEDED", "Delay is too long", { "org.matrix.msc4140.max_delay": this.maxDelayMs });
31
+ if (opts.delayMs > this.maxDelayMs) throw new MatrixError(400, "M_MAX_DELAY_EXCEEDED", "Delay is too long", { [UNSTABLE.ORG.MATRIX.MSC4140.MAX_DELAY]: this.maxDelayMs });
31
32
  const txnKey = opts.txnId ? `${opts.userId}|${opts.deviceId}|${opts.roomId}|${opts.txnId}` : null;
32
33
  if (txnKey) {
33
34
  const existing = await this.hs.db.findOne("delayed_events", [{
@@ -1,5 +1,6 @@
1
1
  import { canonicalJson, isObject } from "./json.js";
2
2
  import { randomString, sha256, signJson, unpaddedBase64, urlSafeBase64 } from "./crypto.js";
3
+ import { M } from "./m.js";
3
4
  //#region src/core/events.ts
4
5
  const STABLE_ROOM_VERSIONS = [
5
6
  "1",
@@ -76,7 +77,7 @@ function redact(event, roomVersion) {
76
77
  const keep = n >= 11 ? KEEP_TOP_LEVEL : [...KEEP_TOP_LEVEL, ...KEEP_TOP_LEVEL_LEGACY];
77
78
  const out = {};
78
79
  for (const k of keep) if (k in event) out[k] = event[k];
79
- const keepRelation = roomVersion.startsWith("org.matrix.msc3389.") && isObject(event.content?.["m.relates_to"]) ? event.content["m.relates_to"] : void 0;
80
+ const keepRelation = roomVersion.startsWith("org.matrix.msc3389.") && isObject(event.content?.[M.RELATES_TO]) ? event.content[M.RELATES_TO] : void 0;
80
81
  const content = isObject(event.content) ? event.content : {};
81
82
  const pick = (...keys) => {
82
83
  const c = {};
@@ -84,28 +85,28 @@ function redact(event, roomVersion) {
84
85
  return c;
85
86
  };
86
87
  switch (event.type) {
87
- case "m.room.member": {
88
+ case M.ROOM.MEMBER: {
88
89
  const c = pick("membership", ...n >= 9 ? ["join_authorised_via_users_server"] : []);
89
90
  if (n >= 11 && isObject(content.third_party_invite) && "signed" in content.third_party_invite) c.third_party_invite = { signed: content.third_party_invite.signed };
90
91
  out.content = c;
91
92
  break;
92
93
  }
93
- case "m.room.create":
94
+ case M.ROOM.CREATE:
94
95
  out.content = n >= 11 ? { ...content } : pick("creator");
95
96
  break;
96
- case "m.room.join_rules":
97
+ case M.ROOM.JOIN_RULES:
97
98
  out.content = pick("join_rule", ...n >= 8 ? ["allow"] : []);
98
99
  break;
99
- case "m.room.power_levels":
100
+ case M.ROOM.POWER_LEVELS:
100
101
  out.content = pick("ban", "events", "events_default", "kick", "redact", "state_default", "users", "users_default", ...n >= 11 ? ["invite"] : []);
101
102
  break;
102
- case "m.room.history_visibility":
103
+ case M.ROOM.HISTORY_VISIBILITY:
103
104
  out.content = pick("history_visibility");
104
105
  break;
105
- case "m.room.aliases":
106
+ case M.ROOM.ALIASES:
106
107
  out.content = n <= 5 ? pick("aliases") : {};
107
108
  break;
108
- case "m.room.redaction":
109
+ case M.ROOM.REDACTION:
109
110
  out.content = n >= 11 ? pick("redacts") : {};
110
111
  break;
111
112
  default: out.content = {};
@@ -116,7 +117,7 @@ function redact(event, roomVersion) {
116
117
  if ("event_id" in keepRelation) rel.event_id = keepRelation.event_id;
117
118
  out.content = {
118
119
  ...out.content,
119
- "m.relates_to": rel
120
+ [M.RELATES_TO]: rel
120
121
  };
121
122
  }
122
123
  return out;
@@ -161,7 +162,7 @@ function toClientEvent(pdu, opts = {}) {
161
162
  if (opts.includeRoomId !== false) ev.room_id = pdu.room_id;
162
163
  if (pdu.state_key !== void 0) ev.state_key = pdu.state_key;
163
164
  if (pdu.redacts !== void 0) ev.redacts = pdu.redacts;
164
- else if (pdu.type === "m.room.redaction" && typeof pdu.content?.redacts === "string") ev.redacts = pdu.content.redacts;
165
+ else if (pdu.type === M.ROOM.REDACTION && typeof pdu.content?.redacts === "string") ev.redacts = pdu.content.redacts;
165
166
  ev.unsigned = {
166
167
  ...pdu.unsigned,
167
168
  age: Math.max(0, Date.now() - pdu.origin_server_ts)
@@ -0,0 +1,13 @@
1
+ //#region src/core/html.ts
2
+ /** Escape text for use in HTML content and quoted attributes. */
3
+ function escapeHtml(s) {
4
+ return s.replace(/[&<>"']/g, (c) => ({
5
+ "&": "&amp;",
6
+ "<": "&lt;",
7
+ ">": "&gt;",
8
+ "\"": "&quot;",
9
+ "'": "&#39;"
10
+ })[c]);
11
+ }
12
+ //#endregion
13
+ export { escapeHtml };
@@ -18,7 +18,7 @@ var IdentityServers = class {
18
18
  if (opts.signed) headers.authorization = this.hs.federation.client.authHeader(idServer, method, path, opts.body);
19
19
  let res;
20
20
  try {
21
- res = await this.hs.fetch(`${this.base(idServer)}${path}`, {
21
+ res = await this.hs.publicFetch(`${this.base(idServer)}${path}`, {
22
22
  method,
23
23
  headers,
24
24
  body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body),
@@ -1,7 +1,8 @@
1
1
  import { isObject } from "./json.js";
2
+ import { UNSTABLE } from "./m.js";
2
3
  import { globMatch } from "./glob.js";
3
4
  //#region src/core/invitefilter.ts
4
- const INVITE_CONFIG_TYPE = "org.matrix.msc4155.invite_permission_config";
5
+ const INVITE_CONFIG_TYPE = UNSTABLE.ORG.MATRIX.MSC4155.INVITE_PERMISSION_CONFIG;
5
6
  /**
6
7
  * MSC4155 invite filtering: "allow", "ignore" (accept but hide) or "block" (refuse).
7
8
  * User rules take precedence over server rules; within each, allow > ignore > block.
package/dist/core/json.js CHANGED
@@ -1,4 +1,13 @@
1
1
  //#region src/core/json.ts
2
+ function byCodePoint(a, b) {
3
+ for (let i = 0; i < a.length && i < b.length;) {
4
+ const ca = a.codePointAt(i);
5
+ const cb = b.codePointAt(i);
6
+ if (ca !== cb) return ca - cb;
7
+ i += ca > 65535 ? 2 : 1;
8
+ }
9
+ return a.length - b.length;
10
+ }
2
11
  /** Matrix canonical JSON: sorted keys, no whitespace, UTF-8, integers only. */
3
12
  function canonicalJson(value) {
4
13
  if (value === null || typeof value !== "object") {
@@ -7,7 +16,7 @@ function canonicalJson(value) {
7
16
  }
8
17
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
9
18
  const obj = value;
10
- return `{${Object.keys(obj).filter((k) => obj[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
19
+ return `{${Object.keys(obj).filter((k) => obj[k] !== void 0).sort(byCodePoint).map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
11
20
  }
12
21
  function isObject(value) {
13
22
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -1,3 +1,4 @@
1
+ import { Row } from "../storage/types.js";
1
2
  import { Homeserver } from "../homeserver.js";
2
3
  //#region src/core/keys.d.ts
3
4
  /** End-to-end encryption key storage (device keys, one-time keys, cross-signing). */
@@ -28,7 +29,15 @@ export declare class KeyService {
28
29
  uploadCrossSigning(userId: string, body: Record<string, any>): Promise<void>;
29
30
  uploadSignatures(signer: string, body: Record<string, any>): Promise<Record<string, any>>;
30
31
  /** Users (sharing a room with `userId`) whose device lists changed after `since`. */
31
- changesSince(userId: string, since: number, upTo: number): Promise<{
32
+ /**
33
+ * Device-list changes for /sync and /keys/changes. /sync passes what it has already read (the
34
+ * membership changes, the device-list rows, the user's joined rooms) to save the queries.
35
+ */
36
+ changesSince(userId: string, since: number, upTo: number, known?: {
37
+ memberChanges?: Row[];
38
+ deviceRows?: Row[];
39
+ joinedRooms?: string[];
40
+ }): Promise<{
32
41
  changed: string[];
33
42
  left: string[];
34
43
  }>;
package/dist/core/keys.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isObject } from "./json.js";
2
+ import { UNSTABLE } from "./m.js";
2
3
  import { errors } from "./errors.js";
3
4
  //#region src/core/keys.ts
4
5
  /** End-to-end encryption key storage (device keys, one-time keys, cross-signing). */
@@ -78,7 +79,7 @@ var KeyService = class {
78
79
  seq
79
80
  }));
80
81
  }
81
- const fallbackKeys = body.fallback_keys ?? body["org.matrix.msc2732.fallback_keys"];
82
+ const fallbackKeys = body.fallback_keys ?? body[UNSTABLE.ORG.MATRIX.MSC2732.FALLBACK_KEYS];
82
83
  if (isObject(fallbackKeys)) for (const [fullId, key] of Object.entries(fallbackKeys)) {
83
84
  const [algorithm] = fullId.split(":");
84
85
  const existing = await this.hs.db.findOne("fallback_keys", [...device, {
@@ -98,39 +99,45 @@ var KeyService = class {
98
99
  return this.oneTimeKeyCounts(userId, deviceId);
99
100
  }
100
101
  async oneTimeKeyCounts(userId, deviceId) {
101
- const rows = await this.hs.db.findMany("one_time_keys", { where: [
102
- {
103
- field: "user_id",
104
- value: userId
105
- },
106
- {
107
- field: "device_id",
108
- value: deviceId
109
- },
110
- {
111
- field: "fallback",
112
- value: false
113
- }
114
- ] });
102
+ const rows = await this.hs.db.findMany("one_time_keys", {
103
+ where: [
104
+ {
105
+ field: "user_id",
106
+ value: userId
107
+ },
108
+ {
109
+ field: "device_id",
110
+ value: deviceId
111
+ },
112
+ {
113
+ field: "fallback",
114
+ value: false
115
+ }
116
+ ],
117
+ fields: ["algorithm"]
118
+ });
115
119
  const counts = { signed_curve25519: 0 };
116
120
  for (const r of rows) counts[r.algorithm] = (counts[r.algorithm] ?? 0) + 1;
117
121
  return counts;
118
122
  }
119
123
  async unusedFallbackKeyTypes(userId, deviceId) {
120
- const rows = await this.hs.db.findMany("fallback_keys", { where: [
121
- {
122
- field: "user_id",
123
- value: userId
124
- },
125
- {
126
- field: "device_id",
127
- value: deviceId
128
- },
129
- {
130
- field: "used",
131
- value: false
132
- }
133
- ] });
124
+ const rows = await this.hs.db.findMany("fallback_keys", {
125
+ where: [
126
+ {
127
+ field: "user_id",
128
+ value: userId
129
+ },
130
+ {
131
+ field: "device_id",
132
+ value: deviceId
133
+ },
134
+ {
135
+ field: "used",
136
+ value: false
137
+ }
138
+ ],
139
+ fields: ["algorithm"]
140
+ });
134
141
  return [...new Set(rows.map((r) => r.algorithm))];
135
142
  }
136
143
  async query(requester, deviceKeys, timeoutMs = 1e4) {
@@ -412,22 +419,18 @@ var KeyService = class {
412
419
  return { failures };
413
420
  }
414
421
  /** Users (sharing a room with `userId`) whose device lists changed after `since`. */
415
- async changesSince(userId, since, upTo) {
422
+ /**
423
+ * Device-list changes for /sync and /keys/changes. /sync passes what it has already read (the
424
+ * membership changes, the device-list rows, the user's joined rooms) to save the queries.
425
+ */
426
+ async changesSince(userId, since, upTo, known = {}) {
416
427
  const sharing = await this.hs.rooms.usersSharingRooms(userId);
417
- const rows = await this.hs.db.findMany("device_list_changes", { where: [{
418
- field: "stream",
419
- operator: "gt",
420
- value: since
421
- }] });
428
+ const rows = known.deviceRows ?? await this.hs.db.findMany("device_list_changes", deviceListChangesQuery(sharing, since));
422
429
  const changed = /* @__PURE__ */ new Set();
423
430
  for (const r of rows) if (r.stream <= upTo && sharing.has(r.user_id)) changed.add(r.user_id);
424
431
  const left = /* @__PURE__ */ new Set();
425
- const memberRows = await this.hs.db.findMany("memberships", { where: [{
426
- field: "stream",
427
- operator: "gt",
428
- value: since
429
- }] });
430
- const myRooms = /* @__PURE__ */ new Set([...await this.hs.rooms.roomsForUser(userId), ...memberRows.filter((m) => m.user_id === userId).map((m) => m.room_id)]);
432
+ const memberRows = known.memberChanges ?? await this.hs.rooms.membershipChangesSince(userId, since);
433
+ const myRooms = /* @__PURE__ */ new Set([...known.joinedRooms ?? await this.hs.rooms.roomsForUser(userId), ...memberRows.filter((m) => m.user_id === userId).map((m) => m.room_id)]);
431
434
  for (const m of memberRows) {
432
435
  if (m.stream > upTo || !myRooms.has(m.room_id)) continue;
433
436
  if (m.user_id === userId) {
@@ -445,5 +448,22 @@ var KeyService = class {
445
448
  };
446
449
  }
447
450
  };
451
+ /**
452
+ * The device-list changes after `since` of the users sharing a room with someone.
453
+ * https://github.com/k18k/matrix-nodejs-homeserver/issues/31: this read every device-list change
454
+ * on the server since `since`; it asks for the users sharing a room only (up to a size where an IN
455
+ * list stays reasonable for the database).
456
+ */
457
+ function deviceListChangesQuery(sharing, since) {
458
+ return { where: [{
459
+ field: "stream",
460
+ operator: "gt",
461
+ value: since
462
+ }, ...sharing.size <= 5e3 ? [{
463
+ field: "user_id",
464
+ operator: "in",
465
+ value: [...sharing]
466
+ }] : []] };
467
+ }
448
468
  //#endregion
449
- export { KeyService };
469
+ export { KeyService, deviceListChangesQuery };