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,369 @@
1
+ import { M, UNSTABLE } from "./m.js";
2
+ import { splitStateKey, toClientEvent } from "./events.js";
3
+ import { MatrixError, errors } from "./errors.js";
4
+ import { readFile, readdir } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ //#region src/core/appservices.ts
7
+ const compile = (ns = []) => ns.map((n) => ({
8
+ exclusive: !!n.exclusive,
9
+ re: new RegExp(`^(?:${n.regex})$`)
10
+ }));
11
+ /**
12
+ * Parse an application service registration file. Accepts JSON or the YAML subset
13
+ * used by registration files (nested mappings and lists of mappings, scalars).
14
+ */
15
+ function parseAppServiceRegistration(text) {
16
+ const trimmed = text.trim();
17
+ if (trimmed.startsWith("{")) return JSON.parse(trimmed);
18
+ return parseSimpleYaml(text);
19
+ }
20
+ function parseScalar(raw) {
21
+ const v = raw.trim();
22
+ if (v === "" || v === "~" || v === "null") return null;
23
+ if (v === "true") return true;
24
+ if (v === "false") return false;
25
+ if (v === "[]") return [];
26
+ if (v === "{}") return {};
27
+ if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
28
+ if (v.startsWith("\"") && v.endsWith("\"")) return JSON.parse(v);
29
+ if (v.startsWith("'") && v.endsWith("'")) return v.slice(1, -1).replace(/''/g, "'");
30
+ if (v.startsWith("[") && v.endsWith("]")) return v.slice(1, -1).split(",").map((x) => parseScalar(x)).filter((x) => x !== null);
31
+ return v;
32
+ }
33
+ /**
34
+ * Remove a `# comment` (at the start of the line, or after whitespace).
35
+ * Security (https://github.com/k18k/matrix-nodejs-homeserver/issues/4): this was the regex `/\s+#.*$/`, polynomial on long whitespace runs (ReDoS,
36
+ * CWE-1333); registration files are operator-provided, but a malformed one could hang startup.
37
+ */
38
+ function stripComment(line) {
39
+ for (let i = 0; i < line.length; i++) if (line[i] === "#" && (i === 0 || /\s/.test(line[i - 1]))) return line.slice(0, i).trimEnd();
40
+ return line;
41
+ }
42
+ /** Minimal YAML reader (indentation-based maps, "- " lists, scalars, comments). */
43
+ function parseSimpleYaml(text) {
44
+ const lines = text.split(/\r?\n/).map(stripComment).filter((l) => l.trim() !== "").map((l) => ({
45
+ indent: l.length - l.trimStart().length,
46
+ text: l.trim()
47
+ }));
48
+ let i = 0;
49
+ const parseBlock = (indent) => {
50
+ if (i < lines.length && lines[i].text.startsWith("- ")) {
51
+ const list = [];
52
+ while (i < lines.length && lines[i].indent === indent && lines[i].text.startsWith("- ")) {
53
+ const rest = lines[i].text.slice(2);
54
+ if (/^[\w.-]+:/.test(rest)) {
55
+ lines[i] = {
56
+ indent: indent + 2,
57
+ text: rest
58
+ };
59
+ list.push(parseBlock(indent + 2));
60
+ } else {
61
+ list.push(parseScalar(rest));
62
+ i++;
63
+ }
64
+ }
65
+ return list;
66
+ }
67
+ const obj = {};
68
+ while (i < lines.length && lines[i].indent === indent && !lines[i].text.startsWith("- ")) {
69
+ const m = /^([^:]+):\s*(.*)$/.exec(lines[i].text);
70
+ if (!m) {
71
+ i++;
72
+ continue;
73
+ }
74
+ const key = m[1].trim().replace(/^["']|["']$/g, "");
75
+ i++;
76
+ if (m[2] !== "") obj[key] = parseScalar(m[2]);
77
+ else if (i < lines.length && (lines[i].indent > indent || lines[i].indent === indent && lines[i].text.startsWith("- "))) obj[key] = parseBlock(lines[i].indent);
78
+ else obj[key] = null;
79
+ }
80
+ return obj;
81
+ };
82
+ return parseBlock(lines[0]?.indent ?? 0);
83
+ }
84
+ /** Load every *.yaml / *.yml / *.json registration in a directory. */
85
+ async function loadAppServiceRegistrations(directory) {
86
+ let names;
87
+ try {
88
+ names = await readdir(directory);
89
+ } catch {
90
+ return [];
91
+ }
92
+ const out = [];
93
+ for (const name of names.filter((n) => /\.(ya?ml|json)$/.test(n)).sort()) out.push(parseAppServiceRegistration(await readFile(join(directory, name), "utf8")));
94
+ return out;
95
+ }
96
+ /** Application Service API: authentication, namespaces, event push and queries. */
97
+ var AppServiceService = class {
98
+ hs;
99
+ services;
100
+ timers = /* @__PURE__ */ new Map();
101
+ constructor(hs, registrations) {
102
+ this.hs = hs;
103
+ this.services = registrations.map((reg) => ({
104
+ reg,
105
+ users: compile(reg.namespaces?.users),
106
+ aliases: compile(reg.namespaces?.aliases),
107
+ rooms: compile(reg.namespaces?.rooms)
108
+ }));
109
+ }
110
+ get registrations() {
111
+ return this.services.map((s) => s.reg);
112
+ }
113
+ byToken(token) {
114
+ return this.services.find((s) => s.reg.as_token === token)?.reg ?? null;
115
+ }
116
+ byId(id) {
117
+ return this.services.find((s) => s.reg.id === id)?.reg ?? null;
118
+ }
119
+ senderOf(reg) {
120
+ return `@${reg.sender_localpart}:${this.hs.serverName}`;
121
+ }
122
+ compiled(reg) {
123
+ return this.services.find((s) => s.reg === reg);
124
+ }
125
+ /** Whether the AS owns this user ID (its sender or its user namespace). */
126
+ isInterestedUser(reg, userId) {
127
+ return userId === this.senderOf(reg) || this.hs.isLocalUser(userId) && this.compiled(reg).users.some((n) => n.re.test(userId));
128
+ }
129
+ /** An exclusive namespace owned by some AS (other than `except`) covers this user ID. */
130
+ exclusiveUserOwner(userId) {
131
+ return this.services.find((s) => s.users.some((n) => n.exclusive && n.re.test(userId)))?.reg ?? null;
132
+ }
133
+ exclusiveAliasOwner(alias) {
134
+ return this.services.find((s) => s.aliases.some((n) => n.exclusive && n.re.test(alias)))?.reg ?? null;
135
+ }
136
+ /** Resolve the user an AS request acts as (masquerading with `user_id`). */
137
+ async masquerade(reg, userIdParam) {
138
+ const userId = userIdParam ?? this.senderOf(reg);
139
+ if (!this.isInterestedUser(reg, userId)) throw errors.forbidden("Application service cannot masquerade as this user");
140
+ if (userId === this.senderOf(reg) && !await this.hs.db.findOne("users", [{
141
+ field: "user_id",
142
+ value: userId
143
+ }])) await this.hs.db.create("users", {
144
+ user_id: userId,
145
+ localpart: reg.sender_localpart,
146
+ password_hash: null,
147
+ displayname: reg.sender_localpart,
148
+ avatar_url: null,
149
+ is_guest: false,
150
+ is_admin: false,
151
+ deactivated: false,
152
+ created_ts: Date.now()
153
+ });
154
+ if (!await this.hs.db.findOne("users", [{
155
+ field: "user_id",
156
+ value: userId
157
+ }])) throw errors.forbidden("Application service has not registered this user");
158
+ return userId;
159
+ }
160
+ async interestedInEvent(svc, pdu, state) {
161
+ const reg = svc.reg;
162
+ if (this.isInterestedUser(reg, pdu.sender)) return true;
163
+ if (pdu.type === M.ROOM.MEMBER && typeof pdu.state_key === "string" && this.isInterestedUser(reg, pdu.state_key)) return true;
164
+ if (svc.rooms.some((n) => n.re.test(pdu.room_id))) return true;
165
+ if (svc.aliases.length) {
166
+ if ((await this.hs.db.findMany("room_aliases", { where: [{
167
+ field: "room_id",
168
+ value: pdu.room_id
169
+ }] })).some((a) => svc.aliases.some((n) => n.re.test(a.alias)))) return true;
170
+ }
171
+ for (const [key, ev] of state) {
172
+ const [type, userId] = splitStateKey(key);
173
+ if (type === M.ROOM.MEMBER && ev.content?.membership === "join" && this.isInterestedUser(reg, userId)) return true;
174
+ }
175
+ return false;
176
+ }
177
+ /** Queue an event for every interested application service. */
178
+ async onEvent(pdu, state) {
179
+ for (const svc of this.services) {
180
+ if (!svc.reg.url || !await this.interestedInEvent(svc, pdu, state)) continue;
181
+ await this.enqueue(svc.reg, "event", toClientEvent(pdu));
182
+ }
183
+ }
184
+ /** Queue an ephemeral event (typing/receipt/presence) for interested services that asked for them. */
185
+ async onEphemeral(roomId, event, users) {
186
+ for (const svc of this.services) {
187
+ const reg = svc.reg;
188
+ if (!reg.url || !(reg.receive_ephemeral || reg[UNSTABLE.DE.SORUNOME.MSC2409.PUSH_EPHEMERAL])) continue;
189
+ let interested = users.some((u) => this.isInterestedUser(reg, u)) || roomId !== null && svc.rooms.some((n) => n.re.test(roomId));
190
+ if (!interested && roomId) {
191
+ for (const u of await this.hs.rooms.joinedMembers(roomId)) if (this.isInterestedUser(reg, u)) {
192
+ interested = true;
193
+ break;
194
+ }
195
+ }
196
+ if (interested) await this.enqueue(reg, "ephemeral", roomId ? {
197
+ ...event,
198
+ room_id: roomId
199
+ } : event);
200
+ }
201
+ }
202
+ async enqueue(reg, kind, payload) {
203
+ const id = await this.hs.cluster.nextId("appservice_outbox");
204
+ await this.hs.db.create("appservice_outbox", {
205
+ id,
206
+ as_id: reg.id,
207
+ kind,
208
+ payload,
209
+ created_ts: Date.now()
210
+ });
211
+ this.schedule(reg.id, 0);
212
+ }
213
+ schedule(asId, delay) {
214
+ if (this.hs.isClosed || this.timers.has(asId) && delay > 0) return;
215
+ const existing = this.timers.get(asId);
216
+ if (existing) clearTimeout(existing);
217
+ const timer = setTimeout(() => {
218
+ this.timers.delete(asId);
219
+ this.flush(asId).catch((err) => this.hs.logger.debug("Appservice flush failed", err));
220
+ }, delay);
221
+ timer.unref?.();
222
+ this.timers.set(asId, timer);
223
+ }
224
+ retries = /* @__PURE__ */ new Map();
225
+ async flush(asId) {
226
+ const reg = this.byId(asId);
227
+ if (!reg?.url) return;
228
+ await this.hs.cluster.lock(`appservice:${asId}`, async () => {
229
+ for (;;) {
230
+ const rows = await this.hs.db.findMany("appservice_outbox", {
231
+ where: [{
232
+ field: "as_id",
233
+ value: asId
234
+ }],
235
+ sortBy: {
236
+ field: "id",
237
+ direction: "asc"
238
+ },
239
+ limit: 100
240
+ });
241
+ if (!rows.length) return;
242
+ const body = { events: rows.filter((r) => r.kind === "event").map((r) => r.payload) };
243
+ const ephemeral = rows.filter((r) => r.kind === "ephemeral").map((r) => r.payload);
244
+ if (ephemeral.length) {
245
+ body.ephemeral = ephemeral;
246
+ body[UNSTABLE.DE.SORUNOME.MSC2409.EPHEMERAL] = ephemeral;
247
+ }
248
+ const txnId = String(rows[rows.length - 1].id);
249
+ try {
250
+ const res = await this.hs.fetch(`${reg.url.replace(/\/$/, "")}/_matrix/app/v1/transactions/${txnId}`, {
251
+ method: "PUT",
252
+ headers: {
253
+ "content-type": "application/json",
254
+ authorization: `Bearer ${reg.hs_token}`
255
+ },
256
+ body: JSON.stringify(body),
257
+ signal: AbortSignal.timeout(3e4)
258
+ });
259
+ if (!res.ok) throw new Error(`Application service returned ${res.status}`);
260
+ } catch (err) {
261
+ const n = (this.retries.get(asId) ?? 0) + 1;
262
+ this.retries.set(asId, n);
263
+ this.schedule(asId, Math.min(1e3 * 2 ** n, 3e5));
264
+ this.hs.logger.debug("Appservice", asId, "unreachable", err);
265
+ return;
266
+ }
267
+ this.retries.delete(asId);
268
+ await this.hs.db.delete("appservice_outbox", [{
269
+ field: "id",
270
+ operator: "in",
271
+ value: rows.map((r) => r.id)
272
+ }]);
273
+ }
274
+ });
275
+ }
276
+ start() {
277
+ for (const s of this.services) if (s.reg.url) this.schedule(s.reg.id, 0);
278
+ }
279
+ stop() {
280
+ for (const t of this.timers.values()) clearTimeout(t);
281
+ this.timers.clear();
282
+ }
283
+ async query(reg, path) {
284
+ if (!reg.url) return null;
285
+ try {
286
+ return await this.hs.fetch(`${reg.url.replace(/\/$/, "")}${path}`, {
287
+ headers: { authorization: `Bearer ${reg.hs_token}` },
288
+ signal: AbortSignal.timeout(1e4)
289
+ });
290
+ } catch {
291
+ return null;
292
+ }
293
+ }
294
+ /** Ask the owning AS whether a user exists (it may create it). */
295
+ async queryUser(userId) {
296
+ for (const svc of this.services) {
297
+ if (!svc.users.some((n) => n.re.test(userId))) continue;
298
+ if ((await this.query(svc.reg, `/_matrix/app/v1/users/${encodeURIComponent(userId)}`))?.ok && await this.hs.db.findOne("users", [{
299
+ field: "user_id",
300
+ value: userId
301
+ }])) return true;
302
+ }
303
+ return false;
304
+ }
305
+ /** Ask the owning AS about an alias; returns the room ID if it now exists. */
306
+ async queryAlias(alias) {
307
+ for (const svc of this.services) {
308
+ if (!svc.aliases.some((n) => n.re.test(alias))) continue;
309
+ if ((await this.query(svc.reg, `/_matrix/app/v1/rooms/${encodeURIComponent(alias)}`))?.ok) {
310
+ const row = await this.hs.db.findOne("room_aliases", [{
311
+ field: "alias",
312
+ value: alias
313
+ }]);
314
+ if (row) return row.room_id;
315
+ }
316
+ }
317
+ return null;
318
+ }
319
+ async ping(reg, transactionId) {
320
+ if (!reg.url) throw new MatrixError(400, "M_URL_NOT_SET", "Application service has no URL");
321
+ const started = Date.now();
322
+ let res;
323
+ try {
324
+ res = await this.hs.fetch(`${reg.url.replace(/\/$/, "")}/_matrix/app/v1/ping`, {
325
+ method: "POST",
326
+ headers: {
327
+ "content-type": "application/json",
328
+ authorization: `Bearer ${reg.hs_token}`
329
+ },
330
+ body: JSON.stringify(transactionId ? { transaction_id: transactionId } : {}),
331
+ signal: AbortSignal.timeout(3e4)
332
+ });
333
+ } catch (err) {
334
+ throw new MatrixError(502, "M_CONNECTION_FAILED", `Could not reach the application service: ${err.message}`);
335
+ }
336
+ if (!res.ok) throw new MatrixError(502, "M_BAD_STATUS", "Application service returned an error", {
337
+ status: res.status,
338
+ body: await res.text().catch(() => "")
339
+ });
340
+ return Date.now() - started;
341
+ }
342
+ async thirdParty(path, query) {
343
+ const results = [];
344
+ for (const svc of this.services) {
345
+ const res = await this.query(svc.reg, `/_matrix/app/v1/thirdparty/${path}${query.toString() ? `?${query}` : ""}`);
346
+ if (res?.ok) {
347
+ const body = await res.json().catch(() => null);
348
+ if (Array.isArray(body)) results.push(...body);
349
+ }
350
+ }
351
+ return results;
352
+ }
353
+ async protocols() {
354
+ const out = {};
355
+ for (const svc of this.services) for (const protocol of svc.reg.protocols ?? []) {
356
+ const res = await this.query(svc.reg, `/_matrix/app/v1/thirdparty/protocol/${encodeURIComponent(protocol)}`);
357
+ out[protocol] = (res?.ok ? await res.json().catch(() => null) : null) ?? {
358
+ user_fields: [],
359
+ location_fields: [],
360
+ icon: "",
361
+ field_types: {},
362
+ instances: []
363
+ };
364
+ }
365
+ return out;
366
+ }
367
+ };
368
+ //#endregion
369
+ export { AppServiceService, loadAppServiceRegistrations, parseAppServiceRegistration };
@@ -0,0 +1,50 @@
1
+ import { Row } from "../storage/types.js";
2
+ import { Homeserver } from "../homeserver.js";
3
+ //#region src/core/audit.d.ts
4
+ /** Security-relevant actions recorded in the audit log. */
5
+ export type AuditAction = "login" | "login_failed" | "logout" | "register" | "password_change" | "deactivate" | "device_create" | "device_delete" | "token_refresh" | "room_create" | "room_delete" | "member_kick" | "member_ban" | "member_unban" | "admin_action" | "admin_command" | "registration_token_create" | "registration_token_delete" | "registration_setting" | "event_report" | "user_report" | "sso_login" | "oauth_login" | "user_lock" | "user_unlock" | "user_suspend" | "user_unsuspend";
6
+ export interface AuditEntry {
7
+ action: AuditAction;
8
+ /** User (or "server") performing the action. */
9
+ actor: string | null;
10
+ /** Subject of the action (user, room, token…). */
11
+ target?: string | null;
12
+ ip?: string | null;
13
+ userAgent?: string | null;
14
+ outcome?: "success" | "failure";
15
+ details?: Record<string, unknown>;
16
+ }
17
+ export interface AuditOptions {
18
+ /** Enable the audit log. Default: false. */
19
+ enabled?: boolean;
20
+ /** Store entries in the database (queryable via the admin API). Default: true. */
21
+ database?: boolean;
22
+ /** Additional sink, e.g. to forward to a SIEM. Errors are logged, never thrown. */
23
+ sink?: (entry: AuditEntry & {
24
+ ts: number;
25
+ id: number;
26
+ }) => void | Promise<void>;
27
+ /** Only record these actions. Default: all. */
28
+ actions?: AuditAction[];
29
+ /** Delete database entries older than this many days. Default: 365. `0` keeps them forever. */
30
+ retentionDays?: number;
31
+ }
32
+ /** Audit log of security-relevant actions (logins, account changes, admin actions…). */
33
+ export declare class AuditService {
34
+ private hs;
35
+ readonly options: AuditOptions;
36
+ constructor(hs: Homeserver, options?: AuditOptions);
37
+ get enabled(): boolean;
38
+ /** Record an entry. Never throws: auditing must not break the action being audited. */
39
+ record(entry: AuditEntry): Promise<void>;
40
+ query(opts?: {
41
+ actor?: string;
42
+ target?: string;
43
+ action?: string;
44
+ since?: number;
45
+ before?: number;
46
+ limit?: number;
47
+ }): Promise<Row[]>;
48
+ prune(): Promise<void>;
49
+ }
50
+ //#endregion
@@ -0,0 +1,86 @@
1
+ //#region src/core/audit.ts
2
+ /** Audit log of security-relevant actions (logins, account changes, admin actions…). */
3
+ var AuditService = class {
4
+ hs;
5
+ options;
6
+ constructor(hs, options = {}) {
7
+ this.hs = hs;
8
+ this.options = options;
9
+ }
10
+ get enabled() {
11
+ return !!this.options.enabled;
12
+ }
13
+ /** Record an entry. Never throws: auditing must not break the action being audited. */
14
+ async record(entry) {
15
+ if (!this.options.enabled) return;
16
+ if (this.options.actions && !this.options.actions.includes(entry.action)) return;
17
+ try {
18
+ const id = await this.hs.cluster.nextId("audit_log");
19
+ const ts = Date.now();
20
+ const row = {
21
+ id,
22
+ ts,
23
+ action: entry.action,
24
+ actor: entry.actor ?? null,
25
+ target: entry.target ?? null,
26
+ ip: entry.ip ?? null,
27
+ user_agent: entry.userAgent ?? null,
28
+ outcome: entry.outcome ?? "success",
29
+ details: entry.details ?? {}
30
+ };
31
+ if (this.options.database !== false) await this.hs.db.create("audit_log", row);
32
+ await this.options.sink?.({
33
+ ...entry,
34
+ outcome: row.outcome,
35
+ id,
36
+ ts
37
+ });
38
+ } catch (err) {
39
+ this.hs.logger.error("Failed to write audit log entry", err);
40
+ }
41
+ }
42
+ async query(opts = {}) {
43
+ const where = [];
44
+ if (opts.actor) where.push({
45
+ field: "actor",
46
+ value: opts.actor
47
+ });
48
+ if (opts.target) where.push({
49
+ field: "target",
50
+ value: opts.target
51
+ });
52
+ if (opts.action) where.push({
53
+ field: "action",
54
+ value: opts.action
55
+ });
56
+ if (opts.since) where.push({
57
+ field: "ts",
58
+ operator: "gte",
59
+ value: opts.since
60
+ });
61
+ if (opts.before) where.push({
62
+ field: "id",
63
+ operator: "lt",
64
+ value: opts.before
65
+ });
66
+ return this.hs.db.findMany("audit_log", {
67
+ where,
68
+ sortBy: {
69
+ field: "id",
70
+ direction: "desc"
71
+ },
72
+ limit: Math.min(opts.limit ?? 100, 1e3)
73
+ });
74
+ }
75
+ async prune() {
76
+ const days = this.options.retentionDays ?? 365;
77
+ if (!this.options.enabled || days <= 0) return;
78
+ await this.hs.db.delete("audit_log", [{
79
+ field: "ts",
80
+ operator: "lt",
81
+ value: Date.now() - days * 864e5
82
+ }]);
83
+ }
84
+ };
85
+ //#endregion
86
+ export { AuditService };
@@ -0,0 +1,48 @@
1
+ import { AppServiceRegistration } from "./appservices.js";
2
+ import { Homeserver } from "../homeserver.js";
3
+ //#region src/core/auth.d.ts
4
+ export interface Session {
5
+ userId: string;
6
+ deviceId: string;
7
+ accessToken: string;
8
+ isGuest: boolean;
9
+ isAdmin: boolean;
10
+ /** The account is locked: only logging out is allowed. */
11
+ locked?: boolean;
12
+ /** The account is suspended: see `assertNotSuspended`. */
13
+ suspended?: boolean;
14
+ /** Set when the request was made with an application service token. */
15
+ appservice?: AppServiceRegistration;
16
+ }
17
+ export interface CreatedSession {
18
+ user_id: string;
19
+ device_id: string;
20
+ access_token: string;
21
+ refresh_token?: string;
22
+ expires_in_ms?: number;
23
+ }
24
+ export declare class AuthService {
25
+ private hs;
26
+ private cache;
27
+ constructor(hs: Homeserver);
28
+ authenticate(token: string, ip: string | null): Promise<Session>;
29
+ /** Create (or reuse) a device and issue a fresh access token for it. */
30
+ createSession(userId: string, opts?: {
31
+ deviceId?: string;
32
+ displayName?: string;
33
+ refreshToken?: boolean;
34
+ }): Promise<CreatedSession>;
35
+ issueToken(userId: string, deviceId: string, withRefresh: boolean): Promise<CreatedSession>;
36
+ refresh(refreshToken: string): Promise<CreatedSession>;
37
+ /** Drop cached sessions of a user (after admin/deactivation changes). */
38
+ invalidateUser(userId: string): Promise<void>;
39
+ /** Issue a short-lived single-use `m.login.token` for a user (SSO, or host-app sign-in). */
40
+ createLoginToken(userId: string, ttlMs?: number): Promise<string>;
41
+ /** Redeem a login token; returns the user ID or throws. */
42
+ consumeLoginToken(token: string): Promise<string>;
43
+ revokeToken(token: string): Promise<void>;
44
+ revokeDeviceTokens(userId: string, deviceId: string): Promise<void>;
45
+ deleteDevice(userId: string, deviceId: string): Promise<void>;
46
+ deleteAllDevices(userId: string, except?: string): Promise<void>;
47
+ }
48
+ //#endregion