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,302 @@
|
|
|
1
|
+
import { loadSigningKey, randomString } from "./core/crypto.js";
|
|
2
|
+
import "./core/m.js";
|
|
3
|
+
import { SUPPORTED_ROOM_VERSIONS } from "./core/events.js";
|
|
4
|
+
import { PartialStateService } from "./core/partialstate.js";
|
|
5
|
+
import { RoomService } from "./core/rooms.js";
|
|
6
|
+
import { FederationService } from "./federation/index.js";
|
|
7
|
+
import { PolicyServers } from "./core/policy.js";
|
|
8
|
+
import { ThreepidService } from "./core/threepid.js";
|
|
9
|
+
import { IdentityServers } from "./core/identity.js";
|
|
10
|
+
import { AuthService } from "./core/auth.js";
|
|
11
|
+
import { KeyService } from "./core/keys.js";
|
|
12
|
+
import { Notifier } from "./core/notifier.js";
|
|
13
|
+
import { StreamManager, localCluster, newInstanceId } from "./core/cluster.js";
|
|
14
|
+
import { TypingService } from "./core/typing.js";
|
|
15
|
+
import { PushService } from "./core/push.js";
|
|
16
|
+
import { AdminService } from "./core/admin.js";
|
|
17
|
+
import { OAuthDelegation } from "./core/oauth.js";
|
|
18
|
+
import { SsoService } from "./core/sso.js";
|
|
19
|
+
import { DelayedEventService } from "./core/delayed.js";
|
|
20
|
+
import { AuditService } from "./core/audit.js";
|
|
21
|
+
import { SettingsService } from "./core/settings.js";
|
|
22
|
+
import { AdminRoomService } from "./core/adminroom.js";
|
|
23
|
+
import { AppServiceService } from "./core/appservices.js";
|
|
24
|
+
import { RateLimits, memoryRateLimiter } from "./core/ratelimit.js";
|
|
25
|
+
import { resolveUserAuth } from "./adapters/auth.js";
|
|
26
|
+
import { memoryMedia } from "./adapters/media.js";
|
|
27
|
+
import { memoryAdapter } from "./storage/memory.js";
|
|
28
|
+
import { schema } from "./storage/schema.js";
|
|
29
|
+
import { Router } from "./http/router.js";
|
|
30
|
+
import { registerRoutes } from "./api/index.js";
|
|
31
|
+
import { EventEmitter } from "node:events";
|
|
32
|
+
//#region src/homeserver.ts
|
|
33
|
+
const consoleLogger = {
|
|
34
|
+
debug: () => {},
|
|
35
|
+
info: (...a) => console.info("[matrix]", ...a),
|
|
36
|
+
warn: (...a) => console.warn("[matrix]", ...a),
|
|
37
|
+
error: (...a) => console.error("[matrix]", ...a)
|
|
38
|
+
};
|
|
39
|
+
const STREAM_TABLES = [
|
|
40
|
+
["events", "stream"],
|
|
41
|
+
["account_data", "stream"],
|
|
42
|
+
["receipts", "stream"],
|
|
43
|
+
["to_device", "stream"],
|
|
44
|
+
["device_keys", "stream"],
|
|
45
|
+
["cross_signing_keys", "stream"],
|
|
46
|
+
["device_list_changes", "stream"],
|
|
47
|
+
["presence", "stream"],
|
|
48
|
+
["memberships", "stream"]
|
|
49
|
+
];
|
|
50
|
+
var Homeserver = class extends EventEmitter {
|
|
51
|
+
serverName;
|
|
52
|
+
config;
|
|
53
|
+
db;
|
|
54
|
+
media;
|
|
55
|
+
userAuth;
|
|
56
|
+
logger;
|
|
57
|
+
router = new Router();
|
|
58
|
+
cluster;
|
|
59
|
+
instanceId;
|
|
60
|
+
streams;
|
|
61
|
+
notifier;
|
|
62
|
+
auth;
|
|
63
|
+
rooms;
|
|
64
|
+
keys;
|
|
65
|
+
typing;
|
|
66
|
+
push;
|
|
67
|
+
rateLimits;
|
|
68
|
+
admin;
|
|
69
|
+
oauth;
|
|
70
|
+
sso;
|
|
71
|
+
federation;
|
|
72
|
+
delayedEvents;
|
|
73
|
+
audit;
|
|
74
|
+
settings;
|
|
75
|
+
adminRoom;
|
|
76
|
+
appservices;
|
|
77
|
+
partialState;
|
|
78
|
+
policyServers;
|
|
79
|
+
threepids;
|
|
80
|
+
identityServers;
|
|
81
|
+
/** `fetch` used for outgoing requests (push gateways, URL previews, OAuth). Overridable for testing. */
|
|
82
|
+
fetch;
|
|
83
|
+
/** Resolves once storage is migrated and the server is ready to serve requests. */
|
|
84
|
+
ready;
|
|
85
|
+
_signingKey = null;
|
|
86
|
+
cleanupTimer = null;
|
|
87
|
+
closed = false;
|
|
88
|
+
constructor(options) {
|
|
89
|
+
super();
|
|
90
|
+
if (!options.homeserver) throw new Error("`homeserver` (server name) is required");
|
|
91
|
+
this.serverName = options.homeserver;
|
|
92
|
+
this.logger = options.logger ?? consoleLogger;
|
|
93
|
+
const defaultRoomVersion = options.defaultRoomVersion ?? "10";
|
|
94
|
+
if (!SUPPORTED_ROOM_VERSIONS.includes(defaultRoomVersion)) throw new Error(`Unsupported room version ${defaultRoomVersion}`);
|
|
95
|
+
this.config = {
|
|
96
|
+
serverName: options.homeserver,
|
|
97
|
+
publicBaseUrl: options.publicBaseUrl ?? null,
|
|
98
|
+
federationAddress: options.federationAddress ?? null,
|
|
99
|
+
support: options.support && (options.support.contacts?.length || options.support.supportPage) ? {
|
|
100
|
+
contacts: options.support.contacts ?? [],
|
|
101
|
+
...options.support.supportPage ? { support_page: options.support.supportPage } : {}
|
|
102
|
+
} : null,
|
|
103
|
+
loginViaExistingSession: options.loginViaExistingSession ?? true,
|
|
104
|
+
threepid: {
|
|
105
|
+
...options.threepid,
|
|
106
|
+
identityServerProtocol: options.threepid?.identityServerProtocol ?? "https"
|
|
107
|
+
},
|
|
108
|
+
registrationSharedSecret: options.registrationSharedSecret ?? null,
|
|
109
|
+
defaultRoomVersion,
|
|
110
|
+
accessTokenLifetimeMs: options.accessTokenLifetimeMs ?? null,
|
|
111
|
+
maxSyncTimeoutMs: options.maxSyncTimeoutMs ?? 3e4,
|
|
112
|
+
urlPreviews: options.urlPreviews ? { allowPrivateAddresses: typeof options.urlPreviews === "object" && !!options.urlPreviews.allowPrivateAddresses } : null,
|
|
113
|
+
thumbnailer: options.thumbnailer ?? null,
|
|
114
|
+
rtc: options.rtc ?? {},
|
|
115
|
+
serverNotices: {
|
|
116
|
+
localpart: options.serverNotices?.localpart ?? "server",
|
|
117
|
+
displayName: options.serverNotices?.displayName ?? "Server Notices",
|
|
118
|
+
roomName: options.serverNotices?.roomName ?? "Server Notices"
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const db = options.databaseAdapter;
|
|
122
|
+
if (db && typeof db.findOne === "function") this.db = db;
|
|
123
|
+
else {
|
|
124
|
+
this.logger.warn("No database adapter given; using in-memory storage (data is lost on restart)");
|
|
125
|
+
this.db = memoryAdapter();
|
|
126
|
+
}
|
|
127
|
+
const media = options.mediaAdapter;
|
|
128
|
+
this.media = media && typeof media.put === "function" ? media : memoryMedia();
|
|
129
|
+
this.userAuth = resolveUserAuth(options.userAuthAdapter);
|
|
130
|
+
this.cluster = options.cluster ?? localCluster();
|
|
131
|
+
this.instanceId = newInstanceId();
|
|
132
|
+
this.streams = new StreamManager(this.cluster, this.instanceId);
|
|
133
|
+
this.notifier = new Notifier(this.cluster, this.instanceId);
|
|
134
|
+
this.auth = new AuthService(this);
|
|
135
|
+
this.rooms = new RoomService(this, !options.cluster);
|
|
136
|
+
this.keys = new KeyService(this);
|
|
137
|
+
this.typing = new TypingService(this);
|
|
138
|
+
this.push = new PushService(this);
|
|
139
|
+
this.admin = new AdminService(this);
|
|
140
|
+
this.oauth = options.oauth ? new OAuthDelegation(this, options.oauth) : null;
|
|
141
|
+
this.federation = new FederationService(this, options.federation);
|
|
142
|
+
this.delayedEvents = new DelayedEventService(this, options.delayedEvents);
|
|
143
|
+
this.audit = new AuditService(this, options.audit);
|
|
144
|
+
this.settings = new SettingsService(this);
|
|
145
|
+
this.appservices = new AppServiceService(this, options.appservices ?? []);
|
|
146
|
+
this.partialState = new PartialStateService(this);
|
|
147
|
+
this.policyServers = new PolicyServers(this);
|
|
148
|
+
this.threepids = new ThreepidService(this);
|
|
149
|
+
this.identityServers = new IdentityServers(this);
|
|
150
|
+
const adminRoom = options.adminRoom === true ? {} : options.adminRoom || null;
|
|
151
|
+
this.adminRoom = adminRoom && adminRoom.enabled !== false ? new AdminRoomService(this, adminRoom) : null;
|
|
152
|
+
this.sso = options.sso?.providers.length ? new SsoService(this, options.sso) : null;
|
|
153
|
+
if (this.sso && !options.publicBaseUrl) throw new Error("`publicBaseUrl` is required when SSO is configured");
|
|
154
|
+
this.rateLimits = options.rateLimits === false ? new RateLimits(null) : new RateLimits(options.rateLimits?.limiter ?? memoryRateLimiter(), options.rateLimits?.rules);
|
|
155
|
+
const cleanupEvery = options.cleanupIntervalMs ?? 6e5;
|
|
156
|
+
if (cleanupEvery > 0) {
|
|
157
|
+
this.cleanupTimer = setInterval(() => void this.cleanup().catch((err) => this.logger.warn("Cleanup failed", err)), cleanupEvery);
|
|
158
|
+
this.cleanupTimer.unref?.();
|
|
159
|
+
}
|
|
160
|
+
this.fetch = options.fetch ?? globalThis.fetch;
|
|
161
|
+
registerRoutes(this.router);
|
|
162
|
+
this.ready = this.init(options);
|
|
163
|
+
this.ready.catch((err) => this.logger.error("Homeserver failed to start", err));
|
|
164
|
+
}
|
|
165
|
+
get signingKey() {
|
|
166
|
+
if (!this._signingKey) throw new Error("Homeserver not ready");
|
|
167
|
+
return this._signingKey;
|
|
168
|
+
}
|
|
169
|
+
async init(options) {
|
|
170
|
+
await this.cluster.lock("startup", async () => {
|
|
171
|
+
await this.db.migrate?.(schema);
|
|
172
|
+
if (options.signingKey) this._signingKey = loadSigningKey(options.signingKey.keyId, options.signingKey.seed);
|
|
173
|
+
else {
|
|
174
|
+
const row = await this.db.findOne("server_keys", []);
|
|
175
|
+
if (row) this._signingKey = loadSigningKey(row.key_id, row.seed);
|
|
176
|
+
else {
|
|
177
|
+
this._signingKey = loadSigningKey(`a_${randomString(4)}`);
|
|
178
|
+
await this.db.create("server_keys", {
|
|
179
|
+
key_id: this._signingKey.keyId,
|
|
180
|
+
seed: this._signingKey.seed
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
for (const [table, field] of STREAM_TABLES) {
|
|
186
|
+
const max = await this.db.max(table, field);
|
|
187
|
+
if (max !== null) await this.streams.advanceTo(max);
|
|
188
|
+
}
|
|
189
|
+
const snap = await this.db.max("state_snapshots", "id");
|
|
190
|
+
if (snap !== null) await this.streams.advanceTo(snap);
|
|
191
|
+
const oldest = await this.db.findMany("events", {
|
|
192
|
+
sortBy: {
|
|
193
|
+
field: "stream",
|
|
194
|
+
direction: "asc"
|
|
195
|
+
},
|
|
196
|
+
limit: 1
|
|
197
|
+
});
|
|
198
|
+
if (oldest[0] && oldest[0].stream < 0) await this.streams.lowerBackfillTo(oldest[0].stream);
|
|
199
|
+
const oldestSnap = await this.db.findMany("state_snapshots", {
|
|
200
|
+
sortBy: {
|
|
201
|
+
field: "id",
|
|
202
|
+
direction: "asc"
|
|
203
|
+
},
|
|
204
|
+
limit: 1
|
|
205
|
+
});
|
|
206
|
+
if (oldestSnap[0] && oldestSnap[0].id < 0) await this.streams.lowerBackfillTo(oldestSnap[0].id);
|
|
207
|
+
await this.cluster.advance("federation_outbox", await this.db.max("federation_outbox", "id") ?? 0);
|
|
208
|
+
if (this.federation.enabled) this.federation.sender.start();
|
|
209
|
+
this.delayedEvents.start();
|
|
210
|
+
this.appservices.start();
|
|
211
|
+
if (this.federation.enabled) await this.partialState.resume();
|
|
212
|
+
}
|
|
213
|
+
/** Remove expired tokens, abandoned uploads and old notifications. */
|
|
214
|
+
async cleanup() {
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
await this.db.delete("access_tokens", [{
|
|
217
|
+
field: "expires_ts",
|
|
218
|
+
operator: "lt",
|
|
219
|
+
value: now - 6048e5
|
|
220
|
+
}]);
|
|
221
|
+
await this.db.delete("login_tokens", [{
|
|
222
|
+
field: "expires_ts",
|
|
223
|
+
operator: "lt",
|
|
224
|
+
value: now
|
|
225
|
+
}]);
|
|
226
|
+
for (const m of await this.db.findMany("media", { where: [{
|
|
227
|
+
field: "pending",
|
|
228
|
+
value: true
|
|
229
|
+
}, {
|
|
230
|
+
field: "created_ts",
|
|
231
|
+
operator: "lt",
|
|
232
|
+
value: now - 864e5
|
|
233
|
+
}] })) {
|
|
234
|
+
await this.media.delete?.(m.media_id);
|
|
235
|
+
await this.db.delete("media", [{
|
|
236
|
+
field: "media_id",
|
|
237
|
+
value: m.media_id
|
|
238
|
+
}]);
|
|
239
|
+
}
|
|
240
|
+
await this.db.delete("notifications", [{
|
|
241
|
+
field: "ts",
|
|
242
|
+
operator: "lt",
|
|
243
|
+
value: now - 2592e6
|
|
244
|
+
}]);
|
|
245
|
+
await this.audit.prune();
|
|
246
|
+
}
|
|
247
|
+
/** Stop background work and release long-polling requests. Call before shutting down your HTTP server. */
|
|
248
|
+
async close() {
|
|
249
|
+
if (this.closed) return;
|
|
250
|
+
this.closed = true;
|
|
251
|
+
if (this.cleanupTimer) clearInterval(this.cleanupTimer);
|
|
252
|
+
this.federation.sender.stop();
|
|
253
|
+
this.delayedEvents.stop();
|
|
254
|
+
this.appservices.stop();
|
|
255
|
+
this.partialState.stop();
|
|
256
|
+
await this.streams.stop();
|
|
257
|
+
await this.cluster.close?.();
|
|
258
|
+
this.notifier.notifyAll();
|
|
259
|
+
this.emit("close");
|
|
260
|
+
}
|
|
261
|
+
get isClosed() {
|
|
262
|
+
return this.closed;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Effective registration mode: an admin's runtime setting overrides the configuration.
|
|
266
|
+
* "open" | "token" | "closed".
|
|
267
|
+
*/
|
|
268
|
+
async registrationMode() {
|
|
269
|
+
const setting = await this.settings.get("registration");
|
|
270
|
+
if (setting === "open" || setting === "token" || setting === "closed") return setting;
|
|
271
|
+
if (!this.userAuth.registrationEnabled) return "closed";
|
|
272
|
+
return this.userAuth.registrationRequiresToken ? "token" : "open";
|
|
273
|
+
}
|
|
274
|
+
/** Whether `/register` is allowed right now, including the first-user exception. */
|
|
275
|
+
async registrationAllowed() {
|
|
276
|
+
if (await this.registrationMode() !== "closed") return true;
|
|
277
|
+
return !!this.adminRoom?.options.firstUserCanRegister && await this.adminRoom.noUsersYet();
|
|
278
|
+
}
|
|
279
|
+
/** Run `fn` atomically when the database adapter supports transactions. */
|
|
280
|
+
transaction(fn) {
|
|
281
|
+
return this.db.transaction ? this.db.transaction(fn) : fn(this.db);
|
|
282
|
+
}
|
|
283
|
+
/** The user's current profile, as included in member events. */
|
|
284
|
+
async profile(userId) {
|
|
285
|
+
const user = await this.db.findOne("users", [{
|
|
286
|
+
field: "user_id",
|
|
287
|
+
value: userId
|
|
288
|
+
}]);
|
|
289
|
+
const out = {};
|
|
290
|
+
if (user?.displayname) out.displayname = user.displayname;
|
|
291
|
+
if (user?.avatar_url) out.avatar_url = user.avatar_url;
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
isLocalUser(userId) {
|
|
295
|
+
return userId.endsWith(`:${this.serverName}`);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
function createHomeserver(options) {
|
|
299
|
+
return new Homeserver(options);
|
|
300
|
+
}
|
|
301
|
+
//#endregion
|
|
302
|
+
export { Homeserver, createHomeserver };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Homeserver } from "../homeserver.js";
|
|
2
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
//#region src/http/adapters.d.ts
|
|
4
|
+
/** Paths this library serves. Framework adapters forward only these. */
|
|
5
|
+
export declare function isMatrixPath(pathname: string): boolean;
|
|
6
|
+
/** A web-standard fetch handler: `(Request) => Promise<Response>`. Works with Hono, Bun, Deno, workers… */
|
|
7
|
+
export declare function getFetchHandler(hs: Homeserver): (request: Request) => Promise<Response>;
|
|
8
|
+
export type NodeHandler = (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void;
|
|
9
|
+
/**
|
|
10
|
+
* A request listener for `node:http` / `node:https` / `node:http2` (compat).
|
|
11
|
+
* Also usable directly as Express/Connect middleware: non-Matrix paths are
|
|
12
|
+
* passed to `next()` when it is provided, otherwise answered with 404.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getNodeHandler(hs: Homeserver): NodeHandler;
|
|
15
|
+
/** Express / Connect middleware. Mount with `app.use(getExpressHandler(hs))`. */
|
|
16
|
+
export declare function getExpressHandler(hs: Homeserver): (req: any, res: any, next: (err?: unknown) => void) => void;
|
|
17
|
+
/** Minimal structural Fastify types so that fastify is not a dependency. */
|
|
18
|
+
interface FastifyLike {
|
|
19
|
+
removeAllContentTypeParsers(): void;
|
|
20
|
+
addContentTypeParser(type: string, opts: {
|
|
21
|
+
parseAs: "buffer";
|
|
22
|
+
}, parser: (req: any, body: Buffer, done: (err: Error | null, body?: unknown) => void) => void): void;
|
|
23
|
+
route(opts: {
|
|
24
|
+
method: string[];
|
|
25
|
+
url: string;
|
|
26
|
+
handler: (request: any, reply: any) => unknown;
|
|
27
|
+
}): void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Fastify plugin. Register with `fastify.register(getFastifyPlugin(hs))`.
|
|
31
|
+
* The plugin is encapsulated so its raw body parser does not leak into your app.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getFastifyPlugin(hs: Homeserver): (fastify: FastifyLike, opts: unknown, done?: (err?: Error) => void) => Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Hono handler. Mount with `app.all("*", getHonoHandler(hs))` or
|
|
36
|
+
* `app.mount("/", getFetchHandler(hs))`. Non-Matrix paths call `next()`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getHonoHandler(hs: Homeserver): (c: any, next: () => Promise<void>) => Promise<Response | void>;
|
|
39
|
+
//#endregion
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { handleRequest } from "./handler.js";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
//#region src/http/adapters.ts
|
|
4
|
+
/** Paths this library serves. Framework adapters forward only these. */
|
|
5
|
+
function isMatrixPath(pathname) {
|
|
6
|
+
return pathname.startsWith("/_matrix/") || pathname.startsWith("/.well-known/matrix/") || pathname.startsWith("/_synapse/");
|
|
7
|
+
}
|
|
8
|
+
/** A web-standard fetch handler: `(Request) => Promise<Response>`. Works with Hono, Bun, Deno, workers… */
|
|
9
|
+
function getFetchHandler(hs) {
|
|
10
|
+
return (request) => handleRequest(hs, request);
|
|
11
|
+
}
|
|
12
|
+
async function toWebRequest(req, presetBody) {
|
|
13
|
+
const proto = req.socket?.encrypted ? "https" : "http";
|
|
14
|
+
const host = req.headers.host ?? "localhost";
|
|
15
|
+
const url = new URL(req.originalUrl ?? req.url ?? "/", `${proto}://${host}`);
|
|
16
|
+
const headers = new Headers();
|
|
17
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
18
|
+
if (value === void 0 || key.startsWith(":")) continue;
|
|
19
|
+
if (Array.isArray(value)) for (const v of value) headers.append(key, v);
|
|
20
|
+
else headers.set(key, value);
|
|
21
|
+
}
|
|
22
|
+
const method = req.method ?? "GET";
|
|
23
|
+
let body = void 0;
|
|
24
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
25
|
+
const pre = presetBody !== void 0 ? presetBody : req.body;
|
|
26
|
+
if (pre !== void 0 && pre !== null && req.readableEnded !== false) {
|
|
27
|
+
if (pre instanceof Uint8Array || typeof pre === "string") body = pre;
|
|
28
|
+
else if (typeof pre === "object" && Object.keys(pre).length === 0 && headers.get("content-length") === null) body = void 0;
|
|
29
|
+
else body = JSON.stringify(pre);
|
|
30
|
+
headers.delete("content-length");
|
|
31
|
+
} else {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
34
|
+
body = Buffer.concat(chunks);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return new Request(url, {
|
|
38
|
+
method,
|
|
39
|
+
headers,
|
|
40
|
+
body
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function writeWebResponse(res, response) {
|
|
44
|
+
res.statusCode = response.status;
|
|
45
|
+
response.headers.forEach((value, key) => res.setHeader(key, value));
|
|
46
|
+
if (!response.body) {
|
|
47
|
+
res.end();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
51
|
+
nodeStream.on("error", () => res.destroy());
|
|
52
|
+
nodeStream.pipe(res);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A request listener for `node:http` / `node:https` / `node:http2` (compat).
|
|
56
|
+
* Also usable directly as Express/Connect middleware: non-Matrix paths are
|
|
57
|
+
* passed to `next()` when it is provided, otherwise answered with 404.
|
|
58
|
+
*/
|
|
59
|
+
function getNodeHandler(hs) {
|
|
60
|
+
return (req, res, next) => {
|
|
61
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
62
|
+
if (next && !isMatrixPath(path)) {
|
|
63
|
+
next();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
(async () => {
|
|
67
|
+
const request = await toWebRequest(req);
|
|
68
|
+
await writeWebResponse(res, await handleRequest(hs, request, { ip: req.socket?.remoteAddress ?? null }));
|
|
69
|
+
})().catch((err) => {
|
|
70
|
+
if (next) next(err);
|
|
71
|
+
else {
|
|
72
|
+
hs.logger.error("Node handler failure", err);
|
|
73
|
+
if (!res.headersSent) res.statusCode = 500;
|
|
74
|
+
res.end();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Express / Connect middleware. Mount with `app.use(getExpressHandler(hs))`. */
|
|
80
|
+
function getExpressHandler(hs) {
|
|
81
|
+
return getNodeHandler(hs);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Fastify plugin. Register with `fastify.register(getFastifyPlugin(hs))`.
|
|
85
|
+
* The plugin is encapsulated so its raw body parser does not leak into your app.
|
|
86
|
+
*/
|
|
87
|
+
function getFastifyPlugin(hs) {
|
|
88
|
+
return async (fastify) => {
|
|
89
|
+
fastify.removeAllContentTypeParsers();
|
|
90
|
+
fastify.addContentTypeParser("*", { parseAs: "buffer" }, (_req, body, done) => done(null, body));
|
|
91
|
+
const handler = async (request, reply) => {
|
|
92
|
+
const webRequest = await toWebRequest(request.raw, request.body ?? /* @__PURE__ */ new Uint8Array());
|
|
93
|
+
const response = await handleRequest(hs, webRequest, { ip: request.ip ?? null });
|
|
94
|
+
reply.status(response.status);
|
|
95
|
+
response.headers.forEach((value, key) => reply.header(key, value));
|
|
96
|
+
return reply.send(response.body ? Buffer.from(await response.arrayBuffer()) : null);
|
|
97
|
+
};
|
|
98
|
+
const methods = [
|
|
99
|
+
"GET",
|
|
100
|
+
"POST",
|
|
101
|
+
"PUT",
|
|
102
|
+
"DELETE",
|
|
103
|
+
"OPTIONS",
|
|
104
|
+
"HEAD"
|
|
105
|
+
];
|
|
106
|
+
for (const url of [
|
|
107
|
+
"/_matrix/*",
|
|
108
|
+
"/.well-known/matrix/*",
|
|
109
|
+
"/_synapse/*"
|
|
110
|
+
]) fastify.route({
|
|
111
|
+
method: methods,
|
|
112
|
+
url,
|
|
113
|
+
handler
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Hono handler. Mount with `app.all("*", getHonoHandler(hs))` or
|
|
119
|
+
* `app.mount("/", getFetchHandler(hs))`. Non-Matrix paths call `next()`.
|
|
120
|
+
*/
|
|
121
|
+
function getHonoHandler(hs) {
|
|
122
|
+
return async (c, next) => {
|
|
123
|
+
const request = c.req.raw;
|
|
124
|
+
if (!isMatrixPath(new URL(request.url).pathname)) return next();
|
|
125
|
+
const ip = c.env?.incoming?.socket?.remoteAddress ?? null;
|
|
126
|
+
return handleRequest(hs, request, { ip });
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
export { getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, isMatrixPath };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Homeserver } from "../homeserver.js";
|
|
2
|
+
//#region src/http/handler.d.ts
|
|
3
|
+
export interface HandleOptions {
|
|
4
|
+
ip?: string | null;
|
|
5
|
+
}
|
|
6
|
+
/** Turn a web-standard Request into a Response. */
|
|
7
|
+
export declare function handleRequest(hs: Homeserver, request: Request, options?: HandleOptions): Promise<Response>;
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { canonicalJson } from "../core/json.js";
|
|
2
|
+
import { UNSTABLE } from "../core/m.js";
|
|
3
|
+
import { MatrixError, errors } from "../core/errors.js";
|
|
4
|
+
//#region src/http/handler.ts
|
|
5
|
+
const CORS_HEADERS = {
|
|
6
|
+
"access-control-allow-origin": "*",
|
|
7
|
+
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
8
|
+
"access-control-allow-headers": "X-Requested-With, Content-Type, Authorization"
|
|
9
|
+
};
|
|
10
|
+
/** JSON response with a canonical (sorted, compact) body, needed where clients verify signatures over the raw bytes. */
|
|
11
|
+
function canonicalJsonResponse(body) {
|
|
12
|
+
return new Response(canonicalJson(body), {
|
|
13
|
+
status: 200,
|
|
14
|
+
headers: {
|
|
15
|
+
"content-type": "application/json",
|
|
16
|
+
...CORS_HEADERS
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function jsonResponse(body, status = 200, headers = {}) {
|
|
21
|
+
return new Response(JSON.stringify(body), {
|
|
22
|
+
status,
|
|
23
|
+
headers: {
|
|
24
|
+
"content-type": "application/json",
|
|
25
|
+
...CORS_HEADERS,
|
|
26
|
+
...headers
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function withCors(res) {
|
|
31
|
+
const out = new Response(res.body, {
|
|
32
|
+
status: res.status,
|
|
33
|
+
statusText: res.statusText,
|
|
34
|
+
headers: res.headers
|
|
35
|
+
});
|
|
36
|
+
for (const [k, v] of Object.entries(CORS_HEADERS)) if (!out.headers.has(k)) out.headers.set(k, v);
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/** Turn a web-standard Request into a Response. */
|
|
40
|
+
async function handleRequest(hs, request, options = {}) {
|
|
41
|
+
if (request.method === "OPTIONS") return new Response(null, {
|
|
42
|
+
status: 204,
|
|
43
|
+
headers: CORS_HEADERS
|
|
44
|
+
});
|
|
45
|
+
const url = new URL(request.url);
|
|
46
|
+
const started = performance.now();
|
|
47
|
+
const response = await dispatch(hs, request, url, options);
|
|
48
|
+
const info = {
|
|
49
|
+
method: request.method,
|
|
50
|
+
path: url.pathname,
|
|
51
|
+
status: response.status,
|
|
52
|
+
durationMs: performance.now() - started
|
|
53
|
+
};
|
|
54
|
+
if (response.status >= 400 && response.headers.get("content-type") === "application/json" && hs.listenerCount("request") > 0) info.error = await response.clone().json().catch(() => void 0);
|
|
55
|
+
hs.emit("request", info);
|
|
56
|
+
return response;
|
|
57
|
+
}
|
|
58
|
+
async function dispatch(hs, request, url, options) {
|
|
59
|
+
try {
|
|
60
|
+
if (hs.isClosed) throw new MatrixError(503, "M_UNKNOWN", "Server is shutting down");
|
|
61
|
+
await hs.ready;
|
|
62
|
+
const matched = hs.router.match(request.method, url.pathname);
|
|
63
|
+
if (matched === "method") throw new MatrixError(405, "M_UNRECOGNIZED", "Method not allowed");
|
|
64
|
+
if (!matched) throw errors.unrecognized();
|
|
65
|
+
let bodyBytes = null;
|
|
66
|
+
const readBytes = () => bodyBytes ??= request.arrayBuffer().then((b) => new Uint8Array(b));
|
|
67
|
+
let session = null;
|
|
68
|
+
const token = () => extractToken(request, url);
|
|
69
|
+
const ctx = {
|
|
70
|
+
hs,
|
|
71
|
+
request,
|
|
72
|
+
url,
|
|
73
|
+
method: request.method,
|
|
74
|
+
params: matched.params,
|
|
75
|
+
query: url.searchParams,
|
|
76
|
+
ip: options.ip ?? null,
|
|
77
|
+
bytes: readBytes,
|
|
78
|
+
async json() {
|
|
79
|
+
const bytes = await readBytes();
|
|
80
|
+
if (bytes.length === 0) return {};
|
|
81
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
84
|
+
} catch {
|
|
85
|
+
throw errors.notJson();
|
|
86
|
+
}
|
|
87
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw errors.badJson("Body must be a JSON object");
|
|
88
|
+
return parsed;
|
|
89
|
+
},
|
|
90
|
+
accessToken: token,
|
|
91
|
+
auth() {
|
|
92
|
+
return session ??= (async () => {
|
|
93
|
+
const t = token();
|
|
94
|
+
if (!t) throw errors.missingToken();
|
|
95
|
+
const reg = hs.appservices.byToken(t);
|
|
96
|
+
if (reg) {
|
|
97
|
+
const userId = await hs.appservices.masquerade(reg, url.searchParams.get("user_id"));
|
|
98
|
+
const deviceId = url.searchParams.get("device_id") ?? url.searchParams.get(UNSTABLE.ORG.MATRIX.MSC3202.DEVICE_ID);
|
|
99
|
+
if (deviceId && !await hs.db.findOne("devices", [{
|
|
100
|
+
field: "user_id",
|
|
101
|
+
value: userId
|
|
102
|
+
}, {
|
|
103
|
+
field: "device_id",
|
|
104
|
+
value: deviceId
|
|
105
|
+
}])) throw errors.forbidden("Unknown device for this user");
|
|
106
|
+
const user = await hs.db.findOne("users", [{
|
|
107
|
+
field: "user_id",
|
|
108
|
+
value: userId
|
|
109
|
+
}]);
|
|
110
|
+
return {
|
|
111
|
+
userId,
|
|
112
|
+
deviceId: deviceId ?? "",
|
|
113
|
+
accessToken: t,
|
|
114
|
+
isGuest: false,
|
|
115
|
+
isAdmin: !!user?.is_admin,
|
|
116
|
+
appservice: reg
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const session = await hs.auth.authenticate(t, ctx.ip);
|
|
120
|
+
if (session.locked && !/^\/_matrix\/client\/(?:r0|v3)\/logout(?:\/all)?$/.test(url.pathname)) throw errors.userLocked();
|
|
121
|
+
return session;
|
|
122
|
+
})();
|
|
123
|
+
},
|
|
124
|
+
async optionalAuth() {
|
|
125
|
+
return token() ? ctx.auth() : null;
|
|
126
|
+
},
|
|
127
|
+
async rateLimit(action) {
|
|
128
|
+
const s = token() ? await ctx.auth().catch(() => null) : null;
|
|
129
|
+
if (s?.isAdmin || s?.appservice && s.appservice.rate_limited === false) return;
|
|
130
|
+
await hs.rateLimits.check(action, s?.userId ?? ctx.ip ?? "unknown");
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const result = await matched.handler(ctx);
|
|
134
|
+
if (result instanceof Response) return withCors(result);
|
|
135
|
+
return jsonResponse(result ?? {});
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err instanceof MatrixError) return jsonResponse(err.toJSON(), err.status);
|
|
138
|
+
if (isNetworkError(err)) {
|
|
139
|
+
hs.logger.debug("Remote server unreachable", request.method, url.pathname, err);
|
|
140
|
+
return jsonResponse({
|
|
141
|
+
errcode: "M_UNKNOWN",
|
|
142
|
+
error: `Remote server unreachable: ${err.message}`
|
|
143
|
+
}, 502);
|
|
144
|
+
}
|
|
145
|
+
hs.logger.error("Unhandled error", request.method, url.pathname, err);
|
|
146
|
+
return jsonResponse({
|
|
147
|
+
errcode: "M_UNKNOWN",
|
|
148
|
+
error: "Internal server error"
|
|
149
|
+
}, 500);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
153
|
+
"ENOTFOUND",
|
|
154
|
+
"EAI_AGAIN",
|
|
155
|
+
"ECONNREFUSED",
|
|
156
|
+
"ECONNRESET",
|
|
157
|
+
"EHOSTUNREACH",
|
|
158
|
+
"ENETUNREACH",
|
|
159
|
+
"ETIMEDOUT",
|
|
160
|
+
"EPIPE",
|
|
161
|
+
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
162
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
163
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
164
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
165
|
+
"CERT_HAS_EXPIRED"
|
|
166
|
+
]);
|
|
167
|
+
function isNetworkError(err) {
|
|
168
|
+
if (!(err instanceof Error)) return false;
|
|
169
|
+
const code = err.code;
|
|
170
|
+
return code !== void 0 && NETWORK_CODES.has(code) || /^(Connection timed out|Federation request timed out)$/.test(err.message);
|
|
171
|
+
}
|
|
172
|
+
function extractToken(request, url) {
|
|
173
|
+
const header = request.headers.get("authorization");
|
|
174
|
+
if (header) {
|
|
175
|
+
const m = /^Bearer\s+(.+)$/i.exec(header);
|
|
176
|
+
if (m) return m[1].trim();
|
|
177
|
+
}
|
|
178
|
+
return url.searchParams.get("access_token");
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
export { canonicalJsonResponse, handleRequest, jsonResponse };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { MatrixError } from "../core/errors.js";
|
|
2
|
+
import { looseObject } from "../vendor/zod/v4/classic/schemas.js";
|
|
3
|
+
//#region src/http/input.ts
|
|
4
|
+
/** A request body: a JSON object whose unknown keys are kept. */
|
|
5
|
+
const body = (shape) => looseObject(shape);
|
|
6
|
+
/**
|
|
7
|
+
* Errors carry their Matrix errcode in the message, as `M_CODE: text`: `fail` builds the zod
|
|
8
|
+
* error option for a field. Without one, a missing field is M_MISSING_PARAM and any other
|
|
9
|
+
* problem M_BAD_JSON.
|
|
10
|
+
*/
|
|
11
|
+
const fail = (errcode, message, missing) => ({ error: (issue) => issue.input === void 0 && missing ? `M_MISSING_PARAM: ${missing}` : `${errcode}: ${message}` });
|
|
12
|
+
/** A field that, when its value has the wrong type, is ignored (treated as absent), as Matrix clients expect for optional fields. */
|
|
13
|
+
const lenient = (schema) => schema.optional().catch(void 0);
|
|
14
|
+
/** Turn a failed body parse into the Matrix error the client sees. */
|
|
15
|
+
function inputError(error) {
|
|
16
|
+
const issue = error.issues[0];
|
|
17
|
+
const path = issue?.path.join(".") ?? "";
|
|
18
|
+
const tagged = /^(M_[A-Z_]+): ([\s\S]*)$/.exec(issue?.message ?? "");
|
|
19
|
+
if (tagged) return new MatrixError(400, tagged[1], tagged[2]);
|
|
20
|
+
if (issue?.code === "invalid_type" && issue.input === void 0) return new MatrixError(400, "M_MISSING_PARAM", `Missing ${path || "parameter"}`);
|
|
21
|
+
return new MatrixError(400, "M_BAD_JSON", path ? `Invalid ${path}: ${issue.message}` : issue?.message ?? "Bad JSON");
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { body, fail, inputError, lenient };
|