matrix-nodejs-homeserver 0.3.1 → 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.
@@ -2,6 +2,7 @@ import { canonicalJson } from "../core/json.js";
2
2
  import { unpaddedBase64 } from "../core/crypto.js";
3
3
  import { M } from "../core/m.js";
4
4
  import { MatrixError } from "../core/errors.js";
5
+ import { checkLiteral } from "../core/outbound.js";
5
6
  import { sign } from "node:crypto";
6
7
  import { resolveSrv } from "node:dns/promises";
7
8
  import { Agent, request } from "node:https";
@@ -169,6 +170,11 @@ var FederationClient = class {
169
170
  return null;
170
171
  }
171
172
  raw(target, method, pathWithQuery, body, headers, timeoutMs, poolKey = target.hostHeader) {
173
+ try {
174
+ checkLiteral(this.hs.addressPolicy, target.host);
175
+ } catch (err) {
176
+ return Promise.reject(err);
177
+ }
172
178
  return new Promise((resolve, reject) => {
173
179
  let connectTimer = null;
174
180
  const req = request({
@@ -178,6 +184,7 @@ var FederationClient = class {
178
184
  path: pathWithQuery,
179
185
  servername: isIP(target.tlsName) ? void 0 : target.tlsName,
180
186
  agent: this.agentFor(poolKey),
187
+ lookup: this.hs.addressPolicy.lookup,
181
188
  headers: {
182
189
  host: target.hostHeader,
183
190
  ...headers,
@@ -136,7 +136,7 @@ export declare class FederationService {
136
136
  */
137
137
  backfill(roomId: string, fromEventIds: string[], limit?: number): Promise<number>;
138
138
  /** Download remote media (authenticated federation endpoint, falling back to the legacy one). */
139
- fetchMedia(server: string, mediaId: string): Promise<{
139
+ fetchMedia(server: string, mediaId: string, maxBytes: number): Promise<{
140
140
  data: Uint8Array;
141
141
  contentType: string | null;
142
142
  filename: string | null;
@@ -8,6 +8,7 @@ import { authStateFromAuthEvents, checkAuth, getState, membershipOf } from "../c
8
8
  import { globMatch } from "../core/glob.js";
9
9
  import { resolveState } from "../core/stateres.js";
10
10
  import { MissingStateError } from "../core/rooms.js";
11
+ import { publicGet } from "../core/outbound.js";
11
12
  import { FederationClient } from "./client.js";
12
13
  import { KeyRing } from "./keyring.js";
13
14
  import { TransactionSender } from "./sender.js";
@@ -961,13 +962,16 @@ var FederationService = class {
961
962
  return 0;
962
963
  }
963
964
  /** Download remote media (authenticated federation endpoint, falling back to the legacy one). */
964
- async fetchMedia(server, mediaId) {
965
+ async fetchMedia(server, mediaId, maxBytes) {
965
966
  this.requireEnabled();
966
967
  const res = await this.client.requestRaw(server, "GET", `/_matrix/federation/v1/media/download/${encodeURIComponent(mediaId)}`, { timeoutMs: 6e4 });
967
968
  if (res.status === 200) {
968
969
  const ct = String(res.headers["content-type"] ?? "");
969
970
  const m = /boundary=("?)([^";]+)\1/.exec(ct);
970
- if (ct.startsWith("multipart/") && m) return parseMultipartMedia(res.body, m[2], this);
971
+ if (ct.startsWith("multipart/") && m) return parseMultipartMedia(res.body, m[2], {
972
+ maxBytes,
973
+ fetch: this.hs.publicFetch
974
+ });
971
975
  }
972
976
  const legacy = await this.client.requestRaw(server, "GET", `/_matrix/media/v3/download/${encodeURIComponent(server)}/${encodeURIComponent(mediaId)}`, {
973
977
  query: { allow_remote: "false" },
@@ -1020,7 +1024,7 @@ function filenameFromDisposition(header) {
1020
1024
  return plain ? plain[1] : null;
1021
1025
  }
1022
1026
  /** Parse a federation media multipart/mixed response: a JSON part then the file (or a redirect). */
1023
- async function parseMultipartMedia(body, boundary, fed) {
1027
+ async function parseMultipartMedia(body, boundary, limits) {
1024
1028
  const delimiter = Buffer.from(`--${boundary}`);
1025
1029
  const parts = [];
1026
1030
  let idx = body.indexOf(delimiter);
@@ -1041,10 +1045,13 @@ async function parseMultipartMedia(body, boundary, fed) {
1041
1045
  if (i > 0) headers[line.slice(0, i).trim().toLowerCase()] = line.slice(i + 1).trim();
1042
1046
  }
1043
1047
  if (headers.location) {
1044
- const res = await fetch(headers.location, { signal: AbortSignal.timeout(6e4) });
1045
- if (!res.ok) throw errors.notFound("Remote media redirect failed");
1048
+ const res = await publicGet(limits.fetch, headers.location, {
1049
+ maxBytes: limits.maxBytes,
1050
+ timeoutMs: 6e4
1051
+ });
1052
+ if (res.status !== 200) throw errors.notFound("Remote media redirect failed");
1046
1053
  return {
1047
- data: new Uint8Array(await res.arrayBuffer()),
1054
+ data: new Uint8Array(res.body),
1048
1055
  contentType: res.headers.get("content-type"),
1049
1056
  filename: filenameFromDisposition(res.headers.get("content-disposition") ?? void 0)
1050
1057
  };
@@ -6,10 +6,10 @@ import { IdentityServers } from "./core/identity.js";
6
6
  import { M } from "./core/m.js";
7
7
  import { AppServiceRegistration, AppServiceService } from "./core/appservices.js";
8
8
  import { AuthService } from "./core/auth.js";
9
+ import { DatabaseAdapter } from "./storage/types.js";
9
10
  import { KeyService } from "./core/keys.js";
10
11
  import { ClusterAdapter, StreamManager } from "./core/cluster.js";
11
12
  import { Notifier } from "./core/notifier.js";
12
- import { DatabaseAdapter } from "./storage/types.js";
13
13
  import { RoomService } from "./core/rooms.js";
14
14
  import { TypingService } from "./core/typing.js";
15
15
  import { PushService } from "./core/push.js";
@@ -26,6 +26,7 @@ import { SettingsService } from "./core/settings.js";
26
26
  import { AdminRoomOptions, AdminRoomService } from "./core/adminroom.js";
27
27
  import { ResolvedUserAuth, UserAuthAdapter } from "./adapters/auth.js";
28
28
  import { MediaAdapter, Thumbnailer } from "./adapters/media.js";
29
+ import { AddressPolicy } from "./core/outbound.js";
29
30
  import { EventEmitter } from "node:events";
30
31
  //#region src/homeserver.d.ts
31
32
  export interface Logger {
@@ -82,6 +83,11 @@ export interface HomeserverOptions {
82
83
  accessTokenLifetimeMs?: number;
83
84
  /** Maximum `/sync` long-poll duration in ms. Default: 30000. */
84
85
  maxSyncTimeoutMs?: number;
86
+ /**
87
+ * How many `/sync` responses one process builds at once (default 4). Requests woken together
88
+ * wait in the process instead of queueing their database reads ahead of everything else.
89
+ */
90
+ maxConcurrentSyncs?: number;
85
91
  /**
86
92
  * Enables `/preview_url`. Off by default because it makes the server fetch arbitrary URLs.
87
93
  * Private/internal addresses stay blocked unless `allowPrivateAddresses` is set.
@@ -92,6 +98,13 @@ export interface HomeserverOptions {
92
98
  logger?: Logger;
93
99
  /** Custom `fetch` for outgoing HTTP requests. */
94
100
  fetch?: typeof globalThis.fetch;
101
+ /**
102
+ * Let requests to addresses that users or remote servers choose (federation, push gateways,
103
+ * identity servers, URL previews, remote media) reach private addresses: localhost, LAN and
104
+ * other non-public ranges. `true` allows all of them, a list allows those ranges only (e.g.
105
+ * `["127.0.0.1/32"]` for a push gateway on the same machine). Default: none.
106
+ */
107
+ allowPrivateAddresses?: boolean | string[];
95
108
  /**
96
109
  * Rate limiting. `false` disables it; otherwise per-action overrides and an optional
97
110
  * shared limiter (defaults to an in-memory token bucket). Admins are never limited.
@@ -102,7 +115,7 @@ export interface HomeserverOptions {
102
115
  sso?: SsoOptions;
103
116
  /**
104
117
  * Coordination between instances. The default (`localCluster()`) supports one process;
105
- * use `postgresCluster()` / `redisCluster()` to scale horizontally.
118
+ * use `postgresCluster()` to scale horizontally.
106
119
  */
107
120
  cluster?: ClusterAdapter;
108
121
  /**
@@ -151,6 +164,7 @@ export interface ResolvedConfig {
151
164
  defaultRoomVersion: string;
152
165
  accessTokenLifetimeMs: number | null;
153
166
  maxSyncTimeoutMs: number;
167
+ maxConcurrentSyncs: number;
154
168
  urlPreviews: {
155
169
  allowPrivateAddresses: boolean;
156
170
  } | null;
@@ -189,8 +203,12 @@ export declare class Homeserver extends EventEmitter {
189
203
  readonly policyServers: PolicyServers;
190
204
  readonly threepids: ThreepidService;
191
205
  readonly identityServers: IdentityServers;
192
- /** `fetch` used for outgoing requests (push gateways, URL previews, OAuth). Overridable for testing. */
206
+ /** `fetch` for requests to addresses the operator configured (OAuth, SSO, application services). */
193
207
  readonly fetch: typeof globalThis.fetch;
208
+ /** Which addresses requests to user- or remote-chosen destinations may reach (see `allowPrivateAddresses`). */
209
+ readonly addressPolicy: AddressPolicy;
210
+ /** `fetch` for user- or remote-chosen URLs: refuses private addresses unless allowed, never follows redirects. */
211
+ readonly publicFetch: typeof globalThis.fetch;
194
212
  /** Resolves once storage is migrated and the server is ready to serve requests. */
195
213
  readonly ready: Promise<void>;
196
214
  private _signingKey;
@@ -3,6 +3,7 @@ import "./core/m.js";
3
3
  import { SUPPORTED_ROOM_VERSIONS } from "./core/events.js";
4
4
  import { PartialStateService } from "./core/partialstate.js";
5
5
  import { RoomService } from "./core/rooms.js";
6
+ import { addressPolicy, policyFetch } from "./core/outbound.js";
6
7
  import { FederationService } from "./federation/index.js";
7
8
  import { PolicyServers } from "./core/policy.js";
8
9
  import { ThreepidService } from "./core/threepid.js";
@@ -13,9 +14,9 @@ import { Notifier } from "./core/notifier.js";
13
14
  import { StreamManager, localCluster, newInstanceId } from "./core/cluster.js";
14
15
  import { TypingService } from "./core/typing.js";
15
16
  import { PushService } from "./core/push.js";
17
+ import { SsoService } from "./core/sso.js";
16
18
  import { AdminService } from "./core/admin.js";
17
19
  import { OAuthDelegation } from "./core/oauth.js";
18
- import { SsoService } from "./core/sso.js";
19
20
  import { DelayedEventService } from "./core/delayed.js";
20
21
  import { AuditService } from "./core/audit.js";
21
22
  import { SettingsService } from "./core/settings.js";
@@ -78,8 +79,12 @@ var Homeserver = class extends EventEmitter {
78
79
  policyServers;
79
80
  threepids;
80
81
  identityServers;
81
- /** `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). */
82
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;
83
88
  /** Resolves once storage is migrated and the server is ready to serve requests. */
84
89
  ready;
85
90
  _signingKey = null;
@@ -109,6 +114,7 @@ var Homeserver = class extends EventEmitter {
109
114
  defaultRoomVersion,
110
115
  accessTokenLifetimeMs: options.accessTokenLifetimeMs ?? null,
111
116
  maxSyncTimeoutMs: options.maxSyncTimeoutMs ?? 3e4,
117
+ maxConcurrentSyncs: Math.max(1, options.maxConcurrentSyncs ?? 4),
112
118
  urlPreviews: options.urlPreviews ? { allowPrivateAddresses: typeof options.urlPreviews === "object" && !!options.urlPreviews.allowPrivateAddresses } : null,
113
119
  thumbnailer: options.thumbnailer ?? null,
114
120
  rtc: options.rtc ?? {},
@@ -138,6 +144,9 @@ var Homeserver = class extends EventEmitter {
138
144
  this.push = new PushService(this);
139
145
  this.admin = new AdminService(this);
140
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);
141
150
  this.federation = new FederationService(this, options.federation);
142
151
  this.delayedEvents = new DelayedEventService(this, options.delayedEvents);
143
152
  this.audit = new AuditService(this, options.audit);
@@ -157,7 +166,6 @@ var Homeserver = class extends EventEmitter {
157
166
  this.cleanupTimer = setInterval(() => void this.cleanup().catch((err) => this.logger.warn("Cleanup failed", err)), cleanupEvery);
158
167
  this.cleanupTimer.unref?.();
159
168
  }
160
- this.fetch = options.fetch ?? globalThis.fetch;
161
169
  registerRoutes(this.router);
162
170
  this.ready = this.init(options);
163
171
  this.ready.catch((err) => this.logger.error("Homeserver failed to start", err));
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { ThreepidEmail, ThreepidOptions, ThreepidPurpose, ThreepidSms } from "./core/threepid.js";
2
2
  import { M, UNSTABLE } from "./core/m.js";
3
3
  import { AppServiceRegistration, loadAppServiceRegistrations, parseAppServiceRegistration } from "./core/appservices.js";
4
- import { ClusterAdapter, localCluster } from "./core/cluster.js";
5
4
  import { DatabaseAdapter, FieldDef, FindManyOptions, ModelDef, Operator, Row, Schema, WhereClause } from "./storage/types.js";
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
8
  import { SsoOptions, SsoProvider } from "./core/sso.js";
@@ -22,5 +22,5 @@ import { memoryAdapter } from "./storage/memory.js";
22
22
  import { MysqlPoolLike, PgPoolLike, SqlAdapterOptions, SqlDialect, SqlExecutor, SqliteDatabaseLike, mysqlAdapter, postgresAdapter, sqlAdapter, sqliteAdapter } from "./storage/sql.js";
23
23
  import { schema } from "./storage/schema.js";
24
24
  import { S3MediaOptions, s3Media } from "./adapters/s3.js";
25
- import { PgClusterPoolLike, RedisLike, RedisSubscriberLike, postgresCluster, redisCluster } 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 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, UNSTABLE, 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
@@ -11,5 +11,5 @@ import { Homeserver, createHomeserver } from "./homeserver.js";
11
11
  import { getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, isMatrixPath } from "./http/adapters.js";
12
12
  import { mysqlAdapter, postgresAdapter, sqlAdapter, sqliteAdapter } from "./storage/sql.js";
13
13
  import { s3Media } from "./adapters/s3.js";
14
- import { postgresCluster, redisCluster } 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, 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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matrix-nodejs-homeserver",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "A framework- and database-agnostic Matrix homeserver library for Node.js",
5
5
  "keywords": [
6
6
  "chat",
@@ -41,10 +41,12 @@
41
41
  },
42
42
  "scripts": {
43
43
  "typecheck": "tsc -p tsconfig.json",
44
+ "test": "node --test \"test/**/*.test.ts\"",
44
45
  "lint": "oxlint",
45
46
  "format": "oxfmt .",
46
47
  "format:check": "oxfmt --check .",
47
48
  "build": "tsdown",
49
+ "check:package": "publint && attw --pack . --profile esm-only",
48
50
  "prepare": "npm run build",
49
51
  "start:example": "node examples/node-http.ts",
50
52
  "start:multi-node-example": "node examples/multi-node.ts",
@@ -53,15 +55,17 @@
53
55
  "release": "changeset publish"
54
56
  },
55
57
  "devDependencies": {
58
+ "@arethetypeswrong/cli": "^0.18.5",
56
59
  "@changesets/cli": "^3.0.3",
57
60
  "@types/node": "^26.6.5",
58
61
  "oxfmt": "^0.73.0",
59
62
  "oxlint": "^1.88.0",
63
+ "publint": "^0.3.25",
60
64
  "tsdown": "^0.23.1",
61
65
  "typescript": "7.0.2",
62
66
  "zod": "^4.6.7"
63
67
  },
64
68
  "engines": {
65
- "node": ">=26"
69
+ "node": ">=22"
66
70
  }
67
71
  }