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,186 @@
|
|
|
1
|
+
import { canonicalJson, isObject } from "./json.js";
|
|
2
|
+
import { randomString, sha256, signJson, unpaddedBase64, urlSafeBase64 } from "./crypto.js";
|
|
3
|
+
import { M } from "./m.js";
|
|
4
|
+
//#region src/core/events.ts
|
|
5
|
+
const STABLE_ROOM_VERSIONS = [
|
|
6
|
+
"1",
|
|
7
|
+
"2",
|
|
8
|
+
"3",
|
|
9
|
+
"4",
|
|
10
|
+
"5",
|
|
11
|
+
"6",
|
|
12
|
+
"7",
|
|
13
|
+
"8",
|
|
14
|
+
"9",
|
|
15
|
+
"10",
|
|
16
|
+
"11",
|
|
17
|
+
"12"
|
|
18
|
+
];
|
|
19
|
+
/** Experimental versions: an MSC's changes on top of a stable version. */
|
|
20
|
+
const UNSTABLE_ROOM_VERSIONS = [
|
|
21
|
+
"org.matrix.msc3757.10",
|
|
22
|
+
"org.matrix.msc3757.11",
|
|
23
|
+
"org.matrix.msc3389.10"
|
|
24
|
+
];
|
|
25
|
+
const SUPPORTED_ROOM_VERSIONS = [...STABLE_ROOM_VERSIONS, ...UNSTABLE_ROOM_VERSIONS];
|
|
26
|
+
/** The stable version an (experimental) room version is based on, e.g. "org.matrix.msc3757.10" → 10. */
|
|
27
|
+
const versionNum = (v) => {
|
|
28
|
+
let start = v.length;
|
|
29
|
+
while (start > 0 && v.charCodeAt(start - 1) >= 48 && v.charCodeAt(start - 1) <= 57) start--;
|
|
30
|
+
return Number.parseInt(start < v.length ? v.slice(start) : v, 10);
|
|
31
|
+
};
|
|
32
|
+
const roomVersionFeatures = (version) => {
|
|
33
|
+
const n = versionNum(version);
|
|
34
|
+
return {
|
|
35
|
+
/** v1/v2 use server-generated event IDs and `[id, hashes]` tuples in prev/auth events. */
|
|
36
|
+
legacyEventIds: n <= 2,
|
|
37
|
+
urlSafeEventIds: n >= 4,
|
|
38
|
+
knock: n >= 7,
|
|
39
|
+
restricted: n >= 8,
|
|
40
|
+
knockRestricted: n >= 10,
|
|
41
|
+
integerPowerLevels: n >= 10,
|
|
42
|
+
/** v11: creator is the sender of the create event; `redacts` moves into content. */
|
|
43
|
+
v11: n >= 11,
|
|
44
|
+
/**
|
|
45
|
+
* v12: room ID is the create event's reference hash, the create event has no room_id and is
|
|
46
|
+
* omitted from auth_events, and room creators have unlimited power.
|
|
47
|
+
*/
|
|
48
|
+
v12: n >= 12,
|
|
49
|
+
/** MSC3757: state keys "@user:server[_suffix]" are owned by that user. */
|
|
50
|
+
ownedState: version.startsWith("org.matrix.msc3757."),
|
|
51
|
+
/** MSC3389: redactions keep m.relates_to. */
|
|
52
|
+
redactionKeepsRelations: version.startsWith("org.matrix.msc3389.")
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const KEEP_TOP_LEVEL = [
|
|
56
|
+
"event_id",
|
|
57
|
+
"type",
|
|
58
|
+
"room_id",
|
|
59
|
+
"sender",
|
|
60
|
+
"state_key",
|
|
61
|
+
"content",
|
|
62
|
+
"hashes",
|
|
63
|
+
"signatures",
|
|
64
|
+
"depth",
|
|
65
|
+
"prev_events",
|
|
66
|
+
"auth_events",
|
|
67
|
+
"origin_server_ts"
|
|
68
|
+
];
|
|
69
|
+
const KEEP_TOP_LEVEL_LEGACY = [
|
|
70
|
+
"prev_state",
|
|
71
|
+
"origin",
|
|
72
|
+
"membership"
|
|
73
|
+
];
|
|
74
|
+
/** Apply the redaction algorithm for the given room version. */
|
|
75
|
+
function redact(event, roomVersion) {
|
|
76
|
+
const n = versionNum(roomVersion);
|
|
77
|
+
const keep = n >= 11 ? KEEP_TOP_LEVEL : [...KEEP_TOP_LEVEL, ...KEEP_TOP_LEVEL_LEGACY];
|
|
78
|
+
const out = {};
|
|
79
|
+
for (const k of keep) if (k in event) out[k] = event[k];
|
|
80
|
+
const keepRelation = roomVersion.startsWith("org.matrix.msc3389.") && isObject(event.content?.[M.RELATES_TO]) ? event.content[M.RELATES_TO] : void 0;
|
|
81
|
+
const content = isObject(event.content) ? event.content : {};
|
|
82
|
+
const pick = (...keys) => {
|
|
83
|
+
const c = {};
|
|
84
|
+
for (const k of keys) if (k in content) c[k] = content[k];
|
|
85
|
+
return c;
|
|
86
|
+
};
|
|
87
|
+
switch (event.type) {
|
|
88
|
+
case M.ROOM.MEMBER: {
|
|
89
|
+
const c = pick("membership", ...n >= 9 ? ["join_authorised_via_users_server"] : []);
|
|
90
|
+
if (n >= 11 && isObject(content.third_party_invite) && "signed" in content.third_party_invite) c.third_party_invite = { signed: content.third_party_invite.signed };
|
|
91
|
+
out.content = c;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case M.ROOM.CREATE:
|
|
95
|
+
out.content = n >= 11 ? { ...content } : pick("creator");
|
|
96
|
+
break;
|
|
97
|
+
case M.ROOM.JOIN_RULES:
|
|
98
|
+
out.content = pick("join_rule", ...n >= 8 ? ["allow"] : []);
|
|
99
|
+
break;
|
|
100
|
+
case M.ROOM.POWER_LEVELS:
|
|
101
|
+
out.content = pick("ban", "events", "events_default", "kick", "redact", "state_default", "users", "users_default", ...n >= 11 ? ["invite"] : []);
|
|
102
|
+
break;
|
|
103
|
+
case M.ROOM.HISTORY_VISIBILITY:
|
|
104
|
+
out.content = pick("history_visibility");
|
|
105
|
+
break;
|
|
106
|
+
case M.ROOM.ALIASES:
|
|
107
|
+
out.content = n <= 5 ? pick("aliases") : {};
|
|
108
|
+
break;
|
|
109
|
+
case M.ROOM.REDACTION:
|
|
110
|
+
out.content = n >= 11 ? pick("redacts") : {};
|
|
111
|
+
break;
|
|
112
|
+
default: out.content = {};
|
|
113
|
+
}
|
|
114
|
+
if (keepRelation) {
|
|
115
|
+
const rel = {};
|
|
116
|
+
if ("rel_type" in keepRelation) rel.rel_type = keepRelation.rel_type;
|
|
117
|
+
if ("event_id" in keepRelation) rel.event_id = keepRelation.event_id;
|
|
118
|
+
out.content = {
|
|
119
|
+
...out.content,
|
|
120
|
+
[M.RELATES_TO]: rel
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
function computeContentHash(pdu) {
|
|
126
|
+
const { unsigned, signatures, hashes, ...rest } = pdu;
|
|
127
|
+
return unpaddedBase64(sha256(canonicalJson(rest)));
|
|
128
|
+
}
|
|
129
|
+
function computeReferenceHash(pdu, roomVersion) {
|
|
130
|
+
const { signatures, unsigned, event_id, ...rest } = redact(pdu, roomVersion);
|
|
131
|
+
return sha256(canonicalJson(rest));
|
|
132
|
+
}
|
|
133
|
+
/** Finalise a PDU: compute hashes, signatures and the event ID. */
|
|
134
|
+
function finalisePdu(pdu, roomVersion, serverName, key) {
|
|
135
|
+
const features = roomVersionFeatures(roomVersion);
|
|
136
|
+
if (features.legacyEventIds) {
|
|
137
|
+
pdu.event_id = `$${randomString(18)}:${serverName}`;
|
|
138
|
+
pdu.origin = serverName;
|
|
139
|
+
}
|
|
140
|
+
pdu.hashes = { sha256: computeContentHash(pdu) };
|
|
141
|
+
const redacted = redact(pdu, roomVersion);
|
|
142
|
+
signJson(redacted, serverName, key);
|
|
143
|
+
pdu.signatures = redacted.signatures;
|
|
144
|
+
if (!features.legacyEventIds) {
|
|
145
|
+
const hash = computeReferenceHash(pdu, roomVersion);
|
|
146
|
+
pdu.event_id = `$${features.urlSafeEventIds ? urlSafeBase64(hash) : unpaddedBase64(hash)}`;
|
|
147
|
+
}
|
|
148
|
+
return pdu;
|
|
149
|
+
}
|
|
150
|
+
function eventIdOf(ref) {
|
|
151
|
+
return Array.isArray(ref) ? ref[0] : ref;
|
|
152
|
+
}
|
|
153
|
+
/** Convert a stored PDU into the client-server event format. */
|
|
154
|
+
function toClientEvent(pdu, opts = {}) {
|
|
155
|
+
const ev = {
|
|
156
|
+
event_id: pdu.event_id,
|
|
157
|
+
sender: pdu.sender,
|
|
158
|
+
type: pdu.type,
|
|
159
|
+
content: pdu.content,
|
|
160
|
+
origin_server_ts: pdu.origin_server_ts
|
|
161
|
+
};
|
|
162
|
+
if (opts.includeRoomId !== false) ev.room_id = pdu.room_id;
|
|
163
|
+
if (pdu.state_key !== void 0) ev.state_key = pdu.state_key;
|
|
164
|
+
if (pdu.redacts !== void 0) ev.redacts = pdu.redacts;
|
|
165
|
+
else if (pdu.type === M.ROOM.REDACTION && typeof pdu.content?.redacts === "string") ev.redacts = pdu.content.redacts;
|
|
166
|
+
ev.unsigned = {
|
|
167
|
+
...pdu.unsigned,
|
|
168
|
+
age: Math.max(0, Date.now() - pdu.origin_server_ts)
|
|
169
|
+
};
|
|
170
|
+
return ev;
|
|
171
|
+
}
|
|
172
|
+
function stripState(pdu) {
|
|
173
|
+
return {
|
|
174
|
+
type: pdu.type,
|
|
175
|
+
state_key: pdu.state_key,
|
|
176
|
+
sender: pdu.sender,
|
|
177
|
+
content: pdu.content
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const stateKey = (type, key) => `${type}\u0000${key}`;
|
|
181
|
+
const splitStateKey = (k) => {
|
|
182
|
+
const i = k.indexOf("\0");
|
|
183
|
+
return [k.slice(0, i), k.slice(i + 1)];
|
|
184
|
+
};
|
|
185
|
+
//#endregion
|
|
186
|
+
export { STABLE_ROOM_VERSIONS, SUPPORTED_ROOM_VERSIONS, UNSTABLE_ROOM_VERSIONS, computeContentHash, computeReferenceHash, eventIdOf, finalisePdu, redact, roomVersionFeatures, splitStateKey, stateKey, stripState, toClientEvent, versionNum };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
//#region src/core/glob.ts
|
|
2
|
+
/**
|
|
3
|
+
* Glob matching (`*` = any run of characters, `?` = one character) in linear time.
|
|
4
|
+
*
|
|
5
|
+
* Security fix (https://github.com/k18k/matrix-nodejs-homeserver/issues/2 — ReDoS, CWE-1333): globs from untrusted input (m.room.server_acl entries from
|
|
6
|
+
* room state, MSC4155 invite rules from account data, event filters from clients) used to be
|
|
7
|
+
* compiled to regular expressions with `.*`. A pattern such as `****************!` made the
|
|
8
|
+
* regex engine backtrack exponentially: 16 asterisks blocked the event loop for ~110 s, so a
|
|
9
|
+
* room ACL could hang the whole homeserver, again after every restart. This matcher never
|
|
10
|
+
* backtracks more than once per character (classic two-pointer wildcard matching).
|
|
11
|
+
*/
|
|
12
|
+
function globMatch(pattern, value, opts = {}) {
|
|
13
|
+
const p = opts.caseInsensitive ? pattern.toLowerCase() : pattern;
|
|
14
|
+
const v = opts.caseInsensitive ? value.toLowerCase() : value;
|
|
15
|
+
const anyOne = opts.questionMark ?? true;
|
|
16
|
+
let pi = 0, vi = 0;
|
|
17
|
+
let star = -1, mark = 0;
|
|
18
|
+
while (vi < v.length) if (pi < p.length && (p[pi] === v[vi] || anyOne && p[pi] === "?")) {
|
|
19
|
+
pi++;
|
|
20
|
+
vi++;
|
|
21
|
+
} else if (pi < p.length && p[pi] === "*") {
|
|
22
|
+
star = ++pi;
|
|
23
|
+
mark = vi;
|
|
24
|
+
} else if (star !== -1) {
|
|
25
|
+
pi = star;
|
|
26
|
+
vi = ++mark;
|
|
27
|
+
} else return false;
|
|
28
|
+
while (pi < p.length && p[pi] === "*") pi++;
|
|
29
|
+
return pi === p.length;
|
|
30
|
+
}
|
|
31
|
+
const isWordChar = (c) => c !== void 0 && /[\p{L}\p{N}_]/u.test(c);
|
|
32
|
+
/**
|
|
33
|
+
* Whether `pattern` (a glob) matches some run of `value` that starts and ends on a word boundary,
|
|
34
|
+
* as push rules match `content.body`. Case-insensitive.
|
|
35
|
+
*
|
|
36
|
+
* Security fix (https://github.com/k18k/matrix-nodejs-homeserver/issues/2 — ReDoS, CWE-1333): push rule patterns are set by any user and were compiled to
|
|
37
|
+
* `(^|\W)…(\W|$)` regexes with `.*?`, then run against every message in the user's rooms, so one
|
|
38
|
+
* account could hang the server. This simulates the glob as an automaton instead: the cost is
|
|
39
|
+
* O(value length × pattern length) whatever the input, with no backtracking.
|
|
40
|
+
*/
|
|
41
|
+
function globSearchWords(pattern, value) {
|
|
42
|
+
const p = pattern.toLowerCase();
|
|
43
|
+
const v = value.toLowerCase();
|
|
44
|
+
const m = p.length;
|
|
45
|
+
let active = new Uint8Array(m + 1);
|
|
46
|
+
const close = (set) => {
|
|
47
|
+
for (let k = 0; k < m; k++) if (set[k] && p[k] === "*") set[k + 1] = 1;
|
|
48
|
+
};
|
|
49
|
+
for (let i = 0; i <= v.length; i++) {
|
|
50
|
+
if (!isWordChar(v[i - 1])) active[0] = 1;
|
|
51
|
+
close(active);
|
|
52
|
+
if (active[m] && !isWordChar(v[i])) return true;
|
|
53
|
+
if (i === v.length) break;
|
|
54
|
+
const next = new Uint8Array(m + 1);
|
|
55
|
+
const c = v[i];
|
|
56
|
+
for (let k = 0; k < m; k++) {
|
|
57
|
+
if (!active[k]) continue;
|
|
58
|
+
if (p[k] === "*") next[k] = 1;
|
|
59
|
+
else if (p[k] === "?" || p[k] === c) next[k + 1] = 1;
|
|
60
|
+
}
|
|
61
|
+
active = next;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { globMatch, globSearchWords };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Homeserver } from "../homeserver.js";
|
|
2
|
+
//#region src/core/identity.d.ts
|
|
3
|
+
export interface StoredInvite {
|
|
4
|
+
token: string;
|
|
5
|
+
public_keys: {
|
|
6
|
+
public_key: string;
|
|
7
|
+
key_validity_url: string;
|
|
8
|
+
}[];
|
|
9
|
+
display_name: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class IdentityServers {
|
|
12
|
+
private readonly hs;
|
|
13
|
+
constructor(hs: Homeserver);
|
|
14
|
+
private base;
|
|
15
|
+
private call;
|
|
16
|
+
/** The Matrix user ID bound to a 3PID, if any. */
|
|
17
|
+
lookup(idServer: string, token: string, medium: string, address: string): Promise<string | null>;
|
|
18
|
+
storeInvite(idServer: string, token: string, invite: Record<string, unknown>): Promise<StoredInvite>;
|
|
19
|
+
/** Returns the identity server's record of the binding (medium, address, mxid...). */
|
|
20
|
+
bind(idServer: string, token: string, sid: string, clientSecret: string, mxid: string): Promise<Record<string, any>>;
|
|
21
|
+
/** Remove a binding; resolves false when the identity server does not support unbinding. */
|
|
22
|
+
unbind(idServer: string, mxid: string, medium: string, address: string): Promise<boolean>;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { MatrixError } from "./errors.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
//#region src/core/identity.ts
|
|
4
|
+
const urlSafe = (b) => b.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
5
|
+
var IdentityServers = class {
|
|
6
|
+
hs;
|
|
7
|
+
constructor(hs) {
|
|
8
|
+
this.hs = hs;
|
|
9
|
+
}
|
|
10
|
+
base(idServer) {
|
|
11
|
+
if (!/^[A-Za-z0-9.-]+(:\d+)?$/.test(idServer)) throw new MatrixError(400, "M_INVALID_PARAM", "Invalid id_server");
|
|
12
|
+
return `${this.hs.config.threepid.identityServerProtocol}://${idServer}`;
|
|
13
|
+
}
|
|
14
|
+
async call(idServer, method, path, opts = {}) {
|
|
15
|
+
const headers = { accept: "application/json" };
|
|
16
|
+
if (opts.body !== void 0) headers["content-type"] = "application/json";
|
|
17
|
+
if (opts.token) headers.authorization = `Bearer ${opts.token}`;
|
|
18
|
+
if (opts.signed) headers.authorization = this.hs.federation.client.authHeader(idServer, method, path, opts.body);
|
|
19
|
+
let res;
|
|
20
|
+
try {
|
|
21
|
+
res = await this.hs.fetch(`${this.base(idServer)}${path}`, {
|
|
22
|
+
method,
|
|
23
|
+
headers,
|
|
24
|
+
body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body),
|
|
25
|
+
signal: AbortSignal.timeout(15e3)
|
|
26
|
+
});
|
|
27
|
+
} catch (err) {
|
|
28
|
+
throw new MatrixError(502, "M_UNKNOWN", `Identity server ${idServer} unreachable: ${err.message}`);
|
|
29
|
+
}
|
|
30
|
+
const json = await res.json().catch(() => ({}));
|
|
31
|
+
if (!res.ok) throw new MatrixError(res.status >= 500 ? 502 : res.status, typeof json.errcode === "string" ? json.errcode : "M_UNKNOWN", typeof json.error === "string" ? json.error : `Identity server returned ${res.status}`);
|
|
32
|
+
return json;
|
|
33
|
+
}
|
|
34
|
+
/** The Matrix user ID bound to a 3PID, if any. */
|
|
35
|
+
async lookup(idServer, token, medium, address) {
|
|
36
|
+
const details = await this.call(idServer, "GET", "/_matrix/identity/v2/hash_details", { token });
|
|
37
|
+
const algorithms = Array.isArray(details.algorithms) ? details.algorithms : [];
|
|
38
|
+
let query, algorithm;
|
|
39
|
+
if (algorithms.includes("sha256") && typeof details.lookup_pepper === "string") {
|
|
40
|
+
algorithm = "sha256";
|
|
41
|
+
query = urlSafe(createHash("sha256").update(`${address} ${medium} ${details.lookup_pepper}`).digest());
|
|
42
|
+
} else if (algorithms.includes("none")) {
|
|
43
|
+
algorithm = "none";
|
|
44
|
+
query = `${address} ${medium}`;
|
|
45
|
+
} else throw new MatrixError(400, "M_UNKNOWN", "The identity server supports no known lookup algorithm");
|
|
46
|
+
const mxid = (await this.call(idServer, "POST", "/_matrix/identity/v2/lookup", {
|
|
47
|
+
token,
|
|
48
|
+
body: {
|
|
49
|
+
addresses: [query],
|
|
50
|
+
algorithm,
|
|
51
|
+
pepper: details.lookup_pepper ?? ""
|
|
52
|
+
}
|
|
53
|
+
})).mappings?.[query];
|
|
54
|
+
return typeof mxid === "string" ? mxid : null;
|
|
55
|
+
}
|
|
56
|
+
async storeInvite(idServer, token, invite) {
|
|
57
|
+
const res = await this.call(idServer, "POST", "/_matrix/identity/v2/store-invite", {
|
|
58
|
+
token,
|
|
59
|
+
body: invite
|
|
60
|
+
});
|
|
61
|
+
if (typeof res.token !== "string" || !Array.isArray(res.public_keys)) throw new MatrixError(502, "M_UNKNOWN", "Invalid store-invite response");
|
|
62
|
+
return res;
|
|
63
|
+
}
|
|
64
|
+
/** Returns the identity server's record of the binding (medium, address, mxid...). */
|
|
65
|
+
async bind(idServer, token, sid, clientSecret, mxid) {
|
|
66
|
+
return this.call(idServer, "POST", "/_matrix/identity/v2/3pid/bind", {
|
|
67
|
+
token,
|
|
68
|
+
body: {
|
|
69
|
+
sid,
|
|
70
|
+
client_secret: clientSecret,
|
|
71
|
+
mxid
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Remove a binding; resolves false when the identity server does not support unbinding. */
|
|
76
|
+
async unbind(idServer, mxid, medium, address) {
|
|
77
|
+
try {
|
|
78
|
+
await this.call(idServer, "POST", "/_matrix/identity/v2/3pid/unbind", {
|
|
79
|
+
signed: true,
|
|
80
|
+
body: {
|
|
81
|
+
mxid,
|
|
82
|
+
threepid: {
|
|
83
|
+
medium,
|
|
84
|
+
address
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return true;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (err instanceof MatrixError && (err.status === 404 || err.status === 501 || err.errcode === "M_UNRECOGNIZED")) return false;
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
//#endregion
|
|
96
|
+
export { IdentityServers };
|
package/dist/core/ids.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { randomString } from "./crypto.js";
|
|
2
|
+
//#region src/core/ids.ts
|
|
3
|
+
const LOCALPART_RE = /^[a-z0-9._=\-/+]+$/;
|
|
4
|
+
function parseId(id, sigil) {
|
|
5
|
+
if (typeof id !== "string" || !id.startsWith(sigil)) return null;
|
|
6
|
+
const idx = id.indexOf(":");
|
|
7
|
+
if (idx < 2) return null;
|
|
8
|
+
return {
|
|
9
|
+
localpart: id.slice(1, idx),
|
|
10
|
+
server: id.slice(idx + 1)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** Server name grammar: DNS name, IPv4 or bracketed IPv6, with an optional port. */
|
|
14
|
+
const SERVER_NAME_RE = /^(\[[0-9A-Fa-f:.]+\]|[A-Za-z0-9.-]+)(:\d{1,5})?$/;
|
|
15
|
+
function isValidServerName(name) {
|
|
16
|
+
return name.length > 0 && name.length <= 255 && SERVER_NAME_RE.test(name);
|
|
17
|
+
}
|
|
18
|
+
function isValidUserId(id) {
|
|
19
|
+
if (typeof id !== "string" || id.length > 255) return false;
|
|
20
|
+
const parsed = parseId(id, "@");
|
|
21
|
+
return !!parsed && isValidServerName(parsed.server);
|
|
22
|
+
}
|
|
23
|
+
function isValidRoomId(id) {
|
|
24
|
+
return typeof id === "string" && /^![^\s:]+(:[^\s]+)?$/.test(id);
|
|
25
|
+
}
|
|
26
|
+
function isValidAlias(id) {
|
|
27
|
+
return typeof id === "string" && id.length <= 255 && !!parseId(id, "#");
|
|
28
|
+
}
|
|
29
|
+
function generateRoomId(serverName) {
|
|
30
|
+
return `!${randomString(18)}:${serverName}`;
|
|
31
|
+
}
|
|
32
|
+
function generateDeviceId() {
|
|
33
|
+
return randomString(10).toUpperCase();
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { LOCALPART_RE, generateDeviceId, generateRoomId, isValidAlias, isValidRoomId, isValidServerName, isValidUserId, parseId };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isObject } from "./json.js";
|
|
2
|
+
import { UNSTABLE } from "./m.js";
|
|
3
|
+
import { globMatch } from "./glob.js";
|
|
4
|
+
//#region src/core/invitefilter.ts
|
|
5
|
+
const INVITE_CONFIG_TYPE = UNSTABLE.ORG.MATRIX.MSC4155.INVITE_PERMISSION_CONFIG;
|
|
6
|
+
/**
|
|
7
|
+
* MSC4155 invite filtering: "allow", "ignore" (accept but hide) or "block" (refuse).
|
|
8
|
+
* User rules take precedence over server rules; within each, allow > ignore > block.
|
|
9
|
+
*/
|
|
10
|
+
async function inviteDecision(hs, target, inviter) {
|
|
11
|
+
const config = (await hs.db.findOne("account_data", [
|
|
12
|
+
{
|
|
13
|
+
field: "user_id",
|
|
14
|
+
value: target
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
field: "room_id",
|
|
18
|
+
value: ""
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
field: "type",
|
|
22
|
+
value: INVITE_CONFIG_TYPE
|
|
23
|
+
}
|
|
24
|
+
]))?.content;
|
|
25
|
+
if (!isObject(config)) return "allow";
|
|
26
|
+
const list = (k) => Array.isArray(config[k]) ? config[k].filter((x) => typeof x === "string") : [];
|
|
27
|
+
const server = inviter.slice(inviter.indexOf(":") + 1);
|
|
28
|
+
for (const [level, value] of [["users", inviter], ["servers", server]]) {
|
|
29
|
+
if (list(`allowed_${level}`).some((p) => globMatch(p, value))) return "allow";
|
|
30
|
+
if (list(`ignored_${level}`).some((p) => globMatch(p, value))) return "ignore";
|
|
31
|
+
if (list(`blocked_${level}`).some((p) => globMatch(p, value))) return "block";
|
|
32
|
+
}
|
|
33
|
+
return "allow";
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { INVITE_CONFIG_TYPE, inviteDecision };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/core/json.ts
|
|
2
|
+
/** Matrix canonical JSON: sorted keys, no whitespace, UTF-8, integers only. */
|
|
3
|
+
function canonicalJson(value) {
|
|
4
|
+
if (value === null || typeof value !== "object") {
|
|
5
|
+
if (typeof value === "number" && !Number.isInteger(value)) throw new Error("Canonical JSON does not allow non-integer numbers");
|
|
6
|
+
return JSON.stringify(value ?? null);
|
|
7
|
+
}
|
|
8
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
9
|
+
const obj = value;
|
|
10
|
+
return `{${Object.keys(obj).filter((k) => obj[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
|
|
11
|
+
}
|
|
12
|
+
function isObject(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { canonicalJson, isObject };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Homeserver } from "../homeserver.js";
|
|
2
|
+
//#region src/core/keys.d.ts
|
|
3
|
+
/** End-to-end encryption key storage (device keys, one-time keys, cross-signing). */
|
|
4
|
+
export declare class KeyService {
|
|
5
|
+
private hs;
|
|
6
|
+
constructor(hs: Homeserver);
|
|
7
|
+
/** A local user's devices or device keys changed: update local clients and remote servers. */
|
|
8
|
+
deviceChanged(userId: string, deletedDeviceId?: string): Promise<void>;
|
|
9
|
+
markDeviceListChanged(userId: string, notify?: boolean): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* A partial-state room got its full state (MSC3902): device list updates received meanwhile for
|
|
12
|
+
* users we could not place count as changes now, if those users turned out to be members.
|
|
13
|
+
*/
|
|
14
|
+
onPartialStateComplete(roomId: string, pendingUsers: string[]): Promise<void>;
|
|
15
|
+
upload(userId: string, deviceId: string, body: Record<string, any>): Promise<Record<string, number>>;
|
|
16
|
+
oneTimeKeyCounts(userId: string, deviceId: string): Promise<Record<string, number>>;
|
|
17
|
+
unusedFallbackKeyTypes(userId: string, deviceId: string): Promise<string[]>;
|
|
18
|
+
query(requester: string, deviceKeys: Record<string, string[]>, timeoutMs?: number): Promise<Record<string, any>>;
|
|
19
|
+
/** Whether a remote user shares a room with a local user (by known membership). */
|
|
20
|
+
isTracked(userId: string): Promise<boolean>;
|
|
21
|
+
dropRemoteCache(userId: string): Promise<void>;
|
|
22
|
+
/** A tracked user's device list changed: refetch it on the next query. */
|
|
23
|
+
markRemoteStale(userId: string): Promise<void>;
|
|
24
|
+
private storeRemoteCache;
|
|
25
|
+
private fromRemoteCache;
|
|
26
|
+
private withSignatures;
|
|
27
|
+
claim(request: Record<string, Record<string, string>>): Promise<Record<string, any>>;
|
|
28
|
+
uploadCrossSigning(userId: string, body: Record<string, any>): Promise<void>;
|
|
29
|
+
uploadSignatures(signer: string, body: Record<string, any>): Promise<Record<string, any>>;
|
|
30
|
+
/** Users (sharing a room with `userId`) whose device lists changed after `since`. */
|
|
31
|
+
changesSince(userId: string, since: number, upTo: number): Promise<{
|
|
32
|
+
changed: string[];
|
|
33
|
+
left: string[];
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|