matrix-nodejs-homeserver 0.0.0-stage → 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 (141) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +479 -2
  3. package/dist/adapters/auth.d.ts +50 -0
  4. package/dist/adapters/auth.js +18 -0
  5. package/dist/adapters/cluster.d.ts +55 -0
  6. package/dist/adapters/cluster.js +249 -0
  7. package/dist/adapters/media.d.ts +50 -0
  8. package/dist/adapters/media.js +63 -0
  9. package/dist/adapters/s3.d.ts +33 -0
  10. package/dist/adapters/s3.js +95 -0
  11. package/dist/api/account.js +594 -0
  12. package/dist/api/admin.js +267 -0
  13. package/dist/api/appservice.js +45 -0
  14. package/dist/api/backup.js +280 -0
  15. package/dist/api/federation.js +826 -0
  16. package/dist/api/filters.js +88 -0
  17. package/dist/api/index.js +47 -0
  18. package/dist/api/legacy.js +183 -0
  19. package/dist/api/media.js +271 -0
  20. package/dist/api/misc.js +95 -0
  21. package/dist/api/preview.js +201 -0
  22. package/dist/api/profile.js +176 -0
  23. package/dist/api/push.js +202 -0
  24. package/dist/api/relationships.js +270 -0
  25. package/dist/api/rooms.js +1487 -0
  26. package/dist/api/rtc.d.ts +23 -0
  27. package/dist/api/rtc.js +66 -0
  28. package/dist/api/search.js +161 -0
  29. package/dist/api/slidingsync.js +474 -0
  30. package/dist/api/spaces.js +177 -0
  31. package/dist/api/sync.js +609 -0
  32. package/dist/api/threadsubs.js +140 -0
  33. package/dist/api/threepid.js +155 -0
  34. package/dist/api/uia.js +127 -0
  35. package/dist/api/user.js +329 -0
  36. package/dist/core/admin.d.ts +84 -0
  37. package/dist/core/admin.js +308 -0
  38. package/dist/core/adminroom.d.ts +49 -0
  39. package/dist/core/adminroom.js +483 -0
  40. package/dist/core/appservices.d.ts +74 -0
  41. package/dist/core/appservices.js +369 -0
  42. package/dist/core/audit.d.ts +50 -0
  43. package/dist/core/audit.js +86 -0
  44. package/dist/core/auth.d.ts +48 -0
  45. package/dist/core/auth.js +237 -0
  46. package/dist/core/authrules.d.ts +4 -0
  47. package/dist/core/authrules.js +348 -0
  48. package/dist/core/cluster.d.ts +66 -0
  49. package/dist/core/cluster.js +191 -0
  50. package/dist/core/crypto.d.ts +10 -0
  51. package/dist/core/crypto.js +100 -0
  52. package/dist/core/delayed.d.ts +39 -0
  53. package/dist/core/delayed.js +160 -0
  54. package/dist/core/errors.d.ts +9 -0
  55. package/dist/core/errors.js +45 -0
  56. package/dist/core/events.d.ts +32 -0
  57. package/dist/core/events.js +186 -0
  58. package/dist/core/glob.js +66 -0
  59. package/dist/core/identity.d.ts +24 -0
  60. package/dist/core/identity.js +96 -0
  61. package/dist/core/ids.js +36 -0
  62. package/dist/core/invitefilter.js +36 -0
  63. package/dist/core/json.js +16 -0
  64. package/dist/core/keys.d.ts +36 -0
  65. package/dist/core/keys.js +450 -0
  66. package/dist/core/m.d.ts +209 -0
  67. package/dist/core/m.js +182 -0
  68. package/dist/core/notifier.d.ts +17 -0
  69. package/dist/core/notifier.js +54 -0
  70. package/dist/core/oauth.d.ts +38 -0
  71. package/dist/core/oauth.js +108 -0
  72. package/dist/core/partialstate.d.ts +32 -0
  73. package/dist/core/partialstate.js +198 -0
  74. package/dist/core/phone.js +268 -0
  75. package/dist/core/policy.d.ts +29 -0
  76. package/dist/core/policy.js +97 -0
  77. package/dist/core/profile.js +39 -0
  78. package/dist/core/push.d.ts +46 -0
  79. package/dist/core/push.js +307 -0
  80. package/dist/core/pushrules.js +229 -0
  81. package/dist/core/ratelimit.d.ts +23 -0
  82. package/dist/core/ratelimit.js +77 -0
  83. package/dist/core/rooms.d.ts +215 -0
  84. package/dist/core/rooms.js +1731 -0
  85. package/dist/core/settings.d.ts +11 -0
  86. package/dist/core/settings.js +29 -0
  87. package/dist/core/sso.d.ts +46 -0
  88. package/dist/core/sso.js +165 -0
  89. package/dist/core/stateres.d.ts +4 -0
  90. package/dist/core/stateres.js +209 -0
  91. package/dist/core/threepid.d.ts +80 -0
  92. package/dist/core/threepid.js +275 -0
  93. package/dist/core/typing.d.ts +16 -0
  94. package/dist/core/typing.js +60 -0
  95. package/dist/federation/client.d.ts +51 -0
  96. package/dist/federation/client.js +279 -0
  97. package/dist/federation/index.d.ts +153 -0
  98. package/dist/federation/index.js +1059 -0
  99. package/dist/federation/keyring.d.ts +26 -0
  100. package/dist/federation/keyring.js +122 -0
  101. package/dist/federation/sender.d.ts +26 -0
  102. package/dist/federation/sender.js +164 -0
  103. package/dist/homeserver.d.ts +224 -0
  104. package/dist/homeserver.js +302 -0
  105. package/dist/http/adapters.d.ts +40 -0
  106. package/dist/http/adapters.js +130 -0
  107. package/dist/http/handler.d.ts +8 -0
  108. package/dist/http/handler.js +181 -0
  109. package/dist/http/input.js +24 -0
  110. package/dist/http/procedure.js +25 -0
  111. package/dist/http/router.d.ts +43 -0
  112. package/dist/http/router.js +56 -0
  113. package/dist/index.d.ts +26 -0
  114. package/dist/index.js +15 -0
  115. package/dist/storage/memory.d.ts +5 -0
  116. package/dist/storage/memory.js +122 -0
  117. package/dist/storage/schema.d.ts +4 -0
  118. package/dist/storage/schema.js +664 -0
  119. package/dist/storage/sql.d.ts +72 -0
  120. package/dist/storage/sql.js +274 -0
  121. package/dist/storage/types.d.ts +55 -0
  122. package/dist/vendor/zod/LICENSE +21 -0
  123. package/dist/vendor/zod/v4/classic/errors.js +54 -0
  124. package/dist/vendor/zod/v4/classic/parse.js +17 -0
  125. package/dist/vendor/zod/v4/classic/schemas.js +865 -0
  126. package/dist/vendor/zod/v4/core/api.js +512 -0
  127. package/dist/vendor/zod/v4/core/checks.js +300 -0
  128. package/dist/vendor/zod/v4/core/core.js +109 -0
  129. package/dist/vendor/zod/v4/core/doc.js +35 -0
  130. package/dist/vendor/zod/v4/core/errors.js +118 -0
  131. package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
  132. package/dist/vendor/zod/v4/core/memoizer.js +255 -0
  133. package/dist/vendor/zod/v4/core/parse.js +206 -0
  134. package/dist/vendor/zod/v4/core/regexes.js +73 -0
  135. package/dist/vendor/zod/v4/core/registries.js +48 -0
  136. package/dist/vendor/zod/v4/core/schemas.js +1378 -0
  137. package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
  138. package/dist/vendor/zod/v4/core/util.js +537 -0
  139. package/dist/vendor/zod/v4/core/versions.js +8 -0
  140. package/dist/vendor/zod/v4/locales/en.js +110 -0
  141. package/package.json +65 -4
@@ -0,0 +1,237 @@
1
+ import { randomToken } from "./crypto.js";
2
+ import { errors } from "./errors.js";
3
+ import { generateDeviceId } from "./ids.js";
4
+ //#region src/core/auth.ts
5
+ var AuthService = class {
6
+ hs;
7
+ cache = /* @__PURE__ */ new Map();
8
+ constructor(hs) {
9
+ this.hs = hs;
10
+ hs.cluster.subscribe("auth", (msg) => {
11
+ if (msg?.from === hs.instanceId) return;
12
+ if (msg.token) this.cache.delete(msg.token);
13
+ if (msg.userId) {
14
+ for (const [t, s] of this.cache) if (s.userId === msg.userId) this.cache.delete(t);
15
+ }
16
+ });
17
+ }
18
+ async authenticate(token, ip) {
19
+ const cached = this.cache.get(token);
20
+ if (cached) return cached;
21
+ const row = await this.hs.db.findOne("access_tokens", [{
22
+ field: "token",
23
+ value: token
24
+ }]);
25
+ if (!row) {
26
+ if (this.hs.oauth) return this.hs.oauth.authenticate(token);
27
+ throw errors.unknownToken();
28
+ }
29
+ if (row.expires_ts !== null && row.expires_ts < Date.now()) throw errors.unknownToken("Access token expired", true);
30
+ const user = await this.hs.db.findOne("users", [{
31
+ field: "user_id",
32
+ value: row.user_id
33
+ }]);
34
+ if (!user || user.deactivated) throw errors.unknownToken();
35
+ const session = {
36
+ userId: row.user_id,
37
+ deviceId: row.device_id,
38
+ accessToken: token,
39
+ isGuest: !!user.is_guest,
40
+ isAdmin: !!user.is_admin,
41
+ locked: !!user.locked,
42
+ suspended: !!user.suspended
43
+ };
44
+ if (row.expires_ts === null) this.cache.set(token, session);
45
+ await this.hs.db.update("devices", [{
46
+ field: "user_id",
47
+ value: row.user_id
48
+ }, {
49
+ field: "device_id",
50
+ value: row.device_id
51
+ }], {
52
+ last_seen_ts: Date.now(),
53
+ last_seen_ip: ip
54
+ });
55
+ return session;
56
+ }
57
+ /** Create (or reuse) a device and issue a fresh access token for it. */
58
+ async createSession(userId, opts = {}) {
59
+ if ((await this.hs.db.findOne("users", [{
60
+ field: "user_id",
61
+ value: userId
62
+ }]))?.locked) throw errors.userLocked();
63
+ const deviceId = opts.deviceId || generateDeviceId();
64
+ if (await this.hs.db.findOne("devices", [{
65
+ field: "user_id",
66
+ value: userId
67
+ }, {
68
+ field: "device_id",
69
+ value: deviceId
70
+ }])) {
71
+ await this.revokeDeviceTokens(userId, deviceId);
72
+ if (opts.displayName) await this.hs.db.update("devices", [{
73
+ field: "user_id",
74
+ value: userId
75
+ }, {
76
+ field: "device_id",
77
+ value: deviceId
78
+ }], { display_name: opts.displayName });
79
+ } else {
80
+ await this.hs.db.create("devices", {
81
+ user_id: userId,
82
+ device_id: deviceId,
83
+ display_name: opts.displayName ?? null,
84
+ last_seen_ts: Date.now(),
85
+ last_seen_ip: null
86
+ });
87
+ await this.hs.keys.deviceChanged(userId);
88
+ await this.hs.audit.record({
89
+ action: "device_create",
90
+ actor: userId,
91
+ target: deviceId
92
+ });
93
+ }
94
+ return this.issueToken(userId, deviceId, !!opts.refreshToken);
95
+ }
96
+ async issueToken(userId, deviceId, withRefresh) {
97
+ const token = `mat_${randomToken()}`;
98
+ const lifetime = this.hs.config.accessTokenLifetimeMs;
99
+ const refresh = withRefresh ? `mar_${randomToken()}` : null;
100
+ await this.hs.db.create("access_tokens", {
101
+ token,
102
+ user_id: userId,
103
+ device_id: deviceId,
104
+ refresh_token: refresh,
105
+ expires_ts: refresh && lifetime ? Date.now() + lifetime : null
106
+ });
107
+ const out = {
108
+ user_id: userId,
109
+ device_id: deviceId,
110
+ access_token: token
111
+ };
112
+ if (refresh) {
113
+ out.refresh_token = refresh;
114
+ if (lifetime) out.expires_in_ms = lifetime;
115
+ }
116
+ return out;
117
+ }
118
+ async refresh(refreshToken) {
119
+ const row = await this.hs.db.findOne("access_tokens", [{
120
+ field: "refresh_token",
121
+ value: refreshToken
122
+ }]);
123
+ if (!row) throw errors.unknownToken("Unknown refresh token");
124
+ await this.hs.db.delete("access_tokens", [{
125
+ field: "token",
126
+ value: row.token
127
+ }]);
128
+ this.cache.delete(row.token);
129
+ return this.issueToken(row.user_id, row.device_id, true);
130
+ }
131
+ /** Drop cached sessions of a user (after admin/deactivation changes). */
132
+ async invalidateUser(userId) {
133
+ for (const [token, s] of this.cache) if (s.userId === userId) this.cache.delete(token);
134
+ await this.hs.cluster.publish("auth", {
135
+ from: this.hs.instanceId,
136
+ userId
137
+ });
138
+ }
139
+ /** Issue a short-lived single-use `m.login.token` for a user (SSO, or host-app sign-in). */
140
+ async createLoginToken(userId, ttlMs = 12e4) {
141
+ const token = `mlt_${randomToken()}`;
142
+ await this.hs.db.create("login_tokens", {
143
+ token,
144
+ user_id: userId,
145
+ expires_ts: Date.now() + ttlMs
146
+ });
147
+ return token;
148
+ }
149
+ /** Redeem a login token; returns the user ID or throws. */
150
+ async consumeLoginToken(token) {
151
+ const row = await this.hs.db.findOne("login_tokens", [{
152
+ field: "token",
153
+ value: token
154
+ }]);
155
+ if (row) await this.hs.db.delete("login_tokens", [{
156
+ field: "token",
157
+ value: token
158
+ }]);
159
+ if (!row || row.expires_ts < Date.now()) throw errors.forbidden("Invalid or expired login token");
160
+ return row.user_id;
161
+ }
162
+ async revokeToken(token) {
163
+ this.hs.oauth?.forget(token);
164
+ await this.hs.cluster.publish("auth", {
165
+ from: this.hs.instanceId,
166
+ token
167
+ });
168
+ this.cache.delete(token);
169
+ await this.hs.db.delete("access_tokens", [{
170
+ field: "token",
171
+ value: token
172
+ }]);
173
+ }
174
+ async revokeDeviceTokens(userId, deviceId) {
175
+ const rows = await this.hs.db.findMany("access_tokens", { where: [{
176
+ field: "user_id",
177
+ value: userId
178
+ }] });
179
+ for (const row of rows) if (row.device_id === deviceId) await this.revokeToken(row.token);
180
+ }
181
+ async deleteDevice(userId, deviceId) {
182
+ await this.revokeDeviceTokens(userId, deviceId);
183
+ const where = [{
184
+ field: "user_id",
185
+ value: userId
186
+ }, {
187
+ field: "device_id",
188
+ value: deviceId
189
+ }];
190
+ await this.hs.db.delete("devices", where);
191
+ await this.hs.db.delete("device_keys", where);
192
+ await this.hs.db.delete("one_time_keys", where);
193
+ await this.hs.db.delete("fallback_keys", where);
194
+ await this.hs.db.delete("to_device", where);
195
+ await this.hs.db.delete("pushers", where);
196
+ await this.hs.keys.deviceChanged(userId, deviceId);
197
+ await this.hs.audit.record({
198
+ action: "device_delete",
199
+ actor: userId,
200
+ target: deviceId
201
+ });
202
+ await this.hs.db.delete("account_data", [
203
+ {
204
+ field: "user_id",
205
+ value: userId
206
+ },
207
+ {
208
+ field: "room_id",
209
+ value: ""
210
+ },
211
+ {
212
+ field: "type",
213
+ value: `org.matrix.msc3890.local_notification_settings.${deviceId}`
214
+ }
215
+ ]);
216
+ await this.hs.keys.markDeviceListChanged(userId);
217
+ }
218
+ async deleteAllDevices(userId, except) {
219
+ const devices = await this.hs.db.findMany("devices", { where: [{
220
+ field: "user_id",
221
+ value: userId
222
+ }] });
223
+ for (const d of devices) if (d.device_id !== except) await this.deleteDevice(userId, d.device_id);
224
+ }
225
+ };
226
+ /**
227
+ * Suspended accounts (spec "Account suspension") may read, leave, redact their own events, manage
228
+ * keys and devices, but not join, invite, send or change their profile. Rooms used to talk to the
229
+ * server administration (the admin room, server notices) stay writable.
230
+ */
231
+ async function assertNotSuspended(hs, session, roomId) {
232
+ if (!session.suspended) return;
233
+ if (roomId && (roomId === await hs.adminRoom?.roomId() || await hs.admin.isNoticeRoom(roomId))) return;
234
+ throw errors.userSuspended();
235
+ }
236
+ //#endregion
237
+ export { AuthService, assertNotSuspended };
@@ -0,0 +1,4 @@
1
+ import { PDU } from "./events.js";
2
+ //#region src/core/authrules.d.ts
3
+ export type StateMap = Map<string, PDU>;
4
+ //#endregion
@@ -0,0 +1,348 @@
1
+ import { isObject } from "./json.js";
2
+ import { publicKeyFromBase64, verifyJsonSignature } from "./crypto.js";
3
+ import { M } from "./m.js";
4
+ import { roomVersionFeatures, stateKey, versionNum } from "./events.js";
5
+ import { errors } from "./errors.js";
6
+ import { isValidUserId } from "./ids.js";
7
+ //#region src/core/authrules.ts
8
+ /** MSC3757: the user owning a state key of the form "@user:server" or "@user:server_suffix". */
9
+ function ownerOfStateKey(stateKeyValue) {
10
+ const key = stateKeyValue;
11
+ if (!key.startsWith("@")) return null;
12
+ if (isValidUserId(stateKeyValue)) return stateKeyValue;
13
+ const colon = key.indexOf(":");
14
+ if (colon < 0) return null;
15
+ const underscore = key.indexOf("_", colon);
16
+ if (underscore < 0) return null;
17
+ const owner = key.slice(0, underscore);
18
+ return isValidUserId(owner) ? owner : null;
19
+ }
20
+ function getState(state, type, key = "") {
21
+ return state.get(stateKey(type, key));
22
+ }
23
+ function membershipOf(state, userId) {
24
+ return getState(state, M.ROOM.MEMBER, userId)?.content?.membership ?? "leave";
25
+ }
26
+ function roomCreator(state, roomVersion) {
27
+ const create = getState(state, M.ROOM.CREATE);
28
+ if (!create) return void 0;
29
+ return roomVersionFeatures(roomVersion).v11 ? create.sender : create.content.creator ?? create.sender;
30
+ }
31
+ const toInt = (v, fallback) => {
32
+ if (typeof v === "number" && Number.isFinite(v)) return Math.trunc(v);
33
+ if (typeof v === "string" && /^\s*[+-]?\d+\s*$/.test(v)) return Number.parseInt(v, 10);
34
+ return fallback;
35
+ };
36
+ /** v12+: the create event's sender plus `additional_creators`. Empty for older versions. */
37
+ function roomCreators(state, roomVersion) {
38
+ if (!roomVersionFeatures(roomVersion).v12) return /* @__PURE__ */ new Set();
39
+ const create = getState(state, M.ROOM.CREATE);
40
+ if (!create) return /* @__PURE__ */ new Set();
41
+ const extra = Array.isArray(create.content?.additional_creators) ? create.content.additional_creators : [];
42
+ return /* @__PURE__ */ new Set([create.sender, ...extra.filter((u) => typeof u === "string")]);
43
+ }
44
+ function powerLevels(state, roomVersion) {
45
+ const pl = getState(state, M.ROOM.POWER_LEVELS)?.content;
46
+ const creators = roomCreators(state, roomVersion);
47
+ if (!pl) {
48
+ const creator = roomCreator(state, roomVersion);
49
+ return {
50
+ creators,
51
+ users: creator ? { [creator]: 100 } : {},
52
+ users_default: 0,
53
+ events: {},
54
+ events_default: 0,
55
+ state_default: 0,
56
+ ban: 50,
57
+ kick: 50,
58
+ redact: 50,
59
+ invite: 0,
60
+ notifications: { room: 50 }
61
+ };
62
+ }
63
+ const users = {};
64
+ if (isObject(pl.users)) for (const [u, v] of Object.entries(pl.users)) users[u] = toInt(v, 0);
65
+ const events = {};
66
+ if (isObject(pl.events)) for (const [e, v] of Object.entries(pl.events)) events[e] = toInt(v, 0);
67
+ const notifications = { room: 50 };
68
+ if (isObject(pl.notifications)) for (const [e, v] of Object.entries(pl.notifications)) notifications[e] = toInt(v, 50);
69
+ return {
70
+ creators,
71
+ users,
72
+ events,
73
+ notifications,
74
+ users_default: toInt(pl.users_default, 0),
75
+ events_default: toInt(pl.events_default, 0),
76
+ state_default: toInt(pl.state_default, 50),
77
+ ban: toInt(pl.ban, 50),
78
+ kick: toInt(pl.kick, 50),
79
+ redact: toInt(pl.redact, 50),
80
+ invite: toInt(pl.invite, 0)
81
+ };
82
+ }
83
+ function userLevel(pl, userId) {
84
+ if (pl.creators.has(userId)) return Infinity;
85
+ return pl.users[userId] ?? pl.users_default;
86
+ }
87
+ function requiredLevel(pl, type, isState) {
88
+ return pl.events[type] ?? (isState ? pl.state_default : pl.events_default);
89
+ }
90
+ const sameDomain = (a, b) => a.slice(a.indexOf(":") + 1) === b.slice(b.indexOf(":") + 1);
91
+ const domainOf = (id) => id.slice(id.indexOf(":") + 1);
92
+ /** The (type, state_key) pairs an event's auth_events may reference (spec "auth events selection"). */
93
+ function authEventKeys(event, roomVersion) {
94
+ if (event.type === M.ROOM.CREATE) return [];
95
+ const keys = [stateKey(M.ROOM.POWER_LEVELS, ""), stateKey(M.ROOM.MEMBER, event.sender)];
96
+ if (!roomVersionFeatures(roomVersion).v12) keys.unshift(stateKey(M.ROOM.CREATE, ""));
97
+ if (event.type === M.ROOM.MEMBER && typeof event.state_key === "string") {
98
+ keys.push(stateKey(M.ROOM.MEMBER, event.state_key));
99
+ const membership = event.content?.membership;
100
+ if ([
101
+ "join",
102
+ "invite",
103
+ "knock"
104
+ ].includes(membership)) keys.push(stateKey(M.ROOM.JOIN_RULES, ""));
105
+ const token = event.content?.third_party_invite?.signed?.token;
106
+ if (membership === "invite" && typeof token === "string") keys.push(stateKey(M.ROOM.THIRD_PARTY_INVITE, token));
107
+ const via = event.content?.join_authorised_via_users_server;
108
+ if (membership === "join" && typeof via === "string" && roomVersionFeatures(roomVersion).restricted) keys.push(stateKey(M.ROOM.MEMBER, via));
109
+ }
110
+ return [...new Set(keys)];
111
+ }
112
+ /**
113
+ * Build the auth state for an event from its auth_events, validating rule 2 of the
114
+ * authorization rules (no duplicates, only expected types). For v12 the create event
115
+ * (identified by the room ID) must be supplied separately.
116
+ */
117
+ function authStateFromAuthEvents(event, authEvents, roomVersion, createEvent) {
118
+ const allowed = new Set(authEventKeys(event, roomVersion));
119
+ const state = /* @__PURE__ */ new Map();
120
+ for (const ev of authEvents) {
121
+ if (ev.state_key === void 0) throw errors.forbidden("Auth event is not a state event");
122
+ const key = stateKey(ev.type, ev.state_key);
123
+ if (state.has(key)) throw errors.forbidden("Duplicate auth event");
124
+ if (!allowed.has(key)) throw errors.forbidden(`Unexpected auth event ${ev.type}`);
125
+ if (roomVersionFeatures(roomVersion).v12 && ev.type === M.ROOM.CREATE) throw errors.forbidden("Create event must not be in auth_events");
126
+ state.set(key, ev);
127
+ }
128
+ if (createEvent) state.set(stateKey(M.ROOM.CREATE, ""), createEvent);
129
+ return state;
130
+ }
131
+ /**
132
+ * The Matrix authorization rules: throws M_FORBIDDEN if `event` is not allowed
133
+ * given `state` (either its auth events or the state before it).
134
+ */
135
+ function checkAuth(event, state, roomVersion, opts = {}) {
136
+ const features = roomVersionFeatures(roomVersion);
137
+ const n = versionNum(roomVersion);
138
+ const deny = (msg) => {
139
+ throw errors.forbidden(msg);
140
+ };
141
+ if (event.type === M.ROOM.CREATE) {
142
+ if (state.size > 0 || Array.isArray(event.prev_events) && event.prev_events.length > 0) deny("Create event must be the first event");
143
+ if (!features.v12 && event.room_id && !sameDomain(event.room_id, event.sender)) deny("Room ID domain must match the creator");
144
+ const rv = event.content?.room_version;
145
+ if (rv !== void 0 && (typeof rv !== "string" || !Number.isFinite(versionNum(rv)))) deny("Unknown room version");
146
+ if (!features.v11 && typeof event.content?.creator !== "string") deny("Create event lacks a creator");
147
+ if (features.v12) {
148
+ const extra = event.content?.additional_creators;
149
+ if (extra !== void 0 && (!Array.isArray(extra) || !extra.every(isValidUserId))) deny("Invalid additional_creators");
150
+ }
151
+ return;
152
+ }
153
+ const create = getState(state, M.ROOM.CREATE);
154
+ if (!create) deny("No create event in room");
155
+ if (create.content?.[M.FEDERATE] === false && !sameDomain(event.sender, create.sender)) deny("Room is not federated");
156
+ if (n <= 5 && event.type === M.ROOM.ALIASES) {
157
+ if (event.state_key === void 0) deny("Aliases event lacks a state key");
158
+ if (event.state_key !== domainOf(event.sender)) deny("Aliases state key must be the sender's domain");
159
+ return;
160
+ }
161
+ const pl = powerLevels(state, roomVersion);
162
+ const senderMembership = membershipOf(state, event.sender);
163
+ const senderLevel = userLevel(pl, event.sender);
164
+ if (event.type === M.ROOM.MEMBER) {
165
+ const target = event.state_key;
166
+ const membership = event.content?.membership;
167
+ if (typeof target !== "string" || typeof membership !== "string") deny("Member event lacks state_key or membership");
168
+ const targetMembership = membershipOf(state, target);
169
+ const joinRule = getState(state, M.ROOM.JOIN_RULES)?.content?.join_rule ?? "invite";
170
+ const via = event.content?.join_authorised_via_users_server;
171
+ switch (membership) {
172
+ case "join": {
173
+ const prev = Array.isArray(event.prev_events) ? event.prev_events.map((p) => Array.isArray(p) ? p[0] : p) : [];
174
+ const creator = roomCreator(state, roomVersion);
175
+ if (prev.length === 1 && prev[0] === create.event_id && target === creator && event.sender === target) return;
176
+ if (state.size === 1 && creator === target && event.sender === target) return;
177
+ if (event.sender !== target) deny("Cannot force another user to join");
178
+ if (targetMembership === "ban") deny("User is banned from the room");
179
+ if (joinRule === "invite" || features.knock && joinRule === "knock") {
180
+ if (targetMembership === "join" || targetMembership === "invite") return;
181
+ deny("You are not invited to this room");
182
+ }
183
+ if (features.restricted && joinRule === "restricted" || features.knockRestricted && joinRule === "knock_restricted") {
184
+ if (targetMembership === "join" || targetMembership === "invite") return;
185
+ if (typeof via === "string" && opts.joinAuthorisedSignatureValid === false) deny("join_authorised_via_users_server is not signed by that server");
186
+ if (typeof via === "string" && membershipOf(state, via) === "join" && userLevel(pl, via) >= pl.invite) return;
187
+ deny("Join not authorised by restricted join rules");
188
+ }
189
+ if (joinRule === "public") return;
190
+ deny("You are not allowed to join this room");
191
+ break;
192
+ }
193
+ case "invite":
194
+ if (event.content?.third_party_invite !== void 0) {
195
+ if (targetMembership === "ban") deny("User is banned");
196
+ if (!checkThirdPartyInvite(event, state)) deny("Invalid third party invite");
197
+ return;
198
+ }
199
+ if (senderMembership !== "join") deny("Sender not in room");
200
+ if (targetMembership === "join" || targetMembership === "ban") deny(`User is already ${targetMembership === "join" ? "joined" : "banned"}`);
201
+ if (senderLevel >= pl.invite) return;
202
+ deny("Insufficient power level to invite");
203
+ break;
204
+ case "leave":
205
+ if (event.sender === target) {
206
+ if (targetMembership === "join" || targetMembership === "invite" || features.knock && targetMembership === "knock") return;
207
+ deny("User is not in the room");
208
+ }
209
+ if (senderMembership !== "join") deny("Sender not in room");
210
+ if (targetMembership === "ban" && senderLevel < pl.ban) deny("Insufficient power level to unban");
211
+ if (senderLevel >= pl.kick && userLevel(pl, target) < senderLevel) return;
212
+ deny("Insufficient power level to kick");
213
+ break;
214
+ case "ban":
215
+ if (senderMembership !== "join") deny("Sender not in room");
216
+ if (senderLevel >= pl.ban && userLevel(pl, target) < senderLevel) return;
217
+ deny("Insufficient power level to ban");
218
+ break;
219
+ case "knock":
220
+ if (!features.knock) deny("Knocking is not supported in this room version");
221
+ if (joinRule !== "knock" && !(features.knockRestricted && joinRule === "knock_restricted")) deny("Room does not allow knocking");
222
+ if (event.sender !== target) deny("Cannot knock on behalf of another user");
223
+ if (targetMembership === "ban" || targetMembership === "join" || targetMembership === "invite") deny(`Cannot knock while ${targetMembership}`);
224
+ return;
225
+ default: deny("Unknown membership");
226
+ }
227
+ return;
228
+ }
229
+ if (senderMembership !== "join") deny("You are not joined to this room");
230
+ if (event.type === M.ROOM.THIRD_PARTY_INVITE) {
231
+ if (senderLevel >= pl.invite) return;
232
+ deny("Insufficient power level to invite");
233
+ }
234
+ const isState = event.state_key !== void 0;
235
+ if (senderLevel < requiredLevel(pl, event.type, isState)) deny("Insufficient power level to send this event");
236
+ if (isState && event.state_key.startsWith("@") && event.state_key !== event.sender) {
237
+ if (!features.ownedState) deny("State keys starting with @ are reserved for that user");
238
+ const owner = ownerOfStateKey(event.state_key);
239
+ if (!owner) deny("Malformed user-owned state key");
240
+ if (owner !== event.sender && senderLevel <= userLevel(pl, owner)) deny("This state key belongs to another user");
241
+ }
242
+ if (event.type === M.ROOM.POWER_LEVELS && isState) {
243
+ if (isObject(event.content?.users)) for (const u of Object.keys(event.content.users)) {
244
+ if (!isValidUserId(u)) deny("Power level users must be valid user IDs");
245
+ if (pl.creators.has(u)) deny("Room creators cannot be given a power level");
246
+ }
247
+ if (getState(state, M.ROOM.POWER_LEVELS)) checkPowerLevelChange(event, pl, senderLevel, features.integerPowerLevels, n >= 6, deny);
248
+ else checkPowerLevelShape(event, features.integerPowerLevels, deny);
249
+ return;
250
+ }
251
+ if (n <= 2 && event.type === M.ROOM.REDACTION) {
252
+ if (senderLevel >= pl.redact) return;
253
+ const redacts = event.redacts;
254
+ if (typeof redacts === "string" && redacts.includes(":") && domainOf(redacts) === domainOf(event.sender)) return;
255
+ deny("Insufficient power level to redact");
256
+ }
257
+ }
258
+ function checkThirdPartyInvite(event, state) {
259
+ const signed = event.content?.third_party_invite?.signed;
260
+ if (!isObject(signed) || typeof signed.mxid !== "string" || typeof signed.token !== "string") return false;
261
+ if (signed.mxid !== event.state_key) return false;
262
+ const invite = getState(state, M.ROOM.THIRD_PARTY_INVITE, signed.token);
263
+ if (!invite || invite.sender !== event.sender) return false;
264
+ const keys = [];
265
+ if (typeof invite.content?.public_key === "string") keys.push(invite.content.public_key);
266
+ for (const k of Array.isArray(invite.content?.public_keys) ? invite.content.public_keys : []) if (typeof k?.public_key === "string") keys.push(k.public_key);
267
+ for (const key of keys) {
268
+ let pub;
269
+ try {
270
+ pub = publicKeyFromBase64(key);
271
+ } catch {
272
+ continue;
273
+ }
274
+ for (const [server, sigs] of Object.entries(isObject(signed.signatures) ? signed.signatures : {})) for (const keyId of Object.keys(isObject(sigs) ? sigs : {})) if (verifyJsonSignature(signed, server, keyId, pub)) return true;
275
+ }
276
+ return false;
277
+ }
278
+ function checkPowerLevelShape(event, integerOnly, deny) {
279
+ if (!integerOnly) return;
280
+ const c = event.content ?? {};
281
+ const isInt = (v) => typeof v === "number" && Number.isInteger(v);
282
+ for (const key of [
283
+ "users_default",
284
+ "events_default",
285
+ "state_default",
286
+ "ban",
287
+ "redact",
288
+ "kick",
289
+ "invite"
290
+ ]) if (key in c && !isInt(c[key])) deny(`${key} must be an integer`);
291
+ for (const map of [
292
+ "users",
293
+ "events",
294
+ "notifications"
295
+ ]) {
296
+ if (map in c && !isObject(c[map])) deny(`${map} must be an object`);
297
+ if (isObject(c[map])) {
298
+ for (const v of Object.values(c[map])) if (!isInt(v)) deny(`${map} values must be integers`);
299
+ }
300
+ }
301
+ }
302
+ function checkPowerLevelChange(event, current, senderLevel, integerOnly, checkNotifications, deny) {
303
+ const c = event.content ?? {};
304
+ checkPowerLevelShape(event, integerOnly, deny);
305
+ if (!integerOnly) {
306
+ for (const map of [
307
+ "users",
308
+ "events",
309
+ "notifications"
310
+ ]) if (map in c && !isObject(c[map])) deny(`${map} must be an object`);
311
+ }
312
+ const next = {
313
+ users_default: toInt(c.users_default, 0),
314
+ events_default: toInt(c.events_default, 0),
315
+ state_default: toInt(c.state_default, 50),
316
+ ban: toInt(c.ban, 50),
317
+ redact: toInt(c.redact, 50),
318
+ kick: toInt(c.kick, 50),
319
+ invite: toInt(c.invite, 0)
320
+ };
321
+ for (const key of Object.keys(next)) {
322
+ const old = current[key];
323
+ if (old !== next[key] && (old > senderLevel || next[key] > senderLevel)) deny(`Cannot change ${key} beyond your own level`);
324
+ }
325
+ const compareMap = (oldMap, newMap, isUsers) => {
326
+ const keys = /* @__PURE__ */ new Set([...Object.keys(oldMap), ...Object.keys(newMap ?? {})]);
327
+ for (const k of keys) {
328
+ const o = oldMap[k];
329
+ const nv = newMap?.[k] === void 0 ? void 0 : toInt(newMap[k], 0);
330
+ if (o === nv) continue;
331
+ if (isUsers) {
332
+ if (k !== event.sender && o !== void 0 && o >= senderLevel) deny("Cannot change the level of a user with equal or higher power");
333
+ if (nv !== void 0 && nv > senderLevel) deny("Cannot set a level above your own");
334
+ } else {
335
+ if (o !== void 0 && o > senderLevel) deny("Cannot change a level above your own");
336
+ if (nv !== void 0 && nv > senderLevel) deny("Cannot set a level above your own");
337
+ }
338
+ }
339
+ };
340
+ compareMap(current.users, c.users ?? {}, true);
341
+ compareMap(current.events, c.events ?? {}, false);
342
+ if (checkNotifications) compareMap({ ...current.notifications }, {
343
+ room: 50,
344
+ ...c.notifications
345
+ }, false);
346
+ }
347
+ //#endregion
348
+ export { authEventKeys, authStateFromAuthEvents, checkAuth, getState, membershipOf, ownerOfStateKey, powerLevels, requiredLevel, roomCreator, roomCreators, userLevel };
@@ -0,0 +1,66 @@
1
+ //#region src/core/cluster.d.ts
2
+ /**
3
+ * Coordination between homeserver instances sharing one database. The default
4
+ * (`localCluster()`) keeps everything in process; use `postgresCluster()` or
5
+ * `redisCluster()` to run several instances behind a load balancer.
6
+ */
7
+ export interface ClusterAdapter {
8
+ /** Allocate the next value of a cluster-wide, strictly increasing sequence. */
9
+ nextId(sequence: string): Promise<number>;
10
+ /** Current (latest allocated) value of a sequence. */
11
+ currentId(sequence: string): Promise<number>;
12
+ /** Ensure a sequence is at least `value` (used at startup with existing data). */
13
+ advance(sequence: string, value: number): Promise<void>;
14
+ /** Run `fn` while holding a cluster-wide mutex for `key`. */
15
+ lock<T>(key: string, fn: () => Promise<T>): Promise<T>;
16
+ publish(channel: string, message: unknown): Promise<void>;
17
+ subscribe(channel: string, handler: (message: any) => void): void;
18
+ /** Ephemeral shared key-value storage. */
19
+ get<T = unknown>(key: string): Promise<T | null>;
20
+ set(key: string, value: unknown, ttlMs?: number): Promise<void>;
21
+ /** Set only if the key is absent (or expired). Returns true when the value was set. */
22
+ setIfAbsent(key: string, value: unknown, ttlMs?: number): Promise<boolean>;
23
+ delete(key: string): Promise<void>;
24
+ /** All live entries whose key starts with `prefix`. */
25
+ entries<T = unknown>(prefix: string): Promise<[string, T][]>;
26
+ close?(): Promise<void>;
27
+ }
28
+ /** In-process cluster adapter: a single homeserver instance. */
29
+ export declare function localCluster(): ClusterAdapter;
30
+ export interface StreamLease {
31
+ id: number;
32
+ /** Mark the position as persisted (or abandoned). */
33
+ done(): Promise<void>;
34
+ }
35
+ /**
36
+ * Stream positions with multiple writers. Positions are allocated from a global
37
+ * sequence; readers only see positions up to which every writer has finished, so
38
+ * an event committed late with a lower position can never be skipped by /sync.
39
+ *
40
+ * Each writer publishes `{ busy, pos }`: when busy, all of its positions ≤ pos are
41
+ * persisted. Readers use min(global, busy writers' pos).
42
+ */
43
+ export declare class StreamManager {
44
+ private cluster;
45
+ private instanceId;
46
+ private inflight;
47
+ private published;
48
+ private transition;
49
+ private heartbeat;
50
+ private positionCache;
51
+ constructor(cluster: ClusterAdapter, instanceId: string);
52
+ private key;
53
+ private publishState;
54
+ /** Allocate a position. The caller must call `done()` once its writes are committed. */
55
+ allocate(): Promise<StreamLease>;
56
+ /** Allocate a position, run `fn` with it, then complete it. */
57
+ write<T>(fn: (id: number) => Promise<T>): Promise<T>;
58
+ /** A fresh decreasing position for backfilled (historical) events. */
59
+ backfillId(): Promise<number>;
60
+ /** The highest position below which everything is persisted, cluster-wide. */
61
+ position(): Promise<number>;
62
+ advanceTo(value: number): Promise<void>;
63
+ lowerBackfillTo(value: number): Promise<void>;
64
+ stop(): Promise<void>;
65
+ }
66
+ //#endregion