matrix-nodejs-homeserver 0.0.0-stage → 0.2.0

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 (120) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +457 -2
  3. package/dist/adapters/auth.d.ts +50 -0
  4. package/dist/adapters/auth.js +18 -0
  5. package/dist/adapters/cluster.d.ts +55 -0
  6. package/dist/adapters/cluster.js +249 -0
  7. package/dist/adapters/media.d.ts +50 -0
  8. package/dist/adapters/media.js +63 -0
  9. package/dist/adapters/s3.d.ts +33 -0
  10. package/dist/adapters/s3.js +95 -0
  11. package/dist/api/account.js +560 -0
  12. package/dist/api/admin.js +251 -0
  13. package/dist/api/appservice.js +33 -0
  14. package/dist/api/backup.js +269 -0
  15. package/dist/api/federation.js +792 -0
  16. package/dist/api/filters.js +87 -0
  17. package/dist/api/index.js +47 -0
  18. package/dist/api/legacy.js +170 -0
  19. package/dist/api/media.js +255 -0
  20. package/dist/api/misc.js +86 -0
  21. package/dist/api/preview.js +200 -0
  22. package/dist/api/profile.js +165 -0
  23. package/dist/api/push.js +187 -0
  24. package/dist/api/relationships.js +264 -0
  25. package/dist/api/rooms.js +1364 -0
  26. package/dist/api/rtc.d.ts +24 -0
  27. package/dist/api/rtc.js +52 -0
  28. package/dist/api/search.js +155 -0
  29. package/dist/api/slidingsync.js +464 -0
  30. package/dist/api/spaces.js +172 -0
  31. package/dist/api/sync.js +600 -0
  32. package/dist/api/threadsubs.js +129 -0
  33. package/dist/api/threepid.js +122 -0
  34. package/dist/api/uia.js +126 -0
  35. package/dist/api/user.js +286 -0
  36. package/dist/core/admin.d.ts +85 -0
  37. package/dist/core/admin.js +307 -0
  38. package/dist/core/adminroom.d.ts +49 -0
  39. package/dist/core/adminroom.js +482 -0
  40. package/dist/core/appservices.d.ts +73 -0
  41. package/dist/core/appservices.js +368 -0
  42. package/dist/core/audit.d.ts +51 -0
  43. package/dist/core/audit.js +86 -0
  44. package/dist/core/auth.d.ts +48 -0
  45. package/dist/core/auth.js +237 -0
  46. package/dist/core/authrules.d.ts +4 -0
  47. package/dist/core/authrules.js +347 -0
  48. package/dist/core/cluster.d.ts +66 -0
  49. package/dist/core/cluster.js +191 -0
  50. package/dist/core/crypto.d.ts +10 -0
  51. package/dist/core/crypto.js +100 -0
  52. package/dist/core/delayed.d.ts +39 -0
  53. package/dist/core/delayed.js +159 -0
  54. package/dist/core/errors.d.ts +9 -0
  55. package/dist/core/errors.js +45 -0
  56. package/dist/core/events.d.ts +32 -0
  57. package/dist/core/events.js +185 -0
  58. package/dist/core/glob.js +66 -0
  59. package/dist/core/identity.d.ts +24 -0
  60. package/dist/core/identity.js +96 -0
  61. package/dist/core/ids.js +36 -0
  62. package/dist/core/invitefilter.js +35 -0
  63. package/dist/core/json.js +16 -0
  64. package/dist/core/keys.d.ts +36 -0
  65. package/dist/core/keys.js +449 -0
  66. package/dist/core/notifier.d.ts +17 -0
  67. package/dist/core/notifier.js +54 -0
  68. package/dist/core/oauth.d.ts +38 -0
  69. package/dist/core/oauth.js +108 -0
  70. package/dist/core/partialstate.d.ts +32 -0
  71. package/dist/core/partialstate.js +197 -0
  72. package/dist/core/phone.js +268 -0
  73. package/dist/core/policy.d.ts +29 -0
  74. package/dist/core/policy.js +96 -0
  75. package/dist/core/profile.js +39 -0
  76. package/dist/core/push.d.ts +47 -0
  77. package/dist/core/push.js +306 -0
  78. package/dist/core/pushrules.d.ts +1 -0
  79. package/dist/core/pushrules.js +228 -0
  80. package/dist/core/ratelimit.d.ts +23 -0
  81. package/dist/core/ratelimit.js +77 -0
  82. package/dist/core/rooms.d.ts +215 -0
  83. package/dist/core/rooms.js +1729 -0
  84. package/dist/core/settings.d.ts +11 -0
  85. package/dist/core/settings.js +29 -0
  86. package/dist/core/sso.d.ts +46 -0
  87. package/dist/core/sso.js +165 -0
  88. package/dist/core/stateres.d.ts +4 -0
  89. package/dist/core/stateres.js +208 -0
  90. package/dist/core/threepid.d.ts +80 -0
  91. package/dist/core/threepid.js +275 -0
  92. package/dist/core/typing.d.ts +16 -0
  93. package/dist/core/typing.js +60 -0
  94. package/dist/federation/client.d.ts +51 -0
  95. package/dist/federation/client.js +278 -0
  96. package/dist/federation/index.d.ts +153 -0
  97. package/dist/federation/index.js +1058 -0
  98. package/dist/federation/keyring.d.ts +26 -0
  99. package/dist/federation/keyring.js +122 -0
  100. package/dist/federation/sender.d.ts +26 -0
  101. package/dist/federation/sender.js +164 -0
  102. package/dist/homeserver.d.ts +223 -0
  103. package/dist/homeserver.js +301 -0
  104. package/dist/http/adapters.d.ts +40 -0
  105. package/dist/http/adapters.js +130 -0
  106. package/dist/http/handler.d.ts +8 -0
  107. package/dist/http/handler.js +202 -0
  108. package/dist/http/input.js +24 -0
  109. package/dist/http/router.d.ts +60 -0
  110. package/dist/http/router.js +76 -0
  111. package/dist/index.d.ts +25 -0
  112. package/dist/index.js +14 -0
  113. package/dist/storage/memory.d.ts +5 -0
  114. package/dist/storage/memory.js +122 -0
  115. package/dist/storage/schema.d.ts +4 -0
  116. package/dist/storage/schema.js +664 -0
  117. package/dist/storage/sql.d.ts +72 -0
  118. package/dist/storage/sql.js +274 -0
  119. package/dist/storage/types.d.ts +55 -0
  120. package/package.json +67 -4
@@ -0,0 +1,274 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ //#region src/storage/sql.ts
3
+ const OPS = {
4
+ eq: "=",
5
+ ne: "<>",
6
+ lt: "<",
7
+ lte: "<=",
8
+ gt: ">",
9
+ gte: ">="
10
+ };
11
+ /**
12
+ * Generic SQL adapter. Works with any driver that can run parameterised
13
+ * queries; see `sqliteAdapter` and `postgresAdapter` for ready-made wrappers.
14
+ */
15
+ function sqlAdapter({ dialect, executor: rootExecutor, tablePrefix = "", transaction }) {
16
+ let schema = {};
17
+ const q = (ident) => dialect === "mysql" ? `\`${ident}\`` : `"${ident}"`;
18
+ const t = (model) => q(tablePrefix + model);
19
+ const columnType = (type, isKey) => {
20
+ switch (type) {
21
+ case "string": return dialect === "mysql" ? isKey ? "VARCHAR(255)" : "TEXT" : "TEXT";
22
+ case "number": return dialect === "sqlite" ? "INTEGER" : "BIGINT";
23
+ case "boolean": return dialect === "postgres" ? "BOOLEAN" : "INTEGER";
24
+ case "json": return dialect === "mysql" ? "LONGTEXT" : "TEXT";
25
+ }
26
+ };
27
+ const columnsOf = async (executor, table) => {
28
+ const rows = dialect === "sqlite" ? await executor.query(`PRAGMA table_info(${q(table)})`, []) : dialect === "postgres" ? await executor.query("SELECT column_name AS name FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = $1", [table]) : await executor.query("SELECT COLUMN_NAME AS name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ?", [table]);
29
+ return new Set(rows.map((r) => String(r.name)));
30
+ };
31
+ const fieldType = (model, field) => schema[model]?.fields[field]?.type ?? "string";
32
+ const toDb = (model, field, value) => {
33
+ if (value === void 0 || value === null) return null;
34
+ switch (fieldType(model, field)) {
35
+ case "json": return JSON.stringify(value);
36
+ case "boolean": return dialect === "postgres" ? !!value : value ? 1 : 0;
37
+ default: return value;
38
+ }
39
+ };
40
+ const fromDb = (model, row) => {
41
+ const out = {};
42
+ for (const [field, value] of Object.entries(row)) {
43
+ if (value === null || value === void 0) {
44
+ out[field] = null;
45
+ continue;
46
+ }
47
+ switch (fieldType(model, field)) {
48
+ case "json":
49
+ out[field] = typeof value === "string" ? JSON.parse(value) : value;
50
+ break;
51
+ case "boolean":
52
+ out[field] = value === true || value === 1 || value === "1" || value === 1n;
53
+ break;
54
+ case "number":
55
+ out[field] = Number(value);
56
+ break;
57
+ default: out[field] = value;
58
+ }
59
+ }
60
+ return out;
61
+ };
62
+ const placeholder = (i) => dialect === "postgres" ? `$${i}` : "?";
63
+ const buildWhere = (model, where = [], params) => {
64
+ if (where.length === 0) return "";
65
+ return ` WHERE ${where.map((w) => {
66
+ const op = w.operator ?? "eq";
67
+ if (op === "in") {
68
+ const values = w.value;
69
+ if (values.length === 0) return "1=0";
70
+ const ph = values.map((v) => {
71
+ params.push(toDb(model, w.field, v));
72
+ return placeholder(params.length);
73
+ });
74
+ return `${q(w.field)} IN (${ph.join(", ")})`;
75
+ }
76
+ if (w.value === null || w.value === void 0) {
77
+ if (op === "eq") return `${q(w.field)} IS NULL`;
78
+ if (op === "ne") return `${q(w.field)} IS NOT NULL`;
79
+ }
80
+ params.push(toDb(model, w.field, w.value));
81
+ return `${q(w.field)} ${OPS[op]} ${placeholder(params.length)}`;
82
+ }).join(" AND ")}`;
83
+ };
84
+ const build = (executor, inTx) => {
85
+ const insert = async (model, data, mode) => {
86
+ const fields = Object.keys(data).filter((k) => data[k] !== void 0);
87
+ const params = fields.map((f) => toDb(model, f, data[f]));
88
+ const cols = fields.map(q).join(", ");
89
+ const ph = fields.map((_, i) => placeholder(i + 1)).join(", ");
90
+ let sql = `INSERT INTO ${t(model)} (${cols}) VALUES (${ph})`;
91
+ if (mode === "upsert") {
92
+ const pk = schema[model]?.primaryKey ?? [];
93
+ const rest = fields.filter((f) => !pk.includes(f));
94
+ if (dialect === "mysql") sql += rest.length ? ` ON DUPLICATE KEY UPDATE ${rest.map((f) => `${q(f)} = VALUES(${q(f)})`).join(", ")}` : ` ON DUPLICATE KEY UPDATE ${q(pk[0])} = ${q(pk[0])}`;
95
+ else sql += ` ON CONFLICT (${pk.map(q).join(", ")}) DO ${rest.length ? `UPDATE SET ${rest.map((f) => `${q(f)} = excluded.${q(f)}`).join(", ")}` : "NOTHING"}`;
96
+ }
97
+ await executor.execute(sql, params);
98
+ };
99
+ const adapter = {
100
+ async migrate(s) {
101
+ schema = s;
102
+ for (const [model, def] of Object.entries(s)) {
103
+ const cols = Object.entries(def.fields).map(([name, f]) => {
104
+ const isKey = def.primaryKey.includes(name) || (def.indexes ?? []).some((i) => i.includes(name));
105
+ return `${q(name)} ${columnType(f.type, isKey)}${f.required === false ? "" : " NOT NULL"}`;
106
+ });
107
+ cols.push(`PRIMARY KEY (${def.primaryKey.map(q).join(", ")})`);
108
+ const ine = "IF NOT EXISTS";
109
+ await executor.execute(`CREATE TABLE ${ine} ${t(model)} (${cols.join(", ")})`, []);
110
+ const existing = await columnsOf(executor, tablePrefix + model);
111
+ for (const [name, f] of Object.entries(def.fields)) {
112
+ if (existing.has(name)) continue;
113
+ await executor.execute(`ALTER TABLE ${t(model)} ADD COLUMN ${q(name)} ${columnType(f.type, false)}`, []);
114
+ }
115
+ for (const index of def.indexes ?? []) {
116
+ const name = q(`idx_${tablePrefix}${model}_${index.join("_")}`);
117
+ if (dialect === "mysql") try {
118
+ await executor.execute(`CREATE INDEX ${name} ON ${t(model)} (${index.map(q).join(", ")})`, []);
119
+ } catch {}
120
+ else await executor.execute(`CREATE INDEX ${ine} ${name} ON ${t(model)} (${index.map(q).join(", ")})`, []);
121
+ }
122
+ }
123
+ },
124
+ create: (model, data) => insert(model, data, "insert"),
125
+ upsert: (model, data) => insert(model, data, "upsert"),
126
+ async findOne(model, where) {
127
+ const params = [];
128
+ const rows = await executor.query(`SELECT * FROM ${t(model)}${buildWhere(model, where, params)} LIMIT 1`, params);
129
+ return rows[0] ? fromDb(model, rows[0]) : null;
130
+ },
131
+ async findMany(model, opts = {}) {
132
+ const params = [];
133
+ let sql = `SELECT * FROM ${t(model)}${buildWhere(model, opts.where, params)}`;
134
+ if (opts.sortBy) sql += ` ORDER BY ${q(opts.sortBy.field)} ${opts.sortBy.direction === "asc" ? "ASC" : "DESC"}`;
135
+ if (opts.limit !== void 0) sql += ` LIMIT ${Math.max(0, Math.floor(opts.limit))}`;
136
+ return (await executor.query(sql, params)).map((r) => fromDb(model, r));
137
+ },
138
+ async update(model, where, data) {
139
+ const fields = Object.keys(data).filter((k) => data[k] !== void 0);
140
+ if (fields.length === 0) return 0;
141
+ const params = fields.map((f) => toDb(model, f, data[f]));
142
+ const set = fields.map((f, i) => `${q(f)} = ${placeholder(i + 1)}`).join(", ");
143
+ return executor.execute(`UPDATE ${t(model)} SET ${set}${buildWhere(model, where, params)}`, params);
144
+ },
145
+ async delete(model, where) {
146
+ const params = [];
147
+ return executor.execute(`DELETE FROM ${t(model)}${buildWhere(model, where, params)}`, params);
148
+ },
149
+ async max(model, field) {
150
+ const m = (await executor.query(`SELECT MAX(${q(field)}) AS m FROM ${t(model)}`, []))[0]?.m;
151
+ return m === null || m === void 0 ? null : Number(m);
152
+ }
153
+ };
154
+ if (transaction) adapter.transaction = (fn) => inTx ? fn(adapter) : transaction((ex) => fn(build(ex, true)));
155
+ return adapter;
156
+ };
157
+ return build(rootExecutor, false);
158
+ }
159
+ /** SQLite adapter for `node:sqlite` (`new DatabaseSync(path)`) or `better-sqlite3`. */
160
+ function sqliteAdapter(db, options = {}) {
161
+ try {
162
+ db.exec("PRAGMA journal_mode = WAL");
163
+ } catch {}
164
+ const cache = /* @__PURE__ */ new Map();
165
+ const stmt = (sql) => {
166
+ let s = cache.get(sql);
167
+ if (!s) cache.set(sql, s = db.prepare(sql));
168
+ return s;
169
+ };
170
+ const inTransaction = new AsyncLocalStorage();
171
+ let gate = Promise.resolve();
172
+ const waitGate = async () => {
173
+ if (!inTransaction.getStore()) await gate;
174
+ };
175
+ const executor = {
176
+ async query(sql, params) {
177
+ await waitGate();
178
+ return stmt(sql).all(...params);
179
+ },
180
+ async execute(sql, params) {
181
+ await waitGate();
182
+ return Number(stmt(sql).run(...params).changes);
183
+ }
184
+ };
185
+ return sqlAdapter({
186
+ dialect: "sqlite",
187
+ tablePrefix: options.tablePrefix,
188
+ executor,
189
+ async transaction(fn) {
190
+ if (inTransaction.getStore()) return fn(executor);
191
+ const previous = gate;
192
+ let release;
193
+ gate = new Promise((r) => release = r);
194
+ await previous;
195
+ try {
196
+ return await inTransaction.run(true, async () => {
197
+ db.exec("BEGIN IMMEDIATE");
198
+ try {
199
+ const result = await fn(executor);
200
+ db.exec("COMMIT");
201
+ return result;
202
+ } catch (err) {
203
+ db.exec("ROLLBACK");
204
+ throw err;
205
+ }
206
+ });
207
+ } finally {
208
+ release();
209
+ }
210
+ }
211
+ });
212
+ }
213
+ const pgExecutor = (conn) => ({
214
+ async query(sql, params) {
215
+ return (await conn.query(sql, params)).rows;
216
+ },
217
+ async execute(sql, params) {
218
+ return (await conn.query(sql, params)).rowCount ?? 0;
219
+ }
220
+ });
221
+ /** PostgreSQL adapter for `pg` (node-postgres). Pass a `Pool` to get transactions. */
222
+ function postgresAdapter(pool, options = {}) {
223
+ return sqlAdapter({
224
+ dialect: "postgres",
225
+ tablePrefix: options.tablePrefix,
226
+ executor: pgExecutor(pool),
227
+ transaction: pool.connect ? async (fn) => {
228
+ const client = await pool.connect();
229
+ try {
230
+ await client.query("BEGIN");
231
+ const result = await fn(pgExecutor(client));
232
+ await client.query("COMMIT");
233
+ return result;
234
+ } catch (err) {
235
+ await client.query("ROLLBACK").catch(() => {});
236
+ throw err;
237
+ } finally {
238
+ client.release();
239
+ }
240
+ } : void 0
241
+ });
242
+ }
243
+ const mysqlExecutor = (conn) => ({
244
+ async query(sql, params) {
245
+ return (await conn.query(sql, params))[0];
246
+ },
247
+ async execute(sql, params) {
248
+ return (await conn.query(sql, params))[0]?.affectedRows ?? 0;
249
+ }
250
+ });
251
+ /** MySQL / MariaDB adapter for `mysql2/promise` pools. */
252
+ function mysqlAdapter(pool, options = {}) {
253
+ return sqlAdapter({
254
+ dialect: "mysql",
255
+ tablePrefix: options.tablePrefix,
256
+ executor: mysqlExecutor(pool),
257
+ transaction: pool.getConnection ? async (fn) => {
258
+ const conn = await pool.getConnection();
259
+ try {
260
+ await conn.query("START TRANSACTION");
261
+ const result = await fn(mysqlExecutor(conn));
262
+ await conn.query("COMMIT");
263
+ return result;
264
+ } catch (err) {
265
+ await conn.query("ROLLBACK").catch(() => {});
266
+ throw err;
267
+ } finally {
268
+ conn.release();
269
+ }
270
+ } : void 0
271
+ });
272
+ }
273
+ //#endregion
274
+ export { mysqlAdapter, postgresAdapter, sqlAdapter, sqliteAdapter };
@@ -0,0 +1,55 @@
1
+ //#region src/storage/types.d.ts
2
+ export type FieldType = "string" | "number" | "boolean" | "json";
3
+ export interface FieldDef {
4
+ type: FieldType;
5
+ /** Defaults to true. Nullable fields may be omitted on create. */
6
+ required?: boolean;
7
+ }
8
+ export interface ModelDef {
9
+ fields: Record<string, FieldDef>;
10
+ /** Primary key, possibly composite. */
11
+ primaryKey: string[];
12
+ /** Secondary indexes to create (SQL adapters). */
13
+ indexes?: string[][];
14
+ }
15
+ export type Schema = Record<string, ModelDef>;
16
+ export type Operator = "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in";
17
+ export interface WhereClause {
18
+ field: string;
19
+ value: unknown;
20
+ /** Defaults to "eq". */
21
+ operator?: Operator;
22
+ }
23
+ export type Row = Record<string, any>;
24
+ export interface FindManyOptions {
25
+ where?: WhereClause[];
26
+ sortBy?: {
27
+ field: string;
28
+ direction: "asc" | "desc";
29
+ };
30
+ limit?: number;
31
+ }
32
+ /**
33
+ * A storage backend. Implement this interface to plug in any database
34
+ * (the "raw adapter"), or use one of the bundled adapters.
35
+ * All `where` clauses are combined with AND.
36
+ */
37
+ export interface DatabaseAdapter {
38
+ /** Create tables / collections for the given schema. Must be idempotent. */
39
+ migrate?(schema: Schema): Promise<void>;
40
+ create(model: string, data: Row): Promise<void>;
41
+ /** Insert or replace by primary key. */
42
+ upsert(model: string, data: Row): Promise<void>;
43
+ findOne(model: string, where: WhereClause[]): Promise<Row | null>;
44
+ findMany(model: string, options?: FindManyOptions): Promise<Row[]>;
45
+ update(model: string, where: WhereClause[], data: Row): Promise<number>;
46
+ delete(model: string, where: WhereClause[]): Promise<number>;
47
+ /** Maximum value of a numeric field, or null if there are no rows. */
48
+ max(model: string, field: string): Promise<number | null>;
49
+ /**
50
+ * Run `fn` atomically: either all of its writes are applied or none are.
51
+ * Optional; without it, writes are applied one by one.
52
+ */
53
+ transaction?<T>(fn: (tx: DatabaseAdapter) => Promise<T>): Promise<T>;
54
+ }
55
+ //#endregion
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "matrix-nodejs-homeserver",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "A framework- and database-agnostic Matrix homeserver library for Node.js",
5
+ "keywords": [
6
+ "chat",
7
+ "e2ee",
8
+ "federation",
9
+ "homeserver",
10
+ "matrix"
11
+ ],
12
+ "homepage": "https://github.com/k18k/matrix-nodejs-homeserver#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/k18k/matrix-nodejs-homeserver/issues"
15
+ },
16
+ "license": "PolyForm-Noncommercial-1.0.0",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/k18k/matrix-nodejs-homeserver.git"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "type": "module",
27
+ "main": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "imports": {
30
+ "#*": "./src/*"
31
+ },
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "default": "./dist/index.js"
36
+ }
37
+ },
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "registry": "https://registry.npmjs.org/"
41
+ },
42
+ "scripts": {
43
+ "typecheck": "tsc -p tsconfig.json",
44
+ "lint": "oxlint",
45
+ "format": "oxfmt .",
46
+ "format:check": "oxfmt --check .",
47
+ "build": "tsdown",
48
+ "prepare": "npm run build",
49
+ "start:example": "node examples/node-http.ts",
50
+ "start:multi-node-example": "node examples/multi-node.ts",
51
+ "changeset": "changeset",
52
+ "version-packages": "changeset version && oxfmt package.json",
53
+ "release": "changeset publish"
54
+ },
55
+ "dependencies": {
56
+ "zod": "^4.6.7"
57
+ },
58
+ "devDependencies": {
59
+ "@changesets/cli": "^3.0.3",
60
+ "@types/node": "^26.6.5",
61
+ "oxfmt": "^0.73.0",
62
+ "oxlint": "^1.88.0",
63
+ "tsdown": "^0.23.1",
64
+ "typescript": "7.0.2"
65
+ },
66
+ "engines": {
67
+ "node": ">=26"
68
+ }
69
+ }