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.
- package/README.md +89 -11
- package/dist/adapters/cluster.d.ts +1 -26
- package/dist/adapters/cluster.js +50 -103
- package/dist/api/account.js +407 -354
- package/dist/api/admin.js +82 -61
- package/dist/api/appservice.js +37 -25
- package/dist/api/backup.js +73 -62
- package/dist/api/federation.js +172 -138
- package/dist/api/filters.js +4 -3
- package/dist/api/legacy.js +91 -78
- package/dist/api/media.js +71 -55
- package/dist/api/misc.js +46 -37
- package/dist/api/preview.js +37 -73
- package/dist/api/profile.js +108 -97
- package/dist/api/push.js +114 -99
- package/dist/api/relationships.js +64 -58
- package/dist/api/rooms.js +703 -580
- package/dist/api/rtc.d.ts +0 -1
- package/dist/api/rtc.js +48 -34
- package/dist/api/search.js +133 -127
- package/dist/api/slidingsync.js +41 -31
- package/dist/api/spaces.js +82 -77
- package/dist/api/sync.js +325 -135
- package/dist/api/threadsubs.js +72 -61
- package/dist/api/threepid.js +99 -66
- package/dist/api/uia.js +17 -16
- package/dist/api/user.js +221 -178
- package/dist/core/admin.d.ts +0 -1
- package/dist/core/admin.js +6 -5
- package/dist/core/adminroom.js +9 -8
- package/dist/core/appservices.d.ts +2 -1
- package/dist/core/appservices.js +5 -4
- package/dist/core/audit.d.ts +0 -1
- package/dist/core/authrules.js +26 -25
- package/dist/core/cluster.d.ts +12 -5
- package/dist/core/cluster.js +46 -29
- package/dist/core/delayed.js +2 -1
- package/dist/core/events.js +11 -10
- package/dist/core/html.js +13 -0
- package/dist/core/identity.js +1 -1
- package/dist/core/invitefilter.js +2 -1
- package/dist/core/json.js +10 -1
- package/dist/core/keys.d.ts +10 -1
- package/dist/core/keys.js +62 -42
- package/dist/core/m.d.ts +209 -0
- package/dist/core/m.js +182 -0
- package/dist/core/outbound.d.ts +16 -0
- package/dist/core/outbound.js +172 -0
- package/dist/core/partialstate.js +2 -1
- package/dist/core/policy.js +4 -3
- package/dist/core/push.d.ts +0 -1
- package/dist/core/push.js +41 -28
- package/dist/core/pushrules.js +36 -35
- package/dist/core/ratelimit.d.ts +1 -1
- package/dist/core/rooms.d.ts +10 -1
- package/dist/core/rooms.js +101 -74
- package/dist/core/semaphore.js +23 -0
- package/dist/core/sso.d.ts +15 -1
- package/dist/core/sso.js +38 -2
- package/dist/core/stateres.js +7 -6
- package/dist/core/threepid.js +1 -9
- package/dist/federation/client.js +9 -1
- package/dist/federation/index.d.ts +2 -2
- package/dist/federation/index.js +43 -35
- package/dist/homeserver.d.ts +23 -4
- package/dist/homeserver.js +12 -3
- package/dist/http/handler.js +2 -23
- package/dist/http/input.js +2 -2
- package/dist/http/procedure.js +25 -0
- package/dist/http/router.d.ts +9 -26
- package/dist/http/router.js +10 -30
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/storage/batch.js +11 -0
- package/dist/storage/memory.js +1 -0
- package/dist/storage/schema.js +1 -1
- package/dist/storage/sql.js +20 -1
- package/dist/storage/types.d.ts +16 -0
- package/dist/vendor/zod/LICENSE +21 -0
- package/dist/vendor/zod/v4/classic/errors.js +54 -0
- package/dist/vendor/zod/v4/classic/parse.js +17 -0
- package/dist/vendor/zod/v4/classic/schemas.js +865 -0
- package/dist/vendor/zod/v4/core/api.js +512 -0
- package/dist/vendor/zod/v4/core/checks.js +300 -0
- package/dist/vendor/zod/v4/core/core.js +109 -0
- package/dist/vendor/zod/v4/core/doc.js +35 -0
- package/dist/vendor/zod/v4/core/errors.js +118 -0
- package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
- package/dist/vendor/zod/v4/core/memoizer.js +255 -0
- package/dist/vendor/zod/v4/core/parse.js +206 -0
- package/dist/vendor/zod/v4/core/regexes.js +73 -0
- package/dist/vendor/zod/v4/core/registries.js +48 -0
- package/dist/vendor/zod/v4/core/schemas.js +1378 -0
- package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
- package/dist/vendor/zod/v4/core/util.js +537 -0
- package/dist/vendor/zod/v4/core/versions.js +8 -0
- package/dist/vendor/zod/v4/locales/en.js +110 -0
- package/package.json +8 -6
- package/dist/core/pushrules.d.ts +0 -1
package/dist/core/sso.js
CHANGED
|
@@ -3,6 +3,34 @@ import { errors } from "./errors.js";
|
|
|
3
3
|
import { LOCALPART_RE } from "./ids.js";
|
|
4
4
|
import { createHash, randomBytes } from "node:crypto";
|
|
5
5
|
//#region src/core/sso.ts
|
|
6
|
+
const UNSAFE_SCHEMES = /* @__PURE__ */ new Set([
|
|
7
|
+
"javascript:",
|
|
8
|
+
"data:",
|
|
9
|
+
"vbscript:",
|
|
10
|
+
"file:",
|
|
11
|
+
"blob:",
|
|
12
|
+
"about:"
|
|
13
|
+
]);
|
|
14
|
+
/** The client's redirect URL, if it is one the server may send a login token to. */
|
|
15
|
+
function checkRedirectUrl(raw) {
|
|
16
|
+
if (!raw) return null;
|
|
17
|
+
try {
|
|
18
|
+
return UNSAFE_SCHEMES.has(new URL(raw).protocol) ? null : raw;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function clientMatches(raw, entry) {
|
|
24
|
+
try {
|
|
25
|
+
const url = new URL(raw);
|
|
26
|
+
const allowed = new URL(entry);
|
|
27
|
+
if (allowed.host) return url.protocol === allowed.protocol && url.host === allowed.host && url.pathname.startsWith(allowed.pathname);
|
|
28
|
+
return raw.startsWith(entry);
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const sameIssuer = (a, b) => typeof a === "string" && a.replace(/\/$/, "") === b.replace(/\/$/, "");
|
|
6
34
|
function sanitizeLocalpart(raw) {
|
|
7
35
|
return raw.toLowerCase().replace(/[^a-z0-9._=\-/+]/g, "_").slice(0, 200);
|
|
8
36
|
}
|
|
@@ -15,10 +43,12 @@ function decodeJwtPayload(jwt) {
|
|
|
15
43
|
var SsoService = class {
|
|
16
44
|
hs;
|
|
17
45
|
providers;
|
|
46
|
+
clientAllowlist;
|
|
18
47
|
discovery = /* @__PURE__ */ new Map();
|
|
19
48
|
constructor(hs, options) {
|
|
20
49
|
this.hs = hs;
|
|
21
50
|
this.providers = options.providers;
|
|
51
|
+
this.clientAllowlist = options.clientAllowlist ?? [];
|
|
22
52
|
}
|
|
23
53
|
callbackUrl(provider) {
|
|
24
54
|
const base = this.hs.config.publicBaseUrl;
|
|
@@ -31,6 +61,7 @@ var SsoService = class {
|
|
|
31
61
|
const res = await this.hs.fetch(`${provider.issuer.replace(/\/$/, "")}/.well-known/openid-configuration`, { signal: AbortSignal.timeout(1e4) });
|
|
32
62
|
if (!res.ok) throw new Error(`OIDC discovery for ${provider.id} failed with ${res.status}`);
|
|
33
63
|
const meta = await res.json();
|
|
64
|
+
if (!sameIssuer(meta.issuer, provider.issuer)) throw new Error(`OIDC discovery for ${provider.id} returned another issuer`);
|
|
34
65
|
this.discovery.set(provider.id, meta);
|
|
35
66
|
return meta;
|
|
36
67
|
}
|
|
@@ -96,6 +127,7 @@ var SsoService = class {
|
|
|
96
127
|
if (!res.ok) throw errors.forbidden("Code exchange failed");
|
|
97
128
|
const tokens = await res.json();
|
|
98
129
|
let claims = {};
|
|
130
|
+
if ((provider.scopes ?? ["openid"]).includes("openid") && typeof tokens.id_token !== "string") throw errors.forbidden("The provider returned no ID token");
|
|
99
131
|
if (typeof tokens.id_token === "string") {
|
|
100
132
|
claims = decodeJwtPayload(tokens.id_token);
|
|
101
133
|
const aud = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
|
|
@@ -130,7 +162,11 @@ var SsoService = class {
|
|
|
130
162
|
});
|
|
131
163
|
const redirect = new URL(pending.redirectUrl);
|
|
132
164
|
redirect.searchParams.set("loginToken", loginToken);
|
|
133
|
-
return
|
|
165
|
+
return {
|
|
166
|
+
url: redirect.toString(),
|
|
167
|
+
confirm: !this.clientAllowlist.some((entry) => clientMatches(pending.redirectUrl, entry)),
|
|
168
|
+
client: redirect.host || redirect.protocol.replace(/:$/, "")
|
|
169
|
+
};
|
|
134
170
|
}
|
|
135
171
|
async resolveUser(provider, claims) {
|
|
136
172
|
const link = await this.hs.db.findOne("sso_identities", [{
|
|
@@ -162,4 +198,4 @@ var SsoService = class {
|
|
|
162
198
|
}
|
|
163
199
|
};
|
|
164
200
|
//#endregion
|
|
165
|
-
export { SsoService };
|
|
201
|
+
export { SsoService, checkRedirectUrl };
|
package/dist/core/stateres.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { M } from "./m.js";
|
|
1
2
|
import { eventIdOf, roomVersionFeatures, stateKey } from "./events.js";
|
|
2
3
|
import { authEventKeys, checkAuth, powerLevels, userLevel } from "./authrules.js";
|
|
3
4
|
//#region src/core/stateres.ts
|
|
4
|
-
const isPowerEvent = (ev) => ev.type ===
|
|
5
|
+
const isPowerEvent = (ev) => ev.type === M.ROOM.POWER_LEVELS && ev.state_key === "" || ev.type === M.ROOM.JOIN_RULES && ev.state_key === "" || ev.type === M.ROOM.CREATE && ev.state_key === "" || ev.type === M.ROOM.MEMBER && ["leave", "ban"].includes(ev.content?.membership) && ev.sender !== ev.state_key;
|
|
5
6
|
const authIds = (ev) => (ev.auth_events ?? []).map(eventIdOf);
|
|
6
7
|
var EventCache = class {
|
|
7
8
|
events = /* @__PURE__ */ new Map();
|
|
@@ -66,7 +67,7 @@ async function resolveState(stateSets, roomVersion, source, createEvent) {
|
|
|
66
67
|
for (const id of await cache.authChain(powerEvents)) if (full.has(id)) control.add(id);
|
|
67
68
|
let resolved = await iterativeAuthChecks(cache, await reverseTopologicalPowerSort(cache, control, roomVersion, createEvent), v21 ? {} : { ...unconflicted }, roomVersion, createEvent);
|
|
68
69
|
const remaining = [...full].filter((id) => !control.has(id));
|
|
69
|
-
const mainline = await mainlineOf(cache, resolved[stateKey(
|
|
70
|
+
const mainline = await mainlineOf(cache, resolved[stateKey(M.ROOM.POWER_LEVELS, "")]);
|
|
70
71
|
const positions = /* @__PURE__ */ new Map();
|
|
71
72
|
for (const id of remaining) positions.set(id, await mainlinePosition(cache, id, mainline));
|
|
72
73
|
remaining.sort((a, b) => {
|
|
@@ -107,7 +108,7 @@ async function senderPowerLevel(cache, ev, roomVersion, createEvent) {
|
|
|
107
108
|
const a = cache.get(id);
|
|
108
109
|
if (a && a.state_key !== void 0) state.set(stateKey(a.type, a.state_key), a);
|
|
109
110
|
}
|
|
110
|
-
if (createEvent) state.set(stateKey(
|
|
111
|
+
if (createEvent) state.set(stateKey(M.ROOM.CREATE, ""), createEvent);
|
|
111
112
|
const level = userLevel(powerLevels(state, roomVersion), ev.sender);
|
|
112
113
|
return Number.isFinite(level) ? level : Number.MAX_SAFE_INTEGER;
|
|
113
114
|
}
|
|
@@ -160,7 +161,7 @@ async function iterativeAuthChecks(cache, ids, base, roomVersion, createEvent) {
|
|
|
160
161
|
const sev = sid ? cache.get(sid) : void 0;
|
|
161
162
|
if (sev) authState.set(k, sev);
|
|
162
163
|
}
|
|
163
|
-
if (createEvent) authState.set(stateKey(
|
|
164
|
+
if (createEvent) authState.set(stateKey(M.ROOM.CREATE, ""), createEvent);
|
|
164
165
|
try {
|
|
165
166
|
checkAuth(ev, authState, roomVersion);
|
|
166
167
|
state[stateKey(ev.type, ev.state_key)] = id;
|
|
@@ -179,7 +180,7 @@ async function mainlineOf(cache, plId) {
|
|
|
179
180
|
await cache.load(authIds(ev));
|
|
180
181
|
current = authIds(ev).find((a) => {
|
|
181
182
|
const e = cache.get(a);
|
|
182
|
-
return e?.type ===
|
|
183
|
+
return e?.type === M.ROOM.POWER_LEVELS && e.state_key === "";
|
|
183
184
|
});
|
|
184
185
|
}
|
|
185
186
|
const positions = /* @__PURE__ */ new Map();
|
|
@@ -199,7 +200,7 @@ async function mainlinePosition(cache, id, mainline) {
|
|
|
199
200
|
await cache.load(authIds(ev));
|
|
200
201
|
current = authIds(ev).find((a) => {
|
|
201
202
|
const e = cache.get(a);
|
|
202
|
-
return e?.type ===
|
|
203
|
+
return e?.type === M.ROOM.POWER_LEVELS && e.state_key === "";
|
|
203
204
|
});
|
|
204
205
|
}
|
|
205
206
|
return 0;
|
package/dist/core/threepid.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomString } from "./crypto.js";
|
|
2
2
|
import { MatrixError, errors } from "./errors.js";
|
|
3
3
|
import { toMsisdn } from "./phone.js";
|
|
4
|
+
import { escapeHtml } from "./html.js";
|
|
4
5
|
import { randomInt } from "node:crypto";
|
|
5
6
|
//#region src/core/threepid.ts
|
|
6
7
|
const CLIENT_SECRET_RE = /^[0-9a-zA-Z.=_-]{1,255}$/;
|
|
@@ -262,14 +263,5 @@ var ThreepidService = class {
|
|
|
262
263
|
};
|
|
263
264
|
}
|
|
264
265
|
};
|
|
265
|
-
function escapeHtml(s) {
|
|
266
|
-
return s.replace(/[&<>"']/g, (c) => ({
|
|
267
|
-
"&": "&",
|
|
268
|
-
"<": "<",
|
|
269
|
-
">": ">",
|
|
270
|
-
"\"": """,
|
|
271
|
-
"'": "'"
|
|
272
|
-
})[c]);
|
|
273
|
-
}
|
|
274
266
|
//#endregion
|
|
275
267
|
export { ThreepidService, canonicalAddress, isMedium };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { canonicalJson } from "../core/json.js";
|
|
2
2
|
import { unpaddedBase64 } from "../core/crypto.js";
|
|
3
|
+
import { M } from "../core/m.js";
|
|
3
4
|
import { MatrixError } from "../core/errors.js";
|
|
5
|
+
import { checkLiteral } from "../core/outbound.js";
|
|
4
6
|
import { sign } from "node:crypto";
|
|
5
7
|
import { resolveSrv } from "node:dns/promises";
|
|
6
8
|
import { Agent, request } from "node:https";
|
|
@@ -149,7 +151,7 @@ var FederationClient = class {
|
|
|
149
151
|
}, "GET", "/.well-known/matrix/server", void 0, {}, 5e3);
|
|
150
152
|
if (res.status !== 200) return null;
|
|
151
153
|
const body = JSON.parse(res.body.toString("utf8"));
|
|
152
|
-
return typeof body[
|
|
154
|
+
return typeof body[M.SERVER] === "string" && body[M.SERVER] ? body[M.SERVER] : null;
|
|
153
155
|
} catch {
|
|
154
156
|
return null;
|
|
155
157
|
}
|
|
@@ -168,6 +170,11 @@ var FederationClient = class {
|
|
|
168
170
|
return null;
|
|
169
171
|
}
|
|
170
172
|
raw(target, method, pathWithQuery, body, headers, timeoutMs, poolKey = target.hostHeader) {
|
|
173
|
+
try {
|
|
174
|
+
checkLiteral(this.hs.addressPolicy, target.host);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
return Promise.reject(err);
|
|
177
|
+
}
|
|
171
178
|
return new Promise((resolve, reject) => {
|
|
172
179
|
let connectTimer = null;
|
|
173
180
|
const req = request({
|
|
@@ -177,6 +184,7 @@ var FederationClient = class {
|
|
|
177
184
|
path: pathWithQuery,
|
|
178
185
|
servername: isIP(target.tlsName) ? void 0 : target.tlsName,
|
|
179
186
|
agent: this.agentFor(poolKey),
|
|
187
|
+
lookup: this.hs.addressPolicy.lookup,
|
|
180
188
|
headers: {
|
|
181
189
|
host: target.hostHeader,
|
|
182
190
|
...headers,
|
|
@@ -7,7 +7,7 @@ import { TransactionSender } from "./sender.js";
|
|
|
7
7
|
import { Homeserver } from "../homeserver.js";
|
|
8
8
|
//#region src/federation/index.d.ts
|
|
9
9
|
export interface FederationOptions {
|
|
10
|
-
/** Enable federation. Default:
|
|
10
|
+
/** Enable federation (talking to other homeservers). Default: false, so that a server only joins the network on purpose. */
|
|
11
11
|
enabled?: boolean;
|
|
12
12
|
/** Join remote rooms with partial state first (MSC3902 "faster joins"). Default true. */
|
|
13
13
|
fasterJoins?: boolean;
|
|
@@ -136,7 +136,7 @@ export declare class FederationService {
|
|
|
136
136
|
*/
|
|
137
137
|
backfill(roomId: string, fromEventIds: string[], limit?: number): Promise<number>;
|
|
138
138
|
/** Download remote media (authenticated federation endpoint, falling back to the legacy one). */
|
|
139
|
-
fetchMedia(server: string, mediaId: string): Promise<{
|
|
139
|
+
fetchMedia(server: string, mediaId: string, maxBytes: number): Promise<{
|
|
140
140
|
data: Uint8Array;
|
|
141
141
|
contentType: string | null;
|
|
142
142
|
filename: string | null;
|
package/dist/federation/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isObject } from "../core/json.js";
|
|
2
2
|
import { unpaddedBase64, urlSafeBase64 } from "../core/crypto.js";
|
|
3
|
+
import { M } from "../core/m.js";
|
|
3
4
|
import { SUPPORTED_ROOM_VERSIONS, computeContentHash, computeReferenceHash, eventIdOf, finalisePdu, redact, roomVersionFeatures, splitStateKey, stateKey, stripState } from "../core/events.js";
|
|
4
5
|
import { MatrixError, errors } from "../core/errors.js";
|
|
5
6
|
import { parseId } from "../core/ids.js";
|
|
@@ -7,6 +8,7 @@ import { authStateFromAuthEvents, checkAuth, getState, membershipOf } from "../c
|
|
|
7
8
|
import { globMatch } from "../core/glob.js";
|
|
8
9
|
import { resolveState } from "../core/stateres.js";
|
|
9
10
|
import { MissingStateError } from "../core/rooms.js";
|
|
11
|
+
import { publicGet } from "../core/outbound.js";
|
|
10
12
|
import { FederationClient } from "./client.js";
|
|
11
13
|
import { KeyRing } from "./keyring.js";
|
|
12
14
|
import { TransactionSender } from "./sender.js";
|
|
@@ -14,7 +16,7 @@ import { TransactionSender } from "./sender.js";
|
|
|
14
16
|
const domainOf = (id) => id.slice(id.indexOf(":") + 1);
|
|
15
17
|
/** Server ACL check (m.room.server_acl). */
|
|
16
18
|
function serverAllowedByAcl(state, serverName) {
|
|
17
|
-
const acl = getState(state,
|
|
19
|
+
const acl = getState(state, M.ROOM.SERVER_ACL)?.content;
|
|
18
20
|
if (!isObject(acl)) return true;
|
|
19
21
|
const host = serverName.replace(/:\d+$/, "").replace(/^\[|\]$/g, "");
|
|
20
22
|
if (acl.allow_ip_literals === false && (/^\d+\.\d+\.\d+\.\d+$/.test(host) || host.includes(":"))) return false;
|
|
@@ -37,7 +39,7 @@ var FederationService = class {
|
|
|
37
39
|
constructor(hs, options = {}) {
|
|
38
40
|
this.hs = hs;
|
|
39
41
|
this.options = options;
|
|
40
|
-
this.enabled = options.enabled
|
|
42
|
+
this.enabled = options.enabled === true;
|
|
41
43
|
this.fasterJoins = options.fasterJoins !== false;
|
|
42
44
|
this.client = new FederationClient(hs, { rejectUnauthorized: options.verifyTls !== false });
|
|
43
45
|
this.keyring = new KeyRing(hs);
|
|
@@ -57,7 +59,7 @@ var FederationService = class {
|
|
|
57
59
|
const out = { ...rest };
|
|
58
60
|
const f = roomVersionFeatures(version);
|
|
59
61
|
if (!f.legacyEventIds) delete out.event_id;
|
|
60
|
-
if (f.v12 && out.type ===
|
|
62
|
+
if (f.v12 && out.type === M.ROOM.CREATE) delete out.room_id;
|
|
61
63
|
return out;
|
|
62
64
|
}
|
|
63
65
|
computeEventId(raw, version) {
|
|
@@ -73,7 +75,7 @@ var FederationService = class {
|
|
|
73
75
|
async verifyPdu(raw, version, roomId) {
|
|
74
76
|
if (!isObject(raw) || typeof raw.type !== "string" || typeof raw.sender !== "string" || !isObject(raw.content)) throw errors.badJson("Malformed PDU");
|
|
75
77
|
const f = roomVersionFeatures(version);
|
|
76
|
-
if (!f.v12 || raw.type !==
|
|
78
|
+
if (!f.v12 || raw.type !== M.ROOM.CREATE) {
|
|
77
79
|
if (raw.room_id !== roomId) throw errors.badJson("PDU room_id mismatch");
|
|
78
80
|
}
|
|
79
81
|
const eventId = this.computeEventId(raw, version);
|
|
@@ -84,7 +86,7 @@ var FederationService = class {
|
|
|
84
86
|
if (f.legacyEventIds && domainOf(eventId) !== senderDomain && !await this.keyring.verifySignedBy(redacted, domainOf(eventId))) throw errors.forbidden(`Event ${eventId} is not signed by its origin`);
|
|
85
87
|
let joinAuthorisedSignatureValid;
|
|
86
88
|
const via = raw.content?.join_authorised_via_users_server;
|
|
87
|
-
if (f.restricted && raw.type ===
|
|
89
|
+
if (f.restricted && raw.type === M.ROOM.MEMBER && raw.content?.membership === "join" && typeof via === "string" && via.includes(":")) joinAuthorisedSignatureValid = await this.keyring.verifySignedBy(redacted, domainOf(via));
|
|
88
90
|
let pdu = {
|
|
89
91
|
...raw,
|
|
90
92
|
event_id: eventId
|
|
@@ -93,7 +95,7 @@ var FederationService = class {
|
|
|
93
95
|
...redact(raw, version),
|
|
94
96
|
event_id: eventId
|
|
95
97
|
};
|
|
96
|
-
if (f.v12 && pdu.type ===
|
|
98
|
+
if (f.v12 && pdu.type === M.ROOM.CREATE) pdu.room_id = roomId;
|
|
97
99
|
return {
|
|
98
100
|
pdu,
|
|
99
101
|
joinAuthorisedSignatureValid
|
|
@@ -104,7 +106,7 @@ var FederationService = class {
|
|
|
104
106
|
const servers = /* @__PURE__ */ new Set();
|
|
105
107
|
for (const [key, ev] of state) {
|
|
106
108
|
const [type, userId] = splitStateKey(key);
|
|
107
|
-
if (type ===
|
|
109
|
+
if (type === M.ROOM.MEMBER && ev.content?.membership === "join") servers.add(domainOf(userId));
|
|
108
110
|
}
|
|
109
111
|
servers.delete(this.hs.serverName);
|
|
110
112
|
for (const s of servers) if (!this.serverAllowed(s) || applyAcl && !serverAllowedByAcl(state, s)) servers.delete(s);
|
|
@@ -123,8 +125,8 @@ var FederationService = class {
|
|
|
123
125
|
onLocalEvent(pdu, state) {
|
|
124
126
|
if (!this.enabled) return;
|
|
125
127
|
(async () => {
|
|
126
|
-
const destinations = await this.roomDestinations(pdu.room_id, state, pdu.type !==
|
|
127
|
-
if (pdu.type ===
|
|
128
|
+
const destinations = await this.roomDestinations(pdu.room_id, state, pdu.type !== M.ROOM.SERVER_ACL);
|
|
129
|
+
if (pdu.type === M.ROOM.MEMBER && typeof pdu.state_key === "string" && !this.hs.isLocalUser(pdu.state_key)) destinations.add(domainOf(pdu.state_key));
|
|
128
130
|
if (!destinations.size) return;
|
|
129
131
|
const version = await this.hs.rooms.roomVersion(pdu.room_id) ?? "10";
|
|
130
132
|
await this.sender.sendPdu(destinations, this.toFederationPdu(pdu, version));
|
|
@@ -147,17 +149,17 @@ var FederationService = class {
|
|
|
147
149
|
return out;
|
|
148
150
|
}
|
|
149
151
|
async onTyping(roomId, userId, typing) {
|
|
150
|
-
if (this.hs.isLocalUser(userId)) await this.sendEduToRooms([roomId],
|
|
152
|
+
if (this.hs.isLocalUser(userId)) await this.sendEduToRooms([roomId], M.TYPING, {
|
|
151
153
|
room_id: roomId,
|
|
152
154
|
user_id: userId,
|
|
153
155
|
typing
|
|
154
156
|
}, false);
|
|
155
157
|
}
|
|
156
158
|
async onReceipt(roomId, userId, receiptType, eventId, ts, threadId) {
|
|
157
|
-
if (!this.hs.isLocalUser(userId) || receiptType ===
|
|
159
|
+
if (!this.hs.isLocalUser(userId) || receiptType === M.READ.PRIVATE) return;
|
|
158
160
|
const data = { ts };
|
|
159
161
|
if (threadId) data.thread_id = threadId;
|
|
160
|
-
await this.sendEduToRooms([roomId],
|
|
162
|
+
await this.sendEduToRooms([roomId], M.RECEIPT, { [roomId]: { [receiptType]: { [userId]: {
|
|
161
163
|
event_ids: [eventId],
|
|
162
164
|
data
|
|
163
165
|
} } } });
|
|
@@ -166,7 +168,7 @@ var FederationService = class {
|
|
|
166
168
|
if (!this.enabled || !this.hs.isLocalUser(userId)) return;
|
|
167
169
|
const servers = await this.serversSharingRooms(userId);
|
|
168
170
|
if (!servers.size) return;
|
|
169
|
-
await this.sender.sendEdu(servers,
|
|
171
|
+
await this.sender.sendEdu(servers, M.PRESENCE, { push: [{
|
|
170
172
|
user_id: userId,
|
|
171
173
|
presence,
|
|
172
174
|
last_active_ago: 0,
|
|
@@ -217,7 +219,7 @@ var FederationService = class {
|
|
|
217
219
|
const d = devices.find((x) => x.device_id === u.deviceId);
|
|
218
220
|
if (d?.display_name) content.device_display_name = d.display_name;
|
|
219
221
|
}
|
|
220
|
-
await this.sender.sendEdu(servers,
|
|
222
|
+
await this.sender.sendEdu(servers, M.DEVICE_LIST_UPDATE, content);
|
|
221
223
|
}
|
|
222
224
|
}
|
|
223
225
|
async onSigningKeyUpdate(userId) {
|
|
@@ -230,7 +232,7 @@ var FederationService = class {
|
|
|
230
232
|
}] });
|
|
231
233
|
const content = { user_id: userId };
|
|
232
234
|
for (const r of rows) if (r.key_type !== "user_signing") content[`${r.key_type}_key`] = r.key;
|
|
233
|
-
await this.sender.sendEdu(servers,
|
|
235
|
+
await this.sender.sendEdu(servers, M.SIGNING_KEY_UPDATE, content);
|
|
234
236
|
}
|
|
235
237
|
/** Deliver to-device messages addressed to remote users. */
|
|
236
238
|
async sendToDevice(sender, type, messages, messageId) {
|
|
@@ -241,7 +243,7 @@ var FederationService = class {
|
|
|
241
243
|
if (server === this.hs.serverName || !this.serverAllowed(server)) continue;
|
|
242
244
|
(byServer.get(server) ?? byServer.set(server, {}).get(server))[userId] = devices;
|
|
243
245
|
}
|
|
244
|
-
for (const [server, msgs] of byServer) await this.sender.sendEdu([server],
|
|
246
|
+
for (const [server, msgs] of byServer) await this.sender.sendEdu([server], M.DIRECT_TO_DEVICE, {
|
|
245
247
|
sender,
|
|
246
248
|
type,
|
|
247
249
|
message_id: `${messageId}-${server}`,
|
|
@@ -301,7 +303,7 @@ var FederationService = class {
|
|
|
301
303
|
* rescinded or their knock being accepted/rejected.
|
|
302
304
|
*/
|
|
303
305
|
async handleOutOfRoomMembership(origin, roomId, raw) {
|
|
304
|
-
if (raw?.type !==
|
|
306
|
+
if (raw?.type !== M.ROOM.MEMBER || typeof raw.state_key !== "string" || !this.hs.isLocalUser(raw.state_key)) throw errors.notFound("Unknown room");
|
|
305
307
|
const row = await this.hs.db.findOne("memberships", [{
|
|
306
308
|
field: "room_id",
|
|
307
309
|
value: roomId
|
|
@@ -311,7 +313,7 @@ var FederationService = class {
|
|
|
311
313
|
}]);
|
|
312
314
|
if (!row) throw errors.notFound("Unknown room");
|
|
313
315
|
const previous = await this.hs.rooms.getPdu(row.event_id);
|
|
314
|
-
const create = (previous?.unsigned?.invite_room_state ?? previous?.unsigned?.knock_room_state ?? []).find((e) => e?.type ===
|
|
316
|
+
const create = (previous?.unsigned?.invite_room_state ?? previous?.unsigned?.knock_room_state ?? []).find((e) => e?.type === M.ROOM.CREATE);
|
|
315
317
|
const version = typeof create?.content?.room_version === "string" ? create.content.room_version : "1";
|
|
316
318
|
const { pdu } = await this.verifyPdu(raw, version, roomId);
|
|
317
319
|
if (domainOf(pdu.sender) !== origin && domainOf(pdu.sender) !== domainOf(raw.state_key)) throw errors.forbidden("Unexpected sender");
|
|
@@ -571,7 +573,7 @@ var FederationService = class {
|
|
|
571
573
|
authPdus.push(p);
|
|
572
574
|
}
|
|
573
575
|
try {
|
|
574
|
-
const create = roomVersionFeatures(version).v12 && pdu.type !==
|
|
576
|
+
const create = roomVersionFeatures(version).v12 && pdu.type !== M.ROOM.CREATE ? await this.hs.rooms.getPdu(`$${roomId.slice(1)}`) ?? void 0 : void 0;
|
|
575
577
|
checkAuth(pdu, authStateFromAuthEvents(pdu, authPdus, version, create), version);
|
|
576
578
|
return null;
|
|
577
579
|
} catch (err) {
|
|
@@ -592,7 +594,7 @@ var FederationService = class {
|
|
|
592
594
|
const content = edu?.content;
|
|
593
595
|
if (!isObject(content)) return;
|
|
594
596
|
switch (edu.edu_type) {
|
|
595
|
-
case
|
|
597
|
+
case M.TYPING: {
|
|
596
598
|
const { room_id, user_id, typing } = content;
|
|
597
599
|
if (typeof user_id !== "string" || domainOf(user_id) !== origin || typeof room_id !== "string") return;
|
|
598
600
|
if (!await this.hs.rooms.exists(room_id) || !await this.mayActInRoom(room_id, user_id, origin)) return;
|
|
@@ -600,12 +602,12 @@ var FederationService = class {
|
|
|
600
602
|
await this.hs.typing.setTyping(room_id, user_id, !!typing, typeof content.timeout === "number" ? content.timeout : 3e4);
|
|
601
603
|
return;
|
|
602
604
|
}
|
|
603
|
-
case
|
|
605
|
+
case M.RECEIPT:
|
|
604
606
|
for (const [roomId, byType] of Object.entries(content)) {
|
|
605
607
|
if (!isObject(byType) || !await this.hs.rooms.exists(roomId)) continue;
|
|
606
608
|
if (!serverAllowedByAcl(await this.hs.rooms.currentState(roomId), origin)) continue;
|
|
607
609
|
for (const [receiptType, byUser] of Object.entries(byType)) {
|
|
608
|
-
if (
|
|
610
|
+
if (receiptType !== M.READ._ && receiptType !== M.READ.PRIVATE || !isObject(byUser)) continue;
|
|
609
611
|
for (const [userId, r] of Object.entries(byUser)) {
|
|
610
612
|
if (domainOf(userId) !== origin || !isObject(r) || !Array.isArray(r.event_ids)) continue;
|
|
611
613
|
if (!await this.mayActInRoom(roomId, userId, origin)) continue;
|
|
@@ -625,7 +627,7 @@ var FederationService = class {
|
|
|
625
627
|
this.hs.notifier.notify(await this.hs.rooms.joinedMembers(roomId));
|
|
626
628
|
}
|
|
627
629
|
return;
|
|
628
|
-
case
|
|
630
|
+
case M.PRESENCE:
|
|
629
631
|
for (const p of Array.isArray(content.push) ? content.push : []) {
|
|
630
632
|
if (!isObject(p) || typeof p.user_id !== "string" || domainOf(p.user_id) !== origin) continue;
|
|
631
633
|
if (![
|
|
@@ -643,7 +645,7 @@ var FederationService = class {
|
|
|
643
645
|
this.hs.notifier.notify(await this.hs.rooms.usersSharingRooms(p.user_id));
|
|
644
646
|
}
|
|
645
647
|
return;
|
|
646
|
-
case
|
|
648
|
+
case M.DEVICE_LIST_UPDATE: {
|
|
647
649
|
const userId = content.user_id;
|
|
648
650
|
if (typeof userId !== "string" || domainOf(userId) !== origin || typeof content.device_id !== "string") return;
|
|
649
651
|
const known = await this.hs.db.findOne("remote_device_lists", [{
|
|
@@ -655,7 +657,7 @@ var FederationService = class {
|
|
|
655
657
|
if (known || content.deleted || isObject(content.keys)) await this.hs.keys.markDeviceListChanged(userId);
|
|
656
658
|
return;
|
|
657
659
|
}
|
|
658
|
-
case
|
|
660
|
+
case M.SIGNING_KEY_UPDATE: {
|
|
659
661
|
const userId = content.user_id;
|
|
660
662
|
if (typeof userId !== "string" || domainOf(userId) !== origin) return;
|
|
661
663
|
for (const type of ["master", "self_signing"]) {
|
|
@@ -671,7 +673,7 @@ var FederationService = class {
|
|
|
671
673
|
await this.hs.keys.markDeviceListChanged(userId);
|
|
672
674
|
return;
|
|
673
675
|
}
|
|
674
|
-
case
|
|
676
|
+
case M.DIRECT_TO_DEVICE: {
|
|
675
677
|
const { sender, type, message_id, messages } = content;
|
|
676
678
|
if (typeof sender !== "string" || domainOf(sender) !== origin || typeof type !== "string" || !isObject(messages)) return;
|
|
677
679
|
if (!await this.hs.cluster.setIfAbsent(`fedtodevice:${origin}|${message_id}`, true, 864e5)) return;
|
|
@@ -751,7 +753,7 @@ var FederationService = class {
|
|
|
751
753
|
const pdu = await this.hs.rooms.getPdu(id);
|
|
752
754
|
if (!pdu) continue;
|
|
753
755
|
stateIds[stateKey(pdu.type, pdu.state_key)] = id;
|
|
754
|
-
if (pdu.type ===
|
|
756
|
+
if (pdu.type === M.ROOM.CREATE) create = pdu;
|
|
755
757
|
}
|
|
756
758
|
if (!create) throw errors.badJson("send_join response lacks the create event");
|
|
757
759
|
if (!await this.hs.rooms.exists(roomId)) await this.hs.db.upsert("rooms", {
|
|
@@ -960,13 +962,16 @@ var FederationService = class {
|
|
|
960
962
|
return 0;
|
|
961
963
|
}
|
|
962
964
|
/** Download remote media (authenticated federation endpoint, falling back to the legacy one). */
|
|
963
|
-
async fetchMedia(server, mediaId) {
|
|
965
|
+
async fetchMedia(server, mediaId, maxBytes) {
|
|
964
966
|
this.requireEnabled();
|
|
965
967
|
const res = await this.client.requestRaw(server, "GET", `/_matrix/federation/v1/media/download/${encodeURIComponent(mediaId)}`, { timeoutMs: 6e4 });
|
|
966
968
|
if (res.status === 200) {
|
|
967
969
|
const ct = String(res.headers["content-type"] ?? "");
|
|
968
970
|
const m = /boundary=("?)([^";]+)\1/.exec(ct);
|
|
969
|
-
if (ct.startsWith("multipart/") && m) return parseMultipartMedia(res.body, m[2],
|
|
971
|
+
if (ct.startsWith("multipart/") && m) return parseMultipartMedia(res.body, m[2], {
|
|
972
|
+
maxBytes,
|
|
973
|
+
fetch: this.hs.publicFetch
|
|
974
|
+
});
|
|
970
975
|
}
|
|
971
976
|
const legacy = await this.client.requestRaw(server, "GET", `/_matrix/media/v3/download/${encodeURIComponent(server)}/${encodeURIComponent(mediaId)}`, {
|
|
972
977
|
query: { allow_remote: "false" },
|
|
@@ -993,14 +998,14 @@ var FederationService = class {
|
|
|
993
998
|
const out = /* @__PURE__ */ new Set();
|
|
994
999
|
for (const [key, ev] of state) {
|
|
995
1000
|
const [type, userId] = splitStateKey(key);
|
|
996
|
-
if (type ===
|
|
1001
|
+
if (type === M.ROOM.MEMBER && ev.content?.membership === "join") out.add(domainOf(userId));
|
|
997
1002
|
}
|
|
998
1003
|
return [...out];
|
|
999
1004
|
}
|
|
1000
1005
|
isMembershipVisibleTo(state, server) {
|
|
1001
1006
|
for (const [key] of state) {
|
|
1002
1007
|
const [type, userId] = splitStateKey(key);
|
|
1003
|
-
if (type ===
|
|
1008
|
+
if (type === M.ROOM.MEMBER && domainOf(userId) === server && membershipOf(state, userId) === "join") return true;
|
|
1004
1009
|
}
|
|
1005
1010
|
return false;
|
|
1006
1011
|
}
|
|
@@ -1019,7 +1024,7 @@ function filenameFromDisposition(header) {
|
|
|
1019
1024
|
return plain ? plain[1] : null;
|
|
1020
1025
|
}
|
|
1021
1026
|
/** Parse a federation media multipart/mixed response: a JSON part then the file (or a redirect). */
|
|
1022
|
-
async function parseMultipartMedia(body, boundary,
|
|
1027
|
+
async function parseMultipartMedia(body, boundary, limits) {
|
|
1023
1028
|
const delimiter = Buffer.from(`--${boundary}`);
|
|
1024
1029
|
const parts = [];
|
|
1025
1030
|
let idx = body.indexOf(delimiter);
|
|
@@ -1040,10 +1045,13 @@ async function parseMultipartMedia(body, boundary, fed) {
|
|
|
1040
1045
|
if (i > 0) headers[line.slice(0, i).trim().toLowerCase()] = line.slice(i + 1).trim();
|
|
1041
1046
|
}
|
|
1042
1047
|
if (headers.location) {
|
|
1043
|
-
const res = await fetch
|
|
1044
|
-
|
|
1048
|
+
const res = await publicGet(limits.fetch, headers.location, {
|
|
1049
|
+
maxBytes: limits.maxBytes,
|
|
1050
|
+
timeoutMs: 6e4
|
|
1051
|
+
});
|
|
1052
|
+
if (res.status !== 200) throw errors.notFound("Remote media redirect failed");
|
|
1045
1053
|
return {
|
|
1046
|
-
data: new Uint8Array(
|
|
1054
|
+
data: new Uint8Array(res.body),
|
|
1047
1055
|
contentType: res.headers.get("content-type"),
|
|
1048
1056
|
filename: filenameFromDisposition(res.headers.get("content-disposition") ?? void 0)
|
|
1049
1057
|
};
|
package/dist/homeserver.d.ts
CHANGED
|
@@ -3,12 +3,13 @@ import { SigningKey } from "./core/crypto.js";
|
|
|
3
3
|
import { PolicyServers } from "./core/policy.js";
|
|
4
4
|
import { ThreepidOptions, ThreepidService } from "./core/threepid.js";
|
|
5
5
|
import { IdentityServers } from "./core/identity.js";
|
|
6
|
+
import { M } from "./core/m.js";
|
|
6
7
|
import { AppServiceRegistration, AppServiceService } from "./core/appservices.js";
|
|
7
8
|
import { AuthService } from "./core/auth.js";
|
|
9
|
+
import { DatabaseAdapter } from "./storage/types.js";
|
|
8
10
|
import { KeyService } from "./core/keys.js";
|
|
9
11
|
import { ClusterAdapter, StreamManager } from "./core/cluster.js";
|
|
10
12
|
import { Notifier } from "./core/notifier.js";
|
|
11
|
-
import { DatabaseAdapter } from "./storage/types.js";
|
|
12
13
|
import { RoomService } from "./core/rooms.js";
|
|
13
14
|
import { TypingService } from "./core/typing.js";
|
|
14
15
|
import { PushService } from "./core/push.js";
|
|
@@ -25,6 +26,7 @@ import { SettingsService } from "./core/settings.js";
|
|
|
25
26
|
import { AdminRoomOptions, AdminRoomService } from "./core/adminroom.js";
|
|
26
27
|
import { ResolvedUserAuth, UserAuthAdapter } from "./adapters/auth.js";
|
|
27
28
|
import { MediaAdapter, Thumbnailer } from "./adapters/media.js";
|
|
29
|
+
import { AddressPolicy } from "./core/outbound.js";
|
|
28
30
|
import { EventEmitter } from "node:events";
|
|
29
31
|
//#region src/homeserver.d.ts
|
|
30
32
|
export interface Logger {
|
|
@@ -52,7 +54,7 @@ export interface HomeserverOptions {
|
|
|
52
54
|
/** Contacts and page served at `/.well-known/matrix/support` (e.g. to appeal a locked account). */
|
|
53
55
|
support?: {
|
|
54
56
|
contacts?: {
|
|
55
|
-
role:
|
|
57
|
+
role: typeof M.ROLE.ADMIN | typeof M.ROLE.SECURITY | (string & {});
|
|
56
58
|
matrix_id?: string;
|
|
57
59
|
email_address?: string;
|
|
58
60
|
}[];
|
|
@@ -81,6 +83,11 @@ export interface HomeserverOptions {
|
|
|
81
83
|
accessTokenLifetimeMs?: number;
|
|
82
84
|
/** Maximum `/sync` long-poll duration in ms. Default: 30000. */
|
|
83
85
|
maxSyncTimeoutMs?: number;
|
|
86
|
+
/**
|
|
87
|
+
* How many `/sync` responses one process builds at once (default 4). Requests woken together
|
|
88
|
+
* wait in the process instead of queueing their database reads ahead of everything else.
|
|
89
|
+
*/
|
|
90
|
+
maxConcurrentSyncs?: number;
|
|
84
91
|
/**
|
|
85
92
|
* Enables `/preview_url`. Off by default because it makes the server fetch arbitrary URLs.
|
|
86
93
|
* Private/internal addresses stay blocked unless `allowPrivateAddresses` is set.
|
|
@@ -91,6 +98,13 @@ export interface HomeserverOptions {
|
|
|
91
98
|
logger?: Logger;
|
|
92
99
|
/** Custom `fetch` for outgoing HTTP requests. */
|
|
93
100
|
fetch?: typeof globalThis.fetch;
|
|
101
|
+
/**
|
|
102
|
+
* Let requests to addresses that users or remote servers choose (federation, push gateways,
|
|
103
|
+
* identity servers, URL previews, remote media) reach private addresses: localhost, LAN and
|
|
104
|
+
* other non-public ranges. `true` allows all of them, a list allows those ranges only (e.g.
|
|
105
|
+
* `["127.0.0.1/32"]` for a push gateway on the same machine). Default: none.
|
|
106
|
+
*/
|
|
107
|
+
allowPrivateAddresses?: boolean | string[];
|
|
94
108
|
/**
|
|
95
109
|
* Rate limiting. `false` disables it; otherwise per-action overrides and an optional
|
|
96
110
|
* shared limiter (defaults to an in-memory token bucket). Admins are never limited.
|
|
@@ -101,7 +115,7 @@ export interface HomeserverOptions {
|
|
|
101
115
|
sso?: SsoOptions;
|
|
102
116
|
/**
|
|
103
117
|
* Coordination between instances. The default (`localCluster()`) supports one process;
|
|
104
|
-
* use `postgresCluster()`
|
|
118
|
+
* use `postgresCluster()` to scale horizontally.
|
|
105
119
|
*/
|
|
106
120
|
cluster?: ClusterAdapter;
|
|
107
121
|
/**
|
|
@@ -150,6 +164,7 @@ export interface ResolvedConfig {
|
|
|
150
164
|
defaultRoomVersion: string;
|
|
151
165
|
accessTokenLifetimeMs: number | null;
|
|
152
166
|
maxSyncTimeoutMs: number;
|
|
167
|
+
maxConcurrentSyncs: number;
|
|
153
168
|
urlPreviews: {
|
|
154
169
|
allowPrivateAddresses: boolean;
|
|
155
170
|
} | null;
|
|
@@ -188,8 +203,12 @@ export declare class Homeserver extends EventEmitter {
|
|
|
188
203
|
readonly policyServers: PolicyServers;
|
|
189
204
|
readonly threepids: ThreepidService;
|
|
190
205
|
readonly identityServers: IdentityServers;
|
|
191
|
-
/** `fetch`
|
|
206
|
+
/** `fetch` for requests to addresses the operator configured (OAuth, SSO, application services). */
|
|
192
207
|
readonly fetch: typeof globalThis.fetch;
|
|
208
|
+
/** Which addresses requests to user- or remote-chosen destinations may reach (see `allowPrivateAddresses`). */
|
|
209
|
+
readonly addressPolicy: AddressPolicy;
|
|
210
|
+
/** `fetch` for user- or remote-chosen URLs: refuses private addresses unless allowed, never follows redirects. */
|
|
211
|
+
readonly publicFetch: typeof globalThis.fetch;
|
|
193
212
|
/** Resolves once storage is migrated and the server is ready to serve requests. */
|
|
194
213
|
readonly ready: Promise<void>;
|
|
195
214
|
private _signingKey;
|