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.
- package/LICENSE +75 -0
- package/README.md +479 -2
- package/dist/adapters/auth.d.ts +50 -0
- package/dist/adapters/auth.js +18 -0
- package/dist/adapters/cluster.d.ts +55 -0
- package/dist/adapters/cluster.js +249 -0
- package/dist/adapters/media.d.ts +50 -0
- package/dist/adapters/media.js +63 -0
- package/dist/adapters/s3.d.ts +33 -0
- package/dist/adapters/s3.js +95 -0
- package/dist/api/account.js +594 -0
- package/dist/api/admin.js +267 -0
- package/dist/api/appservice.js +45 -0
- package/dist/api/backup.js +280 -0
- package/dist/api/federation.js +826 -0
- package/dist/api/filters.js +88 -0
- package/dist/api/index.js +47 -0
- package/dist/api/legacy.js +183 -0
- package/dist/api/media.js +271 -0
- package/dist/api/misc.js +95 -0
- package/dist/api/preview.js +201 -0
- package/dist/api/profile.js +176 -0
- package/dist/api/push.js +202 -0
- package/dist/api/relationships.js +270 -0
- package/dist/api/rooms.js +1487 -0
- package/dist/api/rtc.d.ts +23 -0
- package/dist/api/rtc.js +66 -0
- package/dist/api/search.js +161 -0
- package/dist/api/slidingsync.js +474 -0
- package/dist/api/spaces.js +177 -0
- package/dist/api/sync.js +609 -0
- package/dist/api/threadsubs.js +140 -0
- package/dist/api/threepid.js +155 -0
- package/dist/api/uia.js +127 -0
- package/dist/api/user.js +329 -0
- package/dist/core/admin.d.ts +84 -0
- package/dist/core/admin.js +308 -0
- package/dist/core/adminroom.d.ts +49 -0
- package/dist/core/adminroom.js +483 -0
- package/dist/core/appservices.d.ts +74 -0
- package/dist/core/appservices.js +369 -0
- package/dist/core/audit.d.ts +50 -0
- package/dist/core/audit.js +86 -0
- package/dist/core/auth.d.ts +48 -0
- package/dist/core/auth.js +237 -0
- package/dist/core/authrules.d.ts +4 -0
- package/dist/core/authrules.js +348 -0
- package/dist/core/cluster.d.ts +66 -0
- package/dist/core/cluster.js +191 -0
- package/dist/core/crypto.d.ts +10 -0
- package/dist/core/crypto.js +100 -0
- package/dist/core/delayed.d.ts +39 -0
- package/dist/core/delayed.js +160 -0
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +45 -0
- package/dist/core/events.d.ts +32 -0
- package/dist/core/events.js +186 -0
- package/dist/core/glob.js +66 -0
- package/dist/core/identity.d.ts +24 -0
- package/dist/core/identity.js +96 -0
- package/dist/core/ids.js +36 -0
- package/dist/core/invitefilter.js +36 -0
- package/dist/core/json.js +16 -0
- package/dist/core/keys.d.ts +36 -0
- package/dist/core/keys.js +450 -0
- package/dist/core/m.d.ts +209 -0
- package/dist/core/m.js +182 -0
- package/dist/core/notifier.d.ts +17 -0
- package/dist/core/notifier.js +54 -0
- package/dist/core/oauth.d.ts +38 -0
- package/dist/core/oauth.js +108 -0
- package/dist/core/partialstate.d.ts +32 -0
- package/dist/core/partialstate.js +198 -0
- package/dist/core/phone.js +268 -0
- package/dist/core/policy.d.ts +29 -0
- package/dist/core/policy.js +97 -0
- package/dist/core/profile.js +39 -0
- package/dist/core/push.d.ts +46 -0
- package/dist/core/push.js +307 -0
- package/dist/core/pushrules.js +229 -0
- package/dist/core/ratelimit.d.ts +23 -0
- package/dist/core/ratelimit.js +77 -0
- package/dist/core/rooms.d.ts +215 -0
- package/dist/core/rooms.js +1731 -0
- package/dist/core/settings.d.ts +11 -0
- package/dist/core/settings.js +29 -0
- package/dist/core/sso.d.ts +46 -0
- package/dist/core/sso.js +165 -0
- package/dist/core/stateres.d.ts +4 -0
- package/dist/core/stateres.js +209 -0
- package/dist/core/threepid.d.ts +80 -0
- package/dist/core/threepid.js +275 -0
- package/dist/core/typing.d.ts +16 -0
- package/dist/core/typing.js +60 -0
- package/dist/federation/client.d.ts +51 -0
- package/dist/federation/client.js +279 -0
- package/dist/federation/index.d.ts +153 -0
- package/dist/federation/index.js +1059 -0
- package/dist/federation/keyring.d.ts +26 -0
- package/dist/federation/keyring.js +122 -0
- package/dist/federation/sender.d.ts +26 -0
- package/dist/federation/sender.js +164 -0
- package/dist/homeserver.d.ts +224 -0
- package/dist/homeserver.js +302 -0
- package/dist/http/adapters.d.ts +40 -0
- package/dist/http/adapters.js +130 -0
- package/dist/http/handler.d.ts +8 -0
- package/dist/http/handler.js +181 -0
- package/dist/http/input.js +24 -0
- package/dist/http/procedure.js +25 -0
- package/dist/http/router.d.ts +43 -0
- package/dist/http/router.js +56 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +15 -0
- package/dist/storage/memory.d.ts +5 -0
- package/dist/storage/memory.js +122 -0
- package/dist/storage/schema.d.ts +4 -0
- package/dist/storage/schema.js +664 -0
- package/dist/storage/sql.d.ts +72 -0
- package/dist/storage/sql.js +274 -0
- package/dist/storage/types.d.ts +55 -0
- package/dist/vendor/zod/LICENSE +21 -0
- package/dist/vendor/zod/v4/classic/errors.js +54 -0
- package/dist/vendor/zod/v4/classic/parse.js +17 -0
- package/dist/vendor/zod/v4/classic/schemas.js +865 -0
- package/dist/vendor/zod/v4/core/api.js +512 -0
- package/dist/vendor/zod/v4/core/checks.js +300 -0
- package/dist/vendor/zod/v4/core/core.js +109 -0
- package/dist/vendor/zod/v4/core/doc.js +35 -0
- package/dist/vendor/zod/v4/core/errors.js +118 -0
- package/dist/vendor/zod/v4/core/json-schema-processors.js +428 -0
- package/dist/vendor/zod/v4/core/memoizer.js +255 -0
- package/dist/vendor/zod/v4/core/parse.js +206 -0
- package/dist/vendor/zod/v4/core/regexes.js +73 -0
- package/dist/vendor/zod/v4/core/registries.js +48 -0
- package/dist/vendor/zod/v4/core/schemas.js +1378 -0
- package/dist/vendor/zod/v4/core/to-json-schema.js +448 -0
- package/dist/vendor/zod/v4/core/util.js +537 -0
- package/dist/vendor/zod/v4/core/versions.js +8 -0
- package/dist/vendor/zod/v4/locales/en.js +110 -0
- 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
|
-
#
|
|
1
|
+
# matrix-nodejs-homeserver
|
|
2
2
|
|
|
3
|
-
|
|
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 };
|