matrix-nodejs-homeserver 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +29 -7
  2. package/dist/api/account.js +385 -351
  3. package/dist/api/admin.js +76 -60
  4. package/dist/api/appservice.js +37 -25
  5. package/dist/api/backup.js +73 -62
  6. package/dist/api/federation.js +172 -138
  7. package/dist/api/filters.js +4 -3
  8. package/dist/api/legacy.js +91 -78
  9. package/dist/api/media.js +70 -54
  10. package/dist/api/misc.js +46 -37
  11. package/dist/api/preview.js +4 -3
  12. package/dist/api/profile.js +108 -97
  13. package/dist/api/push.js +114 -99
  14. package/dist/api/relationships.js +64 -58
  15. package/dist/api/rooms.js +703 -580
  16. package/dist/api/rtc.d.ts +0 -1
  17. package/dist/api/rtc.js +48 -34
  18. package/dist/api/search.js +133 -127
  19. package/dist/api/slidingsync.js +41 -31
  20. package/dist/api/spaces.js +82 -77
  21. package/dist/api/sync.js +59 -50
  22. package/dist/api/threadsubs.js +72 -61
  23. package/dist/api/threepid.js +99 -66
  24. package/dist/api/uia.js +17 -16
  25. package/dist/api/user.js +221 -178
  26. package/dist/core/admin.d.ts +0 -1
  27. package/dist/core/admin.js +6 -5
  28. package/dist/core/adminroom.js +9 -8
  29. package/dist/core/appservices.d.ts +2 -1
  30. package/dist/core/appservices.js +5 -4
  31. package/dist/core/audit.d.ts +0 -1
  32. package/dist/core/authrules.js +26 -25
  33. package/dist/core/delayed.js +2 -1
  34. package/dist/core/events.js +11 -10
  35. package/dist/core/invitefilter.js +2 -1
  36. package/dist/core/keys.js +2 -1
  37. package/dist/core/m.d.ts +209 -0
  38. package/dist/core/m.js +182 -0
  39. package/dist/core/partialstate.js +2 -1
  40. package/dist/core/policy.js +4 -3
  41. package/dist/core/push.d.ts +0 -1
  42. package/dist/core/push.js +12 -11
  43. package/dist/core/pushrules.js +36 -35
  44. package/dist/core/rooms.js +76 -74
  45. package/dist/core/stateres.js +7 -6
  46. package/dist/federation/client.js +2 -1
  47. package/dist/federation/index.d.ts +1 -1
  48. package/dist/federation/index.js +30 -29
  49. package/dist/homeserver.d.ts +2 -1
  50. package/dist/homeserver.js +1 -0
  51. package/dist/http/handler.js +2 -23
  52. package/dist/http/input.js +2 -2
  53. package/dist/http/procedure.js +25 -0
  54. package/dist/http/router.d.ts +9 -26
  55. package/dist/http/router.js +10 -30
  56. package/dist/index.d.ts +2 -1
  57. package/dist/index.js +2 -1
  58. package/dist/vendor/zod/LICENSE +21 -0
  59. package/dist/vendor/zod/v4/classic/errors.js +54 -0
  60. package/dist/vendor/zod/v4/classic/parse.js +17 -0
  61. package/dist/vendor/zod/v4/classic/schemas.js +865 -0
  62. package/dist/vendor/zod/v4/core/api.js +512 -0
  63. package/dist/vendor/zod/v4/core/checks.js +300 -0
  64. package/dist/vendor/zod/v4/core/core.js +109 -0
  65. package/dist/vendor/zod/v4/core/doc.js +35 -0
  66. package/dist/vendor/zod/v4/core/errors.js +118 -0
  67. package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
  68. package/dist/vendor/zod/v4/core/memoizer.js +255 -0
  69. package/dist/vendor/zod/v4/core/parse.js +206 -0
  70. package/dist/vendor/zod/v4/core/regexes.js +73 -0
  71. package/dist/vendor/zod/v4/core/registries.js +48 -0
  72. package/dist/vendor/zod/v4/core/schemas.js +1378 -0
  73. package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
  74. package/dist/vendor/zod/v4/core/util.js +537 -0
  75. package/dist/vendor/zod/v4/core/versions.js +8 -0
  76. package/dist/vendor/zod/v4/locales/en.js +110 -0
  77. package/package.json +3 -5
  78. package/dist/core/pushrules.d.ts +0 -1
@@ -1,5 +1,6 @@
1
1
  import { isObject } from "../core/json.js";
2
2
  import { unpaddedBase64, urlSafeBase64 } from "../core/crypto.js";
3
+ import { M } from "../core/m.js";
3
4
  import { SUPPORTED_ROOM_VERSIONS, computeContentHash, computeReferenceHash, eventIdOf, finalisePdu, redact, roomVersionFeatures, splitStateKey, stateKey, stripState } from "../core/events.js";
4
5
  import { MatrixError, errors } from "../core/errors.js";
5
6
  import { parseId } from "../core/ids.js";
@@ -14,7 +15,7 @@ import { TransactionSender } from "./sender.js";
14
15
  const domainOf = (id) => id.slice(id.indexOf(":") + 1);
15
16
  /** Server ACL check (m.room.server_acl). */
16
17
  function serverAllowedByAcl(state, serverName) {
17
- const acl = getState(state, "m.room.server_acl")?.content;
18
+ const acl = getState(state, M.ROOM.SERVER_ACL)?.content;
18
19
  if (!isObject(acl)) return true;
19
20
  const host = serverName.replace(/:\d+$/, "").replace(/^\[|\]$/g, "");
20
21
  if (acl.allow_ip_literals === false && (/^\d+\.\d+\.\d+\.\d+$/.test(host) || host.includes(":"))) return false;
@@ -37,7 +38,7 @@ var FederationService = class {
37
38
  constructor(hs, options = {}) {
38
39
  this.hs = hs;
39
40
  this.options = options;
40
- this.enabled = options.enabled !== false;
41
+ this.enabled = options.enabled === true;
41
42
  this.fasterJoins = options.fasterJoins !== false;
42
43
  this.client = new FederationClient(hs, { rejectUnauthorized: options.verifyTls !== false });
43
44
  this.keyring = new KeyRing(hs);
@@ -57,7 +58,7 @@ var FederationService = class {
57
58
  const out = { ...rest };
58
59
  const f = roomVersionFeatures(version);
59
60
  if (!f.legacyEventIds) delete out.event_id;
60
- if (f.v12 && out.type === "m.room.create") delete out.room_id;
61
+ if (f.v12 && out.type === M.ROOM.CREATE) delete out.room_id;
61
62
  return out;
62
63
  }
63
64
  computeEventId(raw, version) {
@@ -73,7 +74,7 @@ var FederationService = class {
73
74
  async verifyPdu(raw, version, roomId) {
74
75
  if (!isObject(raw) || typeof raw.type !== "string" || typeof raw.sender !== "string" || !isObject(raw.content)) throw errors.badJson("Malformed PDU");
75
76
  const f = roomVersionFeatures(version);
76
- if (!f.v12 || raw.type !== "m.room.create") {
77
+ if (!f.v12 || raw.type !== M.ROOM.CREATE) {
77
78
  if (raw.room_id !== roomId) throw errors.badJson("PDU room_id mismatch");
78
79
  }
79
80
  const eventId = this.computeEventId(raw, version);
@@ -84,7 +85,7 @@ var FederationService = class {
84
85
  if (f.legacyEventIds && domainOf(eventId) !== senderDomain && !await this.keyring.verifySignedBy(redacted, domainOf(eventId))) throw errors.forbidden(`Event ${eventId} is not signed by its origin`);
85
86
  let joinAuthorisedSignatureValid;
86
87
  const via = raw.content?.join_authorised_via_users_server;
87
- if (f.restricted && raw.type === "m.room.member" && raw.content?.membership === "join" && typeof via === "string" && via.includes(":")) joinAuthorisedSignatureValid = await this.keyring.verifySignedBy(redacted, domainOf(via));
88
+ if (f.restricted && raw.type === M.ROOM.MEMBER && raw.content?.membership === "join" && typeof via === "string" && via.includes(":")) joinAuthorisedSignatureValid = await this.keyring.verifySignedBy(redacted, domainOf(via));
88
89
  let pdu = {
89
90
  ...raw,
90
91
  event_id: eventId
@@ -93,7 +94,7 @@ var FederationService = class {
93
94
  ...redact(raw, version),
94
95
  event_id: eventId
95
96
  };
96
- if (f.v12 && pdu.type === "m.room.create") pdu.room_id = roomId;
97
+ if (f.v12 && pdu.type === M.ROOM.CREATE) pdu.room_id = roomId;
97
98
  return {
98
99
  pdu,
99
100
  joinAuthorisedSignatureValid
@@ -104,7 +105,7 @@ var FederationService = class {
104
105
  const servers = /* @__PURE__ */ new Set();
105
106
  for (const [key, ev] of state) {
106
107
  const [type, userId] = splitStateKey(key);
107
- if (type === "m.room.member" && ev.content?.membership === "join") servers.add(domainOf(userId));
108
+ if (type === M.ROOM.MEMBER && ev.content?.membership === "join") servers.add(domainOf(userId));
108
109
  }
109
110
  servers.delete(this.hs.serverName);
110
111
  for (const s of servers) if (!this.serverAllowed(s) || applyAcl && !serverAllowedByAcl(state, s)) servers.delete(s);
@@ -123,8 +124,8 @@ var FederationService = class {
123
124
  onLocalEvent(pdu, state) {
124
125
  if (!this.enabled) return;
125
126
  (async () => {
126
- const destinations = await this.roomDestinations(pdu.room_id, state, pdu.type !== "m.room.server_acl");
127
- if (pdu.type === "m.room.member" && typeof pdu.state_key === "string" && !this.hs.isLocalUser(pdu.state_key)) destinations.add(domainOf(pdu.state_key));
127
+ const destinations = await this.roomDestinations(pdu.room_id, state, pdu.type !== M.ROOM.SERVER_ACL);
128
+ if (pdu.type === M.ROOM.MEMBER && typeof pdu.state_key === "string" && !this.hs.isLocalUser(pdu.state_key)) destinations.add(domainOf(pdu.state_key));
128
129
  if (!destinations.size) return;
129
130
  const version = await this.hs.rooms.roomVersion(pdu.room_id) ?? "10";
130
131
  await this.sender.sendPdu(destinations, this.toFederationPdu(pdu, version));
@@ -147,17 +148,17 @@ var FederationService = class {
147
148
  return out;
148
149
  }
149
150
  async onTyping(roomId, userId, typing) {
150
- if (this.hs.isLocalUser(userId)) await this.sendEduToRooms([roomId], "m.typing", {
151
+ if (this.hs.isLocalUser(userId)) await this.sendEduToRooms([roomId], M.TYPING, {
151
152
  room_id: roomId,
152
153
  user_id: userId,
153
154
  typing
154
155
  }, false);
155
156
  }
156
157
  async onReceipt(roomId, userId, receiptType, eventId, ts, threadId) {
157
- if (!this.hs.isLocalUser(userId) || receiptType === "m.read.private") return;
158
+ if (!this.hs.isLocalUser(userId) || receiptType === M.READ.PRIVATE) return;
158
159
  const data = { ts };
159
160
  if (threadId) data.thread_id = threadId;
160
- await this.sendEduToRooms([roomId], "m.receipt", { [roomId]: { [receiptType]: { [userId]: {
161
+ await this.sendEduToRooms([roomId], M.RECEIPT, { [roomId]: { [receiptType]: { [userId]: {
161
162
  event_ids: [eventId],
162
163
  data
163
164
  } } } });
@@ -166,7 +167,7 @@ var FederationService = class {
166
167
  if (!this.enabled || !this.hs.isLocalUser(userId)) return;
167
168
  const servers = await this.serversSharingRooms(userId);
168
169
  if (!servers.size) return;
169
- await this.sender.sendEdu(servers, "m.presence", { push: [{
170
+ await this.sender.sendEdu(servers, M.PRESENCE, { push: [{
170
171
  user_id: userId,
171
172
  presence,
172
173
  last_active_ago: 0,
@@ -217,7 +218,7 @@ var FederationService = class {
217
218
  const d = devices.find((x) => x.device_id === u.deviceId);
218
219
  if (d?.display_name) content.device_display_name = d.display_name;
219
220
  }
220
- await this.sender.sendEdu(servers, "m.device_list_update", content);
221
+ await this.sender.sendEdu(servers, M.DEVICE_LIST_UPDATE, content);
221
222
  }
222
223
  }
223
224
  async onSigningKeyUpdate(userId) {
@@ -230,7 +231,7 @@ var FederationService = class {
230
231
  }] });
231
232
  const content = { user_id: userId };
232
233
  for (const r of rows) if (r.key_type !== "user_signing") content[`${r.key_type}_key`] = r.key;
233
- await this.sender.sendEdu(servers, "m.signing_key_update", content);
234
+ await this.sender.sendEdu(servers, M.SIGNING_KEY_UPDATE, content);
234
235
  }
235
236
  /** Deliver to-device messages addressed to remote users. */
236
237
  async sendToDevice(sender, type, messages, messageId) {
@@ -241,7 +242,7 @@ var FederationService = class {
241
242
  if (server === this.hs.serverName || !this.serverAllowed(server)) continue;
242
243
  (byServer.get(server) ?? byServer.set(server, {}).get(server))[userId] = devices;
243
244
  }
244
- for (const [server, msgs] of byServer) await this.sender.sendEdu([server], "m.direct_to_device", {
245
+ for (const [server, msgs] of byServer) await this.sender.sendEdu([server], M.DIRECT_TO_DEVICE, {
245
246
  sender,
246
247
  type,
247
248
  message_id: `${messageId}-${server}`,
@@ -301,7 +302,7 @@ var FederationService = class {
301
302
  * rescinded or their knock being accepted/rejected.
302
303
  */
303
304
  async handleOutOfRoomMembership(origin, roomId, raw) {
304
- if (raw?.type !== "m.room.member" || typeof raw.state_key !== "string" || !this.hs.isLocalUser(raw.state_key)) throw errors.notFound("Unknown room");
305
+ if (raw?.type !== M.ROOM.MEMBER || typeof raw.state_key !== "string" || !this.hs.isLocalUser(raw.state_key)) throw errors.notFound("Unknown room");
305
306
  const row = await this.hs.db.findOne("memberships", [{
306
307
  field: "room_id",
307
308
  value: roomId
@@ -311,7 +312,7 @@ var FederationService = class {
311
312
  }]);
312
313
  if (!row) throw errors.notFound("Unknown room");
313
314
  const previous = await this.hs.rooms.getPdu(row.event_id);
314
- const create = (previous?.unsigned?.invite_room_state ?? previous?.unsigned?.knock_room_state ?? []).find((e) => e?.type === "m.room.create");
315
+ const create = (previous?.unsigned?.invite_room_state ?? previous?.unsigned?.knock_room_state ?? []).find((e) => e?.type === M.ROOM.CREATE);
315
316
  const version = typeof create?.content?.room_version === "string" ? create.content.room_version : "1";
316
317
  const { pdu } = await this.verifyPdu(raw, version, roomId);
317
318
  if (domainOf(pdu.sender) !== origin && domainOf(pdu.sender) !== domainOf(raw.state_key)) throw errors.forbidden("Unexpected sender");
@@ -571,7 +572,7 @@ var FederationService = class {
571
572
  authPdus.push(p);
572
573
  }
573
574
  try {
574
- const create = roomVersionFeatures(version).v12 && pdu.type !== "m.room.create" ? await this.hs.rooms.getPdu(`$${roomId.slice(1)}`) ?? void 0 : void 0;
575
+ const create = roomVersionFeatures(version).v12 && pdu.type !== M.ROOM.CREATE ? await this.hs.rooms.getPdu(`$${roomId.slice(1)}`) ?? void 0 : void 0;
575
576
  checkAuth(pdu, authStateFromAuthEvents(pdu, authPdus, version, create), version);
576
577
  return null;
577
578
  } catch (err) {
@@ -592,7 +593,7 @@ var FederationService = class {
592
593
  const content = edu?.content;
593
594
  if (!isObject(content)) return;
594
595
  switch (edu.edu_type) {
595
- case "m.typing": {
596
+ case M.TYPING: {
596
597
  const { room_id, user_id, typing } = content;
597
598
  if (typeof user_id !== "string" || domainOf(user_id) !== origin || typeof room_id !== "string") return;
598
599
  if (!await this.hs.rooms.exists(room_id) || !await this.mayActInRoom(room_id, user_id, origin)) return;
@@ -600,12 +601,12 @@ var FederationService = class {
600
601
  await this.hs.typing.setTyping(room_id, user_id, !!typing, typeof content.timeout === "number" ? content.timeout : 3e4);
601
602
  return;
602
603
  }
603
- case "m.receipt":
604
+ case M.RECEIPT:
604
605
  for (const [roomId, byType] of Object.entries(content)) {
605
606
  if (!isObject(byType) || !await this.hs.rooms.exists(roomId)) continue;
606
607
  if (!serverAllowedByAcl(await this.hs.rooms.currentState(roomId), origin)) continue;
607
608
  for (const [receiptType, byUser] of Object.entries(byType)) {
608
- if (!["m.read", "m.read.private"].includes(receiptType) || !isObject(byUser)) continue;
609
+ if (receiptType !== M.READ._ && receiptType !== M.READ.PRIVATE || !isObject(byUser)) continue;
609
610
  for (const [userId, r] of Object.entries(byUser)) {
610
611
  if (domainOf(userId) !== origin || !isObject(r) || !Array.isArray(r.event_ids)) continue;
611
612
  if (!await this.mayActInRoom(roomId, userId, origin)) continue;
@@ -625,7 +626,7 @@ var FederationService = class {
625
626
  this.hs.notifier.notify(await this.hs.rooms.joinedMembers(roomId));
626
627
  }
627
628
  return;
628
- case "m.presence":
629
+ case M.PRESENCE:
629
630
  for (const p of Array.isArray(content.push) ? content.push : []) {
630
631
  if (!isObject(p) || typeof p.user_id !== "string" || domainOf(p.user_id) !== origin) continue;
631
632
  if (![
@@ -643,7 +644,7 @@ var FederationService = class {
643
644
  this.hs.notifier.notify(await this.hs.rooms.usersSharingRooms(p.user_id));
644
645
  }
645
646
  return;
646
- case "m.device_list_update": {
647
+ case M.DEVICE_LIST_UPDATE: {
647
648
  const userId = content.user_id;
648
649
  if (typeof userId !== "string" || domainOf(userId) !== origin || typeof content.device_id !== "string") return;
649
650
  const known = await this.hs.db.findOne("remote_device_lists", [{
@@ -655,7 +656,7 @@ var FederationService = class {
655
656
  if (known || content.deleted || isObject(content.keys)) await this.hs.keys.markDeviceListChanged(userId);
656
657
  return;
657
658
  }
658
- case "m.signing_key_update": {
659
+ case M.SIGNING_KEY_UPDATE: {
659
660
  const userId = content.user_id;
660
661
  if (typeof userId !== "string" || domainOf(userId) !== origin) return;
661
662
  for (const type of ["master", "self_signing"]) {
@@ -671,7 +672,7 @@ var FederationService = class {
671
672
  await this.hs.keys.markDeviceListChanged(userId);
672
673
  return;
673
674
  }
674
- case "m.direct_to_device": {
675
+ case M.DIRECT_TO_DEVICE: {
675
676
  const { sender, type, message_id, messages } = content;
676
677
  if (typeof sender !== "string" || domainOf(sender) !== origin || typeof type !== "string" || !isObject(messages)) return;
677
678
  if (!await this.hs.cluster.setIfAbsent(`fedtodevice:${origin}|${message_id}`, true, 864e5)) return;
@@ -751,7 +752,7 @@ var FederationService = class {
751
752
  const pdu = await this.hs.rooms.getPdu(id);
752
753
  if (!pdu) continue;
753
754
  stateIds[stateKey(pdu.type, pdu.state_key)] = id;
754
- if (pdu.type === "m.room.create") create = pdu;
755
+ if (pdu.type === M.ROOM.CREATE) create = pdu;
755
756
  }
756
757
  if (!create) throw errors.badJson("send_join response lacks the create event");
757
758
  if (!await this.hs.rooms.exists(roomId)) await this.hs.db.upsert("rooms", {
@@ -993,14 +994,14 @@ var FederationService = class {
993
994
  const out = /* @__PURE__ */ new Set();
994
995
  for (const [key, ev] of state) {
995
996
  const [type, userId] = splitStateKey(key);
996
- if (type === "m.room.member" && ev.content?.membership === "join") out.add(domainOf(userId));
997
+ if (type === M.ROOM.MEMBER && ev.content?.membership === "join") out.add(domainOf(userId));
997
998
  }
998
999
  return [...out];
999
1000
  }
1000
1001
  isMembershipVisibleTo(state, server) {
1001
1002
  for (const [key] of state) {
1002
1003
  const [type, userId] = splitStateKey(key);
1003
- if (type === "m.room.member" && domainOf(userId) === server && membershipOf(state, userId) === "join") return true;
1004
+ if (type === M.ROOM.MEMBER && domainOf(userId) === server && membershipOf(state, userId) === "join") return true;
1004
1005
  }
1005
1006
  return false;
1006
1007
  }
@@ -3,6 +3,7 @@ import { SigningKey } from "./core/crypto.js";
3
3
  import { PolicyServers } from "./core/policy.js";
4
4
  import { ThreepidOptions, ThreepidService } from "./core/threepid.js";
5
5
  import { IdentityServers } from "./core/identity.js";
6
+ import { M } from "./core/m.js";
6
7
  import { AppServiceRegistration, AppServiceService } from "./core/appservices.js";
7
8
  import { AuthService } from "./core/auth.js";
8
9
  import { KeyService } from "./core/keys.js";
@@ -52,7 +53,7 @@ export interface HomeserverOptions {
52
53
  /** Contacts and page served at `/.well-known/matrix/support` (e.g. to appeal a locked account). */
53
54
  support?: {
54
55
  contacts?: {
55
- role: "m.role.admin" | "m.role.security" | (string & {});
56
+ role: typeof M.ROLE.ADMIN | typeof M.ROLE.SECURITY | (string & {});
56
57
  matrix_id?: string;
57
58
  email_address?: string;
58
59
  }[];
@@ -1,4 +1,5 @@
1
1
  import { loadSigningKey, randomString } from "./core/crypto.js";
2
+ import "./core/m.js";
2
3
  import { SUPPORTED_ROOM_VERSIONS } from "./core/events.js";
3
4
  import { PartialStateService } from "./core/partialstate.js";
4
5
  import { RoomService } from "./core/rooms.js";
@@ -1,6 +1,6 @@
1
1
  import { canonicalJson } from "../core/json.js";
2
+ import { UNSTABLE } from "../core/m.js";
2
3
  import { MatrixError, errors } from "../core/errors.js";
3
- import { inputError } from "./input.js";
4
4
  //#region src/http/handler.ts
5
5
  const CORS_HEADERS = {
6
6
  "access-control-allow-origin": "*",
@@ -95,7 +95,7 @@ async function dispatch(hs, request, url, options) {
95
95
  const reg = hs.appservices.byToken(t);
96
96
  if (reg) {
97
97
  const userId = await hs.appservices.masquerade(reg, url.searchParams.get("user_id"));
98
- const deviceId = url.searchParams.get("device_id") ?? url.searchParams.get("org.matrix.msc3202.device_id");
98
+ const deviceId = url.searchParams.get("device_id") ?? url.searchParams.get(UNSTABLE.ORG.MATRIX.MSC3202.DEVICE_ID);
99
99
  if (deviceId && !await hs.db.findOne("devices", [{
100
100
  field: "user_id",
101
101
  value: userId
@@ -124,33 +124,12 @@ async function dispatch(hs, request, url, options) {
124
124
  async optionalAuth() {
125
125
  return token() ? ctx.auth() : null;
126
126
  },
127
- input: void 0,
128
127
  async rateLimit(action) {
129
128
  const s = token() ? await ctx.auth().catch(() => null) : null;
130
129
  if (s?.isAdmin || s?.appservice && s.appservice.rate_limited === false) return;
131
130
  await hs.rateLimits.check(action, s?.userId ?? ctx.ip ?? "unknown");
132
131
  }
133
132
  };
134
- if (matched.input) {
135
- const schema = matched.input;
136
- let raw;
137
- let readError;
138
- try {
139
- raw = await ctx.json();
140
- } catch (err) {
141
- readError = err;
142
- }
143
- let parsed = null;
144
- Object.defineProperty(ctx, "input", { get() {
145
- if (readError) throw readError;
146
- if (!parsed) {
147
- const result = schema.safeParse(raw);
148
- if (!result.success) throw inputError(result.error);
149
- parsed = { data: result.data };
150
- }
151
- return parsed.data;
152
- } });
153
- }
154
133
  const result = await matched.handler(ctx);
155
134
  if (result instanceof Response) return withCors(result);
156
135
  return jsonResponse(result ?? {});
@@ -1,8 +1,8 @@
1
1
  import { MatrixError } from "../core/errors.js";
2
- import { z } from "zod";
2
+ import { looseObject } from "../vendor/zod/v4/classic/schemas.js";
3
3
  //#region src/http/input.ts
4
4
  /** A request body: a JSON object whose unknown keys are kept. */
5
- const body = (shape) => z.looseObject(shape);
5
+ const body = (shape) => looseObject(shape);
6
6
  /**
7
7
  * Errors carry their Matrix errcode in the message, as `M_CODE: text`: `fail` builds the zod
8
8
  * error option for a field. Without one, a missing field is M_MISSING_PARAM and any other
@@ -0,0 +1,25 @@
1
+ import { inputError } from "./input.js";
2
+ //#region src/http/procedure.ts
3
+ function procedure(def) {
4
+ return async (ctx) => {
5
+ const session = def.auth === "user" ? await ctx.auth() : def.auth === "optional" ? await ctx.optionalAuth() : void 0;
6
+ const extra = def.guard ? await def.guard(ctx) : {};
7
+ if (def.rateLimit) await ctx.rateLimit(def.rateLimit);
8
+ let input;
9
+ if (def.input) {
10
+ const parsed = def.input.safeParse(await ctx.json());
11
+ if (!parsed.success) throw inputError(parsed.error);
12
+ input = parsed.data;
13
+ }
14
+ const result = await def.handler(Object.assign(ctx, extra, {
15
+ session,
16
+ input
17
+ }));
18
+ if (!def.output || result instanceof Response) return result;
19
+ const checked = def.output.safeParse(result);
20
+ if (!checked.success) throw new Error(`Response does not match the output schema: ${checked.error.message}`);
21
+ return checked.data;
22
+ };
23
+ }
24
+ //#endregion
25
+ export { procedure };
@@ -1,10 +1,9 @@
1
1
  import { Session } from "../core/auth.js";
2
2
  import { RateLimitAction } from "../core/ratelimit.js";
3
3
  import { Homeserver } from "../homeserver.js";
4
- import { z } from "zod";
5
4
  //#region src/http/router.d.ts
6
5
  export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
7
- export interface RequestContext<I = unknown> {
6
+ export interface RequestContext {
8
7
  hs: Homeserver;
9
8
  request: Request;
10
9
  url: URL;
@@ -23,38 +22,22 @@ export interface RequestContext<I = unknown> {
23
22
  accessToken(): string | null;
24
23
  /** Apply the rate limit for `action` to the requesting user (or IP when unauthenticated). */
25
24
  rateLimit(action: RateLimitAction): Promise<void>;
26
- /**
27
- * The request body parsed with the route's `input` schema (undefined for routes without one).
28
- * Validated on first access, which throws the Matrix error for an invalid body: read it where
29
- * the handler needs the body, after authenticating.
30
- */
31
- input: I;
32
25
  }
33
26
  export type HandlerResult = Response | object | void;
34
- export type Handler<I = any> = (ctx: RequestContext<I>) => Promise<HandlerResult> | HandlerResult;
35
- /** Per-route options. `input` validates and parses the JSON body before the handler runs. */
36
- export interface RouteOptions<S extends z.ZodType = z.ZodType> {
37
- input?: S;
38
- }
39
- /** A route's handler, with the body type its `input` schema produces. */
40
- type Routed<S> = S extends z.ZodType ? Handler<z.output<S>> : Handler;
27
+ /** What the router calls; endpoints build one with `procedure()` (see http/procedure.ts). */
28
+ export type Handler = (ctx: RequestContext) => Promise<HandlerResult> | HandlerResult;
41
29
  export declare class Router {
42
30
  private routes;
43
- add<S extends z.ZodType | undefined = undefined>(method: HttpMethod, path: string, handler: Routed<S>, options?: RouteOptions<NonNullable<S>>): void;
44
- get(path: string, h: Handler): void;
45
- post<S extends z.ZodType>(path: string, options: RouteOptions<S>, h: Routed<S>): void;
46
- post(path: string, h: Handler): void;
47
- put<S extends z.ZodType>(path: string, options: RouteOptions<S>, h: Routed<S>): void;
48
- put(path: string, h: Handler): void;
49
- delete(path: string, h: Handler): void;
31
+ add(method: HttpMethod, path: string, handler: Handler): void;
32
+ get: (path: string, h: Handler) => void;
33
+ post: (path: string, h: Handler) => void;
34
+ put: (path: string, h: Handler) => void;
35
+ delete: (path: string, h: Handler) => void;
50
36
  /** Register a client-server API route under both `r0` and `v3` (or the given versions). */
51
- client<S extends z.ZodType>(method: HttpMethod, path: string, options: RouteOptions<S>, handler: Routed<S>, versions?: string[]): void;
52
37
  client(method: HttpMethod, path: string, handler: Handler, versions?: string[]): void;
53
38
  match(method: string, pathname: string): {
54
39
  handler: Handler;
55
40
  params: Record<string, string>;
56
- input?: z.ZodType;
57
41
  } | "method" | null;
58
42
  }
59
- //#endregion
60
- export {};
43
+ //#endregion
@@ -1,39 +1,20 @@
1
1
  //#region src/http/router.ts
2
2
  var Router = class {
3
3
  routes = [];
4
- add(method, path, handler, options = {}) {
4
+ add(method, path, handler) {
5
5
  this.routes.push({
6
6
  method,
7
7
  segments: path.split("/").filter(Boolean),
8
- handler,
9
- input: options.input
8
+ handler
10
9
  });
11
10
  }
12
- get(path, h) {
13
- this.add("GET", path, h);
14
- }
15
- post(path, a, b) {
16
- if (typeof a === "function") this.add("POST", path, a);
17
- else this.add("POST", path, b, a);
18
- }
19
- put(path, a, b) {
20
- if (typeof a === "function") this.add("PUT", path, a);
21
- else this.add("PUT", path, b, a);
22
- }
23
- delete(path, h) {
24
- this.add("DELETE", path, h);
25
- }
26
- client(method, path, a, b, c) {
27
- const [options, handler, versions] = typeof a === "function" ? [
28
- {},
29
- a,
30
- b ?? ["r0", "v3"]
31
- ] : [
32
- a,
33
- b,
34
- c ?? ["r0", "v3"]
35
- ];
36
- for (const v of versions) this.add(method, `/_matrix/client/${v}${path}`, handler, options);
11
+ get = (path, h) => this.add("GET", path, h);
12
+ post = (path, h) => this.add("POST", path, h);
13
+ put = (path, h) => this.add("PUT", path, h);
14
+ delete = (path, h) => this.add("DELETE", path, h);
15
+ /** Register a client-server API route under both `r0` and `v3` (or the given versions). */
16
+ client(method, path, handler, versions = ["r0", "v3"]) {
17
+ for (const v of versions) this.add(method, `/_matrix/client/${v}${path}`, handler);
37
18
  }
38
19
  match(method, pathname) {
39
20
  const parts = pathname.split("/").filter(Boolean);
@@ -47,8 +28,7 @@ var Router = class {
47
28
  }
48
29
  return {
49
30
  handler: route.handler,
50
- params,
51
- input: route.input
31
+ params
52
32
  };
53
33
  }
54
34
  return methodMismatch ? "method" : null;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ThreepidEmail, ThreepidOptions, ThreepidPurpose, ThreepidSms } from "./core/threepid.js";
2
+ import { M, UNSTABLE } from "./core/m.js";
2
3
  import { AppServiceRegistration, loadAppServiceRegistrations, parseAppServiceRegistration } from "./core/appservices.js";
3
4
  import { ClusterAdapter, localCluster } from "./core/cluster.js";
4
5
  import { DatabaseAdapter, FieldDef, FindManyOptions, ModelDef, Operator, Row, Schema, WhereClause } from "./storage/types.js";
@@ -22,4 +23,4 @@ import { MysqlPoolLike, PgPoolLike, SqlAdapterOptions, SqlDialect, SqlExecutor,
22
23
  import { schema } from "./storage/schema.js";
23
24
  import { S3MediaOptions, s3Media } from "./adapters/s3.js";
24
25
  import { PgClusterPoolLike, RedisLike, RedisSubscriberLike, postgresCluster, redisCluster } from "./adapters/cluster.js";
25
- export { type AdminRoomOptions, type AdminService, type AppServiceRegistration, type AuditAction, type AuditEntry, type AuditOptions, type ClusterAdapter, type DatabaseAdapter, type DelayedEventsOptions, type FederationOptions, type FieldDef, type FindManyOptions, Homeserver, type HomeserverOptions, type Logger, MatrixError, type MediaAdapter, type MediaMetadata, type ModelDef, type MysqlPoolLike, type NodeHandler, type OAuthDelegationOptions, type Operator, type PasswordCheckContext, type PgClusterPoolLike, type PgPoolLike, type RateLimitAction, type RateLimitRule, type RateLimiter, type RedisLike, type RedisSubscriberLike, type Row, type RtcOptions, type S3MediaOptions, type Schema, type ServerNoticesOptions, type SqlAdapterOptions, type SqlDialect, type SqlExecutor, type SqliteDatabaseLike, type SsoOptions, type SsoProvider, type ThreepidEmail, type ThreepidOptions, type ThreepidPurpose, type ThreepidSms, type ThumbnailRequest, type Thumbnailer, type TurnOptions, type UserAuthAdapter, type WhereClause, createHomeserver, filesystemMedia, getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, handleRequest, isMatrixPath, loadAppServiceRegistrations, localCluster, memoryAdapter, memoryMedia, memoryRateLimiter, mysqlAdapter, parseAppServiceRegistration, postgresAdapter, postgresCluster, redisCluster, s3Media, schema, sharpThumbnailer, sqlAdapter, sqliteAdapter };
26
+ export { type AdminRoomOptions, type AdminService, type AppServiceRegistration, type AuditAction, type AuditEntry, type AuditOptions, type ClusterAdapter, type DatabaseAdapter, type DelayedEventsOptions, type FederationOptions, type FieldDef, type FindManyOptions, Homeserver, type HomeserverOptions, type Logger, M, MatrixError, type MediaAdapter, type MediaMetadata, type ModelDef, type MysqlPoolLike, type NodeHandler, type OAuthDelegationOptions, type Operator, type PasswordCheckContext, type PgClusterPoolLike, type PgPoolLike, type RateLimitAction, type RateLimitRule, type RateLimiter, type RedisLike, type RedisSubscriberLike, type Row, type RtcOptions, type S3MediaOptions, type Schema, type ServerNoticesOptions, type SqlAdapterOptions, type SqlDialect, type SqlExecutor, type SqliteDatabaseLike, type SsoOptions, type SsoProvider, type ThreepidEmail, type ThreepidOptions, type ThreepidPurpose, type ThreepidSms, type ThumbnailRequest, type Thumbnailer, type TurnOptions, UNSTABLE, type UserAuthAdapter, type WhereClause, createHomeserver, filesystemMedia, getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, handleRequest, isMatrixPath, loadAppServiceRegistrations, localCluster, memoryAdapter, memoryMedia, memoryRateLimiter, mysqlAdapter, parseAppServiceRegistration, postgresAdapter, postgresCluster, redisCluster, s3Media, schema, sharpThumbnailer, sqlAdapter, sqliteAdapter };
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { M, UNSTABLE } from "./core/m.js";
1
2
  import { MatrixError } from "./core/errors.js";
2
3
  import { localCluster } from "./core/cluster.js";
3
4
  import { loadAppServiceRegistrations, parseAppServiceRegistration } from "./core/appservices.js";
@@ -11,4 +12,4 @@ import { getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, g
11
12
  import { mysqlAdapter, postgresAdapter, sqlAdapter, sqliteAdapter } from "./storage/sql.js";
12
13
  import { s3Media } from "./adapters/s3.js";
13
14
  import { postgresCluster, redisCluster } from "./adapters/cluster.js";
14
- export { Homeserver, MatrixError, createHomeserver, filesystemMedia, getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, handleRequest, isMatrixPath, loadAppServiceRegistrations, localCluster, memoryAdapter, memoryMedia, memoryRateLimiter, mysqlAdapter, parseAppServiceRegistration, postgresAdapter, postgresCluster, redisCluster, s3Media, schema, sharpThumbnailer, sqlAdapter, sqliteAdapter };
15
+ export { Homeserver, M, MatrixError, UNSTABLE, createHomeserver, filesystemMedia, getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, handleRequest, isMatrixPath, loadAppServiceRegistrations, localCluster, memoryAdapter, memoryMedia, memoryRateLimiter, mysqlAdapter, parseAppServiceRegistration, postgresAdapter, postgresCluster, redisCluster, s3Media, schema, sharpThumbnailer, sqlAdapter, sqliteAdapter };
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Colin McDonnell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,54 @@
1
+ import { jsonStringifyReplacer } from "../core/util.js";
2
+ import { $constructor } from "../core/core.js";
3
+ import { $ZodError, flattenError, formatError } from "../core/errors.js";
4
+ //#region node_modules/zod/v4/classic/errors.js
5
+ const _installedErrorProtos = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]);
6
+ function _lazyMethod(proto, key, make) {
7
+ Object.defineProperty(proto, key, {
8
+ configurable: true,
9
+ enumerable: false,
10
+ get() {
11
+ const value = make(this);
12
+ Object.defineProperty(this, key, {
13
+ value,
14
+ configurable: true,
15
+ writable: true
16
+ });
17
+ return value;
18
+ },
19
+ set(value) {
20
+ Object.defineProperty(this, key, {
21
+ value,
22
+ configurable: true,
23
+ writable: true
24
+ });
25
+ }
26
+ });
27
+ }
28
+ const initializer = (inst, issues) => {
29
+ $ZodError.init(inst, issues);
30
+ inst.name = "ZodError";
31
+ const proto = Object.getPrototypeOf(inst);
32
+ if (_installedErrorProtos.has(proto)) return;
33
+ _installedErrorProtos.add(proto);
34
+ _lazyMethod(proto, "format", (self) => (mapper) => formatError(self, mapper));
35
+ _lazyMethod(proto, "flatten", (self) => (mapper) => flattenError(self, mapper));
36
+ _lazyMethod(proto, "addIssue", (self) => (issue) => {
37
+ self.issues.push(issue);
38
+ self.message = JSON.stringify(self.issues, jsonStringifyReplacer, 2);
39
+ });
40
+ _lazyMethod(proto, "addIssues", (self) => (issues) => {
41
+ self.issues.push(...issues);
42
+ self.message = JSON.stringify(self.issues, jsonStringifyReplacer, 2);
43
+ });
44
+ Object.defineProperty(proto, "isEmpty", {
45
+ configurable: true,
46
+ enumerable: false,
47
+ get() {
48
+ return this.issues.length === 0;
49
+ }
50
+ });
51
+ };
52
+ const ZodRealError = /*@__PURE__*/ $constructor("ZodError", initializer, void 0, { Parent: Error });
53
+ //#endregion
54
+ export { ZodRealError };
@@ -0,0 +1,17 @@
1
+ import { _decode, _decodeAsync, _encode, _encodeAsync, _parse, _parseAsync, _safeDecode, _safeDecodeAsync, _safeEncode, _safeEncodeAsync, _safeParse, _safeParseAsync } from "../core/parse.js";
2
+ import { ZodRealError } from "./errors.js";
3
+ //#region node_modules/zod/v4/classic/parse.js
4
+ const parse = /* @__PURE__ */ _parse(ZodRealError);
5
+ const parseAsync = /* @__PURE__ */ _parseAsync(ZodRealError);
6
+ const safeParse = /* @__PURE__ */ _safeParse(ZodRealError);
7
+ const safeParseAsync = /* @__PURE__ */ _safeParseAsync(ZodRealError);
8
+ const encode = /* @__PURE__ */ _encode(ZodRealError);
9
+ const decode = /* @__PURE__ */ _decode(ZodRealError);
10
+ const encodeAsync = /* @__PURE__ */ _encodeAsync(ZodRealError);
11
+ const decodeAsync = /* @__PURE__ */ _decodeAsync(ZodRealError);
12
+ const safeEncode = /* @__PURE__ */ _safeEncode(ZodRealError);
13
+ const safeDecode = /* @__PURE__ */ _safeDecode(ZodRealError);
14
+ const safeEncodeAsync = /* @__PURE__ */ _safeEncodeAsync(ZodRealError);
15
+ const safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync(ZodRealError);
16
+ //#endregion
17
+ export { decode, decodeAsync, encode, encodeAsync, parse, parseAsync, safeDecode, safeDecodeAsync, safeEncode, safeEncodeAsync, safeParse, safeParseAsync };