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,6 +1,7 @@
1
1
  import { toClientEvent } from "../core/events.js";
2
2
  import { errors } from "../core/errors.js";
3
3
  import { isValidRoomId } from "../core/ids.js";
4
+ import { procedure } from "../http/procedure.js";
4
5
  import { paginate, visibleState } from "./rooms.js";
5
6
  //#region src/api/legacy.ts
6
7
  const viewerOf = (s) => ({
@@ -83,88 +84,100 @@ async function eventsSince(hs, s, roomIds, from, limit) {
83
84
  return all.sort((a, b) => a.stream - b.stream).slice(0, limit);
84
85
  }
85
86
  function registerLegacyRoutes(r) {
86
- r.client("GET", "/initialSync", async (ctx) => {
87
- const s = await ctx.auth();
88
- const hs = ctx.hs;
89
- const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "10", 10) || 10, 0), 100);
90
- const memberships = [
91
- "join",
92
- "invite",
93
- ...ctx.query.get("archived") === "true" ? ["leave", "ban"] : []
94
- ];
95
- const rooms = [];
96
- for (const membership of memberships) for (const roomId of await hs.rooms.roomsForUser(s.userId, membership)) {
97
- if (membership === "invite") {
98
- const invite = await hs.rooms.membershipEvent(roomId, s.userId);
99
- rooms.push({
100
- room_id: roomId,
101
- membership,
102
- ...invite ? { invite: hs.rooms.formatEvent(invite, viewerOf(s)) } : {}
103
- });
104
- continue;
87
+ r.client("GET", "/initialSync", procedure({
88
+ auth: "user",
89
+ handler: async (ctx) => {
90
+ const s = ctx.session;
91
+ const hs = ctx.hs;
92
+ const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "10", 10) || 10, 0), 100);
93
+ const memberships = [
94
+ "join",
95
+ "invite",
96
+ ...ctx.query.get("archived") === "true" ? ["leave", "ban"] : []
97
+ ];
98
+ const rooms = [];
99
+ for (const membership of memberships) for (const roomId of await hs.rooms.roomsForUser(s.userId, membership)) {
100
+ if (membership === "invite") {
101
+ const invite = await hs.rooms.membershipEvent(roomId, s.userId);
102
+ rooms.push({
103
+ room_id: roomId,
104
+ membership,
105
+ ...invite ? { invite: hs.rooms.formatEvent(invite, viewerOf(s)) } : {}
106
+ });
107
+ continue;
108
+ }
109
+ try {
110
+ rooms.push(await roomInfo(hs, s, roomId, limit));
111
+ } catch {}
105
112
  }
106
- try {
107
- rooms.push(await roomInfo(hs, s, roomId, limit));
108
- } catch {}
113
+ const account = await hs.db.findMany("account_data", { where: [{
114
+ field: "user_id",
115
+ value: s.userId
116
+ }, {
117
+ field: "room_id",
118
+ value: ""
119
+ }] });
120
+ return {
121
+ end: `s${await hs.streams.position()}`,
122
+ rooms,
123
+ presence: [],
124
+ account_data: account.map((a) => ({
125
+ type: a.type,
126
+ content: a.content
127
+ }))
128
+ };
109
129
  }
110
- const account = await hs.db.findMany("account_data", { where: [{
111
- field: "user_id",
112
- value: s.userId
113
- }, {
114
- field: "room_id",
115
- value: ""
116
- }] });
117
- return {
118
- end: `s${await hs.streams.position()}`,
119
- rooms,
120
- presence: [],
121
- account_data: account.map((a) => ({
122
- type: a.type,
123
- content: a.content
124
- }))
125
- };
126
- });
127
- r.client("GET", "/rooms/:roomId/initialSync", async (ctx) => {
128
- const s = await ctx.auth();
129
- if (!isValidRoomId(ctx.params.roomId)) throw errors.invalidParam("Invalid room ID");
130
- const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "10", 10) || 10, 0), 100);
131
- return roomInfo(ctx.hs, s, ctx.params.roomId, limit);
132
- });
133
- r.client("GET", "/events/:eventId", async (ctx) => {
134
- const s = await ctx.auth();
135
- const ev = await ctx.hs.rooms.getEvent(ctx.params.eventId);
136
- if (!ev || !(await ctx.hs.rooms.filterVisible(s.userId, ev.pdu.room_id, [ev])).length) throw errors.notFound("Event not found");
137
- return ctx.hs.rooms.formatEvent(ev, viewerOf(s), { includeRoomId: true });
138
- });
139
- r.client("GET", "/events", async (ctx) => {
140
- const s = await ctx.auth();
141
- const hs = ctx.hs;
142
- const now = await hs.streams.position();
143
- const from = parseStreamToken(ctx.query.get("from"), now);
144
- const timeout = Math.min(Math.max(Number(ctx.query.get("timeout") ?? 3e4) || 0, 0), hs.config.maxSyncTimeoutMs);
145
- const peekRoom = ctx.query.get("room_id");
146
- let roomIds;
147
- if (peekRoom !== null) {
148
- if (!isValidRoomId(peekRoom)) throw errors.invalidParam("Invalid room ID");
149
- const { joined, worldReadable } = await hs.rooms.canPeek(peekRoom, s.userId);
150
- if (!joined && !worldReadable) throw errors.forbidden("You cannot peek into this room");
151
- roomIds = [peekRoom];
152
- } else roomIds = await hs.rooms.roomsForUser(s.userId);
153
- const deadline = Date.now() + timeout;
154
- for (;;) {
155
- const events = await eventsSince(hs, s, roomIds, from, 100);
156
- const remaining = deadline - Date.now();
157
- if (events.length || remaining <= 0 || ctx.request.signal.aborted) {
158
- const end = events.length ? events[events.length - 1].stream : Math.max(from, await hs.streams.position());
159
- return {
160
- start: `s${from}`,
161
- end: `s${end}`,
162
- chunk: events.map((e) => hs.rooms.formatEvent(e, viewerOf(s), { includeRoomId: true }))
163
- };
130
+ }));
131
+ r.client("GET", "/rooms/:roomId/initialSync", procedure({
132
+ auth: "user",
133
+ handler: async (ctx) => {
134
+ const s = ctx.session;
135
+ if (!isValidRoomId(ctx.params.roomId)) throw errors.invalidParam("Invalid room ID");
136
+ const limit = Math.min(Math.max(Number.parseInt(ctx.query.get("limit") ?? "10", 10) || 10, 0), 100);
137
+ return roomInfo(ctx.hs, s, ctx.params.roomId, limit);
138
+ }
139
+ }));
140
+ r.client("GET", "/events/:eventId", procedure({
141
+ auth: "user",
142
+ handler: async (ctx) => {
143
+ const s = ctx.session;
144
+ const ev = await ctx.hs.rooms.getEvent(ctx.params.eventId);
145
+ if (!ev || !(await ctx.hs.rooms.filterVisible(s.userId, ev.pdu.room_id, [ev])).length) throw errors.notFound("Event not found");
146
+ return ctx.hs.rooms.formatEvent(ev, viewerOf(s), { includeRoomId: true });
147
+ }
148
+ }));
149
+ r.client("GET", "/events", procedure({
150
+ auth: "user",
151
+ handler: async (ctx) => {
152
+ const s = ctx.session;
153
+ const hs = ctx.hs;
154
+ const now = await hs.streams.position();
155
+ const from = parseStreamToken(ctx.query.get("from"), now);
156
+ const timeout = Math.min(Math.max(Number(ctx.query.get("timeout") ?? 3e4) || 0, 0), hs.config.maxSyncTimeoutMs);
157
+ const peekRoom = ctx.query.get("room_id");
158
+ let roomIds;
159
+ if (peekRoom !== null) {
160
+ if (!isValidRoomId(peekRoom)) throw errors.invalidParam("Invalid room ID");
161
+ const { joined, worldReadable } = await hs.rooms.canPeek(peekRoom, s.userId);
162
+ if (!joined && !worldReadable) throw errors.forbidden("You cannot peek into this room");
163
+ roomIds = [peekRoom];
164
+ } else roomIds = await hs.rooms.roomsForUser(s.userId);
165
+ const deadline = Date.now() + timeout;
166
+ for (;;) {
167
+ const events = await eventsSince(hs, s, roomIds, from, 100);
168
+ const remaining = deadline - Date.now();
169
+ if (events.length || remaining <= 0 || ctx.request.signal.aborted) {
170
+ const end = events.length ? events[events.length - 1].stream : Math.max(from, await hs.streams.position());
171
+ return {
172
+ start: `s${from}`,
173
+ end: `s${end}`,
174
+ chunk: events.map((e) => hs.rooms.formatEvent(e, viewerOf(s), { includeRoomId: true }))
175
+ };
176
+ }
177
+ await Promise.race([hs.notifier.wait(s.userId, remaining, ctx.request.signal), new Promise((resolve) => setTimeout(resolve, Math.min(remaining, peekRoom !== null ? 1e3 : remaining)).unref?.())]);
164
178
  }
165
- await Promise.race([hs.notifier.wait(s.userId, remaining, ctx.request.signal), new Promise((resolve) => setTimeout(resolve, Math.min(remaining, peekRoom !== null ? 1e3 : remaining)).unref?.())]);
166
179
  }
167
- });
180
+ }));
168
181
  }
169
182
  //#endregion
170
183
  export { registerLegacyRoutes };
package/dist/api/media.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { randomString } from "../core/crypto.js";
2
+ import { M } from "../core/m.js";
2
3
  import { MatrixError, errors } from "../core/errors.js";
4
+ import { procedure } from "../http/procedure.js";
3
5
  //#region src/api/media.ts
4
6
  const DEFAULT_MAX_UPLOAD = 52428800;
5
7
  const INLINE_TYPES = /^(image\/(png|jpeg|gif|webp|avif|apng)|video\/(mp4|webm|ogg)|audio\/(mpeg|ogg|wav|webm|mp4|aac|flac)|text\/plain)$/;
@@ -51,7 +53,7 @@ async function remoteMedia(ctx) {
51
53
  }
52
54
  let fetched;
53
55
  try {
54
- fetched = await hs.federation.fetchMedia(serverName, mediaId);
56
+ fetched = await hs.federation.fetchMedia(serverName, mediaId, maxUpload(hs));
55
57
  } catch (err) {
56
58
  if (err instanceof MatrixError) throw err;
57
59
  throw new MatrixError(502, "M_UNKNOWN", "Could not fetch remote media");
@@ -183,19 +185,25 @@ async function thumbnail(ctx) {
183
185
  });
184
186
  }
185
187
  function registerMediaRoutes(r) {
186
- const config = (ctx) => ({ "m.upload.size": maxUpload(ctx.hs) });
187
- r.get("/_matrix/media/v3/config", async (ctx) => {
188
- await ctx.auth();
189
- return config(ctx);
190
- });
191
- r.get("/_matrix/media/r0/config", async (ctx) => {
192
- await ctx.auth();
193
- return config(ctx);
194
- });
195
- r.get("/_matrix/client/v1/media/config", async (ctx) => {
196
- await ctx.auth();
197
- return config(ctx);
198
- });
188
+ const config = (ctx) => ({ [M.UPLOAD.SIZE]: maxUpload(ctx.hs) });
189
+ r.get("/_matrix/media/v3/config", procedure({
190
+ auth: "user",
191
+ handler: async (ctx) => {
192
+ return config(ctx);
193
+ }
194
+ }));
195
+ r.get("/_matrix/media/r0/config", procedure({
196
+ auth: "user",
197
+ handler: async (ctx) => {
198
+ return config(ctx);
199
+ }
200
+ }));
201
+ r.get("/_matrix/client/v1/media/config", procedure({
202
+ auth: "user",
203
+ handler: async (ctx) => {
204
+ return config(ctx);
205
+ }
206
+ }));
199
207
  const upload = async (ctx) => {
200
208
  await ctx.auth();
201
209
  await ctx.rateLimit("upload");
@@ -203,53 +211,61 @@ function registerMediaRoutes(r) {
203
211
  await storeUpload(ctx, mediaId);
204
212
  return { content_uri: `mxc://${ctx.hs.serverName}/${mediaId}` };
205
213
  };
206
- r.post("/_matrix/media/v3/upload", upload);
207
- r.post("/_matrix/media/r0/upload", upload);
208
- r.post("/_matrix/media/v1/create", async (ctx) => {
209
- const s = await ctx.auth();
210
- const mediaId = randomString(24);
211
- await ctx.hs.db.create("media", {
212
- media_id: mediaId,
213
- content_type: null,
214
- filename: null,
215
- size: 0,
216
- uploader: s.userId,
217
- created_ts: Date.now(),
218
- pending: true
219
- });
220
- return {
221
- content_uri: `mxc://${ctx.hs.serverName}/${mediaId}`,
222
- unused_expires_at: Date.now() + 864e5
223
- };
224
- });
225
- r.put("/_matrix/media/v3/upload/:serverName/:mediaId", async (ctx) => {
226
- const s = await ctx.auth();
227
- if (ctx.params.serverName !== ctx.hs.serverName) throw errors.notFound("Unknown media");
228
- const meta = await ctx.hs.db.findOne("media", [{
229
- field: "media_id",
230
- value: ctx.params.mediaId
231
- }]);
232
- if (!meta) throw errors.notFound("Unknown media");
233
- if (meta.uploader !== s.userId) throw errors.forbidden("You did not create this media ID");
234
- if (!meta.pending) throw new MatrixError(409, "M_CANNOT_OVERWRITE_MEDIA", "Media already uploaded");
235
- await storeUpload(ctx, ctx.params.mediaId);
236
- return {};
237
- });
214
+ r.post("/_matrix/media/v3/upload", procedure({ handler: upload }));
215
+ r.post("/_matrix/media/r0/upload", procedure({ handler: upload }));
216
+ r.post("/_matrix/media/v1/create", procedure({
217
+ auth: "user",
218
+ handler: async (ctx) => {
219
+ const s = ctx.session;
220
+ const mediaId = randomString(24);
221
+ await ctx.hs.db.create("media", {
222
+ media_id: mediaId,
223
+ content_type: null,
224
+ filename: null,
225
+ size: 0,
226
+ uploader: s.userId,
227
+ created_ts: Date.now(),
228
+ pending: true
229
+ });
230
+ return {
231
+ content_uri: `mxc://${ctx.hs.serverName}/${mediaId}`,
232
+ unused_expires_at: Date.now() + 864e5
233
+ };
234
+ }
235
+ }));
236
+ r.put("/_matrix/media/v3/upload/:serverName/:mediaId", procedure({
237
+ auth: "user",
238
+ handler: async (ctx) => {
239
+ const s = ctx.session;
240
+ if (ctx.params.serverName !== ctx.hs.serverName) throw errors.notFound("Unknown media");
241
+ const meta = await ctx.hs.db.findOne("media", [{
242
+ field: "media_id",
243
+ value: ctx.params.mediaId
244
+ }]);
245
+ if (!meta) throw errors.notFound("Unknown media");
246
+ if (meta.uploader !== s.userId) throw errors.forbidden("You did not create this media ID");
247
+ if (!meta.pending) throw new MatrixError(409, "M_CANNOT_OVERWRITE_MEDIA", "Media already uploaded");
248
+ await storeUpload(ctx, ctx.params.mediaId);
249
+ return {};
250
+ }
251
+ }));
238
252
  for (const prefix of ["/_matrix/media/v3", "/_matrix/media/r0"]) {
239
- r.get(`${prefix}/download/:serverName/:mediaId`, download);
240
- r.get(`${prefix}/download/:serverName/:mediaId/:fileName`, download);
241
- r.get(`${prefix}/thumbnail/:serverName/:mediaId`, thumbnail);
253
+ r.get(`${prefix}/download/:serverName/:mediaId`, procedure({ handler: download }));
254
+ r.get(`${prefix}/download/:serverName/:mediaId/:fileName`, procedure({ handler: download }));
255
+ r.get(`${prefix}/thumbnail/:serverName/:mediaId`, procedure({ handler: thumbnail }));
242
256
  }
243
257
  const authed = async (ctx) => {
244
258
  await ctx.auth();
245
259
  return download(ctx);
246
260
  };
247
- r.get("/_matrix/client/v1/media/download/:serverName/:mediaId", authed);
248
- r.get("/_matrix/client/v1/media/download/:serverName/:mediaId/:fileName", authed);
249
- r.get("/_matrix/client/v1/media/thumbnail/:serverName/:mediaId", async (ctx) => {
250
- await ctx.auth();
251
- return thumbnail(ctx);
252
- });
261
+ r.get("/_matrix/client/v1/media/download/:serverName/:mediaId", procedure({ handler: authed }));
262
+ r.get("/_matrix/client/v1/media/download/:serverName/:mediaId/:fileName", procedure({ handler: authed }));
263
+ r.get("/_matrix/client/v1/media/thumbnail/:serverName/:mediaId", procedure({
264
+ auth: "user",
265
+ handler: async (ctx) => {
266
+ return thumbnail(ctx);
267
+ }
268
+ }));
253
269
  }
254
270
  //#endregion
255
271
  export { registerMediaRoutes };
package/dist/api/misc.js CHANGED
@@ -1,9 +1,13 @@
1
1
  import { signJson } from "../core/crypto.js";
2
+ import { M, UNSTABLE } from "../core/m.js";
2
3
  import { SUPPORTED_ROOM_VERSIONS } from "../core/events.js";
3
4
  import { errors } from "../core/errors.js";
5
+ import { procedure } from "../http/procedure.js";
4
6
  import { wellKnown } from "./account.js";
5
7
  import { canonicalJsonResponse } from "../http/handler.js";
8
+ import { readFileSync } from "node:fs";
6
9
  //#region src/api/misc.ts
10
+ const PACKAGE = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
7
11
  const SPEC_VERSIONS = [
8
12
  "r0.0.1",
9
13
  "r0.1.0",
@@ -26,48 +30,50 @@ const SPEC_VERSIONS = [
26
30
  "v1.11"
27
31
  ];
28
32
  function registerMiscRoutes(r) {
29
- r.get("/_matrix/client/versions", () => ({
33
+ r.get("/_matrix/client/versions", procedure({ handler: () => ({
30
34
  versions: SPEC_VERSIONS,
31
35
  unstable_features: {
32
- "org.matrix.simplified_msc3575": true,
33
- "org.matrix.msc3916.stable": true,
34
- "org.matrix.msc4140": true,
35
- "org.matrix.msc4143": true,
36
- "org.matrix.msc4222": true,
37
- "org.matrix.msc4306": true
36
+ [UNSTABLE.ORG.MATRIX.SIMPLIFIED_MSC3575]: true,
37
+ [UNSTABLE.ORG.MATRIX.MSC3916.STABLE]: true,
38
+ [UNSTABLE.ORG.MATRIX.MSC4140._]: true,
39
+ [UNSTABLE.ORG.MATRIX.MSC4143._]: true,
40
+ [UNSTABLE.ORG.MATRIX.MSC4222._]: true,
41
+ [UNSTABLE.ORG.MATRIX.MSC4306]: true
38
42
  }
39
- }));
40
- r.get("/.well-known/matrix/client", (ctx) => {
43
+ }) }));
44
+ r.get("/.well-known/matrix/client", procedure({ handler: (ctx) => {
41
45
  const wk = wellKnown(ctx.hs);
42
46
  if (!wk) throw errors.notFound("No well-known configured");
43
47
  return wk;
44
- });
45
- r.get("/.well-known/matrix/server", (ctx) => {
48
+ } }));
49
+ r.get("/.well-known/matrix/server", procedure({ handler: (ctx) => {
46
50
  if (!ctx.hs.config.federationAddress) throw errors.notFound("No delegation configured");
47
- return { "m.server": ctx.hs.config.federationAddress };
48
- });
49
- r.get("/.well-known/matrix/support", (ctx) => {
51
+ return { [M.SERVER]: ctx.hs.config.federationAddress };
52
+ } }));
53
+ r.get("/.well-known/matrix/support", procedure({ handler: (ctx) => {
50
54
  if (!ctx.hs.config.support) throw errors.notFound("No support information configured");
51
55
  return ctx.hs.config.support;
52
- });
53
- r.client("GET", "/capabilities", async (ctx) => {
54
- await ctx.auth();
55
- const available = {};
56
- for (const v of SUPPORTED_ROOM_VERSIONS) available[v] = /^\d+$/.test(v) ? "stable" : "unstable";
57
- return { capabilities: {
58
- "m.change_password": { enabled: !ctx.hs.oauth && ctx.hs.userAuth.passwordLoginEnabled },
59
- "m.room_versions": {
60
- default: ctx.hs.config.defaultRoomVersion,
61
- available
62
- },
63
- "m.set_displayname": { enabled: true },
64
- "m.set_avatar_url": { enabled: true },
65
- "m.profile_fields": { enabled: true },
66
- "m.3pid_changes": { enabled: ctx.hs.threepids.enabled && !ctx.hs.oauth },
67
- "m.get_login_token": { enabled: ctx.hs.config.loginViaExistingSession && !ctx.hs.oauth }
68
- } };
69
- });
70
- r.get("/_matrix/key/v2/server", (ctx) => {
56
+ } }));
57
+ r.client("GET", "/capabilities", procedure({
58
+ auth: "user",
59
+ handler: async (ctx) => {
60
+ const available = {};
61
+ for (const v of SUPPORTED_ROOM_VERSIONS) available[v] = /^\d+$/.test(v) ? "stable" : "unstable";
62
+ return { capabilities: {
63
+ [M.CHANGE_PASSWORD]: { enabled: !ctx.hs.oauth && ctx.hs.userAuth.passwordLoginEnabled },
64
+ [M.ROOM_VERSIONS]: {
65
+ default: ctx.hs.config.defaultRoomVersion,
66
+ available
67
+ },
68
+ [M.SET_DISPLAYNAME]: { enabled: true },
69
+ [M.SET_AVATAR_URL]: { enabled: true },
70
+ [M.PROFILE_FIELDS]: { enabled: true },
71
+ [M.THREEPID_CHANGES]: { enabled: ctx.hs.threepids.enabled && !ctx.hs.oauth },
72
+ [M.GET_LOGIN_TOKEN]: { enabled: ctx.hs.config.loginViaExistingSession && !ctx.hs.oauth }
73
+ } };
74
+ }
75
+ }));
76
+ r.get("/_matrix/key/v2/server", procedure({ handler: (ctx) => {
71
77
  const hs = ctx.hs;
72
78
  const key = hs.signingKey;
73
79
  return canonicalJsonResponse(signJson({
@@ -76,10 +82,13 @@ function registerMiscRoutes(r) {
76
82
  verify_keys: { [`ed25519:${key.keyId}`]: { key: key.publicKeyBase64 } },
77
83
  old_verify_keys: {}
78
84
  }, hs.serverName, key));
79
- });
80
- r.get("/_matrix/federation/v1/version", () => ({ server: {
81
- name: "matrix-nodejs-homeserver",
82
- version: "0.1.0"
85
+ } }));
86
+ r.get("/_matrix/federation/v1/version", procedure({ handler: (ctx) => {
87
+ if (!ctx.hs.federation.enabled) throw errors.unrecognized("Federation is disabled");
88
+ return { server: {
89
+ name: PACKAGE.name,
90
+ version: PACKAGE.version
91
+ } };
83
92
  } }));
84
93
  }
85
94
  //#endregion
@@ -1,39 +1,9 @@
1
1
  import { randomString } from "../core/crypto.js";
2
- import { errors } from "../core/errors.js";
3
- import { lookup } from "node:dns/promises";
4
- import { BlockList, isIP } from "node:net";
2
+ import { MatrixError, errors } from "../core/errors.js";
3
+ import { publicGet } from "../core/outbound.js";
4
+ import { procedure } from "../http/procedure.js";
5
5
  //#region src/api/preview.ts
6
6
  const MAX_BYTES = 1048576;
7
- const privateRanges = new BlockList();
8
- for (const [net, prefix] of [
9
- ["0.0.0.0", 8],
10
- ["10.0.0.0", 8],
11
- ["100.64.0.0", 10],
12
- ["127.0.0.0", 8],
13
- ["169.254.0.0", 16],
14
- ["172.16.0.0", 12],
15
- ["192.0.0.0", 24],
16
- ["192.168.0.0", 16],
17
- ["198.18.0.0", 15],
18
- ["224.0.0.0", 4],
19
- ["240.0.0.0", 4]
20
- ]) privateRanges.addSubnet(net, prefix, "ipv4");
21
- for (const [net, prefix] of [
22
- ["::", 128],
23
- ["::1", 128],
24
- ["fc00::", 7],
25
- ["fe80::", 10],
26
- ["ff00::", 8],
27
- ["::ffff:0:0", 96]
28
- ]) privateRanges.addSubnet(net, prefix, "ipv6");
29
- async function assertPublicHost(hostname) {
30
- const host = hostname.replace(/^\[|\]$/g, "");
31
- const addresses = isIP(host) ? [{
32
- address: host,
33
- family: isIP(host)
34
- }] : await lookup(host, { all: true });
35
- for (const { address, family } of addresses) if (privateRanges.check(address, family === 6 ? "ipv6" : "ipv4")) throw errors.forbidden("URL resolves to a private address");
36
- }
37
7
  function decodeEntities(s) {
38
8
  return s.replace(/&(amp|lt|gt|quot|#39|#x27);/g, (m) => ({
39
9
  "&amp;": "&",
@@ -71,42 +41,37 @@ async function preview(hs, rawUrl) {
71
41
  throw errors.invalidParam("Invalid URL");
72
42
  }
73
43
  if (url.protocol !== "http:" && url.protocol !== "https:") throw errors.invalidParam("Only http(s) URLs can be previewed");
74
- for (let redirects = 0; redirects < 5; redirects++) {
75
- if (!opts.allowPrivateAddresses) await assertPublicHost(url.hostname);
76
- const res = await fetch(url, {
77
- redirect: "manual",
78
- signal: AbortSignal.timeout(1e4),
79
- headers: { "user-agent": "matrix-nodejs-homeserver" }
80
- });
81
- if (res.status >= 300 && res.status < 400 && res.headers.get("location")) {
82
- url = new URL(res.headers.get("location"), url);
83
- continue;
84
- }
85
- if (!res.ok || !res.body) throw errors.notFound("Could not fetch URL");
86
- const type = res.headers.get("content-type") ?? "";
87
- if (type.startsWith("image/")) return {
88
- ...await storeImage(hs, res, type),
89
- "og:url": url.href
44
+ const get = opts.allowPrivateAddresses ? hs.fetch : hs.publicFetch;
45
+ const headers = { "user-agent": "matrix-nodejs-homeserver" };
46
+ const res = await publicGet(get, url, {
47
+ maxBytes: MAX_BYTES * 10,
48
+ truncate: true,
49
+ timeoutMs: 1e4,
50
+ headers
51
+ }).catch((err) => {
52
+ throw err instanceof MatrixError ? err : errors.notFound("Could not fetch URL");
53
+ });
54
+ if (res.status !== 200) throw errors.notFound("Could not fetch URL");
55
+ const type = res.headers.get("content-type") ?? "";
56
+ if (type.startsWith("image/")) {
57
+ if (res.body.length >= MAX_BYTES * 10) return {};
58
+ return {
59
+ ...await storeImage(hs, res.body, type),
60
+ "og:url": res.url.href
90
61
  };
91
- if (!type.includes("html")) return {};
92
- let size = 0;
93
- const chunks = [];
94
- for await (const chunk of res.body) {
95
- size += chunk.length;
96
- if (size > MAX_BYTES) break;
97
- chunks.push(chunk);
98
- }
99
- const og = parseOpenGraph(Buffer.concat(chunks).toString("utf8"));
100
- if (og["og:image"]) try {
101
- const imageUrl = new URL(og["og:image"], url);
102
- if (!opts.allowPrivateAddresses) await assertPublicHost(imageUrl.hostname);
103
- const img = await fetch(imageUrl, { signal: AbortSignal.timeout(1e4) });
104
- const imgType = img.headers.get("content-type") ?? "";
105
- if (img.ok && imgType.startsWith("image/")) Object.assign(og, await storeImage(hs, img, imgType));
106
- } catch {}
107
- return og;
108
62
  }
109
- throw errors.notFound("Too many redirects");
63
+ if (!type.includes("html")) return {};
64
+ const og = parseOpenGraph(res.body.subarray(0, MAX_BYTES).toString("utf8"));
65
+ if (og["og:image"]) try {
66
+ const img = await publicGet(get, new URL(og["og:image"], res.url), {
67
+ maxBytes: MAX_BYTES * 10,
68
+ timeoutMs: 1e4,
69
+ headers
70
+ });
71
+ const imgType = img.headers.get("content-type") ?? "";
72
+ if (img.status === 200 && imgType.startsWith("image/")) Object.assign(og, await storeImage(hs, img.body, imgType));
73
+ } catch {}
74
+ return og;
110
75
  }
111
76
  /** Read width/height from PNG, GIF, JPEG or WebP headers. */
112
77
  function imageSize(data) {
@@ -156,9 +121,8 @@ function imageSize(data) {
156
121
  return null;
157
122
  }
158
123
  /** Store a fetched image in the media repository and describe it for a preview. */
159
- async function storeImage(hs, res, type) {
160
- const data = new Uint8Array(await res.arrayBuffer());
161
- if (data.length > MAX_BYTES * 10) return {};
124
+ async function storeImage(hs, body, type) {
125
+ const data = new Uint8Array(body);
162
126
  const mediaId = randomString(24);
163
127
  await hs.media.put(mediaId, data, {
164
128
  contentType: type,
@@ -192,9 +156,9 @@ function registerPreviewRoutes(r) {
192
156
  if (!url) throw errors.missingParam("Missing url");
193
157
  return preview(ctx.hs, url);
194
158
  };
195
- r.get("/_matrix/media/v3/preview_url", handler);
196
- r.get("/_matrix/media/r0/preview_url", handler);
197
- r.get("/_matrix/client/v1/media/preview_url", handler);
159
+ r.get("/_matrix/media/v3/preview_url", procedure({ handler }));
160
+ r.get("/_matrix/media/r0/preview_url", procedure({ handler }));
161
+ r.get("/_matrix/client/v1/media/preview_url", procedure({ handler }));
198
162
  }
199
163
  //#endregion
200
164
  export { imageSize, parseOpenGraph, registerPreviewRoutes };