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,51 +1,53 @@
1
1
  import { isObject } from "../core/json.js";
2
2
  import { randomString } from "../core/crypto.js";
3
+ import { M, UNSTABLE } from "../core/m.js";
3
4
  import { MatrixError, errors } from "../core/errors.js";
4
5
  import { LOCALPART_RE } from "../core/ids.js";
5
6
  import { toMsisdn } from "../core/phone.js";
6
7
  import { canonicalAddress, isMedium } from "../core/threepid.js";
7
- import { requireUia } from "./uia.js";
8
+ import { boolean, looseObject, string } from "../vendor/zod/v4/classic/schemas.js";
8
9
  import { body, fail, lenient } from "../http/input.js";
10
+ import { procedure } from "../http/procedure.js";
11
+ import { requireUia } from "./uia.js";
9
12
  import { createHmac, randomInt, timingSafeEqual } from "node:crypto";
10
- import { z } from "zod";
11
13
  //#region src/api/account.ts
12
14
  const deviceFields = {
13
- device_id: lenient(z.string()),
14
- initial_device_display_name: lenient(z.string()),
15
- refresh_token: lenient(z.boolean()),
16
- inhibit_login: lenient(z.boolean())
15
+ device_id: lenient(string()),
16
+ initial_device_display_name: lenient(string()),
17
+ refresh_token: lenient(boolean()),
18
+ inhibit_login: lenient(boolean())
17
19
  };
18
20
  const loginBody = body({
19
- type: lenient(z.string()),
20
- token: lenient(z.string()),
21
- password: lenient(z.string()),
21
+ type: lenient(string()),
22
+ token: lenient(string()),
23
+ password: lenient(string()),
22
24
  ...deviceFields
23
25
  });
24
26
  const registerBody = body({
25
- type: lenient(z.string()),
26
- auth: lenient(z.looseObject({ type: lenient(z.string()) })),
27
- username: z.string(fail("M_INVALID_PARAM", "username must be a string")).optional(),
28
- password: z.string(fail("M_INVALID_PARAM", "password must be a string")).optional(),
27
+ type: lenient(string()),
28
+ auth: lenient(looseObject({ type: lenient(string()) })),
29
+ username: string(fail("M_INVALID_PARAM", "username must be a string")).optional(),
30
+ password: string(fail("M_INVALID_PARAM", "password must be a string")).optional(),
29
31
  ...deviceFields
30
32
  });
31
33
  const passwordBody = body({
32
- new_password: lenient(z.string()),
33
- logout_devices: lenient(z.boolean())
34
+ new_password: lenient(string()),
35
+ logout_devices: lenient(boolean())
34
36
  });
35
37
  const sharedSecretRegisterBody = body({
36
- nonce: lenient(z.string()),
37
- username: z.string(fail("M_INVALID_PARAM", "Invalid username/password")),
38
- password: z.string(fail("M_INVALID_PARAM", "Invalid username/password")),
39
- admin: lenient(z.boolean()),
40
- mac: lenient(z.string()),
41
- displayname: lenient(z.string())
38
+ nonce: lenient(string()),
39
+ username: string(fail("M_INVALID_PARAM", "Invalid username/password")),
40
+ password: string(fail("M_INVALID_PARAM", "Invalid username/password")),
41
+ admin: lenient(boolean()),
42
+ mac: lenient(string()),
43
+ displayname: lenient(string())
42
44
  });
43
45
  function loginFlows(hs) {
44
46
  if (hs.oauth) return [];
45
47
  const flows = [];
46
- if (hs.userAuth.passwordLoginEnabled) flows.push({ type: "m.login.password" });
48
+ if (hs.userAuth.passwordLoginEnabled) flows.push({ type: M.LOGIN.PASSWORD });
47
49
  if (hs.sso) flows.push({
48
- type: "m.login.sso",
50
+ type: M.LOGIN.SSO,
49
51
  identity_providers: hs.sso.providers.map((p) => ({
50
52
  id: p.id,
51
53
  name: p.name,
@@ -54,7 +56,7 @@ function loginFlows(hs) {
54
56
  }))
55
57
  });
56
58
  flows.push({
57
- type: "m.login.token",
59
+ type: M.LOGIN.TOKEN,
58
60
  ...hs.config.loginViaExistingSession ? { get_login_token: true } : {}
59
61
  });
60
62
  return flows;
@@ -105,11 +107,11 @@ async function resolveLoginUser(hs, body) {
105
107
  let user;
106
108
  const id = isObject(body.identifier) ? body.identifier : null;
107
109
  let threepid = null;
108
- if (id?.type === "m.id.thirdparty" && typeof id.medium === "string" && typeof id.address === "string") threepid = {
110
+ if (id?.type === M.ID.THIRDPARTY && typeof id.medium === "string" && typeof id.address === "string") threepid = {
109
111
  medium: id.medium,
110
112
  address: isMedium(id.medium) ? canonicalAddress(id.medium, id.address) : null
111
113
  };
112
- else if (id?.type === "m.id.phone" && typeof id.country === "string" && typeof id.phone === "string") threepid = {
114
+ else if (id?.type === M.ID.PHONE && typeof id.country === "string" && typeof id.phone === "string") threepid = {
113
115
  medium: "msisdn",
114
116
  address: toMsisdn(id.country, id.phone)
115
117
  };
@@ -123,7 +125,7 @@ async function resolveLoginUser(hs, body) {
123
125
  return owner;
124
126
  }
125
127
  if (id) {
126
- if (id.type !== "m.id.user") throw errors.unknown("Unsupported identifier type");
128
+ if (id.type !== M.ID.USER) throw errors.unknown("Unsupported identifier type");
127
129
  user = id.user;
128
130
  } else user = body.user;
129
131
  if (typeof user !== "string" || !user) throw errors.missingParam("Missing user identifier");
@@ -135,77 +137,135 @@ async function resolveLoginUser(hs, body) {
135
137
  return `@${user.toLowerCase()}:${hs.serverName}`;
136
138
  }
137
139
  function registerAccountRoutes(r) {
138
- r.client("GET", "/login", (ctx) => ({ flows: loginFlows(ctx.hs) }));
139
- r.client("GET", "/auth_metadata", async (ctx) => {
140
+ r.client("GET", "/login", procedure({ handler: (ctx) => ({ flows: loginFlows(ctx.hs) }) }));
141
+ r.client("GET", "/auth_metadata", procedure({ handler: async (ctx) => {
140
142
  if (!ctx.hs.oauth) throw errors.unrecognized("OAuth is not enabled");
141
143
  return ctx.hs.oauth.serverMetadata();
142
- }, ["v1"]);
143
- r.client("GET", "/auth_issuer", (ctx) => {
144
+ } }), ["v1"]);
145
+ r.client("GET", "/auth_issuer", procedure({ handler: (ctx) => {
144
146
  if (!ctx.hs.oauth) throw errors.unrecognized("OAuth is not enabled");
145
147
  return { issuer: ctx.hs.oauth.options.issuer };
146
- }, ["v1"]);
148
+ } }), ["v1"]);
147
149
  const ssoRedirect = async (ctx) => {
148
150
  if (!ctx.hs.sso) throw errors.unrecognized("SSO is not enabled");
149
151
  const redirectUrl = ctx.query.get("redirectUrl");
150
152
  if (!redirectUrl || !/^https?:\/\/|^[a-z][a-z0-9+.-]*:/i.test(redirectUrl)) throw errors.missingParam("Missing or invalid redirectUrl");
151
153
  return Response.redirect(await ctx.hs.sso.startLogin(ctx.params.idpId ?? null, redirectUrl), 302);
152
154
  };
153
- r.client("POST", "/login/get_token", async (ctx) => {
154
- const s = await ctx.auth();
155
- if (!ctx.hs.config.loginViaExistingSession || ctx.hs.oauth || s.isGuest) throw errors.unrecognized();
156
- await ctx.rateLimit("login");
157
- await requireUia(ctx.hs, await ctx.json(), [["m.login.password"]], s.userId);
158
- const ttl = 12e4;
159
- return {
160
- login_token: await ctx.hs.auth.createLoginToken(s.userId, ttl),
161
- expires_in_ms: ttl
162
- };
163
- }, ["v1"]);
164
- r.client("GET", "/login/sso/redirect", ssoRedirect);
165
- r.client("GET", "/login/sso/redirect/:idpId", ssoRedirect);
166
- r.client("GET", "/login/sso/callback/:idpId", async (ctx) => {
155
+ r.client("POST", "/login/get_token", procedure({
156
+ auth: "user",
157
+ handler: async (ctx) => {
158
+ const s = ctx.session;
159
+ if (!ctx.hs.config.loginViaExistingSession || ctx.hs.oauth || s.isGuest) throw errors.unrecognized();
160
+ await ctx.rateLimit("login");
161
+ await requireUia(ctx.hs, await ctx.json(), [[M.LOGIN.PASSWORD]], s.userId);
162
+ const ttl = 12e4;
163
+ return {
164
+ login_token: await ctx.hs.auth.createLoginToken(s.userId, ttl),
165
+ expires_in_ms: ttl
166
+ };
167
+ }
168
+ }), ["v1"]);
169
+ r.client("GET", "/login/sso/redirect", procedure({ handler: ssoRedirect }));
170
+ r.client("GET", "/login/sso/redirect/:idpId", procedure({ handler: ssoRedirect }));
171
+ r.client("GET", "/login/sso/callback/:idpId", procedure({ handler: async (ctx) => {
167
172
  if (!ctx.hs.sso) throw errors.unrecognized("SSO is not enabled");
168
173
  return Response.redirect(await ctx.hs.sso.finishLogin(ctx.params.idpId, ctx.query), 302);
169
- });
170
- r.client("POST", "/login", { input: loginBody }, async (ctx) => {
171
- await ctx.rateLimit("login");
172
- const body = ctx.input;
173
- const hs = ctx.hs;
174
- if (hs.oauth) throw errors.unrecognized("Log in through the OAuth provider");
175
- if (body.type === "m.login.application_service") {
176
- const reg = ctx.accessToken() ? hs.appservices.byToken(ctx.accessToken()) : null;
177
- if (!reg) throw errors.unknownToken("An application service token is required");
174
+ } }));
175
+ r.client("POST", "/login", procedure({
176
+ rateLimit: "login",
177
+ input: loginBody,
178
+ handler: async (ctx) => {
179
+ const body = ctx.input;
180
+ const hs = ctx.hs;
181
+ if (hs.oauth) throw errors.unrecognized("Log in through the OAuth provider");
182
+ if (body.type === M.LOGIN.APPLICATION_SERVICE) {
183
+ const reg = ctx.accessToken() ? hs.appservices.byToken(ctx.accessToken()) : null;
184
+ if (!reg) throw errors.unknownToken("An application service token is required");
185
+ const userId = await resolveLoginUser(hs, body);
186
+ if (!hs.appservices.isInterestedUser(reg, userId)) throw errors.forbidden("User is not in the application service's namespace");
187
+ if (!await hs.db.findOne("users", [{
188
+ field: "user_id",
189
+ value: userId
190
+ }])) throw errors.forbidden("Unknown user");
191
+ return {
192
+ ...await hs.auth.createSession(userId, {
193
+ deviceId: body.device_id,
194
+ displayName: body.initial_device_display_name
195
+ }),
196
+ home_server: hs.serverName
197
+ };
198
+ }
199
+ if (body.type === M.LOGIN.TOKEN) {
200
+ if (body.token === void 0) throw errors.missingParam("Missing token");
201
+ const userId = await hs.auth.consumeLoginToken(body.token).catch(async (err) => {
202
+ await hs.audit.record({
203
+ action: "login_failed",
204
+ actor: null,
205
+ ip: ctx.ip,
206
+ outcome: "failure",
207
+ details: { type: M.LOGIN.TOKEN }
208
+ });
209
+ throw err;
210
+ });
211
+ const user = await hs.db.findOne("users", [{
212
+ field: "user_id",
213
+ value: userId
214
+ }]);
215
+ if (!user || user.deactivated) throw errors.userDeactivated();
216
+ const session = await hs.auth.createSession(userId, {
217
+ deviceId: body.device_id,
218
+ displayName: body.initial_device_display_name,
219
+ refreshToken: body.refresh_token === true
220
+ });
221
+ await hs.audit.record({
222
+ action: "login",
223
+ actor: userId,
224
+ target: session.device_id,
225
+ ip: ctx.ip,
226
+ userAgent: ctx.request.headers.get("user-agent"),
227
+ details: { type: M.LOGIN.TOKEN }
228
+ });
229
+ return {
230
+ ...session,
231
+ home_server: hs.serverName,
232
+ well_known: wellKnown(hs) ?? void 0
233
+ };
234
+ }
235
+ if (body.type !== M.LOGIN.PASSWORD || !hs.userAuth.passwordLoginEnabled) throw errors.unknown("Unsupported login type");
178
236
  const userId = await resolveLoginUser(hs, body);
179
- if (!hs.appservices.isInterestedUser(reg, userId)) throw errors.forbidden("User is not in the application service's namespace");
180
- if (!await hs.db.findOne("users", [{
237
+ if (body.password === void 0) throw errors.missingParam("Missing password");
238
+ let user = await hs.db.findOne("users", [{
181
239
  field: "user_id",
182
240
  value: userId
183
- }])) throw errors.forbidden("Unknown user");
184
- return {
185
- ...await hs.auth.createSession(userId, {
186
- deviceId: body.device_id,
187
- displayName: body.initial_device_display_name
188
- }),
189
- home_server: hs.serverName
190
- };
191
- }
192
- if (body.type === "m.login.token") {
193
- if (body.token === void 0) throw errors.missingParam("Missing token");
194
- const userId = await hs.auth.consumeLoginToken(body.token).catch(async (err) => {
241
+ }]);
242
+ const ok = await hs.userAuth.checkPassword({
243
+ userId,
244
+ localpart: userId.slice(1).split(":")[0],
245
+ password: body.password,
246
+ storedHash: user?.password_hash ?? null
247
+ });
248
+ const ua = ctx.request.headers.get("user-agent");
249
+ if (!ok) {
195
250
  await hs.audit.record({
196
251
  action: "login_failed",
197
- actor: null,
252
+ actor: userId,
198
253
  ip: ctx.ip,
254
+ userAgent: ua,
199
255
  outcome: "failure",
200
- details: { type: "m.login.token" }
256
+ details: { type: M.LOGIN.PASSWORD }
201
257
  });
202
- throw err;
203
- });
204
- const user = await hs.db.findOne("users", [{
205
- field: "user_id",
206
- value: userId
207
- }]);
208
- if (!user || user.deactivated) throw errors.userDeactivated();
258
+ throw errors.forbidden("Invalid username or password");
259
+ }
260
+ if (!user) {
261
+ if (!await hs.userAuth.autoProvision(userId)) throw errors.forbidden("Invalid username or password");
262
+ await createUser(hs, userId.slice(1).split(":")[0], {});
263
+ user = await hs.db.findOne("users", [{
264
+ field: "user_id",
265
+ value: userId
266
+ }]);
267
+ }
268
+ if (user.deactivated) throw errors.userDeactivated();
209
269
  const session = await hs.auth.createSession(userId, {
210
270
  deviceId: body.device_id,
211
271
  displayName: body.initial_device_display_name,
@@ -216,8 +276,8 @@ function registerAccountRoutes(r) {
216
276
  actor: userId,
217
277
  target: session.device_id,
218
278
  ip: ctx.ip,
219
- userAgent: ctx.request.headers.get("user-agent"),
220
- details: { type: "m.login.token" }
279
+ userAgent: ua,
280
+ details: { type: M.LOGIN.PASSWORD }
221
281
  });
222
282
  return {
223
283
  ...session,
@@ -225,141 +285,143 @@ function registerAccountRoutes(r) {
225
285
  well_known: wellKnown(hs) ?? void 0
226
286
  };
227
287
  }
228
- if (body.type !== "m.login.password" || !hs.userAuth.passwordLoginEnabled) throw errors.unknown("Unsupported login type");
229
- const userId = await resolveLoginUser(hs, body);
230
- if (body.password === void 0) throw errors.missingParam("Missing password");
231
- let user = await hs.db.findOne("users", [{
232
- field: "user_id",
233
- value: userId
234
- }]);
235
- const ok = await hs.userAuth.checkPassword({
236
- userId,
237
- localpart: userId.slice(1).split(":")[0],
238
- password: body.password,
239
- storedHash: user?.password_hash ?? null
240
- });
241
- const ua = ctx.request.headers.get("user-agent");
242
- if (!ok) {
243
- await hs.audit.record({
244
- action: "login_failed",
245
- actor: userId,
288
+ }));
289
+ r.client("POST", "/refresh", procedure({
290
+ input: body({ refresh_token: lenient(string()) }),
291
+ handler: async (ctx) => {
292
+ const body = ctx.input;
293
+ if (body.refresh_token === void 0) throw errors.missingParam("Missing refresh_token");
294
+ const { access_token, refresh_token, expires_in_ms } = await ctx.hs.auth.refresh(body.refresh_token);
295
+ return {
296
+ access_token,
297
+ refresh_token,
298
+ expires_in_ms
299
+ };
300
+ }
301
+ }));
302
+ r.client("POST", "/logout", procedure({
303
+ auth: "user",
304
+ handler: async (ctx) => {
305
+ const s = ctx.session;
306
+ await ctx.hs.auth.deleteDevice(s.userId, s.deviceId);
307
+ await ctx.hs.audit.record({
308
+ action: "logout",
309
+ actor: s.userId,
310
+ target: s.deviceId,
311
+ ip: ctx.ip
312
+ });
313
+ return {};
314
+ }
315
+ }));
316
+ r.client("POST", "/logout/all", procedure({
317
+ auth: "user",
318
+ handler: async (ctx) => {
319
+ const s = ctx.session;
320
+ await ctx.hs.auth.deleteAllDevices(s.userId);
321
+ await ctx.hs.audit.record({
322
+ action: "logout",
323
+ actor: s.userId,
246
324
  ip: ctx.ip,
247
- userAgent: ua,
248
- outcome: "failure",
249
- details: { type: "m.login.password" }
325
+ details: { all: true }
250
326
  });
251
- throw errors.forbidden("Invalid username or password");
327
+ return {};
252
328
  }
253
- if (!user) {
254
- if (!await hs.userAuth.autoProvision(userId)) throw errors.forbidden("Invalid username or password");
255
- await createUser(hs, userId.slice(1).split(":")[0], {});
256
- user = await hs.db.findOne("users", [{
257
- field: "user_id",
258
- value: userId
259
- }]);
329
+ }));
330
+ r.client("GET", "/account/whoami", procedure({
331
+ auth: "user",
332
+ handler: async (ctx) => {
333
+ const s = ctx.session;
334
+ return {
335
+ user_id: s.userId,
336
+ ...s.deviceId ? { device_id: s.deviceId } : {},
337
+ is_guest: s.isGuest
338
+ };
260
339
  }
261
- if (user.deactivated) throw errors.userDeactivated();
262
- const session = await hs.auth.createSession(userId, {
263
- deviceId: body.device_id,
264
- displayName: body.initial_device_display_name,
265
- refreshToken: body.refresh_token === true
266
- });
267
- await hs.audit.record({
268
- action: "login",
269
- actor: userId,
270
- target: session.device_id,
271
- ip: ctx.ip,
272
- userAgent: ua,
273
- details: { type: "m.login.password" }
274
- });
275
- return {
276
- ...session,
277
- home_server: hs.serverName,
278
- well_known: wellKnown(hs) ?? void 0
279
- };
280
- });
281
- r.client("POST", "/refresh", { input: body({ refresh_token: lenient(z.string()) }) }, async (ctx) => {
282
- const body = ctx.input;
283
- if (body.refresh_token === void 0) throw errors.missingParam("Missing refresh_token");
284
- const { access_token, refresh_token, expires_in_ms } = await ctx.hs.auth.refresh(body.refresh_token);
285
- return {
286
- access_token,
287
- refresh_token,
288
- expires_in_ms
289
- };
290
- });
291
- r.client("POST", "/logout", async (ctx) => {
292
- const s = await ctx.auth();
293
- await ctx.hs.auth.deleteDevice(s.userId, s.deviceId);
294
- await ctx.hs.audit.record({
295
- action: "logout",
296
- actor: s.userId,
297
- target: s.deviceId,
298
- ip: ctx.ip
299
- });
300
- return {};
301
- });
302
- r.client("POST", "/logout/all", async (ctx) => {
303
- const s = await ctx.auth();
304
- await ctx.hs.auth.deleteAllDevices(s.userId);
305
- await ctx.hs.audit.record({
306
- action: "logout",
307
- actor: s.userId,
308
- ip: ctx.ip,
309
- details: { all: true }
310
- });
311
- return {};
312
- });
313
- r.client("GET", "/account/whoami", async (ctx) => {
314
- const s = await ctx.auth();
315
- return {
316
- user_id: s.userId,
317
- ...s.deviceId ? { device_id: s.deviceId } : {},
318
- is_guest: s.isGuest
319
- };
320
- });
321
- r.client("GET", "/register/available", async (ctx) => {
340
+ }));
341
+ r.client("GET", "/register/available", procedure({ handler: async (ctx) => {
322
342
  const username = ctx.query.get("username");
323
343
  if (!username) throw errors.missingParam("Missing username");
324
344
  await validateNewUsername(ctx.hs, username);
325
345
  return { available: true };
326
- });
327
- r.client("GET", "/register", async (ctx) => ({
346
+ } }));
347
+ r.client("GET", "/register", procedure({ handler: async (ctx) => ({
328
348
  flows: (await registrationFlows(ctx.hs)).map((stages) => ({ stages })),
329
349
  params: {}
330
- }));
331
- r.client("GET", "/register/m.login.registration_token/validity", async (ctx) => {
332
- await ctx.rateLimit("register");
333
- const token = ctx.query.get("token");
334
- if (!token) throw errors.missingParam("Missing token");
335
- return { valid: await registrationTokenValid(ctx.hs, token) };
336
- }, ["v1"]);
337
- r.client("POST", "/register", { input: registerBody }, async (ctx) => {
338
- const hs = ctx.hs;
339
- const body = ctx.input;
340
- const kind = ctx.query.get("kind") ?? "user";
341
- if (kind === "guest") {
342
- if (!hs.userAuth.guestAccessEnabled) throw errors.forbidden("Guest access is disabled");
343
- const userId = await createUser(hs, await generateLocalpart(hs), { guest: true });
344
- return {
345
- ...await hs.auth.createSession(userId, { displayName: body.initial_device_display_name }),
346
- home_server: hs.serverName
347
- };
350
+ }) }));
351
+ r.client("GET", "/register/m.login.registration_token/validity", procedure({
352
+ rateLimit: "register",
353
+ handler: async (ctx) => {
354
+ const token = ctx.query.get("token");
355
+ if (!token) throw errors.missingParam("Missing token");
356
+ return { valid: await registrationTokenValid(ctx.hs, token) };
348
357
  }
349
- if (kind !== "user") throw errors.invalidParam("Unknown kind");
350
- if (body.type === "m.login.application_service" || body.auth?.type === "m.login.application_service") {
351
- const s = await ctx.auth();
352
- if (!s.appservice) throw errors.forbidden("An application service token is required");
353
- if (body.username === void 0) throw errors.missingParam("Missing username");
354
- const localpart = body.username.toLowerCase();
355
- const userId = `@${localpart}:${hs.serverName}`;
356
- if (!hs.appservices.isInterestedUser(s.appservice, userId)) throw errors.exclusive("User is not in the application service's namespace");
357
- if (!LOCALPART_RE.test(localpart)) throw errors.invalidUsername();
358
+ }), ["v1"]);
359
+ r.client("POST", "/register", procedure({
360
+ input: registerBody,
361
+ handler: async (ctx) => {
362
+ const hs = ctx.hs;
363
+ const body = ctx.input;
364
+ const kind = ctx.query.get("kind") ?? "user";
365
+ if (kind === "guest") {
366
+ if (!hs.userAuth.guestAccessEnabled) throw errors.forbidden("Guest access is disabled");
367
+ const userId = await createUser(hs, await generateLocalpart(hs), { guest: true });
368
+ return {
369
+ ...await hs.auth.createSession(userId, { displayName: body.initial_device_display_name }),
370
+ home_server: hs.serverName
371
+ };
372
+ }
373
+ if (kind !== "user") throw errors.invalidParam("Unknown kind");
374
+ if (body.type === M.LOGIN.APPLICATION_SERVICE || body.auth?.type === M.LOGIN.APPLICATION_SERVICE) {
375
+ const s = await ctx.auth();
376
+ if (!s.appservice) throw errors.forbidden("An application service token is required");
377
+ if (body.username === void 0) throw errors.missingParam("Missing username");
378
+ const localpart = body.username.toLowerCase();
379
+ const userId = `@${localpart}:${hs.serverName}`;
380
+ if (!hs.appservices.isInterestedUser(s.appservice, userId)) throw errors.exclusive("User is not in the application service's namespace");
381
+ if (!LOCALPART_RE.test(localpart)) throw errors.invalidUsername();
382
+ if (await hs.db.findOne("users", [{
383
+ field: "user_id",
384
+ value: userId
385
+ }])) throw errors.userInUse();
386
+ await createUser(hs, localpart, {});
387
+ if (body.inhibit_login === true) return {
388
+ user_id: userId,
389
+ home_server: hs.serverName
390
+ };
391
+ return {
392
+ ...await hs.auth.createSession(userId, {
393
+ deviceId: body.device_id,
394
+ displayName: body.initial_device_display_name
395
+ }),
396
+ home_server: hs.serverName
397
+ };
398
+ }
399
+ if (ctx.accessToken() && hs.appservices.byToken(ctx.accessToken())) throw errors.missingParam("Application services must use type m.login.application_service");
400
+ rejectIfDelegated(hs);
401
+ if (!await hs.registrationAllowed()) throw errors.forbidden("Registration is disabled");
402
+ await ctx.rateLimit("register");
403
+ const localpart = body.username !== void 0 ? await validateNewUsername(hs, body.username.toLowerCase()) : await generateLocalpart(hs);
404
+ if (hs.appservices.exclusiveUserOwner(`@${localpart}:${hs.serverName}`)) throw errors.exclusive("This user ID is reserved by an application service");
405
+ if (body.password !== void 0) {
406
+ const problem = await hs.userAuth.validatePassword(body.password);
407
+ if (problem) throw new MatrixError(400, "M_WEAK_PASSWORD", problem);
408
+ }
409
+ const flows = await registrationFlows(hs);
410
+ const { threepids } = await requireUia(hs, body, flows, null);
411
+ await consumeRegistrationToken(hs, body);
358
412
  if (await hs.db.findOne("users", [{
359
- field: "user_id",
360
- value: userId
413
+ field: "localpart",
414
+ value: localpart
361
415
  }])) throw errors.userInUse();
362
- await createUser(hs, localpart, {});
416
+ const userId = await createUser(hs, localpart, { password: body.password });
417
+ for (const t of threepids) await hs.threepids.add(userId, t).catch((err) => hs.logger.warn("Could not attach", t.medium, "to", userId, err));
418
+ await hs.audit.record({
419
+ action: "register",
420
+ actor: userId,
421
+ ip: ctx.ip,
422
+ userAgent: ctx.request.headers.get("user-agent"),
423
+ details: { token: body.auth?.type === M.LOGIN.REGISTRATION_TOKEN }
424
+ });
363
425
  if (body.inhibit_login === true) return {
364
426
  user_id: userId,
365
427
  home_server: hs.serverName
@@ -367,166 +429,138 @@ function registerAccountRoutes(r) {
367
429
  return {
368
430
  ...await hs.auth.createSession(userId, {
369
431
  deviceId: body.device_id,
370
- displayName: body.initial_device_display_name
432
+ displayName: body.initial_device_display_name,
433
+ refreshToken: body.refresh_token === true
371
434
  }),
372
435
  home_server: hs.serverName
373
436
  };
374
437
  }
375
- if (ctx.accessToken() && hs.appservices.byToken(ctx.accessToken())) throw errors.missingParam("Application services must use type m.login.application_service");
376
- rejectIfDelegated(hs);
377
- if (!await hs.registrationAllowed()) throw errors.forbidden("Registration is disabled");
378
- await ctx.rateLimit("register");
379
- const localpart = body.username !== void 0 ? await validateNewUsername(hs, body.username.toLowerCase()) : await generateLocalpart(hs);
380
- if (hs.appservices.exclusiveUserOwner(`@${localpart}:${hs.serverName}`)) throw errors.exclusive("This user ID is reserved by an application service");
381
- if (body.password !== void 0) {
382
- const problem = await hs.userAuth.validatePassword(body.password);
438
+ }));
439
+ r.client("POST", "/account/password", procedure({
440
+ input: passwordBody,
441
+ handler: async (ctx) => {
442
+ const hs = ctx.hs;
443
+ rejectIfDelegated(hs);
444
+ const body = ctx.input;
445
+ let s;
446
+ if (ctx.accessToken()) {
447
+ const session = await ctx.auth();
448
+ await requireUia(hs, body, [[M.LOGIN.PASSWORD]], session.userId);
449
+ s = {
450
+ userId: session.userId,
451
+ deviceId: session.deviceId
452
+ };
453
+ } else {
454
+ const { threepids } = await requireUia(hs, body, [[M.LOGIN.EMAIL.IDENTITY], [M.LOGIN.MSISDN]], null);
455
+ const owner = threepids.length ? await hs.threepids.userFor(threepids[0].medium, threepids[0].address) : null;
456
+ if (!owner) throw new MatrixError(400, "M_THREEPID_NOT_FOUND", "No account uses this address");
457
+ s = {
458
+ userId: owner,
459
+ deviceId: null
460
+ };
461
+ }
462
+ if (!body.new_password) throw errors.missingParam("Missing new_password");
463
+ const problem = await hs.userAuth.validatePassword(body.new_password);
383
464
  if (problem) throw new MatrixError(400, "M_WEAK_PASSWORD", problem);
465
+ await hs.db.update("users", [{
466
+ field: "user_id",
467
+ value: s.userId
468
+ }], { password_hash: await hs.userAuth.hashPassword(body.new_password) });
469
+ await hs.audit.record({
470
+ action: "password_change",
471
+ actor: s.userId,
472
+ ip: ctx.ip,
473
+ details: { logout_devices: body.logout_devices !== false }
474
+ });
475
+ if (body.logout_devices !== false) await hs.auth.deleteAllDevices(s.userId, s.deviceId ?? void 0);
476
+ return {};
384
477
  }
385
- const flows = await registrationFlows(hs);
386
- const { threepids } = await requireUia(hs, body, flows, null);
387
- await consumeRegistrationToken(hs, body);
388
- if (await hs.db.findOne("users", [{
389
- field: "localpart",
390
- value: localpart
391
- }])) throw errors.userInUse();
392
- const userId = await createUser(hs, localpart, { password: body.password });
393
- for (const t of threepids) await hs.threepids.add(userId, t).catch((err) => hs.logger.warn("Could not attach", t.medium, "to", userId, err));
394
- await hs.audit.record({
395
- action: "register",
396
- actor: userId,
397
- ip: ctx.ip,
398
- userAgent: ctx.request.headers.get("user-agent"),
399
- details: { token: body.auth?.type === "m.login.registration_token" }
400
- });
401
- if (body.inhibit_login === true) return {
402
- user_id: userId,
403
- home_server: hs.serverName
404
- };
405
- return {
406
- ...await hs.auth.createSession(userId, {
407
- deviceId: body.device_id,
408
- displayName: body.initial_device_display_name,
409
- refreshToken: body.refresh_token === true
410
- }),
411
- home_server: hs.serverName
412
- };
413
- });
414
- r.client("POST", "/account/password", { input: passwordBody }, async (ctx) => {
415
- const hs = ctx.hs;
416
- rejectIfDelegated(hs);
417
- const body = ctx.input;
418
- let s;
419
- if (ctx.accessToken()) {
420
- const session = await ctx.auth();
421
- await requireUia(hs, body, [["m.login.password"]], session.userId);
422
- s = {
423
- userId: session.userId,
424
- deviceId: session.deviceId
425
- };
426
- } else {
427
- const { threepids } = await requireUia(hs, body, [["m.login.email.identity"], ["m.login.msisdn"]], null);
428
- const owner = threepids.length ? await hs.threepids.userFor(threepids[0].medium, threepids[0].address) : null;
429
- if (!owner) throw new MatrixError(400, "M_THREEPID_NOT_FOUND", "No account uses this address");
430
- s = {
431
- userId: owner,
432
- deviceId: null
433
- };
478
+ }));
479
+ r.client("POST", "/account/deactivate", procedure({
480
+ input: body({}),
481
+ handler: async (ctx) => {
482
+ const hs = ctx.hs;
483
+ rejectIfDelegated(hs);
484
+ const s = await ctx.auth();
485
+ await requireUia(hs, ctx.input, [[M.LOGIN.PASSWORD]], s.userId);
486
+ for (const membership of [
487
+ "join",
488
+ "invite",
489
+ "knock"
490
+ ]) for (const roomId of await hs.rooms.roomsForUser(s.userId, membership)) try {
491
+ await hs.rooms.setMembership(roomId, s.userId, s.userId, "leave");
492
+ } catch {}
493
+ await hs.auth.deleteAllDevices(s.userId);
494
+ await hs.db.update("users", [{
495
+ field: "user_id",
496
+ value: s.userId
497
+ }], {
498
+ deactivated: true,
499
+ password_hash: null,
500
+ displayname: null,
501
+ avatar_url: null
502
+ });
503
+ await hs.db.delete("profile_fields", [{
504
+ field: "user_id",
505
+ value: s.userId
506
+ }]);
507
+ const unbind = await hs.threepids.removeAll(s.userId);
508
+ await hs.audit.record({
509
+ action: "deactivate",
510
+ actor: s.userId,
511
+ target: s.userId,
512
+ ip: ctx.ip
513
+ });
514
+ return { id_server_unbind_result: unbind };
434
515
  }
435
- if (!body.new_password) throw errors.missingParam("Missing new_password");
436
- const problem = await hs.userAuth.validatePassword(body.new_password);
437
- if (problem) throw new MatrixError(400, "M_WEAK_PASSWORD", problem);
438
- await hs.db.update("users", [{
439
- field: "user_id",
440
- value: s.userId
441
- }], { password_hash: await hs.userAuth.hashPassword(body.new_password) });
442
- await hs.audit.record({
443
- action: "password_change",
444
- actor: s.userId,
445
- ip: ctx.ip,
446
- details: { logout_devices: body.logout_devices !== false }
447
- });
448
- if (body.logout_devices !== false) await hs.auth.deleteAllDevices(s.userId, s.deviceId ?? void 0);
449
- return {};
450
- });
451
- r.client("POST", "/account/deactivate", { input: body({}) }, async (ctx) => {
452
- const hs = ctx.hs;
453
- rejectIfDelegated(hs);
454
- const s = await ctx.auth();
455
- await requireUia(hs, ctx.input, [["m.login.password"]], s.userId);
456
- for (const membership of [
457
- "join",
458
- "invite",
459
- "knock"
460
- ]) for (const roomId of await hs.rooms.roomsForUser(s.userId, membership)) try {
461
- await hs.rooms.setMembership(roomId, s.userId, s.userId, "leave");
462
- } catch {}
463
- await hs.auth.deleteAllDevices(s.userId);
464
- await hs.db.update("users", [{
465
- field: "user_id",
466
- value: s.userId
467
- }], {
468
- deactivated: true,
469
- password_hash: null,
470
- displayname: null,
471
- avatar_url: null
472
- });
473
- await hs.db.delete("profile_fields", [{
474
- field: "user_id",
475
- value: s.userId
476
- }]);
477
- const unbind = await hs.threepids.removeAll(s.userId);
478
- await hs.audit.record({
479
- action: "deactivate",
480
- actor: s.userId,
481
- target: s.userId,
482
- ip: ctx.ip
483
- });
484
- return { id_server_unbind_result: unbind };
485
- });
486
- r.get("/_synapse/admin/v1/register", async (ctx) => {
516
+ }));
517
+ r.get("/_synapse/admin/v1/register", procedure({ handler: async (ctx) => {
487
518
  if (!ctx.hs.config.registrationSharedSecret) throw errors.unrecognized();
488
519
  const nonce = randomString(32);
489
520
  await ctx.hs.cluster.set(`regnonce:${nonce}`, true, 6e4);
490
521
  return { nonce };
491
- });
492
- r.post("/_synapse/admin/v1/register", { input: sharedSecretRegisterBody }, async (ctx) => {
493
- const hs = ctx.hs;
494
- const secret = hs.config.registrationSharedSecret;
495
- if (!secret) throw errors.unrecognized();
496
- const body = ctx.input;
497
- if (body.nonce === void 0 || !await hs.cluster.get(`regnonce:${body.nonce}`)) throw errors.unknown("Unrecognised nonce");
498
- await hs.cluster.delete(`regnonce:${body.nonce}`);
499
- const admin = body.admin === true;
500
- const expected = createHmac("sha1", secret).update(`${body.nonce}\u0000${body.username}\u0000${body.password}\u0000${admin ? "admin" : "notadmin"}`).digest("hex");
501
- const given = Buffer.from(body.mac ?? "", "utf8");
502
- if (given.length !== expected.length || !timingSafeEqual(given, Buffer.from(expected, "utf8"))) throw errors.forbidden("HMAC incorrect");
503
- const userId = await createUser(hs, await validateNewUsername(hs, body.username.toLowerCase()), {
504
- password: body.password,
505
- admin,
506
- displayname: body.displayname
507
- });
508
- const session = await hs.auth.createSession(userId);
509
- await hs.audit.record({
510
- action: "register",
511
- actor: "server",
512
- target: userId,
513
- ip: ctx.ip,
514
- details: {
515
- shared_secret: true,
516
- admin
517
- }
518
- });
519
- return {
520
- ...session,
521
- home_server: hs.serverName
522
- };
523
- });
522
+ } }));
523
+ r.post("/_synapse/admin/v1/register", procedure({
524
+ input: sharedSecretRegisterBody,
525
+ handler: async (ctx) => {
526
+ const hs = ctx.hs;
527
+ const secret = hs.config.registrationSharedSecret;
528
+ if (!secret) throw errors.unrecognized();
529
+ const body = ctx.input;
530
+ if (body.nonce === void 0 || !await hs.cluster.get(`regnonce:${body.nonce}`)) throw errors.unknown("Unrecognised nonce");
531
+ await hs.cluster.delete(`regnonce:${body.nonce}`);
532
+ const admin = body.admin === true;
533
+ const expected = createHmac("sha1", secret).update(`${body.nonce}\u0000${body.username}\u0000${body.password}\u0000${admin ? "admin" : "notadmin"}`).digest("hex");
534
+ const given = Buffer.from(body.mac ?? "", "utf8");
535
+ if (given.length !== expected.length || !timingSafeEqual(given, Buffer.from(expected, "utf8"))) throw errors.forbidden("HMAC incorrect");
536
+ const userId = await createUser(hs, await validateNewUsername(hs, body.username.toLowerCase()), {
537
+ password: body.password,
538
+ admin,
539
+ displayname: body.displayname
540
+ });
541
+ const session = await hs.auth.createSession(userId);
542
+ await hs.audit.record({
543
+ action: "register",
544
+ actor: "server",
545
+ target: userId,
546
+ ip: ctx.ip,
547
+ details: {
548
+ shared_secret: true,
549
+ admin
550
+ }
551
+ });
552
+ return {
553
+ ...session,
554
+ home_server: hs.serverName
555
+ };
556
+ }
557
+ }));
524
558
  }
525
559
  async function registrationFlows(hs) {
526
- if (hs.adminRoom?.options.firstUserCanRegister && await hs.adminRoom.noUsersYet()) return [["m.login.dummy"]];
527
- const base = await hs.registrationMode() === "token" ? ["m.login.registration_token"] : ["m.login.dummy"];
560
+ if (hs.adminRoom?.options.firstUserCanRegister && await hs.adminRoom.noUsersYet()) return [[M.LOGIN.DUMMY]];
561
+ const base = await hs.registrationMode() === "token" ? [M.LOGIN.REGISTRATION_TOKEN] : [M.LOGIN.DUMMY];
528
562
  if (!hs.threepids.canValidate("email")) return [base];
529
- const withEmail = [...base.filter((s) => s !== "m.login.dummy"), "m.login.email.identity"];
563
+ const withEmail = [...base.filter((s) => s !== M.LOGIN.DUMMY), M.LOGIN.EMAIL.IDENTITY];
530
564
  return hs.config.threepid.requireEmailForRegistration ? [withEmail] : [base, withEmail];
531
565
  }
532
566
  async function registrationTokenValid(hs, token) {
@@ -539,7 +573,7 @@ async function registrationTokenValid(hs, token) {
539
573
  return row.uses_allowed === null || row.completed < row.uses_allowed;
540
574
  }
541
575
  async function consumeRegistrationToken(hs, body) {
542
- if (body.auth?.type !== "m.login.registration_token") return;
576
+ if (body.auth?.type !== M.LOGIN.REGISTRATION_TOKEN) return;
543
577
  const token = body.auth?.token;
544
578
  const row = await hs.db.findOne("registration_tokens", [{
545
579
  field: "token",
@@ -552,8 +586,8 @@ async function consumeRegistrationToken(hs, body) {
552
586
  }
553
587
  function wellKnown(hs) {
554
588
  if (!hs.config.publicBaseUrl) return null;
555
- const wk = { "m.homeserver": { base_url: hs.config.publicBaseUrl } };
556
- if (hs.config.rtc.transports?.length) wk["org.matrix.msc4143.rtc_foci"] = hs.config.rtc.transports;
589
+ const wk = { [M.HOMESERVER]: { base_url: hs.config.publicBaseUrl } };
590
+ if (hs.config.rtc.transports?.length) wk[UNSTABLE.ORG.MATRIX.MSC4143.RTC_FOCI] = hs.config.rtc.transports;
557
591
  return wk;
558
592
  }
559
593
  //#endregion