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
package/dist/core/m.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
//#region src/core/m.ts
|
|
2
|
+
function deepFreeze(o) {
|
|
3
|
+
for (const v of Object.values(o)) if (typeof v === "object" && v !== null) deepFreeze(v);
|
|
4
|
+
return Object.freeze(o);
|
|
5
|
+
}
|
|
6
|
+
const M = deepFreeze({
|
|
7
|
+
BEACON_INFO: "m.beacon_info",
|
|
8
|
+
CALL: { INVITE: "m.call.invite" },
|
|
9
|
+
CHANGE_PASSWORD: "m.change_password",
|
|
10
|
+
DEVICE_LIST_UPDATE: "m.device_list_update",
|
|
11
|
+
DIRECT: "m.direct",
|
|
12
|
+
DIRECT_TO_DEVICE: "m.direct_to_device",
|
|
13
|
+
FEDERATE: "m.federate",
|
|
14
|
+
FULLY_READ: "m.fully_read",
|
|
15
|
+
GET_LOGIN_TOKEN: "m.get_login_token",
|
|
16
|
+
HEROES: "m.heroes",
|
|
17
|
+
HOMESERVER: "m.homeserver",
|
|
18
|
+
ID: {
|
|
19
|
+
PHONE: "m.id.phone",
|
|
20
|
+
THIRDPARTY: "m.id.thirdparty",
|
|
21
|
+
USER: "m.id.user"
|
|
22
|
+
},
|
|
23
|
+
IGNORED_USER_LIST: "m.ignored_user_list",
|
|
24
|
+
IN_REPLY_TO: "m.in_reply_to",
|
|
25
|
+
INVITED_MEMBER_COUNT: "m.invited_member_count",
|
|
26
|
+
JOINED_MEMBER_COUNT: "m.joined_member_count",
|
|
27
|
+
LOGIN: {
|
|
28
|
+
APPLICATION_SERVICE: "m.login.application_service",
|
|
29
|
+
DUMMY: "m.login.dummy",
|
|
30
|
+
EMAIL: { IDENTITY: "m.login.email.identity" },
|
|
31
|
+
MSISDN: "m.login.msisdn",
|
|
32
|
+
PASSWORD: "m.login.password",
|
|
33
|
+
REGISTRATION_TOKEN: "m.login.registration_token",
|
|
34
|
+
SSO: "m.login.sso",
|
|
35
|
+
TOKEN: "m.login.token"
|
|
36
|
+
},
|
|
37
|
+
NOTICE: "m.notice",
|
|
38
|
+
OAUTH: "m.oauth",
|
|
39
|
+
POLL: { START: "m.poll.start" },
|
|
40
|
+
PRESENCE: "m.presence",
|
|
41
|
+
PROFILE_FIELDS: "m.profile_fields",
|
|
42
|
+
PUSH_RULES: "m.push_rules",
|
|
43
|
+
REACTION: "m.reaction",
|
|
44
|
+
READ: {
|
|
45
|
+
_: "m.read",
|
|
46
|
+
PRIVATE: "m.read.private"
|
|
47
|
+
},
|
|
48
|
+
RECEIPT: "m.receipt",
|
|
49
|
+
RELATES_TO: "m.relates_to",
|
|
50
|
+
RELATIONS: "m.relations",
|
|
51
|
+
RELATIONSHIP: "m.relationship",
|
|
52
|
+
REPLACE: "m.replace",
|
|
53
|
+
ROLE: {
|
|
54
|
+
ADMIN: "m.role.admin",
|
|
55
|
+
SECURITY: "m.role.security"
|
|
56
|
+
},
|
|
57
|
+
ROOM: {
|
|
58
|
+
ALIASES: "m.room.aliases",
|
|
59
|
+
AVATAR: "m.room.avatar",
|
|
60
|
+
CANONICAL_ALIAS: "m.room.canonical_alias",
|
|
61
|
+
CREATE: "m.room.create",
|
|
62
|
+
ENCRYPTED: "m.room.encrypted",
|
|
63
|
+
ENCRYPTION: "m.room.encryption",
|
|
64
|
+
GUEST_ACCESS: "m.room.guest_access",
|
|
65
|
+
HISTORY_VISIBILITY: "m.room.history_visibility",
|
|
66
|
+
JOIN_RULES: "m.room.join_rules",
|
|
67
|
+
MEMBER: "m.room.member",
|
|
68
|
+
MESSAGE: "m.room.message",
|
|
69
|
+
NAME: "m.room.name",
|
|
70
|
+
POLICY: "m.room.policy",
|
|
71
|
+
POWER_LEVELS: "m.room.power_levels",
|
|
72
|
+
REDACTION: "m.room.redaction",
|
|
73
|
+
SERVER_ACL: "m.room.server_acl",
|
|
74
|
+
THIRD_PARTY_INVITE: "m.room.third_party_invite",
|
|
75
|
+
TOMBSTONE: "m.room.tombstone",
|
|
76
|
+
TOPIC: "m.room.topic"
|
|
77
|
+
},
|
|
78
|
+
ROOM_MEMBERSHIP: "m.room_membership",
|
|
79
|
+
ROOM_VERSIONS: "m.room_versions",
|
|
80
|
+
RULE: {
|
|
81
|
+
CALL: ".m.rule.call",
|
|
82
|
+
CONTAINS_DISPLAY_NAME: ".m.rule.contains_display_name",
|
|
83
|
+
CONTAINS_USER_NAME: ".m.rule.contains_user_name",
|
|
84
|
+
ENCRYPTED: ".m.rule.encrypted",
|
|
85
|
+
ENCRYPTED_ROOM_ONE_TO_ONE: ".m.rule.encrypted_room_one_to_one",
|
|
86
|
+
INVITE_FOR_ME: ".m.rule.invite_for_me",
|
|
87
|
+
IS_ROOM_MENTION: ".m.rule.is_room_mention",
|
|
88
|
+
IS_USER_MENTION: ".m.rule.is_user_mention",
|
|
89
|
+
MASTER: ".m.rule.master",
|
|
90
|
+
MEMBER_EVENT: ".m.rule.member_event",
|
|
91
|
+
MESSAGE: ".m.rule.message",
|
|
92
|
+
REACTION: ".m.rule.reaction",
|
|
93
|
+
ROOM: { SERVER_ACL: ".m.rule.room.server_acl" },
|
|
94
|
+
ROOM_ONE_TO_ONE: ".m.rule.room_one_to_one",
|
|
95
|
+
ROOMNOTIF: ".m.rule.roomnotif",
|
|
96
|
+
SUPPRESS_EDITS: ".m.rule.suppress_edits",
|
|
97
|
+
SUPPRESS_NOTICES: ".m.rule.suppress_notices",
|
|
98
|
+
TOMBSTONE: ".m.rule.tombstone"
|
|
99
|
+
},
|
|
100
|
+
SERVER: "m.server",
|
|
101
|
+
SERVER_NOTICE: "m.server_notice",
|
|
102
|
+
SET_AVATAR_URL: "m.set_avatar_url",
|
|
103
|
+
SET_DISPLAYNAME: "m.set_displayname",
|
|
104
|
+
SIGNING_KEY_UPDATE: "m.signing_key_update",
|
|
105
|
+
SPACE: {
|
|
106
|
+
_: "m.space",
|
|
107
|
+
CHILD: "m.space.child"
|
|
108
|
+
},
|
|
109
|
+
STICKER: "m.sticker",
|
|
110
|
+
TAG: "m.tag",
|
|
111
|
+
TEXT: "m.text",
|
|
112
|
+
THREAD: "m.thread",
|
|
113
|
+
THREEPID_CHANGES: "m.3pid_changes",
|
|
114
|
+
TOPIC: "m.topic",
|
|
115
|
+
TYPING: "m.typing",
|
|
116
|
+
UPLOAD: { SIZE: "m.upload.size" }
|
|
117
|
+
});
|
|
118
|
+
const UNSTABLE = deepFreeze({
|
|
119
|
+
DE: { SORUNOME: { MSC2409: {
|
|
120
|
+
EPHEMERAL: "de.sorunome.msc2409.ephemeral",
|
|
121
|
+
PUSH_EPHEMERAL: "de.sorunome.msc2409.push_ephemeral"
|
|
122
|
+
} } },
|
|
123
|
+
IO: { ELEMENT: {
|
|
124
|
+
MSC4306: {
|
|
125
|
+
RULE: {
|
|
126
|
+
SUBSCRIBED_THREAD: ".io.element.msc4306.rule.subscribed_thread",
|
|
127
|
+
UNSUBSCRIBED_THREAD: ".io.element.msc4306.rule.unsubscribed_thread"
|
|
128
|
+
},
|
|
129
|
+
THREAD_SUBSCRIPTION: "io.element.msc4306.thread_subscription"
|
|
130
|
+
},
|
|
131
|
+
MSC4308: { THREAD_SUBSCRIPTIONS: "io.element.msc4308.thread_subscriptions" }
|
|
132
|
+
} },
|
|
133
|
+
ORG: { MATRIX: {
|
|
134
|
+
CROSS_SIGNING_RESET: "org.matrix.cross_signing_reset",
|
|
135
|
+
CUSTOM: { HTML: "org.matrix.custom.html" },
|
|
136
|
+
MSC2732: { FALLBACK_KEYS: "org.matrix.msc2732.fallback_keys" },
|
|
137
|
+
MSC3202: { DEVICE_ID: "org.matrix.msc3202.device_id" },
|
|
138
|
+
MSC3381: { POLL: {
|
|
139
|
+
END: "org.matrix.msc3381.poll.end",
|
|
140
|
+
RESPONSE: "org.matrix.msc3381.poll.response",
|
|
141
|
+
START: "org.matrix.msc3381.poll.start"
|
|
142
|
+
} },
|
|
143
|
+
MSC3874: {
|
|
144
|
+
NOT_REL_TYPES: "org.matrix.msc3874.not_rel_types",
|
|
145
|
+
REL_TYPES: "org.matrix.msc3874.rel_types"
|
|
146
|
+
},
|
|
147
|
+
MSC3916: { STABLE: "org.matrix.msc3916.stable" },
|
|
148
|
+
MSC3930: { RULE: {
|
|
149
|
+
POLL_END: ".org.matrix.msc3930.rule.poll_end",
|
|
150
|
+
POLL_END_ONE_TO_ONE: ".org.matrix.msc3930.rule.poll_end_one_to_one",
|
|
151
|
+
POLL_RESPONSE: ".org.matrix.msc3930.rule.poll_response",
|
|
152
|
+
POLL_START: ".org.matrix.msc3930.rule.poll_start",
|
|
153
|
+
POLL_START_ONE_TO_ONE: ".org.matrix.msc3930.rule.poll_start_one_to_one"
|
|
154
|
+
} },
|
|
155
|
+
MSC4140: {
|
|
156
|
+
_: "org.matrix.msc4140",
|
|
157
|
+
DELAY: "org.matrix.msc4140.delay",
|
|
158
|
+
MAX_DELAY: "org.matrix.msc4140.max_delay"
|
|
159
|
+
},
|
|
160
|
+
MSC4143: {
|
|
161
|
+
_: "org.matrix.msc4143",
|
|
162
|
+
RTC_FOCI: "org.matrix.msc4143.rtc_foci"
|
|
163
|
+
},
|
|
164
|
+
MSC4155: {
|
|
165
|
+
ERRCODE: "org.matrix.msc4155.errcode",
|
|
166
|
+
INVITE_PERMISSION_CONFIG: "org.matrix.msc4155.invite_permission_config"
|
|
167
|
+
},
|
|
168
|
+
MSC4222: {
|
|
169
|
+
_: "org.matrix.msc4222",
|
|
170
|
+
STATE_AFTER: "org.matrix.msc4222.state_after",
|
|
171
|
+
USE_STATE_AFTER: "org.matrix.msc4222.use_state_after"
|
|
172
|
+
},
|
|
173
|
+
MSC4306: "org.matrix.msc4306",
|
|
174
|
+
MSC4429: {
|
|
175
|
+
PROFILE_FIELDS: "org.matrix.msc4429.profile_fields",
|
|
176
|
+
USERS: "org.matrix.msc4429.users"
|
|
177
|
+
},
|
|
178
|
+
SIMPLIFIED_MSC3575: "org.matrix.simplified_msc3575"
|
|
179
|
+
} }
|
|
180
|
+
});
|
|
181
|
+
//#endregion
|
|
182
|
+
export { M, UNSTABLE };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ClusterAdapter } from "./cluster.js";
|
|
2
|
+
//#region src/core/notifier.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Wakes long-polling `/sync` requests when something relevant happens. Notifications
|
|
5
|
+
* are broadcast through the cluster adapter so waiters on other instances wake too.
|
|
6
|
+
*/
|
|
7
|
+
export declare class Notifier {
|
|
8
|
+
private waiters;
|
|
9
|
+
private cluster;
|
|
10
|
+
private instanceId;
|
|
11
|
+
constructor(cluster: ClusterAdapter, instanceId: string);
|
|
12
|
+
notify(userIds: Iterable<string>): void;
|
|
13
|
+
private wakeLocal;
|
|
14
|
+
notifyAll(): void;
|
|
15
|
+
wait(userId: string, timeoutMs: number, signal?: AbortSignal): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/core/notifier.ts
|
|
2
|
+
/**
|
|
3
|
+
* Wakes long-polling `/sync` requests when something relevant happens. Notifications
|
|
4
|
+
* are broadcast through the cluster adapter so waiters on other instances wake too.
|
|
5
|
+
*/
|
|
6
|
+
var Notifier = class {
|
|
7
|
+
waiters = /* @__PURE__ */ new Map();
|
|
8
|
+
cluster;
|
|
9
|
+
instanceId;
|
|
10
|
+
constructor(cluster, instanceId) {
|
|
11
|
+
this.cluster = cluster;
|
|
12
|
+
this.instanceId = instanceId;
|
|
13
|
+
cluster.subscribe("notify", (msg) => {
|
|
14
|
+
if (msg?.from !== this.instanceId && Array.isArray(msg?.users)) this.wakeLocal(msg.users);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
notify(userIds) {
|
|
18
|
+
const users = [...userIds];
|
|
19
|
+
if (!users.length) return;
|
|
20
|
+
this.wakeLocal(users);
|
|
21
|
+
this.cluster.publish("notify", {
|
|
22
|
+
from: this.instanceId,
|
|
23
|
+
users
|
|
24
|
+
}).catch(() => {});
|
|
25
|
+
}
|
|
26
|
+
wakeLocal(userIds) {
|
|
27
|
+
for (const u of userIds) {
|
|
28
|
+
const set = this.waiters.get(u);
|
|
29
|
+
if (!set) continue;
|
|
30
|
+
this.waiters.delete(u);
|
|
31
|
+
for (const wake of set) wake();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
notifyAll() {
|
|
35
|
+
this.wakeLocal([...this.waiters.keys()]);
|
|
36
|
+
}
|
|
37
|
+
wait(userId, timeoutMs, signal) {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
let set = this.waiters.get(userId);
|
|
40
|
+
if (!set) this.waiters.set(userId, set = /* @__PURE__ */ new Set());
|
|
41
|
+
const done = () => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
set.delete(done);
|
|
44
|
+
signal?.removeEventListener("abort", done);
|
|
45
|
+
resolve();
|
|
46
|
+
};
|
|
47
|
+
const timer = setTimeout(done, timeoutMs);
|
|
48
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
49
|
+
set.add(done);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { Notifier };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Session } from "./auth.js";
|
|
2
|
+
import { Homeserver } from "../homeserver.js";
|
|
3
|
+
//#region src/core/oauth.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Delegate authentication to an external OAuth 2.0 / OpenID Connect provider
|
|
6
|
+
* (Matrix "OAuth 2.0 API", e.g. matrix-authentication-service, Keycloak, Authentik).
|
|
7
|
+
*/
|
|
8
|
+
export interface OAuthDelegationOptions {
|
|
9
|
+
/** Issuer URL of the authorization server. */
|
|
10
|
+
issuer: string;
|
|
11
|
+
/** Client credentials the homeserver uses for token introspection. */
|
|
12
|
+
clientId: string;
|
|
13
|
+
clientSecret: string;
|
|
14
|
+
/** Override the introspection endpoint instead of discovering it. */
|
|
15
|
+
introspectionEndpoint?: string;
|
|
16
|
+
/** Where users manage their account (advertised to clients). */
|
|
17
|
+
accountManagementUrl?: string;
|
|
18
|
+
/** Map introspection claims to a Matrix localpart. Default: `username`, then `sub`. */
|
|
19
|
+
localpartFromClaims?: (claims: Record<string, any>) => string | null;
|
|
20
|
+
/** Scope granting server admin rights. Default: "urn:synapse:admin:*". */
|
|
21
|
+
adminScope?: string;
|
|
22
|
+
/** How long successful introspections are cached, in ms. Default: 60 000. */
|
|
23
|
+
cacheTtlMs?: number;
|
|
24
|
+
}
|
|
25
|
+
export declare class OAuthDelegation {
|
|
26
|
+
private hs;
|
|
27
|
+
readonly options: OAuthDelegationOptions;
|
|
28
|
+
private metadata;
|
|
29
|
+
private metadataFetched;
|
|
30
|
+
private cache;
|
|
31
|
+
constructor(hs: Homeserver, options: OAuthDelegationOptions);
|
|
32
|
+
/** Authorization server metadata (RFC 8414 / OIDC discovery), cached for an hour. */
|
|
33
|
+
serverMetadata(): Promise<Record<string, any>>;
|
|
34
|
+
/** Validate an access token by introspection, provisioning the user and device on first use. */
|
|
35
|
+
authenticate(token: string): Promise<Session>;
|
|
36
|
+
forget(token: string): void;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { errors } from "./errors.js";
|
|
2
|
+
import { LOCALPART_RE } from "./ids.js";
|
|
3
|
+
//#region src/core/oauth.ts
|
|
4
|
+
const API_SCOPES = ["urn:matrix:client:api:*", "urn:matrix:org.matrix.msc2967.client:api:*"];
|
|
5
|
+
const DEVICE_SCOPE_PREFIXES = ["urn:matrix:client:device:", "urn:matrix:org.matrix.msc2967.client:device:"];
|
|
6
|
+
var OAuthDelegation = class {
|
|
7
|
+
hs;
|
|
8
|
+
options;
|
|
9
|
+
metadata = null;
|
|
10
|
+
metadataFetched = 0;
|
|
11
|
+
cache = /* @__PURE__ */ new Map();
|
|
12
|
+
constructor(hs, options) {
|
|
13
|
+
this.hs = hs;
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
/** Authorization server metadata (RFC 8414 / OIDC discovery), cached for an hour. */
|
|
17
|
+
async serverMetadata() {
|
|
18
|
+
if (this.metadata && Date.now() - this.metadataFetched < 36e5) return this.metadata;
|
|
19
|
+
const base = this.options.issuer.replace(/\/$/, "");
|
|
20
|
+
let res = await this.hs.fetch(`${base}/.well-known/openid-configuration`, { signal: AbortSignal.timeout(1e4) });
|
|
21
|
+
if (!res.ok) res = await this.hs.fetch(`${base}/.well-known/oauth-authorization-server`, { signal: AbortSignal.timeout(1e4) });
|
|
22
|
+
if (!res.ok) throw new Error(`OAuth discovery failed with ${res.status}`);
|
|
23
|
+
const meta = await res.json();
|
|
24
|
+
if (this.options.accountManagementUrl) meta.account_management_uri = this.options.accountManagementUrl;
|
|
25
|
+
this.metadata = meta;
|
|
26
|
+
this.metadataFetched = Date.now();
|
|
27
|
+
return meta;
|
|
28
|
+
}
|
|
29
|
+
/** Validate an access token by introspection, provisioning the user and device on first use. */
|
|
30
|
+
async authenticate(token) {
|
|
31
|
+
const cached = this.cache.get(token);
|
|
32
|
+
if (cached && cached.expires > Date.now()) return cached.session;
|
|
33
|
+
const endpoint = this.options.introspectionEndpoint ?? (await this.serverMetadata()).introspection_endpoint;
|
|
34
|
+
if (!endpoint) throw new Error("Authorization server has no introspection endpoint");
|
|
35
|
+
const res = await this.hs.fetch(endpoint, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: {
|
|
38
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
39
|
+
authorization: `Basic ${Buffer.from(`${encodeURIComponent(this.options.clientId)}:${encodeURIComponent(this.options.clientSecret)}`).toString("base64")}`
|
|
40
|
+
},
|
|
41
|
+
body: new URLSearchParams({
|
|
42
|
+
token,
|
|
43
|
+
token_type_hint: "access_token"
|
|
44
|
+
}),
|
|
45
|
+
signal: AbortSignal.timeout(1e4)
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok) throw errors.unknownToken("Token introspection failed");
|
|
48
|
+
const claims = await res.json();
|
|
49
|
+
if (!claims.active) throw errors.unknownToken();
|
|
50
|
+
const scopes = String(claims.scope ?? "").split(" ");
|
|
51
|
+
if (!scopes.some((s) => API_SCOPES.includes(s))) throw errors.unknownToken("Token lacks the Matrix API scope");
|
|
52
|
+
let deviceId = null;
|
|
53
|
+
for (const s of scopes) for (const p of DEVICE_SCOPE_PREFIXES) if (s.startsWith(p)) deviceId = s.slice(p.length);
|
|
54
|
+
if (!deviceId) throw errors.unknownToken("Token lacks a device scope");
|
|
55
|
+
const localpart = (this.options.localpartFromClaims?.(claims) ?? claims.username ?? claims.sub ?? "").toLowerCase();
|
|
56
|
+
if (!LOCALPART_RE.test(localpart)) throw errors.unknownToken("Token subject cannot be mapped to a Matrix user");
|
|
57
|
+
const userId = `@${localpart}:${this.hs.serverName}`;
|
|
58
|
+
let user = await this.hs.db.findOne("users", [{
|
|
59
|
+
field: "user_id",
|
|
60
|
+
value: userId
|
|
61
|
+
}]);
|
|
62
|
+
if (!user) {
|
|
63
|
+
await this.hs.admin.createUser(localpart, { displayname: claims.name ?? localpart });
|
|
64
|
+
user = await this.hs.db.findOne("users", [{
|
|
65
|
+
field: "user_id",
|
|
66
|
+
value: userId
|
|
67
|
+
}]);
|
|
68
|
+
}
|
|
69
|
+
if (user.deactivated) throw errors.userDeactivated();
|
|
70
|
+
const deviceWhere = [{
|
|
71
|
+
field: "user_id",
|
|
72
|
+
value: userId
|
|
73
|
+
}, {
|
|
74
|
+
field: "device_id",
|
|
75
|
+
value: deviceId
|
|
76
|
+
}];
|
|
77
|
+
if (!await this.hs.db.findOne("devices", deviceWhere)) {
|
|
78
|
+
await this.hs.db.create("devices", {
|
|
79
|
+
user_id: userId,
|
|
80
|
+
device_id: deviceId,
|
|
81
|
+
display_name: null,
|
|
82
|
+
last_seen_ts: Date.now(),
|
|
83
|
+
last_seen_ip: null
|
|
84
|
+
});
|
|
85
|
+
await this.hs.keys.markDeviceListChanged(userId);
|
|
86
|
+
}
|
|
87
|
+
const session = {
|
|
88
|
+
userId,
|
|
89
|
+
deviceId,
|
|
90
|
+
accessToken: token,
|
|
91
|
+
isGuest: false,
|
|
92
|
+
isAdmin: !!user.is_admin || scopes.includes(this.options.adminScope ?? "urn:synapse:admin:*")
|
|
93
|
+
};
|
|
94
|
+
const ttl = this.options.cacheTtlMs ?? 6e4;
|
|
95
|
+
const exp = typeof claims.exp === "number" ? claims.exp * 1e3 : Infinity;
|
|
96
|
+
if (this.cache.size > 5e4) this.cache.clear();
|
|
97
|
+
this.cache.set(token, {
|
|
98
|
+
session,
|
|
99
|
+
expires: Math.min(Date.now() + ttl, exp)
|
|
100
|
+
});
|
|
101
|
+
return session;
|
|
102
|
+
}
|
|
103
|
+
forget(token) {
|
|
104
|
+
this.cache.delete(token);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
//#endregion
|
|
108
|
+
export { OAuthDelegation };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Homeserver } from "../homeserver.js";
|
|
2
|
+
//#region src/core/partialstate.d.ts
|
|
3
|
+
export declare class PartialStateService {
|
|
4
|
+
private readonly hs;
|
|
5
|
+
private readonly partial;
|
|
6
|
+
private readonly completed;
|
|
7
|
+
private readonly running;
|
|
8
|
+
private readonly completedStreams;
|
|
9
|
+
private stopped;
|
|
10
|
+
constructor(hs: Homeserver);
|
|
11
|
+
isPartial(roomId: string): Promise<boolean>;
|
|
12
|
+
/** Servers the joining server told us were in the room (used as destinations and resync sources). */
|
|
13
|
+
servers(roomId: string): Promise<string[]>;
|
|
14
|
+
/** Stream position at which the room got its full state (null if it never was partial). */
|
|
15
|
+
completedAt(roomId: string): Promise<number | null>;
|
|
16
|
+
/**
|
|
17
|
+
* A device list update for a user we do not share a (known) room with: if they may be a member
|
|
18
|
+
* of a partial-state room hosted by the sender, remember them for when the state is complete.
|
|
19
|
+
*/
|
|
20
|
+
notePendingDeviceUpdate(userId: string, origin: string): Promise<void>;
|
|
21
|
+
markPartial(roomId: string, joinEventId: string, origin: string, servers: string[]): Promise<void>;
|
|
22
|
+
/** Resolve once the room has its full state (or the signal aborts / the server closes). */
|
|
23
|
+
waitForFullState(roomId: string, signal?: AbortSignal): Promise<void>;
|
|
24
|
+
/** Resume resyncs of rooms left partial (e.g. by a restart). */
|
|
25
|
+
resume(): Promise<void>;
|
|
26
|
+
stop(): void;
|
|
27
|
+
startResync(roomId: string): void;
|
|
28
|
+
private resync;
|
|
29
|
+
private tryResync;
|
|
30
|
+
private finish;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { M } from "./m.js";
|
|
2
|
+
import { eventIdOf, roomVersionFeatures, stateKey } from "./events.js";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
//#region src/core/partialstate.ts
|
|
5
|
+
const RESYNC_CLAIM_MS = 6e5;
|
|
6
|
+
var PartialStateService = class {
|
|
7
|
+
hs;
|
|
8
|
+
partial = /* @__PURE__ */ new Map();
|
|
9
|
+
completed = new EventEmitter();
|
|
10
|
+
running = /* @__PURE__ */ new Set();
|
|
11
|
+
completedStreams = /* @__PURE__ */ new Map();
|
|
12
|
+
stopped = false;
|
|
13
|
+
constructor(hs) {
|
|
14
|
+
this.hs = hs;
|
|
15
|
+
this.completed.setMaxListeners(0);
|
|
16
|
+
hs.cluster.subscribe("partial-state", (msg) => {
|
|
17
|
+
if (typeof msg?.roomId !== "string") return;
|
|
18
|
+
this.partial.delete(msg.roomId);
|
|
19
|
+
this.completedStreams.delete(msg.roomId);
|
|
20
|
+
this.completed.emit(msg.roomId);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async isPartial(roomId) {
|
|
24
|
+
const cached = this.partial.get(roomId);
|
|
25
|
+
if (cached !== void 0) return cached;
|
|
26
|
+
const row = await this.hs.db.findOne("partial_state_rooms", [{
|
|
27
|
+
field: "room_id",
|
|
28
|
+
value: roomId
|
|
29
|
+
}]);
|
|
30
|
+
const partial = !!row && row.completed_stream === null;
|
|
31
|
+
if (this.partial.size > 1e4) this.partial.clear();
|
|
32
|
+
this.partial.set(roomId, partial);
|
|
33
|
+
return partial;
|
|
34
|
+
}
|
|
35
|
+
/** Servers the joining server told us were in the room (used as destinations and resync sources). */
|
|
36
|
+
async servers(roomId) {
|
|
37
|
+
if (!await this.isPartial(roomId)) return [];
|
|
38
|
+
const row = await this.hs.db.findOne("partial_state_rooms", [{
|
|
39
|
+
field: "room_id",
|
|
40
|
+
value: roomId
|
|
41
|
+
}]);
|
|
42
|
+
return Array.isArray(row?.servers) ? row.servers : [];
|
|
43
|
+
}
|
|
44
|
+
/** Stream position at which the room got its full state (null if it never was partial). */
|
|
45
|
+
async completedAt(roomId) {
|
|
46
|
+
if (this.partial.get(roomId) === false && this.completedStreams.has(roomId)) return this.completedStreams.get(roomId);
|
|
47
|
+
const at = (await this.hs.db.findOne("partial_state_rooms", [{
|
|
48
|
+
field: "room_id",
|
|
49
|
+
value: roomId
|
|
50
|
+
}]))?.completed_stream ?? null;
|
|
51
|
+
if (this.completedStreams.size > 1e4) this.completedStreams.clear();
|
|
52
|
+
this.completedStreams.set(roomId, at);
|
|
53
|
+
return at;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A device list update for a user we do not share a (known) room with: if they may be a member
|
|
57
|
+
* of a partial-state room hosted by the sender, remember them for when the state is complete.
|
|
58
|
+
*/
|
|
59
|
+
async notePendingDeviceUpdate(userId, origin) {
|
|
60
|
+
const rows = (await this.hs.db.findMany("partial_state_rooms", {})).filter((r) => r.completed_stream === null && (r.servers ?? []).includes(origin));
|
|
61
|
+
for (const row of rows) {
|
|
62
|
+
const pending = Array.isArray(row.pending_device_users) ? row.pending_device_users : [];
|
|
63
|
+
if (pending.includes(userId)) continue;
|
|
64
|
+
await this.hs.db.update("partial_state_rooms", [{
|
|
65
|
+
field: "room_id",
|
|
66
|
+
value: row.room_id
|
|
67
|
+
}], { pending_device_users: [...pending, userId] });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async markPartial(roomId, joinEventId, origin, servers) {
|
|
71
|
+
await this.hs.db.upsert("partial_state_rooms", {
|
|
72
|
+
room_id: roomId,
|
|
73
|
+
join_event_id: joinEventId,
|
|
74
|
+
origin,
|
|
75
|
+
servers: [.../* @__PURE__ */ new Set([origin, ...servers])],
|
|
76
|
+
created_ts: Date.now(),
|
|
77
|
+
completed_stream: null,
|
|
78
|
+
pending_device_users: []
|
|
79
|
+
});
|
|
80
|
+
this.partial.set(roomId, true);
|
|
81
|
+
this.completedStreams.delete(roomId);
|
|
82
|
+
this.hs.cluster.publish("partial-state", { roomId }).catch(() => {});
|
|
83
|
+
this.startResync(roomId);
|
|
84
|
+
}
|
|
85
|
+
/** Resolve once the room has its full state (or the signal aborts / the server closes). */
|
|
86
|
+
async waitForFullState(roomId, signal) {
|
|
87
|
+
if (!await this.isPartial(roomId)) return;
|
|
88
|
+
await new Promise((resolve) => {
|
|
89
|
+
const done = () => {
|
|
90
|
+
clearInterval(poll);
|
|
91
|
+
this.completed.off(roomId, check);
|
|
92
|
+
signal?.removeEventListener("abort", done);
|
|
93
|
+
this.hs.off?.("close", done);
|
|
94
|
+
resolve();
|
|
95
|
+
};
|
|
96
|
+
const check = () => void this.isPartial(roomId).then((p) => {
|
|
97
|
+
if (!p) done();
|
|
98
|
+
}).catch(() => {});
|
|
99
|
+
this.completed.on(roomId, check);
|
|
100
|
+
signal?.addEventListener("abort", done);
|
|
101
|
+
this.hs.once?.("close", done);
|
|
102
|
+
const poll = setInterval(() => {
|
|
103
|
+
this.partial.delete(roomId);
|
|
104
|
+
check();
|
|
105
|
+
}, 5e3);
|
|
106
|
+
poll.unref?.();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** Resume resyncs of rooms left partial (e.g. by a restart). */
|
|
110
|
+
async resume() {
|
|
111
|
+
const rows = (await this.hs.db.findMany("partial_state_rooms", {})).filter((r) => r.completed_stream === null);
|
|
112
|
+
this.hs.logger.debug("Resuming partial state resyncs", rows.map((r) => r.room_id));
|
|
113
|
+
for (const row of rows) this.startResync(row.room_id);
|
|
114
|
+
}
|
|
115
|
+
stop() {
|
|
116
|
+
this.stopped = true;
|
|
117
|
+
}
|
|
118
|
+
startResync(roomId) {
|
|
119
|
+
if (this.running.has(roomId)) return;
|
|
120
|
+
this.running.add(roomId);
|
|
121
|
+
this.resync(roomId).catch((err) => this.hs.logger.warn("Partial state resync failed", roomId, err)).finally(() => this.running.delete(roomId));
|
|
122
|
+
}
|
|
123
|
+
async resync(roomId) {
|
|
124
|
+
const claim = `partial-resync:${roomId}`;
|
|
125
|
+
if (!await this.hs.cluster.setIfAbsent(claim, this.hs.instanceId, RESYNC_CLAIM_MS)) {
|
|
126
|
+
this.hs.logger.debug("Partial state resync of", roomId, "is claimed elsewhere");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.hs.logger.debug("Partial state resync of", roomId, "started");
|
|
130
|
+
try {
|
|
131
|
+
let delay = 1e3;
|
|
132
|
+
for (let attempt = 0; !this.stopped && !this.hs.isClosed; attempt++) {
|
|
133
|
+
const row = await this.hs.db.findOne("partial_state_rooms", [{
|
|
134
|
+
field: "room_id",
|
|
135
|
+
value: roomId
|
|
136
|
+
}]);
|
|
137
|
+
if (!row || row.completed_stream !== null) return;
|
|
138
|
+
if (await this.tryResync(roomId, row)) return;
|
|
139
|
+
if (this.stopped || this.hs.isClosed) return;
|
|
140
|
+
if (!await this.hs.rooms.serverIsJoined(roomId)) {
|
|
141
|
+
await this.finish(roomId);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
await new Promise((r) => setTimeout(r, delay).unref?.());
|
|
145
|
+
delay = Math.min(delay * 2, 6e4);
|
|
146
|
+
await this.hs.cluster.set(claim, this.hs.instanceId, RESYNC_CLAIM_MS);
|
|
147
|
+
}
|
|
148
|
+
} finally {
|
|
149
|
+
await this.hs.cluster.delete(claim).catch(() => {});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async tryResync(roomId, row) {
|
|
153
|
+
const version = await this.hs.rooms.roomVersion(roomId);
|
|
154
|
+
const join = await this.hs.rooms.getEvent(row.join_event_id);
|
|
155
|
+
if (!version || !join) {
|
|
156
|
+
await this.finish(roomId);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
const prevIds = (join.pdu.prev_events ?? []).map(eventIdOf);
|
|
160
|
+
const servers = [.../* @__PURE__ */ new Set([row.origin, ...row.servers ?? []])].filter((s) => s && s !== this.hs.serverName);
|
|
161
|
+
for (const server of servers) {
|
|
162
|
+
if (this.stopped || this.hs.isClosed) return true;
|
|
163
|
+
try {
|
|
164
|
+
const fullBefore = await this.hs.federation.stateFromPrevs(server, roomId, version, prevIds, { remote: true });
|
|
165
|
+
if (this.stopped || this.hs.isClosed) return true;
|
|
166
|
+
if (!fullBefore[stateKey(M.ROOM.CREATE, "")] && !roomVersionFeatures(version).v12) throw new Error("Resynced state lacks the create event");
|
|
167
|
+
const notified = /* @__PURE__ */ new Set([...row.servers ?? [], ...this.hs.federation.serversInRoom(await this.hs.rooms.currentState(roomId), false)]);
|
|
168
|
+
const missed = await this.hs.rooms.withLock(roomId, () => this.hs.rooms.completePartialState(roomId, version, row.join_event_id, fullBefore, notified));
|
|
169
|
+
await this.finish(roomId);
|
|
170
|
+
if (missed.length) for (const userId of (await this.hs.rooms.joinedMembers(roomId)).filter((u) => this.hs.isLocalUser(u))) await this.hs.federation.onDeviceListChange(userId, void 0, new Set(missed)).catch(() => {});
|
|
171
|
+
return true;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
this.hs.logger.debug("Partial state resync from", server, "failed", err);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
async finish(roomId) {
|
|
179
|
+
await this.hs.streams.write((stream) => this.hs.db.update("partial_state_rooms", [{
|
|
180
|
+
field: "room_id",
|
|
181
|
+
value: roomId
|
|
182
|
+
}], { completed_stream: stream }));
|
|
183
|
+
this.partial.set(roomId, false);
|
|
184
|
+
this.completedStreams.delete(roomId);
|
|
185
|
+
this.completed.emit(roomId);
|
|
186
|
+
this.hs.cluster.publish("partial-state", { roomId }).catch(() => {});
|
|
187
|
+
const members = await this.hs.rooms.joinedMembers(roomId);
|
|
188
|
+
this.hs.notifier.notify(members.filter((u) => this.hs.isLocalUser(u)));
|
|
189
|
+
const row = await this.hs.db.findOne("partial_state_rooms", [{
|
|
190
|
+
field: "room_id",
|
|
191
|
+
value: roomId
|
|
192
|
+
}]);
|
|
193
|
+
const pending = Array.isArray(row?.pending_device_users) ? row.pending_device_users : [];
|
|
194
|
+
await this.hs.keys.onPartialStateComplete(roomId, pending).catch((err) => this.hs.logger.warn("Device list refresh failed", err));
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
//#endregion
|
|
198
|
+
export { PartialStateService };
|