khala-cli 0.4.1 → 0.4.4
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/dist/{chunk-S72K3GSP.mjs → chunk-6OADQWV7.mjs} +31 -20
- package/dist/{chunk-YMDAY234.mjs → chunk-7CVU5OZ7.mjs} +1 -1
- package/dist/{chunk-PSN7P4AV.mjs → chunk-AK74S7EN.mjs} +4 -4
- package/dist/chunk-AUJEUKVM.mjs +121 -0
- package/dist/chunk-DC3OX6F4.mjs +52 -0
- package/dist/chunk-ENPBK5W2.mjs +187 -0
- package/dist/chunk-FA6Z65DW.mjs +19 -0
- package/dist/{chunk-KEGR6VEQ.mjs → chunk-FUU775QM.mjs} +81 -10
- package/dist/chunk-GE3RXO2X.mjs +26 -0
- package/dist/chunk-LHAMBQK7.mjs +144 -0
- package/dist/{chunk-EVLXC2WS.mjs → chunk-LN3G2LJV.mjs} +13 -10
- package/dist/{chunk-35KKPOKI.mjs → chunk-LQ6KNUNN.mjs} +87 -53
- package/dist/{chunk-CSIFRJFF.mjs → chunk-MU4FUENP.mjs} +3 -2
- package/dist/{chunk-L2HRDTA5.mjs → chunk-MZS7XFMC.mjs} +3 -3
- package/dist/{chunk-U5ZMOPUK.mjs → chunk-N5VYKQMR.mjs} +5 -46
- package/dist/{chunk-3SWCRLWX.mjs → chunk-OS2JLDL7.mjs} +172 -227
- package/dist/chunk-P5CP32X2.mjs +170 -0
- package/dist/{chunk-RX5HGJQT.mjs → chunk-PJ3E3IVH.mjs} +184 -126
- package/dist/chunk-SN3STAO2.mjs +190 -0
- package/dist/{chunk-B5BFL7OD.mjs → chunk-XEYKJMZL.mjs} +1 -1
- package/dist/{chunk-SR5BG7FL.mjs → chunk-ZKDDKUUA.mjs} +1 -1
- package/dist/{chunk-HAD5ZILW.mjs → chunk-ZRFXZK72.mjs} +1 -1
- package/dist/khala.mjs +12 -10
- package/dist/web/assets/apple-touch-icon-Du9V6qit.png +0 -0
- package/dist/web/assets/favicon-16x16-D5waNSZD.png +0 -0
- package/dist/web/assets/favicon-32x32-B9Ubw1Dx.png +0 -0
- package/dist/web/assets/favicon-zCD0pKtT.ico +0 -0
- package/dist/web/assets/local-Bk8hMIT-.js +9 -0
- package/dist/web/assets/{local-DHYvcQG0.css → local-vdvavyq0.css} +1 -1
- package/dist/web/index.html +7 -2
- package/package.json +1 -1
- package/dist/chunk-AVJKIJ34.mjs +0 -91
- package/dist/chunk-N6LMHR7P.mjs +0 -455
- package/dist/web/assets/local-DGdQ2afg.js +0 -9
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
ensureHelper,
|
|
4
|
+
helperPaths,
|
|
5
|
+
readHelperFile
|
|
6
|
+
} from "./chunk-LHAMBQK7.mjs";
|
|
7
|
+
import {
|
|
8
|
+
parseChannelLink,
|
|
9
|
+
resolveLocalOwnerName
|
|
10
|
+
} from "./chunk-MZS7XFMC.mjs";
|
|
11
|
+
import {
|
|
12
|
+
KHALA_AGENT_VERSION
|
|
13
|
+
} from "./chunk-7CVU5OZ7.mjs";
|
|
2
14
|
import {
|
|
3
15
|
LOCAL_AGENT_DEVICE_PREFIX,
|
|
4
16
|
LOCAL_IDLE_EXIT_MS,
|
|
@@ -9,63 +21,55 @@ import {
|
|
|
9
21
|
LOCAL_OWNER_USER_ID,
|
|
10
22
|
LOCAL_TOKEN_BYTES,
|
|
11
23
|
base64url,
|
|
24
|
+
checkName,
|
|
12
25
|
decodeChannelSecrets,
|
|
13
26
|
decodeLocalEvent,
|
|
14
27
|
decodeOwnerProfile,
|
|
15
|
-
|
|
16
|
-
|
|
28
|
+
defaultAgentName,
|
|
29
|
+
defaultHumanColor,
|
|
30
|
+
isDefaultAgentName,
|
|
31
|
+
isHumanColorId,
|
|
17
32
|
isLocalRoomId,
|
|
18
33
|
localRoomKey,
|
|
19
34
|
newLocalAgentUserId,
|
|
20
35
|
newLocalEventId,
|
|
21
36
|
newLocalRoomId,
|
|
22
|
-
|
|
23
|
-
} from "./chunk-
|
|
24
|
-
import {
|
|
25
|
-
parseChannelLink,
|
|
26
|
-
resolveLocalOwnerName
|
|
27
|
-
} from "./chunk-L2HRDTA5.mjs";
|
|
37
|
+
normalizeInitials
|
|
38
|
+
} from "./chunk-OS2JLDL7.mjs";
|
|
28
39
|
import {
|
|
29
|
-
|
|
30
|
-
} from "./chunk-
|
|
40
|
+
bundle
|
|
41
|
+
} from "./chunk-ZRFXZK72.mjs";
|
|
42
|
+
import "./chunk-5GLTKWJ5.mjs";
|
|
31
43
|
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
normalizeInitials
|
|
38
|
-
} from "./chunk-AVJKIJ34.mjs";
|
|
44
|
+
CHANNEL_EVENT_TYPE,
|
|
45
|
+
decodeChannelEvent,
|
|
46
|
+
encodeChannelEvent,
|
|
47
|
+
memberRenameContent
|
|
48
|
+
} from "./chunk-P5CP32X2.mjs";
|
|
39
49
|
import {
|
|
40
50
|
StateError,
|
|
41
51
|
ensureStateDir,
|
|
42
52
|
readJson,
|
|
43
53
|
writeJsonAtomic
|
|
44
|
-
} from "./chunk-
|
|
45
|
-
import "./chunk-5GLTKWJ5.mjs";
|
|
54
|
+
} from "./chunk-XEYKJMZL.mjs";
|
|
46
55
|
import {
|
|
47
|
-
CHANNEL_EVENT_TYPE,
|
|
48
56
|
DEFAULT_LISTENING_MODE,
|
|
49
|
-
HARNESSES,
|
|
50
57
|
LISTENING_MODE_COMMAND_TYPE,
|
|
51
58
|
LISTENING_MODE_MEMBER_KEY,
|
|
52
|
-
agentConfirmPagePath,
|
|
53
|
-
decodeChannelEvent,
|
|
54
59
|
decodeListeningMode,
|
|
55
60
|
decodeListeningModeCommand,
|
|
61
|
+
memberListeningMode
|
|
62
|
+
} from "./chunk-AUJEUKVM.mjs";
|
|
63
|
+
import {
|
|
64
|
+
HARNESSES,
|
|
65
|
+
agentConfirmPagePath,
|
|
56
66
|
decodeWith,
|
|
57
|
-
encodeChannelEvent,
|
|
58
|
-
memberListeningMode,
|
|
59
|
-
memberRenameContent,
|
|
60
67
|
object,
|
|
61
68
|
utf8Length,
|
|
62
69
|
validAgentRejoinSecret,
|
|
63
70
|
validAgentSessionId
|
|
64
|
-
} from "./chunk-
|
|
65
|
-
import
|
|
66
|
-
bundle
|
|
67
|
-
} from "./chunk-HAD5ZILW.mjs";
|
|
68
|
-
import "./chunk-SR5BG7FL.mjs";
|
|
71
|
+
} from "./chunk-SN3STAO2.mjs";
|
|
72
|
+
import "./chunk-ZKDDKUUA.mjs";
|
|
69
73
|
|
|
70
74
|
// src/local/cli.ts
|
|
71
75
|
import { setTimeout as sleepDefault } from "node:timers/promises";
|
|
@@ -173,6 +177,8 @@ async function openLocalStore(input) {
|
|
|
173
177
|
if (decoded.ok && decoded.value.roomId === roomId && decoded.value.seq > lastSeq(channel)) indexEvent(channel, decoded.value);
|
|
174
178
|
}
|
|
175
179
|
if (channel.events[0]?.type !== "m.room.create") continue;
|
|
180
|
+
const names = await readJson(path.join(channel.dir, "names.json"));
|
|
181
|
+
if (names?.v === 1 && typeof names.owner === "string" && checkName(names.owner, "username").ok) channel.ownerName = names.owner;
|
|
176
182
|
const secrets = decodeChannelSecrets(await readJson(path.join(channel.dir, "secrets.json")));
|
|
177
183
|
channel.secrets = secrets.ok ? secrets.value : emptySecrets();
|
|
178
184
|
register(channel);
|
|
@@ -254,7 +260,7 @@ async function openLocalStore(input) {
|
|
|
254
260
|
participantId: userId,
|
|
255
261
|
ownerId: LOCAL_OWNER_ID,
|
|
256
262
|
deviceId: human ? LOCAL_OWNER_DEVICE_ID : LOCAL_AGENT_DEVICE_PREFIX + userId.slice(7, 15),
|
|
257
|
-
displayName: human ? owner2.username : content.displayname,
|
|
263
|
+
displayName: human ? channel.ownerName ?? owner2.username : content.displayname,
|
|
258
264
|
kind: content.kind,
|
|
259
265
|
membership: content.membership,
|
|
260
266
|
...!human ? {
|
|
@@ -290,11 +296,13 @@ async function openLocalStore(input) {
|
|
|
290
296
|
lastTs: channel.events.at(-1).ts,
|
|
291
297
|
preview: message ? message.content.body : null,
|
|
292
298
|
...message ? { lastSender: { userId: message.sender, displayName: member(channel, message.sender)?.displayName ?? message.sender } } : {},
|
|
299
|
+
// `since` orders who held a name first in this channel, for the per-channel name prompt.
|
|
293
300
|
members: members(channel).map((m) => ({
|
|
294
301
|
userId: m.userId,
|
|
295
302
|
displayName: m.displayName,
|
|
296
303
|
kind: m.kind,
|
|
297
|
-
...m.harness !== void 0 ? { harness: m.harness } : {}
|
|
304
|
+
...m.harness !== void 0 ? { harness: m.harness } : {},
|
|
305
|
+
since: channel.lastMember.get(m.userId).event.ts
|
|
298
306
|
}))
|
|
299
307
|
};
|
|
300
308
|
}
|
|
@@ -472,6 +480,29 @@ async function openLocalStore(input) {
|
|
|
472
480
|
}
|
|
473
481
|
return result;
|
|
474
482
|
},
|
|
483
|
+
ownerChannelName: (roomId) => channels.get(roomId)?.ownerName,
|
|
484
|
+
setOwnerChannelName: (roomId, name) => enqueue(async () => {
|
|
485
|
+
const channel = requireChannel(roomId);
|
|
486
|
+
const before = member(channel, LOCAL_OWNER_USER_ID);
|
|
487
|
+
const file = path.join(channel.dir, "names.json");
|
|
488
|
+
if (name === null) await fs.rm(file, { force: true });
|
|
489
|
+
else await writeJsonAtomic(file, { v: 1, owner: name });
|
|
490
|
+
if (name === null) delete channel.ownerName;
|
|
491
|
+
else channel.ownerName = name;
|
|
492
|
+
const after = member(channel, LOCAL_OWNER_USER_ID);
|
|
493
|
+
if (!before || !after || before.displayName === after.displayName) {
|
|
494
|
+
bump();
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
const event = await writeEvent(channel, {
|
|
498
|
+
type: "m.room.member",
|
|
499
|
+
sender: LOCAL_OWNER_USER_ID,
|
|
500
|
+
content: { user: LOCAL_OWNER_USER_ID, membership: before.membership, displayname: after.displayName, kind: "human" }
|
|
501
|
+
});
|
|
502
|
+
bump();
|
|
503
|
+
fire(channel.waiters);
|
|
504
|
+
return structuredClone(event);
|
|
505
|
+
}),
|
|
475
506
|
owner: () => structuredClone(owner2),
|
|
476
507
|
setOwner: (next) => {
|
|
477
508
|
const owned = structuredClone(next);
|
|
@@ -520,8 +551,8 @@ var mime = {
|
|
|
520
551
|
};
|
|
521
552
|
function respond(res, result) {
|
|
522
553
|
if (res.destroyed || res.writableEnded) return;
|
|
523
|
-
for (const [
|
|
524
|
-
if (!
|
|
554
|
+
for (const [key2, value] of Object.entries(result.headers ?? {})) {
|
|
555
|
+
if (!key2.toLowerCase().startsWith("access-control-")) res.setHeader(key2, value);
|
|
525
556
|
}
|
|
526
557
|
res.setHeader("cache-control", "no-store");
|
|
527
558
|
res.setHeader("x-content-type-options", "nosniff");
|
|
@@ -546,14 +577,14 @@ function readBody(req, signal) {
|
|
|
546
577
|
const cleanup = () => {
|
|
547
578
|
req.off("data", data);
|
|
548
579
|
req.off("end", end);
|
|
549
|
-
req.off("error",
|
|
580
|
+
req.off("error", fail6);
|
|
550
581
|
signal.removeEventListener("abort", abort);
|
|
551
582
|
};
|
|
552
|
-
const
|
|
583
|
+
const fail6 = (err) => {
|
|
553
584
|
cleanup();
|
|
554
585
|
reject(err);
|
|
555
586
|
};
|
|
556
|
-
const abort = () =>
|
|
587
|
+
const abort = () => fail6(new Error("client_disconnected"));
|
|
557
588
|
const data = (chunk) => {
|
|
558
589
|
size += chunk.length;
|
|
559
590
|
if (size > LOCAL_BODY_LIMIT_BYTES) {
|
|
@@ -573,7 +604,7 @@ function readBody(req, signal) {
|
|
|
573
604
|
};
|
|
574
605
|
req.on("data", data);
|
|
575
606
|
req.on("end", end);
|
|
576
|
-
req.on("error",
|
|
607
|
+
req.on("error", fail6);
|
|
577
608
|
signal.addEventListener("abort", abort, { once: true });
|
|
578
609
|
if (signal.aborted) abort();
|
|
579
610
|
});
|
|
@@ -730,17 +761,17 @@ function createHelperServer(options) {
|
|
|
730
761
|
listen() {
|
|
731
762
|
if (closing) return Promise.reject(new Error("server_closed"));
|
|
732
763
|
return listenPromise ??= new Promise((resolve2, reject) => {
|
|
733
|
-
const
|
|
764
|
+
const fail6 = (err) => {
|
|
734
765
|
cancelListen = void 0;
|
|
735
766
|
reject(err);
|
|
736
767
|
};
|
|
737
768
|
cancelListen = () => {
|
|
738
|
-
server.off("error",
|
|
769
|
+
server.off("error", fail6);
|
|
739
770
|
reject(new Error("server_closed"));
|
|
740
771
|
};
|
|
741
|
-
server.once("error",
|
|
772
|
+
server.once("error", fail6);
|
|
742
773
|
server.listen({ host: "127.0.0.1", port: options.port }, () => {
|
|
743
|
-
server.off("error",
|
|
774
|
+
server.off("error", fail6);
|
|
744
775
|
cancelListen = void 0;
|
|
745
776
|
const address = server.address();
|
|
746
777
|
if (!address || typeof address === "string") return reject(new Error("invalid_address"));
|
|
@@ -775,9 +806,23 @@ function createHelperServer(options) {
|
|
|
775
806
|
|
|
776
807
|
// src/local/routes/agent-join.ts
|
|
777
808
|
import { createHash as createHash2, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
809
|
+
|
|
810
|
+
// ../contracts/src/m1/channel-names.ts
|
|
811
|
+
var key = (name) => name.trim().toLowerCase();
|
|
812
|
+
function takenNames(names) {
|
|
813
|
+
return new Set([...names].map(key));
|
|
814
|
+
}
|
|
815
|
+
function freeAgentName(username, harness, taken) {
|
|
816
|
+
const used = takenNames(taken);
|
|
817
|
+
let n = 1;
|
|
818
|
+
while (n < 1e3 && used.has(key(defaultAgentName(username, harness, n)))) n++;
|
|
819
|
+
return defaultAgentName(username, harness, n);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// src/local/routes/agent-join.ts
|
|
778
823
|
var sha256hex = (value) => createHash2("sha256").update(value).digest("hex");
|
|
779
824
|
var b64 = (bytes) => Buffer.from(bytes).toString("base64url");
|
|
780
|
-
var
|
|
825
|
+
var fail2 = (status, error2) => ({ status, json: { error: error2 } });
|
|
781
826
|
function safeEqual(a, b) {
|
|
782
827
|
const left = Buffer.from(a), right = Buffer.from(b);
|
|
783
828
|
return left.length === right.length && timingSafeEqual2(left, right);
|
|
@@ -807,34 +852,31 @@ function agentJoinRoutes() {
|
|
|
807
852
|
async function create(req, _params, ctx) {
|
|
808
853
|
try {
|
|
809
854
|
prune(ctx);
|
|
810
|
-
if (typeof req.body !== "object" || req.body === null || Array.isArray(req.body)) return
|
|
855
|
+
if (typeof req.body !== "object" || req.body === null || Array.isArray(req.body)) return fail2(400, "invalid_link");
|
|
811
856
|
const body = req.body;
|
|
812
|
-
if (!Object.hasOwn(body, "link") || Object.keys(body).some((
|
|
813
|
-
if (typeof body.link !== "string" || !parseChannelLink(body.link)) return
|
|
814
|
-
if (Object.hasOwn(body, "sessionId") && !validAgentSessionId(body.sessionId)) return
|
|
815
|
-
if (Object.hasOwn(body, "rejoinSecret") && !validAgentRejoinSecret(body.rejoinSecret)) return
|
|
857
|
+
if (!Object.hasOwn(body, "link") || Object.keys(body).some((key2) => !["link", "harness", "label", "sessionId", "rejoinSecret"].includes(key2))) return fail2(400, "invalid_link");
|
|
858
|
+
if (typeof body.link !== "string" || !parseChannelLink(body.link)) return fail2(400, "invalid_link");
|
|
859
|
+
if (Object.hasOwn(body, "sessionId") && !validAgentSessionId(body.sessionId)) return fail2(400, "invalid_link");
|
|
860
|
+
if (Object.hasOwn(body, "rejoinSecret") && !validAgentRejoinSecret(body.rejoinSecret)) return fail2(400, "invalid_link");
|
|
816
861
|
const harness = body.harness;
|
|
817
|
-
if (!HARNESSES.includes(harness)) return
|
|
862
|
+
if (!HARNESSES.includes(harness)) return fail2(400, "invalid_harness");
|
|
818
863
|
const url = new URL(body.link);
|
|
819
|
-
if (url.origin !== ctx.origin && url.origin !== ctx.origin.replace("://127.0.0.1:", "://localhost:")) return
|
|
864
|
+
if (url.origin !== ctx.origin && url.origin !== ctx.origin.replace("://127.0.0.1:", "://localhost:")) return fail2(404, "link_unavailable");
|
|
820
865
|
const token = url.pathname.slice("/join/".length);
|
|
821
|
-
if (!/^[A-Za-z0-9_-]{43}$/u.test(token)) return
|
|
866
|
+
if (!/^[A-Za-z0-9_-]{43}$/u.test(token)) return fail2(404, "link_unavailable");
|
|
822
867
|
const username = ctx.store.owner().username;
|
|
823
|
-
if (!checkName(defaultAgentName(username, harness, 2), "agent").ok) return
|
|
868
|
+
if (!checkName(defaultAgentName(username, harness, 2), "agent").ok) return fail2(503, "unavailable");
|
|
824
869
|
return await queue(async () => {
|
|
825
870
|
const link = await ctx.store.consumeLink(token);
|
|
826
|
-
if (!link || !ctx.store.hasChannel(link.roomId)) return
|
|
871
|
+
if (!link || !ctx.store.hasChannel(link.roomId)) return fail2(404, "link_unavailable");
|
|
827
872
|
const roomId = link.roomId;
|
|
828
873
|
const sessionKey = body.sessionId === void 0 || body.rejoinSecret === void 0 ? void 0 : sha256hex(JSON.stringify([harness, body.sessionId, body.rejoinSecret]));
|
|
829
874
|
const previous = sessionKey ? ctx.store.memberForSession(roomId, sessionKey) : void 0;
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
while (taken.has(defaultAgentName(username, harness, n).toLowerCase())) n++;
|
|
833
|
-
const checked = checkName(defaultAgentName(username, harness, n), "agent");
|
|
834
|
-
if (!checked.ok) return fail(503, "unavailable");
|
|
875
|
+
const checked = checkName(freeAgentName(username, harness, ctx.store.members(roomId).map((member) => member.displayName)), "agent");
|
|
876
|
+
if (!checked.ok) return fail2(503, "unavailable");
|
|
835
877
|
let userId = previous?.userId ?? newLocalAgentUserId(ctx.random(4));
|
|
836
878
|
for (let attempt = 0; attempt < 5 && !previous && ctx.store.channelOfMember(userId) !== void 0; attempt++) userId = newLocalAgentUserId(ctx.random(4));
|
|
837
|
-
if (!previous && ctx.store.channelOfMember(userId) !== void 0) return
|
|
879
|
+
if (!previous && ctx.store.channelOfMember(userId) !== void 0) return fail2(503, "unavailable");
|
|
838
880
|
const deviceId = "KH_LOCAL_" + userId.slice("@agent-".length, -":local".length);
|
|
839
881
|
const accessToken = b64(ctx.random(LOCAL_TOKEN_BYTES));
|
|
840
882
|
await ctx.store.append(roomId, {
|
|
@@ -865,13 +907,13 @@ function agentJoinRoutes() {
|
|
|
865
907
|
return { status: 201, json: result };
|
|
866
908
|
});
|
|
867
909
|
} catch {
|
|
868
|
-
return
|
|
910
|
+
return fail2(503, "unavailable");
|
|
869
911
|
}
|
|
870
912
|
}
|
|
871
913
|
async function poll(req, _params, ctx) {
|
|
872
914
|
prune(ctx);
|
|
873
915
|
const entry = authenticate(req, ctx);
|
|
874
|
-
if (!entry) return
|
|
916
|
+
if (!entry) return fail2(404, "not_found");
|
|
875
917
|
if (entry.state === "confirmed" && ctx.now() >= entry.expiresAt) {
|
|
876
918
|
ctx.joins.delete(entry.joinId);
|
|
877
919
|
return { status: 200, json: { state: "expired" } };
|
|
@@ -883,8 +925,8 @@ function agentJoinRoutes() {
|
|
|
883
925
|
async function ready(req, _params, ctx) {
|
|
884
926
|
prune(ctx);
|
|
885
927
|
const entry = authenticate(req, ctx);
|
|
886
|
-
if (!entry) return
|
|
887
|
-
if (entry.state === "confirmed") return
|
|
928
|
+
if (!entry) return fail2(404, "not_found");
|
|
929
|
+
if (entry.state === "confirmed") return fail2(409, "not_confirmed");
|
|
888
930
|
entry.state = "ready";
|
|
889
931
|
return { status: 204 };
|
|
890
932
|
}
|
|
@@ -896,7 +938,7 @@ function agentJoinRoutes() {
|
|
|
896
938
|
}
|
|
897
939
|
|
|
898
940
|
// src/local/routes/rooms.ts
|
|
899
|
-
var
|
|
941
|
+
var fail3 = (status, error2) => ({ status, json: { error: error2 } });
|
|
900
942
|
function serial2() {
|
|
901
943
|
let tail = Promise.resolve();
|
|
902
944
|
return (fn) => {
|
|
@@ -909,18 +951,18 @@ function isPlainObject(value) {
|
|
|
909
951
|
return typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
910
952
|
}
|
|
911
953
|
function resolve(req, params, ctx, need) {
|
|
912
|
-
if (req.auth.kind === "none") return
|
|
954
|
+
if (req.auth.kind === "none") return fail3(401, "unauthorized");
|
|
913
955
|
const userId = req.auth.kind === "owner" ? LOCAL_OWNER_USER_ID : req.auth.userId;
|
|
914
956
|
let roomId;
|
|
915
957
|
try {
|
|
916
958
|
roomId = decodeURIComponent(params[0]);
|
|
917
959
|
} catch {
|
|
918
|
-
return
|
|
960
|
+
return fail3(404, "not_found");
|
|
919
961
|
}
|
|
920
|
-
if (!ctx.store.hasChannel(roomId)) return
|
|
921
|
-
if (req.auth.kind === "agent" && req.auth.roomId !== roomId) return
|
|
962
|
+
if (!ctx.store.hasChannel(roomId)) return fail3(404, "not_found");
|
|
963
|
+
if (req.auth.kind === "agent" && req.auth.roomId !== roomId) return fail3(403, "not_member");
|
|
922
964
|
const member = ctx.store.member(roomId, userId);
|
|
923
|
-
if (!member || need === "present" && member.membership === "leave" || need === "joined" && member.membership !== "join") return
|
|
965
|
+
if (!member || need === "present" && member.membership === "leave" || need === "joined" && member.membership !== "join") return fail3(403, "not_member");
|
|
924
966
|
return { userId, kind: req.auth.kind, roomId, member };
|
|
925
967
|
}
|
|
926
968
|
var announcementTxn = (event) => `local.join.${event.eventId.slice(1)}`;
|
|
@@ -972,7 +1014,7 @@ function roomRoutes() {
|
|
|
972
1014
|
const caller = resolve(req, params, ctx, need);
|
|
973
1015
|
return "status" in caller ? caller : await handle(req, params, ctx, caller);
|
|
974
1016
|
} catch {
|
|
975
|
-
return
|
|
1017
|
+
return fail3(503, "unavailable");
|
|
976
1018
|
}
|
|
977
1019
|
}
|
|
978
1020
|
});
|
|
@@ -989,7 +1031,7 @@ function roomRoutes() {
|
|
|
989
1031
|
}
|
|
990
1032
|
})),
|
|
991
1033
|
route("POST", /^\/api\/local\/rooms\/([^/]+)\/join$/u, "present", async (req, params, ctx) => {
|
|
992
|
-
if (req.body !== void 0 && (!isPlainObject(req.body) || Object.keys(req.body).length > 0)) return
|
|
1034
|
+
if (req.body !== void 0 && (!isPlainObject(req.body) || Object.keys(req.body).length > 0)) return fail3(400, "invalid_request");
|
|
993
1035
|
return enqueue(async () => {
|
|
994
1036
|
const caller = resolve(req, params, ctx, "present");
|
|
995
1037
|
if ("status" in caller) return caller;
|
|
@@ -1015,13 +1057,13 @@ function roomRoutes() {
|
|
|
1015
1057
|
txnId: announcementTxn(joined)
|
|
1016
1058
|
});
|
|
1017
1059
|
}
|
|
1018
|
-
return event ? { status: 200, json: { seq: event.seq, ts: event.ts } } :
|
|
1060
|
+
return event ? { status: 200, json: { seq: event.seq, ts: event.ts } } : fail3(503, "unavailable");
|
|
1019
1061
|
});
|
|
1020
1062
|
}),
|
|
1021
1063
|
route("GET", /^\/api\/local\/rooms\/([^/]+)\/events$/u, "joined", async (req, params, ctx, caller) => {
|
|
1022
1064
|
const rawAfter = req.query.get("after") ?? "0";
|
|
1023
1065
|
const rawWait = req.query.get("wait") ?? "0";
|
|
1024
|
-
if (!/^\d{1,15}$/u.test(rawAfter) || !/^\d{1,2}$/u.test(rawWait) || Number(rawWait) > LOCAL_LONG_POLL_MAX_S) return
|
|
1066
|
+
if (!/^\d{1,15}$/u.test(rawAfter) || !/^\d{1,2}$/u.test(rawWait) || Number(rawWait) > LOCAL_LONG_POLL_MAX_S) return fail3(400, "invalid_request");
|
|
1025
1067
|
const after = Number(rawAfter);
|
|
1026
1068
|
const wait = Number(rawWait);
|
|
1027
1069
|
if (req.signal.aborted) return { status: 200, json: { events: [], next: after } };
|
|
@@ -1043,7 +1085,7 @@ function roomRoutes() {
|
|
|
1043
1085
|
route("GET", /^\/api\/local\/rooms\/([^/]+)\/messages$/u, "joined", async (req, _params, ctx, caller) => {
|
|
1044
1086
|
const before = req.query.get("before");
|
|
1045
1087
|
const limit = req.query.get("limit") ?? "50";
|
|
1046
|
-
if (before !== null && !/^\$[A-Za-z0-9_-]{1,128}$/u.test(before) || !/^\d{1,3}$/u.test(limit) || Number(limit) < 1 || Number(limit) > 100) return
|
|
1088
|
+
if (before !== null && !/^\$[A-Za-z0-9_-]{1,128}$/u.test(before) || !/^\d{1,3}$/u.test(limit) || Number(limit) < 1 || Number(limit) > 100) return fail3(400, "invalid_request");
|
|
1047
1089
|
return { status: 200, json: ctx.store.history(caller.roomId, before ?? void 0, Number(limit)) };
|
|
1048
1090
|
}),
|
|
1049
1091
|
route("POST", /^\/api\/local\/rooms\/([^/]+)\/send$/u, "joined", async (req, params, ctx) => {
|
|
@@ -1051,16 +1093,16 @@ function roomRoutes() {
|
|
|
1051
1093
|
const r = object(req.body, "", ["txnId", "type", "content"]);
|
|
1052
1094
|
return { txnId: r.field("txnId"), type: r.field("type"), content: r.field("content") };
|
|
1053
1095
|
});
|
|
1054
|
-
if (!decoded.ok) return
|
|
1096
|
+
if (!decoded.ok) return fail3(400, "invalid_request");
|
|
1055
1097
|
const { txnId, type, content } = decoded.value;
|
|
1056
|
-
if (typeof txnId !== "string" || !/^[A-Za-z0-9._-]{1,64}$/u.test(txnId) || type !== "m.room.message" && type !== "com.khala.event.v1" || !isPlainObject(content)) return
|
|
1057
|
-
if (utf8Length(JSON.stringify(content)) > 32768) return
|
|
1098
|
+
if (typeof txnId !== "string" || !/^[A-Za-z0-9._-]{1,64}$/u.test(txnId) || type !== "m.room.message" && type !== "com.khala.event.v1" || !isPlainObject(content)) return fail3(400, "invalid_request");
|
|
1099
|
+
if (utf8Length(JSON.stringify(content)) > 32768) return fail3(413, "payload_too_large");
|
|
1058
1100
|
let stored = content;
|
|
1059
1101
|
if (type === "m.room.message") {
|
|
1060
|
-
if (content["msgtype"] !== "m.text" && content["msgtype"] !== "m.notice" || typeof content["body"] !== "string" || content["body"].length < 1 || content["body"].length > 8e3) return
|
|
1102
|
+
if (content["msgtype"] !== "m.text" && content["msgtype"] !== "m.notice" || typeof content["body"] !== "string" || content["body"].length < 1 || content["body"].length > 8e3) return fail3(400, "invalid_request");
|
|
1061
1103
|
} else {
|
|
1062
1104
|
const event = decodeChannelEvent(content);
|
|
1063
|
-
if (!event.ok) return
|
|
1105
|
+
if (!event.ok) return fail3(400, "invalid_request");
|
|
1064
1106
|
stored = { ...event.value };
|
|
1065
1107
|
}
|
|
1066
1108
|
return enqueue(async () => {
|
|
@@ -1079,12 +1121,12 @@ function roomRoutes() {
|
|
|
1079
1121
|
try {
|
|
1080
1122
|
target = decodeURIComponent(params[1]);
|
|
1081
1123
|
} catch {
|
|
1082
|
-
return
|
|
1124
|
+
return fail3(400, "invalid_request");
|
|
1083
1125
|
}
|
|
1084
|
-
if (caller.kind !== "agent" || target !== caller.userId) return
|
|
1126
|
+
if (caller.kind !== "agent" || target !== caller.userId) return fail3(409, "conflict");
|
|
1085
1127
|
const body = decodeWith(() => object(req.body, "", ["listeningMode"]).field("listeningMode"));
|
|
1086
1128
|
const mode = decodeListeningMode(body.ok ? body.value : void 0);
|
|
1087
|
-
if (!body.ok || !mode.ok) return
|
|
1129
|
+
if (!body.ok || !mode.ok) return fail3(400, "invalid_request");
|
|
1088
1130
|
return enqueue(async () => {
|
|
1089
1131
|
const current = resolve(req, params, ctx, "joined");
|
|
1090
1132
|
if ("status" in current) return current;
|
|
@@ -1102,7 +1144,7 @@ function roomRoutes() {
|
|
|
1102
1144
|
// src/local/routes/owner.ts
|
|
1103
1145
|
var ID = /^[A-Za-z0-9._-]{1,64}$/u;
|
|
1104
1146
|
var USER = /^@[^\s:]+:\S+$/u;
|
|
1105
|
-
var
|
|
1147
|
+
var fail4 = (status, error2) => ({ status, json: { error: error2 } });
|
|
1106
1148
|
function serial3() {
|
|
1107
1149
|
let tail = Promise.resolve();
|
|
1108
1150
|
return (work) => {
|
|
@@ -1112,8 +1154,8 @@ function serial3() {
|
|
|
1112
1154
|
};
|
|
1113
1155
|
}
|
|
1114
1156
|
function owner(req, adminOnly) {
|
|
1115
|
-
if (req.auth.kind === "none") return
|
|
1116
|
-
if (req.auth.kind !== "owner" || adminOnly && req.auth.via !== "admin") return
|
|
1157
|
+
if (req.auth.kind === "none") return fail4(401, "unauthorized");
|
|
1158
|
+
if (req.auth.kind !== "owner" || adminOnly && req.auth.via !== "admin") return fail4(403, "forbidden");
|
|
1117
1159
|
return null;
|
|
1118
1160
|
}
|
|
1119
1161
|
function plainObject(value) {
|
|
@@ -1157,7 +1199,7 @@ function ownerRoutes(options = {}) {
|
|
|
1157
1199
|
}
|
|
1158
1200
|
return await (options2.serial ? queue(() => handler(req, params, ctx)) : handler(req, params, ctx));
|
|
1159
1201
|
} catch {
|
|
1160
|
-
return
|
|
1202
|
+
return fail4(503, "unavailable");
|
|
1161
1203
|
}
|
|
1162
1204
|
} };
|
|
1163
1205
|
}
|
|
@@ -1165,27 +1207,27 @@ function ownerRoutes(options = {}) {
|
|
|
1165
1207
|
route("GET", /^\/api\/local\/channels$/u, async (req, _params, { store }) => {
|
|
1166
1208
|
const since = req.query.get("since");
|
|
1167
1209
|
const wait = req.query.get("wait") ?? "0";
|
|
1168
|
-
if (since !== null && !/^\d{1,15}$/u.test(since) || !/^\d{1,2}$/u.test(wait) || Number(wait) > LOCAL_LONG_POLL_MAX_S) return
|
|
1210
|
+
if (since !== null && !/^\d{1,15}$/u.test(since) || !/^\d{1,2}$/u.test(wait) || Number(wait) > LOCAL_LONG_POLL_MAX_S) return fail4(400, "invalid_request");
|
|
1169
1211
|
if (since !== null && Number(since) === store.revision() && Number(wait) > 0) await store.waitForRevision(Number(since), Number(wait) * 1e3, req.signal);
|
|
1170
1212
|
return { status: 200, json: { revision: store.revision(), channels: store.listChannels() } };
|
|
1171
1213
|
}),
|
|
1172
1214
|
route("GET", /^\/api\/local\/channels\/by-operation\/([^/]+)$/u, async (_req, params, { store }) => {
|
|
1173
1215
|
const id = decodeParam(params[0]);
|
|
1174
|
-
if (id === null || !ID.test(id)) return
|
|
1216
|
+
if (id === null || !ID.test(id)) return fail4(404, "not_found");
|
|
1175
1217
|
const roomId = store.findByOperation(id);
|
|
1176
|
-
return roomId === void 0 ?
|
|
1218
|
+
return roomId === void 0 ? fail4(404, "not_found") : { status: 200, json: { roomId } };
|
|
1177
1219
|
}),
|
|
1178
1220
|
route("GET", /^\/api\/local\/channels\/([^/]+)$/u, async (_req, params, { store }) => {
|
|
1179
1221
|
const roomId = channelParam(params[0], store);
|
|
1180
1222
|
const summary = roomId === null ? void 0 : store.channelSummary(roomId);
|
|
1181
|
-
return summary === void 0 ?
|
|
1223
|
+
return summary === void 0 ? fail4(404, "not_found") : { status: 200, json: summary };
|
|
1182
1224
|
}),
|
|
1183
1225
|
route("POST", /^\/api\/local\/channels$/u, async (req, _params, ctx) => {
|
|
1184
1226
|
const body = req.body;
|
|
1185
|
-
if (!plainObject(body) || !Object.hasOwn(body, "name") || Object.keys(body).some((
|
|
1227
|
+
if (!plainObject(body) || !Object.hasOwn(body, "name") || Object.keys(body).some((key2) => key2 !== "name" && key2 !== "operationId") || typeof body.name !== "string") return fail4(400, "invalid_request");
|
|
1186
1228
|
const name = body.name.trim();
|
|
1187
|
-
if ([...name].length < 1 || [...name].length > 64 || /[\u0000-\u001f\u007f]/u.test(name)) return
|
|
1188
|
-
if (Object.hasOwn(body, "operationId") && (typeof body.operationId !== "string" || !ID.test(body.operationId))) return
|
|
1229
|
+
if ([...name].length < 1 || [...name].length > 64 || /[\u0000-\u001f\u007f]/u.test(name)) return fail4(400, "invalid_request");
|
|
1230
|
+
if (Object.hasOwn(body, "operationId") && (typeof body.operationId !== "string" || !ID.test(body.operationId))) return fail4(400, "invalid_request");
|
|
1189
1231
|
const { store } = ctx;
|
|
1190
1232
|
const { roomId } = await store.createChannel(name, body.operationId);
|
|
1191
1233
|
if (store.member(roomId, LOCAL_OWNER_USER_ID)?.membership !== "join") await store.append(roomId, {
|
|
@@ -1200,54 +1242,69 @@ function ownerRoutes(options = {}) {
|
|
|
1200
1242
|
}, { serial: true }),
|
|
1201
1243
|
route("DELETE", /^\/api\/local\/channels\/([^/]+)$/u, async (_req, params, ctx) => {
|
|
1202
1244
|
const roomId = channelParam(params[0], ctx.store);
|
|
1203
|
-
if (roomId === null) return
|
|
1245
|
+
if (roomId === null) return fail4(404, "not_found");
|
|
1204
1246
|
await ctx.store.deleteChannel(roomId);
|
|
1205
1247
|
for (const [id, join] of ctx.joins) if (join.roomId === roomId) ctx.joins.delete(id);
|
|
1206
1248
|
return { status: 204 };
|
|
1207
1249
|
}, { serial: true }),
|
|
1208
1250
|
route("POST", /^\/api\/local\/channels\/([^/]+)\/links$/u, async (_req, params, ctx) => {
|
|
1209
1251
|
const roomId = channelParam(params[0], ctx.store);
|
|
1210
|
-
if (roomId === null) return
|
|
1252
|
+
if (roomId === null) return fail4(404, "not_found");
|
|
1211
1253
|
const link = await ctx.store.mintLink(roomId, "join");
|
|
1212
1254
|
return { status: 200, json: { shareLink: `${ctx.origin}/join/${link.token}`, expiresAt: link.expiresAt } };
|
|
1213
1255
|
}, { serial: true }),
|
|
1214
1256
|
route("POST", /^\/api\/local\/channels\/([^/]+)\/mode$/u, async (req, params, { store }) => {
|
|
1215
1257
|
const roomId = channelParam(params[0], store);
|
|
1216
|
-
if (roomId === null) return
|
|
1258
|
+
if (roomId === null) return fail4(404, "not_found");
|
|
1217
1259
|
const body = decodeWith(() => {
|
|
1218
1260
|
const reader = object(req.body, "", ["agent", "mode", "txnId"]);
|
|
1219
1261
|
return { agent: reader.field("agent"), mode: reader.field("mode"), txnId: reader.field("txnId") };
|
|
1220
1262
|
});
|
|
1221
|
-
if (!body.ok || typeof body.value.txnId !== "string" || !ID.test(body.value.txnId)) return
|
|
1263
|
+
if (!body.ok || typeof body.value.txnId !== "string" || !ID.test(body.value.txnId)) return fail4(400, "invalid_request");
|
|
1222
1264
|
const command = decodeListeningModeCommand({ v: 1, agent: body.value.agent, mode: body.value.mode });
|
|
1223
|
-
if (!command.ok) return
|
|
1265
|
+
if (!command.ok) return fail4(400, "invalid_request");
|
|
1224
1266
|
const member = store.member(roomId, command.value.agent);
|
|
1225
|
-
if (!member || member.membership === "leave" || member.kind !== "agent") return
|
|
1267
|
+
if (!member || member.membership === "leave" || member.kind !== "agent") return fail4(404, "not_found");
|
|
1226
1268
|
const event = await store.append(roomId, { type: LISTENING_MODE_COMMAND_TYPE, sender: LOCAL_OWNER_USER_ID, content: command.value, txnId: body.value.txnId });
|
|
1227
1269
|
return { status: 200, json: { eventId: event.eventId } };
|
|
1228
1270
|
}, { serial: true }),
|
|
1229
1271
|
route("POST", /^\/api\/local\/agents\/([^/]+)\/name$/u, async (req, params, { store }) => {
|
|
1230
1272
|
const userId = decodeParam(params[0]);
|
|
1231
|
-
if (userId === null || !USER.test(userId)) return
|
|
1273
|
+
if (userId === null || !USER.test(userId)) return fail4(400, "invalid_request");
|
|
1232
1274
|
const body = decodeWith(() => object(req.body, "", ["name"]).field("name"));
|
|
1233
|
-
if (!body.ok || typeof body.value !== "string") return
|
|
1275
|
+
if (!body.ok || typeof body.value !== "string") return fail4(400, "invalid_request");
|
|
1234
1276
|
const checked = checkName(body.value, "agent");
|
|
1235
1277
|
if (!checked.ok) return { status: 400, json: { error: "invalid_name", reason: checked.error } };
|
|
1236
1278
|
const roomId = store.channelOfMember(userId);
|
|
1237
|
-
if (roomId === void 0) return
|
|
1279
|
+
if (roomId === void 0) return fail4(404, "not_found");
|
|
1238
1280
|
const member = store.member(roomId, userId);
|
|
1239
|
-
if (!member || member.membership === "leave" || member.kind !== "agent") return
|
|
1240
|
-
if (store.members(roomId).some((other) => other.userId !== userId && other.displayName.toLowerCase() === checked.name.toLowerCase())) return
|
|
1281
|
+
if (!member || member.membership === "leave" || member.kind !== "agent") return fail4(404, "not_found");
|
|
1282
|
+
if (store.members(roomId).some((other) => other.userId !== userId && other.displayName.toLowerCase() === checked.name.toLowerCase())) return fail4(409, "name_taken");
|
|
1241
1283
|
if (member.displayName !== checked.name) await store.append(roomId, { type: "m.room.member", sender: LOCAL_OWNER_USER_ID, content: memberContent2(member, member.membership, { displayname: checked.name }) });
|
|
1242
1284
|
return { status: 200, json: { matrixUserId: userId, name: checked.name } };
|
|
1243
1285
|
}, { serial: true }),
|
|
1286
|
+
// The owner's own name in one channel. Only the owner's session reaches it; agents get 403 above,
|
|
1287
|
+
// and there is no subject field, so no one can rename another member here.
|
|
1288
|
+
route("POST", /^\/api\/local\/channels\/([^/]+)\/name$/u, async (req, params, { store }) => {
|
|
1289
|
+
const roomId = channelParam(params[0], store);
|
|
1290
|
+
if (roomId === null) return fail4(404, "not_found");
|
|
1291
|
+
const body = decodeWith(() => object(req.body, "", ["name"]).field("name"));
|
|
1292
|
+
if (!body.ok || typeof body.value !== "string") return fail4(400, "invalid_request");
|
|
1293
|
+
const checked = checkName(body.value, "username");
|
|
1294
|
+
if (!checked.ok) return { status: 400, json: { error: "invalid_name", reason: checked.error } };
|
|
1295
|
+
const self = store.member(roomId, LOCAL_OWNER_USER_ID);
|
|
1296
|
+
if (!self || self.membership !== "join") return fail4(404, "not_found");
|
|
1297
|
+
if (store.members(roomId).some((other) => other.userId !== LOCAL_OWNER_USER_ID && other.displayName.toLowerCase() === checked.name.toLowerCase())) return fail4(409, "name_taken");
|
|
1298
|
+
await store.setOwnerChannelName(roomId, checked.name === store.owner().username ? null : checked.name);
|
|
1299
|
+
return { status: 200, json: { name: checked.name } };
|
|
1300
|
+
}, { serial: true }),
|
|
1244
1301
|
route("DELETE", /^\/api\/local\/channels\/([^/]+)\/members\/([^/]+)$/u, async (_req, params, ctx) => {
|
|
1245
1302
|
const roomId = channelParam(params[0], ctx.store);
|
|
1246
|
-
if (roomId === null) return
|
|
1303
|
+
if (roomId === null) return fail4(404, "not_found");
|
|
1247
1304
|
const userId = decodeParam(params[1]);
|
|
1248
|
-
if (userId === null || !USER.test(userId) || userId === LOCAL_OWNER_USER_ID) return
|
|
1305
|
+
if (userId === null || !USER.test(userId) || userId === LOCAL_OWNER_USER_ID) return fail4(400, "invalid_request");
|
|
1249
1306
|
const member = ctx.store.member(roomId, userId);
|
|
1250
|
-
if (!member || member.membership === "leave") return
|
|
1307
|
+
if (!member || member.membership === "leave") return fail4(404, "not_found");
|
|
1251
1308
|
await ctx.store.append(roomId, { type: "m.room.member", sender: LOCAL_OWNER_USER_ID, content: memberContent2(member, "leave") });
|
|
1252
1309
|
await announce(ctx.store, roomId, `${member.displayName} left`);
|
|
1253
1310
|
for (const [id, join] of ctx.joins) if (join.credentials.userId === userId) ctx.joins.delete(id);
|
|
@@ -1255,15 +1312,15 @@ function ownerRoutes(options = {}) {
|
|
|
1255
1312
|
}, { serial: true }),
|
|
1256
1313
|
route("POST", /^\/api\/local\/open$/u, async (req, _params, ctx) => {
|
|
1257
1314
|
const body = req.body === void 0 ? {} : req.body;
|
|
1258
|
-
if (!plainObject(body) || Object.keys(body).some((
|
|
1315
|
+
if (!plainObject(body) || Object.keys(body).some((key2) => key2 !== "roomId") || Object.hasOwn(body, "roomId") && typeof body.roomId !== "string") return fail4(400, "invalid_request");
|
|
1259
1316
|
const roomId = body.roomId;
|
|
1260
|
-
if (roomId !== void 0 && !ctx.store.hasChannel(roomId)) return
|
|
1317
|
+
if (roomId !== void 0 && !ctx.store.hasChannel(roomId)) return fail4(404, "not_found");
|
|
1261
1318
|
const link = ctx.mintOpenToken(roomId);
|
|
1262
1319
|
return { status: 200, json: { openUrl: `${ctx.origin}/open/${link.token}`, expiresAt: link.expiresAt } };
|
|
1263
1320
|
}, { adminOnly: true }),
|
|
1264
1321
|
route("GET", /^\/open\/([A-Za-z0-9_-]{43})$/u, async (_req, params, ctx) => {
|
|
1265
1322
|
const opened = ctx.consumeOpenToken(params[0]);
|
|
1266
|
-
if (opened === null) return
|
|
1323
|
+
if (opened === null) return fail4(404, "link_unavailable");
|
|
1267
1324
|
const sid = ctx.createOwnerSession();
|
|
1268
1325
|
const location = opened.roomId && ctx.store.hasChannel(opened.roomId) ? `/channels/${encodeURIComponent(opened.roomId)}` : "/conversations";
|
|
1269
1326
|
return { status: 302, location, headers: { "set-cookie": `khala_local_owner=${sid}; HttpOnly; SameSite=Strict; Path=/`, "cache-control": "no-store", "referrer-policy": "no-referrer" } };
|
|
@@ -1276,8 +1333,8 @@ function ownerRoutes(options = {}) {
|
|
|
1276
1333
|
}
|
|
1277
1334
|
|
|
1278
1335
|
// src/local/routes/profile.ts
|
|
1279
|
-
var
|
|
1280
|
-
var field = (body,
|
|
1336
|
+
var fail5 = (status, error2) => ({ status, json: { error: error2 } });
|
|
1337
|
+
var field = (body, key2) => decodeWith(() => object(body, "", [key2]).field(key2));
|
|
1281
1338
|
var iso = (ctx) => new Date(ctx.now()).toISOString();
|
|
1282
1339
|
async function cascade(store, previous, next) {
|
|
1283
1340
|
for (const { roomId } of store.listChannels()) {
|
|
@@ -1287,12 +1344,13 @@ async function cascade(store, previous, next) {
|
|
|
1287
1344
|
const owner2 = members.find((m) => m.userId === LOCAL_OWNER_USER_ID);
|
|
1288
1345
|
if (owner2 && previous !== next) {
|
|
1289
1346
|
try {
|
|
1290
|
-
await store.
|
|
1347
|
+
if (store.ownerChannelName(roomId) !== void 0) await store.setOwnerChannelName(roomId, null);
|
|
1348
|
+
else await store.append(roomId, {
|
|
1291
1349
|
type: "m.room.member",
|
|
1292
1350
|
sender: LOCAL_OWNER_USER_ID,
|
|
1293
1351
|
content: { user: LOCAL_OWNER_USER_ID, membership: owner2.membership, displayname: next, kind: "human" }
|
|
1294
1352
|
});
|
|
1295
|
-
taken.delete(
|
|
1353
|
+
taken.delete(owner2.displayName.toLowerCase());
|
|
1296
1354
|
taken.add(next.toLowerCase());
|
|
1297
1355
|
} catch {
|
|
1298
1356
|
}
|
|
@@ -1344,12 +1402,12 @@ function profileRoutes(options = {}) {
|
|
|
1344
1402
|
method,
|
|
1345
1403
|
pattern,
|
|
1346
1404
|
async handle(req, _params, ctx) {
|
|
1347
|
-
if (req.auth.kind === "none") return
|
|
1348
|
-
if (req.auth.kind === "agent") return
|
|
1405
|
+
if (req.auth.kind === "none") return fail5(401, "unauthorized");
|
|
1406
|
+
if (req.auth.kind === "agent") return fail5(403, "forbidden");
|
|
1349
1407
|
try {
|
|
1350
1408
|
return await queue(() => run(req, ctx));
|
|
1351
1409
|
} catch {
|
|
1352
|
-
return
|
|
1410
|
+
return fail5(503, "unavailable");
|
|
1353
1411
|
}
|
|
1354
1412
|
}
|
|
1355
1413
|
});
|
|
@@ -1368,7 +1426,7 @@ function profileRoutes(options = {}) {
|
|
|
1368
1426
|
}),
|
|
1369
1427
|
route("POST", /^\/api\/local\/profile\/username$/u, async (req, ctx) => {
|
|
1370
1428
|
const parsed = field(req.body, "username");
|
|
1371
|
-
if (!parsed.ok) return
|
|
1429
|
+
if (!parsed.ok) return fail5(400, "invalid_request");
|
|
1372
1430
|
const checked = checkName(parsed.value, "username");
|
|
1373
1431
|
if (!checked.ok) return { status: 400, json: { error: "invalid_username", reason: checked.error } };
|
|
1374
1432
|
const username = checked.name;
|
|
@@ -1384,8 +1442,8 @@ function profileRoutes(options = {}) {
|
|
|
1384
1442
|
}),
|
|
1385
1443
|
route("POST", /^\/api\/local\/profile\/color$/u, async (req, ctx) => {
|
|
1386
1444
|
const parsed = field(req.body, "color");
|
|
1387
|
-
if (!parsed.ok) return
|
|
1388
|
-
if (!isHumanColorId(parsed.value)) return
|
|
1445
|
+
if (!parsed.ok) return fail5(400, "invalid_request");
|
|
1446
|
+
if (!isHumanColorId(parsed.value)) return fail5(400, "invalid_color");
|
|
1389
1447
|
const color = parsed.value;
|
|
1390
1448
|
const p = ctx.store.owner();
|
|
1391
1449
|
if (p.color !== color) await ctx.store.setOwner({ ...p, color, updatedAt: iso(ctx) });
|
|
@@ -1393,9 +1451,9 @@ function profileRoutes(options = {}) {
|
|
|
1393
1451
|
}),
|
|
1394
1452
|
route("POST", /^\/api\/local\/profile\/initials$/u, async (req, ctx) => {
|
|
1395
1453
|
const parsed = field(req.body, "initials");
|
|
1396
|
-
if (!parsed.ok) return
|
|
1454
|
+
if (!parsed.ok) return fail5(400, "invalid_request");
|
|
1397
1455
|
const initials = parsed.value === null ? null : normalizeInitials(parsed.value);
|
|
1398
|
-
if (parsed.value !== null && initials === null) return
|
|
1456
|
+
if (parsed.value !== null && initials === null) return fail5(400, "invalid_initials");
|
|
1399
1457
|
const p = ctx.store.owner();
|
|
1400
1458
|
if (p.initials !== initials) await ctx.store.setOwner({ ...p, initials, updatedAt: iso(ctx) });
|
|
1401
1459
|
return { status: 200, json: { initials } };
|
|
@@ -1468,9 +1526,9 @@ async function runHelper(options = {}) {
|
|
|
1468
1526
|
return { token, expiresAt: new Date(expiresAt).toISOString() };
|
|
1469
1527
|
},
|
|
1470
1528
|
consumeOpenToken(token) {
|
|
1471
|
-
const
|
|
1472
|
-
const entry = openTokens.get(
|
|
1473
|
-
openTokens.delete(
|
|
1529
|
+
const key2 = hash(token).toString("hex");
|
|
1530
|
+
const entry = openTokens.get(key2);
|
|
1531
|
+
openTokens.delete(key2);
|
|
1474
1532
|
return !entry || entry.expiresAt <= now() ? null : entry.roomId === void 0 ? {} : { roomId: entry.roomId };
|
|
1475
1533
|
},
|
|
1476
1534
|
createOwnerSession() {
|