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/authrules.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isObject } from "./json.js";
|
|
2
2
|
import { publicKeyFromBase64, verifyJsonSignature } from "./crypto.js";
|
|
3
|
+
import { M } from "./m.js";
|
|
3
4
|
import { roomVersionFeatures, stateKey, versionNum } from "./events.js";
|
|
4
5
|
import { errors } from "./errors.js";
|
|
5
6
|
import { isValidUserId } from "./ids.js";
|
|
@@ -20,10 +21,10 @@ function getState(state, type, key = "") {
|
|
|
20
21
|
return state.get(stateKey(type, key));
|
|
21
22
|
}
|
|
22
23
|
function membershipOf(state, userId) {
|
|
23
|
-
return getState(state,
|
|
24
|
+
return getState(state, M.ROOM.MEMBER, userId)?.content?.membership ?? "leave";
|
|
24
25
|
}
|
|
25
26
|
function roomCreator(state, roomVersion) {
|
|
26
|
-
const create = getState(state,
|
|
27
|
+
const create = getState(state, M.ROOM.CREATE);
|
|
27
28
|
if (!create) return void 0;
|
|
28
29
|
return roomVersionFeatures(roomVersion).v11 ? create.sender : create.content.creator ?? create.sender;
|
|
29
30
|
}
|
|
@@ -35,13 +36,13 @@ const toInt = (v, fallback) => {
|
|
|
35
36
|
/** v12+: the create event's sender plus `additional_creators`. Empty for older versions. */
|
|
36
37
|
function roomCreators(state, roomVersion) {
|
|
37
38
|
if (!roomVersionFeatures(roomVersion).v12) return /* @__PURE__ */ new Set();
|
|
38
|
-
const create = getState(state,
|
|
39
|
+
const create = getState(state, M.ROOM.CREATE);
|
|
39
40
|
if (!create) return /* @__PURE__ */ new Set();
|
|
40
41
|
const extra = Array.isArray(create.content?.additional_creators) ? create.content.additional_creators : [];
|
|
41
42
|
return /* @__PURE__ */ new Set([create.sender, ...extra.filter((u) => typeof u === "string")]);
|
|
42
43
|
}
|
|
43
44
|
function powerLevels(state, roomVersion) {
|
|
44
|
-
const pl = getState(state,
|
|
45
|
+
const pl = getState(state, M.ROOM.POWER_LEVELS)?.content;
|
|
45
46
|
const creators = roomCreators(state, roomVersion);
|
|
46
47
|
if (!pl) {
|
|
47
48
|
const creator = roomCreator(state, roomVersion);
|
|
@@ -90,21 +91,21 @@ const sameDomain = (a, b) => a.slice(a.indexOf(":") + 1) === b.slice(b.indexOf("
|
|
|
90
91
|
const domainOf = (id) => id.slice(id.indexOf(":") + 1);
|
|
91
92
|
/** The (type, state_key) pairs an event's auth_events may reference (spec "auth events selection"). */
|
|
92
93
|
function authEventKeys(event, roomVersion) {
|
|
93
|
-
if (event.type ===
|
|
94
|
-
const keys = [stateKey(
|
|
95
|
-
if (!roomVersionFeatures(roomVersion).v12) keys.unshift(stateKey(
|
|
96
|
-
if (event.type ===
|
|
97
|
-
keys.push(stateKey(
|
|
94
|
+
if (event.type === M.ROOM.CREATE) return [];
|
|
95
|
+
const keys = [stateKey(M.ROOM.POWER_LEVELS, ""), stateKey(M.ROOM.MEMBER, event.sender)];
|
|
96
|
+
if (!roomVersionFeatures(roomVersion).v12) keys.unshift(stateKey(M.ROOM.CREATE, ""));
|
|
97
|
+
if (event.type === M.ROOM.MEMBER && typeof event.state_key === "string") {
|
|
98
|
+
keys.push(stateKey(M.ROOM.MEMBER, event.state_key));
|
|
98
99
|
const membership = event.content?.membership;
|
|
99
100
|
if ([
|
|
100
101
|
"join",
|
|
101
102
|
"invite",
|
|
102
103
|
"knock"
|
|
103
|
-
].includes(membership)) keys.push(stateKey(
|
|
104
|
+
].includes(membership)) keys.push(stateKey(M.ROOM.JOIN_RULES, ""));
|
|
104
105
|
const token = event.content?.third_party_invite?.signed?.token;
|
|
105
|
-
if (membership === "invite" && typeof token === "string") keys.push(stateKey(
|
|
106
|
+
if (membership === "invite" && typeof token === "string") keys.push(stateKey(M.ROOM.THIRD_PARTY_INVITE, token));
|
|
106
107
|
const via = event.content?.join_authorised_via_users_server;
|
|
107
|
-
if (membership === "join" && typeof via === "string" && roomVersionFeatures(roomVersion).restricted) keys.push(stateKey(
|
|
108
|
+
if (membership === "join" && typeof via === "string" && roomVersionFeatures(roomVersion).restricted) keys.push(stateKey(M.ROOM.MEMBER, via));
|
|
108
109
|
}
|
|
109
110
|
return [...new Set(keys)];
|
|
110
111
|
}
|
|
@@ -121,10 +122,10 @@ function authStateFromAuthEvents(event, authEvents, roomVersion, createEvent) {
|
|
|
121
122
|
const key = stateKey(ev.type, ev.state_key);
|
|
122
123
|
if (state.has(key)) throw errors.forbidden("Duplicate auth event");
|
|
123
124
|
if (!allowed.has(key)) throw errors.forbidden(`Unexpected auth event ${ev.type}`);
|
|
124
|
-
if (roomVersionFeatures(roomVersion).v12 && ev.type ===
|
|
125
|
+
if (roomVersionFeatures(roomVersion).v12 && ev.type === M.ROOM.CREATE) throw errors.forbidden("Create event must not be in auth_events");
|
|
125
126
|
state.set(key, ev);
|
|
126
127
|
}
|
|
127
|
-
if (createEvent) state.set(stateKey(
|
|
128
|
+
if (createEvent) state.set(stateKey(M.ROOM.CREATE, ""), createEvent);
|
|
128
129
|
return state;
|
|
129
130
|
}
|
|
130
131
|
/**
|
|
@@ -137,7 +138,7 @@ function checkAuth(event, state, roomVersion, opts = {}) {
|
|
|
137
138
|
const deny = (msg) => {
|
|
138
139
|
throw errors.forbidden(msg);
|
|
139
140
|
};
|
|
140
|
-
if (event.type ===
|
|
141
|
+
if (event.type === M.ROOM.CREATE) {
|
|
141
142
|
if (state.size > 0 || Array.isArray(event.prev_events) && event.prev_events.length > 0) deny("Create event must be the first event");
|
|
142
143
|
if (!features.v12 && event.room_id && !sameDomain(event.room_id, event.sender)) deny("Room ID domain must match the creator");
|
|
143
144
|
const rv = event.content?.room_version;
|
|
@@ -149,10 +150,10 @@ function checkAuth(event, state, roomVersion, opts = {}) {
|
|
|
149
150
|
}
|
|
150
151
|
return;
|
|
151
152
|
}
|
|
152
|
-
const create = getState(state,
|
|
153
|
+
const create = getState(state, M.ROOM.CREATE);
|
|
153
154
|
if (!create) deny("No create event in room");
|
|
154
|
-
if (create.content?.[
|
|
155
|
-
if (n <= 5 && event.type ===
|
|
155
|
+
if (create.content?.[M.FEDERATE] === false && !sameDomain(event.sender, create.sender)) deny("Room is not federated");
|
|
156
|
+
if (n <= 5 && event.type === M.ROOM.ALIASES) {
|
|
156
157
|
if (event.state_key === void 0) deny("Aliases event lacks a state key");
|
|
157
158
|
if (event.state_key !== domainOf(event.sender)) deny("Aliases state key must be the sender's domain");
|
|
158
159
|
return;
|
|
@@ -160,12 +161,12 @@ function checkAuth(event, state, roomVersion, opts = {}) {
|
|
|
160
161
|
const pl = powerLevels(state, roomVersion);
|
|
161
162
|
const senderMembership = membershipOf(state, event.sender);
|
|
162
163
|
const senderLevel = userLevel(pl, event.sender);
|
|
163
|
-
if (event.type ===
|
|
164
|
+
if (event.type === M.ROOM.MEMBER) {
|
|
164
165
|
const target = event.state_key;
|
|
165
166
|
const membership = event.content?.membership;
|
|
166
167
|
if (typeof target !== "string" || typeof membership !== "string") deny("Member event lacks state_key or membership");
|
|
167
168
|
const targetMembership = membershipOf(state, target);
|
|
168
|
-
const joinRule = getState(state,
|
|
169
|
+
const joinRule = getState(state, M.ROOM.JOIN_RULES)?.content?.join_rule ?? "invite";
|
|
169
170
|
const via = event.content?.join_authorised_via_users_server;
|
|
170
171
|
switch (membership) {
|
|
171
172
|
case "join": {
|
|
@@ -226,7 +227,7 @@ function checkAuth(event, state, roomVersion, opts = {}) {
|
|
|
226
227
|
return;
|
|
227
228
|
}
|
|
228
229
|
if (senderMembership !== "join") deny("You are not joined to this room");
|
|
229
|
-
if (event.type ===
|
|
230
|
+
if (event.type === M.ROOM.THIRD_PARTY_INVITE) {
|
|
230
231
|
if (senderLevel >= pl.invite) return;
|
|
231
232
|
deny("Insufficient power level to invite");
|
|
232
233
|
}
|
|
@@ -238,16 +239,16 @@ function checkAuth(event, state, roomVersion, opts = {}) {
|
|
|
238
239
|
if (!owner) deny("Malformed user-owned state key");
|
|
239
240
|
if (owner !== event.sender && senderLevel <= userLevel(pl, owner)) deny("This state key belongs to another user");
|
|
240
241
|
}
|
|
241
|
-
if (event.type ===
|
|
242
|
+
if (event.type === M.ROOM.POWER_LEVELS && isState) {
|
|
242
243
|
if (isObject(event.content?.users)) for (const u of Object.keys(event.content.users)) {
|
|
243
244
|
if (!isValidUserId(u)) deny("Power level users must be valid user IDs");
|
|
244
245
|
if (pl.creators.has(u)) deny("Room creators cannot be given a power level");
|
|
245
246
|
}
|
|
246
|
-
if (getState(state,
|
|
247
|
+
if (getState(state, M.ROOM.POWER_LEVELS)) checkPowerLevelChange(event, pl, senderLevel, features.integerPowerLevels, n >= 6, deny);
|
|
247
248
|
else checkPowerLevelShape(event, features.integerPowerLevels, deny);
|
|
248
249
|
return;
|
|
249
250
|
}
|
|
250
|
-
if (n <= 2 && event.type ===
|
|
251
|
+
if (n <= 2 && event.type === M.ROOM.REDACTION) {
|
|
251
252
|
if (senderLevel >= pl.redact) return;
|
|
252
253
|
const redacts = event.redacts;
|
|
253
254
|
if (typeof redacts === "string" && redacts.includes(":") && domainOf(redacts) === domainOf(event.sender)) return;
|
|
@@ -258,7 +259,7 @@ function checkThirdPartyInvite(event, state) {
|
|
|
258
259
|
const signed = event.content?.third_party_invite?.signed;
|
|
259
260
|
if (!isObject(signed) || typeof signed.mxid !== "string" || typeof signed.token !== "string") return false;
|
|
260
261
|
if (signed.mxid !== event.state_key) return false;
|
|
261
|
-
const invite = getState(state,
|
|
262
|
+
const invite = getState(state, M.ROOM.THIRD_PARTY_INVITE, signed.token);
|
|
262
263
|
if (!invite || invite.sender !== event.sender) return false;
|
|
263
264
|
const keys = [];
|
|
264
265
|
if (typeof invite.content?.public_key === "string") keys.push(invite.content.public_key);
|
package/dist/core/cluster.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
//#region src/core/cluster.d.ts
|
|
2
2
|
/**
|
|
3
3
|
* Coordination between homeserver instances sharing one database. The default
|
|
4
|
-
* (`localCluster()`) keeps everything in process; use `postgresCluster()`
|
|
5
|
-
*
|
|
4
|
+
* (`localCluster()`) keeps everything in process; use `postgresCluster()` to run several
|
|
5
|
+
* instances behind a load balancer.
|
|
6
6
|
*/
|
|
7
7
|
export interface ClusterAdapter {
|
|
8
8
|
/** Allocate the next value of a cluster-wide, strictly increasing sequence. */
|
|
@@ -44,13 +44,20 @@ export declare class StreamManager {
|
|
|
44
44
|
private cluster;
|
|
45
45
|
private instanceId;
|
|
46
46
|
private inflight;
|
|
47
|
-
|
|
48
|
-
private
|
|
47
|
+
/** Allocations waiting for a publish that covers them. */
|
|
48
|
+
private reserving;
|
|
49
|
+
/** Allocations whose publish is done, asking the sequence for their position now. */
|
|
50
|
+
private allocating;
|
|
51
|
+
/** The position of the last published busy state: below every position still being allocated. */
|
|
52
|
+
private lastPos;
|
|
49
53
|
private heartbeat;
|
|
50
54
|
private positionCache;
|
|
55
|
+
private publishing;
|
|
56
|
+
private nextPublish;
|
|
51
57
|
constructor(cluster: ClusterAdapter, instanceId: string);
|
|
52
58
|
private key;
|
|
53
|
-
|
|
59
|
+
/** Publish this writer's state as it is when the publish starts; concurrent callers share it. */
|
|
60
|
+
private flush;
|
|
54
61
|
/** Allocate a position. The caller must call `done()` once its writes are committed. */
|
|
55
62
|
allocate(): Promise<StreamLease>;
|
|
56
63
|
/** Allocate a position, run `fn` with it, then complete it. */
|
package/dist/core/cluster.js
CHANGED
|
@@ -91,38 +91,64 @@ var StreamManager = class {
|
|
|
91
91
|
cluster;
|
|
92
92
|
instanceId;
|
|
93
93
|
inflight = /* @__PURE__ */ new Set();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
/** Allocations waiting for a publish that covers them. */
|
|
95
|
+
reserving = 0;
|
|
96
|
+
/** Allocations whose publish is done, asking the sequence for their position now. */
|
|
97
|
+
allocating = 0;
|
|
98
|
+
/** The position of the last published busy state: below every position still being allocated. */
|
|
99
|
+
lastPos = 0;
|
|
99
100
|
heartbeat;
|
|
100
101
|
positionCache = null;
|
|
102
|
+
publishing = Promise.resolve();
|
|
103
|
+
nextPublish = null;
|
|
101
104
|
constructor(cluster, instanceId) {
|
|
102
105
|
this.cluster = cluster;
|
|
103
106
|
this.instanceId = instanceId;
|
|
104
|
-
this.heartbeat = setInterval(() => void this.
|
|
107
|
+
this.heartbeat = setInterval(() => void this.flush().catch(() => {}), WRITER_TTL_MS / 3);
|
|
105
108
|
this.heartbeat.unref?.();
|
|
106
109
|
}
|
|
107
110
|
key() {
|
|
108
111
|
return `writer:${this.instanceId}`;
|
|
109
112
|
}
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
/** Publish this writer's state as it is when the publish starts; concurrent callers share it. */
|
|
114
|
+
flush() {
|
|
115
|
+
if (this.nextPublish) return this.nextPublish;
|
|
116
|
+
const run = async () => {
|
|
117
|
+
this.nextPublish = null;
|
|
118
|
+
const covered = this.reserving;
|
|
119
|
+
const earlier = this.allocating;
|
|
120
|
+
this.reserving = 0;
|
|
121
|
+
this.allocating += covered;
|
|
122
|
+
const bounds = [];
|
|
123
|
+
if (this.inflight.size) bounds.push(Math.min(...this.inflight) - 1);
|
|
124
|
+
if (earlier) bounds.push(this.lastPos);
|
|
125
|
+
if (covered) bounds.push(await this.cluster.currentId("stream"));
|
|
126
|
+
const state = bounds.length ? {
|
|
127
|
+
busy: true,
|
|
128
|
+
pos: Math.min(...bounds)
|
|
129
|
+
} : {
|
|
130
|
+
busy: false,
|
|
131
|
+
pos: 0
|
|
132
|
+
};
|
|
133
|
+
if (state.busy) this.lastPos = state.pos;
|
|
134
|
+
await this.cluster.set(this.key(), state, WRITER_TTL_MS);
|
|
135
|
+
};
|
|
136
|
+
const p = this.publishing.then(run, run);
|
|
137
|
+
this.nextPublish = p;
|
|
138
|
+
this.publishing = p.catch(() => {});
|
|
139
|
+
return p;
|
|
112
140
|
}
|
|
113
141
|
/** Allocate a position. The caller must call `done()` once its writes are committed. */
|
|
114
142
|
async allocate() {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
const id = await this.cluster.nextId("stream");
|
|
125
|
-
this.inflight.add(id);
|
|
143
|
+
this.reserving++;
|
|
144
|
+
let id;
|
|
145
|
+
try {
|
|
146
|
+
await this.flush();
|
|
147
|
+
id = await this.cluster.nextId("stream");
|
|
148
|
+
this.inflight.add(id);
|
|
149
|
+
} finally {
|
|
150
|
+
this.allocating--;
|
|
151
|
+
}
|
|
126
152
|
let finished = false;
|
|
127
153
|
const timer = setTimeout(() => void lease.done(), 6e4);
|
|
128
154
|
timer.unref?.();
|
|
@@ -133,16 +159,7 @@ var StreamManager = class {
|
|
|
133
159
|
finished = true;
|
|
134
160
|
clearTimeout(timer);
|
|
135
161
|
this.inflight.delete(id);
|
|
136
|
-
await
|
|
137
|
-
this.published = this.inflight.size ? {
|
|
138
|
-
busy: true,
|
|
139
|
-
pos: Math.min(...this.inflight) - 1
|
|
140
|
-
} : {
|
|
141
|
-
busy: false,
|
|
142
|
-
pos: await this.cluster.currentId("stream")
|
|
143
|
-
};
|
|
144
|
-
await this.publishState();
|
|
145
|
-
}));
|
|
162
|
+
await this.flush();
|
|
146
163
|
this.positionCache = null;
|
|
147
164
|
}
|
|
148
165
|
};
|
package/dist/core/delayed.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomString } from "./crypto.js";
|
|
2
|
+
import { UNSTABLE } from "./m.js";
|
|
2
3
|
import { MatrixError, errors } from "./errors.js";
|
|
3
4
|
//#region src/core/delayed.ts
|
|
4
5
|
/**
|
|
@@ -27,7 +28,7 @@ var DelayedEventService = class {
|
|
|
27
28
|
}
|
|
28
29
|
async schedule(opts) {
|
|
29
30
|
if (!Number.isInteger(opts.delayMs) || opts.delayMs < 0) throw errors.invalidParam("Invalid delay");
|
|
30
|
-
if (opts.delayMs > this.maxDelayMs) throw new MatrixError(400, "M_MAX_DELAY_EXCEEDED", "Delay is too long", {
|
|
31
|
+
if (opts.delayMs > this.maxDelayMs) throw new MatrixError(400, "M_MAX_DELAY_EXCEEDED", "Delay is too long", { [UNSTABLE.ORG.MATRIX.MSC4140.MAX_DELAY]: this.maxDelayMs });
|
|
31
32
|
const txnKey = opts.txnId ? `${opts.userId}|${opts.deviceId}|${opts.roomId}|${opts.txnId}` : null;
|
|
32
33
|
if (txnKey) {
|
|
33
34
|
const existing = await this.hs.db.findOne("delayed_events", [{
|
package/dist/core/events.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { canonicalJson, isObject } from "./json.js";
|
|
2
2
|
import { randomString, sha256, signJson, unpaddedBase64, urlSafeBase64 } from "./crypto.js";
|
|
3
|
+
import { M } from "./m.js";
|
|
3
4
|
//#region src/core/events.ts
|
|
4
5
|
const STABLE_ROOM_VERSIONS = [
|
|
5
6
|
"1",
|
|
@@ -76,7 +77,7 @@ function redact(event, roomVersion) {
|
|
|
76
77
|
const keep = n >= 11 ? KEEP_TOP_LEVEL : [...KEEP_TOP_LEVEL, ...KEEP_TOP_LEVEL_LEGACY];
|
|
77
78
|
const out = {};
|
|
78
79
|
for (const k of keep) if (k in event) out[k] = event[k];
|
|
79
|
-
const keepRelation = roomVersion.startsWith("org.matrix.msc3389.") && isObject(event.content?.[
|
|
80
|
+
const keepRelation = roomVersion.startsWith("org.matrix.msc3389.") && isObject(event.content?.[M.RELATES_TO]) ? event.content[M.RELATES_TO] : void 0;
|
|
80
81
|
const content = isObject(event.content) ? event.content : {};
|
|
81
82
|
const pick = (...keys) => {
|
|
82
83
|
const c = {};
|
|
@@ -84,28 +85,28 @@ function redact(event, roomVersion) {
|
|
|
84
85
|
return c;
|
|
85
86
|
};
|
|
86
87
|
switch (event.type) {
|
|
87
|
-
case
|
|
88
|
+
case M.ROOM.MEMBER: {
|
|
88
89
|
const c = pick("membership", ...n >= 9 ? ["join_authorised_via_users_server"] : []);
|
|
89
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 };
|
|
90
91
|
out.content = c;
|
|
91
92
|
break;
|
|
92
93
|
}
|
|
93
|
-
case
|
|
94
|
+
case M.ROOM.CREATE:
|
|
94
95
|
out.content = n >= 11 ? { ...content } : pick("creator");
|
|
95
96
|
break;
|
|
96
|
-
case
|
|
97
|
+
case M.ROOM.JOIN_RULES:
|
|
97
98
|
out.content = pick("join_rule", ...n >= 8 ? ["allow"] : []);
|
|
98
99
|
break;
|
|
99
|
-
case
|
|
100
|
+
case M.ROOM.POWER_LEVELS:
|
|
100
101
|
out.content = pick("ban", "events", "events_default", "kick", "redact", "state_default", "users", "users_default", ...n >= 11 ? ["invite"] : []);
|
|
101
102
|
break;
|
|
102
|
-
case
|
|
103
|
+
case M.ROOM.HISTORY_VISIBILITY:
|
|
103
104
|
out.content = pick("history_visibility");
|
|
104
105
|
break;
|
|
105
|
-
case
|
|
106
|
+
case M.ROOM.ALIASES:
|
|
106
107
|
out.content = n <= 5 ? pick("aliases") : {};
|
|
107
108
|
break;
|
|
108
|
-
case
|
|
109
|
+
case M.ROOM.REDACTION:
|
|
109
110
|
out.content = n >= 11 ? pick("redacts") : {};
|
|
110
111
|
break;
|
|
111
112
|
default: out.content = {};
|
|
@@ -116,7 +117,7 @@ function redact(event, roomVersion) {
|
|
|
116
117
|
if ("event_id" in keepRelation) rel.event_id = keepRelation.event_id;
|
|
117
118
|
out.content = {
|
|
118
119
|
...out.content,
|
|
119
|
-
|
|
120
|
+
[M.RELATES_TO]: rel
|
|
120
121
|
};
|
|
121
122
|
}
|
|
122
123
|
return out;
|
|
@@ -161,7 +162,7 @@ function toClientEvent(pdu, opts = {}) {
|
|
|
161
162
|
if (opts.includeRoomId !== false) ev.room_id = pdu.room_id;
|
|
162
163
|
if (pdu.state_key !== void 0) ev.state_key = pdu.state_key;
|
|
163
164
|
if (pdu.redacts !== void 0) ev.redacts = pdu.redacts;
|
|
164
|
-
else if (pdu.type ===
|
|
165
|
+
else if (pdu.type === M.ROOM.REDACTION && typeof pdu.content?.redacts === "string") ev.redacts = pdu.content.redacts;
|
|
165
166
|
ev.unsigned = {
|
|
166
167
|
...pdu.unsigned,
|
|
167
168
|
age: Math.max(0, Date.now() - pdu.origin_server_ts)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/core/html.ts
|
|
2
|
+
/** Escape text for use in HTML content and quoted attributes. */
|
|
3
|
+
function escapeHtml(s) {
|
|
4
|
+
return s.replace(/[&<>"']/g, (c) => ({
|
|
5
|
+
"&": "&",
|
|
6
|
+
"<": "<",
|
|
7
|
+
">": ">",
|
|
8
|
+
"\"": """,
|
|
9
|
+
"'": "'"
|
|
10
|
+
})[c]);
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { escapeHtml };
|
package/dist/core/identity.js
CHANGED
|
@@ -18,7 +18,7 @@ var IdentityServers = class {
|
|
|
18
18
|
if (opts.signed) headers.authorization = this.hs.federation.client.authHeader(idServer, method, path, opts.body);
|
|
19
19
|
let res;
|
|
20
20
|
try {
|
|
21
|
-
res = await this.hs.
|
|
21
|
+
res = await this.hs.publicFetch(`${this.base(idServer)}${path}`, {
|
|
22
22
|
method,
|
|
23
23
|
headers,
|
|
24
24
|
body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body),
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { isObject } from "./json.js";
|
|
2
|
+
import { UNSTABLE } from "./m.js";
|
|
2
3
|
import { globMatch } from "./glob.js";
|
|
3
4
|
//#region src/core/invitefilter.ts
|
|
4
|
-
const INVITE_CONFIG_TYPE =
|
|
5
|
+
const INVITE_CONFIG_TYPE = UNSTABLE.ORG.MATRIX.MSC4155.INVITE_PERMISSION_CONFIG;
|
|
5
6
|
/**
|
|
6
7
|
* MSC4155 invite filtering: "allow", "ignore" (accept but hide) or "block" (refuse).
|
|
7
8
|
* User rules take precedence over server rules; within each, allow > ignore > block.
|
package/dist/core/json.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
//#region src/core/json.ts
|
|
2
|
+
function byCodePoint(a, b) {
|
|
3
|
+
for (let i = 0; i < a.length && i < b.length;) {
|
|
4
|
+
const ca = a.codePointAt(i);
|
|
5
|
+
const cb = b.codePointAt(i);
|
|
6
|
+
if (ca !== cb) return ca - cb;
|
|
7
|
+
i += ca > 65535 ? 2 : 1;
|
|
8
|
+
}
|
|
9
|
+
return a.length - b.length;
|
|
10
|
+
}
|
|
2
11
|
/** Matrix canonical JSON: sorted keys, no whitespace, UTF-8, integers only. */
|
|
3
12
|
function canonicalJson(value) {
|
|
4
13
|
if (value === null || typeof value !== "object") {
|
|
@@ -7,7 +16,7 @@ function canonicalJson(value) {
|
|
|
7
16
|
}
|
|
8
17
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
9
18
|
const obj = value;
|
|
10
|
-
return `{${Object.keys(obj).filter((k) => obj[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
|
|
19
|
+
return `{${Object.keys(obj).filter((k) => obj[k] !== void 0).sort(byCodePoint).map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
|
|
11
20
|
}
|
|
12
21
|
function isObject(value) {
|
|
13
22
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
package/dist/core/keys.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Row } from "../storage/types.js";
|
|
1
2
|
import { Homeserver } from "../homeserver.js";
|
|
2
3
|
//#region src/core/keys.d.ts
|
|
3
4
|
/** End-to-end encryption key storage (device keys, one-time keys, cross-signing). */
|
|
@@ -28,7 +29,15 @@ export declare class KeyService {
|
|
|
28
29
|
uploadCrossSigning(userId: string, body: Record<string, any>): Promise<void>;
|
|
29
30
|
uploadSignatures(signer: string, body: Record<string, any>): Promise<Record<string, any>>;
|
|
30
31
|
/** Users (sharing a room with `userId`) whose device lists changed after `since`. */
|
|
31
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Device-list changes for /sync and /keys/changes. /sync passes what it has already read (the
|
|
34
|
+
* membership changes, the device-list rows, the user's joined rooms) to save the queries.
|
|
35
|
+
*/
|
|
36
|
+
changesSince(userId: string, since: number, upTo: number, known?: {
|
|
37
|
+
memberChanges?: Row[];
|
|
38
|
+
deviceRows?: Row[];
|
|
39
|
+
joinedRooms?: string[];
|
|
40
|
+
}): Promise<{
|
|
32
41
|
changed: string[];
|
|
33
42
|
left: string[];
|
|
34
43
|
}>;
|
package/dist/core/keys.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isObject } from "./json.js";
|
|
2
|
+
import { UNSTABLE } from "./m.js";
|
|
2
3
|
import { errors } from "./errors.js";
|
|
3
4
|
//#region src/core/keys.ts
|
|
4
5
|
/** End-to-end encryption key storage (device keys, one-time keys, cross-signing). */
|
|
@@ -78,7 +79,7 @@ var KeyService = class {
|
|
|
78
79
|
seq
|
|
79
80
|
}));
|
|
80
81
|
}
|
|
81
|
-
const fallbackKeys = body.fallback_keys ?? body[
|
|
82
|
+
const fallbackKeys = body.fallback_keys ?? body[UNSTABLE.ORG.MATRIX.MSC2732.FALLBACK_KEYS];
|
|
82
83
|
if (isObject(fallbackKeys)) for (const [fullId, key] of Object.entries(fallbackKeys)) {
|
|
83
84
|
const [algorithm] = fullId.split(":");
|
|
84
85
|
const existing = await this.hs.db.findOne("fallback_keys", [...device, {
|
|
@@ -98,39 +99,45 @@ var KeyService = class {
|
|
|
98
99
|
return this.oneTimeKeyCounts(userId, deviceId);
|
|
99
100
|
}
|
|
100
101
|
async oneTimeKeyCounts(userId, deviceId) {
|
|
101
|
-
const rows = await this.hs.db.findMany("one_time_keys", {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
102
|
+
const rows = await this.hs.db.findMany("one_time_keys", {
|
|
103
|
+
where: [
|
|
104
|
+
{
|
|
105
|
+
field: "user_id",
|
|
106
|
+
value: userId
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
field: "device_id",
|
|
110
|
+
value: deviceId
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
field: "fallback",
|
|
114
|
+
value: false
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
fields: ["algorithm"]
|
|
118
|
+
});
|
|
115
119
|
const counts = { signed_curve25519: 0 };
|
|
116
120
|
for (const r of rows) counts[r.algorithm] = (counts[r.algorithm] ?? 0) + 1;
|
|
117
121
|
return counts;
|
|
118
122
|
}
|
|
119
123
|
async unusedFallbackKeyTypes(userId, deviceId) {
|
|
120
|
-
const rows = await this.hs.db.findMany("fallback_keys", {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
124
|
+
const rows = await this.hs.db.findMany("fallback_keys", {
|
|
125
|
+
where: [
|
|
126
|
+
{
|
|
127
|
+
field: "user_id",
|
|
128
|
+
value: userId
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
field: "device_id",
|
|
132
|
+
value: deviceId
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
field: "used",
|
|
136
|
+
value: false
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
fields: ["algorithm"]
|
|
140
|
+
});
|
|
134
141
|
return [...new Set(rows.map((r) => r.algorithm))];
|
|
135
142
|
}
|
|
136
143
|
async query(requester, deviceKeys, timeoutMs = 1e4) {
|
|
@@ -412,22 +419,18 @@ var KeyService = class {
|
|
|
412
419
|
return { failures };
|
|
413
420
|
}
|
|
414
421
|
/** Users (sharing a room with `userId`) whose device lists changed after `since`. */
|
|
415
|
-
|
|
422
|
+
/**
|
|
423
|
+
* Device-list changes for /sync and /keys/changes. /sync passes what it has already read (the
|
|
424
|
+
* membership changes, the device-list rows, the user's joined rooms) to save the queries.
|
|
425
|
+
*/
|
|
426
|
+
async changesSince(userId, since, upTo, known = {}) {
|
|
416
427
|
const sharing = await this.hs.rooms.usersSharingRooms(userId);
|
|
417
|
-
const rows = await this.hs.db.findMany("device_list_changes",
|
|
418
|
-
field: "stream",
|
|
419
|
-
operator: "gt",
|
|
420
|
-
value: since
|
|
421
|
-
}] });
|
|
428
|
+
const rows = known.deviceRows ?? await this.hs.db.findMany("device_list_changes", deviceListChangesQuery(sharing, since));
|
|
422
429
|
const changed = /* @__PURE__ */ new Set();
|
|
423
430
|
for (const r of rows) if (r.stream <= upTo && sharing.has(r.user_id)) changed.add(r.user_id);
|
|
424
431
|
const left = /* @__PURE__ */ new Set();
|
|
425
|
-
const memberRows = await this.hs.
|
|
426
|
-
|
|
427
|
-
operator: "gt",
|
|
428
|
-
value: since
|
|
429
|
-
}] });
|
|
430
|
-
const myRooms = /* @__PURE__ */ new Set([...await this.hs.rooms.roomsForUser(userId), ...memberRows.filter((m) => m.user_id === userId).map((m) => m.room_id)]);
|
|
432
|
+
const memberRows = known.memberChanges ?? await this.hs.rooms.membershipChangesSince(userId, since);
|
|
433
|
+
const myRooms = /* @__PURE__ */ new Set([...known.joinedRooms ?? await this.hs.rooms.roomsForUser(userId), ...memberRows.filter((m) => m.user_id === userId).map((m) => m.room_id)]);
|
|
431
434
|
for (const m of memberRows) {
|
|
432
435
|
if (m.stream > upTo || !myRooms.has(m.room_id)) continue;
|
|
433
436
|
if (m.user_id === userId) {
|
|
@@ -445,5 +448,22 @@ var KeyService = class {
|
|
|
445
448
|
};
|
|
446
449
|
}
|
|
447
450
|
};
|
|
451
|
+
/**
|
|
452
|
+
* The device-list changes after `since` of the users sharing a room with someone.
|
|
453
|
+
* https://github.com/k18k/matrix-nodejs-homeserver/issues/31: this read every device-list change
|
|
454
|
+
* on the server since `since`; it asks for the users sharing a room only (up to a size where an IN
|
|
455
|
+
* list stays reasonable for the database).
|
|
456
|
+
*/
|
|
457
|
+
function deviceListChangesQuery(sharing, since) {
|
|
458
|
+
return { where: [{
|
|
459
|
+
field: "stream",
|
|
460
|
+
operator: "gt",
|
|
461
|
+
value: since
|
|
462
|
+
}, ...sharing.size <= 5e3 ? [{
|
|
463
|
+
field: "user_id",
|
|
464
|
+
operator: "in",
|
|
465
|
+
value: [...sharing]
|
|
466
|
+
}] : []] };
|
|
467
|
+
}
|
|
448
468
|
//#endregion
|
|
449
|
-
export { KeyService };
|
|
469
|
+
export { KeyService, deviceListChangesQuery };
|