matrix-nodejs-homeserver 0.2.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +89 -11
  2. package/dist/adapters/cluster.d.ts +1 -26
  3. package/dist/adapters/cluster.js +50 -103
  4. package/dist/api/account.js +407 -354
  5. package/dist/api/admin.js +82 -61
  6. package/dist/api/appservice.js +37 -25
  7. package/dist/api/backup.js +73 -62
  8. package/dist/api/federation.js +172 -138
  9. package/dist/api/filters.js +4 -3
  10. package/dist/api/legacy.js +91 -78
  11. package/dist/api/media.js +71 -55
  12. package/dist/api/misc.js +46 -37
  13. package/dist/api/preview.js +37 -73
  14. package/dist/api/profile.js +108 -97
  15. package/dist/api/push.js +114 -99
  16. package/dist/api/relationships.js +64 -58
  17. package/dist/api/rooms.js +703 -580
  18. package/dist/api/rtc.d.ts +0 -1
  19. package/dist/api/rtc.js +48 -34
  20. package/dist/api/search.js +133 -127
  21. package/dist/api/slidingsync.js +41 -31
  22. package/dist/api/spaces.js +82 -77
  23. package/dist/api/sync.js +325 -135
  24. package/dist/api/threadsubs.js +72 -61
  25. package/dist/api/threepid.js +99 -66
  26. package/dist/api/uia.js +17 -16
  27. package/dist/api/user.js +221 -178
  28. package/dist/core/admin.d.ts +0 -1
  29. package/dist/core/admin.js +6 -5
  30. package/dist/core/adminroom.js +9 -8
  31. package/dist/core/appservices.d.ts +2 -1
  32. package/dist/core/appservices.js +5 -4
  33. package/dist/core/audit.d.ts +0 -1
  34. package/dist/core/authrules.js +26 -25
  35. package/dist/core/cluster.d.ts +12 -5
  36. package/dist/core/cluster.js +46 -29
  37. package/dist/core/delayed.js +2 -1
  38. package/dist/core/events.js +11 -10
  39. package/dist/core/html.js +13 -0
  40. package/dist/core/identity.js +1 -1
  41. package/dist/core/invitefilter.js +2 -1
  42. package/dist/core/json.js +10 -1
  43. package/dist/core/keys.d.ts +10 -1
  44. package/dist/core/keys.js +62 -42
  45. package/dist/core/m.d.ts +209 -0
  46. package/dist/core/m.js +182 -0
  47. package/dist/core/outbound.d.ts +16 -0
  48. package/dist/core/outbound.js +172 -0
  49. package/dist/core/partialstate.js +2 -1
  50. package/dist/core/policy.js +4 -3
  51. package/dist/core/push.d.ts +0 -1
  52. package/dist/core/push.js +41 -28
  53. package/dist/core/pushrules.js +36 -35
  54. package/dist/core/ratelimit.d.ts +1 -1
  55. package/dist/core/rooms.d.ts +10 -1
  56. package/dist/core/rooms.js +101 -74
  57. package/dist/core/semaphore.js +23 -0
  58. package/dist/core/sso.d.ts +15 -1
  59. package/dist/core/sso.js +38 -2
  60. package/dist/core/stateres.js +7 -6
  61. package/dist/core/threepid.js +1 -9
  62. package/dist/federation/client.js +9 -1
  63. package/dist/federation/index.d.ts +2 -2
  64. package/dist/federation/index.js +43 -35
  65. package/dist/homeserver.d.ts +23 -4
  66. package/dist/homeserver.js +12 -3
  67. package/dist/http/handler.js +2 -23
  68. package/dist/http/input.js +2 -2
  69. package/dist/http/procedure.js +25 -0
  70. package/dist/http/router.d.ts +9 -26
  71. package/dist/http/router.js +10 -30
  72. package/dist/index.d.ts +4 -3
  73. package/dist/index.js +3 -2
  74. package/dist/storage/batch.js +11 -0
  75. package/dist/storage/memory.js +1 -0
  76. package/dist/storage/schema.js +1 -1
  77. package/dist/storage/sql.js +20 -1
  78. package/dist/storage/types.d.ts +16 -0
  79. package/dist/vendor/zod/LICENSE +21 -0
  80. package/dist/vendor/zod/v4/classic/errors.js +54 -0
  81. package/dist/vendor/zod/v4/classic/parse.js +17 -0
  82. package/dist/vendor/zod/v4/classic/schemas.js +865 -0
  83. package/dist/vendor/zod/v4/core/api.js +512 -0
  84. package/dist/vendor/zod/v4/core/checks.js +300 -0
  85. package/dist/vendor/zod/v4/core/core.js +109 -0
  86. package/dist/vendor/zod/v4/core/doc.js +35 -0
  87. package/dist/vendor/zod/v4/core/errors.js +118 -0
  88. package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
  89. package/dist/vendor/zod/v4/core/memoizer.js +255 -0
  90. package/dist/vendor/zod/v4/core/parse.js +206 -0
  91. package/dist/vendor/zod/v4/core/regexes.js +73 -0
  92. package/dist/vendor/zod/v4/core/registries.js +48 -0
  93. package/dist/vendor/zod/v4/core/schemas.js +1378 -0
  94. package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
  95. package/dist/vendor/zod/v4/core/util.js +537 -0
  96. package/dist/vendor/zod/v4/core/versions.js +8 -0
  97. package/dist/vendor/zod/v4/locales/en.js +110 -0
  98. package/package.json +8 -6
  99. package/dist/core/pushrules.d.ts +0 -1
@@ -1,51 +1,55 @@
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";
7
+ import { escapeHtml } from "../core/html.js";
6
8
  import { canonicalAddress, isMedium } from "../core/threepid.js";
7
- import { requireUia } from "./uia.js";
9
+ import { boolean, looseObject, string } from "../vendor/zod/v4/classic/schemas.js";
8
10
  import { body, fail, lenient } from "../http/input.js";
11
+ import { procedure } from "../http/procedure.js";
12
+ import { requireUia } from "./uia.js";
13
+ import { checkRedirectUrl } from "../core/sso.js";
9
14
  import { createHmac, randomInt, timingSafeEqual } from "node:crypto";
10
- import { z } from "zod";
11
15
  //#region src/api/account.ts
12
16
  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())
17
+ device_id: lenient(string()),
18
+ initial_device_display_name: lenient(string()),
19
+ refresh_token: lenient(boolean()),
20
+ inhibit_login: lenient(boolean())
17
21
  };
18
22
  const loginBody = body({
19
- type: lenient(z.string()),
20
- token: lenient(z.string()),
21
- password: lenient(z.string()),
23
+ type: lenient(string()),
24
+ token: lenient(string()),
25
+ password: lenient(string()),
22
26
  ...deviceFields
23
27
  });
24
28
  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(),
29
+ type: lenient(string()),
30
+ auth: lenient(looseObject({ type: lenient(string()) })),
31
+ username: string(fail("M_INVALID_PARAM", "username must be a string")).optional(),
32
+ password: string(fail("M_INVALID_PARAM", "password must be a string")).optional(),
29
33
  ...deviceFields
30
34
  });
31
35
  const passwordBody = body({
32
- new_password: lenient(z.string()),
33
- logout_devices: lenient(z.boolean())
36
+ new_password: lenient(string()),
37
+ logout_devices: lenient(boolean())
34
38
  });
35
39
  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())
40
+ nonce: lenient(string()),
41
+ username: string(fail("M_INVALID_PARAM", "Invalid username/password")),
42
+ password: string(fail("M_INVALID_PARAM", "Invalid username/password")),
43
+ admin: lenient(boolean()),
44
+ mac: lenient(string()),
45
+ displayname: lenient(string())
42
46
  });
43
47
  function loginFlows(hs) {
44
48
  if (hs.oauth) return [];
45
49
  const flows = [];
46
- if (hs.userAuth.passwordLoginEnabled) flows.push({ type: "m.login.password" });
50
+ if (hs.userAuth.passwordLoginEnabled) flows.push({ type: M.LOGIN.PASSWORD });
47
51
  if (hs.sso) flows.push({
48
- type: "m.login.sso",
52
+ type: M.LOGIN.SSO,
49
53
  identity_providers: hs.sso.providers.map((p) => ({
50
54
  id: p.id,
51
55
  name: p.name,
@@ -54,7 +58,7 @@ function loginFlows(hs) {
54
58
  }))
55
59
  });
56
60
  flows.push({
57
- type: "m.login.token",
61
+ type: M.LOGIN.TOKEN,
58
62
  ...hs.config.loginViaExistingSession ? { get_login_token: true } : {}
59
63
  });
60
64
  return flows;
@@ -105,11 +109,11 @@ async function resolveLoginUser(hs, body) {
105
109
  let user;
106
110
  const id = isObject(body.identifier) ? body.identifier : null;
107
111
  let threepid = null;
108
- if (id?.type === "m.id.thirdparty" && typeof id.medium === "string" && typeof id.address === "string") threepid = {
112
+ if (id?.type === M.ID.THIRDPARTY && typeof id.medium === "string" && typeof id.address === "string") threepid = {
109
113
  medium: id.medium,
110
114
  address: isMedium(id.medium) ? canonicalAddress(id.medium, id.address) : null
111
115
  };
112
- else if (id?.type === "m.id.phone" && typeof id.country === "string" && typeof id.phone === "string") threepid = {
116
+ else if (id?.type === M.ID.PHONE && typeof id.country === "string" && typeof id.phone === "string") threepid = {
113
117
  medium: "msisdn",
114
118
  address: toMsisdn(id.country, id.phone)
115
119
  };
@@ -123,7 +127,7 @@ async function resolveLoginUser(hs, body) {
123
127
  return owner;
124
128
  }
125
129
  if (id) {
126
- if (id.type !== "m.id.user") throw errors.unknown("Unsupported identifier type");
130
+ if (id.type !== M.ID.USER) throw errors.unknown("Unsupported identifier type");
127
131
  user = id.user;
128
132
  } else user = body.user;
129
133
  if (typeof user !== "string" || !user) throw errors.missingParam("Missing user identifier");
@@ -135,77 +139,152 @@ async function resolveLoginUser(hs, body) {
135
139
  return `@${user.toLowerCase()}:${hs.serverName}`;
136
140
  }
137
141
  function registerAccountRoutes(r) {
138
- r.client("GET", "/login", (ctx) => ({ flows: loginFlows(ctx.hs) }));
139
- r.client("GET", "/auth_metadata", async (ctx) => {
142
+ r.client("GET", "/login", procedure({ handler: (ctx) => ({ flows: loginFlows(ctx.hs) }) }));
143
+ r.client("GET", "/auth_metadata", procedure({ handler: async (ctx) => {
140
144
  if (!ctx.hs.oauth) throw errors.unrecognized("OAuth is not enabled");
141
145
  return ctx.hs.oauth.serverMetadata();
142
- }, ["v1"]);
143
- r.client("GET", "/auth_issuer", (ctx) => {
146
+ } }), ["v1"]);
147
+ r.client("GET", "/auth_issuer", procedure({ handler: (ctx) => {
144
148
  if (!ctx.hs.oauth) throw errors.unrecognized("OAuth is not enabled");
145
149
  return { issuer: ctx.hs.oauth.options.issuer };
146
- }, ["v1"]);
150
+ } }), ["v1"]);
151
+ const ssoConfirmPage = (serverName, result) => {
152
+ const client = escapeHtml(result.client);
153
+ const html = `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Continue to ${client}</title>
154
+ <style>body{font:16px system-ui,sans-serif;max-width:32rem;margin:3rem auto;padding:0 1rem}a.button{display:inline-block;padding:.6rem 1.2rem;background:#0b7a55;color:#fff;border-radius:6px;text-decoration:none}</style>
155
+ <h1>Continue to ${client}?</h1>
156
+ <p>You signed in to <b>${escapeHtml(serverName)}</b>. <b>${client}</b> will get access to your account.</p>
157
+ <p>If you did not just sign in with this app, close this page.</p>
158
+ <p><a class="button" href="${escapeHtml(result.url)}">Continue to ${client}</a></p>`;
159
+ return new Response(html, { headers: {
160
+ "content-type": "text/html; charset=utf-8",
161
+ "cache-control": "no-store",
162
+ "referrer-policy": "no-referrer",
163
+ "x-frame-options": "DENY",
164
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'"
165
+ } });
166
+ };
147
167
  const ssoRedirect = async (ctx) => {
148
168
  if (!ctx.hs.sso) throw errors.unrecognized("SSO is not enabled");
149
- const redirectUrl = ctx.query.get("redirectUrl");
150
- if (!redirectUrl || !/^https?:\/\/|^[a-z][a-z0-9+.-]*:/i.test(redirectUrl)) throw errors.missingParam("Missing or invalid redirectUrl");
169
+ const redirectUrl = checkRedirectUrl(ctx.query.get("redirectUrl"));
170
+ if (!redirectUrl) throw errors.missingParam("Missing or invalid redirectUrl");
151
171
  return Response.redirect(await ctx.hs.sso.startLogin(ctx.params.idpId ?? null, redirectUrl), 302);
152
172
  };
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) => {
167
- if (!ctx.hs.sso) throw errors.unrecognized("SSO is not enabled");
168
- 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");
178
- 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", [{
181
- field: "user_id",
182
- value: userId
183
- }])) throw errors.forbidden("Unknown user");
173
+ r.client("POST", "/login/get_token", procedure({
174
+ auth: "user",
175
+ handler: async (ctx) => {
176
+ const s = ctx.session;
177
+ if (!ctx.hs.config.loginViaExistingSession || ctx.hs.oauth || s.isGuest) throw errors.unrecognized();
178
+ await ctx.rateLimit("login");
179
+ await requireUia(ctx.hs, await ctx.json(), [[M.LOGIN.PASSWORD]], s.userId);
180
+ const ttl = 12e4;
184
181
  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
182
+ login_token: await ctx.hs.auth.createLoginToken(s.userId, ttl),
183
+ expires_in_ms: ttl
190
184
  };
191
185
  }
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) => {
186
+ }), ["v1"]);
187
+ r.client("GET", "/login/sso/redirect", procedure({ handler: ssoRedirect }));
188
+ r.client("GET", "/login/sso/redirect/:idpId", procedure({ handler: ssoRedirect }));
189
+ r.client("GET", "/login/sso/callback/:idpId", procedure({ handler: async (ctx) => {
190
+ if (!ctx.hs.sso) throw errors.unrecognized("SSO is not enabled");
191
+ const result = await ctx.hs.sso.finishLogin(ctx.params.idpId, ctx.query);
192
+ return result.confirm ? ssoConfirmPage(ctx.hs.serverName, result) : Response.redirect(result.url, 302);
193
+ } }));
194
+ r.client("POST", "/login", procedure({
195
+ rateLimit: "login",
196
+ input: loginBody,
197
+ handler: async (ctx) => {
198
+ const body = ctx.input;
199
+ const hs = ctx.hs;
200
+ if (hs.oauth) throw errors.unrecognized("Log in through the OAuth provider");
201
+ if (body.type === M.LOGIN.APPLICATION_SERVICE) {
202
+ const reg = ctx.accessToken() ? hs.appservices.byToken(ctx.accessToken()) : null;
203
+ if (!reg) throw errors.unknownToken("An application service token is required");
204
+ const userId = await resolveLoginUser(hs, body);
205
+ if (!hs.appservices.isInterestedUser(reg, userId)) throw errors.forbidden("User is not in the application service's namespace");
206
+ if (!await hs.db.findOne("users", [{
207
+ field: "user_id",
208
+ value: userId
209
+ }])) throw errors.forbidden("Unknown user");
210
+ return {
211
+ ...await hs.auth.createSession(userId, {
212
+ deviceId: body.device_id,
213
+ displayName: body.initial_device_display_name
214
+ }),
215
+ home_server: hs.serverName
216
+ };
217
+ }
218
+ if (body.type === M.LOGIN.TOKEN) {
219
+ if (body.token === void 0) throw errors.missingParam("Missing token");
220
+ const userId = await hs.auth.consumeLoginToken(body.token).catch(async (err) => {
221
+ await hs.audit.record({
222
+ action: "login_failed",
223
+ actor: null,
224
+ ip: ctx.ip,
225
+ outcome: "failure",
226
+ details: { type: M.LOGIN.TOKEN }
227
+ });
228
+ throw err;
229
+ });
230
+ const user = await hs.db.findOne("users", [{
231
+ field: "user_id",
232
+ value: userId
233
+ }]);
234
+ if (!user || user.deactivated) throw errors.userDeactivated();
235
+ const session = await hs.auth.createSession(userId, {
236
+ deviceId: body.device_id,
237
+ displayName: body.initial_device_display_name,
238
+ refreshToken: body.refresh_token === true
239
+ });
195
240
  await hs.audit.record({
196
- action: "login_failed",
197
- actor: null,
241
+ action: "login",
242
+ actor: userId,
243
+ target: session.device_id,
198
244
  ip: ctx.ip,
199
- outcome: "failure",
200
- details: { type: "m.login.token" }
245
+ userAgent: ctx.request.headers.get("user-agent"),
246
+ details: { type: M.LOGIN.TOKEN }
201
247
  });
202
- throw err;
203
- });
204
- const user = await hs.db.findOne("users", [{
248
+ return {
249
+ ...session,
250
+ home_server: hs.serverName,
251
+ well_known: wellKnown(hs) ?? void 0
252
+ };
253
+ }
254
+ if (body.type !== M.LOGIN.PASSWORD || !hs.userAuth.passwordLoginEnabled) throw errors.unknown("Unsupported login type");
255
+ const userId = await resolveLoginUser(hs, body);
256
+ if (body.password === void 0) throw errors.missingParam("Missing password");
257
+ let user = await hs.db.findOne("users", [{
205
258
  field: "user_id",
206
259
  value: userId
207
260
  }]);
208
- if (!user || user.deactivated) throw errors.userDeactivated();
261
+ const ok = await hs.userAuth.checkPassword({
262
+ userId,
263
+ localpart: userId.slice(1).split(":")[0],
264
+ password: body.password,
265
+ storedHash: user?.password_hash ?? null
266
+ });
267
+ const ua = ctx.request.headers.get("user-agent");
268
+ if (!ok) {
269
+ await hs.audit.record({
270
+ action: "login_failed",
271
+ actor: userId,
272
+ ip: ctx.ip,
273
+ userAgent: ua,
274
+ outcome: "failure",
275
+ details: { type: M.LOGIN.PASSWORD }
276
+ });
277
+ throw errors.forbidden("Invalid username or password");
278
+ }
279
+ if (!user) {
280
+ if (!await hs.userAuth.autoProvision(userId)) throw errors.forbidden("Invalid username or password");
281
+ await createUser(hs, userId.slice(1).split(":")[0], {});
282
+ user = await hs.db.findOne("users", [{
283
+ field: "user_id",
284
+ value: userId
285
+ }]);
286
+ }
287
+ if (user.deactivated) throw errors.userDeactivated();
209
288
  const session = await hs.auth.createSession(userId, {
210
289
  deviceId: body.device_id,
211
290
  displayName: body.initial_device_display_name,
@@ -216,8 +295,8 @@ function registerAccountRoutes(r) {
216
295
  actor: userId,
217
296
  target: session.device_id,
218
297
  ip: ctx.ip,
219
- userAgent: ctx.request.headers.get("user-agent"),
220
- details: { type: "m.login.token" }
298
+ userAgent: ua,
299
+ details: { type: M.LOGIN.PASSWORD }
221
300
  });
222
301
  return {
223
302
  ...session,
@@ -225,141 +304,143 @@ function registerAccountRoutes(r) {
225
304
  well_known: wellKnown(hs) ?? void 0
226
305
  };
227
306
  }
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,
307
+ }));
308
+ r.client("POST", "/refresh", procedure({
309
+ input: body({ refresh_token: lenient(string()) }),
310
+ handler: async (ctx) => {
311
+ const body = ctx.input;
312
+ if (body.refresh_token === void 0) throw errors.missingParam("Missing refresh_token");
313
+ const { access_token, refresh_token, expires_in_ms } = await ctx.hs.auth.refresh(body.refresh_token);
314
+ return {
315
+ access_token,
316
+ refresh_token,
317
+ expires_in_ms
318
+ };
319
+ }
320
+ }));
321
+ r.client("POST", "/logout", procedure({
322
+ auth: "user",
323
+ handler: async (ctx) => {
324
+ const s = ctx.session;
325
+ await ctx.hs.auth.deleteDevice(s.userId, s.deviceId);
326
+ await ctx.hs.audit.record({
327
+ action: "logout",
328
+ actor: s.userId,
329
+ target: s.deviceId,
330
+ ip: ctx.ip
331
+ });
332
+ return {};
333
+ }
334
+ }));
335
+ r.client("POST", "/logout/all", procedure({
336
+ auth: "user",
337
+ handler: async (ctx) => {
338
+ const s = ctx.session;
339
+ await ctx.hs.auth.deleteAllDevices(s.userId);
340
+ await ctx.hs.audit.record({
341
+ action: "logout",
342
+ actor: s.userId,
246
343
  ip: ctx.ip,
247
- userAgent: ua,
248
- outcome: "failure",
249
- details: { type: "m.login.password" }
344
+ details: { all: true }
250
345
  });
251
- throw errors.forbidden("Invalid username or password");
346
+ return {};
252
347
  }
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
- }]);
348
+ }));
349
+ r.client("GET", "/account/whoami", procedure({
350
+ auth: "user",
351
+ handler: async (ctx) => {
352
+ const s = ctx.session;
353
+ return {
354
+ user_id: s.userId,
355
+ ...s.deviceId ? { device_id: s.deviceId } : {},
356
+ is_guest: s.isGuest
357
+ };
260
358
  }
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) => {
359
+ }));
360
+ r.client("GET", "/register/available", procedure({ handler: async (ctx) => {
322
361
  const username = ctx.query.get("username");
323
362
  if (!username) throw errors.missingParam("Missing username");
324
363
  await validateNewUsername(ctx.hs, username);
325
364
  return { available: true };
326
- });
327
- r.client("GET", "/register", async (ctx) => ({
365
+ } }));
366
+ r.client("GET", "/register", procedure({ handler: async (ctx) => ({
328
367
  flows: (await registrationFlows(ctx.hs)).map((stages) => ({ stages })),
329
368
  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
- };
369
+ }) }));
370
+ r.client("GET", "/register/m.login.registration_token/validity", procedure({
371
+ rateLimit: "register",
372
+ handler: async (ctx) => {
373
+ const token = ctx.query.get("token");
374
+ if (!token) throw errors.missingParam("Missing token");
375
+ return { valid: await registrationTokenValid(ctx.hs, token) };
348
376
  }
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();
377
+ }), ["v1"]);
378
+ r.client("POST", "/register", procedure({
379
+ input: registerBody,
380
+ handler: async (ctx) => {
381
+ const hs = ctx.hs;
382
+ const body = ctx.input;
383
+ const kind = ctx.query.get("kind") ?? "user";
384
+ if (kind === "guest") {
385
+ if (!hs.userAuth.guestAccessEnabled) throw errors.forbidden("Guest access is disabled");
386
+ const userId = await createUser(hs, await generateLocalpart(hs), { guest: true });
387
+ return {
388
+ ...await hs.auth.createSession(userId, { displayName: body.initial_device_display_name }),
389
+ home_server: hs.serverName
390
+ };
391
+ }
392
+ if (kind !== "user") throw errors.invalidParam("Unknown kind");
393
+ if (body.type === M.LOGIN.APPLICATION_SERVICE || body.auth?.type === M.LOGIN.APPLICATION_SERVICE) {
394
+ const s = await ctx.auth();
395
+ if (!s.appservice) throw errors.forbidden("An application service token is required");
396
+ if (body.username === void 0) throw errors.missingParam("Missing username");
397
+ const localpart = body.username.toLowerCase();
398
+ const userId = `@${localpart}:${hs.serverName}`;
399
+ if (!hs.appservices.isInterestedUser(s.appservice, userId)) throw errors.exclusive("User is not in the application service's namespace");
400
+ if (!LOCALPART_RE.test(localpart)) throw errors.invalidUsername();
401
+ if (await hs.db.findOne("users", [{
402
+ field: "user_id",
403
+ value: userId
404
+ }])) throw errors.userInUse();
405
+ await createUser(hs, localpart, {});
406
+ if (body.inhibit_login === true) return {
407
+ user_id: userId,
408
+ home_server: hs.serverName
409
+ };
410
+ return {
411
+ ...await hs.auth.createSession(userId, {
412
+ deviceId: body.device_id,
413
+ displayName: body.initial_device_display_name
414
+ }),
415
+ home_server: hs.serverName
416
+ };
417
+ }
418
+ if (ctx.accessToken() && hs.appservices.byToken(ctx.accessToken())) throw errors.missingParam("Application services must use type m.login.application_service");
419
+ rejectIfDelegated(hs);
420
+ if (!await hs.registrationAllowed()) throw errors.forbidden("Registration is disabled");
421
+ await ctx.rateLimit("register");
422
+ const localpart = body.username !== void 0 ? await validateNewUsername(hs, body.username.toLowerCase()) : await generateLocalpart(hs);
423
+ if (hs.appservices.exclusiveUserOwner(`@${localpart}:${hs.serverName}`)) throw errors.exclusive("This user ID is reserved by an application service");
424
+ if (body.password !== void 0) {
425
+ const problem = await hs.userAuth.validatePassword(body.password);
426
+ if (problem) throw new MatrixError(400, "M_WEAK_PASSWORD", problem);
427
+ }
428
+ const flows = await registrationFlows(hs);
429
+ const { threepids } = await requireUia(hs, body, flows, null);
430
+ await consumeRegistrationToken(hs, body);
358
431
  if (await hs.db.findOne("users", [{
359
- field: "user_id",
360
- value: userId
432
+ field: "localpart",
433
+ value: localpart
361
434
  }])) throw errors.userInUse();
362
- await createUser(hs, localpart, {});
435
+ const userId = await createUser(hs, localpart, { password: body.password });
436
+ for (const t of threepids) await hs.threepids.add(userId, t).catch((err) => hs.logger.warn("Could not attach", t.medium, "to", userId, err));
437
+ await hs.audit.record({
438
+ action: "register",
439
+ actor: userId,
440
+ ip: ctx.ip,
441
+ userAgent: ctx.request.headers.get("user-agent"),
442
+ details: { token: body.auth?.type === M.LOGIN.REGISTRATION_TOKEN }
443
+ });
363
444
  if (body.inhibit_login === true) return {
364
445
  user_id: userId,
365
446
  home_server: hs.serverName
@@ -367,166 +448,138 @@ function registerAccountRoutes(r) {
367
448
  return {
368
449
  ...await hs.auth.createSession(userId, {
369
450
  deviceId: body.device_id,
370
- displayName: body.initial_device_display_name
451
+ displayName: body.initial_device_display_name,
452
+ refreshToken: body.refresh_token === true
371
453
  }),
372
454
  home_server: hs.serverName
373
455
  };
374
456
  }
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);
457
+ }));
458
+ r.client("POST", "/account/password", procedure({
459
+ input: passwordBody,
460
+ handler: async (ctx) => {
461
+ const hs = ctx.hs;
462
+ rejectIfDelegated(hs);
463
+ const body = ctx.input;
464
+ let s;
465
+ if (ctx.accessToken()) {
466
+ const session = await ctx.auth();
467
+ await requireUia(hs, body, [[M.LOGIN.PASSWORD]], session.userId);
468
+ s = {
469
+ userId: session.userId,
470
+ deviceId: session.deviceId
471
+ };
472
+ } else {
473
+ const { threepids } = await requireUia(hs, body, [[M.LOGIN.EMAIL.IDENTITY], [M.LOGIN.MSISDN]], null);
474
+ const owner = threepids.length ? await hs.threepids.userFor(threepids[0].medium, threepids[0].address) : null;
475
+ if (!owner) throw new MatrixError(400, "M_THREEPID_NOT_FOUND", "No account uses this address");
476
+ s = {
477
+ userId: owner,
478
+ deviceId: null
479
+ };
480
+ }
481
+ if (!body.new_password) throw errors.missingParam("Missing new_password");
482
+ const problem = await hs.userAuth.validatePassword(body.new_password);
383
483
  if (problem) throw new MatrixError(400, "M_WEAK_PASSWORD", problem);
484
+ await hs.db.update("users", [{
485
+ field: "user_id",
486
+ value: s.userId
487
+ }], { password_hash: await hs.userAuth.hashPassword(body.new_password) });
488
+ await hs.audit.record({
489
+ action: "password_change",
490
+ actor: s.userId,
491
+ ip: ctx.ip,
492
+ details: { logout_devices: body.logout_devices !== false }
493
+ });
494
+ if (body.logout_devices !== false) await hs.auth.deleteAllDevices(s.userId, s.deviceId ?? void 0);
495
+ return {};
384
496
  }
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
- };
497
+ }));
498
+ r.client("POST", "/account/deactivate", procedure({
499
+ input: body({}),
500
+ handler: async (ctx) => {
501
+ const hs = ctx.hs;
502
+ rejectIfDelegated(hs);
503
+ const s = await ctx.auth();
504
+ await requireUia(hs, ctx.input, [[M.LOGIN.PASSWORD]], s.userId);
505
+ for (const membership of [
506
+ "join",
507
+ "invite",
508
+ "knock"
509
+ ]) for (const roomId of await hs.rooms.roomsForUser(s.userId, membership)) try {
510
+ await hs.rooms.setMembership(roomId, s.userId, s.userId, "leave");
511
+ } catch {}
512
+ await hs.auth.deleteAllDevices(s.userId);
513
+ await hs.db.update("users", [{
514
+ field: "user_id",
515
+ value: s.userId
516
+ }], {
517
+ deactivated: true,
518
+ password_hash: null,
519
+ displayname: null,
520
+ avatar_url: null
521
+ });
522
+ await hs.db.delete("profile_fields", [{
523
+ field: "user_id",
524
+ value: s.userId
525
+ }]);
526
+ const unbind = await hs.threepids.removeAll(s.userId);
527
+ await hs.audit.record({
528
+ action: "deactivate",
529
+ actor: s.userId,
530
+ target: s.userId,
531
+ ip: ctx.ip
532
+ });
533
+ return { id_server_unbind_result: unbind };
434
534
  }
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) => {
535
+ }));
536
+ r.get("/_synapse/admin/v1/register", procedure({ handler: async (ctx) => {
487
537
  if (!ctx.hs.config.registrationSharedSecret) throw errors.unrecognized();
488
538
  const nonce = randomString(32);
489
539
  await ctx.hs.cluster.set(`regnonce:${nonce}`, true, 6e4);
490
540
  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
- });
541
+ } }));
542
+ r.post("/_synapse/admin/v1/register", procedure({
543
+ input: sharedSecretRegisterBody,
544
+ handler: async (ctx) => {
545
+ const hs = ctx.hs;
546
+ const secret = hs.config.registrationSharedSecret;
547
+ if (!secret) throw errors.unrecognized();
548
+ const body = ctx.input;
549
+ if (body.nonce === void 0 || !await hs.cluster.get(`regnonce:${body.nonce}`)) throw errors.unknown("Unrecognised nonce");
550
+ await hs.cluster.delete(`regnonce:${body.nonce}`);
551
+ const admin = body.admin === true;
552
+ const expected = createHmac("sha1", secret).update(`${body.nonce}\u0000${body.username}\u0000${body.password}\u0000${admin ? "admin" : "notadmin"}`).digest("hex");
553
+ const given = Buffer.from(body.mac ?? "", "utf8");
554
+ if (given.length !== expected.length || !timingSafeEqual(given, Buffer.from(expected, "utf8"))) throw errors.forbidden("HMAC incorrect");
555
+ const userId = await createUser(hs, await validateNewUsername(hs, body.username.toLowerCase()), {
556
+ password: body.password,
557
+ admin,
558
+ displayname: body.displayname
559
+ });
560
+ const session = await hs.auth.createSession(userId);
561
+ await hs.audit.record({
562
+ action: "register",
563
+ actor: "server",
564
+ target: userId,
565
+ ip: ctx.ip,
566
+ details: {
567
+ shared_secret: true,
568
+ admin
569
+ }
570
+ });
571
+ return {
572
+ ...session,
573
+ home_server: hs.serverName
574
+ };
575
+ }
576
+ }));
524
577
  }
525
578
  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"];
579
+ if (hs.adminRoom?.options.firstUserCanRegister && await hs.adminRoom.noUsersYet()) return [[M.LOGIN.DUMMY]];
580
+ const base = await hs.registrationMode() === "token" ? [M.LOGIN.REGISTRATION_TOKEN] : [M.LOGIN.DUMMY];
528
581
  if (!hs.threepids.canValidate("email")) return [base];
529
- const withEmail = [...base.filter((s) => s !== "m.login.dummy"), "m.login.email.identity"];
582
+ const withEmail = [...base.filter((s) => s !== M.LOGIN.DUMMY), M.LOGIN.EMAIL.IDENTITY];
530
583
  return hs.config.threepid.requireEmailForRegistration ? [withEmail] : [base, withEmail];
531
584
  }
532
585
  async function registrationTokenValid(hs, token) {
@@ -539,7 +592,7 @@ async function registrationTokenValid(hs, token) {
539
592
  return row.uses_allowed === null || row.completed < row.uses_allowed;
540
593
  }
541
594
  async function consumeRegistrationToken(hs, body) {
542
- if (body.auth?.type !== "m.login.registration_token") return;
595
+ if (body.auth?.type !== M.LOGIN.REGISTRATION_TOKEN) return;
543
596
  const token = body.auth?.token;
544
597
  const row = await hs.db.findOne("registration_tokens", [{
545
598
  field: "token",
@@ -552,8 +605,8 @@ async function consumeRegistrationToken(hs, body) {
552
605
  }
553
606
  function wellKnown(hs) {
554
607
  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;
608
+ const wk = { [M.HOMESERVER]: { base_url: hs.config.publicBaseUrl } };
609
+ if (hs.config.rtc.transports?.length) wk[UNSTABLE.ORG.MATRIX.MSC4143.RTC_FOCI] = hs.config.rtc.transports;
557
610
  return wk;
558
611
  }
559
612
  //#endregion