matrix-nodejs-homeserver 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +479 -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 +594 -0
  12. package/dist/api/admin.js +267 -0
  13. package/dist/api/appservice.js +45 -0
  14. package/dist/api/backup.js +280 -0
  15. package/dist/api/federation.js +826 -0
  16. package/dist/api/filters.js +88 -0
  17. package/dist/api/index.js +47 -0
  18. package/dist/api/legacy.js +183 -0
  19. package/dist/api/media.js +271 -0
  20. package/dist/api/misc.js +95 -0
  21. package/dist/api/preview.js +201 -0
  22. package/dist/api/profile.js +176 -0
  23. package/dist/api/push.js +202 -0
  24. package/dist/api/relationships.js +270 -0
  25. package/dist/api/rooms.js +1487 -0
  26. package/dist/api/rtc.d.ts +23 -0
  27. package/dist/api/rtc.js +66 -0
  28. package/dist/api/search.js +161 -0
  29. package/dist/api/slidingsync.js +474 -0
  30. package/dist/api/spaces.js +177 -0
  31. package/dist/api/sync.js +609 -0
  32. package/dist/api/threadsubs.js +140 -0
  33. package/dist/api/threepid.js +155 -0
  34. package/dist/api/uia.js +127 -0
  35. package/dist/api/user.js +329 -0
  36. package/dist/core/admin.d.ts +84 -0
  37. package/dist/core/admin.js +308 -0
  38. package/dist/core/adminroom.d.ts +49 -0
  39. package/dist/core/adminroom.js +483 -0
  40. package/dist/core/appservices.d.ts +74 -0
  41. package/dist/core/appservices.js +369 -0
  42. package/dist/core/audit.d.ts +50 -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 +348 -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 +160 -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 +186 -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 +36 -0
  63. package/dist/core/json.js +16 -0
  64. package/dist/core/keys.d.ts +36 -0
  65. package/dist/core/keys.js +450 -0
  66. package/dist/core/m.d.ts +209 -0
  67. package/dist/core/m.js +182 -0
  68. package/dist/core/notifier.d.ts +17 -0
  69. package/dist/core/notifier.js +54 -0
  70. package/dist/core/oauth.d.ts +38 -0
  71. package/dist/core/oauth.js +108 -0
  72. package/dist/core/partialstate.d.ts +32 -0
  73. package/dist/core/partialstate.js +198 -0
  74. package/dist/core/phone.js +268 -0
  75. package/dist/core/policy.d.ts +29 -0
  76. package/dist/core/policy.js +97 -0
  77. package/dist/core/profile.js +39 -0
  78. package/dist/core/push.d.ts +46 -0
  79. package/dist/core/push.js +307 -0
  80. package/dist/core/pushrules.js +229 -0
  81. package/dist/core/ratelimit.d.ts +23 -0
  82. package/dist/core/ratelimit.js +77 -0
  83. package/dist/core/rooms.d.ts +215 -0
  84. package/dist/core/rooms.js +1731 -0
  85. package/dist/core/settings.d.ts +11 -0
  86. package/dist/core/settings.js +29 -0
  87. package/dist/core/sso.d.ts +46 -0
  88. package/dist/core/sso.js +165 -0
  89. package/dist/core/stateres.d.ts +4 -0
  90. package/dist/core/stateres.js +209 -0
  91. package/dist/core/threepid.d.ts +80 -0
  92. package/dist/core/threepid.js +275 -0
  93. package/dist/core/typing.d.ts +16 -0
  94. package/dist/core/typing.js +60 -0
  95. package/dist/federation/client.d.ts +51 -0
  96. package/dist/federation/client.js +279 -0
  97. package/dist/federation/index.d.ts +153 -0
  98. package/dist/federation/index.js +1059 -0
  99. package/dist/federation/keyring.d.ts +26 -0
  100. package/dist/federation/keyring.js +122 -0
  101. package/dist/federation/sender.d.ts +26 -0
  102. package/dist/federation/sender.js +164 -0
  103. package/dist/homeserver.d.ts +224 -0
  104. package/dist/homeserver.js +302 -0
  105. package/dist/http/adapters.d.ts +40 -0
  106. package/dist/http/adapters.js +130 -0
  107. package/dist/http/handler.d.ts +8 -0
  108. package/dist/http/handler.js +181 -0
  109. package/dist/http/input.js +24 -0
  110. package/dist/http/procedure.js +25 -0
  111. package/dist/http/router.d.ts +43 -0
  112. package/dist/http/router.js +56 -0
  113. package/dist/index.d.ts +26 -0
  114. package/dist/index.js +15 -0
  115. package/dist/storage/memory.d.ts +5 -0
  116. package/dist/storage/memory.js +122 -0
  117. package/dist/storage/schema.d.ts +4 -0
  118. package/dist/storage/schema.js +664 -0
  119. package/dist/storage/sql.d.ts +72 -0
  120. package/dist/storage/sql.js +274 -0
  121. package/dist/storage/types.d.ts +55 -0
  122. package/dist/vendor/zod/LICENSE +21 -0
  123. package/dist/vendor/zod/v4/classic/errors.js +54 -0
  124. package/dist/vendor/zod/v4/classic/parse.js +17 -0
  125. package/dist/vendor/zod/v4/classic/schemas.js +865 -0
  126. package/dist/vendor/zod/v4/core/api.js +512 -0
  127. package/dist/vendor/zod/v4/core/checks.js +300 -0
  128. package/dist/vendor/zod/v4/core/core.js +109 -0
  129. package/dist/vendor/zod/v4/core/doc.js +35 -0
  130. package/dist/vendor/zod/v4/core/errors.js +118 -0
  131. package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
  132. package/dist/vendor/zod/v4/core/memoizer.js +255 -0
  133. package/dist/vendor/zod/v4/core/parse.js +206 -0
  134. package/dist/vendor/zod/v4/core/regexes.js +73 -0
  135. package/dist/vendor/zod/v4/core/registries.js +48 -0
  136. package/dist/vendor/zod/v4/core/schemas.js +1378 -0
  137. package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
  138. package/dist/vendor/zod/v4/core/util.js +537 -0
  139. package/dist/vendor/zod/v4/core/versions.js +8 -0
  140. package/dist/vendor/zod/v4/locales/en.js +110 -0
  141. package/package.json +65 -4
package/LICENSE ADDED
@@ -0,0 +1,75 @@
1
+ Required Notice: Copyright Krystian Mikołajczyk (https://github.com/k18k)
2
+
3
+ # PolyForm Noncommercial License 1.0.0
4
+
5
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
6
+
7
+ ## Acceptance
8
+
9
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
14
+
15
+ ## Distribution License
16
+
17
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
18
+
19
+ ## Notices
20
+
21
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
22
+
23
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
24
+
25
+ ## Changes and New Works License
26
+
27
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
28
+
29
+ ## Patent License
30
+
31
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
32
+
33
+ ## Noncommercial Purposes
34
+
35
+ Any noncommercial purpose is a permitted purpose.
36
+
37
+ ## Personal Uses
38
+
39
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
40
+
41
+ ## Noncommercial Organizations
42
+
43
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
44
+
45
+ ## Fair Use
46
+
47
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
48
+
49
+ ## No Other Rights
50
+
51
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
52
+
53
+ ## Patent Defense
54
+
55
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
56
+
57
+ ## Violations
58
+
59
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
60
+
61
+ ## No Liability
62
+
63
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
64
+
65
+ ## Definitions
66
+
67
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
68
+
69
+ **You** refers to the individual or entity agreeing to these terms.
70
+
71
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
72
+
73
+ **Your licenses** are all the licenses granted to you for the software under these terms.
74
+
75
+ **Use** means anything you do with the software requiring one of your licenses.
package/README.md CHANGED
@@ -1,3 +1,480 @@
1
- # Temporary Holding Version
1
+ # matrix-nodejs-homeserver
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A [Matrix](https://matrix.org) homeserver as an importable Node.js library. It isn't
4
+ tied to any HTTP framework, database, auth system or deployment shape. You can use
5
+ it for a demo on SQLite, embed it in a monolith next to your existing users, or run
6
+ it as a horizontally scaled chat backend that replaces Slack or Teams.
7
+
8
+ - Node.js **26+**. TypeScript with only erasable syntax (`erasableSyntaxOnly`), and no runtime dependencies ([zod](https://zod.dev), used for request validation, is bundled).
9
+ - Client-Server API, Server-Server API (federation), end-to-end encryption, sliding sync, VoIP/MatrixRTC and push
10
+ - Runs on `node:http`, Express, Fastify, Hono or any fetch runtime
11
+ - Stores data in SQLite, PostgreSQL, MySQL, memory or your own adapter
12
+ - Can run as several instances behind a load balancer
13
+ - Tested against [Complement](https://github.com/matrix-org/complement) in CI
14
+
15
+ ## Installation
16
+
17
+ ```sh
18
+ npm install matrix-nodejs-homeserver
19
+ ```
20
+
21
+ ```ts
22
+ import { createServer } from "node:http";
23
+ import { DatabaseSync } from "node:sqlite";
24
+ import { createHomeserver, getNodeHandler, sqliteAdapter, filesystemMedia } from "matrix-nodejs-homeserver";
25
+
26
+ const homeserver = createHomeserver({
27
+ homeserver: "k18k.app",
28
+ databaseAdapter: sqliteAdapter(new DatabaseSync("homeserver.db")),
29
+ userAuthAdapter: { registrationEnabled: true },
30
+ mediaAdapter: filesystemMedia("./media"),
31
+ });
32
+
33
+ createServer(getNodeHandler(homeserver)).listen(8008);
34
+ ```
35
+
36
+ The adapters are optional. If you leave one out, or pass `{}`, the server uses
37
+ in-memory storage, built-in password auth (with registration **closed**) and
38
+ in-memory media.
39
+
40
+ ## Frameworks
41
+
42
+ | Framework | Usage |
43
+ |---|---|
44
+ | `node:http` / `node:https` | `createServer(getNodeHandler(hs))` |
45
+ | Express / Connect | `app.use(getExpressHandler(hs))` (works with or without `express.json()`) |
46
+ | Fastify | `fastify.register(getFastifyPlugin(hs))` |
47
+ | Hono | `app.use("*", getHonoHandler(hs))` |
48
+ | Any fetch runtime | `getFetchHandler(hs)` → `(Request) => Promise<Response>` |
49
+
50
+ The handler serves `/_matrix/*`, `/.well-known/matrix/*` and `/_synapse/*`. Every
51
+ other path goes to `next()`. Federation also needs port 8448 (HTTPS), or `.well-known`
52
+ delegation to the port you serve on.
53
+
54
+ ## Databases
55
+
56
+ ```ts
57
+ sqliteAdapter(new DatabaseSync("hs.db")); // node:sqlite or better-sqlite3
58
+ postgresAdapter(new pg.Pool({ connectionString })); // node-postgres (pass a Pool to get transactions)
59
+ mysqlAdapter(mysql.createPool(uri)); // mysql2/promise
60
+ memoryAdapter(); // tests / demos
61
+ ```
62
+
63
+ Tables are created on startup. Writes for each event are atomic: SQLite uses
64
+ `BEGIN IMMEDIATE`, Postgres and MySQL use a dedicated pooled connection, and the
65
+ memory adapter keeps an undo log.
66
+
67
+ To use another store, implement `DatabaseAdapter` (`create`, `upsert`, `findOne`,
68
+ `findMany`, `update`, `delete`, `max`, and optionally `migrate` and `transaction`).
69
+ The exported `schema` describes every model. If your driver can run parameterised
70
+ SQL, `sqlAdapter({ dialect, executor, transaction })` handles the SQL generation for you.
71
+
72
+ ## Scaling horizontally
73
+
74
+ By default a homeserver assumes it is the only process using its database. To run
75
+ several instances behind a load balancer, give them a shared **cluster adapter**:
76
+
77
+ ```ts
78
+ import { postgresCluster, redisCluster } from "matrix-nodejs-homeserver";
79
+
80
+ createHomeserver({
81
+ homeserver: "example.com",
82
+ databaseAdapter: postgresAdapter(pool),
83
+ cluster: postgresCluster(pool), // or: redisCluster({ client: new Redis(), subscriber: new Redis() })
84
+ });
85
+ ```
86
+
87
+ The cluster adapter provides:
88
+
89
+ - **Stream positions:** the global sequence is shared. Each instance tracks its
90
+ in-flight writes, so `/sync` never skips an event that committed late.
91
+ - **Locks:** per-room write locks (keeping the room DAG linear), push-rule
92
+ updates, per-destination federation sending, and startup migrations.
93
+ - **Pub/sub:** `/sync` long-poll wakeups, cache invalidation, typing notifications
94
+ and token revocation.
95
+ - **Shared ephemeral state:** UIA sessions, SSO logins and transaction deduplication.
96
+
97
+ [examples/multi-node.ts](examples/multi-node.ts) is meant to run unchanged on several
98
+ machines behind a load balancer, all sharing one Postgres database (and optionally Redis
99
+ for coordination). It also shows a media adapter backed by that database, since local
100
+ disks differ between machines.
101
+
102
+ The database adapter has to support concurrent access from several processes, so
103
+ use Postgres or MySQL. The SQLite and memory adapters are single-process. You can
104
+ implement `ClusterAdapter` yourself to use another backend (etcd, NATS, …).
105
+
106
+ ## Authentication
107
+
108
+ ```ts
109
+ createHomeserver({
110
+ homeserver: "example.com",
111
+ userAuthAdapter: {
112
+ registrationEnabled: true,
113
+ registrationRequiresToken: true, // m.login.registration_token
114
+ passwordLoginEnabled: true,
115
+ async checkPassword({ userId, password, storedHash }) { /* LDAP, your user table… */ return true; },
116
+ async autoProvision(userId) { return true; }, // create unknown users on login
117
+ async validatePassword(pw) { return pw.length < 8 ? "Too short" : null; },
118
+ },
119
+ });
120
+ ```
121
+
122
+ - **Your app's own sign-in:** after authenticating a user your own way, call
123
+ `await hs.auth.createLoginToken(userId)` and let the client exchange the token via
124
+ `m.login.token`. You can also call `hs.auth.createSession(userId)` to issue an
125
+ access token directly.
126
+ - **SSO with upstream OpenID Connect providers** (Google, GitHub via an OIDC
127
+ bridge, Keycloak, Authentik, …). Users can sign in with `m.login.sso`:
128
+ ```ts
129
+ sso: { providers: [{ id: "google", name: "Google", issuer: "https://accounts.google.com",
130
+ clientId, clientSecret, mapUser: (claims) => ({ localpart: claims.email.split("@")[0] }) }] },
131
+ publicBaseUrl: "https://matrix.example.com",
132
+ ```
133
+ - **Delegated OAuth 2.0** (the Matrix OAuth API, e.g. with
134
+ matrix-authentication-service, Keycloak or Authentik). The provider owns the
135
+ accounts, and the homeserver validates its access tokens by introspection:
136
+ ```ts
137
+ oauth: { issuer: "https://auth.example.com/", clientId, clientSecret, accountManagementUrl },
138
+ ```
139
+
140
+ ## Federation
141
+
142
+ Federation is **off by default**, so a server only joins the Matrix network on purpose.
143
+ Turn it on with `federation: { enabled: true }`. It then needs no configuration beyond a
144
+ reachable port 8448, or `.well-known/matrix/server` delegation (`federationAddress`). The
145
+ implementation includes:
146
+
147
+ - Server discovery (`.well-known` and SRV) and signed requests
148
+ - A persistent outbound queue with retries
149
+ - Room versions 1–12 with full authorization rules and state resolution v2/v2.1
150
+ - Joins, invites, leaves and knocks over federation
151
+ - Backfill and missing-event recovery
152
+ - Remote media, profiles, directories, key queries and claims
153
+ - EDUs (typing, receipts, presence, device lists, to-device, signing keys)
154
+ - Server ACLs
155
+
156
+ ```ts
157
+ federation: { enabled: true, allowedServers: ["partner.org"], blockedServers: ["spam.example"] },
158
+ ```
159
+
160
+ Without federation, local users can only talk to each other: federation endpoints answer
161
+ `M_UNRECOGNIZED`, and remote users, rooms and media cannot be reached.
162
+
163
+ ## VoIP and MatrixRTC
164
+
165
+ ```ts
166
+ rtc: {
167
+ turn: { uris: ["turn:turn.example.com:3478?transport=udp"], sharedSecret: process.env.TURN_SECRET },
168
+ transports: [{ type: "livekit", livekit_service_url: "https://livekit-jwt.example.com" }],
169
+ },
170
+ ```
171
+
172
+ TURN credentials are time-limited (coturn `static-auth-secret`). MatrixRTC
173
+ transports are advertised for Element Call. Delayed events (MSC4140) remove a
174
+ call membership when a client disappears.
175
+
176
+ ## Push notifications
177
+
178
+ Push rules are evaluated for every event (all spec condition kinds). The results
179
+ drive unread and highlight counts (including per-thread counts) and
180
+ `/notifications`, and are delivered to HTTP pushers (Sygnal or any push gateway).
181
+
182
+ ## Media
183
+
184
+ `memoryMedia()`, `filesystemMedia(dir)`, `s3Media({...})`, or your own
185
+ `{ put, get, delete?, maxUploadSize? }`.
186
+
187
+ `s3Media` works with AWS S3 and S3-compatible stores (MinIO, Cloudflare R2, Garage, Ceph). It signs
188
+ requests itself (AWS Signature Version 4), so no AWS SDK is needed:
189
+
190
+ ```ts
191
+ import { s3Media } from "matrix-nodejs-homeserver";
192
+ createHomeserver({
193
+ ...,
194
+ mediaAdapter: s3Media({
195
+ bucket: "matrix-media",
196
+ region: "eu-central-1", // "auto" for R2
197
+ accessKeyId: process.env.S3_ACCESS_KEY_ID!,
198
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
199
+ // endpoint: "https://<account>.r2.cloudflarestorage.com", // for non-AWS stores (path-style by default)
200
+ // prefix: "media/",
201
+ }),
202
+ });
203
+ ```
204
+
205
+ Thumbnails come from a pluggable thumbnailer:
206
+
207
+ ```ts
208
+ import sharp from "sharp";
209
+ createHomeserver({ ..., thumbnailer: sharpThumbnailer(sharp) });
210
+ ```
211
+
212
+ URL previews are opt-in (`urlPreviews: true`), and private network addresses are
213
+ blocked by default.
214
+
215
+ ## Email addresses and phone numbers
216
+
217
+ Accounts can have validated email addresses and phone numbers (3PIDs): for registration,
218
+ password reset, signing in, and for being found by invites. The server only needs a way to
219
+ deliver messages:
220
+
221
+ ```ts
222
+ createHomeserver({
223
+ ...,
224
+ publicBaseUrl: "https://matrix.example.com", // validation links point here
225
+ threepid: {
226
+ sendEmail: async ({ to, subject, text, html }) => { await mailer.send({ to, subject, text, html }); },
227
+ sendSms: async ({ to, text }) => { await sms.send(`+${to}`, text); }, // optional
228
+ requireEmailForRegistration: false,
229
+ },
230
+ });
231
+ ```
232
+
233
+ Invites by email address go through the identity server the client names (`id_server`), and
234
+ work without `threepid`.
235
+
236
+ ## Account moderation
237
+
238
+ Admins can lock an account (it cannot be used until unlocked, but its sessions survive) or
239
+ suspend it (read-only: the user can still read, leave rooms, redact their own messages and
240
+ log out). Use the admin room (`users lock alice`), `PUT /_matrix/client/v1/admin/lock/{userId}`
241
+ and `.../admin/suspend/{userId}`, or `hs.admin.setLocked()` / `hs.admin.setSuspended()`.
242
+
243
+ ## Admin room and first-user bootstrap
244
+
245
+ ```ts
246
+ createHomeserver({
247
+ ...,
248
+ userAuthAdapter: { registrationEnabled: false },
249
+ adminRoom: { firstUserIsAdmin: true }, // or simply `adminRoom: true`
250
+ });
251
+ ```
252
+
253
+ This works like tuwunel or conduwuit. The **first account** registered on the server
254
+ becomes a server admin, even while registration is closed. It is placed in the admin
255
+ room (`#admins:<server>`) with an admin bot. From then on, admins manage the server
256
+ by chatting with the bot:
257
+
258
+ ```
259
+ users list users create alice [password] [--admin]
260
+ users reset-password alice users deactivate alice users make-admin alice
261
+ users lock alice | unlock users suspend alice | unsuspend (reversible moderation)
262
+ tokens create 5 7 (5 uses, valid for 7 days)
263
+ registration open|close|token (runtime setting; overrides the configuration)
264
+ rooms list | rooms info <room> | rooms delete <room> --purge
265
+ media purge 90 notice alice "Maintenance tonight"
266
+ audit [user] reports list
267
+ server stats help
268
+ ```
269
+
270
+ Making someone an admin (through the bot, `hs.admin.setAdmin()` or the admin API)
271
+ invites them to the admin room, and revoking admin removes them. Options:
272
+ `firstUserIsAdmin`, `firstUserCanRegister`, `botLocalpart`, `botDisplayName`,
273
+ `roomName` and `aliasLocalpart`.
274
+
275
+ ## Audit log
276
+
277
+ ```ts
278
+ audit: {
279
+ enabled: true,
280
+ database: true, // queryable via the admin API / `audit` command
281
+ sink: (entry) => siem.send(entry), // optional extra sink
282
+ actions: ["login", "login_failed", "admin_action"], // optional filter
283
+ retentionDays: 365,
284
+ },
285
+ ```
286
+
287
+ The audit log records successful and failed logins, logouts, registrations,
288
+ password changes, deactivations, devices, room creation and deletion,
289
+ kicks/bans/unbans, every state-changing admin API call and admin-room command,
290
+ registration tokens and settings, SSO logins, and content reports. Each entry has
291
+ the actor, target, IP, user agent and outcome. You can read it with
292
+ `GET /_synapse/admin/v1/audit_log?actor=&action=&since=` or the `audit` command.
293
+
294
+ Users can report content (`POST /rooms/{roomId}/report[/{eventId}]`). Reports show
295
+ up for admins (`/_synapse/admin/v1/event_reports`, `reports list`), and the admin
296
+ room is notified. `GET /_matrix/client/v3/admin/whois/{userId}` shows a user's
297
+ devices and IP addresses.
298
+
299
+ ## Administration
300
+
301
+ You can use `hs.admin` from your own code:
302
+
303
+ ```ts
304
+ await hs.admin.createUser("alice", { password: "…", admin: true });
305
+ await hs.admin.createRegistrationToken({ usesAllowed: 10 });
306
+ await hs.admin.deactivateUser("@spammer:example.com");
307
+ await hs.admin.deleteRoom(roomId, { purge: true });
308
+ await hs.admin.sendServerNotice("@alice:example.com", { msgtype: M.TEXT, body: "Maintenance tonight" });
309
+ ```
310
+
311
+ `M` (exported from the package) holds the Matrix identifiers as one readonly object:
312
+ `M.ROOM.MEMBER` is `"m.room.member"`, `M.LOGIN.PASSWORD` is `"m.login.password"`.
313
+ `UNSTABLE` does the same for unstable and vendor identifiers:
314
+ `UNSTABLE.ORG.MATRIX.CROSS_SIGNING_RESET` is `"org.matrix.cross_signing_reset"`.
315
+
316
+ The same operations are available over HTTP at Synapse-compatible `/_synapse/admin/…`
317
+ paths (server admins only), so existing tools keep working.
318
+
319
+ ## Options
320
+
321
+ | Option | Description |
322
+ |---|---|
323
+ | `homeserver` | Server name. Required. |
324
+ | `databaseAdapter`, `mediaAdapter`, `userAuthAdapter` | See above. |
325
+ | `cluster` | Cluster adapter for running several instances. |
326
+ | `adminRoom` | `true` or `{ firstUserIsAdmin, firstUserCanRegister, botLocalpart, … }`. Disabled by default. |
327
+ | `audit` | `{ enabled, database, sink, actions, retentionDays }`. Disabled by default. |
328
+ | `publicBaseUrl` | Advertised via `/.well-known/matrix/client`. Required for SSO. |
329
+ | `oauth`, `sso` | Delegated OAuth or upstream OIDC SSO. |
330
+ | `federation` | `{ enabled, verifyTls, allowedServers, blockedServers, fasterJoins }`. Off unless `enabled: true`. |
331
+ | `rtc` | `{ turn, transports }` |
332
+ | `delayedEvents` | `{ maxDelayMs, maxPerUser }` |
333
+ | `rateLimits` | `false`, or `{ limiter, rules }`. In-memory token buckets by default. Plug in a shared limiter when running a cluster. |
334
+ | `thumbnailer` | For example `sharpThumbnailer(sharp)`. |
335
+ | `urlPreviews` | `true` or `{ allowPrivateAddresses }`. |
336
+ | `registrationSharedSecret` | Turns on Synapse-compatible shared-secret registration. |
337
+ | `serverNotices` | `{ localpart, displayName, roomName }` |
338
+ | `signingKey` | `{ keyId, seed }`. By default a key is generated and stored in the database. |
339
+ | `defaultRoomVersion` | Defaults to `"10"`. Versions 1–12 are supported. |
340
+ | `accessTokenLifetimeMs` | Lifetime of access tokens issued together with refresh tokens. |
341
+ | `maxSyncTimeoutMs`, `cleanupIntervalMs` | Long-poll cap and the background cleanup interval. |
342
+ | `threepid` | Email/phone support: `{ sendEmail, sendSms, requireEmailForRegistration, sessionLifetimeMs }`. See below. |
343
+ | `support` | `{ contacts, supportPage }` served at `/.well-known/matrix/support`. |
344
+ | `federationAddress` | `"host:port"` served at `/.well-known/matrix/server` (delegation). |
345
+ | `loginViaExistingSession` | `POST /login/get_token` for signing in new devices. On by default (needs re-authentication). |
346
+ | `fetch`, `logger` | Outgoing HTTP client and logger. |
347
+
348
+ Call `await hs.close()` on shutdown. It ends long-polls, stops background jobs and
349
+ leaves the cluster cleanly. `hs.on("event", …)` and `hs.on("request", …)` are
350
+ available for integration and metrics.
351
+
352
+ ## Spec coverage
353
+
354
+ - **Accounts:** registration (open, closed or token-gated), password, SSO, OAuth, login tokens, refresh tokens, devices, deactivation
355
+ - **Rooms:** versions 1–12, presets, aliases, the directory, spaces (`/hierarchy`, `/room_summary`), upgrades, restricted and knock rooms
356
+ - **Events:** `/sync` (with filters, lazy loading and `state_after`), simplified sliding sync, `/messages` with backfill, `/context`, relations, threads, search, redactions, receipts, typing, presence, account data, tags
357
+ - **E2EE:** device keys, one-time and fallback keys, cross-signing, signatures, key backup, to-device (also over federation)
358
+ - **Push:** rule evaluation, notification counts, pushers, `/notifications`
359
+ - **Media:** legacy and authenticated endpoints, async uploads, thumbnails, the remote media proxy, URL previews
360
+ - **Federation, VoIP/MatrixRTC, admin, server notices**
361
+
362
+ Not implemented: third-party identifiers (email/phone), application services,
363
+ and a built-in OAuth authorization server. For OAuth, delegate to an existing provider.
364
+
365
+ ## Development
366
+
367
+ ```sh
368
+ npm install
369
+ npm run typecheck # tsc only type-checks (noEmit)
370
+ npm run build # tsdown → dist/ (what the published package contains)
371
+ node examples/node-http.ts # development runs the TypeScript sources directly (type stripping)
372
+ ```
373
+
374
+ Endpoints are procedures (`src/http/procedure.ts`): a route declares what it needs and a
375
+ handler, and the procedure checks the request in a fixed order (authentication, rate limit,
376
+ body, then the response) before and after the handler runs:
377
+
378
+ ```ts
379
+ r.client("POST", "/rooms/:roomId/invite", procedure({
380
+ auth: "user", // ctx.session (or "optional", "none")
381
+ rateLimit: "invite",
382
+ input: body({ user_id: lenient(z.string()), reason: lenient(z.string()) }), // ctx.input
383
+ handler: async (ctx) => { /* ctx.hs, ctx.session, ctx.input, ctx.params */ },
384
+ }));
385
+ ```
386
+
387
+ Inside `src`, modules import each other through the package's
388
+ [subpath imports](https://nodejs.org/api/packages.html#subpath-imports): `#core/errors.ts` is
389
+ `src/core/errors.ts`. The build turns them back into relative imports.
390
+
391
+ Node does not strip types from files inside `node_modules`, so the package ships `dist/`,
392
+ built by tsdown: one `.js` and one `.d.ts` per source module, with zod bundled under
393
+ `dist/vendor/`. `npm pack`, `npm publish` and
394
+ installs from GitHub build it through the `prepare` script. Recent npm versions ask you to
395
+ approve install scripts of git dependencies (`npm install-scripts approve`).
396
+
397
+ ### Releasing
398
+
399
+ Changes that users of the package should know about come with a changeset:
400
+
401
+ ```sh
402
+ npx changeset # pick the bump (patch, minor, major) and describe the change
403
+ ```
404
+
405
+ Commit the generated file in `.changeset/` with the change. On `main`, the Release workflow
406
+ keeps a "Release" pull request open that bumps the version and writes `CHANGELOG.md`.
407
+ Merging it publishes the package to npm (trusted publishing, with provenance), tags the
408
+ release and creates the GitHub release.
409
+
410
+ ### Complement
411
+
412
+ ```sh
413
+ scripts/complement.sh # whole suite (only Docker is needed)
414
+ scripts/complement.sh ./tests/ -run 'TestLogin|TestSync' # selected tests
415
+ ```
416
+
417
+ Complement runs in a Go container at the commit pinned in the script, and starts this
418
+ homeserver's image (rebuilt from the checkout on each run) through the Docker socket.
419
+
420
+ Live status, updated after every Complement run (full or targeted) with
421
+ `node complement/status.ts [--reset] --note "what changed" results.json…`
422
+ (`go test -json` output; the ledger lives in `complement/results.json`):
423
+
424
+ <!-- complement-status:start -->
425
+ <!-- Generated by complement/status.ts; do not edit by hand. -->
426
+
427
+ **214 / 214** top-level tests passing (100.0%) · **851 / 851** including subtests (100.0%)
428
+
429
+ `████████████████████`
430
+
431
+ | Area | Passing | |
432
+ |---|---|---|
433
+ | csapi | 105 / 105 | `██████████` |
434
+ | federation | 90 / 90 | `██████████` |
435
+ | msc2836 | 2 / 2 | `██████████` |
436
+ | msc3389 | 3 / 3 | `██████████` |
437
+ | msc3391 | 1 / 1 | `██████████` |
438
+ | msc3757 | 2 / 2 | `██████████` |
439
+ | msc3874 | 1 / 1 | `██████████` |
440
+ | msc3890 | 1 / 1 | `██████████` |
441
+ | msc3902 | 1 / 1 | `██████████` |
442
+ | msc3930 | 1 / 1 | `██████████` |
443
+ | msc3967 | 1 / 1 | `██████████` |
444
+ | msc4140 | 1 / 1 | `██████████` |
445
+ | msc4155 | 1 / 1 | `██████████` |
446
+ | msc4222 | 1 / 1 | `██████████` |
447
+ | msc4306 | 2 / 2 | `██████████` |
448
+ | msc4429 | 1 / 1 | `██████████` |
449
+
450
+ <details><summary>Progress history</summary>
451
+
452
+ | Run | Top-level | Subtests | Note |
453
+ |---|---|---|---|
454
+ | 8 | 214 / 214 | 851 / 851 | Complement runs in a Go container (no submodule, no local Go); account locking/suspension, policy servers, 3PIDs, legacy sync endpoints |
455
+ | 7 | 214 / 214 | 851 / 851 | All Complement tests pass (previous full run: 211/214). Fallback keys stored apart from one-time keys; user directory public-room rules; MSC4311 create event required from v12 |
456
+ | 6 | 189 / 214 | 803 / 851 | Partial-state joins (MSC3902): all subtests pass |
457
+ | 5 | 188 / 214 | 785 / 851 | Full run #8 + partial-state joins (MSC3902) first pass |
458
+ | 4 | 207 / 214 | 785 / 851 | Extended profiles + MSC4429, MSC3967, MSC2836, MSC3389 recheck |
459
+ | 3 | 202 / 214 | 774 / 851 | MSC4306 thread subscriptions, MSC4102 receipts |
460
+ | 2 | 198 / 214 | 761 / 851 | Restricted rooms, knocking, spaces, MSC batch |
461
+ | 1 | 180 / 214 | 692 / 834 | Full run #7 |
462
+
463
+ </details>
464
+
465
+ <details><summary>Still failing (0)</summary>
466
+
467
+
468
+ </details>
469
+ <!-- complement-status:end -->
470
+
471
+ CI runs the whole suite with the same script. `complement/passing.txt` lists the tests
472
+ (and subtests) that are known to pass, and CI fails if any of them regress.
473
+
474
+ ## License
475
+
476
+ [PolyForm Noncommercial 1.0.0](LICENSE). Free to use, modify and share for any noncommercial
477
+ purpose: personal use, study, hobby projects, and use by charities, schools, research,
478
+ public-safety and government organisations. Commercial use needs a separate agreement with
479
+ the author (contact via [github.com/k18k](https://github.com/k18k)). This is a
480
+ source-available license, not an OSI open-source one.
@@ -0,0 +1,50 @@
1
+ //#region src/adapters/auth.d.ts
2
+ export interface PasswordCheckContext {
3
+ userId: string;
4
+ localpart: string;
5
+ password: string;
6
+ /** The password hash stored by the homeserver, if any. */
7
+ storedHash: string | null;
8
+ }
9
+ /**
10
+ * Controls how users authenticate. Every member is optional; omitted members
11
+ * fall back to built-in behaviour (scrypt password hashes stored in the database).
12
+ */
13
+ export interface UserAuthAdapter {
14
+ /** Whether new accounts may be created via `/register`. Default: false. */
15
+ registrationEnabled?: boolean;
16
+ /** Require a registration token (`m.login.registration_token`) to register. Default: false. */
17
+ registrationRequiresToken?: boolean;
18
+ /** Whether `m.login.password` is offered. Default: true. */
19
+ passwordLoginEnabled?: boolean;
20
+ /** Whether guest accounts may be created. Default: false. */
21
+ guestAccessEnabled?: boolean;
22
+ /** Verify credentials for `m.login.password`. Return `true`/`false`. */
23
+ checkPassword?(ctx: PasswordCheckContext): Promise<boolean>;
24
+ /** Hash a password for storage. */
25
+ hashPassword?(password: string): Promise<string>;
26
+ /**
27
+ * Called when an unknown user logs in successfully through `checkPassword`
28
+ * (e.g. LDAP). Return true to auto-provision the account.
29
+ */
30
+ autoProvision?(userId: string): Promise<boolean>;
31
+ /** Hook fired after a new account has been created. */
32
+ onUserRegistered?(userId: string): Promise<void>;
33
+ /** Optional validation of usernames beyond the Matrix grammar. Throw or return false to reject. */
34
+ validateUsername?(localpart: string): Promise<boolean>;
35
+ /** Optional password policy. Return an error message to reject. */
36
+ validatePassword?(password: string): Promise<string | null>;
37
+ }
38
+ export interface ResolvedUserAuth {
39
+ registrationEnabled: boolean;
40
+ registrationRequiresToken: boolean;
41
+ passwordLoginEnabled: boolean;
42
+ guestAccessEnabled: boolean;
43
+ checkPassword(ctx: PasswordCheckContext): Promise<boolean>;
44
+ hashPassword(password: string): Promise<string>;
45
+ autoProvision(userId: string): Promise<boolean>;
46
+ onUserRegistered(userId: string): Promise<void>;
47
+ validateUsername(localpart: string): Promise<boolean>;
48
+ validatePassword(password: string): Promise<string | null>;
49
+ }
50
+ //#endregion
@@ -0,0 +1,18 @@
1
+ import { hashPassword, verifyPassword } from "../core/crypto.js";
2
+ //#region src/adapters/auth.ts
3
+ function resolveUserAuth(adapter = {}) {
4
+ return {
5
+ registrationEnabled: adapter.registrationEnabled ?? false,
6
+ registrationRequiresToken: adapter.registrationRequiresToken ?? false,
7
+ passwordLoginEnabled: adapter.passwordLoginEnabled ?? true,
8
+ guestAccessEnabled: adapter.guestAccessEnabled ?? false,
9
+ checkPassword: adapter.checkPassword?.bind(adapter) ?? (async ({ password, storedHash }) => storedHash ? verifyPassword(password, storedHash) : false),
10
+ hashPassword: adapter.hashPassword?.bind(adapter) ?? hashPassword,
11
+ autoProvision: adapter.autoProvision?.bind(adapter) ?? (async () => false),
12
+ onUserRegistered: adapter.onUserRegistered?.bind(adapter) ?? (async () => {}),
13
+ validateUsername: adapter.validateUsername?.bind(adapter) ?? (async () => true),
14
+ validatePassword: adapter.validatePassword?.bind(adapter) ?? (async () => null)
15
+ };
16
+ }
17
+ //#endregion
18
+ export { resolveUserAuth };