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/api/user.js CHANGED
@@ -1,16 +1,18 @@
1
1
  import { canonicalJson, isObject } from "../core/json.js";
2
+ import { M } from "../core/m.js";
2
3
  import { errors } from "../core/errors.js";
3
- import { requireUia } from "./uia.js";
4
+ import { array, record, string, unknown } from "../vendor/zod/v4/classic/schemas.js";
4
5
  import { body, fail } from "../http/input.js";
5
- import { z } from "zod";
6
+ import { procedure } from "../http/procedure.js";
7
+ import { requireUia } from "./uia.js";
6
8
  //#region src/api/user.ts
7
- const deleteDevicesBody = body({ devices: z.array(z.unknown(), fail("M_BAD_JSON", "devices must be a list")) });
9
+ const deleteDevicesBody = body({ devices: array(unknown(), fail("M_BAD_JSON", "devices must be a list")) });
8
10
  const keysQueryBody = body({
9
- device_keys: z.record(z.string(), z.array(z.string(), fail("M_BAD_JSON", "device_keys values must be lists of device IDs")), fail("M_BAD_JSON", "device_keys must be an object")),
10
- timeout: z.unknown().optional()
11
+ device_keys: record(string(), array(string(), fail("M_BAD_JSON", "device_keys values must be lists of device IDs")), fail("M_BAD_JSON", "device_keys must be an object")),
12
+ timeout: unknown().optional()
11
13
  });
12
- const keysClaimBody = body({ one_time_keys: z.record(z.string(), z.unknown(), fail("M_BAD_JSON", "one_time_keys must be an object")) });
13
- const SERVER_MANAGED_TYPES = ["m.fully_read", "m.push_rules"];
14
+ const keysClaimBody = body({ one_time_keys: record(string(), unknown(), fail("M_BAD_JSON", "one_time_keys must be an object")) });
15
+ const SERVER_MANAGED_TYPES = [M.FULLY_READ, M.PUSH_RULES];
14
16
  async function setAccountData(hs, userId, roomId, type, content) {
15
17
  await hs.streams.write((stream) => hs.db.upsert("account_data", {
16
18
  user_id: userId,
@@ -48,39 +50,39 @@ async function checkRoom(ctx) {
48
50
  if (!ctx.params.roomId.startsWith("!")) throw errors.invalidParam("Invalid room ID");
49
51
  }
50
52
  function registerUserRoutes(r) {
51
- r.client("PUT", "/user/:userId/account_data/:type", async (ctx) => {
53
+ r.client("PUT", "/user/:userId/account_data/:type", procedure({ handler: async (ctx) => {
52
54
  const s = await checkUser(ctx);
53
55
  if (SERVER_MANAGED_TYPES.includes(ctx.params.type)) throw errors.badJson("This type is managed by the server");
54
56
  await setAccountData(ctx.hs, s.userId, "", ctx.params.type, await ctx.json());
55
57
  return {};
56
- });
57
- r.client("GET", "/user/:userId/account_data/:type", async (ctx) => {
58
+ } }));
59
+ r.client("GET", "/user/:userId/account_data/:type", procedure({ handler: async (ctx) => {
58
60
  const s = await checkUser(ctx);
59
61
  return getAccountData(ctx.hs, s.userId, "", ctx.params.type);
60
- });
61
- r.client("PUT", "/user/:userId/rooms/:roomId/account_data/:type", async (ctx) => {
62
+ } }));
63
+ r.client("PUT", "/user/:userId/rooms/:roomId/account_data/:type", procedure({ handler: async (ctx) => {
62
64
  const s = await checkUser(ctx);
63
65
  await checkRoom(ctx);
64
66
  if (SERVER_MANAGED_TYPES.includes(ctx.params.type)) throw errors.badJson("This type is managed by the server");
65
67
  await setAccountData(ctx.hs, s.userId, ctx.params.roomId, ctx.params.type, await ctx.json());
66
68
  return {};
67
- });
68
- r.client("GET", "/user/:userId/rooms/:roomId/account_data/:type", async (ctx) => {
69
+ } }));
70
+ r.client("GET", "/user/:userId/rooms/:roomId/account_data/:type", procedure({ handler: async (ctx) => {
69
71
  const s = await checkUser(ctx);
70
72
  await checkRoom(ctx);
71
73
  return getAccountData(ctx.hs, s.userId, ctx.params.roomId, ctx.params.type);
72
- });
74
+ } }));
73
75
  const del = "/_matrix/client/unstable/org.matrix.msc3391";
74
- r.delete(`${del}/user/:userId/account_data/:type`, async (ctx) => {
76
+ r.delete(`${del}/user/:userId/account_data/:type`, procedure({ handler: async (ctx) => {
75
77
  const s = await checkUser(ctx);
76
78
  await setAccountData(ctx.hs, s.userId, "", ctx.params.type, {});
77
79
  return {};
78
- });
79
- r.delete(`${del}/user/:userId/rooms/:roomId/account_data/:type`, async (ctx) => {
80
+ } }));
81
+ r.delete(`${del}/user/:userId/rooms/:roomId/account_data/:type`, procedure({ handler: async (ctx) => {
80
82
  const s = await checkUser(ctx);
81
83
  await setAccountData(ctx.hs, s.userId, ctx.params.roomId, ctx.params.type, {});
82
84
  return {};
83
- });
85
+ } }));
84
86
  const readTags = async (hs, userId, roomId) => {
85
87
  const row = await hs.db.findOne("account_data", [
86
88
  {
@@ -93,194 +95,235 @@ function registerUserRoutes(r) {
93
95
  },
94
96
  {
95
97
  field: "type",
96
- value: "m.tag"
98
+ value: M.TAG
97
99
  }
98
100
  ]);
99
101
  return isObject(row?.content?.tags) ? row.content.tags : {};
100
102
  };
101
- r.client("GET", "/user/:userId/rooms/:roomId/tags", async (ctx) => {
103
+ r.client("GET", "/user/:userId/rooms/:roomId/tags", procedure({ handler: async (ctx) => {
102
104
  const s = await checkUser(ctx);
103
105
  return { tags: await readTags(ctx.hs, s.userId, ctx.params.roomId) };
104
- });
105
- r.client("PUT", "/user/:userId/rooms/:roomId/tags/:tag", async (ctx) => {
106
+ } }));
107
+ r.client("PUT", "/user/:userId/rooms/:roomId/tags/:tag", procedure({ handler: async (ctx) => {
106
108
  const s = await checkUser(ctx);
107
109
  const body = await ctx.json();
108
110
  if (body.order !== void 0 && typeof body.order !== "number") throw errors.badJson("order must be a number");
109
111
  const tags = await readTags(ctx.hs, s.userId, ctx.params.roomId);
110
112
  tags[ctx.params.tag] = body;
111
- await setAccountData(ctx.hs, s.userId, ctx.params.roomId, "m.tag", { tags });
113
+ await setAccountData(ctx.hs, s.userId, ctx.params.roomId, M.TAG, { tags });
112
114
  return {};
113
- });
114
- r.client("DELETE", "/user/:userId/rooms/:roomId/tags/:tag", async (ctx) => {
115
+ } }));
116
+ r.client("DELETE", "/user/:userId/rooms/:roomId/tags/:tag", procedure({ handler: async (ctx) => {
115
117
  const s = await checkUser(ctx);
116
118
  const tags = await readTags(ctx.hs, s.userId, ctx.params.roomId);
117
119
  delete tags[ctx.params.tag];
118
- await setAccountData(ctx.hs, s.userId, ctx.params.roomId, "m.tag", { tags });
120
+ await setAccountData(ctx.hs, s.userId, ctx.params.roomId, M.TAG, { tags });
119
121
  return {};
120
- });
122
+ } }));
121
123
  const formatDevice = (d) => ({
122
124
  device_id: d.device_id,
123
125
  display_name: d.display_name ?? void 0,
124
126
  last_seen_ip: d.last_seen_ip ?? void 0,
125
127
  last_seen_ts: d.last_seen_ts ?? void 0
126
128
  });
127
- r.client("GET", "/devices", async (ctx) => {
128
- const s = await ctx.auth();
129
- return { devices: (await ctx.hs.db.findMany("devices", { where: [{
130
- field: "user_id",
131
- value: s.userId
132
- }] })).map(formatDevice) };
133
- });
134
- r.client("GET", "/devices/:deviceId", async (ctx) => {
135
- const s = await ctx.auth();
136
- const row = await ctx.hs.db.findOne("devices", [{
137
- field: "user_id",
138
- value: s.userId
139
- }, {
140
- field: "device_id",
141
- value: ctx.params.deviceId
142
- }]);
143
- if (!row) throw errors.notFound("Device not found");
144
- return formatDevice(row);
145
- });
146
- r.client("PUT", "/devices/:deviceId", async (ctx) => {
147
- const s = await ctx.auth();
148
- const body = await ctx.json();
149
- const where = [{
150
- field: "user_id",
151
- value: s.userId
152
- }, {
153
- field: "device_id",
154
- value: ctx.params.deviceId
155
- }];
156
- if (!await ctx.hs.db.findOne("devices", where)) throw errors.notFound("Device not found");
157
- if (body.display_name !== void 0) {
158
- if (typeof body.display_name !== "string") throw errors.badJson("display_name must be a string");
159
- await ctx.hs.db.update("devices", where, { display_name: body.display_name });
160
- await ctx.hs.keys.deviceChanged(s.userId);
129
+ r.client("GET", "/devices", procedure({
130
+ auth: "user",
131
+ handler: async (ctx) => {
132
+ const s = ctx.session;
133
+ return { devices: (await ctx.hs.db.findMany("devices", { where: [{
134
+ field: "user_id",
135
+ value: s.userId
136
+ }] })).map(formatDevice) };
161
137
  }
162
- return {};
163
- });
164
- r.client("DELETE", "/devices/:deviceId", async (ctx) => {
165
- const s = await ctx.auth();
166
- const body = await ctx.json();
167
- const where = [{
168
- field: "user_id",
169
- value: s.userId
170
- }, {
171
- field: "device_id",
172
- value: ctx.params.deviceId
173
- }];
174
- if (!await ctx.hs.db.findOne("devices", where)) throw errors.notFound("Device not found");
175
- await requireUia(ctx.hs, body, [["m.login.password"]], s.userId);
176
- await ctx.hs.auth.deleteDevice(s.userId, ctx.params.deviceId);
177
- return {};
178
- });
179
- r.client("POST", "/delete_devices", { input: deleteDevicesBody }, async (ctx) => {
180
- const s = await ctx.auth();
181
- const body = ctx.input;
182
- await requireUia(ctx.hs, body, [["m.login.password"]], s.userId);
183
- for (const d of body.devices) if (typeof d === "string") await ctx.hs.auth.deleteDevice(s.userId, d);
184
- return {};
185
- });
186
- r.client("POST", "/keys/upload", { input: body({}) }, async (ctx) => {
187
- const s = await ctx.auth();
188
- return { one_time_key_counts: await ctx.hs.keys.upload(s.userId, s.deviceId, ctx.input) };
189
- });
190
- r.client("POST", "/keys/query", { input: keysQueryBody }, async (ctx) => {
191
- const s = await ctx.auth();
192
- await ctx.rateLimit("keyQuery");
193
- const body = ctx.input;
194
- return ctx.hs.keys.query(s.userId, body.device_keys, Math.min(Number(body.timeout ?? 1e4) || 1e4, 3e4));
195
- });
196
- r.client("POST", "/keys/claim", { input: keysClaimBody }, async (ctx) => {
197
- await ctx.auth();
198
- const body = ctx.input;
199
- const local = {};
200
- const remote = {};
201
- for (const [userId, devices] of Object.entries(body.one_time_keys)) (ctx.hs.isLocalUser(userId) ? local : remote)[userId] = devices;
202
- const claimed = await ctx.hs.keys.claim(local);
203
- const fed = Object.keys(remote).length ? await ctx.hs.federation.claimKeys(remote) : {
204
- results: {},
205
- failures: {}
206
- };
207
- return {
208
- one_time_keys: {
209
- ...claimed,
210
- ...fed.results
211
- },
212
- failures: fed.failures
213
- };
214
- });
215
- r.client("GET", "/keys/changes", async (ctx) => {
216
- const s = await ctx.auth();
217
- const parse = (v) => {
218
- const m = /^s?(\d+)/.exec(v ?? "");
219
- if (!m) throw errors.invalidParam("Invalid token");
220
- return Number(m[1]);
221
- };
222
- return ctx.hs.keys.changesSince(s.userId, parse(ctx.query.get("from")), parse(ctx.query.get("to")));
223
- });
224
- r.client("POST", "/keys/device_signing/upload", { input: body({}) }, async (ctx) => {
225
- const s = await ctx.auth();
226
- const body = ctx.input;
227
- if (await ctx.hs.db.findOne("cross_signing_keys", [{
228
- field: "user_id",
229
- value: s.userId
230
- }, {
231
- field: "key_type",
232
- value: "master"
233
- }])) {
234
- const stored = new Map((await ctx.hs.db.findMany("cross_signing_keys", { where: [{
138
+ }));
139
+ r.client("GET", "/devices/:deviceId", procedure({
140
+ auth: "user",
141
+ handler: async (ctx) => {
142
+ const s = ctx.session;
143
+ const row = await ctx.hs.db.findOne("devices", [{
235
144
  field: "user_id",
236
145
  value: s.userId
237
- }] })).map((row) => [row.key_type, canonicalJson(row.key)]));
238
- if ([
239
- "master",
240
- "self_signing",
241
- "user_signing"
242
- ].filter((type) => body[`${type}_key`] !== void 0).every((type) => stored.get(type) === canonicalJson(body[`${type}_key`]))) return {};
243
- await requireUia(ctx.hs, body, [["m.login.password"]], s.userId);
146
+ }, {
147
+ field: "device_id",
148
+ value: ctx.params.deviceId
149
+ }]);
150
+ if (!row) throw errors.notFound("Device not found");
151
+ return formatDevice(row);
244
152
  }
245
- await ctx.hs.keys.uploadCrossSigning(s.userId, body);
246
- return {};
247
- });
248
- r.client("POST", "/keys/signatures/upload", { input: body({}) }, async (ctx) => {
249
- const s = await ctx.auth();
250
- return ctx.hs.keys.uploadSignatures(s.userId, ctx.input);
251
- });
252
- r.client("PUT", "/sendToDevice/:eventType/:txnId", async (ctx) => {
253
- const s = await ctx.auth();
254
- const hs = ctx.hs;
255
- const body = await ctx.json();
256
- if (!isObject(body.messages)) throw errors.badJson("messages must be an object");
257
- const txnKey = `${s.userId}|${s.deviceId}|${ctx.params.txnId}`;
258
- if (!await hs.cluster.setIfAbsent(`todevice-txn:${txnKey}`, true, 864e5)) return {};
259
- const recipients = /* @__PURE__ */ new Set();
260
- for (const [userId, devices] of Object.entries(body.messages)) {
261
- if (!isObject(devices) || !hs.isLocalUser(userId)) continue;
262
- let targets = Object.keys(devices);
263
- if (targets.includes("*")) targets = (await hs.db.findMany("devices", { where: [{
153
+ }));
154
+ r.client("PUT", "/devices/:deviceId", procedure({
155
+ auth: "user",
156
+ handler: async (ctx) => {
157
+ const s = ctx.session;
158
+ const body = await ctx.json();
159
+ const where = [{
264
160
  field: "user_id",
265
- value: userId
266
- }] })).map((d) => d.device_id);
267
- for (const deviceId of targets) {
268
- const content = devices[deviceId] ?? devices["*"];
269
- await hs.streams.write((stream) => hs.db.create("to_device", {
270
- stream,
271
- user_id: userId,
272
- device_id: deviceId,
273
- sender: s.userId,
274
- type: ctx.params.eventType,
275
- content
276
- }));
161
+ value: s.userId
162
+ }, {
163
+ field: "device_id",
164
+ value: ctx.params.deviceId
165
+ }];
166
+ if (!await ctx.hs.db.findOne("devices", where)) throw errors.notFound("Device not found");
167
+ if (body.display_name !== void 0) {
168
+ if (typeof body.display_name !== "string") throw errors.badJson("display_name must be a string");
169
+ await ctx.hs.db.update("devices", where, { display_name: body.display_name });
170
+ await ctx.hs.keys.deviceChanged(s.userId);
277
171
  }
278
- recipients.add(userId);
172
+ return {};
279
173
  }
280
- hs.notifier.notify(recipients);
281
- await hs.federation.sendToDevice(s.userId, ctx.params.eventType, body.messages, `${s.deviceId}-${ctx.params.txnId}`);
282
- return {};
283
- });
174
+ }));
175
+ r.client("DELETE", "/devices/:deviceId", procedure({
176
+ auth: "user",
177
+ handler: async (ctx) => {
178
+ const s = ctx.session;
179
+ const body = await ctx.json();
180
+ const where = [{
181
+ field: "user_id",
182
+ value: s.userId
183
+ }, {
184
+ field: "device_id",
185
+ value: ctx.params.deviceId
186
+ }];
187
+ if (!await ctx.hs.db.findOne("devices", where)) throw errors.notFound("Device not found");
188
+ await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId);
189
+ await ctx.hs.auth.deleteDevice(s.userId, ctx.params.deviceId);
190
+ return {};
191
+ }
192
+ }));
193
+ r.client("POST", "/delete_devices", procedure({
194
+ auth: "user",
195
+ input: deleteDevicesBody,
196
+ handler: async (ctx) => {
197
+ const s = ctx.session;
198
+ const body = ctx.input;
199
+ await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId);
200
+ for (const d of body.devices) if (typeof d === "string") await ctx.hs.auth.deleteDevice(s.userId, d);
201
+ return {};
202
+ }
203
+ }));
204
+ r.client("POST", "/keys/upload", procedure({
205
+ auth: "user",
206
+ input: body({}),
207
+ handler: async (ctx) => {
208
+ const s = ctx.session;
209
+ return { one_time_key_counts: await ctx.hs.keys.upload(s.userId, s.deviceId, ctx.input) };
210
+ }
211
+ }));
212
+ r.client("POST", "/keys/query", procedure({
213
+ auth: "user",
214
+ rateLimit: "keyQuery",
215
+ input: keysQueryBody,
216
+ handler: async (ctx) => {
217
+ const s = ctx.session;
218
+ const body = ctx.input;
219
+ return ctx.hs.keys.query(s.userId, body.device_keys, Math.min(Number(body.timeout ?? 1e4) || 1e4, 3e4));
220
+ }
221
+ }));
222
+ r.client("POST", "/keys/claim", procedure({
223
+ auth: "user",
224
+ input: keysClaimBody,
225
+ handler: async (ctx) => {
226
+ const body = ctx.input;
227
+ const local = {};
228
+ const remote = {};
229
+ for (const [userId, devices] of Object.entries(body.one_time_keys)) (ctx.hs.isLocalUser(userId) ? local : remote)[userId] = devices;
230
+ const claimed = await ctx.hs.keys.claim(local);
231
+ const fed = Object.keys(remote).length ? await ctx.hs.federation.claimKeys(remote) : {
232
+ results: {},
233
+ failures: {}
234
+ };
235
+ return {
236
+ one_time_keys: {
237
+ ...claimed,
238
+ ...fed.results
239
+ },
240
+ failures: fed.failures
241
+ };
242
+ }
243
+ }));
244
+ r.client("GET", "/keys/changes", procedure({
245
+ auth: "user",
246
+ handler: async (ctx) => {
247
+ const s = ctx.session;
248
+ const parse = (v) => {
249
+ const m = /^s?(\d+)/.exec(v ?? "");
250
+ if (!m) throw errors.invalidParam("Invalid token");
251
+ return Number(m[1]);
252
+ };
253
+ return ctx.hs.keys.changesSince(s.userId, parse(ctx.query.get("from")), parse(ctx.query.get("to")));
254
+ }
255
+ }));
256
+ r.client("POST", "/keys/device_signing/upload", procedure({
257
+ auth: "user",
258
+ input: body({}),
259
+ handler: async (ctx) => {
260
+ const s = ctx.session;
261
+ const body = ctx.input;
262
+ if (await ctx.hs.db.findOne("cross_signing_keys", [{
263
+ field: "user_id",
264
+ value: s.userId
265
+ }, {
266
+ field: "key_type",
267
+ value: "master"
268
+ }])) {
269
+ const stored = new Map((await ctx.hs.db.findMany("cross_signing_keys", { where: [{
270
+ field: "user_id",
271
+ value: s.userId
272
+ }] })).map((row) => [row.key_type, canonicalJson(row.key)]));
273
+ if ([
274
+ "master",
275
+ "self_signing",
276
+ "user_signing"
277
+ ].filter((type) => body[`${type}_key`] !== void 0).every((type) => stored.get(type) === canonicalJson(body[`${type}_key`]))) return {};
278
+ await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId);
279
+ }
280
+ await ctx.hs.keys.uploadCrossSigning(s.userId, body);
281
+ return {};
282
+ }
283
+ }));
284
+ r.client("POST", "/keys/signatures/upload", procedure({
285
+ auth: "user",
286
+ input: body({}),
287
+ handler: async (ctx) => {
288
+ const s = ctx.session;
289
+ return ctx.hs.keys.uploadSignatures(s.userId, ctx.input);
290
+ }
291
+ }));
292
+ r.client("PUT", "/sendToDevice/:eventType/:txnId", procedure({
293
+ auth: "user",
294
+ handler: async (ctx) => {
295
+ const s = ctx.session;
296
+ const hs = ctx.hs;
297
+ const body = await ctx.json();
298
+ if (!isObject(body.messages)) throw errors.badJson("messages must be an object");
299
+ const txnKey = `${s.userId}|${s.deviceId}|${ctx.params.txnId}`;
300
+ if (!await hs.cluster.setIfAbsent(`todevice-txn:${txnKey}`, true, 864e5)) return {};
301
+ const recipients = /* @__PURE__ */ new Set();
302
+ for (const [userId, devices] of Object.entries(body.messages)) {
303
+ if (!isObject(devices) || !hs.isLocalUser(userId)) continue;
304
+ let targets = Object.keys(devices);
305
+ if (targets.includes("*")) targets = (await hs.db.findMany("devices", { where: [{
306
+ field: "user_id",
307
+ value: userId
308
+ }] })).map((d) => d.device_id);
309
+ for (const deviceId of targets) {
310
+ const content = devices[deviceId] ?? devices["*"];
311
+ await hs.streams.write((stream) => hs.db.create("to_device", {
312
+ stream,
313
+ user_id: userId,
314
+ device_id: deviceId,
315
+ sender: s.userId,
316
+ type: ctx.params.eventType,
317
+ content
318
+ }));
319
+ }
320
+ recipients.add(userId);
321
+ }
322
+ hs.notifier.notify(recipients);
323
+ await hs.federation.sendToDevice(s.userId, ctx.params.eventType, body.messages, `${s.deviceId}-${ctx.params.txnId}`);
324
+ return {};
325
+ }
326
+ }));
284
327
  }
285
328
  //#endregion
286
329
  export { registerUserRoutes };
@@ -1,6 +1,5 @@
1
1
  import { Row } from "../storage/types.js";
2
2
  import { Homeserver } from "../homeserver.js";
3
- import "../index.js";
4
3
  //#region src/core/admin.d.ts
5
4
  export interface ServerNoticesOptions {
6
5
  /** Localpart of the user that sends server notices. Default: "server". */
@@ -1,4 +1,5 @@
1
1
  import { randomString } from "./crypto.js";
2
+ import { M } from "./m.js";
2
3
  import { errors } from "./errors.js";
3
4
  import { LOCALPART_RE, isValidUserId } from "./ids.js";
4
5
  import { createUser } from "../api/account.js";
@@ -259,7 +260,7 @@ var AdminService = class {
259
260
  const membership = await this.hs.rooms.membership(roomId, userId);
260
261
  if (membership !== "join" && membership !== "invite") await this.hs.rooms.invite(roomId, noticeUser, userId);
261
262
  const eventId = await this.hs.rooms.sendEvent(roomId, noticeUser, {
262
- type: opts.type ?? "m.room.message",
263
+ type: opts.type ?? M.ROOM.MESSAGE,
263
264
  content
264
265
  });
265
266
  if (txnKey) await this.hs.cluster.set(txnKey, eventId, 864e5);
@@ -282,9 +283,9 @@ var AdminService = class {
282
283
  },
283
284
  {
284
285
  field: "type",
285
- value: "m.tag"
286
+ value: M.TAG
286
287
  }
287
- ]))?.content?.tags?.["m.server_notice"]) return room.room_id;
288
+ ]))?.content?.tags?.[M.SERVER_NOTICE]) return room.room_id;
288
289
  const { room_id } = await this.hs.rooms.createRoom(noticeUser, {
289
290
  preset: "private_chat",
290
291
  name: this.hs.config.serverNotices.roomName,
@@ -296,8 +297,8 @@ var AdminService = class {
296
297
  await this.hs.streams.write((stream) => this.hs.db.upsert("account_data", {
297
298
  user_id: userId,
298
299
  room_id,
299
- type: "m.tag",
300
- content: { tags: { "m.server_notice": { order: .5 } } },
300
+ type: M.TAG,
301
+ content: { tags: { [M.SERVER_NOTICE]: { order: .5 } } },
301
302
  stream
302
303
  }));
303
304
  return room_id;
@@ -1,4 +1,5 @@
1
1
  import { randomString } from "./crypto.js";
2
+ import { M, UNSTABLE } from "./m.js";
2
3
  import { MatrixError } from "./errors.js";
3
4
  import { isValidRoomId, isValidUserId } from "./ids.js";
4
5
  import { createUser } from "../api/account.js";
@@ -93,7 +94,7 @@ var AdminRoomService = class {
93
94
  ban: 100
94
95
  },
95
96
  initial_state: [{
96
- type: "m.room.history_visibility",
97
+ type: M.ROOM.HISTORY_VISIBILITY,
97
98
  state_key: "",
98
99
  content: { history_visibility: "joined" }
99
100
  }]
@@ -144,23 +145,23 @@ var AdminRoomService = class {
144
145
  if (!roomId) return;
145
146
  const escaped = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
146
147
  const content = {
147
- msgtype: "m.notice",
148
+ msgtype: M.NOTICE,
148
149
  body: text,
149
- format: "org.matrix.custom.html",
150
+ format: UNSTABLE.ORG.MATRIX.CUSTOM.HTML,
150
151
  formatted_body: text.includes("\n") ? `<pre><code>${escaped}</code></pre>` : escaped.replace(/`([^`]+)`/g, "<code>$1</code>")
151
152
  };
152
- if (replyTo) content["m.relates_to"] = { "m.in_reply_to": { event_id: replyTo } };
153
+ if (replyTo) content[M.RELATES_TO] = { [M.IN_REPLY_TO]: { event_id: replyTo } };
153
154
  await this.hs.rooms.sendEvent(roomId, this.botUserId, {
154
- type: "m.room.message",
155
+ type: M.ROOM.MESSAGE,
155
156
  content
156
157
  });
157
158
  }
158
159
  /** Called for every new event; handles commands posted in the admin room. */
159
160
  async onEvent(event) {
160
- if (!this.options.enabled || event.type !== "m.room.message" || event.sender === this.botUserId) return;
161
+ if (!this.options.enabled || event.type !== M.ROOM.MESSAGE || event.sender === this.botUserId) return;
161
162
  if (event.room_id !== await this.roomId()) return;
162
163
  const body = typeof event.content?.body === "string" ? event.content.body.trim() : "";
163
- if (!body || event.content?.msgtype === "m.notice") return;
164
+ if (!body || event.content?.msgtype === M.NOTICE) return;
164
165
  if (!(await this.hs.db.findOne("users", [{
165
166
  field: "user_id",
166
167
  value: event.sender
@@ -418,7 +419,7 @@ var AdminRoomService = class {
418
419
  const message = pos.slice(2).join(" ");
419
420
  if (!message) return "Usage: notice <user> <message>";
420
421
  await hs.admin.sendServerNotice(target, {
421
- msgtype: "m.text",
422
+ msgtype: M.TEXT,
422
423
  body: message
423
424
  });
424
425
  await hs.audit.record({
@@ -1,5 +1,6 @@
1
1
  import { PDU } from "./events.js";
2
2
  import { StateMap } from "./authrules.js";
3
+ import { UNSTABLE } from "./m.js";
3
4
  import { Homeserver } from "../homeserver.js";
4
5
  //#region src/core/appservices.d.ts
5
6
  export interface AppServiceNamespace {
@@ -22,7 +23,7 @@ export interface AppServiceRegistration {
22
23
  rate_limited?: boolean;
23
24
  protocols?: string[];
24
25
  /** MSC2409: also push ephemeral events (typing, receipts, presence). */
25
- "de.sorunome.msc2409.push_ephemeral"?: boolean;
26
+ [UNSTABLE.DE.SORUNOME.MSC2409.PUSH_EPHEMERAL]?: boolean;
26
27
  receive_ephemeral?: boolean;
27
28
  }
28
29
  /**
@@ -1,3 +1,4 @@
1
+ import { M, UNSTABLE } from "./m.js";
1
2
  import { splitStateKey, toClientEvent } from "./events.js";
2
3
  import { MatrixError, errors } from "./errors.js";
3
4
  import { readFile, readdir } from "node:fs/promises";
@@ -159,7 +160,7 @@ var AppServiceService = class {
159
160
  async interestedInEvent(svc, pdu, state) {
160
161
  const reg = svc.reg;
161
162
  if (this.isInterestedUser(reg, pdu.sender)) return true;
162
- if (pdu.type === "m.room.member" && typeof pdu.state_key === "string" && this.isInterestedUser(reg, pdu.state_key)) return true;
163
+ if (pdu.type === M.ROOM.MEMBER && typeof pdu.state_key === "string" && this.isInterestedUser(reg, pdu.state_key)) return true;
163
164
  if (svc.rooms.some((n) => n.re.test(pdu.room_id))) return true;
164
165
  if (svc.aliases.length) {
165
166
  if ((await this.hs.db.findMany("room_aliases", { where: [{
@@ -169,7 +170,7 @@ var AppServiceService = class {
169
170
  }
170
171
  for (const [key, ev] of state) {
171
172
  const [type, userId] = splitStateKey(key);
172
- if (type === "m.room.member" && ev.content?.membership === "join" && this.isInterestedUser(reg, userId)) return true;
173
+ if (type === M.ROOM.MEMBER && ev.content?.membership === "join" && this.isInterestedUser(reg, userId)) return true;
173
174
  }
174
175
  return false;
175
176
  }
@@ -184,7 +185,7 @@ var AppServiceService = class {
184
185
  async onEphemeral(roomId, event, users) {
185
186
  for (const svc of this.services) {
186
187
  const reg = svc.reg;
187
- if (!reg.url || !(reg.receive_ephemeral || reg["de.sorunome.msc2409.push_ephemeral"])) continue;
188
+ if (!reg.url || !(reg.receive_ephemeral || reg[UNSTABLE.DE.SORUNOME.MSC2409.PUSH_EPHEMERAL])) continue;
188
189
  let interested = users.some((u) => this.isInterestedUser(reg, u)) || roomId !== null && svc.rooms.some((n) => n.re.test(roomId));
189
190
  if (!interested && roomId) {
190
191
  for (const u of await this.hs.rooms.joinedMembers(roomId)) if (this.isInterestedUser(reg, u)) {
@@ -242,7 +243,7 @@ var AppServiceService = class {
242
243
  const ephemeral = rows.filter((r) => r.kind === "ephemeral").map((r) => r.payload);
243
244
  if (ephemeral.length) {
244
245
  body.ephemeral = ephemeral;
245
- body["de.sorunome.msc2409.ephemeral"] = ephemeral;
246
+ body[UNSTABLE.DE.SORUNOME.MSC2409.EPHEMERAL] = ephemeral;
246
247
  }
247
248
  const txnId = String(rows[rows.length - 1].id);
248
249
  try {
@@ -1,6 +1,5 @@
1
1
  import { Row } from "../storage/types.js";
2
2
  import { Homeserver } from "../homeserver.js";
3
- import "../index.js";
4
3
  //#region src/core/audit.d.ts
5
4
  /** Security-relevant actions recorded in the audit log. */
6
5
  export type AuditAction = "login" | "login_failed" | "logout" | "register" | "password_change" | "deactivate" | "device_create" | "device_delete" | "token_refresh" | "room_create" | "room_delete" | "member_kick" | "member_ban" | "member_unban" | "admin_action" | "admin_command" | "registration_token_create" | "registration_token_delete" | "registration_setting" | "event_report" | "user_report" | "sso_login" | "oauth_login" | "user_lock" | "user_unlock" | "user_suspend" | "user_unsuspend";