matrix-nodejs-homeserver 0.3.3 → 0.3.5

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.
Files changed (59) hide show
  1. package/README.md +89 -44
  2. package/dist/api/account.js +109 -64
  3. package/dist/api/admin.js +9 -0
  4. package/dist/api/federation.js +1 -1
  5. package/dist/api/index.js +2 -0
  6. package/dist/api/longpoll.js +61 -0
  7. package/dist/api/misc.js +3 -3
  8. package/dist/api/oauthserver.js +620 -0
  9. package/dist/api/slidingsync.js +28 -30
  10. package/dist/api/sync.js +181 -120
  11. package/dist/api/threepid.js +1 -1
  12. package/dist/api/uia.js +20 -8
  13. package/dist/api/user.js +6 -3
  14. package/dist/core/admin.js +1 -0
  15. package/dist/core/auth.d.ts +33 -2
  16. package/dist/core/auth.js +84 -21
  17. package/dist/core/changes.d.ts +79 -0
  18. package/dist/core/changes.js +332 -0
  19. package/dist/core/cluster.d.ts +19 -3
  20. package/dist/core/cluster.js +44 -4
  21. package/dist/core/keys.js +4 -2
  22. package/dist/core/notifier.d.ts +49 -3
  23. package/dist/core/notifier.js +123 -26
  24. package/dist/core/oauth.js +2 -1
  25. package/dist/core/oauthserver.d.ts +141 -0
  26. package/dist/core/oauthserver.js +603 -0
  27. package/dist/core/push.d.ts +12 -11
  28. package/dist/core/push.js +6 -4
  29. package/dist/core/ratelimit.d.ts +1 -1
  30. package/dist/core/ratelimit.js +4 -0
  31. package/dist/core/rooms.js +7 -1
  32. package/dist/core/semaphore.js +1 -1
  33. package/dist/core/sso.d.ts +51 -15
  34. package/dist/core/sso.js +86 -26
  35. package/dist/homeserver.d.ts +19 -2
  36. package/dist/homeserver.js +33 -9
  37. package/dist/index.d.ts +3 -2
  38. package/dist/index.js +2 -1
  39. package/dist/storage/schema.js +20 -2
  40. package/dist/storage/sql.js +2 -2
  41. package/dist/vendor/zod/v4/classic/errors.js +1 -1
  42. package/dist/vendor/zod/v4/classic/parse.js +1 -1
  43. package/dist/vendor/zod/v4/classic/schemas.js +1 -1
  44. package/dist/vendor/zod/v4/core/api.js +1 -1
  45. package/dist/vendor/zod/v4/core/checks.js +1 -1
  46. package/dist/vendor/zod/v4/core/core.js +1 -1
  47. package/dist/vendor/zod/v4/core/doc.js +1 -1
  48. package/dist/vendor/zod/v4/core/errors.js +1 -1
  49. package/dist/vendor/zod/v4/core/json-schema-processors.js +1 -1
  50. package/dist/vendor/zod/v4/core/memoizer.js +1 -1
  51. package/dist/vendor/zod/v4/core/parse.js +1 -1
  52. package/dist/vendor/zod/v4/core/regexes.js +1 -1
  53. package/dist/vendor/zod/v4/core/registries.js +1 -1
  54. package/dist/vendor/zod/v4/core/schemas.js +1 -1
  55. package/dist/vendor/zod/v4/core/to-json-schema.js +1 -1
  56. package/dist/vendor/zod/v4/core/util.js +1 -1
  57. package/dist/vendor/zod/v4/core/versions.js +1 -1
  58. package/dist/vendor/zod/v4/locales/en.js +1 -1
  59. package/package.json +12 -15
@@ -32,6 +32,10 @@ const DEFAULT_RATE_LIMITS = {
32
32
  keyQuery: {
33
33
  perSecond: 5,
34
34
  burst: 50
35
+ },
36
+ oauthClient: {
37
+ perSecond: .5,
38
+ burst: 10
35
39
  }
36
40
  };
37
41
  /** In-process token bucket limiter. */
@@ -731,6 +731,12 @@ var RoomService = class {
731
731
  events: touched,
732
732
  sharing: true
733
733
  });
734
+ const changedMembers = /* @__PURE__ */ new Set();
735
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(oldIds), ...Object.keys(newIds)])) {
736
+ const [type, userId] = splitStateKey(k);
737
+ if (type === M.ROOM.MEMBER && oldIds[k] !== newIds[k] && this.hs.isLocalUser(userId)) changedMembers.add(userId);
738
+ }
739
+ if (changedMembers.size) this.hs.notifier.notify([...changedMembers, ...await this.interestedUsers(roomId)]);
734
740
  for (const k of Object.keys(oldIds)) {
735
741
  const [type, userId] = splitStateKey(k);
736
742
  if (type !== M.ROOM.MEMBER || this.hs.isLocalUser(userId) || oldIds[k] === newIds[k]) continue;
@@ -995,7 +1001,7 @@ var RoomService = class {
995
1001
  if (this.hs.appservices.registrations.length) await this.hs.appservices.onEvent(view, newState).catch((err) => this.hs.logger.warn("Appservice push failed", err));
996
1002
  if (isState) await this.hs.delayedEvents.onStateEvent(roomId, pdu.type, pdu.state_key, sender).catch(() => {});
997
1003
  wake = async () => {
998
- this.hs.notifier.notify(await this.interestedUsers(roomId));
1004
+ this.hs.notifier.notify(await this.interestedUsers(roomId), backfill ? void 0 : stream);
999
1005
  this.hs.emit("event", toClientEvent(view));
1000
1006
  if (this.hs.adminRoom) this.hs.adminRoom.onEvent(view).catch((err) => this.hs.logger.error("Admin command failed", err));
1001
1007
  };
@@ -14,7 +14,7 @@ var Semaphore = class {
14
14
  return await fn();
15
15
  } finally {
16
16
  const next = this.waiting.shift();
17
- if (next) next();
17
+ if (next) setImmediate(next);
18
18
  else this.active--;
19
19
  }
20
20
  }
@@ -1,15 +1,27 @@
1
1
  import { Homeserver } from "../homeserver.js";
2
2
  //#region src/core/sso.d.ts
3
- /** An upstream OpenID Connect provider for `m.login.sso`. */
3
+ /**
4
+ * An upstream identity provider: OpenID Connect (found through `issuer`), or plain OAuth 2.0 with
5
+ * a user info endpoint (`endpoints`, e.g. GitHub; see `githubProvider`).
6
+ */
4
7
  export interface SsoProvider {
5
8
  /** Stable identifier, used in URLs and stored with linked accounts. */
6
9
  id: string;
7
10
  name: string;
8
- issuer: string;
11
+ /** OpenID Connect issuer; its discovery document gives the endpoints. Required without `endpoints`. */
12
+ issuer?: string;
13
+ /** Endpoints of a provider without OpenID Connect discovery. */
14
+ endpoints?: {
15
+ authorization: string;
16
+ token: string;
17
+ userinfo?: string;
18
+ };
9
19
  clientId: string;
10
20
  clientSecret?: string;
11
- /** Default: ["openid", "profile", "email"]. */
21
+ /** Default: ["openid", "profile", "email"] with `issuer`, none with `endpoints`. */
12
22
  scopes?: string[];
23
+ /** Claim that identifies the user at the provider. Default: "sub". */
24
+ subjectClaim?: string;
13
25
  icon?: string;
14
26
  brand?: string;
15
27
  /**
@@ -35,26 +47,50 @@ export interface SsoOptions {
35
47
  */
36
48
  clientAllowlist?: string[];
37
49
  }
38
- /** Where the browser goes after login, and whether the user has to confirm that client first. */
39
- export interface SsoResult {
40
- url: string;
41
- confirm: boolean;
42
- /** The client as shown to the user: its host, or its scheme for app links. */
43
- client: string;
44
- }
45
- /** Upstream OIDC login (authorization code + PKCE) issuing Matrix login tokens. */
50
+ /**
51
+ * What a login through a provider is for: a client of the legacy API (`m.login.sso`, which gets a
52
+ * login token at `redirectUrl`), or the server's own web pages (OAuth 2.0 API), which continue
53
+ * at `next` in the browser session `sid`.
54
+ */
55
+ export type SsoTarget = {
56
+ kind: "client";
57
+ redirectUrl: string;
58
+ } | {
59
+ kind: "web";
60
+ next: string;
61
+ sid: string;
62
+ };
63
+ /**
64
+ * GitHub as an identity provider (an OAuth app or GitHub app; its callback URL is
65
+ * `{publicBaseUrl}/_matrix/client/v3/login/sso/callback/github`). Users are matched by their
66
+ * numeric GitHub ID, so renaming the GitHub account keeps the Matrix account; new accounts take
67
+ * the GitHub login as their localpart.
68
+ */
69
+ export declare function githubProvider(opts: {
70
+ clientId: string;
71
+ clientSecret: string;
72
+ id?: string;
73
+ name?: string;
74
+ allowRegistration?: boolean;
75
+ mapUser?: SsoProvider["mapUser"];
76
+ }): SsoProvider;
77
+ /** Upstream login (authorization code + PKCE) for the legacy `m.login.sso` and the OAuth 2.0 API's web pages. */
46
78
  export declare class SsoService {
47
79
  private hs;
48
80
  readonly providers: SsoProvider[];
49
- private clientAllowlist;
81
+ readonly clientAllowlist: string[];
50
82
  private discovery;
51
83
  constructor(hs: Homeserver, options: SsoOptions);
52
84
  private callbackUrl;
53
85
  private discover;
86
+ private scopes;
54
87
  /** URL to send the user's browser to. */
55
- startLogin(providerId: string | null, redirectUrl: string): Promise<string>;
56
- /** Handle the provider's redirect; returns the client URL carrying a login token. */
57
- finishLogin(providerId: string, params: URLSearchParams): Promise<SsoResult>;
88
+ startLogin(providerId: string | null, target: SsoTarget): Promise<string>;
89
+ /** Handle the provider's redirect: the user who logged in, and what the login was for. */
90
+ finishLogin(providerId: string, params: URLSearchParams): Promise<{
91
+ userId: string;
92
+ target: SsoTarget;
93
+ }>;
58
94
  private resolveUser;
59
95
  }
60
96
  //#endregion
package/dist/core/sso.js CHANGED
@@ -39,7 +39,35 @@ function decodeJwtPayload(jwt) {
39
39
  if (!part) throw new Error("Malformed ID token");
40
40
  return JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
41
41
  }
42
- /** Upstream OIDC login (authorization code + PKCE) issuing Matrix login tokens. */
42
+ /**
43
+ * GitHub as an identity provider (an OAuth app or GitHub app; its callback URL is
44
+ * `{publicBaseUrl}/_matrix/client/v3/login/sso/callback/github`). Users are matched by their
45
+ * numeric GitHub ID, so renaming the GitHub account keeps the Matrix account; new accounts take
46
+ * the GitHub login as their localpart.
47
+ */
48
+ function githubProvider(opts) {
49
+ return {
50
+ id: opts.id ?? "github",
51
+ name: opts.name ?? "GitHub",
52
+ brand: "github",
53
+ clientId: opts.clientId,
54
+ clientSecret: opts.clientSecret,
55
+ endpoints: {
56
+ authorization: "https://github.com/login/oauth/authorize",
57
+ token: "https://github.com/login/oauth/access_token",
58
+ userinfo: "https://api.github.com/user"
59
+ },
60
+ scopes: ["read:user"],
61
+ subjectClaim: "id",
62
+ mapUser: opts.mapUser ?? ((c) => ({
63
+ localpart: sanitizeLocalpart(String(c.login ?? c.id)),
64
+ displayname: c.name ?? c.login
65
+ })),
66
+ ...opts.allowRegistration !== void 0 ? { allowRegistration: opts.allowRegistration } : {}
67
+ };
68
+ }
69
+ const USER_AGENT = "matrix-nodejs-homeserver";
70
+ /** Upstream login (authorization code + PKCE) for the legacy `m.login.sso` and the OAuth 2.0 API's web pages. */
43
71
  var SsoService = class {
44
72
  hs;
45
73
  providers;
@@ -56,8 +84,18 @@ var SsoService = class {
56
84
  return `${base.replace(/\/$/, "")}/_matrix/client/v3/login/sso/callback/${encodeURIComponent(provider.id)}`;
57
85
  }
58
86
  async discover(provider) {
87
+ if (provider.endpoints) {
88
+ const e = provider.endpoints;
89
+ return {
90
+ issuer: provider.issuer,
91
+ authorization_endpoint: e.authorization,
92
+ token_endpoint: e.token,
93
+ userinfo_endpoint: e.userinfo
94
+ };
95
+ }
59
96
  const cached = this.discovery.get(provider.id);
60
97
  if (cached) return cached;
98
+ if (!provider.issuer) throw new Error(`Identity provider ${provider.id} needs an issuer or endpoints`);
61
99
  const res = await this.hs.fetch(`${provider.issuer.replace(/\/$/, "")}/.well-known/openid-configuration`, { signal: AbortSignal.timeout(1e4) });
62
100
  if (!res.ok) throw new Error(`OIDC discovery for ${provider.id} failed with ${res.status}`);
63
101
  const meta = await res.json();
@@ -65,8 +103,15 @@ var SsoService = class {
65
103
  this.discovery.set(provider.id, meta);
66
104
  return meta;
67
105
  }
106
+ scopes(provider) {
107
+ return provider.scopes ?? (provider.endpoints ? [] : [
108
+ "openid",
109
+ "profile",
110
+ "email"
111
+ ]);
112
+ }
68
113
  /** URL to send the user's browser to. */
69
- async startLogin(providerId, redirectUrl) {
114
+ async startLogin(providerId, target) {
70
115
  const provider = providerId ? this.providers.find((p) => p.id === providerId) : this.providers[0];
71
116
  if (!provider) throw errors.notFound("Unknown identity provider");
72
117
  const meta = await this.discover(provider);
@@ -75,7 +120,7 @@ var SsoService = class {
75
120
  const nonce = randomToken(16);
76
121
  await this.hs.cluster.set(`sso:${state}`, {
77
122
  providerId: provider.id,
78
- redirectUrl,
123
+ target,
79
124
  verifier,
80
125
  nonce,
81
126
  expires: Date.now() + 6e5
@@ -84,18 +129,15 @@ var SsoService = class {
84
129
  url.searchParams.set("response_type", "code");
85
130
  url.searchParams.set("client_id", provider.clientId);
86
131
  url.searchParams.set("redirect_uri", this.callbackUrl(provider));
87
- url.searchParams.set("scope", (provider.scopes ?? [
88
- "openid",
89
- "profile",
90
- "email"
91
- ]).join(" "));
132
+ const scopes = this.scopes(provider);
133
+ if (scopes.length) url.searchParams.set("scope", scopes.join(" "));
92
134
  url.searchParams.set("state", state);
93
135
  url.searchParams.set("nonce", nonce);
94
136
  url.searchParams.set("code_challenge", urlSafeBase64(createHash("sha256").update(verifier).digest()));
95
137
  url.searchParams.set("code_challenge_method", "S256");
96
138
  return url.toString();
97
139
  }
98
- /** Handle the provider's redirect; returns the client URL carrying a login token. */
140
+ /** Handle the provider's redirect: the user who logged in, and what the login was for. */
99
141
  async finishLogin(providerId, params) {
100
142
  const state = params.get("state") ?? "";
101
143
  const pending = await this.hs.cluster.get(`sso:${state}`);
@@ -115,7 +157,8 @@ var SsoService = class {
115
157
  });
116
158
  const headers = {
117
159
  "content-type": "application/x-www-form-urlencoded",
118
- accept: "application/json"
160
+ accept: "application/json",
161
+ "user-agent": USER_AGENT
119
162
  };
120
163
  if (provider.clientSecret) headers.authorization = `Basic ${Buffer.from(`${encodeURIComponent(provider.clientId)}:${encodeURIComponent(provider.clientSecret)}`).toString("base64")}`;
121
164
  const res = await this.hs.fetch(meta.token_endpoint, {
@@ -126,8 +169,9 @@ var SsoService = class {
126
169
  });
127
170
  if (!res.ok) throw errors.forbidden("Code exchange failed");
128
171
  const tokens = await res.json();
172
+ if (typeof tokens.error === "string") throw errors.forbidden("Code exchange failed");
129
173
  let claims = {};
130
- if ((provider.scopes ?? ["openid"]).includes("openid") && typeof tokens.id_token !== "string") throw errors.forbidden("The provider returned no ID token");
174
+ if (this.scopes(provider).includes("openid") && typeof tokens.id_token !== "string") throw errors.forbidden("The provider returned no ID token");
131
175
  if (typeof tokens.id_token === "string") {
132
176
  claims = decodeJwtPayload(tokens.id_token);
133
177
  const aud = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
@@ -137,7 +181,11 @@ var SsoService = class {
137
181
  }
138
182
  if (meta.userinfo_endpoint && typeof tokens.access_token === "string") {
139
183
  const info = await this.hs.fetch(meta.userinfo_endpoint, {
140
- headers: { authorization: `Bearer ${tokens.access_token}` },
184
+ headers: {
185
+ authorization: `Bearer ${tokens.access_token}`,
186
+ accept: "application/json",
187
+ "user-agent": USER_AGENT
188
+ },
141
189
  signal: AbortSignal.timeout(1e4)
142
190
  });
143
191
  if (info.ok) {
@@ -149,37 +197,35 @@ var SsoService = class {
149
197
  };
150
198
  }
151
199
  }
152
- if (typeof claims.sub !== "string") throw errors.forbidden("Provider did not return a subject");
153
- const userId = await this.resolveUser(provider, claims);
154
- const loginToken = await this.hs.auth.createLoginToken(userId);
200
+ const rawSubject = claims[provider.subjectClaim ?? "sub"];
201
+ const subject = typeof rawSubject === "number" ? String(rawSubject) : rawSubject;
202
+ if (typeof subject !== "string" || !subject) throw errors.forbidden("Provider did not return a subject");
203
+ const userId = await this.resolveUser(provider, subject, claims);
155
204
  await this.hs.audit.record({
156
205
  action: "sso_login",
157
206
  actor: userId,
158
207
  details: {
159
208
  idp: provider.id,
160
- subject: claims.sub
209
+ subject
161
210
  }
162
211
  });
163
- const redirect = new URL(pending.redirectUrl);
164
- redirect.searchParams.set("loginToken", loginToken);
165
212
  return {
166
- url: redirect.toString(),
167
- confirm: !this.clientAllowlist.some((entry) => clientMatches(pending.redirectUrl, entry)),
168
- client: redirect.host || redirect.protocol.replace(/:$/, "")
213
+ userId,
214
+ target: pending.target
169
215
  };
170
216
  }
171
- async resolveUser(provider, claims) {
217
+ async resolveUser(provider, subject, claims) {
172
218
  const link = await this.hs.db.findOne("sso_identities", [{
173
219
  field: "idp_id",
174
220
  value: provider.id
175
221
  }, {
176
222
  field: "subject",
177
- value: claims.sub
223
+ value: subject
178
224
  }]);
179
225
  if (link) return link.user_id;
180
226
  if (provider.allowRegistration === false) throw errors.forbidden("No account is linked to this identity");
181
227
  const mapped = provider.mapUser ? await provider.mapUser(claims) : {
182
- localpart: sanitizeLocalpart(claims.preferred_username ?? claims.sub),
228
+ localpart: sanitizeLocalpart(claims.preferred_username ?? subject),
183
229
  displayname: claims.name
184
230
  };
185
231
  if (!mapped || !LOCALPART_RE.test(mapped.localpart)) throw errors.forbidden("Could not map identity to a Matrix user");
@@ -191,11 +237,25 @@ var SsoService = class {
191
237
  const userId = await this.hs.admin.createUser(localpart, { displayname: mapped.displayname });
192
238
  await this.hs.db.create("sso_identities", {
193
239
  idp_id: provider.id,
194
- subject: claims.sub,
240
+ subject,
195
241
  user_id: userId
196
242
  });
197
243
  return userId;
198
244
  }
199
245
  };
246
+ /**
247
+ * The legacy API's last step: the client URL carrying a login token for `userId`, and whether
248
+ * the user confirms the client first (clients not on `sso.clientAllowlist`).
249
+ */
250
+ async function legacyClientRedirect(hs, userId, redirectUrl) {
251
+ const loginToken = await hs.auth.createLoginToken(userId);
252
+ const redirect = new URL(redirectUrl);
253
+ redirect.searchParams.set("loginToken", loginToken);
254
+ return {
255
+ url: redirect.toString(),
256
+ confirm: !(hs.sso?.clientAllowlist ?? []).some((entry) => clientMatches(redirectUrl, entry)),
257
+ client: redirect.host || redirect.protocol.replace(/:$/, "")
258
+ };
259
+ }
200
260
  //#endregion
201
- export { SsoService, checkRedirectUrl };
261
+ export { SsoService, checkRedirectUrl, githubProvider, legacyClientRedirect, sanitizeLocalpart };
@@ -15,6 +15,7 @@ import { TypingService } from "./core/typing.js";
15
15
  import { PushService } from "./core/push.js";
16
16
  import { AdminService, ServerNoticesOptions } from "./core/admin.js";
17
17
  import { OAuthDelegation, OAuthDelegationOptions } from "./core/oauth.js";
18
+ import { OAuthServer, OAuthServerOptions } from "./core/oauthserver.js";
18
19
  import { SsoOptions, SsoService } from "./core/sso.js";
19
20
  import { FederationOptions, FederationService } from "./federation/index.js";
20
21
  import { DelayedEventService, DelayedEventsOptions } from "./core/delayed.js";
@@ -26,6 +27,7 @@ import { SettingsService } from "./core/settings.js";
26
27
  import { AdminRoomOptions, AdminRoomService } from "./core/adminroom.js";
27
28
  import { ResolvedUserAuth, UserAuthAdapter } from "./adapters/auth.js";
28
29
  import { MediaAdapter, Thumbnailer } from "./adapters/media.js";
30
+ import { ChangeTracker } from "./core/changes.js";
29
31
  import { AddressPolicy } from "./core/outbound.js";
30
32
  import { EventEmitter } from "node:events";
31
33
  //#region src/homeserver.d.ts
@@ -84,7 +86,7 @@ export interface HomeserverOptions {
84
86
  /** Maximum `/sync` long-poll duration in ms. Default: 30000. */
85
87
  maxSyncTimeoutMs?: number;
86
88
  /**
87
- * How many `/sync` responses one process builds at once (default 4). Requests woken together
89
+ * How many `/sync` responses one process builds at once (default 16). Requests woken together
88
90
  * wait in the process instead of queueing their database reads ahead of everything else.
89
91
  */
90
92
  maxConcurrentSyncs?: number;
@@ -111,7 +113,16 @@ export interface HomeserverOptions {
111
113
  */
112
114
  /** Delegate authentication to an external OAuth 2.0 / OIDC provider (disables password login and registration). */
113
115
  oauth?: OAuthDelegationOptions;
114
- /** Upstream OpenID Connect providers for `m.login.sso`. Requires `publicBaseUrl`. */
116
+ /**
117
+ * Serve the spec's OAuth 2.0 API: users log in on the server's own web pages, with a password
118
+ * and/or the upstream providers of `sso` (e.g. `githubProvider`). Requires `publicBaseUrl`.
119
+ * Cannot be combined with `oauth`.
120
+ */
121
+ oauthServer?: boolean | OAuthServerOptions;
122
+ /**
123
+ * Upstream identity providers (OpenID Connect, or OAuth 2.0 such as GitHub) for `m.login.sso`
124
+ * and the `oauthServer` login page. Requires `publicBaseUrl`.
125
+ */
115
126
  sso?: SsoOptions;
116
127
  /**
117
128
  * Coordination between instances. The default (`localCluster()`) supports one process;
@@ -176,6 +187,11 @@ export declare class Homeserver extends EventEmitter {
176
187
  readonly serverName: string;
177
188
  readonly config: ResolvedConfig;
178
189
  readonly db: DatabaseAdapter;
190
+ /**
191
+ * Where keys last changed, when this is the only instance on its database (no cluster
192
+ * adapter): /sync skips the reads that would find nothing new (#51). Null in a cluster.
193
+ */
194
+ readonly changes: ChangeTracker | null;
179
195
  readonly media: MediaAdapter;
180
196
  readonly userAuth: ResolvedUserAuth;
181
197
  readonly logger: Logger;
@@ -192,6 +208,7 @@ export declare class Homeserver extends EventEmitter {
192
208
  readonly rateLimits: RateLimits;
193
209
  readonly admin: AdminService;
194
210
  readonly oauth: OAuthDelegation | null;
211
+ readonly oauthServer: OAuthServer | null;
195
212
  readonly sso: SsoService | null;
196
213
  readonly federation: FederationService;
197
214
  readonly delayedEvents: DelayedEventService;
@@ -14,7 +14,9 @@ import { Notifier } from "./core/notifier.js";
14
14
  import { StreamManager, localCluster, newInstanceId } from "./core/cluster.js";
15
15
  import { TypingService } from "./core/typing.js";
16
16
  import { coalesceReads } from "./storage/batch.js";
17
+ import { ChangeTracker } from "./core/changes.js";
17
18
  import { PushService } from "./core/push.js";
19
+ import { OAuthServer } from "./core/oauthserver.js";
18
20
  import { SsoService } from "./core/sso.js";
19
21
  import { AdminService } from "./core/admin.js";
20
22
  import { OAuthDelegation } from "./core/oauth.js";
@@ -53,6 +55,11 @@ var Homeserver = class extends EventEmitter {
53
55
  serverName;
54
56
  config;
55
57
  db;
58
+ /**
59
+ * Where keys last changed, when this is the only instance on its database (no cluster
60
+ * adapter): /sync skips the reads that would find nothing new (#51). Null in a cluster.
61
+ */
62
+ changes;
56
63
  media;
57
64
  userAuth;
58
65
  logger;
@@ -69,6 +76,7 @@ var Homeserver = class extends EventEmitter {
69
76
  rateLimits;
70
77
  admin;
71
78
  oauth;
79
+ oauthServer;
72
80
  sso;
73
81
  federation;
74
82
  delayedEvents;
@@ -115,7 +123,7 @@ var Homeserver = class extends EventEmitter {
115
123
  defaultRoomVersion,
116
124
  accessTokenLifetimeMs: options.accessTokenLifetimeMs ?? null,
117
125
  maxSyncTimeoutMs: options.maxSyncTimeoutMs ?? 3e4,
118
- maxConcurrentSyncs: Math.max(1, options.maxConcurrentSyncs ?? 4),
126
+ maxConcurrentSyncs: Math.max(1, options.maxConcurrentSyncs ?? 16),
119
127
  urlPreviews: options.urlPreviews ? { allowPrivateAddresses: typeof options.urlPreviews === "object" && !!options.urlPreviews.allowPrivateAddresses } : null,
120
128
  thumbnailer: options.thumbnailer ?? null,
121
129
  rtc: options.rtc ?? {},
@@ -125,19 +133,21 @@ var Homeserver = class extends EventEmitter {
125
133
  roomName: options.serverNotices?.roomName ?? "Server Notices"
126
134
  }
127
135
  };
128
- const db = options.databaseAdapter;
129
- if (db && typeof db.findOne === "function") this.db = coalesceReads(db);
130
- else {
136
+ this.changes = options.cluster ? null : new ChangeTracker();
137
+ let db = options.databaseAdapter;
138
+ if (!db || typeof db.findOne !== "function") {
131
139
  this.logger.warn("No database adapter given; using in-memory storage (data is lost on restart)");
132
- this.db = memoryAdapter();
140
+ db = memoryAdapter();
133
141
  }
142
+ this.db = coalesceReads(this.changes ? this.changes.wrap(db) : db);
134
143
  const media = options.mediaAdapter;
135
144
  this.media = media && typeof media.put === "function" ? media : memoryMedia();
136
145
  this.userAuth = resolveUserAuth(options.userAuthAdapter);
137
146
  this.cluster = options.cluster ?? localCluster();
138
147
  this.instanceId = newInstanceId();
139
- this.streams = new StreamManager(this.cluster, this.instanceId);
140
- this.notifier = new Notifier(this.cluster, this.instanceId);
148
+ this.streams = new StreamManager(this.cluster, this.instanceId, (...args) => this.logger.warn(...args));
149
+ this.notifier = new Notifier(this.cluster, this.instanceId, () => this.streams.lastAllocated, (fresh) => this.streams.position(fresh));
150
+ this.streams.onAdvance = () => this.notifier.advanced();
141
151
  this.auth = new AuthService(this);
142
152
  this.rooms = new RoomService(this, !options.cluster);
143
153
  this.keys = new KeyService(this);
@@ -145,6 +155,9 @@ var Homeserver = class extends EventEmitter {
145
155
  this.push = new PushService(this);
146
156
  this.admin = new AdminService(this);
147
157
  this.oauth = options.oauth ? new OAuthDelegation(this, options.oauth) : null;
158
+ if (options.oauthServer && options.oauth) throw new Error("`oauthServer` and `oauth` (delegated OAuth) cannot be used together");
159
+ if (options.oauthServer && !options.publicBaseUrl) throw new Error("`publicBaseUrl` is required for `oauthServer`");
160
+ this.oauthServer = options.oauthServer ? new OAuthServer(this, options.oauthServer === true ? {} : options.oauthServer, options.publicBaseUrl) : null;
148
161
  this.fetch = options.fetch ?? globalThis.fetch;
149
162
  this.addressPolicy = addressPolicy(options.allowPrivateAddresses);
150
163
  this.publicFetch = policyFetch(this.addressPolicy, options.fetch);
@@ -159,7 +172,7 @@ var Homeserver = class extends EventEmitter {
159
172
  this.identityServers = new IdentityServers(this);
160
173
  const adminRoom = options.adminRoom === true ? {} : options.adminRoom || null;
161
174
  this.adminRoom = adminRoom && adminRoom.enabled !== false ? new AdminRoomService(this, adminRoom) : null;
162
- this.sso = options.sso?.providers.length ? new SsoService(this, options.sso) : null;
175
+ this.sso = options.sso && (options.sso.providers.length || options.oauthServer) ? new SsoService(this, options.sso) : null;
163
176
  if (this.sso && !options.publicBaseUrl) throw new Error("`publicBaseUrl` is required when SSO is configured");
164
177
  this.rateLimits = options.rateLimits === false ? new RateLimits(null) : new RateLimits(options.rateLimits?.limiter ?? memoryRateLimiter(), options.rateLimits?.rules);
165
178
  const cleanupEvery = options.cleanupIntervalMs ?? 6e5;
@@ -197,6 +210,7 @@ var Homeserver = class extends EventEmitter {
197
210
  }
198
211
  const snap = await this.db.max("state_snapshots", "id");
199
212
  if (snap !== null) await this.streams.advanceTo(snap);
213
+ this.changes?.start(await this.streams.position(true));
200
214
  const oldest = await this.db.findMany("events", {
201
215
  sortBy: {
202
216
  field: "stream",
@@ -213,7 +227,13 @@ var Homeserver = class extends EventEmitter {
213
227
  limit: 1
214
228
  });
215
229
  if (oldestSnap[0] && oldestSnap[0].id < 0) await this.streams.lowerBackfillTo(oldestSnap[0].id);
216
- await this.cluster.advance("federation_outbox", await this.db.max("federation_outbox", "id") ?? 0);
230
+ for (const model of [
231
+ "federation_outbox",
232
+ "audit_log",
233
+ "event_reports",
234
+ "user_reports",
235
+ "appservice_outbox"
236
+ ]) await this.cluster.advance(model, await this.db.max(model, "id") ?? 0);
217
237
  if (this.federation.enabled) this.federation.sender.start();
218
238
  this.delayedEvents.start();
219
239
  this.appservices.start();
@@ -226,7 +246,11 @@ var Homeserver = class extends EventEmitter {
226
246
  field: "expires_ts",
227
247
  operator: "lt",
228
248
  value: now - 6048e5
249
+ }, {
250
+ field: "refresh_used",
251
+ value: true
229
252
  }]);
253
+ await this.oauthServer?.prune();
230
254
  await this.db.delete("login_tokens", [{
231
255
  field: "expires_ts",
232
256
  operator: "lt",
package/dist/index.d.ts CHANGED
@@ -5,7 +5,8 @@ import { DatabaseAdapter, FieldDef, FindManyOptions, ModelDef, Operator, Row, Sc
5
5
  import { ClusterAdapter, localCluster } from "./core/cluster.js";
6
6
  import { AdminService, ServerNoticesOptions } from "./core/admin.js";
7
7
  import { OAuthDelegationOptions } from "./core/oauth.js";
8
- import { SsoOptions, SsoProvider } from "./core/sso.js";
8
+ import { OAuthServerOptions } from "./core/oauthserver.js";
9
+ import { SsoOptions, SsoProvider, githubProvider } from "./core/sso.js";
9
10
  import { MatrixError } from "./core/errors.js";
10
11
  import { FederationOptions } from "./federation/index.js";
11
12
  import { DelayedEventsOptions } from "./core/delayed.js";
@@ -23,4 +24,4 @@ import { MysqlPoolLike, PgPoolLike, SqlAdapterOptions, SqlDialect, SqlExecutor,
23
24
  import { schema } from "./storage/schema.js";
24
25
  import { S3MediaOptions, s3Media } from "./adapters/s3.js";
25
26
  import { PgClusterPoolLike, postgresCluster } from "./adapters/cluster.js";
26
- export { type AdminRoomOptions, type AdminService, type AppServiceRegistration, type AuditAction, type AuditEntry, type AuditOptions, type ClusterAdapter, type DatabaseAdapter, type DelayedEventsOptions, type FederationOptions, type FieldDef, type FindManyOptions, Homeserver, type HomeserverOptions, type Logger, M, MatrixError, type MediaAdapter, type MediaMetadata, type ModelDef, type MysqlPoolLike, type NodeHandler, type OAuthDelegationOptions, type Operator, type PasswordCheckContext, type PgClusterPoolLike, type PgPoolLike, type RateLimitAction, type RateLimitRule, type RateLimiter, type Row, type RtcOptions, type S3MediaOptions, type Schema, type ServerNoticesOptions, type SqlAdapterOptions, type SqlDialect, type SqlExecutor, type SqliteDatabaseLike, type SsoOptions, type SsoProvider, type ThreepidEmail, type ThreepidOptions, type ThreepidPurpose, type ThreepidSms, type ThumbnailRequest, type Thumbnailer, type TurnOptions, UNSTABLE, type UserAuthAdapter, type WhereClause, createHomeserver, filesystemMedia, getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, handleRequest, isMatrixPath, loadAppServiceRegistrations, localCluster, memoryAdapter, memoryMedia, memoryRateLimiter, mysqlAdapter, parseAppServiceRegistration, postgresAdapter, postgresCluster, s3Media, schema, sharpThumbnailer, sqlAdapter, sqliteAdapter };
27
+ export { type AdminRoomOptions, type AdminService, type AppServiceRegistration, type AuditAction, type AuditEntry, type AuditOptions, type ClusterAdapter, type DatabaseAdapter, type DelayedEventsOptions, type FederationOptions, type FieldDef, type FindManyOptions, Homeserver, type HomeserverOptions, type Logger, M, MatrixError, type MediaAdapter, type MediaMetadata, type ModelDef, type MysqlPoolLike, type NodeHandler, type OAuthDelegationOptions, type OAuthServerOptions, type Operator, type PasswordCheckContext, type PgClusterPoolLike, type PgPoolLike, type RateLimitAction, type RateLimitRule, type RateLimiter, type Row, type RtcOptions, type S3MediaOptions, type Schema, type ServerNoticesOptions, type SqlAdapterOptions, type SqlDialect, type SqlExecutor, type SqliteDatabaseLike, type SsoOptions, type SsoProvider, type ThreepidEmail, type ThreepidOptions, type ThreepidPurpose, type ThreepidSms, type ThumbnailRequest, type Thumbnailer, type TurnOptions, UNSTABLE, type UserAuthAdapter, type WhereClause, createHomeserver, filesystemMedia, getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, githubProvider, handleRequest, isMatrixPath, loadAppServiceRegistrations, localCluster, memoryAdapter, memoryMedia, memoryRateLimiter, mysqlAdapter, parseAppServiceRegistration, postgresAdapter, postgresCluster, s3Media, schema, sharpThumbnailer, sqlAdapter, sqliteAdapter };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { M, UNSTABLE } from "./core/m.js";
2
2
  import { MatrixError } from "./core/errors.js";
3
3
  import { localCluster } from "./core/cluster.js";
4
+ import { githubProvider } from "./core/sso.js";
4
5
  import { loadAppServiceRegistrations, parseAppServiceRegistration } from "./core/appservices.js";
5
6
  import { memoryRateLimiter } from "./core/ratelimit.js";
6
7
  import { filesystemMedia, memoryMedia, sharpThumbnailer } from "./adapters/media.js";
@@ -12,4 +13,4 @@ import { getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, g
12
13
  import { mysqlAdapter, postgresAdapter, sqlAdapter, sqliteAdapter } from "./storage/sql.js";
13
14
  import { s3Media } from "./adapters/s3.js";
14
15
  import { postgresCluster } from "./adapters/cluster.js";
15
- export { Homeserver, M, MatrixError, UNSTABLE, createHomeserver, filesystemMedia, getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, handleRequest, isMatrixPath, loadAppServiceRegistrations, localCluster, memoryAdapter, memoryMedia, memoryRateLimiter, mysqlAdapter, parseAppServiceRegistration, postgresAdapter, postgresCluster, s3Media, schema, sharpThumbnailer, sqlAdapter, sqliteAdapter };
16
+ export { Homeserver, M, MatrixError, UNSTABLE, createHomeserver, filesystemMedia, getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, githubProvider, handleRequest, isMatrixPath, loadAppServiceRegistrations, localCluster, memoryAdapter, memoryMedia, memoryRateLimiter, mysqlAdapter, parseAppServiceRegistration, postgresAdapter, postgresCluster, s3Media, schema, sharpThumbnailer, sqlAdapter, sqliteAdapter };
@@ -48,10 +48,28 @@ const schema = {
48
48
  user_id: s,
49
49
  device_id: s,
50
50
  refresh_token: sn,
51
- expires_ts: nn
51
+ expires_ts: nn,
52
+ client_id: sn,
53
+ scope: sn,
54
+ refresh_parent: sn,
55
+ refresh_used: bn
52
56
  },
53
57
  primaryKey: ["token"],
54
- indexes: [["user_id"], ["refresh_token"]]
58
+ indexes: [
59
+ ["user_id"],
60
+ ["refresh_token"],
61
+ ["client_id"],
62
+ ["refresh_parent"]
63
+ ]
64
+ },
65
+ oauth_clients: {
66
+ fields: {
67
+ client_id: s,
68
+ metadata: j,
69
+ created_ts: n
70
+ },
71
+ primaryKey: ["client_id"],
72
+ indexes: [["created_ts"]]
55
73
  },
56
74
  rooms: {
57
75
  fields: {
@@ -207,8 +207,8 @@ function sqlAdapter({ dialect, executor: rootExecutor, tablePrefix = "", transac
207
207
  };
208
208
  return build(rootExecutor, false);
209
209
  }
210
- /** How long SQLite statements may run back to back before yielding to the event loop (#47). */
211
- const YIELD_AFTER_MS = 10;
210
+ /** How long SQLite statements may run back to back before yielding to the event loop (#47, #52). */
211
+ const YIELD_AFTER_MS = 50;
212
212
  /** Prepared statements an SQLite adapter keeps (#40). */
213
213
  const STATEMENT_CACHE = 500;
214
214
  /** SQLite adapter for `node:sqlite` (`new DatabaseSync(path)`) or `better-sqlite3`. */
@@ -1,7 +1,7 @@
1
1
  import { jsonStringifyReplacer } from "../core/util.js";
2
2
  import { $constructor } from "../core/core.js";
3
3
  import { $ZodError, flattenError, formatError } from "../core/errors.js";
4
- //#region node_modules/zod/v4/classic/errors.js
4
+ //#region ../../node_modules/zod/v4/classic/errors.js
5
5
  const _installedErrorProtos = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]);
6
6
  function _lazyMethod(proto, key, make) {
7
7
  Object.defineProperty(proto, key, {
@@ -1,6 +1,6 @@
1
1
  import { _decode, _decodeAsync, _encode, _encodeAsync, _parse, _parseAsync, _safeDecode, _safeDecodeAsync, _safeEncode, _safeEncodeAsync, _safeParse, _safeParseAsync } from "../core/parse.js";
2
2
  import { ZodRealError } from "./errors.js";
3
- //#region node_modules/zod/v4/classic/parse.js
3
+ //#region ../../node_modules/zod/v4/classic/parse.js
4
4
  const parse = /* @__PURE__ */ _parse(ZodRealError);
5
5
  const parseAsync = /* @__PURE__ */ _parseAsync(ZodRealError);
6
6
  const safeParse = /* @__PURE__ */ _safeParse(ZodRealError);
@@ -9,7 +9,7 @@ import { _array, _base64, _base64url, _boolean, _cidrv4, _cidrv6, _cuid, _cuid2,
9
9
  import { createStandardJSONSchemaMethod, createToJSONSchemaMethod } from "../core/to-json-schema.js";
10
10
  import { aggregateChecks, arrayProcessor, booleanProcessor, catchProcessor, customProcessor, defaultProcessor, enumProcessor, intersectionProcessor, neverProcessor, nonoptionalProcessor, nullableProcessor, numberProcessor, objectProcessor, optionalProcessor, pipeProcessor, prefaultProcessor, readonlyProcessor, recordProcessor, stringProcessor, transformProcessor, unionProcessor, unknownProcessor } from "../core/json-schema-processors.js";
11
11
  import { decode, decodeAsync, encode, encodeAsync, parse, parseAsync, safeDecode, safeDecodeAsync, safeEncode, safeEncodeAsync, safeParse, safeParseAsync } from "./parse.js";
12
- //#region node_modules/zod/v4/classic/schemas.js
12
+ //#region ../../node_modules/zod/v4/classic/schemas.js
13
13
  function _ensureDefaultLocale() {
14
14
  if (!globalConfig.localeError) config(en_default());
15
15
  }
@@ -1,6 +1,6 @@
1
1
  import { issue, normalizeParams, slugify } from "./util.js";
2
2
  import { $ZodCheck, $ZodCheckEndsWith, $ZodCheckGreaterThan, $ZodCheckIncludes, $ZodCheckLengthEquals, $ZodCheckLessThan, $ZodCheckLowerCase, $ZodCheckMaxLength, $ZodCheckMinLength, $ZodCheckMultipleOf, $ZodCheckOverwrite, $ZodCheckRegex, $ZodCheckStartsWith, $ZodCheckUpperCase } from "./checks.js";
3
- //#region node_modules/zod/v4/core/api.js
3
+ //#region ../../node_modules/zod/v4/core/api.js
4
4
  function snapshotChecks(def) {
5
5
  if (def.checks) def.checks = [...def.checks];
6
6
  return def;