khala-cli 0.4.5 → 0.4.7
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-TZKEQIHP.mjs → chunk-25AX7UMF.mjs} +305 -97
- package/dist/{chunk-AUJEUKVM.mjs → chunk-3OFKVYOX.mjs} +1 -1
- package/dist/{chunk-7G2G4E4U.mjs → chunk-4LSCOWWN.mjs} +18 -17
- package/dist/chunk-5RGEO5HN.mjs +142 -0
- package/dist/{chunk-V7M27QLT.mjs → chunk-6YEYXZZB.mjs} +8 -7
- package/dist/{chunk-EXHCVFVH.mjs → chunk-7V2YQIRI.mjs} +11 -5
- package/dist/{chunk-PXKGKVGW.mjs → chunk-APIWKRKG.mjs} +1 -1
- package/dist/{chunk-H46OG22B.mjs → chunk-C5SMOUTO.mjs} +34 -5
- package/dist/{chunk-Y4WVILEJ.mjs → chunk-IG2NU5J6.mjs} +4 -4
- package/dist/chunk-JEI32DEA.mjs +634 -0
- package/dist/{chunk-MBLP7LQJ.mjs → chunk-K6EJSZYO.mjs} +95 -98
- package/dist/{chunk-V4Y26O4R.mjs → chunk-L7L6UNXN.mjs} +85 -36
- package/dist/{chunk-P5CP32X2.mjs → chunk-LNG3ZZBK.mjs} +1 -1
- package/dist/{chunk-IR3VN2ID.mjs → chunk-OAFYPBCP.mjs} +1 -19
- package/dist/{chunk-Y2DRTRBJ.mjs → chunk-ODTSGEID.mjs} +24 -8
- package/dist/{chunk-T7YRVE43.mjs → chunk-OP2HZ7EM.mjs} +8 -30
- package/dist/{chunk-RSDOTTZQ.mjs → chunk-S3FX7VCY.mjs} +30 -12
- package/dist/{chunk-OLZ62X7S.mjs → chunk-SU6362YU.mjs} +1 -1
- package/dist/{chunk-EW22K3JO.mjs → chunk-TOBQUAPD.mjs} +210 -311
- package/dist/{chunk-SN3STAO2.mjs → chunk-U3MKSPRO.mjs} +40 -3
- package/dist/{chunk-6LC66VHK.mjs → chunk-W5HS2PQO.mjs} +12 -13
- package/dist/{chunk-KCJFSVDN.mjs → chunk-WNOIHCCC.mjs} +9 -7
- package/dist/{chunk-SQXDATEC.mjs → chunk-YIHRHD7W.mjs} +1 -1
- package/dist/khala.mjs +10 -10
- package/dist/web/assets/local-BQl2KrE1.js +9 -0
- package/dist/web/assets/local-OwDq47O4.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/chunk-HB3UGIZG.mjs +0 -144
- package/dist/chunk-VUE2DDYY.mjs +0 -34
- package/dist/web/assets/local-C1vz22ps.js +0 -9
- package/dist/web/assets/local-DdjwMERJ.css +0 -1
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
KHALA_AGENT_VERSION
|
|
4
|
+
} from "./chunk-YIHRHD7W.mjs";
|
|
5
|
+
import {
|
|
6
|
+
bundle
|
|
7
|
+
} from "./chunk-SU6362YU.mjs";
|
|
8
|
+
import {
|
|
9
|
+
StateError,
|
|
10
|
+
ensureStateDir,
|
|
11
|
+
readJson,
|
|
12
|
+
stateRoot
|
|
13
|
+
} from "./chunk-WNOIHCCC.mjs";
|
|
2
14
|
import {
|
|
3
15
|
LISTENING_MODES
|
|
4
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-3OFKVYOX.mjs";
|
|
5
17
|
import {
|
|
18
|
+
HARNESS_REGISTRY,
|
|
6
19
|
M1_LABEL_MAX_BYTES,
|
|
7
20
|
array,
|
|
8
21
|
decodeWith,
|
|
9
22
|
displayText,
|
|
10
23
|
elementPath,
|
|
11
24
|
fail,
|
|
25
|
+
harnessInfo,
|
|
12
26
|
identifier,
|
|
13
27
|
literal,
|
|
14
28
|
nullable,
|
|
@@ -23,7 +37,7 @@ import {
|
|
|
23
37
|
utcTimestamp,
|
|
24
38
|
utf8Length,
|
|
25
39
|
version
|
|
26
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-U3MKSPRO.mjs";
|
|
27
41
|
|
|
28
42
|
// src/client.ts
|
|
29
43
|
var KhalaClientError = class extends Error {
|
|
@@ -53,8 +67,8 @@ function fnv1a(value) {
|
|
|
53
67
|
function defaultHumanColor(ownerId) {
|
|
54
68
|
return HUMAN_COLOR_IDS[fnv1a(ownerId) % HUMAN_COLOR_IDS.length];
|
|
55
69
|
}
|
|
56
|
-
function readHumanColorId(input,
|
|
57
|
-
if (!isHumanColorId(input)) return fail(
|
|
70
|
+
function readHumanColorId(input, path2) {
|
|
71
|
+
if (!isHumanColorId(input)) return fail(path2, "invalid_value");
|
|
58
72
|
return input;
|
|
59
73
|
}
|
|
60
74
|
|
|
@@ -69,8 +83,8 @@ function normalizeInitials(input) {
|
|
|
69
83
|
function isCanonicalInitials(value) {
|
|
70
84
|
return typeof value === "string" && normalizeInitials(value) === value;
|
|
71
85
|
}
|
|
72
|
-
function readHumanInitials(input,
|
|
73
|
-
if (!isCanonicalInitials(input)) return fail(
|
|
86
|
+
function readHumanInitials(input, path2) {
|
|
87
|
+
if (!isCanonicalInitials(input)) return fail(path2, "invalid_value");
|
|
74
88
|
return input;
|
|
75
89
|
}
|
|
76
90
|
|
|
@@ -91,12 +105,18 @@ function checkName(input, kind) {
|
|
|
91
105
|
if (reserved.test(name) || kind === "username" && agentSuffix.test(name)) return { ok: false, error: "reserved" };
|
|
92
106
|
return { ok: true, name };
|
|
93
107
|
}
|
|
94
|
-
var
|
|
108
|
+
var escapePattern = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
109
|
+
var newAgentSuffix = new RegExp(`-(?:${HARNESS_REGISTRY.filter((row) => row.modelName !== "Agent").map((row) => escapePattern(row.modelName)).join("|")})(?:-\\d+)?$`, "iu");
|
|
110
|
+
function checkNewUsername(input) {
|
|
111
|
+
const checked = checkName(input, "username");
|
|
112
|
+
if (!checked.ok) return checked;
|
|
113
|
+
return newAgentSuffix.test(checked.name) ? { ok: false, error: "reserved" } : checked;
|
|
114
|
+
}
|
|
95
115
|
function defaultAgentName(username, harness, n = 1) {
|
|
96
|
-
return `${username}-${
|
|
116
|
+
return `${username}-${harnessInfo(harness).modelName}${n === 1 ? "" : `-${n}`}`;
|
|
97
117
|
}
|
|
98
118
|
function isDefaultAgentName(name, username, harness) {
|
|
99
|
-
const escaped = `${username}-${
|
|
119
|
+
const escaped = escapePattern(`${username}-${harnessInfo(harness).modelName}`);
|
|
100
120
|
return new RegExp(`^${escaped}(?:-\\d+)?$`, "iu").test(name);
|
|
101
121
|
}
|
|
102
122
|
|
|
@@ -147,90 +167,90 @@ var localRoomKey = (roomId) => {
|
|
|
147
167
|
var localRoomPath = (roomId, tail) => `/api/local/rooms/${encodeURIComponent(roomId)}/${tail}`;
|
|
148
168
|
var LOCAL_OWNER_ID = "local-owner";
|
|
149
169
|
var LOCAL_OWNER_DEVICE_ID = "KH_LOCAL_OWNER";
|
|
150
|
-
function readLocalRoomId(input,
|
|
151
|
-
const value = readRoomId(input,
|
|
152
|
-
if (!isLocalRoomId(value)) fail(
|
|
170
|
+
function readLocalRoomId(input, path2) {
|
|
171
|
+
const value = readRoomId(input, path2);
|
|
172
|
+
if (!isLocalRoomId(value)) fail(path2, "invalid_value");
|
|
153
173
|
return value;
|
|
154
174
|
}
|
|
155
|
-
function readLocalUserId(input,
|
|
156
|
-
const value = readMatrixUserId(input,
|
|
157
|
-
if (!isLocalUserId(value)) fail(
|
|
175
|
+
function readLocalUserId(input, path2) {
|
|
176
|
+
const value = readMatrixUserId(input, path2);
|
|
177
|
+
if (!isLocalUserId(value)) fail(path2, "invalid_value");
|
|
158
178
|
return value;
|
|
159
179
|
}
|
|
160
|
-
function readLocalEventId(input,
|
|
161
|
-
const value = identifier(input,
|
|
162
|
-
if (!isLocalEventId(value)) fail(
|
|
180
|
+
function readLocalEventId(input, path2) {
|
|
181
|
+
const value = identifier(input, path2);
|
|
182
|
+
if (!isLocalEventId(value)) fail(path2, "invalid_value");
|
|
163
183
|
return value;
|
|
164
184
|
}
|
|
165
|
-
function readLocalTxnId(input,
|
|
166
|
-
const value = identifier(input,
|
|
167
|
-
if (!isLocalTxnId(value)) fail(
|
|
185
|
+
function readLocalTxnId(input, path2) {
|
|
186
|
+
const value = identifier(input, path2);
|
|
187
|
+
if (!isLocalTxnId(value)) fail(path2, "invalid_value");
|
|
168
188
|
return value;
|
|
169
189
|
}
|
|
170
|
-
function readLocalUsername(input,
|
|
171
|
-
const value = identifier(input,
|
|
190
|
+
function readLocalUsername(input, path2) {
|
|
191
|
+
const value = identifier(input, path2);
|
|
172
192
|
const checked = checkName(value, "username");
|
|
173
|
-
if (!checked.ok || checked.name !== value) fail(
|
|
193
|
+
if (!checked.ok || checked.name !== value) fail(path2, "invalid_value");
|
|
174
194
|
return value;
|
|
175
195
|
}
|
|
176
|
-
function readLocalInitials(input,
|
|
177
|
-
return readHumanInitials(input,
|
|
196
|
+
function readLocalInitials(input, path2) {
|
|
197
|
+
return readHumanInitials(input, path2);
|
|
178
198
|
}
|
|
179
|
-
function record(input,
|
|
199
|
+
function record(input, path2, required, optional = []) {
|
|
180
200
|
const present = optional.filter((key) => typeof input === "object" && input !== null && Object.hasOwn(input, key));
|
|
181
|
-
return object(input,
|
|
201
|
+
return object(input, path2, [...required, ...present]);
|
|
182
202
|
}
|
|
183
203
|
function has(input, key) {
|
|
184
204
|
return typeof input === "object" && input !== null && Object.hasOwn(input, key);
|
|
185
205
|
}
|
|
186
|
-
function plainObject(input,
|
|
187
|
-
if (typeof input !== "object" || input === null || Array.isArray(input)) fail(
|
|
206
|
+
function plainObject(input, path2) {
|
|
207
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) fail(path2, "not_object");
|
|
188
208
|
const prototype = Object.getPrototypeOf(input);
|
|
189
|
-
if (prototype !== Object.prototype && prototype !== null) fail(
|
|
209
|
+
if (prototype !== Object.prototype && prototype !== null) fail(path2, "not_object");
|
|
190
210
|
return input;
|
|
191
211
|
}
|
|
192
|
-
function contentObject(input,
|
|
193
|
-
const value = plainObject(input,
|
|
212
|
+
function contentObject(input, path2) {
|
|
213
|
+
const value = plainObject(input, path2);
|
|
194
214
|
let encoded;
|
|
195
215
|
try {
|
|
196
216
|
encoded = JSON.stringify(value);
|
|
197
217
|
} catch {
|
|
198
|
-
fail(
|
|
218
|
+
fail(path2, "invalid_value");
|
|
199
219
|
}
|
|
200
|
-
if (utf8Length(encoded) > LOCAL_CONTENT_MAX_BYTES) fail(
|
|
220
|
+
if (utf8Length(encoded) > LOCAL_CONTENT_MAX_BYTES) fail(path2, "too_long");
|
|
201
221
|
return value;
|
|
202
222
|
}
|
|
203
|
-
function channelName(input,
|
|
204
|
-
const value = displayName(input,
|
|
205
|
-
if ([...value].length > LOCAL_CHANNEL_NAME_MAX_CHARS) fail(
|
|
206
|
-
if (value !== value.trim()) fail(
|
|
223
|
+
function channelName(input, path2) {
|
|
224
|
+
const value = displayName(input, path2);
|
|
225
|
+
if ([...value].length > LOCAL_CHANNEL_NAME_MAX_CHARS) fail(path2, "too_long");
|
|
226
|
+
if (value !== value.trim()) fail(path2, "invalid_value");
|
|
207
227
|
return value;
|
|
208
228
|
}
|
|
209
|
-
function displayName(input,
|
|
210
|
-
const value = displayText(input,
|
|
211
|
-
if (value.length === 0) fail(
|
|
229
|
+
function displayName(input, path2) {
|
|
230
|
+
const value = displayText(input, path2, M1_LABEL_MAX_BYTES);
|
|
231
|
+
if (value.length === 0) fail(path2, "empty");
|
|
212
232
|
return value;
|
|
213
233
|
}
|
|
214
|
-
function seq(input,
|
|
215
|
-
const value = safeInteger(input,
|
|
216
|
-
if (value === 0) fail(
|
|
234
|
+
function seq(input, path2) {
|
|
235
|
+
const value = safeInteger(input, path2);
|
|
236
|
+
if (value === 0) fail(path2, "invalid_value");
|
|
217
237
|
return value;
|
|
218
238
|
}
|
|
219
|
-
function readCreateContent(input,
|
|
220
|
-
const r = record(input,
|
|
239
|
+
function readCreateContent(input, path2) {
|
|
240
|
+
const r = record(input, path2, ["name", "createdBy"], ["operationId"]);
|
|
221
241
|
return {
|
|
222
242
|
name: channelName(r.field("name"), r.at("name")),
|
|
223
243
|
createdBy: readLocalUserId(r.field("createdBy"), r.at("createdBy")),
|
|
224
244
|
...has(input, "operationId") ? { operationId: readLocalTxnId(r.field("operationId"), r.at("operationId")) } : {}
|
|
225
245
|
};
|
|
226
246
|
}
|
|
227
|
-
function readNameContent(input,
|
|
228
|
-
const r = object(input,
|
|
247
|
+
function readNameContent(input, path2) {
|
|
248
|
+
const r = object(input, path2, ["name"]);
|
|
229
249
|
return { name: channelName(r.field("name"), r.at("name")) };
|
|
230
250
|
}
|
|
231
|
-
function readMemberContent(input,
|
|
251
|
+
function readMemberContent(input, path2) {
|
|
232
252
|
const modeKey = "com.khala.listening_mode";
|
|
233
|
-
const r = record(input,
|
|
253
|
+
const r = record(input, path2, ["user", "membership", "displayname", "kind"], ["harness", "invitedBy", modeKey]);
|
|
234
254
|
const kind = literal(r.field("kind"), r.at("kind"), ["human", "agent"]);
|
|
235
255
|
if (kind === "human") {
|
|
236
256
|
for (const key of ["harness", modeKey]) if (has(input, key)) fail(r.at(key), "invalid_value");
|
|
@@ -252,28 +272,28 @@ function decodeLocalMemberContent(input) {
|
|
|
252
272
|
return decodeWith(() => readMemberContent(input, ""));
|
|
253
273
|
}
|
|
254
274
|
var EVENT_TYPES = ["m.room.create", "m.room.name", "m.room.member", "m.room.message", "com.khala.event.v1", "com.khala.listening_mode.v1"];
|
|
255
|
-
function readContent(input,
|
|
275
|
+
function readContent(input, path2, type) {
|
|
256
276
|
switch (type) {
|
|
257
277
|
case "m.room.create":
|
|
258
|
-
return readCreateContent(input,
|
|
278
|
+
return readCreateContent(input, path2);
|
|
259
279
|
case "m.room.name":
|
|
260
|
-
return readNameContent(input,
|
|
280
|
+
return readNameContent(input, path2);
|
|
261
281
|
case "m.room.member":
|
|
262
|
-
return readMemberContent(input,
|
|
282
|
+
return readMemberContent(input, path2);
|
|
263
283
|
default: {
|
|
264
|
-
const content = contentObject(input,
|
|
284
|
+
const content = contentObject(input, path2);
|
|
265
285
|
if (type === "m.room.message") {
|
|
266
|
-
literal(content["msgtype"], `${
|
|
267
|
-
const body = text(content["body"], `${
|
|
268
|
-
if (body.length === 0) fail(`${
|
|
269
|
-
if (body.length > LOCAL_BODY_MAX_CHARS) fail(`${
|
|
286
|
+
literal(content["msgtype"], `${path2}.msgtype`, ["m.text", "m.notice"]);
|
|
287
|
+
const body = text(content["body"], `${path2}.body`, LOCAL_CONTENT_MAX_BYTES);
|
|
288
|
+
if (body.length === 0) fail(`${path2}.body`, "empty");
|
|
289
|
+
if (body.length > LOCAL_BODY_MAX_CHARS) fail(`${path2}.body`, "too_long");
|
|
270
290
|
}
|
|
271
291
|
return content;
|
|
272
292
|
}
|
|
273
293
|
}
|
|
274
294
|
}
|
|
275
|
-
function readEvent(input,
|
|
276
|
-
const r = record(input,
|
|
295
|
+
function readEvent(input, path2) {
|
|
296
|
+
const r = record(input, path2, ["seq", "eventId", "roomId", "type", "sender", "ts", "content"], ["txnId", "previousContent"]);
|
|
277
297
|
const type = literal(r.field("type"), r.at("type"), EVENT_TYPES);
|
|
278
298
|
return {
|
|
279
299
|
seq: seq(r.field("seq"), r.at("seq")),
|
|
@@ -290,16 +310,16 @@ function readEvent(input, path) {
|
|
|
290
310
|
function decodeLocalEvent(input) {
|
|
291
311
|
return decodeWith(() => readEvent(input, ""));
|
|
292
312
|
}
|
|
293
|
-
function limitedArray(input,
|
|
294
|
-
const values = array(input,
|
|
295
|
-
if (values.length > max) fail(
|
|
296
|
-
return values.map((value, i) => read(value, elementPath(
|
|
313
|
+
function limitedArray(input, path2, max, read) {
|
|
314
|
+
const values = array(input, path2);
|
|
315
|
+
if (values.length > max) fail(path2, "too_long");
|
|
316
|
+
return values.map((value, i) => read(value, elementPath(path2, i)));
|
|
297
317
|
}
|
|
298
|
-
function readEvents(input,
|
|
299
|
-
const events = limitedArray(input,
|
|
318
|
+
function readEvents(input, path2, history) {
|
|
319
|
+
const events = limitedArray(input, path2, history ? LOCAL_HISTORY_LIMIT_MAX : LOCAL_EVENTS_PAGE_MAX, readEvent);
|
|
300
320
|
events.forEach((event, i) => {
|
|
301
|
-
if (i > 0 && event.seq <= events[i - 1].seq) fail(`${elementPath(
|
|
302
|
-
if (history && event.type !== "m.room.message" && event.type !== "com.khala.event.v1") fail(`${elementPath(
|
|
321
|
+
if (i > 0 && event.seq <= events[i - 1].seq) fail(`${elementPath(path2, i)}.seq`, "invalid_value");
|
|
322
|
+
if (history && event.type !== "m.room.message" && event.type !== "com.khala.event.v1") fail(`${elementPath(path2, i)}.type`, "invalid_value");
|
|
303
323
|
});
|
|
304
324
|
return events;
|
|
305
325
|
}
|
|
@@ -321,8 +341,8 @@ function decodeLocalHistoryPage(input) {
|
|
|
321
341
|
};
|
|
322
342
|
});
|
|
323
343
|
}
|
|
324
|
-
function readLocalMe(input,
|
|
325
|
-
const r = record(input,
|
|
344
|
+
function readLocalMe(input, path2) {
|
|
345
|
+
const r = record(input, path2, ["userId", "roomId", "roomName", "membership", "displayName"], ["invitedBy"]);
|
|
326
346
|
return {
|
|
327
347
|
userId: readLocalUserId(r.field("userId"), r.at("userId")),
|
|
328
348
|
roomId: readLocalRoomId(r.field("roomId"), r.at("roomId")),
|
|
@@ -335,8 +355,8 @@ function readLocalMe(input, path) {
|
|
|
335
355
|
function decodeLocalMe(input) {
|
|
336
356
|
return decodeWith(() => readLocalMe(input, ""));
|
|
337
357
|
}
|
|
338
|
-
function readLocalJoined(input,
|
|
339
|
-
const r = record(input,
|
|
358
|
+
function readLocalJoined(input, path2) {
|
|
359
|
+
const r = record(input, path2, ["seq", "ts"], []);
|
|
340
360
|
return {
|
|
341
361
|
seq: seq(r.field("seq"), r.at("seq")),
|
|
342
362
|
ts: safeInteger(r.field("ts"), r.at("ts"))
|
|
@@ -345,8 +365,8 @@ function readLocalJoined(input, path) {
|
|
|
345
365
|
function decodeLocalJoined(input) {
|
|
346
366
|
return decodeWith(() => readLocalJoined(input, ""));
|
|
347
367
|
}
|
|
348
|
-
function readLocalSendResult(input,
|
|
349
|
-
const r = record(input,
|
|
368
|
+
function readLocalSendResult(input, path2) {
|
|
369
|
+
const r = record(input, path2, ["eventId"], []);
|
|
350
370
|
return {
|
|
351
371
|
eventId: readLocalEventId(r.field("eventId"), r.at("eventId"))
|
|
352
372
|
};
|
|
@@ -354,8 +374,8 @@ function readLocalSendResult(input, path) {
|
|
|
354
374
|
function decodeLocalSendResult(input) {
|
|
355
375
|
return decodeWith(() => readLocalSendResult(input, ""));
|
|
356
376
|
}
|
|
357
|
-
function readLocalErrorBody(input,
|
|
358
|
-
const r = record(input,
|
|
377
|
+
function readLocalErrorBody(input, path2) {
|
|
378
|
+
const r = record(input, path2, ["error"], []);
|
|
359
379
|
return {
|
|
360
380
|
error: identifier(r.field("error"), r.at("error"))
|
|
361
381
|
};
|
|
@@ -363,8 +383,8 @@ function readLocalErrorBody(input, path) {
|
|
|
363
383
|
function decodeLocalErrorBody(input) {
|
|
364
384
|
return decodeWith(() => readLocalErrorBody(input, ""));
|
|
365
385
|
}
|
|
366
|
-
function readOwnerProfile(input,
|
|
367
|
-
const r = record(input,
|
|
386
|
+
function readOwnerProfile(input, path2) {
|
|
387
|
+
const r = record(input, path2, ["v", "username", "color", "initials", "updatedAt"]);
|
|
368
388
|
return {
|
|
369
389
|
v: version(r.field("v"), r.at("v")),
|
|
370
390
|
username: readLocalUsername(r.field("username"), r.at("username")),
|
|
@@ -376,11 +396,11 @@ function readOwnerProfile(input, path) {
|
|
|
376
396
|
function decodeOwnerProfile(input) {
|
|
377
397
|
return decodeWith(() => readOwnerProfile(input, ""));
|
|
378
398
|
}
|
|
379
|
-
function nullableInitials(input,
|
|
380
|
-
return nullable(input, (value) => readLocalInitials(value,
|
|
399
|
+
function nullableInitials(input, path2) {
|
|
400
|
+
return nullable(input, (value) => readLocalInitials(value, path2));
|
|
381
401
|
}
|
|
382
|
-
function readMember(input,
|
|
383
|
-
const r = record(input,
|
|
402
|
+
function readMember(input, path2) {
|
|
403
|
+
const r = record(input, path2, ["userId", "participantId", "ownerId", "deviceId", "displayName", "kind", "membership"], ["harness", "ownerLabel", "listeningMode"]);
|
|
384
404
|
const userId = readLocalUserId(r.field("userId"), r.at("userId"));
|
|
385
405
|
const participantId = readLocalUserId(r.field("participantId"), r.at("participantId"));
|
|
386
406
|
if (participantId !== userId) fail(r.at("participantId"), "invalid_value");
|
|
@@ -436,18 +456,18 @@ function decodeHelperFile(input) {
|
|
|
436
456
|
};
|
|
437
457
|
});
|
|
438
458
|
}
|
|
439
|
-
function readSha256(input,
|
|
440
|
-
const value = identifier(input,
|
|
441
|
-
if (!/^[0-9a-f]{64}$/u.test(value)) fail(
|
|
459
|
+
function readSha256(input, path2) {
|
|
460
|
+
const value = identifier(input, path2);
|
|
461
|
+
if (!/^[0-9a-f]{64}$/u.test(value)) fail(path2, "invalid_value");
|
|
442
462
|
return value;
|
|
443
463
|
}
|
|
444
464
|
function decodeChannelSecrets(input) {
|
|
445
465
|
return decodeWith(() => {
|
|
446
466
|
const r = object(input, "", ["v", "links", "members"]);
|
|
447
467
|
const links = Object.fromEntries(Object.entries(plainObject(r.field("links"), r.at("links"))).map(([key, value]) => {
|
|
448
|
-
const
|
|
449
|
-
readSha256(key,
|
|
450
|
-
const link = record(value,
|
|
468
|
+
const path2 = `${r.at("links")}.${key}`;
|
|
469
|
+
readSha256(key, path2);
|
|
470
|
+
const link = record(value, path2, ["expiresAt", "kind"], ["consumedAt"]);
|
|
451
471
|
return [key, {
|
|
452
472
|
expiresAt: utcTimestamp(link.field("expiresAt"), link.at("expiresAt")),
|
|
453
473
|
kind: literal(link.field("kind"), link.at("kind"), ["join"]),
|
|
@@ -455,9 +475,9 @@ function decodeChannelSecrets(input) {
|
|
|
455
475
|
}];
|
|
456
476
|
}));
|
|
457
477
|
const members = Object.fromEntries(Object.entries(plainObject(r.field("members"), r.at("members"))).map(([key, value]) => {
|
|
458
|
-
const
|
|
459
|
-
if (!isLocalAgentUserId(key)) fail(
|
|
460
|
-
const member = record(value,
|
|
478
|
+
const path2 = `${r.at("members")}.${key}`;
|
|
479
|
+
if (!isLocalAgentUserId(key)) fail(path2, "invalid_value");
|
|
480
|
+
const member = record(value, path2, ["tokenSha256"], ["sessionKey"]);
|
|
461
481
|
return [key, {
|
|
462
482
|
tokenSha256: readSha256(member.field("tokenSha256"), member.at("tokenSha256")),
|
|
463
483
|
...has(value, "sessionKey") ? { sessionKey: readSha256(member.field("sessionKey"), member.at("sessionKey")) } : {}
|
|
@@ -467,17 +487,201 @@ function decodeChannelSecrets(input) {
|
|
|
467
487
|
});
|
|
468
488
|
}
|
|
469
489
|
|
|
490
|
+
// src/local/lifecycle.ts
|
|
491
|
+
import * as fs from "node:fs/promises";
|
|
492
|
+
import { close as closeFd } from "node:fs";
|
|
493
|
+
import path from "node:path";
|
|
494
|
+
import { fileURLToPath } from "node:url";
|
|
495
|
+
import { setTimeout as sleepDefault } from "node:timers/promises";
|
|
496
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
497
|
+
function helperPaths(env = process.env) {
|
|
498
|
+
const root = path.join(stateRoot(env), "local");
|
|
499
|
+
const candidate = /^\d{1,5}$/u.test(env.KHALA_LOCAL_PORT ?? "") ? Number(env.KHALA_LOCAL_PORT) : 0;
|
|
500
|
+
const port = candidate >= 1 && candidate <= 65535 ? candidate : LOCAL_DEFAULT_PORT;
|
|
501
|
+
return { root, helperFile: path.join(root, "helper.json"), logFile: path.join(root, "helper.log"), port, origin: `http://127.0.0.1:${port}` };
|
|
502
|
+
}
|
|
503
|
+
async function readHelperFile(env = process.env) {
|
|
504
|
+
const decoded = decodeHelperFile(await readJson(helperPaths(env).helperFile));
|
|
505
|
+
if (!decoded.ok) return null;
|
|
506
|
+
const file = decoded.value;
|
|
507
|
+
if (!Number.isSafeInteger(file.pid) || file.pid <= 0 || !Number.isSafeInteger(file.port) || file.port <= 0 || file.port > 65535 || file.origin !== `http://127.0.0.1:${file.port}` || !/^[A-Za-z0-9_-]{43}$/u.test(file.adminToken)) return null;
|
|
508
|
+
return file;
|
|
509
|
+
}
|
|
510
|
+
function helperBinPath() {
|
|
511
|
+
return fileURLToPath(bundle ? new URL("./khala.mjs", import.meta.url) : new URL("../../bin/khala.mjs", import.meta.url));
|
|
512
|
+
}
|
|
513
|
+
var HELPER_ENV_ALLOWLIST = [
|
|
514
|
+
"PATH",
|
|
515
|
+
"HOME",
|
|
516
|
+
"USER",
|
|
517
|
+
"LOGNAME",
|
|
518
|
+
"LANG",
|
|
519
|
+
"LC_ALL",
|
|
520
|
+
"TMPDIR",
|
|
521
|
+
"XDG_STATE_HOME",
|
|
522
|
+
"NODE_OPTIONS",
|
|
523
|
+
"KHALA_EGRESS_LOG",
|
|
524
|
+
"KHALA_LOCAL_PORT",
|
|
525
|
+
"KHALA_LOCAL_IDLE_MS",
|
|
526
|
+
"KHALA_LOCAL_WEB_DIR"
|
|
527
|
+
];
|
|
528
|
+
function helperChildEnv(env) {
|
|
529
|
+
const childEnv = {};
|
|
530
|
+
for (const key of HELPER_ENV_ALLOWLIST) if (env[key] !== void 0) childEnv[key] = env[key];
|
|
531
|
+
return childEnv;
|
|
532
|
+
}
|
|
533
|
+
var attempts = /* @__PURE__ */ new Map();
|
|
534
|
+
var restarted = false;
|
|
535
|
+
var recovering = /* @__PURE__ */ new Map();
|
|
536
|
+
function olderHelperVersion(version2) {
|
|
537
|
+
if (typeof version2 !== "string") return true;
|
|
538
|
+
const parse = (value) => /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
|
|
539
|
+
const helper = parse(version2), cli = parse(KHALA_AGENT_VERSION);
|
|
540
|
+
if (!helper || !cli) return true;
|
|
541
|
+
for (let i = 1; i <= 3; i++) {
|
|
542
|
+
if (Number(helper[i]) !== Number(cli[i])) return Number(helper[i]) < Number(cli[i]);
|
|
543
|
+
}
|
|
544
|
+
if (helper[4] === cli[4]) return false;
|
|
545
|
+
if (!helper[4] || !cli[4]) return Boolean(helper[4]);
|
|
546
|
+
const a = helper[4].split("."), b = cli[4].split(".");
|
|
547
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
548
|
+
if (a[i] === b[i]) continue;
|
|
549
|
+
if (a[i] === void 0 || b[i] === void 0) return a[i] === void 0;
|
|
550
|
+
const an = /^\d+$/u.test(a[i]), bn = /^\d+$/u.test(b[i]);
|
|
551
|
+
if (an && bn) return Number(a[i]) < Number(b[i]);
|
|
552
|
+
if (an !== bn) return an;
|
|
553
|
+
return a[i] < b[i];
|
|
554
|
+
}
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
async function healthy(fetchImpl, file) {
|
|
558
|
+
try {
|
|
559
|
+
const response = await fetchImpl(`${file.origin}/healthz`, { signal: AbortSignal.timeout(500) });
|
|
560
|
+
if (response.status !== 200) {
|
|
561
|
+
await response.body?.cancel();
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
const body = await response.json();
|
|
565
|
+
return typeof body === "object" && body !== null && "ok" in body && body.ok === true && "pid" in body && body.pid === file.pid ? { version: "version" in body ? body.version : void 0 } : null;
|
|
566
|
+
} catch {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function connection(file) {
|
|
571
|
+
return { origin: file.origin, adminToken: file.adminToken };
|
|
572
|
+
}
|
|
573
|
+
async function attempt(env, deps, restartOrigin) {
|
|
574
|
+
const fetchImpl = deps.fetch ?? globalThis.fetch;
|
|
575
|
+
const spawn = deps.spawn ?? ((command, argv, options) => nodeSpawn(command, [...argv], options));
|
|
576
|
+
const sleep = deps.sleep ?? sleepDefault;
|
|
577
|
+
const now = deps.now ?? Date.now;
|
|
578
|
+
const existing = await readHelperFile(env);
|
|
579
|
+
const status = existing ? await healthy(fetchImpl, existing) : null;
|
|
580
|
+
if (existing && status) {
|
|
581
|
+
if (!olderHelperVersion(status.version) && restartOrigin === void 0 || restarted) return connection(existing);
|
|
582
|
+
if (restartOrigin !== void 0 && existing.origin !== restartOrigin) return connection(existing);
|
|
583
|
+
restarted = true;
|
|
584
|
+
recovering.set(helperPaths(env).helperFile, existing.origin);
|
|
585
|
+
try {
|
|
586
|
+
const response = await fetchImpl(`${existing.origin}/api/local/shutdown`, {
|
|
587
|
+
method: "POST",
|
|
588
|
+
headers: { authorization: `Bearer ${existing.adminToken}` },
|
|
589
|
+
signal: AbortSignal.timeout(2e3)
|
|
590
|
+
});
|
|
591
|
+
await response.body?.cancel();
|
|
592
|
+
if (response.status !== 204) throw new Error("shutdown_failed");
|
|
593
|
+
const deadline = now() + 2e3;
|
|
594
|
+
for (let i = 0; i < 20; i++) {
|
|
595
|
+
if (!await healthy(fetchImpl, existing)) break;
|
|
596
|
+
if (now() >= deadline || i === 19) throw new Error("shutdown_timeout");
|
|
597
|
+
await sleep(100);
|
|
598
|
+
}
|
|
599
|
+
} catch {
|
|
600
|
+
const refreshed = await readHelperFile(env);
|
|
601
|
+
if (refreshed && await healthy(fetchImpl, refreshed)) return connection(refreshed);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const paths = helperPaths(env);
|
|
605
|
+
try {
|
|
606
|
+
await ensureStateDir(path.join(paths.root, "channels"));
|
|
607
|
+
let handle;
|
|
608
|
+
let fd;
|
|
609
|
+
let spawnFailed = false;
|
|
610
|
+
try {
|
|
611
|
+
if (deps.openLog) fd = await deps.openLog(paths.logFile);
|
|
612
|
+
else {
|
|
613
|
+
handle = await fs.open(paths.logFile, "a", 384);
|
|
614
|
+
await handle.chmod(384);
|
|
615
|
+
fd = handle.fd;
|
|
616
|
+
}
|
|
617
|
+
const child = spawn(process.execPath, [helperBinPath(), "local", "serve"], {
|
|
618
|
+
detached: true,
|
|
619
|
+
stdio: ["ignore", fd, fd],
|
|
620
|
+
env: helperChildEnv(existing && status ? { ...env, KHALA_LOCAL_PORT: String(existing.port) } : env),
|
|
621
|
+
cwd: paths.root,
|
|
622
|
+
shell: false
|
|
623
|
+
});
|
|
624
|
+
child.on("error", () => {
|
|
625
|
+
spawnFailed = true;
|
|
626
|
+
});
|
|
627
|
+
child.unref();
|
|
628
|
+
} finally {
|
|
629
|
+
if (handle) await handle.close();
|
|
630
|
+
else if (fd !== void 0) {
|
|
631
|
+
const descriptor = fd;
|
|
632
|
+
await new Promise((resolve) => {
|
|
633
|
+
closeFd(descriptor, () => resolve());
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const deadline = now() + 5e3;
|
|
638
|
+
while (!spawnFailed && now() < deadline) {
|
|
639
|
+
const file = await readHelperFile(env);
|
|
640
|
+
if (file && await healthy(fetchImpl, file) && !spawnFailed && now() <= deadline) return connection(file);
|
|
641
|
+
if (spawnFailed || now() >= deadline) break;
|
|
642
|
+
await sleep(100);
|
|
643
|
+
}
|
|
644
|
+
} catch (error) {
|
|
645
|
+
if (error instanceof StateError) throw error;
|
|
646
|
+
}
|
|
647
|
+
throw new KhalaClientError("internal_error", "helper_unavailable");
|
|
648
|
+
}
|
|
649
|
+
function ensureHelper(env = process.env, deps = {}, restartOrigin) {
|
|
650
|
+
const key = helperPaths(env).helperFile;
|
|
651
|
+
const pending = attempts.get(key);
|
|
652
|
+
if (pending) return pending;
|
|
653
|
+
const promise = attempt(env, deps, restartOrigin).finally(() => {
|
|
654
|
+
attempts.delete(key);
|
|
655
|
+
recovering.delete(key);
|
|
656
|
+
});
|
|
657
|
+
attempts.set(key, promise);
|
|
658
|
+
return promise;
|
|
659
|
+
}
|
|
660
|
+
async function restartHelper(origin, env = process.env, deps = {}) {
|
|
661
|
+
const key = helperPaths(env).helperFile;
|
|
662
|
+
const pending = attempts.get(key);
|
|
663
|
+
if (pending) {
|
|
664
|
+
const shareRecovery = !restarted || recovering.get(key) === origin;
|
|
665
|
+
const connection2 = await pending;
|
|
666
|
+
if (shareRecovery && restarted && connection2.origin === origin) return true;
|
|
667
|
+
}
|
|
668
|
+
if (restarted) return false;
|
|
669
|
+
const file = await readHelperFile(env);
|
|
670
|
+
if (!file || file.origin !== origin) return false;
|
|
671
|
+
await ensureHelper(env, deps, origin);
|
|
672
|
+
return restarted;
|
|
673
|
+
}
|
|
674
|
+
|
|
470
675
|
export {
|
|
471
676
|
KhalaClientError,
|
|
472
677
|
isHumanColorId,
|
|
473
678
|
defaultHumanColor,
|
|
474
679
|
normalizeInitials,
|
|
475
680
|
checkName,
|
|
476
|
-
|
|
681
|
+
checkNewUsername,
|
|
477
682
|
defaultAgentName,
|
|
478
683
|
isDefaultAgentName,
|
|
479
684
|
LOCAL_OWNER_USER_ID,
|
|
480
|
-
LOCAL_DEFAULT_PORT,
|
|
481
685
|
LOCAL_LINK_TTL_MS,
|
|
482
686
|
LOCAL_LONG_POLL_MAX_S,
|
|
483
687
|
LOCAL_IDLE_EXIT_MS,
|
|
@@ -503,6 +707,10 @@ export {
|
|
|
503
707
|
decodeLocalErrorBody,
|
|
504
708
|
decodeOwnerProfile,
|
|
505
709
|
decodeLocalMembersResponse,
|
|
506
|
-
|
|
507
|
-
|
|
710
|
+
decodeChannelSecrets,
|
|
711
|
+
helperPaths,
|
|
712
|
+
readHelperFile,
|
|
713
|
+
olderHelperVersion,
|
|
714
|
+
ensureHelper,
|
|
715
|
+
restartHelper
|
|
508
716
|
};
|