matrix-nodejs-homeserver 0.0.0-stage → 0.2.0

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