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,267 @@
1
+ import { errors } from "../core/errors.js";
2
+ import { boolean, looseObject, number, string } from "../vendor/zod/v4/classic/schemas.js";
3
+ import { body, fail, lenient } from "../http/input.js";
4
+ import { procedure } from "../http/procedure.js";
5
+ //#region src/api/admin.ts
6
+ const required = fail("M_BAD_JSON", "user_id and content are required");
7
+ const noticeBody = body({
8
+ user_id: string(required),
9
+ content: looseObject({}, required),
10
+ type: lenient(string())
11
+ });
12
+ const optionalInt = (name) => number(fail("M_INVALID_PARAM", `${name} must be an integer`)).int(`M_INVALID_PARAM: ${name} must be an integer`);
13
+ const registrationTokenBody = body({
14
+ token: string(fail("M_INVALID_PARAM", "Invalid token")).optional(),
15
+ uses_allowed: optionalInt("uses_allowed").nullable().optional(),
16
+ expiry_time: optionalInt("expiry_time").nullable().optional(),
17
+ length: optionalInt("length").optional()
18
+ });
19
+ async function requireAdmin(ctx) {
20
+ const s = await ctx.auth();
21
+ if (!s.isAdmin) throw errors.forbidden("You are not a server admin");
22
+ if (ctx.method !== "GET") await ctx.hs.audit.record({
23
+ action: "admin_action",
24
+ actor: s.userId,
25
+ ip: ctx.ip,
26
+ details: {
27
+ method: ctx.method,
28
+ path: ctx.url.pathname
29
+ }
30
+ });
31
+ return s;
32
+ }
33
+ /** Admin HTTP API using Synapse-compatible paths, so existing tooling keeps working. */
34
+ function registerAdminRoutes(r) {
35
+ const notice = async (ctx) => {
36
+ await requireAdmin(ctx);
37
+ const body = ctx.input;
38
+ return { event_id: await ctx.hs.admin.sendServerNotice(body.user_id, body.content, {
39
+ type: body.type,
40
+ txnId: ctx.params.txnId
41
+ }) };
42
+ };
43
+ const moderation = (field) => {
44
+ const target = async (ctx) => {
45
+ await requireAdmin(ctx);
46
+ const userId = ctx.params.userId;
47
+ if (!ctx.hs.isLocalUser(userId)) throw errors.invalidParam("Not a local user");
48
+ const user = await ctx.hs.db.findOne("users", [{
49
+ field: "user_id",
50
+ value: userId
51
+ }]);
52
+ if (!user) throw errors.notFound("Unknown user");
53
+ return user;
54
+ };
55
+ return {
56
+ get: async (ctx) => ({ [field]: !!(await target(ctx))[field] }),
57
+ put: async (ctx) => {
58
+ await target(ctx);
59
+ const s = await ctx.auth();
60
+ const body = await ctx.json();
61
+ if (typeof body[field] !== "boolean") throw errors.badJson(`${field} must be a boolean`);
62
+ if (field === "locked") await ctx.hs.admin.setLocked(ctx.params.userId, body.locked, s.userId);
63
+ else await ctx.hs.admin.setSuspended(ctx.params.userId, body.suspended, s.userId);
64
+ return { [field]: body[field] };
65
+ }
66
+ };
67
+ };
68
+ const lock = moderation("locked"), suspend = moderation("suspended");
69
+ r.get("/_matrix/client/v1/admin/lock/:userId", procedure({ handler: lock.get }));
70
+ r.put("/_matrix/client/v1/admin/lock/:userId", procedure({ handler: lock.put }));
71
+ r.get("/_matrix/client/v1/admin/suspend/:userId", procedure({ handler: suspend.get }));
72
+ r.put("/_matrix/client/v1/admin/suspend/:userId", procedure({ handler: suspend.put }));
73
+ r.post("/_synapse/admin/v1/send_server_notice", procedure({
74
+ input: noticeBody,
75
+ handler: notice
76
+ }));
77
+ r.put("/_synapse/admin/v1/send_server_notice/:txnId", procedure({
78
+ input: noticeBody,
79
+ handler: notice
80
+ }));
81
+ r.get("/_synapse/admin/v2/users", procedure({ handler: async (ctx) => {
82
+ await requireAdmin(ctx);
83
+ const from = Number(ctx.query.get("from") ?? 0) || 0;
84
+ const res = await ctx.hs.admin.listUsers({
85
+ from,
86
+ limit: Number(ctx.query.get("limit") ?? 100) || 100,
87
+ search: ctx.query.get("name") ?? void 0,
88
+ includeDeactivated: ctx.query.get("deactivated") === "true"
89
+ });
90
+ return {
91
+ users: res.users,
92
+ total: res.total,
93
+ ...res.next !== null ? { next_token: String(res.next) } : {}
94
+ };
95
+ } }));
96
+ r.get("/_synapse/admin/v2/users/:userId", procedure({ handler: async (ctx) => {
97
+ await requireAdmin(ctx);
98
+ const user = await ctx.hs.admin.getUser(ctx.params.userId);
99
+ if (!user) throw errors.notFound("Unknown user");
100
+ return user;
101
+ } }));
102
+ r.put("/_synapse/admin/v2/users/:userId", procedure({ handler: async (ctx) => {
103
+ await requireAdmin(ctx);
104
+ const body = await ctx.json();
105
+ const userId = ctx.params.userId;
106
+ if (!ctx.hs.isLocalUser(userId)) throw errors.invalidParam("Can only manage local users");
107
+ if (!await ctx.hs.admin.getUser(userId)) {
108
+ await ctx.hs.admin.createUser(userId.slice(1).split(":")[0], {
109
+ password: body.password,
110
+ admin: body.admin === true,
111
+ displayname: body.displayname
112
+ });
113
+ return new Response(JSON.stringify(await ctx.hs.admin.getUser(userId)), {
114
+ status: 201,
115
+ headers: { "content-type": "application/json" }
116
+ });
117
+ }
118
+ if (typeof body.password === "string") await ctx.hs.admin.resetPassword(userId, body.password, body.logout_devices !== false);
119
+ if (typeof body.admin === "boolean") await ctx.hs.admin.setAdmin(userId, body.admin);
120
+ if (typeof body.displayname === "string") {
121
+ await ctx.hs.db.update("users", [{
122
+ field: "user_id",
123
+ value: userId
124
+ }], { displayname: body.displayname });
125
+ await ctx.hs.rooms.propagateProfile(userId);
126
+ }
127
+ if (body.deactivated === true) await ctx.hs.admin.deactivateUser(userId);
128
+ return ctx.hs.admin.getUser(userId);
129
+ } }));
130
+ r.post("/_synapse/admin/v1/deactivate/:userId", procedure({
131
+ input: body({ erase: lenient(boolean()) }),
132
+ handler: async (ctx) => {
133
+ await requireAdmin(ctx);
134
+ const body = ctx.input;
135
+ await ctx.hs.admin.deactivateUser(ctx.params.userId, { erase: body.erase === true });
136
+ return { id_server_unbind_result: "no-support" };
137
+ }
138
+ }));
139
+ const resetPasswordBody = body({
140
+ new_password: string(fail("M_MISSING_PARAM", "Missing new_password")),
141
+ logout_devices: lenient(boolean())
142
+ });
143
+ r.post("/_synapse/admin/v1/reset_password/:userId", procedure({
144
+ input: resetPasswordBody,
145
+ handler: async (ctx) => {
146
+ await requireAdmin(ctx);
147
+ const body = ctx.input;
148
+ await ctx.hs.admin.resetPassword(ctx.params.userId, body.new_password, body.logout_devices !== false);
149
+ return {};
150
+ }
151
+ }));
152
+ r.get("/_synapse/admin/v1/registration_tokens", procedure({ handler: async (ctx) => {
153
+ await requireAdmin(ctx);
154
+ return { registration_tokens: await ctx.hs.admin.listRegistrationTokens() };
155
+ } }));
156
+ r.post("/_synapse/admin/v1/registration_tokens/new", procedure({
157
+ input: registrationTokenBody,
158
+ handler: async (ctx) => {
159
+ await requireAdmin(ctx);
160
+ const body = ctx.input;
161
+ return ctx.hs.admin.createRegistrationToken({
162
+ token: body.token,
163
+ usesAllowed: body.uses_allowed,
164
+ expiryTime: body.expiry_time,
165
+ length: body.length
166
+ });
167
+ }
168
+ }));
169
+ r.delete("/_synapse/admin/v1/registration_tokens/:token", procedure({ handler: async (ctx) => {
170
+ await requireAdmin(ctx);
171
+ await ctx.hs.admin.deleteRegistrationToken(ctx.params.token);
172
+ return {};
173
+ } }));
174
+ r.get("/_synapse/admin/v1/audit_log", procedure({ handler: async (ctx) => {
175
+ await requireAdmin(ctx);
176
+ if (!ctx.hs.audit.enabled) throw errors.unrecognized("The audit log is disabled");
177
+ const rows = await ctx.hs.audit.query({
178
+ actor: ctx.query.get("actor") ?? void 0,
179
+ target: ctx.query.get("target") ?? void 0,
180
+ action: ctx.query.get("action") ?? void 0,
181
+ since: Number(ctx.query.get("since")) || void 0,
182
+ before: Number(ctx.query.get("from")) || void 0,
183
+ limit: Number(ctx.query.get("limit")) || 100
184
+ });
185
+ const out = { entries: rows };
186
+ if (rows.length) out.next_token = String(rows[rows.length - 1].id);
187
+ return out;
188
+ } }));
189
+ r.get("/_synapse/admin/v1/event_reports", procedure({ handler: async (ctx) => {
190
+ await requireAdmin(ctx);
191
+ const rows = await ctx.hs.db.findMany("event_reports", {
192
+ sortBy: {
193
+ field: "id",
194
+ direction: "desc"
195
+ },
196
+ limit: Number(ctx.query.get("limit")) || 100
197
+ });
198
+ return {
199
+ event_reports: rows.map((r) => ({
200
+ id: r.id,
201
+ received_ts: r.ts,
202
+ room_id: r.room_id,
203
+ event_id: r.event_id,
204
+ user_id: r.reporter,
205
+ reason: r.reason,
206
+ score: r.score,
207
+ resolved: r.resolved
208
+ })),
209
+ total: rows.length
210
+ };
211
+ } }));
212
+ r.post("/_synapse/admin/v1/event_reports/:id/resolve", procedure({ handler: async (ctx) => {
213
+ await requireAdmin(ctx);
214
+ if (!await ctx.hs.db.update("event_reports", [{
215
+ field: "id",
216
+ value: Number(ctx.params.id)
217
+ }], { resolved: true })) throw errors.notFound("Unknown report");
218
+ return {};
219
+ } }));
220
+ r.get("/_synapse/admin/v1/rooms", procedure({ handler: async (ctx) => {
221
+ await requireAdmin(ctx);
222
+ const res = await ctx.hs.admin.listRooms({
223
+ from: Number(ctx.query.get("from") ?? 0) || 0,
224
+ limit: Number(ctx.query.get("limit") ?? 100) || 100
225
+ });
226
+ return {
227
+ rooms: res.rooms,
228
+ total_rooms: res.total
229
+ };
230
+ } }));
231
+ r.delete("/_synapse/admin/v1/rooms/:roomId", procedure({ handler: async (ctx) => {
232
+ const s = await requireAdmin(ctx);
233
+ const body = await ctx.json();
234
+ const { kicked } = await ctx.hs.admin.deleteRoom(ctx.params.roomId, {
235
+ actor: s.userId,
236
+ reason: body.message,
237
+ purge: body.purge !== false
238
+ });
239
+ return {
240
+ kicked_users: kicked,
241
+ failed_to_kick_users: [],
242
+ local_aliases: [],
243
+ new_room_id: null
244
+ };
245
+ } }));
246
+ r.delete("/_synapse/admin/v1/media/:serverName/:mediaId", procedure({ handler: async (ctx) => {
247
+ await requireAdmin(ctx);
248
+ if (ctx.params.serverName !== ctx.hs.serverName) throw errors.notFound("Remote media");
249
+ await ctx.hs.admin.deleteMedia(ctx.params.mediaId);
250
+ return {
251
+ deleted_media: [ctx.params.mediaId],
252
+ total: 1
253
+ };
254
+ } }));
255
+ r.post("/_synapse/admin/v1/media/delete", procedure({ handler: async (ctx) => {
256
+ await requireAdmin(ctx);
257
+ const before = Number(ctx.query.get("before_ts"));
258
+ if (!Number.isFinite(before)) throw errors.missingParam("Missing before_ts");
259
+ const deleted = await ctx.hs.admin.purgeMediaBefore(before);
260
+ return {
261
+ deleted_media: deleted,
262
+ total: deleted.length
263
+ };
264
+ } }));
265
+ }
266
+ //#endregion
267
+ export { registerAdminRoutes };
@@ -0,0 +1,45 @@
1
+ import { errors } from "../core/errors.js";
2
+ import { procedure } from "../http/procedure.js";
3
+ //#region src/api/appservice.ts
4
+ function registerAppServiceRoutes(r) {
5
+ r.client("POST", "/appservice/:appserviceId/ping", procedure({
6
+ auth: "user",
7
+ handler: async (ctx) => {
8
+ const s = ctx.session;
9
+ if (!s.appservice) throw errors.forbidden("An application service token is required");
10
+ if (s.appservice.id !== ctx.params.appserviceId) throw errors.forbidden("Token does not belong to this application service");
11
+ const body = await ctx.json();
12
+ return { duration_ms: await ctx.hs.appservices.ping(s.appservice, typeof body.transaction_id === "string" ? body.transaction_id : void 0) };
13
+ }
14
+ }), ["v1"]);
15
+ r.client("GET", "/thirdparty/protocols", procedure({
16
+ auth: "user",
17
+ handler: async (ctx) => {
18
+ return ctx.hs.appservices.protocols();
19
+ }
20
+ }));
21
+ r.client("GET", "/thirdparty/protocol/:protocol", procedure({
22
+ auth: "user",
23
+ handler: async (ctx) => {
24
+ const p = (await ctx.hs.appservices.protocols())[ctx.params.protocol];
25
+ if (!p) throw errors.notFound("Unknown protocol");
26
+ return p;
27
+ }
28
+ }));
29
+ for (const kind of ["location", "user"]) {
30
+ r.client("GET", `/thirdparty/${kind}/:protocol`, procedure({
31
+ auth: "user",
32
+ handler: async (ctx) => {
33
+ return ctx.hs.appservices.thirdParty(`${kind}/${encodeURIComponent(ctx.params.protocol)}`, ctx.query);
34
+ }
35
+ }));
36
+ r.client("GET", `/thirdparty/${kind}`, procedure({
37
+ auth: "user",
38
+ handler: async (ctx) => {
39
+ return ctx.hs.appservices.thirdParty(kind, ctx.query);
40
+ }
41
+ }));
42
+ }
43
+ }
44
+ //#endregion
45
+ export { registerAppServiceRoutes };
@@ -0,0 +1,280 @@
1
+ import { isObject } from "../core/json.js";
2
+ import { MatrixError, errors } from "../core/errors.js";
3
+ import { looseObject, string } from "../vendor/zod/v4/classic/schemas.js";
4
+ import { body, fail } from "../http/input.js";
5
+ import { procedure } from "../http/procedure.js";
6
+ //#region src/api/backup.ts
7
+ const required = fail("M_BAD_JSON", "algorithm and auth_data are required");
8
+ const createVersionBody = body({
9
+ algorithm: string(required),
10
+ auth_data: looseObject({}, required)
11
+ });
12
+ /** Server-side room key backups (`/room_keys`). */
13
+ async function latestVersion(hs, userId) {
14
+ return (await hs.db.findMany("backup_versions", {
15
+ where: [{
16
+ field: "user_id",
17
+ value: userId
18
+ }, {
19
+ field: "deleted",
20
+ value: false
21
+ }],
22
+ sortBy: {
23
+ field: "version",
24
+ direction: "desc"
25
+ },
26
+ limit: 1
27
+ }))[0] ?? null;
28
+ }
29
+ async function getVersion(hs, userId, version) {
30
+ if (version === null) return latestVersion(hs, userId);
31
+ const n = Number(version);
32
+ if (!Number.isInteger(n)) return null;
33
+ const row = await hs.db.findOne("backup_versions", [{
34
+ field: "user_id",
35
+ value: userId
36
+ }, {
37
+ field: "version",
38
+ value: n
39
+ }]);
40
+ return row && !row.deleted ? row : null;
41
+ }
42
+ async function countKeys(hs, userId, version) {
43
+ return (await hs.db.findMany("backup_keys", { where: [{
44
+ field: "user_id",
45
+ value: userId
46
+ }, {
47
+ field: "version",
48
+ value: version
49
+ }] })).length;
50
+ }
51
+ async function describe(hs, row) {
52
+ return {
53
+ algorithm: row.algorithm,
54
+ auth_data: row.auth_data,
55
+ version: String(row.version),
56
+ etag: String(row.etag),
57
+ count: await countKeys(hs, row.user_id, row.version)
58
+ };
59
+ }
60
+ /** Whether `incoming` should replace `existing` according to the spec's key-selection rules. */
61
+ function better(incoming, existing) {
62
+ if (!existing) return true;
63
+ if (!!incoming.is_verified !== !!existing.is_verified) return !!incoming.is_verified;
64
+ if (incoming.first_message_index !== existing.first_message_index) return incoming.first_message_index < existing.first_message_index;
65
+ return incoming.forwarded_count < existing.forwarded_count;
66
+ }
67
+ async function requireCurrentVersion(ctx, userId) {
68
+ const version = ctx.query.get("version");
69
+ if (!version) throw errors.missingParam("Missing version");
70
+ const row = await getVersion(ctx.hs, userId, version);
71
+ if (!row) throw errors.notFound("Unknown backup version");
72
+ return {
73
+ row,
74
+ isLatest: (await latestVersion(ctx.hs, userId))?.version === row.version
75
+ };
76
+ }
77
+ function registerBackupRoutes(r) {
78
+ r.client("POST", "/room_keys/version", procedure({
79
+ auth: "user",
80
+ input: createVersionBody,
81
+ handler: async (ctx) => {
82
+ const s = ctx.session;
83
+ const body = ctx.input;
84
+ const max = (await ctx.hs.db.findMany("backup_versions", { where: [{
85
+ field: "user_id",
86
+ value: s.userId
87
+ }] })).reduce((m, v) => Math.max(m, v.version), 0);
88
+ await ctx.hs.db.create("backup_versions", {
89
+ user_id: s.userId,
90
+ version: max + 1,
91
+ algorithm: body.algorithm,
92
+ auth_data: body.auth_data,
93
+ etag: 0,
94
+ deleted: false
95
+ });
96
+ return { version: String(max + 1) };
97
+ }
98
+ }));
99
+ const getVersionHandler = async (ctx) => {
100
+ const s = await ctx.auth();
101
+ const row = await getVersion(ctx.hs, s.userId, ctx.params.version ?? null);
102
+ if (!row) throw errors.notFound("No backup found");
103
+ return describe(ctx.hs, row);
104
+ };
105
+ r.client("GET", "/room_keys/version", procedure({ handler: getVersionHandler }));
106
+ r.client("GET", "/room_keys/version/:version", procedure({ handler: getVersionHandler }));
107
+ r.client("PUT", "/room_keys/version/:version", procedure({
108
+ auth: "user",
109
+ handler: async (ctx) => {
110
+ const s = ctx.session;
111
+ const body = await ctx.json();
112
+ const row = await getVersion(ctx.hs, s.userId, ctx.params.version);
113
+ if (!row) throw errors.notFound("Unknown backup version");
114
+ if (body.version !== void 0 && String(body.version) !== ctx.params.version) throw errors.invalidParam("Version mismatch");
115
+ if (body.algorithm !== row.algorithm) throw errors.invalidParam("Algorithm cannot be changed");
116
+ if (!isObject(body.auth_data)) throw errors.badJson("auth_data is required");
117
+ await ctx.hs.db.update("backup_versions", [{
118
+ field: "user_id",
119
+ value: s.userId
120
+ }, {
121
+ field: "version",
122
+ value: row.version
123
+ }], { auth_data: body.auth_data });
124
+ return {};
125
+ }
126
+ }));
127
+ r.client("DELETE", "/room_keys/version/:version", procedure({
128
+ auth: "user",
129
+ handler: async (ctx) => {
130
+ const s = ctx.session;
131
+ const row = await getVersion(ctx.hs, s.userId, ctx.params.version);
132
+ if (!row) throw errors.notFound("Unknown backup version");
133
+ await ctx.hs.db.update("backup_versions", [{
134
+ field: "user_id",
135
+ value: s.userId
136
+ }, {
137
+ field: "version",
138
+ value: row.version
139
+ }], { deleted: true });
140
+ await ctx.hs.db.delete("backup_keys", [{
141
+ field: "user_id",
142
+ value: s.userId
143
+ }, {
144
+ field: "version",
145
+ value: row.version
146
+ }]);
147
+ return {};
148
+ }
149
+ }));
150
+ const putKeys = async (ctx) => {
151
+ const s = await ctx.auth();
152
+ const hs = ctx.hs;
153
+ const { row, isLatest } = await requireCurrentVersion(ctx, s.userId);
154
+ if (!isLatest) {
155
+ const latest = await latestVersion(hs, s.userId);
156
+ throw new MatrixError(403, "M_WRONG_ROOM_KEYS_VERSION", "Wrong backup version", { current_version: String(latest?.version ?? "") });
157
+ }
158
+ const body = await ctx.json();
159
+ let rooms;
160
+ if (ctx.params.sessionId) rooms = { [ctx.params.roomId]: { sessions: { [ctx.params.sessionId]: body } } };
161
+ else if (ctx.params.roomId) rooms = { [ctx.params.roomId]: body };
162
+ else rooms = body.rooms ?? {};
163
+ let changed = false;
164
+ for (const [roomId, room] of Object.entries(rooms)) {
165
+ if (!isObject(room) || !isObject(room.sessions)) continue;
166
+ for (const [sessionId, data] of Object.entries(room.sessions)) {
167
+ const where = [
168
+ {
169
+ field: "user_id",
170
+ value: s.userId
171
+ },
172
+ {
173
+ field: "version",
174
+ value: row.version
175
+ },
176
+ {
177
+ field: "room_id",
178
+ value: roomId
179
+ },
180
+ {
181
+ field: "session_id",
182
+ value: sessionId
183
+ }
184
+ ];
185
+ if (!better(data, (await hs.db.findOne("backup_keys", where))?.data ?? null)) continue;
186
+ await hs.db.upsert("backup_keys", {
187
+ user_id: s.userId,
188
+ version: row.version,
189
+ room_id: roomId,
190
+ session_id: sessionId,
191
+ data
192
+ });
193
+ changed = true;
194
+ }
195
+ }
196
+ const etag = changed ? row.etag + 1 : row.etag;
197
+ if (changed) await hs.db.update("backup_versions", [{
198
+ field: "user_id",
199
+ value: s.userId
200
+ }, {
201
+ field: "version",
202
+ value: row.version
203
+ }], { etag });
204
+ return {
205
+ etag: String(etag),
206
+ count: await countKeys(hs, s.userId, row.version)
207
+ };
208
+ };
209
+ const getKeys = async (ctx) => {
210
+ const s = await ctx.auth();
211
+ const { row } = await requireCurrentVersion(ctx, s.userId);
212
+ const where = [{
213
+ field: "user_id",
214
+ value: s.userId
215
+ }, {
216
+ field: "version",
217
+ value: row.version
218
+ }];
219
+ if (ctx.params.roomId) where.push({
220
+ field: "room_id",
221
+ value: ctx.params.roomId
222
+ });
223
+ if (ctx.params.sessionId) where.push({
224
+ field: "session_id",
225
+ value: ctx.params.sessionId
226
+ });
227
+ const keys = await ctx.hs.db.findMany("backup_keys", { where });
228
+ if (ctx.params.sessionId) {
229
+ if (!keys[0]) throw errors.notFound("Key not found");
230
+ return keys[0].data;
231
+ }
232
+ const rooms = {};
233
+ for (const k of keys) (rooms[k.room_id] ??= { sessions: {} }).sessions[k.session_id] = k.data;
234
+ if (ctx.params.roomId) return rooms[ctx.params.roomId] ?? { sessions: {} };
235
+ return { rooms };
236
+ };
237
+ const deleteKeys = async (ctx) => {
238
+ const s = await ctx.auth();
239
+ const { row } = await requireCurrentVersion(ctx, s.userId);
240
+ const where = [{
241
+ field: "user_id",
242
+ value: s.userId
243
+ }, {
244
+ field: "version",
245
+ value: row.version
246
+ }];
247
+ if (ctx.params.roomId) where.push({
248
+ field: "room_id",
249
+ value: ctx.params.roomId
250
+ });
251
+ if (ctx.params.sessionId) where.push({
252
+ field: "session_id",
253
+ value: ctx.params.sessionId
254
+ });
255
+ await ctx.hs.db.delete("backup_keys", where);
256
+ const etag = row.etag + 1;
257
+ await ctx.hs.db.update("backup_versions", [{
258
+ field: "user_id",
259
+ value: s.userId
260
+ }, {
261
+ field: "version",
262
+ value: row.version
263
+ }], { etag });
264
+ return {
265
+ etag: String(etag),
266
+ count: await countKeys(ctx.hs, s.userId, row.version)
267
+ };
268
+ };
269
+ for (const path of [
270
+ "/room_keys/keys",
271
+ "/room_keys/keys/:roomId",
272
+ "/room_keys/keys/:roomId/:sessionId"
273
+ ]) {
274
+ r.client("PUT", path, procedure({ handler: putKeys }));
275
+ r.client("GET", path, procedure({ handler: getKeys }));
276
+ r.client("DELETE", path, procedure({ handler: deleteKeys }));
277
+ }
278
+ }
279
+ //#endregion
280
+ export { registerBackupRoutes };