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.
- package/LICENSE +75 -0
- package/README.md +479 -2
- package/dist/adapters/auth.d.ts +50 -0
- package/dist/adapters/auth.js +18 -0
- package/dist/adapters/cluster.d.ts +55 -0
- package/dist/adapters/cluster.js +249 -0
- package/dist/adapters/media.d.ts +50 -0
- package/dist/adapters/media.js +63 -0
- package/dist/adapters/s3.d.ts +33 -0
- package/dist/adapters/s3.js +95 -0
- package/dist/api/account.js +594 -0
- package/dist/api/admin.js +267 -0
- package/dist/api/appservice.js +45 -0
- package/dist/api/backup.js +280 -0
- package/dist/api/federation.js +826 -0
- package/dist/api/filters.js +88 -0
- package/dist/api/index.js +47 -0
- package/dist/api/legacy.js +183 -0
- package/dist/api/media.js +271 -0
- package/dist/api/misc.js +95 -0
- package/dist/api/preview.js +201 -0
- package/dist/api/profile.js +176 -0
- package/dist/api/push.js +202 -0
- package/dist/api/relationships.js +270 -0
- package/dist/api/rooms.js +1487 -0
- package/dist/api/rtc.d.ts +23 -0
- package/dist/api/rtc.js +66 -0
- package/dist/api/search.js +161 -0
- package/dist/api/slidingsync.js +474 -0
- package/dist/api/spaces.js +177 -0
- package/dist/api/sync.js +609 -0
- package/dist/api/threadsubs.js +140 -0
- package/dist/api/threepid.js +155 -0
- package/dist/api/uia.js +127 -0
- package/dist/api/user.js +329 -0
- package/dist/core/admin.d.ts +84 -0
- package/dist/core/admin.js +308 -0
- package/dist/core/adminroom.d.ts +49 -0
- package/dist/core/adminroom.js +483 -0
- package/dist/core/appservices.d.ts +74 -0
- package/dist/core/appservices.js +369 -0
- package/dist/core/audit.d.ts +50 -0
- package/dist/core/audit.js +86 -0
- package/dist/core/auth.d.ts +48 -0
- package/dist/core/auth.js +237 -0
- package/dist/core/authrules.d.ts +4 -0
- package/dist/core/authrules.js +348 -0
- package/dist/core/cluster.d.ts +66 -0
- package/dist/core/cluster.js +191 -0
- package/dist/core/crypto.d.ts +10 -0
- package/dist/core/crypto.js +100 -0
- package/dist/core/delayed.d.ts +39 -0
- package/dist/core/delayed.js +160 -0
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +45 -0
- package/dist/core/events.d.ts +32 -0
- package/dist/core/events.js +186 -0
- package/dist/core/glob.js +66 -0
- package/dist/core/identity.d.ts +24 -0
- package/dist/core/identity.js +96 -0
- package/dist/core/ids.js +36 -0
- package/dist/core/invitefilter.js +36 -0
- package/dist/core/json.js +16 -0
- package/dist/core/keys.d.ts +36 -0
- package/dist/core/keys.js +450 -0
- package/dist/core/m.d.ts +209 -0
- package/dist/core/m.js +182 -0
- package/dist/core/notifier.d.ts +17 -0
- package/dist/core/notifier.js +54 -0
- package/dist/core/oauth.d.ts +38 -0
- package/dist/core/oauth.js +108 -0
- package/dist/core/partialstate.d.ts +32 -0
- package/dist/core/partialstate.js +198 -0
- package/dist/core/phone.js +268 -0
- package/dist/core/policy.d.ts +29 -0
- package/dist/core/policy.js +97 -0
- package/dist/core/profile.js +39 -0
- package/dist/core/push.d.ts +46 -0
- package/dist/core/push.js +307 -0
- package/dist/core/pushrules.js +229 -0
- package/dist/core/ratelimit.d.ts +23 -0
- package/dist/core/ratelimit.js +77 -0
- package/dist/core/rooms.d.ts +215 -0
- package/dist/core/rooms.js +1731 -0
- package/dist/core/settings.d.ts +11 -0
- package/dist/core/settings.js +29 -0
- package/dist/core/sso.d.ts +46 -0
- package/dist/core/sso.js +165 -0
- package/dist/core/stateres.d.ts +4 -0
- package/dist/core/stateres.js +209 -0
- package/dist/core/threepid.d.ts +80 -0
- package/dist/core/threepid.js +275 -0
- package/dist/core/typing.d.ts +16 -0
- package/dist/core/typing.js +60 -0
- package/dist/federation/client.d.ts +51 -0
- package/dist/federation/client.js +279 -0
- package/dist/federation/index.d.ts +153 -0
- package/dist/federation/index.js +1059 -0
- package/dist/federation/keyring.d.ts +26 -0
- package/dist/federation/keyring.js +122 -0
- package/dist/federation/sender.d.ts +26 -0
- package/dist/federation/sender.js +164 -0
- package/dist/homeserver.d.ts +224 -0
- package/dist/homeserver.js +302 -0
- package/dist/http/adapters.d.ts +40 -0
- package/dist/http/adapters.js +130 -0
- package/dist/http/handler.d.ts +8 -0
- package/dist/http/handler.js +181 -0
- package/dist/http/input.js +24 -0
- package/dist/http/procedure.js +25 -0
- package/dist/http/router.d.ts +43 -0
- package/dist/http/router.js +56 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +15 -0
- package/dist/storage/memory.d.ts +5 -0
- package/dist/storage/memory.js +122 -0
- package/dist/storage/schema.d.ts +4 -0
- package/dist/storage/schema.js +664 -0
- package/dist/storage/sql.d.ts +72 -0
- package/dist/storage/sql.js +274 -0
- package/dist/storage/types.d.ts +55 -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 +65 -4
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Homeserver } from "../homeserver.js";
|
|
2
|
+
//#region src/core/settings.d.ts
|
|
3
|
+
/** Runtime server settings changed by admins (stored in the database, shared across instances). */
|
|
4
|
+
export declare class SettingsService {
|
|
5
|
+
private hs;
|
|
6
|
+
private cache;
|
|
7
|
+
constructor(hs: Homeserver);
|
|
8
|
+
get<T>(key: string): Promise<T | null>;
|
|
9
|
+
set(key: string, value: unknown): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/core/settings.ts
|
|
2
|
+
/** Runtime server settings changed by admins (stored in the database, shared across instances). */
|
|
3
|
+
var SettingsService = class {
|
|
4
|
+
hs;
|
|
5
|
+
cache = /* @__PURE__ */ new Map();
|
|
6
|
+
constructor(hs) {
|
|
7
|
+
this.hs = hs;
|
|
8
|
+
hs.cluster.subscribe("settings", (msg) => this.cache.delete(msg?.key));
|
|
9
|
+
}
|
|
10
|
+
async get(key) {
|
|
11
|
+
if (this.cache.has(key)) return this.cache.get(key);
|
|
12
|
+
const value = (await this.hs.db.findOne("server_settings", [{
|
|
13
|
+
field: "key",
|
|
14
|
+
value: key
|
|
15
|
+
}]))?.value ?? null;
|
|
16
|
+
this.cache.set(key, value);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
async set(key, value) {
|
|
20
|
+
await this.hs.db.upsert("server_settings", {
|
|
21
|
+
key,
|
|
22
|
+
value
|
|
23
|
+
});
|
|
24
|
+
this.cache.set(key, value);
|
|
25
|
+
await this.hs.cluster.publish("settings", { key });
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { SettingsService };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Homeserver } from "../homeserver.js";
|
|
2
|
+
//#region src/core/sso.d.ts
|
|
3
|
+
/** An upstream OpenID Connect provider for `m.login.sso`. */
|
|
4
|
+
export interface SsoProvider {
|
|
5
|
+
/** Stable identifier, used in URLs and stored with linked accounts. */
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
issuer: string;
|
|
9
|
+
clientId: string;
|
|
10
|
+
clientSecret?: string;
|
|
11
|
+
/** Default: ["openid", "profile", "email"]. */
|
|
12
|
+
scopes?: string[];
|
|
13
|
+
icon?: string;
|
|
14
|
+
brand?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Map ID token / userinfo claims to a Matrix account. Return a localpart and optional
|
|
17
|
+
* display name, or null to refuse login. Default: sanitised `preferred_username`, then `sub`.
|
|
18
|
+
*/
|
|
19
|
+
mapUser?: (claims: Record<string, any>) => {
|
|
20
|
+
localpart: string;
|
|
21
|
+
displayname?: string;
|
|
22
|
+
} | null | Promise<{
|
|
23
|
+
localpart: string;
|
|
24
|
+
displayname?: string;
|
|
25
|
+
} | null>;
|
|
26
|
+
/** Create accounts for unknown users. Default: true. */
|
|
27
|
+
allowRegistration?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface SsoOptions {
|
|
30
|
+
providers: SsoProvider[];
|
|
31
|
+
}
|
|
32
|
+
/** Upstream OIDC login (authorization code + PKCE) issuing Matrix login tokens. */
|
|
33
|
+
export declare class SsoService {
|
|
34
|
+
private hs;
|
|
35
|
+
readonly providers: SsoProvider[];
|
|
36
|
+
private discovery;
|
|
37
|
+
constructor(hs: Homeserver, options: SsoOptions);
|
|
38
|
+
private callbackUrl;
|
|
39
|
+
private discover;
|
|
40
|
+
/** URL to send the user's browser to. */
|
|
41
|
+
startLogin(providerId: string | null, redirectUrl: string): Promise<string>;
|
|
42
|
+
/** Handle the provider's redirect; returns the client URL carrying a login token. */
|
|
43
|
+
finishLogin(providerId: string, params: URLSearchParams): Promise<string>;
|
|
44
|
+
private resolveUser;
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
package/dist/core/sso.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { randomToken, urlSafeBase64 } from "./crypto.js";
|
|
2
|
+
import { errors } from "./errors.js";
|
|
3
|
+
import { LOCALPART_RE } from "./ids.js";
|
|
4
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
5
|
+
//#region src/core/sso.ts
|
|
6
|
+
function sanitizeLocalpart(raw) {
|
|
7
|
+
return raw.toLowerCase().replace(/[^a-z0-9._=\-/+]/g, "_").slice(0, 200);
|
|
8
|
+
}
|
|
9
|
+
function decodeJwtPayload(jwt) {
|
|
10
|
+
const part = jwt.split(".")[1];
|
|
11
|
+
if (!part) throw new Error("Malformed ID token");
|
|
12
|
+
return JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
|
|
13
|
+
}
|
|
14
|
+
/** Upstream OIDC login (authorization code + PKCE) issuing Matrix login tokens. */
|
|
15
|
+
var SsoService = class {
|
|
16
|
+
hs;
|
|
17
|
+
providers;
|
|
18
|
+
discovery = /* @__PURE__ */ new Map();
|
|
19
|
+
constructor(hs, options) {
|
|
20
|
+
this.hs = hs;
|
|
21
|
+
this.providers = options.providers;
|
|
22
|
+
}
|
|
23
|
+
callbackUrl(provider) {
|
|
24
|
+
const base = this.hs.config.publicBaseUrl;
|
|
25
|
+
if (!base) throw new Error("publicBaseUrl is required for SSO");
|
|
26
|
+
return `${base.replace(/\/$/, "")}/_matrix/client/v3/login/sso/callback/${encodeURIComponent(provider.id)}`;
|
|
27
|
+
}
|
|
28
|
+
async discover(provider) {
|
|
29
|
+
const cached = this.discovery.get(provider.id);
|
|
30
|
+
if (cached) return cached;
|
|
31
|
+
const res = await this.hs.fetch(`${provider.issuer.replace(/\/$/, "")}/.well-known/openid-configuration`, { signal: AbortSignal.timeout(1e4) });
|
|
32
|
+
if (!res.ok) throw new Error(`OIDC discovery for ${provider.id} failed with ${res.status}`);
|
|
33
|
+
const meta = await res.json();
|
|
34
|
+
this.discovery.set(provider.id, meta);
|
|
35
|
+
return meta;
|
|
36
|
+
}
|
|
37
|
+
/** URL to send the user's browser to. */
|
|
38
|
+
async startLogin(providerId, redirectUrl) {
|
|
39
|
+
const provider = providerId ? this.providers.find((p) => p.id === providerId) : this.providers[0];
|
|
40
|
+
if (!provider) throw errors.notFound("Unknown identity provider");
|
|
41
|
+
const meta = await this.discover(provider);
|
|
42
|
+
const state = randomToken(24);
|
|
43
|
+
const verifier = urlSafeBase64(randomBytes(32));
|
|
44
|
+
const nonce = randomToken(16);
|
|
45
|
+
await this.hs.cluster.set(`sso:${state}`, {
|
|
46
|
+
providerId: provider.id,
|
|
47
|
+
redirectUrl,
|
|
48
|
+
verifier,
|
|
49
|
+
nonce,
|
|
50
|
+
expires: Date.now() + 6e5
|
|
51
|
+
}, 6e5);
|
|
52
|
+
const url = new URL(meta.authorization_endpoint);
|
|
53
|
+
url.searchParams.set("response_type", "code");
|
|
54
|
+
url.searchParams.set("client_id", provider.clientId);
|
|
55
|
+
url.searchParams.set("redirect_uri", this.callbackUrl(provider));
|
|
56
|
+
url.searchParams.set("scope", (provider.scopes ?? [
|
|
57
|
+
"openid",
|
|
58
|
+
"profile",
|
|
59
|
+
"email"
|
|
60
|
+
]).join(" "));
|
|
61
|
+
url.searchParams.set("state", state);
|
|
62
|
+
url.searchParams.set("nonce", nonce);
|
|
63
|
+
url.searchParams.set("code_challenge", urlSafeBase64(createHash("sha256").update(verifier).digest()));
|
|
64
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
65
|
+
return url.toString();
|
|
66
|
+
}
|
|
67
|
+
/** Handle the provider's redirect; returns the client URL carrying a login token. */
|
|
68
|
+
async finishLogin(providerId, params) {
|
|
69
|
+
const state = params.get("state") ?? "";
|
|
70
|
+
const pending = await this.hs.cluster.get(`sso:${state}`);
|
|
71
|
+
await this.hs.cluster.delete(`sso:${state}`);
|
|
72
|
+
if (!pending || pending.expires < Date.now() || pending.providerId !== providerId) throw errors.forbidden("Unknown or expired login attempt");
|
|
73
|
+
if (params.get("error")) throw errors.forbidden(`Login failed: ${params.get("error")}`);
|
|
74
|
+
const code = params.get("code");
|
|
75
|
+
if (!code) throw errors.missingParam("Missing code");
|
|
76
|
+
const provider = this.providers.find((p) => p.id === providerId);
|
|
77
|
+
const meta = await this.discover(provider);
|
|
78
|
+
const body = new URLSearchParams({
|
|
79
|
+
grant_type: "authorization_code",
|
|
80
|
+
code,
|
|
81
|
+
redirect_uri: this.callbackUrl(provider),
|
|
82
|
+
code_verifier: pending.verifier,
|
|
83
|
+
client_id: provider.clientId
|
|
84
|
+
});
|
|
85
|
+
const headers = {
|
|
86
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
87
|
+
accept: "application/json"
|
|
88
|
+
};
|
|
89
|
+
if (provider.clientSecret) headers.authorization = `Basic ${Buffer.from(`${encodeURIComponent(provider.clientId)}:${encodeURIComponent(provider.clientSecret)}`).toString("base64")}`;
|
|
90
|
+
const res = await this.hs.fetch(meta.token_endpoint, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers,
|
|
93
|
+
body,
|
|
94
|
+
signal: AbortSignal.timeout(1e4)
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok) throw errors.forbidden("Code exchange failed");
|
|
97
|
+
const tokens = await res.json();
|
|
98
|
+
let claims = {};
|
|
99
|
+
if (typeof tokens.id_token === "string") {
|
|
100
|
+
claims = decodeJwtPayload(tokens.id_token);
|
|
101
|
+
const aud = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
|
|
102
|
+
if (claims.iss !== meta.issuer || !aud.includes(provider.clientId)) throw errors.forbidden("ID token issuer or audience mismatch");
|
|
103
|
+
if (claims.nonce !== pending.nonce) throw errors.forbidden("ID token nonce mismatch");
|
|
104
|
+
if (typeof claims.exp === "number" && claims.exp * 1e3 < Date.now()) throw errors.forbidden("ID token expired");
|
|
105
|
+
}
|
|
106
|
+
if (meta.userinfo_endpoint && typeof tokens.access_token === "string") {
|
|
107
|
+
const info = await this.hs.fetch(meta.userinfo_endpoint, {
|
|
108
|
+
headers: { authorization: `Bearer ${tokens.access_token}` },
|
|
109
|
+
signal: AbortSignal.timeout(1e4)
|
|
110
|
+
});
|
|
111
|
+
if (info.ok) {
|
|
112
|
+
const userinfo = await info.json();
|
|
113
|
+
if (claims.sub && userinfo.sub !== claims.sub) throw errors.forbidden("Userinfo subject mismatch");
|
|
114
|
+
claims = {
|
|
115
|
+
...userinfo,
|
|
116
|
+
...claims
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (typeof claims.sub !== "string") throw errors.forbidden("Provider did not return a subject");
|
|
121
|
+
const userId = await this.resolveUser(provider, claims);
|
|
122
|
+
const loginToken = await this.hs.auth.createLoginToken(userId);
|
|
123
|
+
await this.hs.audit.record({
|
|
124
|
+
action: "sso_login",
|
|
125
|
+
actor: userId,
|
|
126
|
+
details: {
|
|
127
|
+
idp: provider.id,
|
|
128
|
+
subject: claims.sub
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
const redirect = new URL(pending.redirectUrl);
|
|
132
|
+
redirect.searchParams.set("loginToken", loginToken);
|
|
133
|
+
return redirect.toString();
|
|
134
|
+
}
|
|
135
|
+
async resolveUser(provider, claims) {
|
|
136
|
+
const link = await this.hs.db.findOne("sso_identities", [{
|
|
137
|
+
field: "idp_id",
|
|
138
|
+
value: provider.id
|
|
139
|
+
}, {
|
|
140
|
+
field: "subject",
|
|
141
|
+
value: claims.sub
|
|
142
|
+
}]);
|
|
143
|
+
if (link) return link.user_id;
|
|
144
|
+
if (provider.allowRegistration === false) throw errors.forbidden("No account is linked to this identity");
|
|
145
|
+
const mapped = provider.mapUser ? await provider.mapUser(claims) : {
|
|
146
|
+
localpart: sanitizeLocalpart(claims.preferred_username ?? claims.sub),
|
|
147
|
+
displayname: claims.name
|
|
148
|
+
};
|
|
149
|
+
if (!mapped || !LOCALPART_RE.test(mapped.localpart)) throw errors.forbidden("Could not map identity to a Matrix user");
|
|
150
|
+
let localpart = mapped.localpart;
|
|
151
|
+
for (let i = 1; await this.hs.db.findOne("users", [{
|
|
152
|
+
field: "user_id",
|
|
153
|
+
value: `@${localpart}:${this.hs.serverName}`
|
|
154
|
+
}]); i++) localpart = `${mapped.localpart}${i}`;
|
|
155
|
+
const userId = await this.hs.admin.createUser(localpart, { displayname: mapped.displayname });
|
|
156
|
+
await this.hs.db.create("sso_identities", {
|
|
157
|
+
idp_id: provider.id,
|
|
158
|
+
subject: claims.sub,
|
|
159
|
+
user_id: userId
|
|
160
|
+
});
|
|
161
|
+
return userId;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
//#endregion
|
|
165
|
+
export { SsoService };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { M } from "./m.js";
|
|
2
|
+
import { eventIdOf, roomVersionFeatures, stateKey } from "./events.js";
|
|
3
|
+
import { authEventKeys, checkAuth, powerLevels, userLevel } from "./authrules.js";
|
|
4
|
+
//#region src/core/stateres.ts
|
|
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;
|
|
6
|
+
const authIds = (ev) => (ev.auth_events ?? []).map(eventIdOf);
|
|
7
|
+
var EventCache = class {
|
|
8
|
+
events = /* @__PURE__ */ new Map();
|
|
9
|
+
source;
|
|
10
|
+
constructor(source) {
|
|
11
|
+
this.source = source;
|
|
12
|
+
}
|
|
13
|
+
async load(ids) {
|
|
14
|
+
const missing = [...ids].filter((id) => !this.events.has(id));
|
|
15
|
+
if (!missing.length) return;
|
|
16
|
+
for (const [id, ev] of await this.source(missing)) this.events.set(id, ev);
|
|
17
|
+
}
|
|
18
|
+
get(id) {
|
|
19
|
+
return this.events.get(id);
|
|
20
|
+
}
|
|
21
|
+
/** All auth ancestors of the given events (not including them). */
|
|
22
|
+
async authChain(ids) {
|
|
23
|
+
const chain = /* @__PURE__ */ new Set();
|
|
24
|
+
let frontier = [...ids];
|
|
25
|
+
while (frontier.length) {
|
|
26
|
+
await this.load(frontier);
|
|
27
|
+
const next = [];
|
|
28
|
+
for (const id of frontier) {
|
|
29
|
+
const ev = this.get(id);
|
|
30
|
+
if (!ev) continue;
|
|
31
|
+
for (const a of authIds(ev)) if (!chain.has(a)) {
|
|
32
|
+
chain.add(a);
|
|
33
|
+
next.push(a);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
frontier = next;
|
|
37
|
+
}
|
|
38
|
+
return chain;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* State resolution v2 (and v2.1 for room version 12).
|
|
43
|
+
* `createEvent` is required for v12, where the create event is not in auth_events.
|
|
44
|
+
*/
|
|
45
|
+
async function resolveState(stateSets, roomVersion, source, createEvent) {
|
|
46
|
+
if (stateSets.length === 0) return {};
|
|
47
|
+
if (stateSets.length === 1) return { ...stateSets[0] };
|
|
48
|
+
const v21 = roomVersionFeatures(roomVersion).v12;
|
|
49
|
+
const cache = new EventCache(source);
|
|
50
|
+
const keys = new Set(stateSets.flatMap((s) => Object.keys(s)));
|
|
51
|
+
const unconflicted = {};
|
|
52
|
+
const conflicted = /* @__PURE__ */ new Set();
|
|
53
|
+
for (const key of keys) {
|
|
54
|
+
const values = stateSets.map((s) => s[key]);
|
|
55
|
+
if (values.every((v) => v !== void 0 && v === values[0])) unconflicted[key] = values[0];
|
|
56
|
+
else for (const v of values) if (v !== void 0) conflicted.add(v);
|
|
57
|
+
}
|
|
58
|
+
if (conflicted.size === 0) return unconflicted;
|
|
59
|
+
const chains = await Promise.all(stateSets.map((s) => cache.authChain(Object.values(s))));
|
|
60
|
+
const authDifference = [...new Set(chains.flatMap((c) => [...c]))].filter((id) => !chains.every((c) => c.has(id)));
|
|
61
|
+
const full = /* @__PURE__ */ new Set([...conflicted, ...authDifference]);
|
|
62
|
+
if (v21) for (const id of await conflictedSubgraph(cache, conflicted)) full.add(id);
|
|
63
|
+
await cache.load(full);
|
|
64
|
+
for (const id of [...full]) if (!cache.get(id)) full.delete(id);
|
|
65
|
+
const powerEvents = [...full].filter((id) => isPowerEvent(cache.get(id)));
|
|
66
|
+
const control = new Set(powerEvents);
|
|
67
|
+
for (const id of await cache.authChain(powerEvents)) if (full.has(id)) control.add(id);
|
|
68
|
+
let resolved = await iterativeAuthChecks(cache, await reverseTopologicalPowerSort(cache, control, roomVersion, createEvent), v21 ? {} : { ...unconflicted }, roomVersion, createEvent);
|
|
69
|
+
const remaining = [...full].filter((id) => !control.has(id));
|
|
70
|
+
const mainline = await mainlineOf(cache, resolved[stateKey(M.ROOM.POWER_LEVELS, "")]);
|
|
71
|
+
const positions = /* @__PURE__ */ new Map();
|
|
72
|
+
for (const id of remaining) positions.set(id, await mainlinePosition(cache, id, mainline));
|
|
73
|
+
remaining.sort((a, b) => {
|
|
74
|
+
const ea = cache.get(a);
|
|
75
|
+
const eb = cache.get(b);
|
|
76
|
+
return positions.get(a) - positions.get(b) || ea.origin_server_ts - eb.origin_server_ts || (a < b ? -1 : a > b ? 1 : 0);
|
|
77
|
+
});
|
|
78
|
+
resolved = await iterativeAuthChecks(cache, remaining, resolved, roomVersion, createEvent);
|
|
79
|
+
return {
|
|
80
|
+
...resolved,
|
|
81
|
+
...unconflicted
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** v2.1: events on auth paths between two conflicted events. */
|
|
85
|
+
async function conflictedSubgraph(cache, conflicted) {
|
|
86
|
+
const ancestors = await cache.authChain(conflicted);
|
|
87
|
+
await cache.load(ancestors);
|
|
88
|
+
const memo = /* @__PURE__ */ new Map();
|
|
89
|
+
const reachesConflicted = (id, seen = /* @__PURE__ */ new Set()) => {
|
|
90
|
+
const cached = memo.get(id);
|
|
91
|
+
if (cached !== void 0) return cached;
|
|
92
|
+
if (seen.has(id)) return false;
|
|
93
|
+
seen.add(id);
|
|
94
|
+
const ev = cache.get(id);
|
|
95
|
+
const result = !!ev && authIds(ev).some((a) => conflicted.has(a) || reachesConflicted(a, seen));
|
|
96
|
+
memo.set(id, result);
|
|
97
|
+
return result;
|
|
98
|
+
};
|
|
99
|
+
const out = /* @__PURE__ */ new Set();
|
|
100
|
+
for (const id of ancestors) if (!conflicted.has(id) && reachesConflicted(id)) out.add(id);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/** Power level of an event's sender according to its own auth events. */
|
|
104
|
+
async function senderPowerLevel(cache, ev, roomVersion, createEvent) {
|
|
105
|
+
await cache.load(authIds(ev));
|
|
106
|
+
const state = /* @__PURE__ */ new Map();
|
|
107
|
+
for (const id of authIds(ev)) {
|
|
108
|
+
const a = cache.get(id);
|
|
109
|
+
if (a && a.state_key !== void 0) state.set(stateKey(a.type, a.state_key), a);
|
|
110
|
+
}
|
|
111
|
+
if (createEvent) state.set(stateKey(M.ROOM.CREATE, ""), createEvent);
|
|
112
|
+
const level = userLevel(powerLevels(state, roomVersion), ev.sender);
|
|
113
|
+
return Number.isFinite(level) ? level : Number.MAX_SAFE_INTEGER;
|
|
114
|
+
}
|
|
115
|
+
async function reverseTopologicalPowerSort(cache, ids, roomVersion, createEvent) {
|
|
116
|
+
const levels = /* @__PURE__ */ new Map();
|
|
117
|
+
for (const id of ids) levels.set(id, await senderPowerLevel(cache, cache.get(id), roomVersion, createEvent));
|
|
118
|
+
const pending = /* @__PURE__ */ new Map();
|
|
119
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
120
|
+
for (const id of ids) {
|
|
121
|
+
const deps = new Set(authIds(cache.get(id)).filter((a) => ids.has(a)));
|
|
122
|
+
pending.set(id, deps);
|
|
123
|
+
for (const d of deps) (dependents.get(d) ?? dependents.set(d, []).get(d)).push(id);
|
|
124
|
+
}
|
|
125
|
+
const compare = (a, b) => {
|
|
126
|
+
const ea = cache.get(a);
|
|
127
|
+
const eb = cache.get(b);
|
|
128
|
+
return levels.get(b) - levels.get(a) || ea.origin_server_ts - eb.origin_server_ts || (a < b ? -1 : a > b ? 1 : 0);
|
|
129
|
+
};
|
|
130
|
+
const ready = [...ids].filter((id) => pending.get(id).size === 0).sort(compare);
|
|
131
|
+
const out = [];
|
|
132
|
+
while (ready.length) {
|
|
133
|
+
const id = ready.shift();
|
|
134
|
+
out.push(id);
|
|
135
|
+
for (const dep of dependents.get(id) ?? []) {
|
|
136
|
+
const p = pending.get(dep);
|
|
137
|
+
p.delete(id);
|
|
138
|
+
if (p.size === 0) {
|
|
139
|
+
ready.push(dep);
|
|
140
|
+
ready.sort(compare);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
async function iterativeAuthChecks(cache, ids, base, roomVersion, createEvent) {
|
|
147
|
+
const state = { ...base };
|
|
148
|
+
for (const id of ids) {
|
|
149
|
+
const ev = cache.get(id);
|
|
150
|
+
if (!ev || ev.state_key === void 0) continue;
|
|
151
|
+
await cache.load(authIds(ev));
|
|
152
|
+
const authState = /* @__PURE__ */ new Map();
|
|
153
|
+
for (const aid of authIds(ev)) {
|
|
154
|
+
const a = cache.get(aid);
|
|
155
|
+
if (a && a.state_key !== void 0) authState.set(stateKey(a.type, a.state_key), a);
|
|
156
|
+
}
|
|
157
|
+
const keys = authEventKeys(ev, roomVersion);
|
|
158
|
+
await cache.load(keys.map((k) => state[k]).filter(Boolean));
|
|
159
|
+
for (const k of keys) {
|
|
160
|
+
const sid = state[k];
|
|
161
|
+
const sev = sid ? cache.get(sid) : void 0;
|
|
162
|
+
if (sev) authState.set(k, sev);
|
|
163
|
+
}
|
|
164
|
+
if (createEvent) authState.set(stateKey(M.ROOM.CREATE, ""), createEvent);
|
|
165
|
+
try {
|
|
166
|
+
checkAuth(ev, authState, roomVersion);
|
|
167
|
+
state[stateKey(ev.type, ev.state_key)] = id;
|
|
168
|
+
} catch {}
|
|
169
|
+
}
|
|
170
|
+
return state;
|
|
171
|
+
}
|
|
172
|
+
async function mainlineOf(cache, plId) {
|
|
173
|
+
const chain = [];
|
|
174
|
+
let current = plId;
|
|
175
|
+
while (current) {
|
|
176
|
+
chain.push(current);
|
|
177
|
+
await cache.load([current]);
|
|
178
|
+
const ev = cache.get(current);
|
|
179
|
+
if (!ev) break;
|
|
180
|
+
await cache.load(authIds(ev));
|
|
181
|
+
current = authIds(ev).find((a) => {
|
|
182
|
+
const e = cache.get(a);
|
|
183
|
+
return e?.type === M.ROOM.POWER_LEVELS && e.state_key === "";
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const positions = /* @__PURE__ */ new Map();
|
|
187
|
+
chain.reverse().forEach((id, i) => positions.set(id, i + 1));
|
|
188
|
+
return positions;
|
|
189
|
+
}
|
|
190
|
+
async function mainlinePosition(cache, id, mainline) {
|
|
191
|
+
let current = id;
|
|
192
|
+
const seen = /* @__PURE__ */ new Set();
|
|
193
|
+
while (current && !seen.has(current)) {
|
|
194
|
+
seen.add(current);
|
|
195
|
+
const pos = mainline.get(current);
|
|
196
|
+
if (pos !== void 0) return pos;
|
|
197
|
+
await cache.load([current]);
|
|
198
|
+
const ev = cache.get(current);
|
|
199
|
+
if (!ev) return 0;
|
|
200
|
+
await cache.load(authIds(ev));
|
|
201
|
+
current = authIds(ev).find((a) => {
|
|
202
|
+
const e = cache.get(a);
|
|
203
|
+
return e?.type === M.ROOM.POWER_LEVELS && e.state_key === "";
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
export { resolveState };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Homeserver } from "../homeserver.js";
|
|
2
|
+
//#region src/core/threepid.d.ts
|
|
3
|
+
export type Medium = "email" | "msisdn";
|
|
4
|
+
export type ThreepidPurpose = "register" | "password_reset" | "add_threepid";
|
|
5
|
+
export interface ThreepidEmail {
|
|
6
|
+
to: string;
|
|
7
|
+
purpose: ThreepidPurpose;
|
|
8
|
+
subject: string;
|
|
9
|
+
text: string;
|
|
10
|
+
html: string;
|
|
11
|
+
/** Opening this link validates the address. */
|
|
12
|
+
link: string;
|
|
13
|
+
token: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ThreepidSms {
|
|
16
|
+
/** MSISDN (international format, digits only). */
|
|
17
|
+
to: string;
|
|
18
|
+
purpose: ThreepidPurpose;
|
|
19
|
+
text: string;
|
|
20
|
+
/** Short code the user types into the client. */
|
|
21
|
+
token: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ThreepidOptions {
|
|
24
|
+
/** Sends validation emails. Without it, email addresses cannot be added or used to register. */
|
|
25
|
+
sendEmail?: (message: ThreepidEmail) => Promise<void>;
|
|
26
|
+
/** Sends validation SMS messages. Without it, phone numbers cannot be added. */
|
|
27
|
+
sendSms?: (message: ThreepidSms) => Promise<void>;
|
|
28
|
+
/** Registration requires a validated email address. Needs `sendEmail`. */
|
|
29
|
+
requireEmailForRegistration?: boolean;
|
|
30
|
+
/** How long a validation session stays usable, in ms. Default: 24 hours. */
|
|
31
|
+
sessionLifetimeMs?: number;
|
|
32
|
+
/** Scheme used to reach identity servers. Default: "https" ("http" is for tests only). */
|
|
33
|
+
identityServerProtocol?: "https" | "http";
|
|
34
|
+
}
|
|
35
|
+
export interface Threepid {
|
|
36
|
+
medium: Medium;
|
|
37
|
+
address: string;
|
|
38
|
+
}
|
|
39
|
+
export declare class ThreepidService {
|
|
40
|
+
private readonly hs;
|
|
41
|
+
constructor(hs: Homeserver);
|
|
42
|
+
private get options();
|
|
43
|
+
/** Whether this server can validate addresses of `medium` itself. */
|
|
44
|
+
canValidate(medium: Medium): boolean;
|
|
45
|
+
/** Account changes involving 3PIDs are possible at all (the m.3pid_changes capability). */
|
|
46
|
+
get enabled(): boolean;
|
|
47
|
+
userFor(medium: Medium, address: string): Promise<string | null>;
|
|
48
|
+
list(userId: string): Promise<{
|
|
49
|
+
medium: string;
|
|
50
|
+
address: string;
|
|
51
|
+
validated_at: number;
|
|
52
|
+
added_at: number;
|
|
53
|
+
}[]>;
|
|
54
|
+
add(userId: string, threepid: Threepid & {
|
|
55
|
+
validatedTs: number;
|
|
56
|
+
}): Promise<void>;
|
|
57
|
+
remove(userId: string, medium: Medium, address: string): Promise<boolean>;
|
|
58
|
+
/**
|
|
59
|
+
* Unbind a 3PID from the identity servers it was bound on through this server (or `idServer`).
|
|
60
|
+
* Returns "success" if every unbind succeeded, "no-support" otherwise.
|
|
61
|
+
*/
|
|
62
|
+
unbindAll(userId: string, medium: string, address: string, idServer?: string): Promise<"success" | "no-support">;
|
|
63
|
+
/** Remove every 3PID of a (deactivated) account, unbinding them from identity servers. */
|
|
64
|
+
removeAll(userId: string): Promise<"success" | "no-support">;
|
|
65
|
+
/**
|
|
66
|
+
* Start (or continue) validating an address: `POST .../{medium}/requestToken`. Repeating a
|
|
67
|
+
* request with the same `client_secret`, address and `send_attempt` does not send again.
|
|
68
|
+
*/
|
|
69
|
+
requestToken(purpose: ThreepidPurpose, medium: Medium, body: Record<string, any>): Promise<Record<string, unknown>>;
|
|
70
|
+
private tokenResponse;
|
|
71
|
+
private send;
|
|
72
|
+
private expired;
|
|
73
|
+
/** Validate a session with the token the user received. Returns the session's `next_link`. */
|
|
74
|
+
submitToken(sid: string, clientSecret: string, token: string): Promise<string | null>;
|
|
75
|
+
/** The address a client proved ownership of, given its `threepid_creds`. */
|
|
76
|
+
validated(creds: unknown): Promise<(Threepid & {
|
|
77
|
+
validatedTs: number;
|
|
78
|
+
}) | null>;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|