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.
- package/LICENSE +75 -0
- package/README.md +457 -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 +560 -0
- package/dist/api/admin.js +251 -0
- package/dist/api/appservice.js +33 -0
- package/dist/api/backup.js +269 -0
- package/dist/api/federation.js +792 -0
- package/dist/api/filters.js +87 -0
- package/dist/api/index.js +47 -0
- package/dist/api/legacy.js +170 -0
- package/dist/api/media.js +255 -0
- package/dist/api/misc.js +86 -0
- package/dist/api/preview.js +200 -0
- package/dist/api/profile.js +165 -0
- package/dist/api/push.js +187 -0
- package/dist/api/relationships.js +264 -0
- package/dist/api/rooms.js +1364 -0
- package/dist/api/rtc.d.ts +24 -0
- package/dist/api/rtc.js +52 -0
- package/dist/api/search.js +155 -0
- package/dist/api/slidingsync.js +464 -0
- package/dist/api/spaces.js +172 -0
- package/dist/api/sync.js +600 -0
- package/dist/api/threadsubs.js +129 -0
- package/dist/api/threepid.js +122 -0
- package/dist/api/uia.js +126 -0
- package/dist/api/user.js +286 -0
- package/dist/core/admin.d.ts +85 -0
- package/dist/core/admin.js +307 -0
- package/dist/core/adminroom.d.ts +49 -0
- package/dist/core/adminroom.js +482 -0
- package/dist/core/appservices.d.ts +73 -0
- package/dist/core/appservices.js +368 -0
- package/dist/core/audit.d.ts +51 -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 +347 -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 +159 -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 +185 -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 +35 -0
- package/dist/core/json.js +16 -0
- package/dist/core/keys.d.ts +36 -0
- package/dist/core/keys.js +449 -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 +197 -0
- package/dist/core/phone.js +268 -0
- package/dist/core/policy.d.ts +29 -0
- package/dist/core/policy.js +96 -0
- package/dist/core/profile.js +39 -0
- package/dist/core/push.d.ts +47 -0
- package/dist/core/push.js +306 -0
- package/dist/core/pushrules.d.ts +1 -0
- package/dist/core/pushrules.js +228 -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 +1729 -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 +208 -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 +278 -0
- package/dist/federation/index.d.ts +153 -0
- package/dist/federation/index.js +1058 -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 +223 -0
- package/dist/homeserver.js +301 -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 +202 -0
- package/dist/http/input.js +24 -0
- package/dist/http/router.d.ts +60 -0
- package/dist/http/router.js +76 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +14 -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/package.json +67 -4
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { isObject } from "../core/json.js";
|
|
2
|
+
import { MatrixError, errors } from "../core/errors.js";
|
|
3
|
+
//#region src/api/threadsubs.ts
|
|
4
|
+
const VERSIONS = ["unstable/io.element.msc4306"];
|
|
5
|
+
const notInThread = () => new MatrixError(400, "IO.ELEMENT.MSC4306.M_NOT_IN_THREAD", "Event is not in this thread");
|
|
6
|
+
const conflicting = () => new MatrixError(409, "IO.ELEMENT.MSC4306.M_CONFLICTING_UNSUBSCRIPTION", "The thread was unsubscribed after this event");
|
|
7
|
+
function key(userId, roomId, threadId) {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
field: "user_id",
|
|
11
|
+
value: userId
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
field: "room_id",
|
|
15
|
+
value: roomId
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
field: "thread_id",
|
|
19
|
+
value: threadId
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
/** Whether the user is subscribed to a thread: true, false (explicitly unsubscribed or never subscribed). */
|
|
24
|
+
async function isSubscribed(hs, userId, roomId, threadId) {
|
|
25
|
+
return !!(await hs.db.findOne("thread_subscriptions", key(userId, roomId, threadId)))?.subscribed;
|
|
26
|
+
}
|
|
27
|
+
async function visibleEvent(ctx, userId, roomId, eventId) {
|
|
28
|
+
const ev = await ctx.hs.rooms.getEvent(eventId);
|
|
29
|
+
if (!ev || ev.pdu.room_id !== roomId) return null;
|
|
30
|
+
const [visible] = await ctx.hs.rooms.filterVisible(userId, roomId, [ev]);
|
|
31
|
+
return visible ?? null;
|
|
32
|
+
}
|
|
33
|
+
async function threadRoot(ctx, userId) {
|
|
34
|
+
const { roomId, threadId } = ctx.params;
|
|
35
|
+
const root = await visibleEvent(ctx, userId, roomId, threadId);
|
|
36
|
+
if (!root) throw errors.notFound("Unknown thread root");
|
|
37
|
+
return root;
|
|
38
|
+
}
|
|
39
|
+
function registerThreadSubscriptionRoutes(r) {
|
|
40
|
+
const path = "/rooms/:roomId/thread/:threadId/subscription";
|
|
41
|
+
r.client("GET", path, async (ctx) => {
|
|
42
|
+
const s = await ctx.auth();
|
|
43
|
+
await threadRoot(ctx, s.userId);
|
|
44
|
+
const row = await ctx.hs.db.findOne("thread_subscriptions", key(s.userId, ctx.params.roomId, ctx.params.threadId));
|
|
45
|
+
if (!row?.subscribed) throw errors.notFound("Not subscribed to this thread");
|
|
46
|
+
return { automatic: !!row.automatic };
|
|
47
|
+
}, VERSIONS);
|
|
48
|
+
r.client("PUT", path, async (ctx) => {
|
|
49
|
+
const s = await ctx.auth();
|
|
50
|
+
const hs = ctx.hs;
|
|
51
|
+
const { roomId, threadId } = ctx.params;
|
|
52
|
+
await threadRoot(ctx, s.userId);
|
|
53
|
+
const body = await ctx.json();
|
|
54
|
+
const cause = isObject(body) ? body.automatic : void 0;
|
|
55
|
+
if (cause !== void 0 && typeof cause !== "string") throw errors.badJson("automatic must be an event ID");
|
|
56
|
+
let causeStream = null;
|
|
57
|
+
if (typeof cause === "string") {
|
|
58
|
+
const ev = await visibleEvent(ctx, s.userId, roomId, cause);
|
|
59
|
+
const rel = ev?.pdu.content?.["m.relates_to"];
|
|
60
|
+
if (!ev || !isObject(rel) || rel.rel_type !== "m.thread" || rel.event_id !== threadId) throw notInThread();
|
|
61
|
+
causeStream = ev.stream;
|
|
62
|
+
}
|
|
63
|
+
await hs.cluster.lock(`threadsub:${s.userId}:${roomId}:${threadId}`, async () => {
|
|
64
|
+
const row = await hs.db.findOne("thread_subscriptions", key(s.userId, roomId, threadId));
|
|
65
|
+
if (causeStream !== null) {
|
|
66
|
+
if (row && row.unsubscribed_after !== null && causeStream <= row.unsubscribed_after) throw conflicting();
|
|
67
|
+
if (row?.subscribed) return;
|
|
68
|
+
} else if (row?.subscribed && !row.automatic) return;
|
|
69
|
+
await hs.streams.write((stream) => hs.db.upsert("thread_subscriptions", {
|
|
70
|
+
user_id: s.userId,
|
|
71
|
+
room_id: roomId,
|
|
72
|
+
thread_id: threadId,
|
|
73
|
+
subscribed: true,
|
|
74
|
+
automatic: causeStream !== null,
|
|
75
|
+
unsubscribed_after: row?.unsubscribed_after ?? null,
|
|
76
|
+
stream
|
|
77
|
+
}));
|
|
78
|
+
});
|
|
79
|
+
hs.notifier.notify([s.userId]);
|
|
80
|
+
return {};
|
|
81
|
+
}, VERSIONS);
|
|
82
|
+
r.client("DELETE", path, async (ctx) => {
|
|
83
|
+
const s = await ctx.auth();
|
|
84
|
+
const hs = ctx.hs;
|
|
85
|
+
const { roomId, threadId } = ctx.params;
|
|
86
|
+
await threadRoot(ctx, s.userId);
|
|
87
|
+
await hs.cluster.lock(`threadsub:${s.userId}:${roomId}:${threadId}`, async () => {
|
|
88
|
+
if (!(await hs.db.findOne("thread_subscriptions", key(s.userId, roomId, threadId)))?.subscribed) return;
|
|
89
|
+
const position = await hs.streams.position();
|
|
90
|
+
await hs.streams.write((stream) => hs.db.upsert("thread_subscriptions", {
|
|
91
|
+
user_id: s.userId,
|
|
92
|
+
room_id: roomId,
|
|
93
|
+
thread_id: threadId,
|
|
94
|
+
subscribed: false,
|
|
95
|
+
automatic: false,
|
|
96
|
+
unsubscribed_after: position,
|
|
97
|
+
stream
|
|
98
|
+
}));
|
|
99
|
+
});
|
|
100
|
+
hs.notifier.notify([s.userId]);
|
|
101
|
+
return {};
|
|
102
|
+
}, VERSIONS);
|
|
103
|
+
}
|
|
104
|
+
/** MSC4308 sliding sync extension: subscription changes in (since, to]. */
|
|
105
|
+
async function threadSubscriptionsExtension(hs, userId, ext, since, to) {
|
|
106
|
+
const limit = Math.min(Math.max(Number(ext.limit ?? 100) || 100, 1), 1e3);
|
|
107
|
+
const where = [{
|
|
108
|
+
field: "user_id",
|
|
109
|
+
value: userId
|
|
110
|
+
}];
|
|
111
|
+
if (since !== null) where.push({
|
|
112
|
+
field: "stream",
|
|
113
|
+
operator: "gt",
|
|
114
|
+
value: since
|
|
115
|
+
});
|
|
116
|
+
const rows = (await hs.db.findMany("thread_subscriptions", { where })).filter((r) => r.stream <= to && (since !== null || r.subscribed)).sort((a, b) => b.stream - a.stream);
|
|
117
|
+
const out = {};
|
|
118
|
+
for (const row of rows.slice(0, limit)) {
|
|
119
|
+
const section = row.subscribed ? "subscribed" : "unsubscribed";
|
|
120
|
+
((out[section] ??= {})[row.room_id] ??= {})[row.thread_id] = row.subscribed ? {
|
|
121
|
+
automatic: !!row.automatic,
|
|
122
|
+
bump_stamp: row.stream
|
|
123
|
+
} : { bump_stamp: row.stream };
|
|
124
|
+
}
|
|
125
|
+
if (rows.length > limit) out.prev_batch = String(rows[limit - 1].stream);
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
export { isSubscribed, registerThreadSubscriptionRoutes, threadSubscriptionsExtension };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { MatrixError, errors } from "../core/errors.js";
|
|
2
|
+
import { canonicalAddress, isMedium } from "../core/threepid.js";
|
|
3
|
+
import { requireUia } from "./uia.js";
|
|
4
|
+
import { body, fail, lenient } from "../http/input.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
//#region src/api/threepid.ts
|
|
7
|
+
const submitField = z.string(fail("M_MISSING_PARAM", "sid, client_secret and token are required"));
|
|
8
|
+
const submitBody = body({
|
|
9
|
+
sid: submitField,
|
|
10
|
+
client_secret: submitField,
|
|
11
|
+
token: submitField
|
|
12
|
+
});
|
|
13
|
+
const bindField = (name) => z.string(fail("M_MISSING_PARAM", `Missing ${name}`));
|
|
14
|
+
const bindBody = body({
|
|
15
|
+
client_secret: bindField("client_secret"),
|
|
16
|
+
id_access_token: bindField("id_access_token"),
|
|
17
|
+
id_server: bindField("id_server"),
|
|
18
|
+
sid: bindField("sid")
|
|
19
|
+
});
|
|
20
|
+
const threepidBody = body({
|
|
21
|
+
medium: z.enum(["email", "msisdn"], fail("M_INVALID_PARAM", "Unsupported medium")),
|
|
22
|
+
address: z.string(fail("M_INVALID_PARAM", "Invalid address")),
|
|
23
|
+
id_server: z.string(fail("M_INVALID_PARAM", "id_server must be a string")).optional()
|
|
24
|
+
});
|
|
25
|
+
function notDelegated(ctx) {
|
|
26
|
+
if (ctx.hs.oauth) throw errors.unrecognized("Managed by the OAuth provider");
|
|
27
|
+
}
|
|
28
|
+
function registerThreepidRoutes(r) {
|
|
29
|
+
const requestToken = (purpose, medium) => async (ctx) => {
|
|
30
|
+
notDelegated(ctx);
|
|
31
|
+
await ctx.rateLimit(purpose === "register" ? "register" : "login");
|
|
32
|
+
return ctx.hs.threepids.requestToken(purpose, medium, ctx.input);
|
|
33
|
+
};
|
|
34
|
+
for (const medium of ["email", "msisdn"]) {
|
|
35
|
+
r.client("POST", `/register/${medium}/requestToken`, { input: body({}) }, requestToken("register", medium));
|
|
36
|
+
r.client("POST", `/account/password/${medium}/requestToken`, { input: body({}) }, requestToken("password_reset", medium));
|
|
37
|
+
r.client("POST", `/account/3pid/${medium}/requestToken`, { input: body({}) }, requestToken("add_threepid", medium));
|
|
38
|
+
}
|
|
39
|
+
const submitPath = "/_matrix/client/unstable/threepid/submit_token";
|
|
40
|
+
r.get(submitPath, async (ctx) => {
|
|
41
|
+
const q = ctx.query;
|
|
42
|
+
let nextLink = null;
|
|
43
|
+
let message = "Your address has been validated. You can return to your Matrix client.";
|
|
44
|
+
let status = 200;
|
|
45
|
+
try {
|
|
46
|
+
nextLink = await ctx.hs.threepids.submitToken(q.get("sid") ?? "", q.get("client_secret") ?? "", q.get("token") ?? "");
|
|
47
|
+
} catch (err) {
|
|
48
|
+
message = err instanceof MatrixError ? `Validation failed: ${err.message}.` : "Validation failed.";
|
|
49
|
+
status = 400;
|
|
50
|
+
}
|
|
51
|
+
if (nextLink && status === 200) return Response.redirect(nextLink, 302);
|
|
52
|
+
return new Response(`<!doctype html><meta charset="utf-8"><title>${ctx.hs.serverName}</title><p>${message}</p>`, {
|
|
53
|
+
status,
|
|
54
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
r.post(submitPath, { input: submitBody }, async (ctx) => {
|
|
58
|
+
const body = ctx.input;
|
|
59
|
+
await ctx.hs.threepids.submitToken(body.sid, body.client_secret, body.token);
|
|
60
|
+
return { success: true };
|
|
61
|
+
});
|
|
62
|
+
r.client("GET", "/account/3pid", async (ctx) => {
|
|
63
|
+
const s = await ctx.auth();
|
|
64
|
+
return { threepids: await ctx.hs.threepids.list(s.userId) };
|
|
65
|
+
});
|
|
66
|
+
r.client("POST", "/account/3pid/add", { input: body({
|
|
67
|
+
sid: lenient(z.string()),
|
|
68
|
+
client_secret: lenient(z.string())
|
|
69
|
+
}) }, async (ctx) => {
|
|
70
|
+
notDelegated(ctx);
|
|
71
|
+
const s = await ctx.auth();
|
|
72
|
+
const body = ctx.input;
|
|
73
|
+
await requireUia(ctx.hs, body, [["m.login.password"]], s.userId);
|
|
74
|
+
const validated = await ctx.hs.threepids.validated({
|
|
75
|
+
sid: body.sid,
|
|
76
|
+
client_secret: body.client_secret
|
|
77
|
+
});
|
|
78
|
+
if (!validated) throw new MatrixError(400, "M_THREEPID_AUTH_FAILED", "The address has not been validated");
|
|
79
|
+
await ctx.hs.threepids.add(s.userId, validated);
|
|
80
|
+
return {};
|
|
81
|
+
});
|
|
82
|
+
r.client("POST", "/account/3pid/bind", { input: bindBody }, async (ctx) => {
|
|
83
|
+
const s = await ctx.auth();
|
|
84
|
+
const body = ctx.input;
|
|
85
|
+
const res = await ctx.hs.identityServers.bind(body.id_server, body.id_access_token, body.sid, body.client_secret, s.userId);
|
|
86
|
+
if (res && isMedium(res.medium) && typeof res.address === "string") await ctx.hs.db.upsert("threepid_bindings", {
|
|
87
|
+
user_id: s.userId,
|
|
88
|
+
medium: res.medium,
|
|
89
|
+
address: res.address,
|
|
90
|
+
id_server: body.id_server
|
|
91
|
+
});
|
|
92
|
+
return {};
|
|
93
|
+
});
|
|
94
|
+
const parseThreepid = (body) => {
|
|
95
|
+
const address = canonicalAddress(body.medium, body.address);
|
|
96
|
+
if (!address) throw errors.invalidParam("Invalid address");
|
|
97
|
+
return {
|
|
98
|
+
medium: body.medium,
|
|
99
|
+
address,
|
|
100
|
+
idServer: body.id_server
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
r.client("POST", "/account/3pid/delete", { input: threepidBody }, async (ctx) => {
|
|
104
|
+
notDelegated(ctx);
|
|
105
|
+
const s = await ctx.auth();
|
|
106
|
+
const { medium, address, idServer } = parseThreepid(ctx.input);
|
|
107
|
+
const result = await ctx.hs.threepids.unbindAll(s.userId, medium, address, idServer);
|
|
108
|
+
await ctx.hs.threepids.remove(s.userId, medium, address);
|
|
109
|
+
return { id_server_unbind_result: result };
|
|
110
|
+
});
|
|
111
|
+
r.client("POST", "/account/3pid/unbind", { input: threepidBody }, async (ctx) => {
|
|
112
|
+
const s = await ctx.auth();
|
|
113
|
+
const { medium, address, idServer } = parseThreepid(ctx.input);
|
|
114
|
+
return { id_server_unbind_result: await ctx.hs.threepids.unbindAll(s.userId, medium, address, idServer) };
|
|
115
|
+
});
|
|
116
|
+
r.client("POST", "/account/3pid", async (ctx) => {
|
|
117
|
+
await ctx.auth();
|
|
118
|
+
throw errors.threepidDenied();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
export { registerThreepidRoutes };
|
package/dist/api/uia.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { isObject } from "../core/json.js";
|
|
2
|
+
import { randomString } from "../core/crypto.js";
|
|
3
|
+
import { MatrixError, errors } from "../core/errors.js";
|
|
4
|
+
//#region src/api/uia.ts
|
|
5
|
+
const UIA_TTL_MS = 6e5;
|
|
6
|
+
/**
|
|
7
|
+
* User-interactive authentication. Resolves when one of `flows` has been
|
|
8
|
+
* completed; otherwise throws the 401 response describing what is required.
|
|
9
|
+
* `userId` binds password stages to an already-authenticated user.
|
|
10
|
+
*/
|
|
11
|
+
async function requireUia(hs, body, flows, userId) {
|
|
12
|
+
if (hs.oauth && flows.some((f) => f.includes("m.login.password"))) flows = [["m.oauth"], ["org.matrix.cross_signing_reset"]];
|
|
13
|
+
const auth = isObject(body.auth) ? body.auth : null;
|
|
14
|
+
let sessionId = typeof auth?.session === "string" ? auth.session : void 0;
|
|
15
|
+
const stored = sessionId ? await hs.cluster.get(`uia:${sessionId}`) : null;
|
|
16
|
+
const session = {
|
|
17
|
+
completed: new Set(stored?.completed ?? []),
|
|
18
|
+
userId: stored ? stored.userId : userId,
|
|
19
|
+
threepids: stored?.threepids ?? []
|
|
20
|
+
};
|
|
21
|
+
if (!stored) sessionId = randomString(24);
|
|
22
|
+
const save = () => hs.cluster.set(`uia:${sessionId}`, {
|
|
23
|
+
completed: [...session.completed],
|
|
24
|
+
userId: session.userId,
|
|
25
|
+
threepids: session.threepids
|
|
26
|
+
}, UIA_TTL_MS);
|
|
27
|
+
const oauthUrl = hs.oauth?.options.accountManagementUrl ?? hs.oauth?.options.issuer;
|
|
28
|
+
const params = hs.oauth ? {
|
|
29
|
+
"m.oauth": { url: oauthUrl },
|
|
30
|
+
"org.matrix.cross_signing_reset": { url: oauthUrl }
|
|
31
|
+
} : {};
|
|
32
|
+
const challenge = (extra = {}) => new MatrixError(401, extra.errcode ? String(extra.errcode) : "M_FORBIDDEN", String(extra.error ?? "Authentication required"), {
|
|
33
|
+
flows: flows.map((stages) => ({ stages })),
|
|
34
|
+
params,
|
|
35
|
+
session: sessionId,
|
|
36
|
+
completed: [...session.completed],
|
|
37
|
+
...extra
|
|
38
|
+
});
|
|
39
|
+
if (auth?.type === "m.login.dummy" && !auth.session && typeof body.username === "string" && !userId) {
|
|
40
|
+
if (await hs.cluster.get(`uia-register:${body.username.toLowerCase()}`)) throw challenge({
|
|
41
|
+
errcode: "M_FORBIDDEN",
|
|
42
|
+
error: "Missing session"
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (auth?.type) switch (auth.type) {
|
|
46
|
+
case "m.login.dummy":
|
|
47
|
+
session.completed.add("m.login.dummy");
|
|
48
|
+
break;
|
|
49
|
+
case "m.oauth":
|
|
50
|
+
case "org.matrix.cross_signing_reset":
|
|
51
|
+
if (!hs.oauth) throw challenge({
|
|
52
|
+
errcode: "M_UNRECOGNIZED",
|
|
53
|
+
error: "OAuth is not enabled"
|
|
54
|
+
});
|
|
55
|
+
session.completed.add(auth.type);
|
|
56
|
+
break;
|
|
57
|
+
case "m.login.registration_token": {
|
|
58
|
+
const { registrationTokenValid } = await import("./account.js");
|
|
59
|
+
if (typeof auth.token !== "string" || !await registrationTokenValid(hs, auth.token)) throw challenge({
|
|
60
|
+
errcode: "M_FORBIDDEN",
|
|
61
|
+
error: "Invalid registration token"
|
|
62
|
+
});
|
|
63
|
+
session.completed.add("m.login.registration_token");
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case "m.login.password": {
|
|
67
|
+
const identifier = isObject(auth.identifier) ? auth.identifier : {
|
|
68
|
+
type: "m.id.user",
|
|
69
|
+
user: auth.user
|
|
70
|
+
};
|
|
71
|
+
const user = typeof identifier.user === "string" ? identifier.user : "";
|
|
72
|
+
const target = user.startsWith("@") ? user : `@${user.toLowerCase()}:${hs.serverName}`;
|
|
73
|
+
if (userId && target !== userId) throw errors.forbidden("Credentials do not match the authenticated user");
|
|
74
|
+
const row = await hs.db.findOne("users", [{
|
|
75
|
+
field: "user_id",
|
|
76
|
+
value: target
|
|
77
|
+
}]);
|
|
78
|
+
if (!(row && !row.deactivated && typeof auth.password === "string" && await hs.userAuth.checkPassword({
|
|
79
|
+
userId: target,
|
|
80
|
+
localpart: target.slice(1).split(":")[0],
|
|
81
|
+
password: auth.password,
|
|
82
|
+
storedHash: row.password_hash
|
|
83
|
+
}))) throw challenge({
|
|
84
|
+
errcode: "M_FORBIDDEN",
|
|
85
|
+
error: "Invalid password"
|
|
86
|
+
});
|
|
87
|
+
session.completed.add("m.login.password");
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case "m.login.email.identity":
|
|
91
|
+
case "m.login.msisdn": {
|
|
92
|
+
const validated = await hs.threepids.validated(auth.threepid_creds ?? auth.threepidCreds);
|
|
93
|
+
const medium = auth.type === "m.login.email.identity" ? "email" : "msisdn";
|
|
94
|
+
if (!validated || validated.medium !== medium) throw challenge({
|
|
95
|
+
errcode: "M_UNAUTHORIZED",
|
|
96
|
+
error: "The address has not been validated"
|
|
97
|
+
});
|
|
98
|
+
if (session.userId && await hs.threepids.userFor(validated.medium, validated.address) !== session.userId) throw challenge({
|
|
99
|
+
errcode: "M_FORBIDDEN",
|
|
100
|
+
error: "This address does not belong to your account"
|
|
101
|
+
});
|
|
102
|
+
session.threepids = [...session.threepids.filter((t) => t.medium !== medium), validated];
|
|
103
|
+
session.completed.add(auth.type);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
default: throw challenge({
|
|
107
|
+
errcode: "M_UNRECOGNIZED",
|
|
108
|
+
error: `Unsupported auth type ${auth.type}`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (!flows.some((stages) => stages.every((s) => session.completed.has(s)))) {
|
|
112
|
+
await save();
|
|
113
|
+
if (!userId && typeof body.username === "string") await hs.cluster.set(`uia-register:${body.username.toLowerCase()}`, sessionId, UIA_TTL_MS);
|
|
114
|
+
if (auth?.type) throw challenge();
|
|
115
|
+
const e = challenge();
|
|
116
|
+
throw new MatrixError(401, "M_FORBIDDEN", "", {
|
|
117
|
+
...e.extra,
|
|
118
|
+
errcode: void 0,
|
|
119
|
+
error: void 0
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
await hs.cluster.delete(`uia:${sessionId}`);
|
|
123
|
+
return { threepids: session.threepids };
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
export { requireUia };
|
package/dist/api/user.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { canonicalJson, isObject } from "../core/json.js";
|
|
2
|
+
import { errors } from "../core/errors.js";
|
|
3
|
+
import { requireUia } from "./uia.js";
|
|
4
|
+
import { body, fail } from "../http/input.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
//#region src/api/user.ts
|
|
7
|
+
const deleteDevicesBody = body({ devices: z.array(z.unknown(), fail("M_BAD_JSON", "devices must be a list")) });
|
|
8
|
+
const keysQueryBody = body({
|
|
9
|
+
device_keys: z.record(z.string(), z.array(z.string(), fail("M_BAD_JSON", "device_keys values must be lists of device IDs")), fail("M_BAD_JSON", "device_keys must be an object")),
|
|
10
|
+
timeout: z.unknown().optional()
|
|
11
|
+
});
|
|
12
|
+
const keysClaimBody = body({ one_time_keys: z.record(z.string(), z.unknown(), fail("M_BAD_JSON", "one_time_keys must be an object")) });
|
|
13
|
+
const SERVER_MANAGED_TYPES = ["m.fully_read", "m.push_rules"];
|
|
14
|
+
async function setAccountData(hs, userId, roomId, type, content) {
|
|
15
|
+
await hs.streams.write((stream) => hs.db.upsert("account_data", {
|
|
16
|
+
user_id: userId,
|
|
17
|
+
room_id: roomId,
|
|
18
|
+
type,
|
|
19
|
+
content,
|
|
20
|
+
stream
|
|
21
|
+
}));
|
|
22
|
+
hs.notifier.notify([userId]);
|
|
23
|
+
}
|
|
24
|
+
async function getAccountData(hs, userId, roomId, type) {
|
|
25
|
+
const row = await hs.db.findOne("account_data", [
|
|
26
|
+
{
|
|
27
|
+
field: "user_id",
|
|
28
|
+
value: userId
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
field: "room_id",
|
|
32
|
+
value: roomId
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
field: "type",
|
|
36
|
+
value: type
|
|
37
|
+
}
|
|
38
|
+
]);
|
|
39
|
+
if (!row || Object.keys(row.content ?? {}).length === 0) throw errors.notFound("Account data not found");
|
|
40
|
+
return row.content;
|
|
41
|
+
}
|
|
42
|
+
async function checkUser(ctx) {
|
|
43
|
+
const s = await ctx.auth();
|
|
44
|
+
if (ctx.params.userId !== s.userId) throw errors.forbidden("Cannot access another user's data");
|
|
45
|
+
return s;
|
|
46
|
+
}
|
|
47
|
+
async function checkRoom(ctx) {
|
|
48
|
+
if (!ctx.params.roomId.startsWith("!")) throw errors.invalidParam("Invalid room ID");
|
|
49
|
+
}
|
|
50
|
+
function registerUserRoutes(r) {
|
|
51
|
+
r.client("PUT", "/user/:userId/account_data/:type", async (ctx) => {
|
|
52
|
+
const s = await checkUser(ctx);
|
|
53
|
+
if (SERVER_MANAGED_TYPES.includes(ctx.params.type)) throw errors.badJson("This type is managed by the server");
|
|
54
|
+
await setAccountData(ctx.hs, s.userId, "", ctx.params.type, await ctx.json());
|
|
55
|
+
return {};
|
|
56
|
+
});
|
|
57
|
+
r.client("GET", "/user/:userId/account_data/:type", async (ctx) => {
|
|
58
|
+
const s = await checkUser(ctx);
|
|
59
|
+
return getAccountData(ctx.hs, s.userId, "", ctx.params.type);
|
|
60
|
+
});
|
|
61
|
+
r.client("PUT", "/user/:userId/rooms/:roomId/account_data/:type", async (ctx) => {
|
|
62
|
+
const s = await checkUser(ctx);
|
|
63
|
+
await checkRoom(ctx);
|
|
64
|
+
if (SERVER_MANAGED_TYPES.includes(ctx.params.type)) throw errors.badJson("This type is managed by the server");
|
|
65
|
+
await setAccountData(ctx.hs, s.userId, ctx.params.roomId, ctx.params.type, await ctx.json());
|
|
66
|
+
return {};
|
|
67
|
+
});
|
|
68
|
+
r.client("GET", "/user/:userId/rooms/:roomId/account_data/:type", async (ctx) => {
|
|
69
|
+
const s = await checkUser(ctx);
|
|
70
|
+
await checkRoom(ctx);
|
|
71
|
+
return getAccountData(ctx.hs, s.userId, ctx.params.roomId, ctx.params.type);
|
|
72
|
+
});
|
|
73
|
+
const del = "/_matrix/client/unstable/org.matrix.msc3391";
|
|
74
|
+
r.delete(`${del}/user/:userId/account_data/:type`, async (ctx) => {
|
|
75
|
+
const s = await checkUser(ctx);
|
|
76
|
+
await setAccountData(ctx.hs, s.userId, "", ctx.params.type, {});
|
|
77
|
+
return {};
|
|
78
|
+
});
|
|
79
|
+
r.delete(`${del}/user/:userId/rooms/:roomId/account_data/:type`, async (ctx) => {
|
|
80
|
+
const s = await checkUser(ctx);
|
|
81
|
+
await setAccountData(ctx.hs, s.userId, ctx.params.roomId, ctx.params.type, {});
|
|
82
|
+
return {};
|
|
83
|
+
});
|
|
84
|
+
const readTags = async (hs, userId, roomId) => {
|
|
85
|
+
const row = await hs.db.findOne("account_data", [
|
|
86
|
+
{
|
|
87
|
+
field: "user_id",
|
|
88
|
+
value: userId
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
field: "room_id",
|
|
92
|
+
value: roomId
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
field: "type",
|
|
96
|
+
value: "m.tag"
|
|
97
|
+
}
|
|
98
|
+
]);
|
|
99
|
+
return isObject(row?.content?.tags) ? row.content.tags : {};
|
|
100
|
+
};
|
|
101
|
+
r.client("GET", "/user/:userId/rooms/:roomId/tags", async (ctx) => {
|
|
102
|
+
const s = await checkUser(ctx);
|
|
103
|
+
return { tags: await readTags(ctx.hs, s.userId, ctx.params.roomId) };
|
|
104
|
+
});
|
|
105
|
+
r.client("PUT", "/user/:userId/rooms/:roomId/tags/:tag", async (ctx) => {
|
|
106
|
+
const s = await checkUser(ctx);
|
|
107
|
+
const body = await ctx.json();
|
|
108
|
+
if (body.order !== void 0 && typeof body.order !== "number") throw errors.badJson("order must be a number");
|
|
109
|
+
const tags = await readTags(ctx.hs, s.userId, ctx.params.roomId);
|
|
110
|
+
tags[ctx.params.tag] = body;
|
|
111
|
+
await setAccountData(ctx.hs, s.userId, ctx.params.roomId, "m.tag", { tags });
|
|
112
|
+
return {};
|
|
113
|
+
});
|
|
114
|
+
r.client("DELETE", "/user/:userId/rooms/:roomId/tags/:tag", async (ctx) => {
|
|
115
|
+
const s = await checkUser(ctx);
|
|
116
|
+
const tags = await readTags(ctx.hs, s.userId, ctx.params.roomId);
|
|
117
|
+
delete tags[ctx.params.tag];
|
|
118
|
+
await setAccountData(ctx.hs, s.userId, ctx.params.roomId, "m.tag", { tags });
|
|
119
|
+
return {};
|
|
120
|
+
});
|
|
121
|
+
const formatDevice = (d) => ({
|
|
122
|
+
device_id: d.device_id,
|
|
123
|
+
display_name: d.display_name ?? void 0,
|
|
124
|
+
last_seen_ip: d.last_seen_ip ?? void 0,
|
|
125
|
+
last_seen_ts: d.last_seen_ts ?? void 0
|
|
126
|
+
});
|
|
127
|
+
r.client("GET", "/devices", async (ctx) => {
|
|
128
|
+
const s = await ctx.auth();
|
|
129
|
+
return { devices: (await ctx.hs.db.findMany("devices", { where: [{
|
|
130
|
+
field: "user_id",
|
|
131
|
+
value: s.userId
|
|
132
|
+
}] })).map(formatDevice) };
|
|
133
|
+
});
|
|
134
|
+
r.client("GET", "/devices/:deviceId", async (ctx) => {
|
|
135
|
+
const s = await ctx.auth();
|
|
136
|
+
const row = await ctx.hs.db.findOne("devices", [{
|
|
137
|
+
field: "user_id",
|
|
138
|
+
value: s.userId
|
|
139
|
+
}, {
|
|
140
|
+
field: "device_id",
|
|
141
|
+
value: ctx.params.deviceId
|
|
142
|
+
}]);
|
|
143
|
+
if (!row) throw errors.notFound("Device not found");
|
|
144
|
+
return formatDevice(row);
|
|
145
|
+
});
|
|
146
|
+
r.client("PUT", "/devices/:deviceId", async (ctx) => {
|
|
147
|
+
const s = await ctx.auth();
|
|
148
|
+
const body = await ctx.json();
|
|
149
|
+
const where = [{
|
|
150
|
+
field: "user_id",
|
|
151
|
+
value: s.userId
|
|
152
|
+
}, {
|
|
153
|
+
field: "device_id",
|
|
154
|
+
value: ctx.params.deviceId
|
|
155
|
+
}];
|
|
156
|
+
if (!await ctx.hs.db.findOne("devices", where)) throw errors.notFound("Device not found");
|
|
157
|
+
if (body.display_name !== void 0) {
|
|
158
|
+
if (typeof body.display_name !== "string") throw errors.badJson("display_name must be a string");
|
|
159
|
+
await ctx.hs.db.update("devices", where, { display_name: body.display_name });
|
|
160
|
+
await ctx.hs.keys.deviceChanged(s.userId);
|
|
161
|
+
}
|
|
162
|
+
return {};
|
|
163
|
+
});
|
|
164
|
+
r.client("DELETE", "/devices/:deviceId", async (ctx) => {
|
|
165
|
+
const s = await ctx.auth();
|
|
166
|
+
const body = await ctx.json();
|
|
167
|
+
const where = [{
|
|
168
|
+
field: "user_id",
|
|
169
|
+
value: s.userId
|
|
170
|
+
}, {
|
|
171
|
+
field: "device_id",
|
|
172
|
+
value: ctx.params.deviceId
|
|
173
|
+
}];
|
|
174
|
+
if (!await ctx.hs.db.findOne("devices", where)) throw errors.notFound("Device not found");
|
|
175
|
+
await requireUia(ctx.hs, body, [["m.login.password"]], s.userId);
|
|
176
|
+
await ctx.hs.auth.deleteDevice(s.userId, ctx.params.deviceId);
|
|
177
|
+
return {};
|
|
178
|
+
});
|
|
179
|
+
r.client("POST", "/delete_devices", { input: deleteDevicesBody }, async (ctx) => {
|
|
180
|
+
const s = await ctx.auth();
|
|
181
|
+
const body = ctx.input;
|
|
182
|
+
await requireUia(ctx.hs, body, [["m.login.password"]], s.userId);
|
|
183
|
+
for (const d of body.devices) if (typeof d === "string") await ctx.hs.auth.deleteDevice(s.userId, d);
|
|
184
|
+
return {};
|
|
185
|
+
});
|
|
186
|
+
r.client("POST", "/keys/upload", { input: body({}) }, async (ctx) => {
|
|
187
|
+
const s = await ctx.auth();
|
|
188
|
+
return { one_time_key_counts: await ctx.hs.keys.upload(s.userId, s.deviceId, ctx.input) };
|
|
189
|
+
});
|
|
190
|
+
r.client("POST", "/keys/query", { input: keysQueryBody }, async (ctx) => {
|
|
191
|
+
const s = await ctx.auth();
|
|
192
|
+
await ctx.rateLimit("keyQuery");
|
|
193
|
+
const body = ctx.input;
|
|
194
|
+
return ctx.hs.keys.query(s.userId, body.device_keys, Math.min(Number(body.timeout ?? 1e4) || 1e4, 3e4));
|
|
195
|
+
});
|
|
196
|
+
r.client("POST", "/keys/claim", { input: keysClaimBody }, async (ctx) => {
|
|
197
|
+
await ctx.auth();
|
|
198
|
+
const body = ctx.input;
|
|
199
|
+
const local = {};
|
|
200
|
+
const remote = {};
|
|
201
|
+
for (const [userId, devices] of Object.entries(body.one_time_keys)) (ctx.hs.isLocalUser(userId) ? local : remote)[userId] = devices;
|
|
202
|
+
const claimed = await ctx.hs.keys.claim(local);
|
|
203
|
+
const fed = Object.keys(remote).length ? await ctx.hs.federation.claimKeys(remote) : {
|
|
204
|
+
results: {},
|
|
205
|
+
failures: {}
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
one_time_keys: {
|
|
209
|
+
...claimed,
|
|
210
|
+
...fed.results
|
|
211
|
+
},
|
|
212
|
+
failures: fed.failures
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
r.client("GET", "/keys/changes", async (ctx) => {
|
|
216
|
+
const s = await ctx.auth();
|
|
217
|
+
const parse = (v) => {
|
|
218
|
+
const m = /^s?(\d+)/.exec(v ?? "");
|
|
219
|
+
if (!m) throw errors.invalidParam("Invalid token");
|
|
220
|
+
return Number(m[1]);
|
|
221
|
+
};
|
|
222
|
+
return ctx.hs.keys.changesSince(s.userId, parse(ctx.query.get("from")), parse(ctx.query.get("to")));
|
|
223
|
+
});
|
|
224
|
+
r.client("POST", "/keys/device_signing/upload", { input: body({}) }, async (ctx) => {
|
|
225
|
+
const s = await ctx.auth();
|
|
226
|
+
const body = ctx.input;
|
|
227
|
+
if (await ctx.hs.db.findOne("cross_signing_keys", [{
|
|
228
|
+
field: "user_id",
|
|
229
|
+
value: s.userId
|
|
230
|
+
}, {
|
|
231
|
+
field: "key_type",
|
|
232
|
+
value: "master"
|
|
233
|
+
}])) {
|
|
234
|
+
const stored = new Map((await ctx.hs.db.findMany("cross_signing_keys", { where: [{
|
|
235
|
+
field: "user_id",
|
|
236
|
+
value: s.userId
|
|
237
|
+
}] })).map((row) => [row.key_type, canonicalJson(row.key)]));
|
|
238
|
+
if ([
|
|
239
|
+
"master",
|
|
240
|
+
"self_signing",
|
|
241
|
+
"user_signing"
|
|
242
|
+
].filter((type) => body[`${type}_key`] !== void 0).every((type) => stored.get(type) === canonicalJson(body[`${type}_key`]))) return {};
|
|
243
|
+
await requireUia(ctx.hs, body, [["m.login.password"]], s.userId);
|
|
244
|
+
}
|
|
245
|
+
await ctx.hs.keys.uploadCrossSigning(s.userId, body);
|
|
246
|
+
return {};
|
|
247
|
+
});
|
|
248
|
+
r.client("POST", "/keys/signatures/upload", { input: body({}) }, async (ctx) => {
|
|
249
|
+
const s = await ctx.auth();
|
|
250
|
+
return ctx.hs.keys.uploadSignatures(s.userId, ctx.input);
|
|
251
|
+
});
|
|
252
|
+
r.client("PUT", "/sendToDevice/:eventType/:txnId", async (ctx) => {
|
|
253
|
+
const s = await ctx.auth();
|
|
254
|
+
const hs = ctx.hs;
|
|
255
|
+
const body = await ctx.json();
|
|
256
|
+
if (!isObject(body.messages)) throw errors.badJson("messages must be an object");
|
|
257
|
+
const txnKey = `${s.userId}|${s.deviceId}|${ctx.params.txnId}`;
|
|
258
|
+
if (!await hs.cluster.setIfAbsent(`todevice-txn:${txnKey}`, true, 864e5)) return {};
|
|
259
|
+
const recipients = /* @__PURE__ */ new Set();
|
|
260
|
+
for (const [userId, devices] of Object.entries(body.messages)) {
|
|
261
|
+
if (!isObject(devices) || !hs.isLocalUser(userId)) continue;
|
|
262
|
+
let targets = Object.keys(devices);
|
|
263
|
+
if (targets.includes("*")) targets = (await hs.db.findMany("devices", { where: [{
|
|
264
|
+
field: "user_id",
|
|
265
|
+
value: userId
|
|
266
|
+
}] })).map((d) => d.device_id);
|
|
267
|
+
for (const deviceId of targets) {
|
|
268
|
+
const content = devices[deviceId] ?? devices["*"];
|
|
269
|
+
await hs.streams.write((stream) => hs.db.create("to_device", {
|
|
270
|
+
stream,
|
|
271
|
+
user_id: userId,
|
|
272
|
+
device_id: deviceId,
|
|
273
|
+
sender: s.userId,
|
|
274
|
+
type: ctx.params.eventType,
|
|
275
|
+
content
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
recipients.add(userId);
|
|
279
|
+
}
|
|
280
|
+
hs.notifier.notify(recipients);
|
|
281
|
+
await hs.federation.sendToDevice(s.userId, ctx.params.eventType, body.messages, `${s.deviceId}-${ctx.params.txnId}`);
|
|
282
|
+
return {};
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
286
|
+
export { registerUserRoutes };
|