khala-cli 0.4.2 → 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.
Files changed (33) hide show
  1. package/dist/{chunk-K4MDSFHU.mjs → chunk-6OADQWV7.mjs} +14 -10
  2. package/dist/{chunk-MHZWR4A3.mjs → chunk-7CVU5OZ7.mjs} +1 -1
  3. package/dist/{chunk-7MQAMABY.mjs → chunk-AK74S7EN.mjs} +4 -4
  4. package/dist/chunk-AUJEUKVM.mjs +121 -0
  5. package/dist/chunk-DC3OX6F4.mjs +52 -0
  6. package/dist/chunk-ENPBK5W2.mjs +187 -0
  7. package/dist/chunk-FA6Z65DW.mjs +19 -0
  8. package/dist/{chunk-JRZZVTQ4.mjs → chunk-FUU775QM.mjs} +81 -10
  9. package/dist/chunk-GE3RXO2X.mjs +26 -0
  10. package/dist/{chunk-YP45XTOH.mjs → chunk-LHAMBQK7.mjs} +5 -5
  11. package/dist/{chunk-T6RZE5IQ.mjs → chunk-LN3G2LJV.mjs} +10 -7
  12. package/dist/{chunk-NUS6M27N.mjs → chunk-LQ6KNUNN.mjs} +64 -51
  13. package/dist/{chunk-CDHHFC6C.mjs → chunk-MU4FUENP.mjs} +3 -2
  14. package/dist/{chunk-2OO725IZ.mjs → chunk-MZS7XFMC.mjs} +3 -3
  15. package/dist/{chunk-U5ZMOPUK.mjs → chunk-N5VYKQMR.mjs} +5 -46
  16. package/dist/{chunk-BZ53MZEE.mjs → chunk-OS2JLDL7.mjs} +4 -2
  17. package/dist/chunk-P5CP32X2.mjs +170 -0
  18. package/dist/{chunk-46PTKRZC.mjs → chunk-PJ3E3IVH.mjs} +171 -113
  19. package/dist/chunk-SN3STAO2.mjs +190 -0
  20. package/dist/{chunk-B5BFL7OD.mjs → chunk-XEYKJMZL.mjs} +1 -1
  21. package/dist/{chunk-A3LJU4JA.mjs → chunk-ZKDDKUUA.mjs} +1 -1
  22. package/dist/{chunk-RW3DXWNC.mjs → chunk-ZRFXZK72.mjs} +1 -1
  23. package/dist/khala.mjs +12 -10
  24. package/dist/web/assets/apple-touch-icon-Du9V6qit.png +0 -0
  25. package/dist/web/assets/favicon-16x16-D5waNSZD.png +0 -0
  26. package/dist/web/assets/favicon-32x32-B9Ubw1Dx.png +0 -0
  27. package/dist/web/assets/favicon-zCD0pKtT.ico +0 -0
  28. package/dist/web/assets/local-Bk8hMIT-.js +9 -0
  29. package/dist/web/assets/{local-DHYvcQG0.css → local-vdvavyq0.css} +1 -1
  30. package/dist/web/index.html +7 -2
  31. package/package.json +1 -1
  32. package/dist/chunk-N6LMHR7P.mjs +0 -455
  33. package/dist/web/assets/local-CVA9799w.js +0 -9
@@ -3,14 +3,14 @@ import {
3
3
  ensureHelper,
4
4
  helperPaths,
5
5
  readHelperFile
6
- } from "./chunk-YP45XTOH.mjs";
6
+ } from "./chunk-LHAMBQK7.mjs";
7
7
  import {
8
8
  parseChannelLink,
9
9
  resolveLocalOwnerName
10
- } from "./chunk-2OO725IZ.mjs";
10
+ } from "./chunk-MZS7XFMC.mjs";
11
11
  import {
12
12
  KHALA_AGENT_VERSION
13
- } from "./chunk-MHZWR4A3.mjs";
13
+ } from "./chunk-7CVU5OZ7.mjs";
14
14
  import {
15
15
  LOCAL_AGENT_DEVICE_PREFIX,
16
16
  LOCAL_IDLE_EXIT_MS,
@@ -35,37 +35,41 @@ import {
35
35
  newLocalEventId,
36
36
  newLocalRoomId,
37
37
  normalizeInitials
38
- } from "./chunk-BZ53MZEE.mjs";
38
+ } from "./chunk-OS2JLDL7.mjs";
39
+ import {
40
+ bundle
41
+ } from "./chunk-ZRFXZK72.mjs";
42
+ import "./chunk-5GLTKWJ5.mjs";
43
+ import {
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-B5BFL7OD.mjs";
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-N6LMHR7P.mjs";
65
- import {
66
- bundle
67
- } from "./chunk-RW3DXWNC.mjs";
68
- import "./chunk-A3LJU4JA.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 [key, value] of Object.entries(result.headers ?? {})) {
524
- if (!key.toLowerCase().startsWith("access-control-")) res.setHeader(key, value);
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", fail5);
580
+ req.off("error", fail6);
550
581
  signal.removeEventListener("abort", abort);
551
582
  };
552
- const fail5 = (err) => {
583
+ const fail6 = (err) => {
553
584
  cleanup();
554
585
  reject(err);
555
586
  };
556
- const abort = () => fail5(new Error("client_disconnected"));
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", fail5);
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 fail5 = (err) => {
764
+ const fail6 = (err) => {
734
765
  cancelListen = void 0;
735
766
  reject(err);
736
767
  };
737
768
  cancelListen = () => {
738
- server.off("error", fail5);
769
+ server.off("error", fail6);
739
770
  reject(new Error("server_closed"));
740
771
  };
741
- server.once("error", fail5);
772
+ server.once("error", fail6);
742
773
  server.listen({ host: "127.0.0.1", port: options.port }, () => {
743
- server.off("error", fail5);
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 fail = (status, error2) => ({ status, json: { error: error2 } });
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 fail(400, "invalid_link");
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((key) => !["link", "harness", "label", "sessionId", "rejoinSecret"].includes(key))) return fail(400, "invalid_link");
813
- if (typeof body.link !== "string" || !parseChannelLink(body.link)) return fail(400, "invalid_link");
814
- if (Object.hasOwn(body, "sessionId") && !validAgentSessionId(body.sessionId)) return fail(400, "invalid_link");
815
- if (Object.hasOwn(body, "rejoinSecret") && !validAgentRejoinSecret(body.rejoinSecret)) return fail(400, "invalid_link");
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 fail(400, "invalid_harness");
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 fail(404, "link_unavailable");
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 fail(404, "link_unavailable");
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 fail(503, "unavailable");
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 fail(404, "link_unavailable");
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 taken = new Set(ctx.store.members(roomId).map((member) => member.displayName.toLowerCase()));
831
- let n = 1;
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 fail(503, "unavailable");
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 fail(503, "unavailable");
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 fail(404, "not_found");
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 fail(404, "not_found");
887
- if (entry.state === "confirmed") return fail(409, "not_confirmed");
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 fail2 = (status, error2) => ({ status, json: { error: error2 } });
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 fail2(401, "unauthorized");
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 fail2(404, "not_found");
960
+ return fail3(404, "not_found");
919
961
  }
920
- if (!ctx.store.hasChannel(roomId)) return fail2(404, "not_found");
921
- if (req.auth.kind === "agent" && req.auth.roomId !== roomId) return fail2(403, "not_member");
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 fail2(403, "not_member");
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 fail2(503, "unavailable");
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 fail2(400, "invalid_request");
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 } } : fail2(503, "unavailable");
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 fail2(400, "invalid_request");
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 fail2(400, "invalid_request");
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 fail2(400, "invalid_request");
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 fail2(400, "invalid_request");
1057
- if (utf8Length(JSON.stringify(content)) > 32768) return fail2(413, "payload_too_large");
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 fail2(400, "invalid_request");
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 fail2(400, "invalid_request");
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 fail2(400, "invalid_request");
1124
+ return fail3(400, "invalid_request");
1083
1125
  }
1084
- if (caller.kind !== "agent" || target !== caller.userId) return fail2(409, "conflict");
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 fail2(400, "invalid_request");
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 fail3 = (status, error2) => ({ status, json: { error: error2 } });
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 fail3(401, "unauthorized");
1116
- if (req.auth.kind !== "owner" || adminOnly && req.auth.via !== "admin") return fail3(403, "forbidden");
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 fail3(503, "unavailable");
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 fail3(400, "invalid_request");
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 fail3(404, "not_found");
1216
+ if (id === null || !ID.test(id)) return fail4(404, "not_found");
1175
1217
  const roomId = store.findByOperation(id);
1176
- return roomId === void 0 ? fail3(404, "not_found") : { status: 200, json: { roomId } };
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 ? fail3(404, "not_found") : { status: 200, json: summary };
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((key) => key !== "name" && key !== "operationId") || typeof body.name !== "string") return fail3(400, "invalid_request");
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 fail3(400, "invalid_request");
1188
- if (Object.hasOwn(body, "operationId") && (typeof body.operationId !== "string" || !ID.test(body.operationId))) return fail3(400, "invalid_request");
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 fail3(404, "not_found");
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 fail3(404, "not_found");
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 fail3(404, "not_found");
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 fail3(400, "invalid_request");
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 fail3(400, "invalid_request");
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 fail3(404, "not_found");
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 fail3(400, "invalid_request");
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 fail3(400, "invalid_request");
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 fail3(404, "not_found");
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 fail3(404, "not_found");
1240
- if (store.members(roomId).some((other) => other.userId !== userId && other.displayName.toLowerCase() === checked.name.toLowerCase())) return fail3(409, "name_taken");
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 fail3(404, "not_found");
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 fail3(400, "invalid_request");
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 fail3(404, "not_found");
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((key) => key !== "roomId") || Object.hasOwn(body, "roomId") && typeof body.roomId !== "string") return fail3(400, "invalid_request");
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 fail3(404, "not_found");
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 fail3(404, "link_unavailable");
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 fail4 = (status, error2) => ({ status, json: { error: error2 } });
1280
- var field = (body, key) => decodeWith(() => object(body, "", [key]).field(key));
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.append(roomId, {
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(previous.toLowerCase());
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 fail4(401, "unauthorized");
1348
- if (req.auth.kind === "agent") return fail4(403, "forbidden");
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 fail4(503, "unavailable");
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 fail4(400, "invalid_request");
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 fail4(400, "invalid_request");
1388
- if (!isHumanColorId(parsed.value)) return fail4(400, "invalid_color");
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 fail4(400, "invalid_request");
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 fail4(400, "invalid_initials");
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 key = hash(token).toString("hex");
1472
- const entry = openTokens.get(key);
1473
- openTokens.delete(key);
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() {