matrix-nodejs-homeserver 0.2.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +89 -11
  2. package/dist/adapters/cluster.d.ts +1 -26
  3. package/dist/adapters/cluster.js +50 -103
  4. package/dist/api/account.js +407 -354
  5. package/dist/api/admin.js +82 -61
  6. package/dist/api/appservice.js +37 -25
  7. package/dist/api/backup.js +73 -62
  8. package/dist/api/federation.js +172 -138
  9. package/dist/api/filters.js +4 -3
  10. package/dist/api/legacy.js +91 -78
  11. package/dist/api/media.js +71 -55
  12. package/dist/api/misc.js +46 -37
  13. package/dist/api/preview.js +37 -73
  14. package/dist/api/profile.js +108 -97
  15. package/dist/api/push.js +114 -99
  16. package/dist/api/relationships.js +64 -58
  17. package/dist/api/rooms.js +703 -580
  18. package/dist/api/rtc.d.ts +0 -1
  19. package/dist/api/rtc.js +48 -34
  20. package/dist/api/search.js +133 -127
  21. package/dist/api/slidingsync.js +41 -31
  22. package/dist/api/spaces.js +82 -77
  23. package/dist/api/sync.js +325 -135
  24. package/dist/api/threadsubs.js +72 -61
  25. package/dist/api/threepid.js +99 -66
  26. package/dist/api/uia.js +17 -16
  27. package/dist/api/user.js +221 -178
  28. package/dist/core/admin.d.ts +0 -1
  29. package/dist/core/admin.js +6 -5
  30. package/dist/core/adminroom.js +9 -8
  31. package/dist/core/appservices.d.ts +2 -1
  32. package/dist/core/appservices.js +5 -4
  33. package/dist/core/audit.d.ts +0 -1
  34. package/dist/core/authrules.js +26 -25
  35. package/dist/core/cluster.d.ts +12 -5
  36. package/dist/core/cluster.js +46 -29
  37. package/dist/core/delayed.js +2 -1
  38. package/dist/core/events.js +11 -10
  39. package/dist/core/html.js +13 -0
  40. package/dist/core/identity.js +1 -1
  41. package/dist/core/invitefilter.js +2 -1
  42. package/dist/core/json.js +10 -1
  43. package/dist/core/keys.d.ts +10 -1
  44. package/dist/core/keys.js +62 -42
  45. package/dist/core/m.d.ts +209 -0
  46. package/dist/core/m.js +182 -0
  47. package/dist/core/outbound.d.ts +16 -0
  48. package/dist/core/outbound.js +172 -0
  49. package/dist/core/partialstate.js +2 -1
  50. package/dist/core/policy.js +4 -3
  51. package/dist/core/push.d.ts +0 -1
  52. package/dist/core/push.js +41 -28
  53. package/dist/core/pushrules.js +36 -35
  54. package/dist/core/ratelimit.d.ts +1 -1
  55. package/dist/core/rooms.d.ts +10 -1
  56. package/dist/core/rooms.js +101 -74
  57. package/dist/core/semaphore.js +23 -0
  58. package/dist/core/sso.d.ts +15 -1
  59. package/dist/core/sso.js +38 -2
  60. package/dist/core/stateres.js +7 -6
  61. package/dist/core/threepid.js +1 -9
  62. package/dist/federation/client.js +9 -1
  63. package/dist/federation/index.d.ts +2 -2
  64. package/dist/federation/index.js +43 -35
  65. package/dist/homeserver.d.ts +23 -4
  66. package/dist/homeserver.js +12 -3
  67. package/dist/http/handler.js +2 -23
  68. package/dist/http/input.js +2 -2
  69. package/dist/http/procedure.js +25 -0
  70. package/dist/http/router.d.ts +9 -26
  71. package/dist/http/router.js +10 -30
  72. package/dist/index.d.ts +4 -3
  73. package/dist/index.js +3 -2
  74. package/dist/storage/batch.js +11 -0
  75. package/dist/storage/memory.js +1 -0
  76. package/dist/storage/schema.js +1 -1
  77. package/dist/storage/sql.js +20 -1
  78. package/dist/storage/types.d.ts +16 -0
  79. package/dist/vendor/zod/LICENSE +21 -0
  80. package/dist/vendor/zod/v4/classic/errors.js +54 -0
  81. package/dist/vendor/zod/v4/classic/parse.js +17 -0
  82. package/dist/vendor/zod/v4/classic/schemas.js +865 -0
  83. package/dist/vendor/zod/v4/core/api.js +512 -0
  84. package/dist/vendor/zod/v4/core/checks.js +300 -0
  85. package/dist/vendor/zod/v4/core/core.js +109 -0
  86. package/dist/vendor/zod/v4/core/doc.js +35 -0
  87. package/dist/vendor/zod/v4/core/errors.js +118 -0
  88. package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
  89. package/dist/vendor/zod/v4/core/memoizer.js +255 -0
  90. package/dist/vendor/zod/v4/core/parse.js +206 -0
  91. package/dist/vendor/zod/v4/core/regexes.js +73 -0
  92. package/dist/vendor/zod/v4/core/registries.js +48 -0
  93. package/dist/vendor/zod/v4/core/schemas.js +1378 -0
  94. package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
  95. package/dist/vendor/zod/v4/core/util.js +537 -0
  96. package/dist/vendor/zod/v4/core/versions.js +8 -0
  97. package/dist/vendor/zod/v4/locales/en.js +110 -0
  98. package/package.json +8 -6
  99. package/dist/core/pushrules.d.ts +0 -1
@@ -1,7 +1,9 @@
1
1
  import { loadSigningKey, randomString } from "./core/crypto.js";
2
+ import "./core/m.js";
2
3
  import { SUPPORTED_ROOM_VERSIONS } from "./core/events.js";
3
4
  import { PartialStateService } from "./core/partialstate.js";
4
5
  import { RoomService } from "./core/rooms.js";
6
+ import { addressPolicy, policyFetch } from "./core/outbound.js";
5
7
  import { FederationService } from "./federation/index.js";
6
8
  import { PolicyServers } from "./core/policy.js";
7
9
  import { ThreepidService } from "./core/threepid.js";
@@ -12,9 +14,9 @@ import { Notifier } from "./core/notifier.js";
12
14
  import { StreamManager, localCluster, newInstanceId } from "./core/cluster.js";
13
15
  import { TypingService } from "./core/typing.js";
14
16
  import { PushService } from "./core/push.js";
17
+ import { SsoService } from "./core/sso.js";
15
18
  import { AdminService } from "./core/admin.js";
16
19
  import { OAuthDelegation } from "./core/oauth.js";
17
- import { SsoService } from "./core/sso.js";
18
20
  import { DelayedEventService } from "./core/delayed.js";
19
21
  import { AuditService } from "./core/audit.js";
20
22
  import { SettingsService } from "./core/settings.js";
@@ -77,8 +79,12 @@ var Homeserver = class extends EventEmitter {
77
79
  policyServers;
78
80
  threepids;
79
81
  identityServers;
80
- /** `fetch` used for outgoing requests (push gateways, URL previews, OAuth). Overridable for testing. */
82
+ /** `fetch` for requests to addresses the operator configured (OAuth, SSO, application services). */
81
83
  fetch;
84
+ /** Which addresses requests to user- or remote-chosen destinations may reach (see `allowPrivateAddresses`). */
85
+ addressPolicy;
86
+ /** `fetch` for user- or remote-chosen URLs: refuses private addresses unless allowed, never follows redirects. */
87
+ publicFetch;
82
88
  /** Resolves once storage is migrated and the server is ready to serve requests. */
83
89
  ready;
84
90
  _signingKey = null;
@@ -108,6 +114,7 @@ var Homeserver = class extends EventEmitter {
108
114
  defaultRoomVersion,
109
115
  accessTokenLifetimeMs: options.accessTokenLifetimeMs ?? null,
110
116
  maxSyncTimeoutMs: options.maxSyncTimeoutMs ?? 3e4,
117
+ maxConcurrentSyncs: Math.max(1, options.maxConcurrentSyncs ?? 4),
111
118
  urlPreviews: options.urlPreviews ? { allowPrivateAddresses: typeof options.urlPreviews === "object" && !!options.urlPreviews.allowPrivateAddresses } : null,
112
119
  thumbnailer: options.thumbnailer ?? null,
113
120
  rtc: options.rtc ?? {},
@@ -137,6 +144,9 @@ var Homeserver = class extends EventEmitter {
137
144
  this.push = new PushService(this);
138
145
  this.admin = new AdminService(this);
139
146
  this.oauth = options.oauth ? new OAuthDelegation(this, options.oauth) : null;
147
+ this.fetch = options.fetch ?? globalThis.fetch;
148
+ this.addressPolicy = addressPolicy(options.allowPrivateAddresses);
149
+ this.publicFetch = policyFetch(this.addressPolicy, options.fetch);
140
150
  this.federation = new FederationService(this, options.federation);
141
151
  this.delayedEvents = new DelayedEventService(this, options.delayedEvents);
142
152
  this.audit = new AuditService(this, options.audit);
@@ -156,7 +166,6 @@ var Homeserver = class extends EventEmitter {
156
166
  this.cleanupTimer = setInterval(() => void this.cleanup().catch((err) => this.logger.warn("Cleanup failed", err)), cleanupEvery);
157
167
  this.cleanupTimer.unref?.();
158
168
  }
159
- this.fetch = options.fetch ?? globalThis.fetch;
160
169
  registerRoutes(this.router);
161
170
  this.ready = this.init(options);
162
171
  this.ready.catch((err) => this.logger.error("Homeserver failed to start", err));
@@ -1,6 +1,6 @@
1
1
  import { canonicalJson } from "../core/json.js";
2
+ import { UNSTABLE } from "../core/m.js";
2
3
  import { MatrixError, errors } from "../core/errors.js";
3
- import { inputError } from "./input.js";
4
4
  //#region src/http/handler.ts
5
5
  const CORS_HEADERS = {
6
6
  "access-control-allow-origin": "*",
@@ -95,7 +95,7 @@ async function dispatch(hs, request, url, options) {
95
95
  const reg = hs.appservices.byToken(t);
96
96
  if (reg) {
97
97
  const userId = await hs.appservices.masquerade(reg, url.searchParams.get("user_id"));
98
- const deviceId = url.searchParams.get("device_id") ?? url.searchParams.get("org.matrix.msc3202.device_id");
98
+ const deviceId = url.searchParams.get("device_id") ?? url.searchParams.get(UNSTABLE.ORG.MATRIX.MSC3202.DEVICE_ID);
99
99
  if (deviceId && !await hs.db.findOne("devices", [{
100
100
  field: "user_id",
101
101
  value: userId
@@ -124,33 +124,12 @@ async function dispatch(hs, request, url, options) {
124
124
  async optionalAuth() {
125
125
  return token() ? ctx.auth() : null;
126
126
  },
127
- input: void 0,
128
127
  async rateLimit(action) {
129
128
  const s = token() ? await ctx.auth().catch(() => null) : null;
130
129
  if (s?.isAdmin || s?.appservice && s.appservice.rate_limited === false) return;
131
130
  await hs.rateLimits.check(action, s?.userId ?? ctx.ip ?? "unknown");
132
131
  }
133
132
  };
134
- if (matched.input) {
135
- const schema = matched.input;
136
- let raw;
137
- let readError;
138
- try {
139
- raw = await ctx.json();
140
- } catch (err) {
141
- readError = err;
142
- }
143
- let parsed = null;
144
- Object.defineProperty(ctx, "input", { get() {
145
- if (readError) throw readError;
146
- if (!parsed) {
147
- const result = schema.safeParse(raw);
148
- if (!result.success) throw inputError(result.error);
149
- parsed = { data: result.data };
150
- }
151
- return parsed.data;
152
- } });
153
- }
154
133
  const result = await matched.handler(ctx);
155
134
  if (result instanceof Response) return withCors(result);
156
135
  return jsonResponse(result ?? {});
@@ -1,8 +1,8 @@
1
1
  import { MatrixError } from "../core/errors.js";
2
- import { z } from "zod";
2
+ import { looseObject } from "../vendor/zod/v4/classic/schemas.js";
3
3
  //#region src/http/input.ts
4
4
  /** A request body: a JSON object whose unknown keys are kept. */
5
- const body = (shape) => z.looseObject(shape);
5
+ const body = (shape) => looseObject(shape);
6
6
  /**
7
7
  * Errors carry their Matrix errcode in the message, as `M_CODE: text`: `fail` builds the zod
8
8
  * error option for a field. Without one, a missing field is M_MISSING_PARAM and any other
@@ -0,0 +1,25 @@
1
+ import { inputError } from "./input.js";
2
+ //#region src/http/procedure.ts
3
+ function procedure(def) {
4
+ return async (ctx) => {
5
+ const session = def.auth === "user" ? await ctx.auth() : def.auth === "optional" ? await ctx.optionalAuth() : void 0;
6
+ const extra = def.guard ? await def.guard(ctx) : {};
7
+ if (def.rateLimit) await ctx.rateLimit(def.rateLimit);
8
+ let input;
9
+ if (def.input) {
10
+ const parsed = def.input.safeParse(await ctx.json());
11
+ if (!parsed.success) throw inputError(parsed.error);
12
+ input = parsed.data;
13
+ }
14
+ const result = await def.handler(Object.assign(ctx, extra, {
15
+ session,
16
+ input
17
+ }));
18
+ if (!def.output || result instanceof Response) return result;
19
+ const checked = def.output.safeParse(result);
20
+ if (!checked.success) throw new Error(`Response does not match the output schema: ${checked.error.message}`);
21
+ return checked.data;
22
+ };
23
+ }
24
+ //#endregion
25
+ export { procedure };
@@ -1,10 +1,9 @@
1
1
  import { Session } from "../core/auth.js";
2
2
  import { RateLimitAction } from "../core/ratelimit.js";
3
3
  import { Homeserver } from "../homeserver.js";
4
- import { z } from "zod";
5
4
  //#region src/http/router.d.ts
6
5
  export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
7
- export interface RequestContext<I = unknown> {
6
+ export interface RequestContext {
8
7
  hs: Homeserver;
9
8
  request: Request;
10
9
  url: URL;
@@ -23,38 +22,22 @@ export interface RequestContext<I = unknown> {
23
22
  accessToken(): string | null;
24
23
  /** Apply the rate limit for `action` to the requesting user (or IP when unauthenticated). */
25
24
  rateLimit(action: RateLimitAction): Promise<void>;
26
- /**
27
- * The request body parsed with the route's `input` schema (undefined for routes without one).
28
- * Validated on first access, which throws the Matrix error for an invalid body: read it where
29
- * the handler needs the body, after authenticating.
30
- */
31
- input: I;
32
25
  }
33
26
  export type HandlerResult = Response | object | void;
34
- export type Handler<I = any> = (ctx: RequestContext<I>) => Promise<HandlerResult> | HandlerResult;
35
- /** Per-route options. `input` validates and parses the JSON body before the handler runs. */
36
- export interface RouteOptions<S extends z.ZodType = z.ZodType> {
37
- input?: S;
38
- }
39
- /** A route's handler, with the body type its `input` schema produces. */
40
- type Routed<S> = S extends z.ZodType ? Handler<z.output<S>> : Handler;
27
+ /** What the router calls; endpoints build one with `procedure()` (see http/procedure.ts). */
28
+ export type Handler = (ctx: RequestContext) => Promise<HandlerResult> | HandlerResult;
41
29
  export declare class Router {
42
30
  private routes;
43
- add<S extends z.ZodType | undefined = undefined>(method: HttpMethod, path: string, handler: Routed<S>, options?: RouteOptions<NonNullable<S>>): void;
44
- get(path: string, h: Handler): void;
45
- post<S extends z.ZodType>(path: string, options: RouteOptions<S>, h: Routed<S>): void;
46
- post(path: string, h: Handler): void;
47
- put<S extends z.ZodType>(path: string, options: RouteOptions<S>, h: Routed<S>): void;
48
- put(path: string, h: Handler): void;
49
- delete(path: string, h: Handler): void;
31
+ add(method: HttpMethod, path: string, handler: Handler): void;
32
+ get: (path: string, h: Handler) => void;
33
+ post: (path: string, h: Handler) => void;
34
+ put: (path: string, h: Handler) => void;
35
+ delete: (path: string, h: Handler) => void;
50
36
  /** Register a client-server API route under both `r0` and `v3` (or the given versions). */
51
- client<S extends z.ZodType>(method: HttpMethod, path: string, options: RouteOptions<S>, handler: Routed<S>, versions?: string[]): void;
52
37
  client(method: HttpMethod, path: string, handler: Handler, versions?: string[]): void;
53
38
  match(method: string, pathname: string): {
54
39
  handler: Handler;
55
40
  params: Record<string, string>;
56
- input?: z.ZodType;
57
41
  } | "method" | null;
58
42
  }
59
- //#endregion
60
- export {};
43
+ //#endregion
@@ -1,39 +1,20 @@
1
1
  //#region src/http/router.ts
2
2
  var Router = class {
3
3
  routes = [];
4
- add(method, path, handler, options = {}) {
4
+ add(method, path, handler) {
5
5
  this.routes.push({
6
6
  method,
7
7
  segments: path.split("/").filter(Boolean),
8
- handler,
9
- input: options.input
8
+ handler
10
9
  });
11
10
  }
12
- get(path, h) {
13
- this.add("GET", path, h);
14
- }
15
- post(path, a, b) {
16
- if (typeof a === "function") this.add("POST", path, a);
17
- else this.add("POST", path, b, a);
18
- }
19
- put(path, a, b) {
20
- if (typeof a === "function") this.add("PUT", path, a);
21
- else this.add("PUT", path, b, a);
22
- }
23
- delete(path, h) {
24
- this.add("DELETE", path, h);
25
- }
26
- client(method, path, a, b, c) {
27
- const [options, handler, versions] = typeof a === "function" ? [
28
- {},
29
- a,
30
- b ?? ["r0", "v3"]
31
- ] : [
32
- a,
33
- b,
34
- c ?? ["r0", "v3"]
35
- ];
36
- for (const v of versions) this.add(method, `/_matrix/client/${v}${path}`, handler, options);
11
+ get = (path, h) => this.add("GET", path, h);
12
+ post = (path, h) => this.add("POST", path, h);
13
+ put = (path, h) => this.add("PUT", path, h);
14
+ delete = (path, h) => this.add("DELETE", path, h);
15
+ /** Register a client-server API route under both `r0` and `v3` (or the given versions). */
16
+ client(method, path, handler, versions = ["r0", "v3"]) {
17
+ for (const v of versions) this.add(method, `/_matrix/client/${v}${path}`, handler);
37
18
  }
38
19
  match(method, pathname) {
39
20
  const parts = pathname.split("/").filter(Boolean);
@@ -47,8 +28,7 @@ var Router = class {
47
28
  }
48
29
  return {
49
30
  handler: route.handler,
50
- params,
51
- input: route.input
31
+ params
52
32
  };
53
33
  }
54
34
  return methodMismatch ? "method" : null;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { ThreepidEmail, ThreepidOptions, ThreepidPurpose, ThreepidSms } from "./core/threepid.js";
2
+ import { M, UNSTABLE } from "./core/m.js";
2
3
  import { AppServiceRegistration, loadAppServiceRegistrations, parseAppServiceRegistration } from "./core/appservices.js";
3
- import { ClusterAdapter, localCluster } from "./core/cluster.js";
4
4
  import { DatabaseAdapter, FieldDef, FindManyOptions, ModelDef, Operator, Row, Schema, WhereClause } from "./storage/types.js";
5
+ import { ClusterAdapter, localCluster } from "./core/cluster.js";
5
6
  import { AdminService, ServerNoticesOptions } from "./core/admin.js";
6
7
  import { OAuthDelegationOptions } from "./core/oauth.js";
7
8
  import { SsoOptions, SsoProvider } from "./core/sso.js";
@@ -21,5 +22,5 @@ import { memoryAdapter } from "./storage/memory.js";
21
22
  import { MysqlPoolLike, PgPoolLike, SqlAdapterOptions, SqlDialect, SqlExecutor, SqliteDatabaseLike, mysqlAdapter, postgresAdapter, sqlAdapter, sqliteAdapter } from "./storage/sql.js";
22
23
  import { schema } from "./storage/schema.js";
23
24
  import { S3MediaOptions, s3Media } from "./adapters/s3.js";
24
- import { PgClusterPoolLike, RedisLike, RedisSubscriberLike, postgresCluster, redisCluster } from "./adapters/cluster.js";
25
- 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, 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 RedisLike, type RedisSubscriberLike, 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, type UserAuthAdapter, type WhereClause, createHomeserver, filesystemMedia, getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, handleRequest, isMatrixPath, loadAppServiceRegistrations, localCluster, memoryAdapter, memoryMedia, memoryRateLimiter, mysqlAdapter, parseAppServiceRegistration, postgresAdapter, postgresCluster, redisCluster, s3Media, schema, sharpThumbnailer, sqlAdapter, sqliteAdapter };
25
+ 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 };
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { M, UNSTABLE } from "./core/m.js";
1
2
  import { MatrixError } from "./core/errors.js";
2
3
  import { localCluster } from "./core/cluster.js";
3
4
  import { loadAppServiceRegistrations, parseAppServiceRegistration } from "./core/appservices.js";
@@ -10,5 +11,5 @@ import { Homeserver, createHomeserver } from "./homeserver.js";
10
11
  import { getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, isMatrixPath } from "./http/adapters.js";
11
12
  import { mysqlAdapter, postgresAdapter, sqlAdapter, sqliteAdapter } from "./storage/sql.js";
12
13
  import { s3Media } from "./adapters/s3.js";
13
- import { postgresCluster, redisCluster } from "./adapters/cluster.js";
14
- export { Homeserver, MatrixError, createHomeserver, filesystemMedia, getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, handleRequest, isMatrixPath, loadAppServiceRegistrations, localCluster, memoryAdapter, memoryMedia, memoryRateLimiter, mysqlAdapter, parseAppServiceRegistration, postgresAdapter, postgresCluster, redisCluster, s3Media, schema, sharpThumbnailer, sqlAdapter, sqliteAdapter };
14
+ 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 };
@@ -0,0 +1,11 @@
1
+ //#region src/storage/batch.ts
2
+ /**
3
+ * Run independent reads together: in one round trip when the adapter can (`findManyBatch`),
4
+ * otherwise concurrently. Results come in the order of the queries.
5
+ */
6
+ function readBatch(db, queries) {
7
+ if (db.findManyBatch) return db.findManyBatch(queries);
8
+ return Promise.all(queries.map((query) => db.findMany(query.model, query.opts)));
9
+ }
10
+ //#endregion
11
+ export { readBatch };
@@ -72,6 +72,7 @@ function memoryAdapter() {
72
72
  rows.sort((a, b) => a[field] < b[field] ? -dir : a[field] > b[field] ? dir : 0);
73
73
  }
74
74
  if (opts.limit !== void 0) rows = rows.slice(0, opts.limit);
75
+ if (opts.fields?.length) return rows.map((r) => Object.fromEntries(opts.fields.map((f) => [f, structuredClone(r[f] ?? null)])));
75
76
  return rows.map((r) => structuredClone(r));
76
77
  },
77
78
  async update(model, where, data) {
@@ -249,7 +249,7 @@ const schema = {
249
249
  forgotten: b
250
250
  },
251
251
  primaryKey: ["room_id", "user_id"],
252
- indexes: [["user_id"]]
252
+ indexes: [["user_id"], ["stream"]]
253
253
  },
254
254
  room_aliases: {
255
255
  fields: {
@@ -130,11 +130,30 @@ function sqlAdapter({ dialect, executor: rootExecutor, tablePrefix = "", transac
130
130
  },
131
131
  async findMany(model, opts = {}) {
132
132
  const params = [];
133
- let sql = `SELECT * FROM ${t(model)}${buildWhere(model, opts.where, params)}`;
133
+ let sql = `SELECT ${opts.fields?.length ? opts.fields.map(q).join(", ") : "*"} FROM ${t(model)}${buildWhere(model, opts.where, params)}`;
134
134
  if (opts.sortBy) sql += ` ORDER BY ${q(opts.sortBy.field)} ${opts.sortBy.direction === "asc" ? "ASC" : "DESC"}`;
135
135
  if (opts.limit !== void 0) sql += ` LIMIT ${Math.max(0, Math.floor(opts.limit))}`;
136
136
  return (await executor.query(sql, params)).map((r) => fromDb(model, r));
137
137
  },
138
+ ...dialect === "postgres" ? { async findManyBatch(queries) {
139
+ if (queries.length === 0) return [];
140
+ const params = [];
141
+ const parts = queries.map(({ model, opts = {} }, i) => {
142
+ const fields = opts.fields?.length && opts.sortBy && !opts.fields.includes(opts.sortBy.field) ? [...opts.fields, opts.sortBy.field] : opts.fields;
143
+ let inner = `SELECT ${fields?.length ? fields.map(q).join(", ") : "*"} FROM ${t(model)}${buildWhere(model, opts.where, params)}`;
144
+ const order = opts.sortBy ? `${q(opts.sortBy.field)} ${opts.sortBy.direction === "asc" ? "ASC" : "DESC"}` : "";
145
+ if (order) inner += ` ORDER BY ${order}`;
146
+ if (opts.limit !== void 0) inner += ` LIMIT ${Math.max(0, Math.floor(opts.limit))}`;
147
+ return `SELECT ${i} AS i, row_number() OVER (${order ? `ORDER BY ${order.replace(/^"/, "b.\"")}` : ""}) AS n, to_jsonb(b) AS r FROM (${inner}) b`;
148
+ });
149
+ const rows = await executor.query(`${parts.join(" UNION ALL ")} ORDER BY i, n`, params);
150
+ const out = queries.map(() => []);
151
+ for (const row of rows) {
152
+ const r = typeof row.r === "string" ? JSON.parse(row.r) : row.r;
153
+ out[Number(row.i)].push(fromDb(queries[Number(row.i)].model, r));
154
+ }
155
+ return out;
156
+ } } : {},
138
157
  async update(model, where, data) {
139
158
  const fields = Object.keys(data).filter((k) => data[k] !== void 0);
140
159
  if (fields.length === 0) return 0;
@@ -21,8 +21,18 @@ export interface WhereClause {
21
21
  operator?: Operator;
22
22
  }
23
23
  export type Row = Record<string, any>;
24
+ /** One read of a `findManyBatch`. */
25
+ export interface BatchQuery {
26
+ model: string;
27
+ opts?: FindManyOptions;
28
+ }
24
29
  export interface FindManyOptions {
25
30
  where?: WhereClause[];
31
+ /**
32
+ * Only these fields are needed. Adapters should read just these (a smaller query and less to
33
+ * parse); one that ignores the option and returns whole rows is still correct.
34
+ */
35
+ fields?: string[];
26
36
  sortBy?: {
27
37
  field: string;
28
38
  direction: "asc" | "desc";
@@ -35,6 +45,12 @@ export interface FindManyOptions {
35
45
  * All `where` clauses are combined with AND.
36
46
  */
37
47
  export interface DatabaseAdapter {
48
+ /**
49
+ * Several independent reads in one go: the results in the same order as the queries. Optional;
50
+ * an adapter that talks to a database server can send them in one round trip. Without it,
51
+ * callers run the reads separately (see `readBatch`).
52
+ */
53
+ findManyBatch?(queries: BatchQuery[]): Promise<Row[][]>;
38
54
  /** Create tables / collections for the given schema. Must be idempotent. */
39
55
  migrate?(schema: Schema): Promise<void>;
40
56
  create(model: string, data: Row): Promise<void>;
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Colin McDonnell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,54 @@
1
+ import { jsonStringifyReplacer } from "../core/util.js";
2
+ import { $constructor } from "../core/core.js";
3
+ import { $ZodError, flattenError, formatError } from "../core/errors.js";
4
+ //#region node_modules/zod/v4/classic/errors.js
5
+ const _installedErrorProtos = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]);
6
+ function _lazyMethod(proto, key, make) {
7
+ Object.defineProperty(proto, key, {
8
+ configurable: true,
9
+ enumerable: false,
10
+ get() {
11
+ const value = make(this);
12
+ Object.defineProperty(this, key, {
13
+ value,
14
+ configurable: true,
15
+ writable: true
16
+ });
17
+ return value;
18
+ },
19
+ set(value) {
20
+ Object.defineProperty(this, key, {
21
+ value,
22
+ configurable: true,
23
+ writable: true
24
+ });
25
+ }
26
+ });
27
+ }
28
+ const initializer = (inst, issues) => {
29
+ $ZodError.init(inst, issues);
30
+ inst.name = "ZodError";
31
+ const proto = Object.getPrototypeOf(inst);
32
+ if (_installedErrorProtos.has(proto)) return;
33
+ _installedErrorProtos.add(proto);
34
+ _lazyMethod(proto, "format", (self) => (mapper) => formatError(self, mapper));
35
+ _lazyMethod(proto, "flatten", (self) => (mapper) => flattenError(self, mapper));
36
+ _lazyMethod(proto, "addIssue", (self) => (issue) => {
37
+ self.issues.push(issue);
38
+ self.message = JSON.stringify(self.issues, jsonStringifyReplacer, 2);
39
+ });
40
+ _lazyMethod(proto, "addIssues", (self) => (issues) => {
41
+ self.issues.push(...issues);
42
+ self.message = JSON.stringify(self.issues, jsonStringifyReplacer, 2);
43
+ });
44
+ Object.defineProperty(proto, "isEmpty", {
45
+ configurable: true,
46
+ enumerable: false,
47
+ get() {
48
+ return this.issues.length === 0;
49
+ }
50
+ });
51
+ };
52
+ const ZodRealError = /*@__PURE__*/ $constructor("ZodError", initializer, void 0, { Parent: Error });
53
+ //#endregion
54
+ export { ZodRealError };
@@ -0,0 +1,17 @@
1
+ import { _decode, _decodeAsync, _encode, _encodeAsync, _parse, _parseAsync, _safeDecode, _safeDecodeAsync, _safeEncode, _safeEncodeAsync, _safeParse, _safeParseAsync } from "../core/parse.js";
2
+ import { ZodRealError } from "./errors.js";
3
+ //#region node_modules/zod/v4/classic/parse.js
4
+ const parse = /* @__PURE__ */ _parse(ZodRealError);
5
+ const parseAsync = /* @__PURE__ */ _parseAsync(ZodRealError);
6
+ const safeParse = /* @__PURE__ */ _safeParse(ZodRealError);
7
+ const safeParseAsync = /* @__PURE__ */ _safeParseAsync(ZodRealError);
8
+ const encode = /* @__PURE__ */ _encode(ZodRealError);
9
+ const decode = /* @__PURE__ */ _decode(ZodRealError);
10
+ const encodeAsync = /* @__PURE__ */ _encodeAsync(ZodRealError);
11
+ const decodeAsync = /* @__PURE__ */ _decodeAsync(ZodRealError);
12
+ const safeEncode = /* @__PURE__ */ _safeEncode(ZodRealError);
13
+ const safeDecode = /* @__PURE__ */ _safeDecode(ZodRealError);
14
+ const safeEncodeAsync = /* @__PURE__ */ _safeEncodeAsync(ZodRealError);
15
+ const safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync(ZodRealError);
16
+ //#endregion
17
+ export { decode, decodeAsync, encode, encodeAsync, parse, parseAsync, safeDecode, safeDecodeAsync, safeEncode, safeEncodeAsync, safeParse, safeParseAsync };