@ours.network/cli 1.0.1 → 2.0.2
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/README.md +21 -8
- package/dist/{boot-35BOHNSZ.js → boot-Y3GXCN6X.js} +1 -1
- package/dist/{chunk-QZIL3IT7.js → chunk-DTN7MN44.js} +29 -12
- package/dist/{chunk-MHVYJY3H.js → chunk-IL724CY2.js} +1517 -1013
- package/dist/{chunk-QRNZFPNJ.js → chunk-MS4TDUZY.js} +41 -42
- package/dist/{chunk-TNNBVUKK.js → chunk-NBAZ2NEL.js} +2 -2
- package/dist/{chunk-SDNTPCKW.js → chunk-VA6UQKYN.js} +16 -18
- package/dist/{chunk-ZFE47KH3.js → chunk-VFAC74XR.js} +4 -4
- package/dist/commands.d.ts +16 -18
- package/dist/commands.js +1 -1
- package/dist/{daemon-UUG6P6GK.js → daemon-A2L6UMGZ.js} +2 -2
- package/dist/help.js +3 -3
- package/dist/lifecycle.d.ts +3 -0
- package/dist/lifecycle.js +5 -1
- package/dist/main.js +6 -6
- package/dist/mufl_code/{7962BB01B1D3E782A1F80B083A900AEC30BAC8F8CE79282B31821802B94A39B6.muflo → 351F16D444E6DAA252D4A8D829D17204360D9015FEF670ACE68FB4747BB3B14E.muflo} +0 -0
- package/dist/operations.d.ts +32 -57
- package/dist/operations.js +1 -1
- package/dist/{server-TDJBEWTU.js → server-6BZCHN6Q.js} +206 -143
- package/dist/service.js +1 -1
- package/package.json +3 -2
|
@@ -89,7 +89,7 @@ function createStartupProgressReporter(stateDir, opts = {}) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// ../../src/runtime/env.ts
|
|
92
|
-
var VERSION =
|
|
92
|
+
var VERSION = true ? "3.0.2" : "0.0.0-dev";
|
|
93
93
|
var CONFIG = loadConfig();
|
|
94
94
|
var STATE_DIR = CONFIG.stateDir;
|
|
95
95
|
var BROKER_URL = CONFIG.brokerUrl;
|
|
@@ -186,9 +186,612 @@ function setUnit(u) {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
// ../../src/identity/model.ts
|
|
189
|
-
import { join as
|
|
190
|
-
import { createHash } from "node:crypto";
|
|
189
|
+
import { join as join4 } from "node:path";
|
|
190
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
191
|
+
import * as fs4 from "node:fs";
|
|
192
|
+
|
|
193
|
+
// ../../src/history.ts
|
|
194
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
191
195
|
import * as fs3 from "node:fs";
|
|
196
|
+
import { dirname as dirname2, join as join3, resolve as resolve2, sep } from "node:path";
|
|
197
|
+
import Database from "better-sqlite3";
|
|
198
|
+
var MAX_BATCH = 200;
|
|
199
|
+
var DEFAULT_BATCH = 50;
|
|
200
|
+
var MAX_TEXT_BYTES = 2 * 1024 * 1024;
|
|
201
|
+
var MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
202
|
+
var DB_NAME = "history.sqlite3";
|
|
203
|
+
var SCHEMA_VERSION = 1;
|
|
204
|
+
var stores = /* @__PURE__ */ new Map();
|
|
205
|
+
var health = /* @__PURE__ */ new Map();
|
|
206
|
+
var outboundFailures = /* @__PURE__ */ new Map();
|
|
207
|
+
var historyPath = (id) => join3(id.dir, DB_NAME);
|
|
208
|
+
function initialHealth() {
|
|
209
|
+
return {
|
|
210
|
+
write_failures: 0,
|
|
211
|
+
message_write_failures: 0,
|
|
212
|
+
file_write_failures: 0,
|
|
213
|
+
receipt_update_failures: 0,
|
|
214
|
+
last_failure_at_ms: null
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function noteHistoryFailure(id, kind, wireId, direction) {
|
|
218
|
+
const next = health.get(id.dir) ?? initialHealth();
|
|
219
|
+
next.write_failures += 1;
|
|
220
|
+
next.last_failure_at_ms = Date.now();
|
|
221
|
+
if (kind === "message") next.message_write_failures += 1;
|
|
222
|
+
else if (kind === "file") next.file_write_failures += 1;
|
|
223
|
+
else next.receipt_update_failures += 1;
|
|
224
|
+
health.set(id.dir, next);
|
|
225
|
+
if (direction === "out" && wireId) {
|
|
226
|
+
let failed = outboundFailures.get(id.dir);
|
|
227
|
+
if (!failed) {
|
|
228
|
+
failed = /* @__PURE__ */ new Set();
|
|
229
|
+
outboundFailures.set(id.dir, failed);
|
|
230
|
+
}
|
|
231
|
+
failed.add(wireId);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function consumeOutboundHistoryFailure(id, wireId) {
|
|
235
|
+
const failed = outboundFailures.get(id.dir);
|
|
236
|
+
if (!failed?.delete(wireId)) return false;
|
|
237
|
+
if (failed.size === 0) outboundFailures.delete(id.dir);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
function schema(db) {
|
|
241
|
+
db.exec(`
|
|
242
|
+
CREATE TABLE IF NOT EXISTS history_meta (
|
|
243
|
+
key TEXT PRIMARY KEY,
|
|
244
|
+
value TEXT NOT NULL
|
|
245
|
+
);
|
|
246
|
+
CREATE TABLE IF NOT EXISTS wire_items (
|
|
247
|
+
wire_id TEXT PRIMARY KEY,
|
|
248
|
+
item_kind TEXT NOT NULL CHECK (item_kind IN ('message','file'))
|
|
249
|
+
);
|
|
250
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
251
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
252
|
+
wire_id TEXT NOT NULL UNIQUE,
|
|
253
|
+
peer_cid TEXT NOT NULL,
|
|
254
|
+
peer_name_snapshot TEXT NOT NULL,
|
|
255
|
+
direction TEXT NOT NULL CHECK (direction IN ('in','out')),
|
|
256
|
+
body TEXT NOT NULL,
|
|
257
|
+
occurred_at_ms INTEGER NOT NULL,
|
|
258
|
+
reply_to_wire_id TEXT,
|
|
259
|
+
reply_to_sentence INTEGER,
|
|
260
|
+
encryption TEXT NOT NULL CHECK (encryption IN ('legacy','e2e')),
|
|
261
|
+
inbox_state TEXT NOT NULL CHECK (inbox_state IN ('pending_introduction','unread','read')),
|
|
262
|
+
delivery_state TEXT,
|
|
263
|
+
human_read_at_ms INTEGER,
|
|
264
|
+
inserted_at_ms INTEGER NOT NULL
|
|
265
|
+
);
|
|
266
|
+
CREATE INDEX IF NOT EXISTS messages_unread_seq ON messages(inbox_state, seq ASC);
|
|
267
|
+
CREATE INDEX IF NOT EXISTS messages_peer_seq ON messages(peer_cid, seq DESC);
|
|
268
|
+
CREATE TRIGGER IF NOT EXISTS messages_release_wire AFTER DELETE ON messages BEGIN
|
|
269
|
+
DELETE FROM wire_items WHERE wire_id = OLD.wire_id AND item_kind = 'message';
|
|
270
|
+
END;
|
|
271
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
272
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
273
|
+
wire_id TEXT NOT NULL UNIQUE,
|
|
274
|
+
peer_cid TEXT NOT NULL,
|
|
275
|
+
peer_name_snapshot TEXT NOT NULL,
|
|
276
|
+
direction TEXT NOT NULL CHECK (direction IN ('in','out')),
|
|
277
|
+
filename TEXT NOT NULL,
|
|
278
|
+
mime TEXT NOT NULL,
|
|
279
|
+
byte_length INTEGER NOT NULL,
|
|
280
|
+
sha256 TEXT NOT NULL,
|
|
281
|
+
blob_relpath TEXT NOT NULL,
|
|
282
|
+
occurred_at_ms INTEGER NOT NULL,
|
|
283
|
+
reply_to_wire_id TEXT,
|
|
284
|
+
reply_to_sentence INTEGER,
|
|
285
|
+
encryption TEXT NOT NULL CHECK (encryption IN ('legacy','e2e')),
|
|
286
|
+
inbox_state TEXT NOT NULL CHECK (inbox_state IN ('pending_introduction','unread','read')),
|
|
287
|
+
delivery_state TEXT,
|
|
288
|
+
human_read_at_ms INTEGER,
|
|
289
|
+
inserted_at_ms INTEGER NOT NULL
|
|
290
|
+
);
|
|
291
|
+
CREATE INDEX IF NOT EXISTS files_unread_seq ON files(inbox_state, seq ASC);
|
|
292
|
+
CREATE INDEX IF NOT EXISTS files_peer_seq ON files(peer_cid, seq DESC);
|
|
293
|
+
CREATE TRIGGER IF NOT EXISTS files_release_wire AFTER DELETE ON files BEGIN
|
|
294
|
+
DELETE FROM wire_items WHERE wire_id = OLD.wire_id AND item_kind = 'file';
|
|
295
|
+
END;
|
|
296
|
+
CREATE TABLE IF NOT EXISTS monitoring_items (
|
|
297
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
298
|
+
item_id TEXT NOT NULL UNIQUE,
|
|
299
|
+
source_cid TEXT NOT NULL,
|
|
300
|
+
source_name_snapshot TEXT NOT NULL,
|
|
301
|
+
direction TEXT NOT NULL CHECK (direction IN ('in','out')),
|
|
302
|
+
peer_cid TEXT NOT NULL,
|
|
303
|
+
peer_name_snapshot TEXT NOT NULL,
|
|
304
|
+
body TEXT NOT NULL,
|
|
305
|
+
occurred_at_ms INTEGER NOT NULL,
|
|
306
|
+
queue_state TEXT NOT NULL CHECK (queue_state IN ('pending','processed')),
|
|
307
|
+
inserted_at_ms INTEGER NOT NULL
|
|
308
|
+
);
|
|
309
|
+
CREATE INDEX IF NOT EXISTS monitoring_pending_seq ON monitoring_items(queue_state, seq ASC);
|
|
310
|
+
CREATE TABLE IF NOT EXISTS control_requests (
|
|
311
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
312
|
+
item_id TEXT NOT NULL UNIQUE,
|
|
313
|
+
sender_cid TEXT NOT NULL,
|
|
314
|
+
sender_name_snapshot TEXT NOT NULL,
|
|
315
|
+
payload TEXT NOT NULL,
|
|
316
|
+
occurred_at_ms INTEGER NOT NULL,
|
|
317
|
+
queue_state TEXT NOT NULL CHECK (queue_state IN ('pending','processed')),
|
|
318
|
+
inserted_at_ms INTEGER NOT NULL
|
|
319
|
+
);
|
|
320
|
+
CREATE INDEX IF NOT EXISTS control_pending_seq ON control_requests(queue_state, seq ASC);
|
|
321
|
+
`);
|
|
322
|
+
const version = db.prepare("SELECT value FROM history_meta WHERE key = ?").pluck().get("schema_version");
|
|
323
|
+
if (version !== void 0 && Number(version) !== SCHEMA_VERSION) {
|
|
324
|
+
throw new Error(`unsupported history database schema ${String(version)} (expected ${SCHEMA_VERSION})`);
|
|
325
|
+
}
|
|
326
|
+
db.prepare("INSERT OR IGNORE INTO history_meta(key, value) VALUES (?, ?)").run("schema_version", String(SCHEMA_VERSION));
|
|
327
|
+
}
|
|
328
|
+
function openHistory(id) {
|
|
329
|
+
const existing = stores.get(id.dir);
|
|
330
|
+
if (existing) return existing;
|
|
331
|
+
fs3.mkdirSync(id.dir, { recursive: true, mode: 448 });
|
|
332
|
+
fs3.chmodSync(id.dir, 448);
|
|
333
|
+
const path = historyPath(id);
|
|
334
|
+
const db = new Database(path);
|
|
335
|
+
try {
|
|
336
|
+
fs3.chmodSync(path, 384);
|
|
337
|
+
db.pragma("journal_mode = WAL");
|
|
338
|
+
db.pragma("synchronous = NORMAL");
|
|
339
|
+
db.pragma("foreign_keys = ON");
|
|
340
|
+
db.pragma("busy_timeout = 5000");
|
|
341
|
+
schema(db);
|
|
342
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
343
|
+
const candidate = `${path}${suffix}`;
|
|
344
|
+
if (fs3.existsSync(candidate)) fs3.chmodSync(candidate, 384);
|
|
345
|
+
}
|
|
346
|
+
} catch (error) {
|
|
347
|
+
db.close();
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
stores.set(id.dir, db);
|
|
351
|
+
return db;
|
|
352
|
+
}
|
|
353
|
+
function closeHistory(id) {
|
|
354
|
+
const db = stores.get(id.dir);
|
|
355
|
+
if (!db) return;
|
|
356
|
+
stores.delete(id.dir);
|
|
357
|
+
db.close();
|
|
358
|
+
}
|
|
359
|
+
function validateCommon(event) {
|
|
360
|
+
if (!event.wireId || event.wireId.length > 256) throw new Error("invalid wire_id");
|
|
361
|
+
if (!event.peerCid || event.peerCid.length > 256) throw new Error("invalid peer cid");
|
|
362
|
+
if (!event.peerName || event.peerName.length > 512) throw new Error("invalid peer name");
|
|
363
|
+
if (!Number.isSafeInteger(event.occurredAtMs) || event.occurredAtMs < 0) throw new Error("invalid timestamp");
|
|
364
|
+
if (event.replyToWireId !== void 0 && (!event.replyToWireId || event.replyToWireId.length > 256)) {
|
|
365
|
+
throw new Error("invalid reply wire_id");
|
|
366
|
+
}
|
|
367
|
+
if (event.replyToSentence !== void 0 && (!Number.isSafeInteger(event.replyToSentence) || event.replyToSentence < 1)) {
|
|
368
|
+
throw new Error("invalid reply sentence");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function validateEvent(event) {
|
|
372
|
+
validateCommon(event);
|
|
373
|
+
if (event.kind === "message") {
|
|
374
|
+
if (Buffer.byteLength(event.body, "utf8") > MAX_TEXT_BYTES) throw new Error("message body exceeds host limit");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (!event.filename || event.filename.length > 512 || /[\0]/.test(event.filename)) throw new Error("invalid filename");
|
|
378
|
+
if (event.mime.length > 512 || /[\r\n\0]/.test(event.mime)) throw new Error("invalid MIME type");
|
|
379
|
+
if (event.bytes.byteLength > MAX_FILE_BYTES) throw new Error("file exceeds host limit");
|
|
380
|
+
}
|
|
381
|
+
function reply(row) {
|
|
382
|
+
if (!row.reply_to_wire_id) return null;
|
|
383
|
+
return row.reply_to_sentence === null ? { wire_id: row.reply_to_wire_id } : { wire_id: row.reply_to_wire_id, sentence: row.reply_to_sentence };
|
|
384
|
+
}
|
|
385
|
+
function iso(ms) {
|
|
386
|
+
return new Date(ms).toISOString();
|
|
387
|
+
}
|
|
388
|
+
function messageFromRow(row) {
|
|
389
|
+
const peer = { id: row.peer_cid, name: row.peer_name_snapshot };
|
|
390
|
+
return {
|
|
391
|
+
seq: row.seq,
|
|
392
|
+
msg_id: row.seq,
|
|
393
|
+
wire_id: row.wire_id,
|
|
394
|
+
from: peer,
|
|
395
|
+
peer,
|
|
396
|
+
direction: row.direction,
|
|
397
|
+
text: row.body,
|
|
398
|
+
body: row.body,
|
|
399
|
+
occurred_at_ms: row.occurred_at_ms,
|
|
400
|
+
date: iso(row.occurred_at_ms),
|
|
401
|
+
encryption: row.encryption,
|
|
402
|
+
transport: row.encryption === "e2e" ? "double_ratchet" : "legacy_box",
|
|
403
|
+
inbox_state: row.inbox_state,
|
|
404
|
+
status: row.inbox_state,
|
|
405
|
+
delivery_state: row.delivery_state,
|
|
406
|
+
human_read_at_ms: row.human_read_at_ms,
|
|
407
|
+
reply_to: reply(row)
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function fileFromRow(id, row) {
|
|
411
|
+
const peer = { id: row.peer_cid, name: row.peer_name_snapshot };
|
|
412
|
+
const mime = row.mime || "application/octet-stream";
|
|
413
|
+
return {
|
|
414
|
+
seq: row.seq,
|
|
415
|
+
file_id: row.seq,
|
|
416
|
+
wire_id: row.wire_id,
|
|
417
|
+
from: peer,
|
|
418
|
+
peer,
|
|
419
|
+
direction: row.direction,
|
|
420
|
+
filename: row.filename,
|
|
421
|
+
mime,
|
|
422
|
+
size: row.byte_length,
|
|
423
|
+
byte_length: row.byte_length,
|
|
424
|
+
sha256: row.sha256,
|
|
425
|
+
occurred_at_ms: row.occurred_at_ms,
|
|
426
|
+
date: iso(row.occurred_at_ms),
|
|
427
|
+
encryption: row.encryption,
|
|
428
|
+
inbox_state: row.inbox_state,
|
|
429
|
+
status: row.inbox_state,
|
|
430
|
+
delivery_state: row.delivery_state,
|
|
431
|
+
human_read_at_ms: row.human_read_at_ms,
|
|
432
|
+
reply_to: reply(row),
|
|
433
|
+
blob_path: join3(id.dir, row.blob_relpath),
|
|
434
|
+
kind: mime.startsWith("audio/") ? "voice_message" : "file"
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function writeBlob(id, bytes) {
|
|
438
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
439
|
+
const relpath = join3("blobs", "sha256", sha256.slice(0, 2), sha256);
|
|
440
|
+
const final = join3(id.dir, relpath);
|
|
441
|
+
fs3.mkdirSync(dirname2(final), { recursive: true, mode: 448 });
|
|
442
|
+
fs3.chmodSync(dirname2(final), 448);
|
|
443
|
+
if (!fs3.existsSync(final)) {
|
|
444
|
+
const tmp = join3(dirname2(final), `.${sha256}.${process.pid}.${randomUUID()}.tmp`);
|
|
445
|
+
try {
|
|
446
|
+
fs3.writeFileSync(tmp, bytes, { flag: "wx", mode: 384 });
|
|
447
|
+
fs3.chmodSync(tmp, 384);
|
|
448
|
+
try {
|
|
449
|
+
fs3.renameSync(tmp, final);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (!fs3.existsSync(final)) throw error;
|
|
452
|
+
fs3.rmSync(tmp, { force: true });
|
|
453
|
+
}
|
|
454
|
+
} catch (error) {
|
|
455
|
+
try {
|
|
456
|
+
fs3.rmSync(tmp, { force: true });
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
fs3.chmodSync(final, 384);
|
|
463
|
+
return { sha256, relpath };
|
|
464
|
+
}
|
|
465
|
+
function ingestApplicationEvent(id, event) {
|
|
466
|
+
validateEvent(event);
|
|
467
|
+
const db = openHistory(id);
|
|
468
|
+
if (event.direction === "out" && event.inboxState !== void 0) throw new Error("outbound event cannot set inbox state");
|
|
469
|
+
const inboxState = event.direction === "in" ? event.inboxState ?? "unread" : "read";
|
|
470
|
+
const deliveryState = event.direction === "out" ? "sent" : null;
|
|
471
|
+
if (event.kind === "message") {
|
|
472
|
+
return db.transaction(() => {
|
|
473
|
+
const claimed = db.prepare(`
|
|
474
|
+
INSERT INTO wire_items(wire_id, item_kind) VALUES (?, 'message')
|
|
475
|
+
ON CONFLICT(wire_id) DO NOTHING
|
|
476
|
+
`).run(event.wireId);
|
|
477
|
+
if (claimed.changes !== 1) return false;
|
|
478
|
+
db.prepare(`
|
|
479
|
+
INSERT INTO messages (
|
|
480
|
+
wire_id, peer_cid, peer_name_snapshot, direction, body, occurred_at_ms,
|
|
481
|
+
reply_to_wire_id, reply_to_sentence, encryption, inbox_state,
|
|
482
|
+
delivery_state, human_read_at_ms, inserted_at_ms
|
|
483
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
484
|
+
`).run(
|
|
485
|
+
event.wireId,
|
|
486
|
+
event.peerCid,
|
|
487
|
+
event.peerName,
|
|
488
|
+
event.direction,
|
|
489
|
+
event.body,
|
|
490
|
+
event.occurredAtMs,
|
|
491
|
+
event.replyToWireId ?? null,
|
|
492
|
+
event.replyToSentence ?? null,
|
|
493
|
+
event.encryption,
|
|
494
|
+
inboxState,
|
|
495
|
+
deliveryState,
|
|
496
|
+
event.direction === "out" ? event.occurredAtMs : null,
|
|
497
|
+
Date.now()
|
|
498
|
+
);
|
|
499
|
+
return true;
|
|
500
|
+
})();
|
|
501
|
+
}
|
|
502
|
+
const duplicate = db.prepare("SELECT 1 FROM wire_items WHERE wire_id = ?").get(event.wireId);
|
|
503
|
+
if (duplicate) return false;
|
|
504
|
+
const blob = writeBlob(id, event.bytes);
|
|
505
|
+
return db.transaction(() => {
|
|
506
|
+
const claimed = db.prepare(`
|
|
507
|
+
INSERT INTO wire_items(wire_id, item_kind) VALUES (?, 'file')
|
|
508
|
+
ON CONFLICT(wire_id) DO NOTHING
|
|
509
|
+
`).run(event.wireId);
|
|
510
|
+
if (claimed.changes !== 1) return false;
|
|
511
|
+
db.prepare(`
|
|
512
|
+
INSERT INTO files (
|
|
513
|
+
wire_id, peer_cid, peer_name_snapshot, direction, filename, mime,
|
|
514
|
+
byte_length, sha256, blob_relpath, occurred_at_ms, reply_to_wire_id,
|
|
515
|
+
reply_to_sentence, encryption, inbox_state, delivery_state,
|
|
516
|
+
human_read_at_ms, inserted_at_ms
|
|
517
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
518
|
+
`).run(
|
|
519
|
+
event.wireId,
|
|
520
|
+
event.peerCid,
|
|
521
|
+
event.peerName,
|
|
522
|
+
event.direction,
|
|
523
|
+
event.filename,
|
|
524
|
+
event.mime || "application/octet-stream",
|
|
525
|
+
event.bytes.byteLength,
|
|
526
|
+
blob.sha256,
|
|
527
|
+
blob.relpath,
|
|
528
|
+
event.occurredAtMs,
|
|
529
|
+
event.replyToWireId ?? null,
|
|
530
|
+
event.replyToSentence ?? null,
|
|
531
|
+
event.encryption,
|
|
532
|
+
inboxState,
|
|
533
|
+
deliveryState,
|
|
534
|
+
event.direction === "out" ? event.occurredAtMs : null,
|
|
535
|
+
Date.now()
|
|
536
|
+
);
|
|
537
|
+
return true;
|
|
538
|
+
})();
|
|
539
|
+
}
|
|
540
|
+
function ingestMonitoringEvent(id, event) {
|
|
541
|
+
if (!event.itemId || !event.sourceCid || !event.sourceName || !event.peerCid || !event.peerName) {
|
|
542
|
+
throw new Error("invalid monitoring event metadata");
|
|
543
|
+
}
|
|
544
|
+
if (!Number.isSafeInteger(event.occurredAtMs) || event.occurredAtMs < 0) throw new Error("invalid monitoring timestamp");
|
|
545
|
+
if (Buffer.byteLength(event.body, "utf8") > MAX_TEXT_BYTES) throw new Error("monitoring body exceeds host limit");
|
|
546
|
+
return openHistory(id).prepare(`
|
|
547
|
+
INSERT INTO monitoring_items (
|
|
548
|
+
item_id, source_cid, source_name_snapshot, direction, peer_cid,
|
|
549
|
+
peer_name_snapshot, body, occurred_at_ms, queue_state, inserted_at_ms
|
|
550
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)
|
|
551
|
+
ON CONFLICT(item_id) DO NOTHING
|
|
552
|
+
`).run(
|
|
553
|
+
event.itemId,
|
|
554
|
+
event.sourceCid,
|
|
555
|
+
event.sourceName,
|
|
556
|
+
event.direction,
|
|
557
|
+
event.peerCid,
|
|
558
|
+
event.peerName,
|
|
559
|
+
event.body,
|
|
560
|
+
event.occurredAtMs,
|
|
561
|
+
Date.now()
|
|
562
|
+
).changes === 1;
|
|
563
|
+
}
|
|
564
|
+
function ingestControlEvent(id, event) {
|
|
565
|
+
if (!event.itemId || !event.senderCid || !event.senderName) throw new Error("invalid control event metadata");
|
|
566
|
+
if (!Number.isSafeInteger(event.occurredAtMs) || event.occurredAtMs < 0) throw new Error("invalid control timestamp");
|
|
567
|
+
if (Buffer.byteLength(event.payload, "utf8") > MAX_TEXT_BYTES) throw new Error("control payload exceeds host limit");
|
|
568
|
+
return openHistory(id).prepare(`
|
|
569
|
+
INSERT INTO control_requests (
|
|
570
|
+
item_id, sender_cid, sender_name_snapshot, payload,
|
|
571
|
+
occurred_at_ms, queue_state, inserted_at_ms
|
|
572
|
+
) VALUES (?, ?, ?, ?, ?, 'pending', ?)
|
|
573
|
+
ON CONFLICT(item_id) DO NOTHING
|
|
574
|
+
`).run(
|
|
575
|
+
event.itemId,
|
|
576
|
+
event.senderCid,
|
|
577
|
+
event.senderName,
|
|
578
|
+
event.payload,
|
|
579
|
+
event.occurredAtMs,
|
|
580
|
+
Date.now()
|
|
581
|
+
).changes === 1;
|
|
582
|
+
}
|
|
583
|
+
function promotePendingIntroduction(id, peerCid) {
|
|
584
|
+
const db = openHistory(id);
|
|
585
|
+
return db.transaction(() => {
|
|
586
|
+
const messages = db.prepare(`
|
|
587
|
+
UPDATE messages SET inbox_state = 'unread'
|
|
588
|
+
WHERE direction = 'in' AND peer_cid = ? AND inbox_state = 'pending_introduction'
|
|
589
|
+
`).run(peerCid).changes;
|
|
590
|
+
const files = db.prepare(`
|
|
591
|
+
UPDATE files SET inbox_state = 'unread'
|
|
592
|
+
WHERE direction = 'in' AND peer_cid = ? AND inbox_state = 'pending_introduction'
|
|
593
|
+
`).run(peerCid).changes;
|
|
594
|
+
return messages + files;
|
|
595
|
+
})();
|
|
596
|
+
}
|
|
597
|
+
function rejectPendingIntroduction(id, peerCid) {
|
|
598
|
+
const db = openHistory(id);
|
|
599
|
+
return db.transaction(() => {
|
|
600
|
+
const messages = db.prepare(`
|
|
601
|
+
DELETE FROM messages
|
|
602
|
+
WHERE direction = 'in' AND peer_cid = ? AND inbox_state = 'pending_introduction'
|
|
603
|
+
`).run(peerCid).changes;
|
|
604
|
+
const files = db.prepare(`
|
|
605
|
+
DELETE FROM files
|
|
606
|
+
WHERE direction = 'in' AND peer_cid = ? AND inbox_state = 'pending_introduction'
|
|
607
|
+
`).run(peerCid).changes;
|
|
608
|
+
return messages + files;
|
|
609
|
+
})();
|
|
610
|
+
}
|
|
611
|
+
function batchLimit(value) {
|
|
612
|
+
const limit = value ?? DEFAULT_BATCH;
|
|
613
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_BATCH) {
|
|
614
|
+
throw new Error(`limit must be an integer from 1 to ${MAX_BATCH}`);
|
|
615
|
+
}
|
|
616
|
+
return limit;
|
|
617
|
+
}
|
|
618
|
+
function listIncomingMessages(id) {
|
|
619
|
+
const rows = openHistory(id).prepare(`
|
|
620
|
+
SELECT seq, wire_id, peer_cid, peer_name_snapshot, occurred_at_ms,
|
|
621
|
+
reply_to_wire_id, reply_to_sentence, encryption
|
|
622
|
+
FROM messages
|
|
623
|
+
WHERE direction = 'in' AND inbox_state = 'unread'
|
|
624
|
+
ORDER BY seq ASC
|
|
625
|
+
`).all();
|
|
626
|
+
return rows.map((row) => ({
|
|
627
|
+
seq: row.seq,
|
|
628
|
+
msg_id: row.seq,
|
|
629
|
+
wire_id: row.wire_id,
|
|
630
|
+
from: { id: row.peer_cid, name: row.peer_name_snapshot },
|
|
631
|
+
occurred_at_ms: row.occurred_at_ms,
|
|
632
|
+
date: iso(row.occurred_at_ms),
|
|
633
|
+
encryption: row.encryption,
|
|
634
|
+
inbox_state: "unread",
|
|
635
|
+
status: "unread",
|
|
636
|
+
reply_to: reply(row)
|
|
637
|
+
}));
|
|
638
|
+
}
|
|
639
|
+
function takeUnreadMessages(id, input = {}) {
|
|
640
|
+
const db = openHistory(id);
|
|
641
|
+
const limit = batchLimit(input.limit);
|
|
642
|
+
const requested = input.wire_ids;
|
|
643
|
+
return db.transaction(() => {
|
|
644
|
+
let rows;
|
|
645
|
+
if (requested !== void 0) {
|
|
646
|
+
if (requested.length < 1 || requested.length > MAX_BATCH) {
|
|
647
|
+
throw new Error(`wire_ids must contain 1-${MAX_BATCH} items`);
|
|
648
|
+
}
|
|
649
|
+
if (new Set(requested).size !== requested.length) {
|
|
650
|
+
throw new Error("wire_ids must not contain duplicates");
|
|
651
|
+
}
|
|
652
|
+
const get = db.prepare(`
|
|
653
|
+
SELECT * FROM messages WHERE wire_id = ? AND direction = 'in' AND inbox_state = 'unread'
|
|
654
|
+
`);
|
|
655
|
+
rows = requested.map((wireId) => get.get(wireId)).filter((row) => !!row);
|
|
656
|
+
if (rows.length !== requested.length) {
|
|
657
|
+
throw new Error("selected message wire_id is unknown, stale, or no longer unread");
|
|
658
|
+
}
|
|
659
|
+
} else {
|
|
660
|
+
rows = db.prepare(`
|
|
661
|
+
SELECT * FROM messages
|
|
662
|
+
WHERE direction = 'in' AND inbox_state = 'unread'
|
|
663
|
+
ORDER BY seq ASC LIMIT ?
|
|
664
|
+
`).all(limit);
|
|
665
|
+
}
|
|
666
|
+
const mark = db.prepare(`
|
|
667
|
+
UPDATE messages SET inbox_state = 'read'
|
|
668
|
+
WHERE seq = ? AND direction = 'in' AND inbox_state = 'unread'
|
|
669
|
+
`);
|
|
670
|
+
for (const row of rows) {
|
|
671
|
+
if (mark.run(row.seq).changes !== 1) throw new Error(`message row ${row.seq} changed during read batch`);
|
|
672
|
+
row.inbox_state = "read";
|
|
673
|
+
}
|
|
674
|
+
const remaining = Number(db.prepare(`
|
|
675
|
+
SELECT COUNT(*) FROM messages WHERE direction = 'in' AND inbox_state = 'unread'
|
|
676
|
+
`).pluck().get());
|
|
677
|
+
return { messages: rows.map(messageFromRow), remaining };
|
|
678
|
+
})();
|
|
679
|
+
}
|
|
680
|
+
function listIncomingFiles(id) {
|
|
681
|
+
const rows = openHistory(id).prepare(`
|
|
682
|
+
SELECT * FROM files WHERE direction = 'in' AND inbox_state = 'unread' ORDER BY seq ASC
|
|
683
|
+
`).all();
|
|
684
|
+
return rows.map((row) => fileFromRow(id, row));
|
|
685
|
+
}
|
|
686
|
+
function takeUnreadFiles(id, input = {}) {
|
|
687
|
+
const db = openHistory(id);
|
|
688
|
+
const limit = batchLimit(input.limit);
|
|
689
|
+
const requested = input.wire_ids;
|
|
690
|
+
return db.transaction(() => {
|
|
691
|
+
let rows;
|
|
692
|
+
if (requested !== void 0) {
|
|
693
|
+
if (requested.length < 1 || requested.length > MAX_BATCH) throw new Error(`wire_ids must contain 1-${MAX_BATCH} items`);
|
|
694
|
+
if (new Set(requested).size !== requested.length) throw new Error("wire_ids must not contain duplicates");
|
|
695
|
+
const get = db.prepare(`
|
|
696
|
+
SELECT * FROM files WHERE wire_id = ? AND direction = 'in' AND inbox_state = 'unread'
|
|
697
|
+
`);
|
|
698
|
+
rows = requested.map((wireId) => get.get(wireId)).filter((row) => !!row);
|
|
699
|
+
if (rows.length !== requested.length) throw new Error("selected file wire_id is unknown, stale, or no longer unread");
|
|
700
|
+
} else {
|
|
701
|
+
rows = db.prepare(`
|
|
702
|
+
SELECT * FROM files WHERE direction = 'in' AND inbox_state = 'unread'
|
|
703
|
+
ORDER BY seq ASC LIMIT ?
|
|
704
|
+
`).all(limit);
|
|
705
|
+
}
|
|
706
|
+
const mark = db.prepare(`
|
|
707
|
+
UPDATE files SET inbox_state = 'read'
|
|
708
|
+
WHERE seq = ? AND direction = 'in' AND inbox_state = 'unread'
|
|
709
|
+
`);
|
|
710
|
+
for (const row of rows) {
|
|
711
|
+
if (mark.run(row.seq).changes !== 1) throw new Error(`file row ${row.seq} changed during read batch`);
|
|
712
|
+
row.inbox_state = "read";
|
|
713
|
+
}
|
|
714
|
+
const remaining = Number(db.prepare(`
|
|
715
|
+
SELECT COUNT(*) FROM files WHERE direction = 'in' AND inbox_state = 'unread'
|
|
716
|
+
`).pluck().get());
|
|
717
|
+
return { files: rows.map((row) => fileFromRow(id, row)), remaining };
|
|
718
|
+
})();
|
|
719
|
+
}
|
|
720
|
+
function historyWhere(query) {
|
|
721
|
+
const clauses = [];
|
|
722
|
+
const args = [];
|
|
723
|
+
if (query.peer_cid !== void 0) {
|
|
724
|
+
clauses.push("peer_cid = ?");
|
|
725
|
+
args.push(query.peer_cid);
|
|
726
|
+
}
|
|
727
|
+
if (query.direction !== void 0) {
|
|
728
|
+
clauses.push("direction = ?");
|
|
729
|
+
args.push(query.direction);
|
|
730
|
+
}
|
|
731
|
+
if (query.before_seq !== void 0) {
|
|
732
|
+
clauses.push("seq < ?");
|
|
733
|
+
args.push(query.before_seq);
|
|
734
|
+
}
|
|
735
|
+
return { sql: clauses.length ? `WHERE ${clauses.join(" AND ")}` : "", args };
|
|
736
|
+
}
|
|
737
|
+
function listMessageHistory(id, query = {}) {
|
|
738
|
+
const db = openHistory(id);
|
|
739
|
+
const limit = batchLimit(query.limit);
|
|
740
|
+
const where = historyWhere(query);
|
|
741
|
+
const rows = db.prepare(`SELECT * FROM messages ${where.sql} ORDER BY seq DESC LIMIT ?`).all(...where.args, limit + 1);
|
|
742
|
+
const hasMore = rows.length > limit;
|
|
743
|
+
if (hasMore) rows.pop();
|
|
744
|
+
return { items: rows.map(messageFromRow), next_cursor: hasMore ? rows.at(-1).seq : null };
|
|
745
|
+
}
|
|
746
|
+
function getMessageHistoryItem(id, wireId) {
|
|
747
|
+
const row = openHistory(id).prepare("SELECT * FROM messages WHERE wire_id = ?").get(wireId);
|
|
748
|
+
return row ? messageFromRow(row) : null;
|
|
749
|
+
}
|
|
750
|
+
function getMessageHistorySummary(id, input) {
|
|
751
|
+
const row = openHistory(id).prepare(`
|
|
752
|
+
SELECT COUNT(*) AS total,
|
|
753
|
+
SUM(CASE WHEN direction = 'in' AND inbox_state = 'unread' THEN 1 ELSE 0 END) AS unread
|
|
754
|
+
FROM messages WHERE peer_cid = ?
|
|
755
|
+
`).get(input.peer_cid);
|
|
756
|
+
return { total: Number(row.total), unread: Number(row.unread ?? 0) };
|
|
757
|
+
}
|
|
758
|
+
function listFileHistory(id, query = {}) {
|
|
759
|
+
const db = openHistory(id);
|
|
760
|
+
const limit = batchLimit(query.limit);
|
|
761
|
+
const where = historyWhere(query);
|
|
762
|
+
const rows = db.prepare(`SELECT * FROM files ${where.sql} ORDER BY seq DESC LIMIT ?`).all(...where.args, limit + 1);
|
|
763
|
+
const hasMore = rows.length > limit;
|
|
764
|
+
if (hasMore) rows.pop();
|
|
765
|
+
return { items: rows.map((row) => fileFromRow(id, row)), next_cursor: hasMore ? rows.at(-1).seq : null };
|
|
766
|
+
}
|
|
767
|
+
function getFileHistoryItem(id, wireId) {
|
|
768
|
+
const row = openHistory(id).prepare("SELECT * FROM files WHERE wire_id = ?").get(wireId);
|
|
769
|
+
return row ? fileFromRow(id, row) : null;
|
|
770
|
+
}
|
|
771
|
+
function resolveStoredFile(id, wireId) {
|
|
772
|
+
const item = getFileHistoryItem(id, wireId);
|
|
773
|
+
if (!item || !fs3.existsSync(item.blob_path)) return null;
|
|
774
|
+
const identityRoot = resolve2(id.dir);
|
|
775
|
+
const resolvedPath = resolve2(item.blob_path);
|
|
776
|
+
return resolvedPath.startsWith(`${identityRoot}${sep}`) ? resolvedPath : null;
|
|
777
|
+
}
|
|
778
|
+
function updateDeliveryState(id, peerCid, kind, wireIds) {
|
|
779
|
+
const db = openHistory(id);
|
|
780
|
+
const rank = kind === "read" ? 2 : 1;
|
|
781
|
+
const tx = db.transaction(() => {
|
|
782
|
+
for (const wireId of wireIds) {
|
|
783
|
+
for (const table of ["messages", "files"]) {
|
|
784
|
+
db.prepare(`
|
|
785
|
+
UPDATE ${table}
|
|
786
|
+
SET delivery_state = ?, human_read_at_ms = CASE WHEN ? = 'read' THEN COALESCE(human_read_at_ms, ?) ELSE human_read_at_ms END
|
|
787
|
+
WHERE wire_id = ? AND peer_cid = ? AND direction = 'out'
|
|
788
|
+
AND (CASE delivery_state WHEN 'read' THEN 2 WHEN 'delivered' THEN 1 WHEN 'sent' THEN 0 ELSE -1 END) < ?
|
|
789
|
+
`).run(kind, kind, Date.now(), wireId, peerCid, rank);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
tx();
|
|
794
|
+
}
|
|
192
795
|
|
|
193
796
|
// ../../src/constants.ts
|
|
194
797
|
var FILE_SELECTION_CAP = 32;
|
|
@@ -207,16 +810,15 @@ function setRegistrar(id) {
|
|
|
207
810
|
function setRegistrarAdBlob(blob) {
|
|
208
811
|
registrarAdBlob = blob;
|
|
209
812
|
}
|
|
210
|
-
var identityDir = (name) =>
|
|
211
|
-
var keyPath = (dir) =>
|
|
212
|
-
var dataPath = (dir) =>
|
|
213
|
-
var notifyLogPath = (dir) =>
|
|
214
|
-
var unreadPath = (dir) =>
|
|
215
|
-
var
|
|
216
|
-
var
|
|
217
|
-
var hashLeaseToken = (token) => createHash("sha256").update(token).digest("hex");
|
|
813
|
+
var identityDir = (name) => join4(STATE_DIR, name);
|
|
814
|
+
var keyPath = (dir) => join4(dir, "identity.key");
|
|
815
|
+
var dataPath = (dir) => join4(dir, "state_data.bin");
|
|
816
|
+
var notifyLogPath = (dir) => join4(dir, "notifications.log");
|
|
817
|
+
var unreadPath = (dir) => join4(dir, "unread.json");
|
|
818
|
+
var tempMetaPath = (dir) => join4(dir, "temp.json");
|
|
819
|
+
var hashLeaseToken = (token) => createHash2("sha256").update(token).digest("hex");
|
|
218
820
|
function writeTempMetaFile(dir, meta) {
|
|
219
|
-
|
|
821
|
+
fs4.writeFileSync(
|
|
220
822
|
tempMetaPath(dir),
|
|
221
823
|
JSON.stringify({ v: 1, owner: { token_sha256: meta.owner.tokenHash, pid: meta.owner.pid }, created_at: meta.createdAt }),
|
|
222
824
|
{ mode: 384 }
|
|
@@ -224,7 +826,7 @@ function writeTempMetaFile(dir, meta) {
|
|
|
224
826
|
}
|
|
225
827
|
function readTempMetaFile(dir) {
|
|
226
828
|
try {
|
|
227
|
-
const raw = JSON.parse(
|
|
829
|
+
const raw = JSON.parse(fs4.readFileSync(tempMetaPath(dir), "utf8"));
|
|
228
830
|
if (raw.v !== 1 || typeof raw.owner?.token_sha256 !== "string" || typeof raw.owner?.pid !== "number") return null;
|
|
229
831
|
return {
|
|
230
832
|
owner: { tokenHash: raw.owner.token_sha256, pid: raw.owner.pid },
|
|
@@ -236,45 +838,58 @@ function readTempMetaFile(dir) {
|
|
|
236
838
|
}
|
|
237
839
|
function tightenIdentityPerms() {
|
|
238
840
|
for (const name of listPersistedNames()) {
|
|
239
|
-
const dir =
|
|
841
|
+
const dir = join4(STATE_DIR, name);
|
|
240
842
|
try {
|
|
241
|
-
|
|
843
|
+
fs4.chmodSync(dir, 448);
|
|
242
844
|
} catch (err) {
|
|
243
845
|
log(`[${name}] chmod 0700 failed:`, String(err));
|
|
244
846
|
}
|
|
245
|
-
for (const f of [dataPath(dir), keyPath(dir)]) {
|
|
246
|
-
if (!
|
|
847
|
+
for (const f of [dataPath(dir), keyPath(dir), historyPath({ dir }), `${historyPath({ dir })}-wal`, `${historyPath({ dir })}-shm`, notifyLogPath(dir), unreadPath(dir)]) {
|
|
848
|
+
if (!fs4.existsSync(f)) continue;
|
|
247
849
|
try {
|
|
248
|
-
|
|
850
|
+
fs4.chmodSync(f, 384);
|
|
249
851
|
} catch (err) {
|
|
250
852
|
log(`[${name}] chmod 0600 ${f} failed:`, String(err));
|
|
251
853
|
}
|
|
252
854
|
}
|
|
855
|
+
const blobs = join4(dir, "blobs");
|
|
856
|
+
if (fs4.existsSync(blobs)) {
|
|
857
|
+
const tightenTree = (path) => {
|
|
858
|
+
for (const entry of fs4.readdirSync(path, { withFileTypes: true })) {
|
|
859
|
+
const child = join4(path, entry.name);
|
|
860
|
+
if (entry.isDirectory()) {
|
|
861
|
+
fs4.chmodSync(child, 448);
|
|
862
|
+
tightenTree(child);
|
|
863
|
+
} else if (entry.isFile()) fs4.chmodSync(child, 384);
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
try {
|
|
867
|
+
fs4.chmodSync(blobs, 448);
|
|
868
|
+
tightenTree(blobs);
|
|
869
|
+
} catch (err) {
|
|
870
|
+
log(`[${name}] chmod history blobs failed:`, String(err));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
253
873
|
}
|
|
254
874
|
}
|
|
255
875
|
var isWireId = (s) => /^[A-Za-z0-9]+$/.test(s) && s.length > 0 && s.length <= 128;
|
|
256
876
|
var isSelectableWireId = (s) => /^[A-Fa-f0-9]{64}$/.test(s);
|
|
257
877
|
function findIdentityFile(id, wireId) {
|
|
258
878
|
if (!isWireId(wireId)) return null;
|
|
259
|
-
const dir = filesDirFor(id);
|
|
260
|
-
let entries;
|
|
261
879
|
try {
|
|
262
|
-
|
|
880
|
+
return resolveStoredFile(id, wireId);
|
|
263
881
|
} catch {
|
|
264
882
|
return null;
|
|
265
883
|
}
|
|
266
|
-
const prefix = `${wireId}-`;
|
|
267
|
-
const match = entries.find((name) => name.startsWith(prefix));
|
|
268
|
-
return match ? join3(dir, match) : null;
|
|
269
884
|
}
|
|
270
885
|
function listPersistedNames() {
|
|
271
|
-
if (!
|
|
272
|
-
return
|
|
886
|
+
if (!fs4.existsSync(STATE_DIR)) return [];
|
|
887
|
+
return fs4.readdirSync(STATE_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && fs4.existsSync(keyPath(join4(STATE_DIR, d.name)))).map((d) => d.name);
|
|
273
888
|
}
|
|
274
889
|
|
|
275
890
|
// ../../src/identity/lease.ts
|
|
276
|
-
import { join as
|
|
277
|
-
import * as
|
|
891
|
+
import { join as join5 } from "node:path";
|
|
892
|
+
import * as fs5 from "node:fs";
|
|
278
893
|
var leases = /* @__PURE__ */ new Map();
|
|
279
894
|
var tombstones = /* @__PURE__ */ new Set();
|
|
280
895
|
var sessionHeaders = /* @__PURE__ */ new Map();
|
|
@@ -292,17 +907,17 @@ function leaseByToken(token) {
|
|
|
292
907
|
for (const l of leases.values()) if (l.token === token) return l;
|
|
293
908
|
return void 0;
|
|
294
909
|
}
|
|
295
|
-
var bindingsSnapshotPath = () =>
|
|
910
|
+
var bindingsSnapshotPath = () => join5(STATE_DIR, "bindings.json");
|
|
296
911
|
function persistBindings() {
|
|
297
912
|
try {
|
|
298
|
-
|
|
913
|
+
fs5.mkdirSync(STATE_DIR, { recursive: true });
|
|
299
914
|
const tmp = `${bindingsSnapshotPath()}.tmp`;
|
|
300
|
-
|
|
915
|
+
fs5.writeFileSync(tmp, JSON.stringify({
|
|
301
916
|
pid: process.pid,
|
|
302
917
|
bound: [...leases.keys()],
|
|
303
918
|
holders: [...leases.values()].map((l) => ({ identity: l.identity, pid: l.pid }))
|
|
304
919
|
}));
|
|
305
|
-
|
|
920
|
+
fs5.renameSync(tmp, bindingsSnapshotPath());
|
|
306
921
|
} catch (err) {
|
|
307
922
|
log("failed to persist bindings snapshot:", String(err));
|
|
308
923
|
}
|
|
@@ -349,18 +964,18 @@ function bindSession(sid, name) {
|
|
|
349
964
|
|
|
350
965
|
// ../../src/identity/hierarchy.ts
|
|
351
966
|
import { join as join8 } from "node:path";
|
|
352
|
-
import * as
|
|
967
|
+
import * as fs10 from "node:fs";
|
|
353
968
|
|
|
354
969
|
// ../../src/mufl/tx.ts
|
|
355
970
|
import { AdaptObjectLifetime as AdaptObjectLifetime2 } from "@adapt-toolkit/sdk/common";
|
|
356
971
|
import { object_to_adapt_value } from "@adapt-toolkit/sdk/wrapper";
|
|
357
972
|
|
|
358
973
|
// ../../src/state.ts
|
|
359
|
-
import * as
|
|
974
|
+
import * as fs9 from "node:fs";
|
|
360
975
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
361
976
|
|
|
362
977
|
// ../../src/identity/provision.ts
|
|
363
|
-
import * as
|
|
978
|
+
import * as fs8 from "node:fs";
|
|
364
979
|
import { randomBytes } from "node:crypto";
|
|
365
980
|
import { PacketWrapperConfigurator } from "@adapt-toolkit/sdk/wrappers";
|
|
366
981
|
|
|
@@ -369,42 +984,7 @@ import { AdaptObjectLifetime } from "@adapt-toolkit/sdk/common";
|
|
|
369
984
|
|
|
370
985
|
// ../../src/notify.ts
|
|
371
986
|
import { join as join6 } from "node:path";
|
|
372
|
-
import * as
|
|
373
|
-
|
|
374
|
-
// ../../src/inbox.ts
|
|
375
|
-
var E2E_RECV_EVENTS = /* @__PURE__ */ new Set(["e2e_app_recv", "migration_deferred_flush"]);
|
|
376
|
-
function e2eWireIdsFromEvents(events) {
|
|
377
|
-
const out = /* @__PURE__ */ new Set();
|
|
378
|
-
for (const ev of events) {
|
|
379
|
-
if (!ev || typeof ev !== "object") continue;
|
|
380
|
-
const event = ev.event;
|
|
381
|
-
if (typeof event !== "string" || !E2E_RECV_EVENTS.has(event)) continue;
|
|
382
|
-
const wire = ev.wire_id;
|
|
383
|
-
if (typeof wire === "string" && wire.length > 0) out.add(wire);
|
|
384
|
-
}
|
|
385
|
-
return out;
|
|
386
|
-
}
|
|
387
|
-
function encryptionFor(wireId, e2eWireIds) {
|
|
388
|
-
return wireId && e2eWireIds.has(wireId) ? "e2e" : "legacy";
|
|
389
|
-
}
|
|
390
|
-
function toMessageJson(m, e2eWireIds) {
|
|
391
|
-
const encryption = encryptionFor(m.wire_id, e2eWireIds);
|
|
392
|
-
return {
|
|
393
|
-
msg_id: m.msg_id,
|
|
394
|
-
wire_id: m.wire_id,
|
|
395
|
-
from: { id: m.sender_id, name: m.sender_name },
|
|
396
|
-
encryption,
|
|
397
|
-
transport: encryption === "e2e" ? "double_ratchet" : "legacy_box",
|
|
398
|
-
text: m.text,
|
|
399
|
-
date: m.date,
|
|
400
|
-
status: m.status,
|
|
401
|
-
reply_to: m.reply_to
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
function buildMessagesPayload(msgs, e2eWireIds) {
|
|
405
|
-
const messages = msgs.map((m) => toMessageJson(m, e2eWireIds));
|
|
406
|
-
return { count: messages.length, messages };
|
|
407
|
-
}
|
|
987
|
+
import * as fs6 from "node:fs";
|
|
408
988
|
|
|
409
989
|
// ../../src/events.ts
|
|
410
990
|
var nonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
@@ -470,618 +1050,138 @@ function emitDaemonNotification(identityName, summary) {
|
|
|
470
1050
|
handlers.notification(identityName, summary);
|
|
471
1051
|
}
|
|
472
1052
|
|
|
473
|
-
// ../../src/
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
// ../../src/files.ts
|
|
480
|
-
import { basename, extname } from "node:path";
|
|
481
|
-
var FILE_MIME = {
|
|
482
|
-
".png": "image/png",
|
|
483
|
-
".jpg": "image/jpeg",
|
|
484
|
-
".jpeg": "image/jpeg",
|
|
485
|
-
".gif": "image/gif",
|
|
486
|
-
".webp": "image/webp",
|
|
487
|
-
".svg": "image/svg+xml",
|
|
488
|
-
".bmp": "image/bmp",
|
|
489
|
-
".ico": "image/x-icon",
|
|
490
|
-
".pdf": "application/pdf",
|
|
491
|
-
".txt": "text/plain",
|
|
492
|
-
".md": "text/markdown",
|
|
493
|
-
".json": "application/json",
|
|
494
|
-
".csv": "text/csv",
|
|
495
|
-
".html": "text/html",
|
|
496
|
-
".xml": "application/xml",
|
|
497
|
-
".zip": "application/zip",
|
|
498
|
-
".gz": "application/gzip",
|
|
499
|
-
".tar": "application/x-tar",
|
|
500
|
-
".mp3": "audio/mpeg",
|
|
501
|
-
".wav": "audio/wav",
|
|
502
|
-
".mp4": "video/mp4",
|
|
503
|
-
".mov": "video/quicktime"
|
|
504
|
-
};
|
|
505
|
-
function mimeFromExt(p) {
|
|
506
|
-
return FILE_MIME[extname(p).toLowerCase()] ?? "application/octet-stream";
|
|
507
|
-
}
|
|
508
|
-
function sanitizeFilename(name) {
|
|
509
|
-
const base = basename(name).replace(/[^A-Za-z0-9._-]/g, "_");
|
|
510
|
-
return base.length ? base.slice(0, 200) : "file";
|
|
1053
|
+
// ../../src/notify.ts
|
|
1054
|
+
var installed = null;
|
|
1055
|
+
function setNotifyHook(fn) {
|
|
1056
|
+
installed?.uninstall();
|
|
1057
|
+
installed = fn === null ? null : { hook: fn, uninstall: setDaemonEventHandler("notification", fn) };
|
|
511
1058
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const parts = (mime ?? "").toLowerCase().split(";").map((p) => p.trim());
|
|
518
|
-
if (parts[0].startsWith("audio/") && parts.slice(1).includes(VOICE_MESSAGE_MIME_PARAM)) return true;
|
|
519
|
-
return filename.toLowerCase().startsWith(VOICE_MESSAGE_FILENAME_PREFIX) && parts[0].startsWith("audio/");
|
|
1059
|
+
function clearNotifyHook(fn) {
|
|
1060
|
+
if (installed?.hook === fn) {
|
|
1061
|
+
installed.uninstall();
|
|
1062
|
+
installed = null;
|
|
1063
|
+
}
|
|
520
1064
|
}
|
|
521
|
-
function
|
|
522
|
-
|
|
1065
|
+
function fireNotify(identityName, summary) {
|
|
1066
|
+
emitDaemonNotification(identityName, summary);
|
|
523
1067
|
}
|
|
524
|
-
var
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
1068
|
+
var notifyHooks = {
|
|
1069
|
+
onNotify: (identityName, summary) => fireNotify(identityName, summary)
|
|
1070
|
+
};
|
|
1071
|
+
var NOTIFY_KIND_SETS = {
|
|
1072
|
+
// Genuine inbound arrivals — something addressed to this identity landed.
|
|
1073
|
+
inbound: ["message_received", "file_received"],
|
|
1074
|
+
// Introduction bookkeeping — not an arrival, but an away agent must act on it.
|
|
1075
|
+
intro: ["local_contact_request", "pending_message"],
|
|
1076
|
+
// What `ours-mcp watch` surfaces: arrivals plus the intro events. This is
|
|
1077
|
+
// 7a7ec16's whitelist exactly, and it is a UNION rather than a third list so
|
|
1078
|
+
// the two cannot drift apart.
|
|
1079
|
+
get wake() {
|
|
1080
|
+
return [...NOTIFY_KIND_SETS.inbound, ...NOTIFY_KIND_SETS.intro];
|
|
535
1081
|
}
|
|
536
|
-
|
|
537
|
-
|
|
1082
|
+
};
|
|
1083
|
+
function parseNotifyKinds(kindsParam) {
|
|
1084
|
+
if (kindsParam === null || kindsParam === "") return null;
|
|
1085
|
+
const keep = /* @__PURE__ */ new Set();
|
|
1086
|
+
for (const raw of kindsParam.split(",")) {
|
|
1087
|
+
const kind = raw.trim();
|
|
1088
|
+
if (kind === "") continue;
|
|
1089
|
+
const events = NOTIFY_KIND_SETS[kind];
|
|
1090
|
+
if (!events) throw new Error(kind);
|
|
1091
|
+
for (const e of events) keep.add(e);
|
|
538
1092
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
1093
|
+
return keep;
|
|
1094
|
+
}
|
|
1095
|
+
function notifyEventMatches(event, keep) {
|
|
1096
|
+
if (keep === null) return true;
|
|
1097
|
+
return typeof event.event === "string" && keep.has(event.event);
|
|
1098
|
+
}
|
|
1099
|
+
var notifyWaiters = /* @__PURE__ */ new Map();
|
|
1100
|
+
function fireNotifyWaiters(name) {
|
|
1101
|
+
const set = notifyWaiters.get(name);
|
|
1102
|
+
if (!set || set.size === 0) return;
|
|
1103
|
+
for (const w of [...set]) {
|
|
1104
|
+
try {
|
|
1105
|
+
w();
|
|
1106
|
+
} catch {
|
|
542
1107
|
}
|
|
543
|
-
|
|
544
|
-
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function waitForNotify(name, ms) {
|
|
1111
|
+
return new Promise((resolve3) => {
|
|
1112
|
+
let set = notifyWaiters.get(name);
|
|
1113
|
+
if (!set) {
|
|
1114
|
+
set = /* @__PURE__ */ new Set();
|
|
1115
|
+
notifyWaiters.set(name, set);
|
|
545
1116
|
}
|
|
1117
|
+
let done = false;
|
|
1118
|
+
const finish = () => {
|
|
1119
|
+
if (done) return;
|
|
1120
|
+
done = true;
|
|
1121
|
+
set.delete(fn);
|
|
1122
|
+
if (set.size === 0) notifyWaiters.delete(name);
|
|
1123
|
+
clearTimeout(timer);
|
|
1124
|
+
resolve3();
|
|
1125
|
+
};
|
|
1126
|
+
const fn = finish;
|
|
1127
|
+
const timer = setTimeout(finish, ms);
|
|
1128
|
+
set.add(fn);
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
var NOTIFY_BODY_KEYS = /* @__PURE__ */ new Set(["body", "text", "payload", "data", "bytes", "content"]);
|
|
1132
|
+
function bodyFreeNotifyValue(value) {
|
|
1133
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return void 0;
|
|
1134
|
+
if (Array.isArray(value)) {
|
|
1135
|
+
return value.map(bodyFreeNotifyValue).filter((item) => item !== void 0);
|
|
546
1136
|
}
|
|
547
|
-
if (
|
|
548
|
-
|
|
1137
|
+
if (!value || typeof value !== "object") return value;
|
|
1138
|
+
const clean = {};
|
|
1139
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1140
|
+
if (NOTIFY_BODY_KEYS.has(key.toLowerCase())) continue;
|
|
1141
|
+
const sanitized = bodyFreeNotifyValue(item);
|
|
1142
|
+
if (sanitized !== void 0) clean[key] = sanitized;
|
|
549
1143
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
1144
|
+
return clean;
|
|
1145
|
+
}
|
|
1146
|
+
function appendNotifyLog(id, event) {
|
|
1147
|
+
const contentFree = bodyFreeNotifyValue(event);
|
|
1148
|
+
try {
|
|
1149
|
+
fs6.mkdirSync(id.dir, { recursive: true, mode: 448 });
|
|
1150
|
+
const path = notifyLogPath(id.dir);
|
|
1151
|
+
fs6.appendFileSync(path, JSON.stringify(contentFree) + "\n", { mode: 384 });
|
|
1152
|
+
fs6.chmodSync(path, 384);
|
|
1153
|
+
} catch (err) {
|
|
1154
|
+
log(`[${id.name}] failed to append notifications.log:`, String(err));
|
|
558
1155
|
}
|
|
559
|
-
|
|
1156
|
+
fireNotifyWaiters(id.name);
|
|
1157
|
+
emitDaemonEvent(id, contentFree);
|
|
560
1158
|
}
|
|
561
|
-
function
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
1159
|
+
function binHexField(av, field) {
|
|
1160
|
+
const x = av.Reduce(field);
|
|
1161
|
+
return x.IsNil() ? "" : Buffer.from(x.GetBinary()).toString("hex");
|
|
1162
|
+
}
|
|
1163
|
+
var NOTIFY_LONGPOLL_MS = Number(process.env.OURS_NOTIFY_LONGPOLL_MS) > 0 ? Number(process.env.OURS_NOTIFY_LONGPOLL_MS) : 25e3;
|
|
1164
|
+
var NOTIFY_RECHECK_MS = 250;
|
|
1165
|
+
function notifyLogSize(logPath) {
|
|
1166
|
+
try {
|
|
1167
|
+
return fs6.statSync(logPath).size;
|
|
1168
|
+
} catch {
|
|
1169
|
+
return 0;
|
|
566
1170
|
}
|
|
567
|
-
return typeof cur === "string" ? cur : void 0;
|
|
568
1171
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const
|
|
1172
|
+
function readNotifyRange(logPath, from, to) {
|
|
1173
|
+
if (to <= from) return { events: [], cursor: from };
|
|
1174
|
+
const buf = Buffer.alloc(to - from);
|
|
1175
|
+
let read = 0;
|
|
572
1176
|
try {
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
return { ok: false, error: `STT HTTP ${resp.status}${detail ? `: ${detail.slice(0, 200)}` : ""}` };
|
|
580
|
-
}
|
|
581
|
-
return { ok: true, json: await resp.json() };
|
|
582
|
-
} catch (err) {
|
|
583
|
-
if (err?.name === "AbortError") {
|
|
584
|
-
return { ok: false, error: `STT timeout after ${timeoutMs}ms` };
|
|
1177
|
+
const fd = fs6.openSync(logPath, "r");
|
|
1178
|
+
try {
|
|
1179
|
+
read = fs6.readSync(fd, buf, 0, buf.length, from);
|
|
1180
|
+
} finally {
|
|
1181
|
+
fs6.closeSync(fd);
|
|
585
1182
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
clearTimeout(timer);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
var audioBlob = (bytes, mime) => (
|
|
592
|
-
// Copy into a plain Uint8Array — a Buffer is not a valid BlobPart.
|
|
593
|
-
new Blob([new Uint8Array(bytes)], { type: baseMime(mime) })
|
|
594
|
-
);
|
|
595
|
-
var openaiCompatible = async (bytes, filename, mime, cfg, timeoutMs) => {
|
|
596
|
-
const form = new FormData();
|
|
597
|
-
form.set("file", audioBlob(bytes, mime), filename);
|
|
598
|
-
form.set("model", cfg.model.trim());
|
|
599
|
-
form.set("response_format", "json");
|
|
600
|
-
if (cfg.language?.trim()) form.set("language", cfg.language.trim());
|
|
601
|
-
const r = await request(
|
|
602
|
-
`${cfg.baseUrl.trim().replace(/\/$/, "")}/audio/transcriptions`,
|
|
603
|
-
{ method: "POST", headers: { Authorization: `Bearer ${cfg.apiKey.trim()}` }, body: form },
|
|
604
|
-
timeoutMs,
|
|
605
|
-
[cfg.apiKey.trim()]
|
|
606
|
-
);
|
|
607
|
-
if (!r.ok) return r;
|
|
608
|
-
const text = textAtPath(r.json, "text");
|
|
609
|
-
return text !== void 0 ? { ok: true, text } : { ok: false, error: "STT response missing text" };
|
|
610
|
-
};
|
|
611
|
-
var elevenlabs = async (bytes, filename, mime, cfg, timeoutMs) => {
|
|
612
|
-
const form = new FormData();
|
|
613
|
-
form.set("file", audioBlob(bytes, mime), filename);
|
|
614
|
-
form.set("model_id", cfg.model.trim());
|
|
615
|
-
if (cfg.language?.trim()) form.set("language_code", cfg.language.trim());
|
|
616
|
-
const r = await request(
|
|
617
|
-
`${(cfg.baseUrl?.trim() || "https://api.elevenlabs.io").replace(/\/$/, "")}/v1/speech-to-text`,
|
|
618
|
-
{ method: "POST", headers: { "xi-api-key": cfg.apiKey.trim() }, body: form },
|
|
619
|
-
timeoutMs,
|
|
620
|
-
[cfg.apiKey.trim()]
|
|
621
|
-
);
|
|
622
|
-
if (!r.ok) return r;
|
|
623
|
-
const text = textAtPath(r.json, "text");
|
|
624
|
-
return text !== void 0 ? { ok: true, text } : { ok: false, error: "STT response missing text" };
|
|
625
|
-
};
|
|
626
|
-
var deepgram = async (bytes, _filename, mime, cfg, timeoutMs) => {
|
|
627
|
-
const params = new URLSearchParams();
|
|
628
|
-
if (cfg.model?.trim()) params.set("model", cfg.model.trim());
|
|
629
|
-
if (cfg.language?.trim()) params.set("language", cfg.language.trim());
|
|
630
|
-
const q = params.toString();
|
|
631
|
-
const r = await request(
|
|
632
|
-
`${(cfg.baseUrl?.trim() || "https://api.deepgram.com").replace(/\/$/, "")}/v1/listen${q ? `?${q}` : ""}`,
|
|
633
|
-
{
|
|
634
|
-
method: "POST",
|
|
635
|
-
headers: { Authorization: `Token ${cfg.apiKey.trim()}`, "Content-Type": baseMime(mime) },
|
|
636
|
-
body: new Uint8Array(bytes)
|
|
637
|
-
},
|
|
638
|
-
timeoutMs,
|
|
639
|
-
[cfg.apiKey.trim()]
|
|
640
|
-
);
|
|
641
|
-
if (!r.ok) return r;
|
|
642
|
-
const text = textAtPath(r.json, "results.channels.0.alternatives.0.transcript");
|
|
643
|
-
return text !== void 0 ? { ok: true, text } : { ok: false, error: "STT response missing transcript" };
|
|
644
|
-
};
|
|
645
|
-
var custom = async (bytes, filename, mime, cfg, timeoutMs) => {
|
|
646
|
-
const t = cfg.custom;
|
|
647
|
-
const model = cfg.model?.trim() ?? "";
|
|
648
|
-
const url = t.url.replaceAll("{model}", encodeURIComponent(model));
|
|
649
|
-
const headers = {};
|
|
650
|
-
const authName = t.authHeaderName?.trim() || "Authorization";
|
|
651
|
-
const authValue = (t.authHeaderTemplate ?? "Bearer {key}").replaceAll("{key}", cfg.apiKey.trim());
|
|
652
|
-
if (authValue) headers[authName] = authValue;
|
|
653
|
-
const mode = t.bodyMode ?? "multipart";
|
|
654
|
-
const fileField = t.fileField?.trim() || "file";
|
|
655
|
-
const modelField = t.modelField === void 0 ? "model" : t.modelField.trim();
|
|
656
|
-
let body;
|
|
657
|
-
if (mode === "multipart") {
|
|
658
|
-
const form = new FormData();
|
|
659
|
-
form.set(fileField, audioBlob(bytes, mime), filename);
|
|
660
|
-
if (modelField && model) form.set(modelField, model);
|
|
661
|
-
for (const [k, v] of Object.entries(t.extraFields ?? {})) form.set(k, v);
|
|
662
|
-
body = form;
|
|
663
|
-
} else if (mode === "raw") {
|
|
664
|
-
headers["Content-Type"] = baseMime(mime);
|
|
665
|
-
body = new Uint8Array(bytes);
|
|
666
|
-
} else {
|
|
667
|
-
headers["Content-Type"] = "application/json";
|
|
668
|
-
body = JSON.stringify({
|
|
669
|
-
[fileField]: bytes.toString("base64"),
|
|
670
|
-
...modelField && model ? { [modelField]: model } : {},
|
|
671
|
-
...t.extraFields ?? {}
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
const r = await request(url, { method: t.method?.trim() || "POST", headers, body }, timeoutMs, [cfg.apiKey.trim()]);
|
|
675
|
-
if (!r.ok) return r;
|
|
676
|
-
const text = textAtPath(r.json, t.responseTextPath?.trim() || "text");
|
|
677
|
-
return text !== void 0 ? { ok: true, text } : { ok: false, error: `STT response has no text at "${t.responseTextPath?.trim() || "text"}"` };
|
|
678
|
-
};
|
|
679
|
-
var ADAPTERS = {
|
|
680
|
-
"openai-compatible": openaiCompatible,
|
|
681
|
-
elevenlabs,
|
|
682
|
-
deepgram,
|
|
683
|
-
custom
|
|
684
|
-
};
|
|
685
|
-
async function transcribeVoice(bytes, filename, mime, cfg) {
|
|
686
|
-
const status = sttStatus(cfg);
|
|
687
|
-
if (!status.ready) return { ok: false, error: status.reason };
|
|
688
|
-
const maxBytes = cfg.maxBytes ?? STT_MAX_BYTES_DEFAULT;
|
|
689
|
-
if (bytes.length > maxBytes) {
|
|
690
|
-
return { ok: false, error: `voice message is ${bytes.length} B \u2014 over the ${maxBytes} B STT limit (stt.maxBytes)` };
|
|
691
|
-
}
|
|
692
|
-
const timeoutMs = cfg.timeoutMs ?? STT_TIMEOUT_MS_DEFAULT;
|
|
693
|
-
try {
|
|
694
|
-
return await ADAPTERS[status.provider](bytes, filename, mime, cfg, timeoutMs);
|
|
695
|
-
} catch (err) {
|
|
696
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
function voiceErrorCategory(error) {
|
|
700
|
-
if (/limit|over .* bytes/i.test(error)) return "size_limit";
|
|
701
|
-
if (/timeout/i.test(error)) return "timeout";
|
|
702
|
-
if (/HTTP \d+/i.test(error)) return "provider_http";
|
|
703
|
-
if (/missing|no text|no transcript/i.test(error)) return "invalid_response";
|
|
704
|
-
return "provider_error";
|
|
705
|
-
}
|
|
706
|
-
function structuredVoiceOutcome(readiness, outcome, association) {
|
|
707
|
-
if (!readiness.ready || outcome.kind === "unconfigured") {
|
|
708
|
-
return {
|
|
709
|
-
configured: false,
|
|
710
|
-
attempted: false,
|
|
711
|
-
status: "unavailable",
|
|
712
|
-
provider: null,
|
|
713
|
-
text: null,
|
|
714
|
-
error_category: "not_configured",
|
|
715
|
-
audio_path: association.audioPath,
|
|
716
|
-
file_wire_id: association.wireId
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
if (outcome.kind === "transcript") {
|
|
720
|
-
return {
|
|
721
|
-
configured: true,
|
|
722
|
-
attempted: true,
|
|
723
|
-
status: "succeeded",
|
|
724
|
-
provider: readiness.provider,
|
|
725
|
-
text: outcome.text,
|
|
726
|
-
error_category: null,
|
|
727
|
-
audio_path: association.audioPath,
|
|
728
|
-
file_wire_id: association.wireId
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
return {
|
|
732
|
-
configured: true,
|
|
733
|
-
attempted: true,
|
|
734
|
-
status: "failed",
|
|
735
|
-
provider: readiness.provider,
|
|
736
|
-
text: null,
|
|
737
|
-
error_category: voiceErrorCategory(outcome.error),
|
|
738
|
-
audio_path: association.audioPath,
|
|
739
|
-
file_wire_id: association.wireId
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
function voiceDeliveryLine(args, outcome) {
|
|
743
|
-
const head = ` \u2022 \u{1F3A4} voice message from ${args.sender} (${args.sizeBytes} B)`;
|
|
744
|
-
const tail = `audio saved \u2192 ${args.savedPath} {${args.wire}}`;
|
|
745
|
-
switch (outcome.kind) {
|
|
746
|
-
case "transcript":
|
|
747
|
-
return `${head}: "${outcome.text}" \u2014 transcribed from voice message (STT); ${tail}`;
|
|
748
|
-
case "unconfigured":
|
|
749
|
-
return `${head}: cannot transcribe \u2014 ${outcome.reason}. Tell the user you can't listen to voice messages until the operator configures transcription on this ours-mcp server, and ask them to send text meanwhile; ${tail}`;
|
|
750
|
-
case "failed":
|
|
751
|
-
return `${head}: transcription failed (${outcome.error}). Tell the user their voice message could not be transcribed right now and ask them to send text or retry; ${tail}`;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// ../../src/render/adapt-to-json.ts
|
|
756
|
-
function renderContacts(v) {
|
|
757
|
-
const out = [];
|
|
758
|
-
if (v.IsNil()) return out;
|
|
759
|
-
for (const key of v.GetKeys()) {
|
|
760
|
-
const c = v.Reduce(key);
|
|
761
|
-
if (c.IsNil()) continue;
|
|
762
|
-
out.push({
|
|
763
|
-
name: c.Reduce("name").Visualize(),
|
|
764
|
-
container_id: c.Reduce("container_id").Visualize()
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
return out;
|
|
768
|
-
}
|
|
769
|
-
function renderImportRenames(v) {
|
|
770
|
-
const out = {};
|
|
771
|
-
if (v.IsNil()) return out;
|
|
772
|
-
for (const key of v.GetKeys()) {
|
|
773
|
-
const n = v.Reduce(key);
|
|
774
|
-
if (!n.IsNil()) out[typeof key === "string" ? key : key.Visualize()] = n.Visualize();
|
|
775
|
-
}
|
|
776
|
-
return out;
|
|
777
|
-
}
|
|
778
|
-
function renderInbox(v) {
|
|
779
|
-
const out = [];
|
|
780
|
-
if (v.IsNil()) return out;
|
|
781
|
-
for (let i = 0; ; i++) {
|
|
782
|
-
const m = v.Reduce(i);
|
|
783
|
-
if (m.IsNil()) break;
|
|
784
|
-
const rt = m.Reduce("reply_to");
|
|
785
|
-
let reply_to = null;
|
|
786
|
-
if (!rt.IsNil()) {
|
|
787
|
-
reply_to = { wire_id: rt.Reduce("wire_id").Visualize() };
|
|
788
|
-
const s = rt.Reduce("sentence");
|
|
789
|
-
if (!s.IsNil()) reply_to.sentence = parseInt(s.Visualize(), 10);
|
|
790
|
-
}
|
|
791
|
-
out.push({
|
|
792
|
-
msg_id: parseInt(m.Reduce("msg_id").Visualize(), 10),
|
|
793
|
-
sender_id: m.Reduce("sender_id").Visualize(),
|
|
794
|
-
sender_name: m.Reduce("sender_name").Visualize(),
|
|
795
|
-
text: m.Reduce("text").Visualize(),
|
|
796
|
-
date: m.Reduce("date").Visualize(),
|
|
797
|
-
status: m.Reduce("status").Visualize(),
|
|
798
|
-
wire_id: m.Reduce("wire_id").Visualize(),
|
|
799
|
-
reply_to
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
return out;
|
|
803
|
-
}
|
|
804
|
-
function renderFileMetadata(v) {
|
|
805
|
-
const files = [];
|
|
806
|
-
if (v.IsNil()) return files;
|
|
807
|
-
for (let i = 0; ; i++) {
|
|
808
|
-
const f = v.Reduce(i);
|
|
809
|
-
if (f.IsNil()) break;
|
|
810
|
-
const filename = f.Reduce("filename").Visualize();
|
|
811
|
-
const mime = f.Reduce("mime").Visualize() || "application/octet-stream";
|
|
812
|
-
const reply = f.Reduce("reply_to");
|
|
813
|
-
let reply_to = null;
|
|
814
|
-
if (!reply.IsNil()) {
|
|
815
|
-
reply_to = { wire_id: reply.Reduce("wire_id").Visualize() };
|
|
816
|
-
const sentence = parseInt(reply.Reduce("sentence").Visualize(), 10);
|
|
817
|
-
if (Number.isSafeInteger(sentence) && sentence > 0) reply_to.sentence = sentence;
|
|
818
|
-
}
|
|
819
|
-
files.push({
|
|
820
|
-
file_id: parseInt(f.Reduce("file_id").Visualize(), 10),
|
|
821
|
-
wire_id: f.Reduce("wire_id").Visualize(),
|
|
822
|
-
from: { id: f.Reduce("sender_id").Visualize(), name: f.Reduce("sender_name").Visualize() },
|
|
823
|
-
filename,
|
|
824
|
-
mime,
|
|
825
|
-
size: parseInt(f.Reduce("size").Visualize(), 10),
|
|
826
|
-
size_source: "received_payload",
|
|
827
|
-
status: f.Reduce("status").Visualize(),
|
|
828
|
-
date: f.Reduce("date").Visualize(),
|
|
829
|
-
sha256: null,
|
|
830
|
-
reply_to,
|
|
831
|
-
kind: isVoiceMessage(mime, filename) ? "voice_message" : "file"
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
return files;
|
|
835
|
-
}
|
|
836
|
-
async function writeReceivedFileSafely(dir, outPath, bytes) {
|
|
837
|
-
await mkdir(dir, { recursive: true, mode: 448 });
|
|
838
|
-
const dirStat = await lstat(dir);
|
|
839
|
-
if (!dirStat.isDirectory() || dirStat.isSymbolicLink()) throw new Error("received-files directory is not a safe directory");
|
|
840
|
-
const tmp = join5(dir, `.${basename2(outPath)}.${randomUUID()}.tmp`);
|
|
841
|
-
const handle = await open(tmp, "wx", 384);
|
|
842
|
-
try {
|
|
843
|
-
await handle.writeFile(bytes);
|
|
844
|
-
await handle.sync();
|
|
845
|
-
} catch (error) {
|
|
846
|
-
await handle.close().catch(() => {
|
|
847
|
-
});
|
|
848
|
-
await unlink(tmp).catch(() => {
|
|
849
|
-
});
|
|
850
|
-
throw error;
|
|
851
|
-
}
|
|
852
|
-
await handle.close();
|
|
853
|
-
try {
|
|
854
|
-
await rename(tmp, outPath);
|
|
855
|
-
} catch (error) {
|
|
856
|
-
await unlink(tmp).catch(() => {
|
|
857
|
-
});
|
|
858
|
-
throw error;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
async function writeIncomingFiles(id, v) {
|
|
862
|
-
const files = [];
|
|
863
|
-
if (v.IsNil()) return { text: "No new files.", files };
|
|
864
|
-
const dir = filesDirFor(id);
|
|
865
|
-
const lines = [];
|
|
866
|
-
for (let i = 0; ; i++) {
|
|
867
|
-
const f = v.Reduce(i);
|
|
868
|
-
if (f.IsNil()) break;
|
|
869
|
-
const name = f.Reduce("filename").Visualize();
|
|
870
|
-
const mime = f.Reduce("mime").Visualize() || "application/octet-stream";
|
|
871
|
-
const sender = f.Reduce("sender_name").Visualize();
|
|
872
|
-
const senderId = f.Reduce("sender_id").Visualize();
|
|
873
|
-
const fileId = parseInt(f.Reduce("file_id").Visualize(), 10);
|
|
874
|
-
const date = f.Reduce("date").Visualize();
|
|
875
|
-
const wire = f.Reduce("wire_id").Visualize();
|
|
876
|
-
const bytes = Buffer.from(f.Reduce("data").GetBinary());
|
|
877
|
-
const sha256 = createHash2("sha256").update(bytes).digest("hex");
|
|
878
|
-
const outPath = join5(dir, `${wire}-${sanitizeFilename(name)}`);
|
|
879
|
-
await writeReceivedFileSafely(dir, outPath, bytes);
|
|
880
|
-
const base = {
|
|
881
|
-
file_id: fileId,
|
|
882
|
-
wire_id: wire,
|
|
883
|
-
from: { id: senderId, name: sender },
|
|
884
|
-
filename: name,
|
|
885
|
-
path: outPath,
|
|
886
|
-
mime,
|
|
887
|
-
size: bytes.length,
|
|
888
|
-
sha256,
|
|
889
|
-
status: "processed",
|
|
890
|
-
date,
|
|
891
|
-
kind: isVoiceMessage(mime, name) ? "voice_message" : "file",
|
|
892
|
-
sender
|
|
893
|
-
};
|
|
894
|
-
if (isVoiceMessage(mime, name)) {
|
|
895
|
-
const st = sttStatus(CONFIG.stt);
|
|
896
|
-
let outcome;
|
|
897
|
-
if (!st.ready) {
|
|
898
|
-
outcome = { kind: "unconfigured", reason: st.reason };
|
|
899
|
-
} else {
|
|
900
|
-
const r = await transcribeVoice(bytes, name, mime, CONFIG.stt);
|
|
901
|
-
outcome = r.ok ? { kind: "transcript", text: r.text } : { kind: "failed", error: r.error };
|
|
902
|
-
}
|
|
903
|
-
base.transcription = structuredVoiceOutcome(st, outcome, { audioPath: outPath, wireId: wire });
|
|
904
|
-
lines.push(voiceDeliveryLine({ sender, wire, savedPath: outPath, sizeBytes: bytes.length }, outcome));
|
|
905
|
-
files.push(base);
|
|
906
|
-
continue;
|
|
907
|
-
}
|
|
908
|
-
files.push(base);
|
|
909
|
-
lines.push(` \u2022 ${name} (${mime}, ${bytes.length} B, sha256 ${sha256}) from ${sender} \u2192 ${outPath} {${wire}}`);
|
|
910
|
-
}
|
|
911
|
-
if (lines.length === 0) return { text: "No new files.", files };
|
|
912
|
-
return {
|
|
913
|
-
text: `${lines.length} new file(s) written to your identity's files dir \u2014 paths + metadata below (bytes stay on disk, never in this result). If your OS user can read the path, use it directly; otherwise use save_file({ wire_id, dest_path }) to stream a copy to a path you can write:
|
|
914
|
-
${lines.join("\n")}`,
|
|
915
|
-
files
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
function renderPending(v) {
|
|
919
|
-
const out = [];
|
|
920
|
-
if (v.IsNil()) return out;
|
|
921
|
-
for (const key of v.GetKeys()) {
|
|
922
|
-
const p = v.Reduce(key);
|
|
923
|
-
if (p.IsNil()) continue;
|
|
924
|
-
out.push({
|
|
925
|
-
container_id: typeof key === "string" ? key : key.Visualize(),
|
|
926
|
-
name: p.Reduce("name").Visualize(),
|
|
927
|
-
queued: parseInt(p.Reduce("queued").Visualize(), 10) || 0
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
return out;
|
|
931
|
-
}
|
|
932
|
-
function renderContactRoots(v) {
|
|
933
|
-
const out = {};
|
|
934
|
-
if (v.IsNil()) return out;
|
|
935
|
-
for (const key of v.GetKeys()) {
|
|
936
|
-
const r = v.Reduce(key);
|
|
937
|
-
if (r.IsNil()) continue;
|
|
938
|
-
out[typeof key === "string" ? key : key.Visualize()] = {
|
|
939
|
-
root_cid: r.Reduce("root_cid").Visualize(),
|
|
940
|
-
root_name: r.Reduce("root_name").Visualize(),
|
|
941
|
-
role_id: r.Reduce("role_id").Visualize()
|
|
942
|
-
};
|
|
943
|
-
}
|
|
944
|
-
return out;
|
|
945
|
-
}
|
|
946
|
-
function encodeWireBin(raw) {
|
|
947
|
-
return brotliCompressSync(raw).toString("base64url");
|
|
948
|
-
}
|
|
949
|
-
function decodeWireBin(s) {
|
|
950
|
-
return brotliDecompressSync(Buffer.from(s.replace(/\s+/g, ""), "base64url"));
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
// ../../src/notify.ts
|
|
954
|
-
var installed = null;
|
|
955
|
-
function setNotifyHook(fn) {
|
|
956
|
-
installed?.uninstall();
|
|
957
|
-
installed = fn === null ? null : { hook: fn, uninstall: setDaemonEventHandler("notification", fn) };
|
|
958
|
-
}
|
|
959
|
-
function clearNotifyHook(fn) {
|
|
960
|
-
if (installed?.hook === fn) {
|
|
961
|
-
installed.uninstall();
|
|
962
|
-
installed = null;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
function fireNotify(identityName, summary) {
|
|
966
|
-
emitDaemonNotification(identityName, summary);
|
|
967
|
-
}
|
|
968
|
-
var notifyHooks = {
|
|
969
|
-
onNotify: (identityName, summary) => fireNotify(identityName, summary)
|
|
970
|
-
};
|
|
971
|
-
var NOTIFY_KIND_SETS = {
|
|
972
|
-
// Genuine inbound arrivals — something addressed to this identity landed.
|
|
973
|
-
inbound: ["message_received", "file_received"],
|
|
974
|
-
// Introduction bookkeeping — not an arrival, but an away agent must act on it.
|
|
975
|
-
intro: ["local_contact_request", "pending_message"],
|
|
976
|
-
// What `ours-mcp watch` surfaces: arrivals plus the intro events. This is
|
|
977
|
-
// 7a7ec16's whitelist exactly, and it is a UNION rather than a third list so
|
|
978
|
-
// the two cannot drift apart.
|
|
979
|
-
get wake() {
|
|
980
|
-
return [...NOTIFY_KIND_SETS.inbound, ...NOTIFY_KIND_SETS.intro];
|
|
981
|
-
}
|
|
982
|
-
};
|
|
983
|
-
function parseNotifyKinds(kindsParam) {
|
|
984
|
-
if (kindsParam === null || kindsParam === "") return null;
|
|
985
|
-
const keep = /* @__PURE__ */ new Set();
|
|
986
|
-
for (const raw of kindsParam.split(",")) {
|
|
987
|
-
const kind = raw.trim();
|
|
988
|
-
if (kind === "") continue;
|
|
989
|
-
const events = NOTIFY_KIND_SETS[kind];
|
|
990
|
-
if (!events) throw new Error(kind);
|
|
991
|
-
for (const e of events) keep.add(e);
|
|
992
|
-
}
|
|
993
|
-
return keep;
|
|
994
|
-
}
|
|
995
|
-
function notifyEventMatches(event, keep) {
|
|
996
|
-
if (keep === null) return true;
|
|
997
|
-
return typeof event.event === "string" && keep.has(event.event);
|
|
998
|
-
}
|
|
999
|
-
var notifyWaiters = /* @__PURE__ */ new Map();
|
|
1000
|
-
function fireNotifyWaiters(name) {
|
|
1001
|
-
const set = notifyWaiters.get(name);
|
|
1002
|
-
if (!set || set.size === 0) return;
|
|
1003
|
-
for (const w of [...set]) {
|
|
1004
|
-
try {
|
|
1005
|
-
w();
|
|
1006
|
-
} catch {
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
function waitForNotify(name, ms) {
|
|
1011
|
-
return new Promise((resolve2) => {
|
|
1012
|
-
let set = notifyWaiters.get(name);
|
|
1013
|
-
if (!set) {
|
|
1014
|
-
set = /* @__PURE__ */ new Set();
|
|
1015
|
-
notifyWaiters.set(name, set);
|
|
1016
|
-
}
|
|
1017
|
-
let done = false;
|
|
1018
|
-
const finish = () => {
|
|
1019
|
-
if (done) return;
|
|
1020
|
-
done = true;
|
|
1021
|
-
set.delete(fn);
|
|
1022
|
-
if (set.size === 0) notifyWaiters.delete(name);
|
|
1023
|
-
clearTimeout(timer);
|
|
1024
|
-
resolve2();
|
|
1025
|
-
};
|
|
1026
|
-
const fn = finish;
|
|
1027
|
-
const timer = setTimeout(finish, ms);
|
|
1028
|
-
set.add(fn);
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
function appendNotifyLog(id, event) {
|
|
1032
|
-
try {
|
|
1033
|
-
fs5.mkdirSync(id.dir, { recursive: true, mode: 448 });
|
|
1034
|
-
fs5.appendFileSync(notifyLogPath(id.dir), JSON.stringify(event) + "\n");
|
|
1035
|
-
} catch (err) {
|
|
1036
|
-
log(`[${id.name}] failed to append notifications.log:`, String(err));
|
|
1037
|
-
}
|
|
1038
|
-
fireNotifyWaiters(id.name);
|
|
1039
|
-
emitDaemonEvent(id, event);
|
|
1040
|
-
}
|
|
1041
|
-
function readE2eWireIds(id) {
|
|
1042
|
-
const logPath = notifyLogPath(id.dir);
|
|
1043
|
-
let text = "";
|
|
1044
|
-
try {
|
|
1045
|
-
text = fs5.readFileSync(logPath, "utf8");
|
|
1046
|
-
} catch {
|
|
1047
|
-
return /* @__PURE__ */ new Set();
|
|
1048
|
-
}
|
|
1049
|
-
const events = [];
|
|
1050
|
-
for (const line of text.split("\n")) {
|
|
1051
|
-
if (!line.trim()) continue;
|
|
1052
|
-
try {
|
|
1053
|
-
events.push(JSON.parse(line));
|
|
1054
|
-
} catch {
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
return e2eWireIdsFromEvents(events);
|
|
1058
|
-
}
|
|
1059
|
-
function binHexField(av, field) {
|
|
1060
|
-
const x = av.Reduce(field);
|
|
1061
|
-
return x.IsNil() ? "" : Buffer.from(x.GetBinary()).toString("hex");
|
|
1062
|
-
}
|
|
1063
|
-
var NOTIFY_LONGPOLL_MS = Number(process.env.OURS_NOTIFY_LONGPOLL_MS) > 0 ? Number(process.env.OURS_NOTIFY_LONGPOLL_MS) : 25e3;
|
|
1064
|
-
var NOTIFY_RECHECK_MS = 250;
|
|
1065
|
-
function notifyLogSize(logPath) {
|
|
1066
|
-
try {
|
|
1067
|
-
return fs5.statSync(logPath).size;
|
|
1068
|
-
} catch {
|
|
1069
|
-
return 0;
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
function readNotifyRange(logPath, from, to) {
|
|
1073
|
-
if (to <= from) return { events: [], cursor: from };
|
|
1074
|
-
const buf = Buffer.alloc(to - from);
|
|
1075
|
-
let read = 0;
|
|
1076
|
-
try {
|
|
1077
|
-
const fd = fs5.openSync(logPath, "r");
|
|
1078
|
-
try {
|
|
1079
|
-
read = fs5.readSync(fd, buf, 0, buf.length, from);
|
|
1080
|
-
} finally {
|
|
1081
|
-
fs5.closeSync(fd);
|
|
1082
|
-
}
|
|
1083
|
-
} catch {
|
|
1084
|
-
return { events: [], cursor: from };
|
|
1183
|
+
} catch {
|
|
1184
|
+
return { events: [], cursor: from };
|
|
1085
1185
|
}
|
|
1086
1186
|
const slice = buf.subarray(0, read);
|
|
1087
1187
|
const lastNl = slice.lastIndexOf(10);
|
|
@@ -1151,40 +1251,28 @@ async function serveNotifications(req, res, name, sinceParam, kindsParam = null)
|
|
|
1151
1251
|
}
|
|
1152
1252
|
function refreshUnread(id) {
|
|
1153
1253
|
try {
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
file_id: f.Reduce("file_id").Visualize(),
|
|
1166
|
-
sender_id: f.Reduce("sender_id").Visualize(),
|
|
1167
|
-
from: f.Reduce("sender_name").Visualize(),
|
|
1168
|
-
filename: f.Reduce("filename").Visualize(),
|
|
1169
|
-
mime: f.Reduce("mime").Visualize(),
|
|
1170
|
-
bytes: f.Reduce("size").Visualize(),
|
|
1171
|
-
date: f.Reduce("date").Visualize(),
|
|
1172
|
-
wire_id: f.Reduce("wire_id").Visualize()
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
return { unread: unread2, unreadFiles: unreadFiles2 };
|
|
1177
|
-
});
|
|
1254
|
+
const unread = listIncomingMessages(id);
|
|
1255
|
+
const unreadFiles = listIncomingFiles(id).map((file) => ({
|
|
1256
|
+
file_id: String(file.file_id),
|
|
1257
|
+
sender_id: file.from.id,
|
|
1258
|
+
from: file.from.name,
|
|
1259
|
+
filename: file.filename,
|
|
1260
|
+
mime: file.mime,
|
|
1261
|
+
bytes: String(file.byte_length),
|
|
1262
|
+
date: file.date,
|
|
1263
|
+
wire_id: file.wire_id
|
|
1264
|
+
}));
|
|
1178
1265
|
const snapshot = {
|
|
1179
1266
|
count: unread.length,
|
|
1180
|
-
recent: unread.slice(-10).map((m) => ({ from: m.
|
|
1267
|
+
recent: unread.slice(-10).map((m) => ({ from: m.from.name, msg_id: m.msg_id, date: m.date })),
|
|
1181
1268
|
files: unreadFiles.length,
|
|
1182
1269
|
unread_files: unreadFiles.slice(-10)
|
|
1183
1270
|
};
|
|
1184
|
-
|
|
1271
|
+
fs6.mkdirSync(id.dir, { recursive: true, mode: 448 });
|
|
1185
1272
|
const tmp = `${unreadPath(id.dir)}.tmp`;
|
|
1186
|
-
|
|
1187
|
-
|
|
1273
|
+
fs6.writeFileSync(tmp, JSON.stringify(snapshot), { mode: 384 });
|
|
1274
|
+
fs6.chmodSync(tmp, 384);
|
|
1275
|
+
fs6.renameSync(tmp, unreadPath(id.dir));
|
|
1188
1276
|
} catch (err) {
|
|
1189
1277
|
log(`[${id.name}] failed to refresh unread snapshot:`, String(err));
|
|
1190
1278
|
}
|
|
@@ -1193,7 +1281,7 @@ function unreadSummary() {
|
|
|
1193
1281
|
const out = [];
|
|
1194
1282
|
let entries = [];
|
|
1195
1283
|
try {
|
|
1196
|
-
entries =
|
|
1284
|
+
entries = fs6.readdirSync(STATE_DIR, { withFileTypes: true });
|
|
1197
1285
|
} catch {
|
|
1198
1286
|
return { identities: out };
|
|
1199
1287
|
}
|
|
@@ -1201,7 +1289,7 @@ function unreadSummary() {
|
|
|
1201
1289
|
if (!entry.isDirectory() || validateName(entry.name) !== null) continue;
|
|
1202
1290
|
let value;
|
|
1203
1291
|
try {
|
|
1204
|
-
value = JSON.parse(
|
|
1292
|
+
value = JSON.parse(fs6.readFileSync(unreadPath(join6(STATE_DIR, entry.name)), "utf8"));
|
|
1205
1293
|
} catch {
|
|
1206
1294
|
continue;
|
|
1207
1295
|
}
|
|
@@ -1355,6 +1443,28 @@ async function e2eRecoverySweep(id) {
|
|
|
1355
1443
|
}
|
|
1356
1444
|
|
|
1357
1445
|
// ../../src/mufl/handlers.ts
|
|
1446
|
+
function eventTimeMs(value) {
|
|
1447
|
+
const shown = value.Visualize();
|
|
1448
|
+
const numeric = Number(shown);
|
|
1449
|
+
if (Number.isFinite(numeric) && numeric >= 0) return numeric;
|
|
1450
|
+
const parsed = Date.parse(shown);
|
|
1451
|
+
if (!Number.isFinite(parsed)) throw new Error("invalid host-event date");
|
|
1452
|
+
return parsed;
|
|
1453
|
+
}
|
|
1454
|
+
function replyFields(payload) {
|
|
1455
|
+
const reply2 = payload.Reduce("reply_to");
|
|
1456
|
+
if (reply2.IsNil()) return {};
|
|
1457
|
+
const wireId = reply2.Reduce("wire_id").Visualize();
|
|
1458
|
+
const sentenceValue = reply2.Reduce("sentence");
|
|
1459
|
+
return sentenceValue.IsNil() ? { replyToWireId: wireId } : { replyToWireId: wireId, replyToSentence: Number(sentenceValue.Visualize()) };
|
|
1460
|
+
}
|
|
1461
|
+
function appendPostInsertWake(id, event) {
|
|
1462
|
+
try {
|
|
1463
|
+
appendNotifyLog(id, event);
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
log(`[${id.name}] post-insert history wake failed event=${String(event.event ?? "")}:`, String(error));
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1358
1468
|
function wireHandlers(id, hooks) {
|
|
1359
1469
|
id.pw.on_return_data = (data) => {
|
|
1360
1470
|
const lt = new AdaptObjectLifetime();
|
|
@@ -1365,31 +1475,133 @@ function wireHandlers(id, hooks) {
|
|
|
1365
1475
|
saveStateFailClosed(id);
|
|
1366
1476
|
return;
|
|
1367
1477
|
}
|
|
1478
|
+
if (kind === "host_event") {
|
|
1479
|
+
const payload = data.Reduce("payload");
|
|
1480
|
+
const event = payload.Reduce("event").Visualize();
|
|
1481
|
+
if (event === "application_payload") {
|
|
1482
|
+
const itemKind = payload.Reduce("item_kind").Visualize();
|
|
1483
|
+
const direction = payload.Reduce("direction").Visualize();
|
|
1484
|
+
const wireId = payload.Reduce("wire_id").Visualize();
|
|
1485
|
+
const peerCid = payload.Reduce("peer_cid").Visualize();
|
|
1486
|
+
const peerName = payload.Reduce("peer_name").Visualize();
|
|
1487
|
+
const encryption = payload.Reduce("encryption").Visualize();
|
|
1488
|
+
const inboxValue = payload.Reduce("inbox_state");
|
|
1489
|
+
const inboxState = direction === "in" && !inboxValue.IsNil() ? inboxValue.Visualize() : void 0;
|
|
1490
|
+
try {
|
|
1491
|
+
const inserted = itemKind === "message" ? ingestApplicationEvent(id, {
|
|
1492
|
+
kind: "message",
|
|
1493
|
+
wireId,
|
|
1494
|
+
peerCid,
|
|
1495
|
+
peerName,
|
|
1496
|
+
direction,
|
|
1497
|
+
inboxState,
|
|
1498
|
+
body: payload.Reduce("body").Visualize(),
|
|
1499
|
+
occurredAtMs: eventTimeMs(payload.Reduce("date")),
|
|
1500
|
+
encryption,
|
|
1501
|
+
...replyFields(payload)
|
|
1502
|
+
}) : ingestApplicationEvent(id, {
|
|
1503
|
+
kind: "file",
|
|
1504
|
+
wireId,
|
|
1505
|
+
peerCid,
|
|
1506
|
+
peerName,
|
|
1507
|
+
direction,
|
|
1508
|
+
inboxState,
|
|
1509
|
+
filename: payload.Reduce("filename").Visualize(),
|
|
1510
|
+
mime: payload.Reduce("mime").Visualize(),
|
|
1511
|
+
bytes: Buffer.from(payload.Reduce("data").GetBinary()),
|
|
1512
|
+
occurredAtMs: eventTimeMs(payload.Reduce("date")),
|
|
1513
|
+
encryption,
|
|
1514
|
+
...replyFields(payload)
|
|
1515
|
+
});
|
|
1516
|
+
if (inserted && direction === "in" && inboxState === "unread") {
|
|
1517
|
+
if (itemKind === "message") {
|
|
1518
|
+
const item = getMessageHistoryItem(id, wireId);
|
|
1519
|
+
appendPostInsertWake(id, {
|
|
1520
|
+
event: "message_received",
|
|
1521
|
+
from: peerName,
|
|
1522
|
+
sender_name: peerName,
|
|
1523
|
+
sender_id: peerCid,
|
|
1524
|
+
msg_id: String(item.msg_id),
|
|
1525
|
+
wire_id: wireId,
|
|
1526
|
+
date: item.date
|
|
1527
|
+
});
|
|
1528
|
+
refreshUnread(id);
|
|
1529
|
+
process.nextTick(
|
|
1530
|
+
() => hooks.onNotify(id.name, `[${id.name}] new message from ${peerName} (#${item.msg_id})`)
|
|
1531
|
+
);
|
|
1532
|
+
} else {
|
|
1533
|
+
const item = getFileHistoryItem(id, wireId);
|
|
1534
|
+
appendPostInsertWake(id, {
|
|
1535
|
+
event: "file_received",
|
|
1536
|
+
from: peerName,
|
|
1537
|
+
sender_id: peerCid,
|
|
1538
|
+
file_id: String(item.file_id),
|
|
1539
|
+
wire_id: wireId,
|
|
1540
|
+
filename: item.filename,
|
|
1541
|
+
mime: item.mime,
|
|
1542
|
+
bytes: String(item.byte_length),
|
|
1543
|
+
date: item.date
|
|
1544
|
+
});
|
|
1545
|
+
refreshUnread(id);
|
|
1546
|
+
process.nextTick(() => hooks.onNotify(
|
|
1547
|
+
id.name,
|
|
1548
|
+
`[${id.name}] new file ${item.filename} (${item.byte_length} B) from ${peerName} (sender_id ${peerCid}, file_id ${item.file_id}, wire_id ${wireId})`
|
|
1549
|
+
));
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
const failureKind = itemKind === "file" ? "file" : "message";
|
|
1554
|
+
noteHistoryFailure(id, failureKind, wireId, direction);
|
|
1555
|
+
log(`[${id.name}] history ${failureKind} write failed direction=${direction} wire_id=${wireId}:`, String(error));
|
|
1556
|
+
}
|
|
1557
|
+
} else if (event === "pending_introduction_decision") {
|
|
1558
|
+
const peerCid = payload.Reduce("peer_cid").Visualize();
|
|
1559
|
+
const action = payload.Reduce("action").Visualize();
|
|
1560
|
+
try {
|
|
1561
|
+
if (action === "approve") promotePendingIntroduction(id, peerCid);
|
|
1562
|
+
else if (action === "reject") rejectPendingIntroduction(id, peerCid);
|
|
1563
|
+
refreshUnread(id);
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
log(`[${id.name}] pending-introduction history update failed action=${action} peer_cid=${peerCid}:`, String(error));
|
|
1566
|
+
}
|
|
1567
|
+
} else if (event === "monitoring_payload") {
|
|
1568
|
+
const itemId = payload.Reduce("item_id").Visualize();
|
|
1569
|
+
try {
|
|
1570
|
+
if (ingestMonitoringEvent(id, {
|
|
1571
|
+
kind: "monitoring_copy",
|
|
1572
|
+
itemId,
|
|
1573
|
+
sourceCid: payload.Reduce("source_cid").Visualize(),
|
|
1574
|
+
sourceName: payload.Reduce("source_name").Visualize(),
|
|
1575
|
+
direction: payload.Reduce("direction").Visualize(),
|
|
1576
|
+
peerCid: payload.Reduce("peer_cid").Visualize(),
|
|
1577
|
+
peerName: payload.Reduce("peer_name").Visualize(),
|
|
1578
|
+
body: payload.Reduce("body").Visualize(),
|
|
1579
|
+
occurredAtMs: eventTimeMs(payload.Reduce("date"))
|
|
1580
|
+
})) appendPostInsertWake(id, { event: "monitoring_copy", item_id: itemId });
|
|
1581
|
+
} catch (error) {
|
|
1582
|
+
log(`[${id.name}] monitoring history write failed item_id=${itemId}:`, String(error));
|
|
1583
|
+
}
|
|
1584
|
+
} else if (event === "control_payload") {
|
|
1585
|
+
const itemId = payload.Reduce("item_id").Visualize();
|
|
1586
|
+
try {
|
|
1587
|
+
if (ingestControlEvent(id, {
|
|
1588
|
+
kind: "control_request",
|
|
1589
|
+
itemId,
|
|
1590
|
+
senderCid: payload.Reduce("sender_cid").Visualize(),
|
|
1591
|
+
senderName: payload.Reduce("sender_name").Visualize(),
|
|
1592
|
+
payload: payload.Reduce("payload").Visualize(),
|
|
1593
|
+
occurredAtMs: eventTimeMs(payload.Reduce("date"))
|
|
1594
|
+
})) appendPostInsertWake(id, { event: "control_request", item_id: itemId });
|
|
1595
|
+
} catch (error) {
|
|
1596
|
+
log(`[${id.name}] control history write failed item_id=${itemId}:`, String(error));
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1368
1601
|
if (kind === "notify_agent") {
|
|
1369
1602
|
const payload = data.Reduce("payload");
|
|
1370
1603
|
const event = payload.Reduce("event").Visualize();
|
|
1371
|
-
if (event === "
|
|
1372
|
-
const sender = payload.Reduce("sender_name").Visualize();
|
|
1373
|
-
const senderIdValue = payload.Reduce("sender_id");
|
|
1374
|
-
const senderId = senderIdValue.IsNil() ? "" : senderIdValue.Visualize();
|
|
1375
|
-
const msgId = payload.Reduce("msg_id").Visualize();
|
|
1376
|
-
const wireIdValue = payload.Reduce("wire_id");
|
|
1377
|
-
const wireId = wireIdValue.IsNil() ? "" : wireIdValue.Visualize();
|
|
1378
|
-
const date = payload.Reduce("date").Visualize();
|
|
1379
|
-
appendNotifyLog(id, {
|
|
1380
|
-
event: "message_received",
|
|
1381
|
-
from: sender,
|
|
1382
|
-
sender_name: sender,
|
|
1383
|
-
sender_id: senderId,
|
|
1384
|
-
msg_id: msgId,
|
|
1385
|
-
wire_id: wireId,
|
|
1386
|
-
date
|
|
1387
|
-
});
|
|
1388
|
-
refreshUnread(id);
|
|
1389
|
-
process.nextTick(
|
|
1390
|
-
() => hooks.onNotify(id.name, `[${id.name}] new message from ${sender} (#${msgId})`)
|
|
1391
|
-
);
|
|
1392
|
-
} else if (event === "receipt_received") {
|
|
1604
|
+
if (event === "receipt_received") {
|
|
1393
1605
|
const senderIdValue = payload.Reduce("sender_id");
|
|
1394
1606
|
const senderId = senderIdValue.IsNil() ? "" : String(senderIdValue.Visualize());
|
|
1395
1607
|
const kindValue = payload.Reduce("kind");
|
|
@@ -1405,6 +1617,14 @@ function wireHandlers(id, hooks) {
|
|
|
1405
1617
|
}
|
|
1406
1618
|
const dateValue = payload.Reduce("date");
|
|
1407
1619
|
const date = dateValue.IsNil() ? (/* @__PURE__ */ new Date()).toISOString() : String(dateValue.Visualize());
|
|
1620
|
+
if (receiptKind === "delivered" || receiptKind === "read") {
|
|
1621
|
+
try {
|
|
1622
|
+
updateDeliveryState(id, senderId, receiptKind, wireIds);
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
noteHistoryFailure(id, "receipt");
|
|
1625
|
+
log(`[${id.name}] history receipt update failed sender_id=${senderId} kind=${receiptKind} count=${wireIds.length}:`, String(error));
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1408
1628
|
appendNotifyLog(id, {
|
|
1409
1629
|
event: "receipt_received",
|
|
1410
1630
|
sender_id: senderId,
|
|
@@ -1412,33 +1632,6 @@ function wireHandlers(id, hooks) {
|
|
|
1412
1632
|
wire_ids: wireIds,
|
|
1413
1633
|
date
|
|
1414
1634
|
});
|
|
1415
|
-
} else if (event === "file_received") {
|
|
1416
|
-
const sender = payload.Reduce("sender_name").Visualize();
|
|
1417
|
-
const senderId = payload.Reduce("sender_id").Visualize();
|
|
1418
|
-
const fileId = payload.Reduce("file_id").Visualize();
|
|
1419
|
-
const wireId = payload.Reduce("wire_id").Visualize();
|
|
1420
|
-
const filename = payload.Reduce("filename").Visualize();
|
|
1421
|
-
const mime = payload.Reduce("mime").Visualize();
|
|
1422
|
-
const bytes = payload.Reduce("bytes").Visualize();
|
|
1423
|
-
const date = payload.Reduce("date").Visualize();
|
|
1424
|
-
appendNotifyLog(id, {
|
|
1425
|
-
event: "file_received",
|
|
1426
|
-
from: sender,
|
|
1427
|
-
sender_id: senderId,
|
|
1428
|
-
file_id: fileId,
|
|
1429
|
-
wire_id: wireId,
|
|
1430
|
-
filename,
|
|
1431
|
-
mime,
|
|
1432
|
-
bytes,
|
|
1433
|
-
date
|
|
1434
|
-
});
|
|
1435
|
-
refreshUnread(id);
|
|
1436
|
-
process.nextTick(
|
|
1437
|
-
() => hooks.onNotify(
|
|
1438
|
-
id.name,
|
|
1439
|
-
`[${id.name}] new file ${filename} (${bytes} B) from ${sender} (sender_id ${senderId}, file_id ${fileId}, wire_id ${wireId})`
|
|
1440
|
-
)
|
|
1441
|
-
);
|
|
1442
1635
|
} else if (event === "contact_accepted") {
|
|
1443
1636
|
const name = payload.Reduce("name").Visualize();
|
|
1444
1637
|
const cid = payload.Reduce("container_id").Visualize();
|
|
@@ -1642,23 +1835,23 @@ function wireHandlers(id, hooks) {
|
|
|
1642
1835
|
|
|
1643
1836
|
// ../../src/book.ts
|
|
1644
1837
|
import { join as join7 } from "node:path";
|
|
1645
|
-
import * as
|
|
1838
|
+
import * as fs7 from "node:fs";
|
|
1646
1839
|
var bookDir = () => join7(STATE_DIR, BOOK_DIR_NAME);
|
|
1647
1840
|
var registrarKeyPath = () => join7(bookDir(), "registrar.key");
|
|
1648
1841
|
var bookPath = () => join7(bookDir(), "book.json");
|
|
1649
1842
|
function readBook() {
|
|
1650
1843
|
try {
|
|
1651
|
-
const parsed = JSON.parse(
|
|
1844
|
+
const parsed = JSON.parse(fs7.readFileSync(bookPath(), "utf8"));
|
|
1652
1845
|
return parsed && typeof parsed.entries === "object" ? parsed.entries : {};
|
|
1653
1846
|
} catch {
|
|
1654
1847
|
return {};
|
|
1655
1848
|
}
|
|
1656
1849
|
}
|
|
1657
1850
|
function writeBook(entries) {
|
|
1658
|
-
|
|
1851
|
+
fs7.mkdirSync(bookDir(), { recursive: true });
|
|
1659
1852
|
const tmp = `${bookPath()}.tmp`;
|
|
1660
|
-
|
|
1661
|
-
|
|
1853
|
+
fs7.writeFileSync(tmp, JSON.stringify({ v: 1, entries }, null, 2), { mode: 384 });
|
|
1854
|
+
fs7.renameSync(tmp, bookPath());
|
|
1662
1855
|
}
|
|
1663
1856
|
function exportAdBlob(id) {
|
|
1664
1857
|
return withScope(
|
|
@@ -1762,9 +1955,10 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
|
|
|
1762
1955
|
);
|
|
1763
1956
|
}
|
|
1764
1957
|
reservedNames.add(name);
|
|
1958
|
+
let provisioned;
|
|
1765
1959
|
try {
|
|
1766
1960
|
const dir = identityDir(name);
|
|
1767
|
-
|
|
1961
|
+
fs8.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1768
1962
|
let tempMeta;
|
|
1769
1963
|
if (opts.temp) {
|
|
1770
1964
|
tempMeta = { owner: { tokenHash: opts.temp.tokenHash, pid: opts.temp.pid }, createdAt: Date.now() };
|
|
@@ -1772,8 +1966,10 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
|
|
|
1772
1966
|
}
|
|
1773
1967
|
const seed = randomBytes(24).toString("hex");
|
|
1774
1968
|
const id = await createPacket(name, seed, dir);
|
|
1969
|
+
provisioned = id;
|
|
1970
|
+
openHistory(id);
|
|
1775
1971
|
if (tempMeta) id.temp = tempMeta;
|
|
1776
|
-
|
|
1972
|
+
fs8.writeFileSync(keyPath(dir), exportSigningSecret(id), { mode: 384 });
|
|
1777
1973
|
await withScopeAsync(async (lt) => {
|
|
1778
1974
|
await mutatingTx(id, "::a2a_messaging::set_my_name", { name }, lt);
|
|
1779
1975
|
});
|
|
@@ -1789,13 +1985,14 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
|
|
|
1789
1985
|
saveStateFailClosed(id);
|
|
1790
1986
|
return id;
|
|
1791
1987
|
} catch (err) {
|
|
1988
|
+
if (provisioned) closeHistory(provisioned);
|
|
1792
1989
|
reservedNames.delete(name);
|
|
1793
1990
|
throw err;
|
|
1794
1991
|
}
|
|
1795
1992
|
}
|
|
1796
1993
|
async function restoreIdentity(name) {
|
|
1797
1994
|
const dir = identityDir(name);
|
|
1798
|
-
const secret =
|
|
1995
|
+
const secret = fs8.readFileSync(keyPath(dir), "utf8").trim();
|
|
1799
1996
|
const id = await createPacket(name, "", dir, false, secret, true);
|
|
1800
1997
|
log(`[${name}] created QUARANTINED (no routing/broker registration, not client-bindable) \u2014 importing state before exposure`);
|
|
1801
1998
|
const holdMs = Number(process.env.OURS_TEST_RESTORE_HOLD_MS || "") || 0;
|
|
@@ -1806,10 +2003,6 @@ async function restoreIdentity(name) {
|
|
|
1806
2003
|
const isTimeout = (err) => /timed out waiting for the transaction result/.test(String(err));
|
|
1807
2004
|
const tearDownUnexposed = (step, why, err) => {
|
|
1808
2005
|
log(`[${name}] ${step} ${why} \u2014 identity left UNEXPOSED and UNTRACKED (fail-closed)`);
|
|
1809
|
-
try {
|
|
1810
|
-
appendNotifyLog(id, { event: "restore_fail_closed", step, error: String(err).slice(0, 300) });
|
|
1811
|
-
} catch {
|
|
1812
|
-
}
|
|
1813
2006
|
identities.delete(name);
|
|
1814
2007
|
try {
|
|
1815
2008
|
wrapper.remove_packet(id.cid);
|
|
@@ -1820,32 +2013,26 @@ async function restoreIdentity(name) {
|
|
|
1820
2013
|
};
|
|
1821
2014
|
const failClosed = (step, err) => tearDownUnexposed(step, "outcome UNKNOWN (timeout): the transaction may still execute and exposure would race it", err);
|
|
1822
2015
|
if (hasSavedState(dir)) {
|
|
1823
|
-
let imported = false;
|
|
1824
2016
|
try {
|
|
1825
2017
|
if (process.env.OURS_TEST_FORCE_IMPORT_TIMEOUT === "1") {
|
|
1826
2018
|
throw new Error("timed out waiting for the transaction result (forced by OURS_TEST_FORCE_IMPORT_TIMEOUT)");
|
|
1827
2019
|
}
|
|
1828
|
-
const buf =
|
|
2020
|
+
const buf = fs8.readFileSync(dataPath(dir));
|
|
1829
2021
|
await withScopeAsync(async (lt) => {
|
|
1830
2022
|
const adaptData = id.pw.packet.ParseValue(new Uint8Array(buf)).Attach(lt);
|
|
1831
2023
|
const importTimeoutMs = Number(process.env.OURS_IMPORT_TIMEOUT_MS || "") || void 0;
|
|
1832
2024
|
await mutatingTx(id, "::actor::import_state", adaptData, lt, importTimeoutMs);
|
|
1833
2025
|
});
|
|
1834
|
-
imported = true;
|
|
1835
2026
|
log(`[${name}] state import completed (positively observed)`);
|
|
1836
2027
|
} catch (err) {
|
|
1837
2028
|
if (isTimeout(err)) failClosed("import_state", err);
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
fs7.chmodSync(failedPath, 384);
|
|
1844
|
-
log(`[${name}] unreadable state blob preserved as state_data.bin.failed-*`);
|
|
1845
|
-
} catch {
|
|
1846
|
-
}
|
|
2029
|
+
tearDownUnexposed(
|
|
2030
|
+
"import_state",
|
|
2031
|
+
"REFUSED: this release accepts only direct-host-storage app format 2; existing state was left untouched",
|
|
2032
|
+
err
|
|
2033
|
+
);
|
|
1847
2034
|
}
|
|
1848
|
-
|
|
2035
|
+
{
|
|
1849
2036
|
try {
|
|
1850
2037
|
const st = await withScopeAsync(async (lt) => {
|
|
1851
2038
|
const r = await mutatingTx(id, "::a2a_messaging::commit_e2e_restore", {}, lt);
|
|
@@ -1894,269 +2081,577 @@ async function restoreIdentity(name) {
|
|
|
1894
2081
|
}
|
|
1895
2082
|
}
|
|
1896
2083
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2084
|
+
try {
|
|
2085
|
+
openHistory(id);
|
|
2086
|
+
} catch (err) {
|
|
2087
|
+
tearDownUnexposed("history_open", "FAILED (SQLite history unavailable)", err);
|
|
2088
|
+
}
|
|
2089
|
+
wrapper.expose_packet(id.cid);
|
|
2090
|
+
identities.set(name, id);
|
|
2091
|
+
log(`[${name}] EXPOSED (routing + broker registration) \u2014 import phase complete`);
|
|
2092
|
+
await contactRestoreSweep(id);
|
|
2093
|
+
refreshUnread(id);
|
|
2094
|
+
return id;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// ../../src/state.ts
|
|
2098
|
+
async function ensureRegistrar() {
|
|
2099
|
+
fs9.mkdirSync(bookDir(), { recursive: true });
|
|
2100
|
+
let secret;
|
|
2101
|
+
try {
|
|
2102
|
+
secret = fs9.readFileSync(registrarKeyPath(), "utf8").trim();
|
|
2103
|
+
} catch {
|
|
2104
|
+
}
|
|
2105
|
+
const seed = randomBytes2(24).toString("hex");
|
|
2106
|
+
setRegistrar(await createPacket(BOOK_DIR_NAME, seed, bookDir(), false, secret));
|
|
2107
|
+
if (!secret) {
|
|
2108
|
+
fs9.writeFileSync(registrarKeyPath(), exportSigningSecret(registrar), { mode: 384 });
|
|
2109
|
+
}
|
|
2110
|
+
setRegistrarAdBlob(exportAdBlob(registrar));
|
|
2111
|
+
log(`contact-book registrar ready (${registrar.cid})`);
|
|
2112
|
+
}
|
|
2113
|
+
function hasSavedState(dir) {
|
|
2114
|
+
try {
|
|
2115
|
+
return fs9.existsSync(dataPath(dir)) && fs9.statSync(dataPath(dir)).size > 0;
|
|
2116
|
+
} catch {
|
|
2117
|
+
return false;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
function saveState(id) {
|
|
2121
|
+
const bytes = withScope(
|
|
2122
|
+
(lt) => Buffer.from(readonlyTx(id, "::actor::export_state", lt).Serialize())
|
|
2123
|
+
);
|
|
2124
|
+
fs9.mkdirSync(id.dir, { recursive: true, mode: 448 });
|
|
2125
|
+
const final = dataPath(id.dir);
|
|
2126
|
+
const tmp = `${final}.tmp`;
|
|
2127
|
+
let fd;
|
|
2128
|
+
try {
|
|
2129
|
+
fd = fs9.openSync(tmp, "w", 384);
|
|
2130
|
+
fs9.fchmodSync(fd, 384);
|
|
2131
|
+
fs9.writeFileSync(fd, bytes);
|
|
2132
|
+
fs9.fsyncSync(fd);
|
|
2133
|
+
fs9.closeSync(fd);
|
|
2134
|
+
fd = void 0;
|
|
2135
|
+
fs9.renameSync(tmp, final);
|
|
2136
|
+
fs9.chmodSync(final, 384);
|
|
2137
|
+
try {
|
|
2138
|
+
fs9.chmodSync(keyPath(id.dir), 384);
|
|
2139
|
+
} catch {
|
|
2140
|
+
}
|
|
2141
|
+
const dirFd = fs9.openSync(id.dir, "r");
|
|
2142
|
+
try {
|
|
2143
|
+
fs9.fsyncSync(dirFd);
|
|
2144
|
+
} finally {
|
|
2145
|
+
fs9.closeSync(dirFd);
|
|
2146
|
+
}
|
|
2147
|
+
} catch (err) {
|
|
2148
|
+
if (fd !== void 0) {
|
|
2149
|
+
try {
|
|
2150
|
+
fs9.closeSync(fd);
|
|
2151
|
+
} catch {
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
try {
|
|
2155
|
+
fs9.rmSync(tmp, { force: true });
|
|
2156
|
+
} catch {
|
|
2157
|
+
}
|
|
2158
|
+
throw err;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
function saveStateFailClosed(id) {
|
|
2162
|
+
try {
|
|
2163
|
+
saveState(id);
|
|
2164
|
+
if (id.persistFailed) {
|
|
2165
|
+
id.persistFailed = false;
|
|
2166
|
+
log(`[${id.name}] persist recovered \u2014 quarantine lifted`);
|
|
2167
|
+
appendNotifyLog(id, { event: "persist_recovered" });
|
|
2168
|
+
}
|
|
2169
|
+
} catch (err) {
|
|
2170
|
+
id.persistFailed = true;
|
|
2171
|
+
log(`[${id.name}] PERSIST FAILED \u2014 identity quarantined (fail-closed), outbound of this txn withheld:`, String(err));
|
|
2172
|
+
try {
|
|
2173
|
+
appendNotifyLog(id, { event: "persist_failed", error: String(err).slice(0, 300) });
|
|
2174
|
+
} catch {
|
|
2175
|
+
}
|
|
2176
|
+
process.nextTick(
|
|
2177
|
+
() => fireNotify(id.name, `[${id.name}] PERSIST FAILED \u2014 messaging quarantined until the state file is writable again`)
|
|
2178
|
+
);
|
|
2179
|
+
throw err;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
// ../../src/mufl/tx.ts
|
|
2184
|
+
function withScope(fn) {
|
|
2185
|
+
const lt = new AdaptObjectLifetime2();
|
|
2186
|
+
try {
|
|
2187
|
+
return fn(lt);
|
|
2188
|
+
} finally {
|
|
2189
|
+
lt.Finalize();
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
async function withScopeAsync(fn) {
|
|
2193
|
+
const lt = new AdaptObjectLifetime2();
|
|
2194
|
+
try {
|
|
2195
|
+
return await fn(lt);
|
|
2196
|
+
} finally {
|
|
2197
|
+
lt.Finalize();
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
function readonlyTx(id, name, lt, targ) {
|
|
2201
|
+
const envelope = object_to_adapt_value({ name, targ });
|
|
2202
|
+
const result = id.pw.packet.ExecuteTransaction(envelope);
|
|
2203
|
+
envelope.Destroy();
|
|
2204
|
+
return lt ? result.Attach(lt) : result;
|
|
2205
|
+
}
|
|
2206
|
+
async function withLock(id, fn) {
|
|
2207
|
+
const prev = id.lock;
|
|
2208
|
+
let release;
|
|
2209
|
+
id.lock = new Promise((r) => release = r);
|
|
2210
|
+
await prev;
|
|
2211
|
+
try {
|
|
2212
|
+
return await fn();
|
|
2213
|
+
} finally {
|
|
2214
|
+
release();
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
function enqueueMutation(id, envelope, timeoutMs = 25e3) {
|
|
2218
|
+
return new Promise((res, rej) => {
|
|
2219
|
+
const timer = setTimeout(() => {
|
|
2220
|
+
const i = id.pending.findIndex((p) => p.timer === timer);
|
|
2221
|
+
if (i >= 0) id.pending.splice(i, 1);
|
|
2222
|
+
rej(new Error("timed out waiting for the transaction result"));
|
|
2223
|
+
}, timeoutMs);
|
|
2224
|
+
id.pending.push({ resolve: res, reject: rej, timer });
|
|
2225
|
+
id.pw.add_client_message(envelope);
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
function mutatingTx(id, name, targ, lt, timeoutMs) {
|
|
2229
|
+
if (id.persistFailed) {
|
|
2230
|
+
try {
|
|
2231
|
+
saveStateFailClosed(id);
|
|
2232
|
+
} catch (err) {
|
|
2233
|
+
return Promise.reject(new Error(
|
|
2234
|
+
`identity "${id.name}" is quarantined: state persist is failing (${String(err)}) \u2014 mutations are rejected until state_data.bin is writable again`
|
|
2235
|
+
));
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
const envelope = object_to_adapt_value({ name, targ });
|
|
2239
|
+
return withLock(id, () => enqueueMutation(id, envelope, timeoutMs)).then(
|
|
2240
|
+
(payload) => {
|
|
2241
|
+
envelope.Destroy();
|
|
2242
|
+
if (id.persistFailed) {
|
|
2243
|
+
try {
|
|
2244
|
+
payload.Destroy();
|
|
2245
|
+
} catch {
|
|
2246
|
+
}
|
|
2247
|
+
throw new Error(
|
|
2248
|
+
`identity "${id.name}": state persist FAILED \u2014 this transaction's outbound was withheld (fail-closed); fix the state directory and retry`
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
return lt ? payload.Attach(lt) : payload;
|
|
2252
|
+
},
|
|
2253
|
+
(err) => {
|
|
2254
|
+
try {
|
|
2255
|
+
envelope.Destroy();
|
|
2256
|
+
} catch {
|
|
2257
|
+
}
|
|
2258
|
+
throw err;
|
|
2259
|
+
}
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// ../../src/identity/hierarchy.ts
|
|
2264
|
+
function setRootName(name) {
|
|
2265
|
+
rootName = name;
|
|
2266
|
+
}
|
|
2267
|
+
var rootMarkerPath = () => join8(STATE_DIR, "root.json");
|
|
2268
|
+
var rootName = null;
|
|
2269
|
+
function readRootMarker() {
|
|
2270
|
+
try {
|
|
2271
|
+
const parsed = JSON.parse(fs10.readFileSync(rootMarkerPath(), "utf8"));
|
|
2272
|
+
return typeof parsed.name === "string" ? parsed.name : null;
|
|
2273
|
+
} catch {
|
|
2274
|
+
return null;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
function writeRootMarker(name) {
|
|
2278
|
+
fs10.mkdirSync(STATE_DIR, { recursive: true });
|
|
2279
|
+
const tmp = `${rootMarkerPath()}.tmp`;
|
|
2280
|
+
fs10.writeFileSync(tmp, JSON.stringify({ v: 1, name }));
|
|
2281
|
+
fs10.renameSync(tmp, rootMarkerPath());
|
|
2282
|
+
}
|
|
2283
|
+
function clearRootMarker() {
|
|
2284
|
+
fs10.rmSync(rootMarkerPath(), { force: true });
|
|
2285
|
+
}
|
|
2286
|
+
function describeIdentity(id) {
|
|
2287
|
+
return withScope((lt) => {
|
|
2288
|
+
const v = readonlyTx(id, "::actor::describe_identity", lt);
|
|
2289
|
+
return {
|
|
2290
|
+
bio: v.Reduce("bio").Visualize(),
|
|
2291
|
+
persona: v.Reduce("persona").Visualize(),
|
|
2292
|
+
hasCert: v.Reduce("has_cert").GetBoolean(),
|
|
2293
|
+
roleId: v.Reduce("role_id").Visualize(),
|
|
2294
|
+
rootCid: v.Reduce("root_cid").Visualize(),
|
|
2295
|
+
rootName: v.Reduce("root_name").Visualize(),
|
|
2296
|
+
monitoringEnabled: v.Reduce("monitoring_enabled").GetBoolean()
|
|
2297
|
+
};
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
async function delegateRole(root, role) {
|
|
2301
|
+
await withScopeAsync(async (lt) => {
|
|
2302
|
+
const roleAd = exportAdBlob(role);
|
|
2303
|
+
const signed = await mutatingTx(root, "::actor::sign_delegation", {
|
|
2304
|
+
role_ad: root.pw.packet.NewBinaryFromBuffer(roleAd).Attach(lt),
|
|
2305
|
+
role_id: role.name
|
|
2306
|
+
}, lt);
|
|
2307
|
+
const certBlob = Buffer.from(signed.Reduce("cert").GetBinary());
|
|
2308
|
+
const profileData = await mutatingTx(root, "::actor::export_root_profile", {}, lt);
|
|
2309
|
+
const profileBlob = Buffer.from(profileData.Reduce("profile").GetBinary());
|
|
2310
|
+
const rootAdBlob = exportAdBlob(root);
|
|
2311
|
+
const roleAdV1 = Buffer.from(
|
|
2312
|
+
(await mutatingTx(role, "::actor::export_v1_address_document", {}, lt)).Reduce("ad").GetBinary()
|
|
2313
|
+
);
|
|
2314
|
+
const signedV1 = await mutatingTx(root, "::actor::sign_delegation", {
|
|
2315
|
+
role_ad: root.pw.packet.NewBinaryFromBuffer(roleAdV1).Attach(lt),
|
|
2316
|
+
role_id: role.name
|
|
2317
|
+
}, lt);
|
|
2318
|
+
const certV1Blob = Buffer.from(signedV1.Reduce("cert").GetBinary());
|
|
2319
|
+
await mutatingTx(role, "::actor::set_delegation", {
|
|
2320
|
+
cert: role.pw.packet.NewBinaryFromBuffer(certBlob).Attach(lt),
|
|
2321
|
+
root_ad: role.pw.packet.NewBinaryFromBuffer(rootAdBlob).Attach(lt),
|
|
2322
|
+
root_profile: role.pw.packet.NewBinaryFromBuffer(profileBlob).Attach(lt),
|
|
2323
|
+
cert_v1: role.pw.packet.NewBinaryFromBuffer(certV1Blob).Attach(lt)
|
|
2324
|
+
}, lt);
|
|
2325
|
+
});
|
|
2326
|
+
log(`[${role.name}] delegated as a role under root "${root.name}"`);
|
|
2327
|
+
}
|
|
2328
|
+
async function establishRoot(id) {
|
|
2329
|
+
rootName = id.name;
|
|
2330
|
+
writeRootMarker(id.name);
|
|
2331
|
+
const adopted = [];
|
|
2332
|
+
const failed = [];
|
|
2333
|
+
for (const other of identities.values()) {
|
|
2334
|
+
if (other.name === id.name) continue;
|
|
2335
|
+
if (other.temp) continue;
|
|
2336
|
+
try {
|
|
2337
|
+
await delegateRole(id, other);
|
|
2338
|
+
adopted.push(other.name);
|
|
2339
|
+
} catch (err) {
|
|
2340
|
+
log(`failed to adopt "${other.name}" as a role under new root "${id.name}":`, String(err));
|
|
2341
|
+
failed.push(other.name);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
log(`[${id.name}] established as the host root${adopted.length ? ` (adopted ${adopted.length} role(s))` : ""}`);
|
|
2345
|
+
return { adopted, failed };
|
|
1903
2346
|
}
|
|
1904
2347
|
|
|
1905
|
-
// ../../src/
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2348
|
+
// ../../src/identity/lifecycle.ts
|
|
2349
|
+
import { join as join9 } from "node:path";
|
|
2350
|
+
import * as fs11 from "node:fs";
|
|
2351
|
+
|
|
2352
|
+
// ../../src/render/adapt-to-json.ts
|
|
2353
|
+
import { brotliCompressSync, brotliDecompressSync } from "node:zlib";
|
|
2354
|
+
|
|
2355
|
+
// ../../src/transcribe.ts
|
|
2356
|
+
function baseMime(mime) {
|
|
2357
|
+
return (mime ?? "").split(";")[0].trim() || "application/octet-stream";
|
|
2358
|
+
}
|
|
2359
|
+
var STT_PROVIDERS = ["openai-compatible", "elevenlabs", "deepgram", "custom"];
|
|
2360
|
+
var STT_MAX_BYTES_DEFAULT = 5 * 1024 * 1024;
|
|
2361
|
+
var STT_TIMEOUT_MS_DEFAULT = 6e4;
|
|
2362
|
+
function sttStatus(cfg) {
|
|
2363
|
+
const hint = "configure it in config.json `stt: {}` or via OURS_STT_* env";
|
|
2364
|
+
if (!cfg?.provider) {
|
|
2365
|
+
return { ready: false, reason: `no STT provider configured (stt.provider: ${STT_PROVIDERS.join(" | ")}) \u2014 ${hint}` };
|
|
1912
2366
|
}
|
|
1913
|
-
const
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
fs8.writeFileSync(registrarKeyPath(), exportSigningSecret(registrar), { mode: 384 });
|
|
2367
|
+
const provider = cfg.provider.trim().toLowerCase();
|
|
2368
|
+
if (!STT_PROVIDERS.includes(provider)) {
|
|
2369
|
+
return { ready: false, reason: `unknown STT provider "${cfg.provider}" (expected: ${STT_PROVIDERS.join(" | ")})` };
|
|
1917
2370
|
}
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
}
|
|
1921
|
-
function hasSavedState(dir) {
|
|
1922
|
-
try {
|
|
1923
|
-
return fs8.existsSync(dataPath(dir)) && fs8.statSync(dataPath(dir)).size > 0;
|
|
1924
|
-
} catch {
|
|
1925
|
-
return false;
|
|
2371
|
+
if (!cfg.apiKey?.trim()) {
|
|
2372
|
+
return { ready: false, reason: `STT provider "${provider}" is set but the API key is missing (stt.apiKey / OURS_STT_API_KEY)` };
|
|
1926
2373
|
}
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
(lt) => Buffer.from(readonlyTx(id, "::actor::export_state", lt).Serialize())
|
|
1931
|
-
);
|
|
1932
|
-
fs8.mkdirSync(id.dir, { recursive: true, mode: 448 });
|
|
1933
|
-
const final = dataPath(id.dir);
|
|
1934
|
-
const tmp = `${final}.tmp`;
|
|
1935
|
-
let fd;
|
|
1936
|
-
try {
|
|
1937
|
-
fd = fs8.openSync(tmp, "w", 384);
|
|
1938
|
-
fs8.fchmodSync(fd, 384);
|
|
1939
|
-
fs8.writeFileSync(fd, bytes);
|
|
1940
|
-
fs8.fsyncSync(fd);
|
|
1941
|
-
fs8.closeSync(fd);
|
|
1942
|
-
fd = void 0;
|
|
1943
|
-
fs8.renameSync(tmp, final);
|
|
1944
|
-
fs8.chmodSync(final, 384);
|
|
1945
|
-
try {
|
|
1946
|
-
fs8.chmodSync(keyPath(id.dir), 384);
|
|
1947
|
-
} catch {
|
|
2374
|
+
if (provider === "openai-compatible") {
|
|
2375
|
+
if (!cfg.baseUrl?.trim()) {
|
|
2376
|
+
return { ready: false, reason: "openai-compatible STT needs stt.baseUrl / OURS_STT_BASE_URL (e.g. your provider's /v1 root) \u2014 no endpoint is assumed" };
|
|
1948
2377
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
fs8.fsyncSync(dirFd);
|
|
1952
|
-
} finally {
|
|
1953
|
-
fs8.closeSync(dirFd);
|
|
2378
|
+
if (!cfg.model?.trim()) {
|
|
2379
|
+
return { ready: false, reason: "openai-compatible STT needs stt.model / OURS_STT_MODEL (passed to the provider verbatim) \u2014 no model is assumed" };
|
|
1954
2380
|
}
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
2381
|
+
}
|
|
2382
|
+
if (provider === "elevenlabs" && !cfg.model?.trim()) {
|
|
2383
|
+
return { ready: false, reason: "elevenlabs STT needs stt.model / OURS_STT_MODEL (the model_id, e.g. from your ElevenLabs account) \u2014 no model is assumed" };
|
|
2384
|
+
}
|
|
2385
|
+
if (provider === "custom") {
|
|
2386
|
+
if (!cfg.custom?.url?.trim()) {
|
|
2387
|
+
return { ready: false, reason: "custom STT needs stt.custom.url (the full endpoint URL of your provider)" };
|
|
1961
2388
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2389
|
+
const wantsModel = cfg.custom.url.includes("{model}") || cfg.custom.modelField !== void 0 && cfg.custom.modelField !== "";
|
|
2390
|
+
if (wantsModel && !cfg.model?.trim()) {
|
|
2391
|
+
return { ready: false, reason: "the custom STT template references a model (url {model} or modelField) but stt.model / OURS_STT_MODEL is not set" };
|
|
1965
2392
|
}
|
|
1966
|
-
throw err;
|
|
1967
2393
|
}
|
|
2394
|
+
return { ready: true, provider };
|
|
1968
2395
|
}
|
|
1969
|
-
function
|
|
2396
|
+
function textAtPath(obj, path) {
|
|
2397
|
+
let cur = obj;
|
|
2398
|
+
for (const seg of path.split(".")) {
|
|
2399
|
+
if (cur === null || typeof cur !== "object") return void 0;
|
|
2400
|
+
cur = cur[seg];
|
|
2401
|
+
}
|
|
2402
|
+
return typeof cur === "string" ? cur : void 0;
|
|
2403
|
+
}
|
|
2404
|
+
async function request(url, init, timeoutMs, secrets = []) {
|
|
2405
|
+
const aborter = new AbortController();
|
|
2406
|
+
const timer = setTimeout(() => aborter.abort(), timeoutMs);
|
|
1970
2407
|
try {
|
|
1971
|
-
|
|
1972
|
-
if (
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2408
|
+
const resp = await fetch(url, { ...init, signal: aborter.signal });
|
|
2409
|
+
if (!resp.ok) {
|
|
2410
|
+
let detail = await resp.text().catch(() => "");
|
|
2411
|
+
for (const secret of secrets) {
|
|
2412
|
+
if (secret) detail = detail.split(secret).join("[redacted]");
|
|
2413
|
+
}
|
|
2414
|
+
return { ok: false, error: `STT HTTP ${resp.status}${detail ? `: ${detail.slice(0, 200)}` : ""}` };
|
|
1976
2415
|
}
|
|
2416
|
+
return { ok: true, json: await resp.json() };
|
|
1977
2417
|
} catch (err) {
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
try {
|
|
1981
|
-
appendNotifyLog(id, { event: "persist_failed", error: String(err).slice(0, 300) });
|
|
1982
|
-
} catch {
|
|
2418
|
+
if (err?.name === "AbortError") {
|
|
2419
|
+
return { ok: false, error: `STT timeout after ${timeoutMs}ms` };
|
|
1983
2420
|
}
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
);
|
|
1987
|
-
throw err;
|
|
2421
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
2422
|
+
} finally {
|
|
2423
|
+
clearTimeout(timer);
|
|
1988
2424
|
}
|
|
1989
2425
|
}
|
|
1990
|
-
|
|
1991
|
-
//
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2426
|
+
var audioBlob = (bytes, mime) => (
|
|
2427
|
+
// Copy into a plain Uint8Array — a Buffer is not a valid BlobPart.
|
|
2428
|
+
new Blob([new Uint8Array(bytes)], { type: baseMime(mime) })
|
|
2429
|
+
);
|
|
2430
|
+
var openaiCompatible = async (bytes, filename, mime, cfg, timeoutMs) => {
|
|
2431
|
+
const form = new FormData();
|
|
2432
|
+
form.set("file", audioBlob(bytes, mime), filename);
|
|
2433
|
+
form.set("model", cfg.model.trim());
|
|
2434
|
+
form.set("response_format", "json");
|
|
2435
|
+
if (cfg.language?.trim()) form.set("language", cfg.language.trim());
|
|
2436
|
+
const r = await request(
|
|
2437
|
+
`${cfg.baseUrl.trim().replace(/\/$/, "")}/audio/transcriptions`,
|
|
2438
|
+
{ method: "POST", headers: { Authorization: `Bearer ${cfg.apiKey.trim()}` }, body: form },
|
|
2439
|
+
timeoutMs,
|
|
2440
|
+
[cfg.apiKey.trim()]
|
|
2441
|
+
);
|
|
2442
|
+
if (!r.ok) return r;
|
|
2443
|
+
const text = textAtPath(r.json, "text");
|
|
2444
|
+
return text !== void 0 ? { ok: true, text } : { ok: false, error: "STT response missing text" };
|
|
2445
|
+
};
|
|
2446
|
+
var elevenlabs = async (bytes, filename, mime, cfg, timeoutMs) => {
|
|
2447
|
+
const form = new FormData();
|
|
2448
|
+
form.set("file", audioBlob(bytes, mime), filename);
|
|
2449
|
+
form.set("model_id", cfg.model.trim());
|
|
2450
|
+
if (cfg.language?.trim()) form.set("language_code", cfg.language.trim());
|
|
2451
|
+
const r = await request(
|
|
2452
|
+
`${(cfg.baseUrl?.trim() || "https://api.elevenlabs.io").replace(/\/$/, "")}/v1/speech-to-text`,
|
|
2453
|
+
{ method: "POST", headers: { "xi-api-key": cfg.apiKey.trim() }, body: form },
|
|
2454
|
+
timeoutMs,
|
|
2455
|
+
[cfg.apiKey.trim()]
|
|
2456
|
+
);
|
|
2457
|
+
if (!r.ok) return r;
|
|
2458
|
+
const text = textAtPath(r.json, "text");
|
|
2459
|
+
return text !== void 0 ? { ok: true, text } : { ok: false, error: "STT response missing text" };
|
|
2460
|
+
};
|
|
2461
|
+
var deepgram = async (bytes, _filename, mime, cfg, timeoutMs) => {
|
|
2462
|
+
const params = new URLSearchParams();
|
|
2463
|
+
if (cfg.model?.trim()) params.set("model", cfg.model.trim());
|
|
2464
|
+
if (cfg.language?.trim()) params.set("language", cfg.language.trim());
|
|
2465
|
+
const q = params.toString();
|
|
2466
|
+
const r = await request(
|
|
2467
|
+
`${(cfg.baseUrl?.trim() || "https://api.deepgram.com").replace(/\/$/, "")}/v1/listen${q ? `?${q}` : ""}`,
|
|
2468
|
+
{
|
|
2469
|
+
method: "POST",
|
|
2470
|
+
headers: { Authorization: `Token ${cfg.apiKey.trim()}`, "Content-Type": baseMime(mime) },
|
|
2471
|
+
body: new Uint8Array(bytes)
|
|
2472
|
+
},
|
|
2473
|
+
timeoutMs,
|
|
2474
|
+
[cfg.apiKey.trim()]
|
|
2475
|
+
);
|
|
2476
|
+
if (!r.ok) return r;
|
|
2477
|
+
const text = textAtPath(r.json, "results.channels.0.alternatives.0.transcript");
|
|
2478
|
+
return text !== void 0 ? { ok: true, text } : { ok: false, error: "STT response missing transcript" };
|
|
2479
|
+
};
|
|
2480
|
+
var custom = async (bytes, filename, mime, cfg, timeoutMs) => {
|
|
2481
|
+
const t = cfg.custom;
|
|
2482
|
+
const model = cfg.model?.trim() ?? "";
|
|
2483
|
+
const url = t.url.replaceAll("{model}", encodeURIComponent(model));
|
|
2484
|
+
const headers = {};
|
|
2485
|
+
const authName = t.authHeaderName?.trim() || "Authorization";
|
|
2486
|
+
const authValue = (t.authHeaderTemplate ?? "Bearer {key}").replaceAll("{key}", cfg.apiKey.trim());
|
|
2487
|
+
if (authValue) headers[authName] = authValue;
|
|
2488
|
+
const mode = t.bodyMode ?? "multipart";
|
|
2489
|
+
const fileField = t.fileField?.trim() || "file";
|
|
2490
|
+
const modelField = t.modelField === void 0 ? "model" : t.modelField.trim();
|
|
2491
|
+
let body;
|
|
2492
|
+
if (mode === "multipart") {
|
|
2493
|
+
const form = new FormData();
|
|
2494
|
+
form.set(fileField, audioBlob(bytes, mime), filename);
|
|
2495
|
+
if (modelField && model) form.set(modelField, model);
|
|
2496
|
+
for (const [k, v] of Object.entries(t.extraFields ?? {})) form.set(k, v);
|
|
2497
|
+
body = form;
|
|
2498
|
+
} else if (mode === "raw") {
|
|
2499
|
+
headers["Content-Type"] = baseMime(mime);
|
|
2500
|
+
body = new Uint8Array(bytes);
|
|
2501
|
+
} else {
|
|
2502
|
+
headers["Content-Type"] = "application/json";
|
|
2503
|
+
body = JSON.stringify({
|
|
2504
|
+
[fileField]: bytes.toString("base64"),
|
|
2505
|
+
...modelField && model ? { [modelField]: model } : {},
|
|
2506
|
+
...t.extraFields ?? {}
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
const r = await request(url, { method: t.method?.trim() || "POST", headers, body }, timeoutMs, [cfg.apiKey.trim()]);
|
|
2510
|
+
if (!r.ok) return r;
|
|
2511
|
+
const text = textAtPath(r.json, t.responseTextPath?.trim() || "text");
|
|
2512
|
+
return text !== void 0 ? { ok: true, text } : { ok: false, error: `STT response has no text at "${t.responseTextPath?.trim() || "text"}"` };
|
|
2513
|
+
};
|
|
2514
|
+
var ADAPTERS = {
|
|
2515
|
+
"openai-compatible": openaiCompatible,
|
|
2516
|
+
elevenlabs,
|
|
2517
|
+
deepgram,
|
|
2518
|
+
custom
|
|
2519
|
+
};
|
|
2520
|
+
async function transcribeVoice(bytes, filename, mime, cfg) {
|
|
2521
|
+
const status = sttStatus(cfg);
|
|
2522
|
+
if (!status.ready) return { ok: false, error: status.reason };
|
|
2523
|
+
const maxBytes = cfg.maxBytes ?? STT_MAX_BYTES_DEFAULT;
|
|
2524
|
+
if (bytes.length > maxBytes) {
|
|
2525
|
+
return { ok: false, error: `voice message is ${bytes.length} B \u2014 over the ${maxBytes} B STT limit (stt.maxBytes)` };
|
|
1998
2526
|
}
|
|
1999
|
-
|
|
2000
|
-
async function withScopeAsync(fn) {
|
|
2001
|
-
const lt = new AdaptObjectLifetime2();
|
|
2527
|
+
const timeoutMs = cfg.timeoutMs ?? STT_TIMEOUT_MS_DEFAULT;
|
|
2002
2528
|
try {
|
|
2003
|
-
return await
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2529
|
+
return await ADAPTERS[status.provider](bytes, filename, mime, cfg, timeoutMs);
|
|
2530
|
+
} catch (err) {
|
|
2531
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
2006
2532
|
}
|
|
2007
2533
|
}
|
|
2008
|
-
function
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2534
|
+
function voiceErrorCategory(error) {
|
|
2535
|
+
if (/limit|over .* bytes/i.test(error)) return "size_limit";
|
|
2536
|
+
if (/timeout/i.test(error)) return "timeout";
|
|
2537
|
+
if (/HTTP \d+/i.test(error)) return "provider_http";
|
|
2538
|
+
if (/missing|no text|no transcript/i.test(error)) return "invalid_response";
|
|
2539
|
+
return "provider_error";
|
|
2013
2540
|
}
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2541
|
+
function structuredVoiceOutcome(readiness, outcome, association) {
|
|
2542
|
+
if (!readiness.ready || outcome.kind === "unconfigured") {
|
|
2543
|
+
return {
|
|
2544
|
+
configured: false,
|
|
2545
|
+
attempted: false,
|
|
2546
|
+
status: "unavailable",
|
|
2547
|
+
provider: null,
|
|
2548
|
+
text: null,
|
|
2549
|
+
error_category: "not_configured",
|
|
2550
|
+
audio_path: association.audioPath,
|
|
2551
|
+
file_wire_id: association.wireId
|
|
2552
|
+
};
|
|
2023
2553
|
}
|
|
2554
|
+
if (outcome.kind === "transcript") {
|
|
2555
|
+
return {
|
|
2556
|
+
configured: true,
|
|
2557
|
+
attempted: true,
|
|
2558
|
+
status: "succeeded",
|
|
2559
|
+
provider: readiness.provider,
|
|
2560
|
+
text: outcome.text,
|
|
2561
|
+
error_category: null,
|
|
2562
|
+
audio_path: association.audioPath,
|
|
2563
|
+
file_wire_id: association.wireId
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
return {
|
|
2567
|
+
configured: true,
|
|
2568
|
+
attempted: true,
|
|
2569
|
+
status: "failed",
|
|
2570
|
+
provider: readiness.provider,
|
|
2571
|
+
text: null,
|
|
2572
|
+
error_category: voiceErrorCategory(outcome.error),
|
|
2573
|
+
audio_path: association.audioPath,
|
|
2574
|
+
file_wire_id: association.wireId
|
|
2575
|
+
};
|
|
2024
2576
|
}
|
|
2025
|
-
function
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
}
|
|
2036
|
-
function mutatingTx(id, name, targ, lt, timeoutMs) {
|
|
2037
|
-
if (id.persistFailed) {
|
|
2038
|
-
try {
|
|
2039
|
-
saveStateFailClosed(id);
|
|
2040
|
-
} catch (err) {
|
|
2041
|
-
return Promise.reject(new Error(
|
|
2042
|
-
`identity "${id.name}" is quarantined: state persist is failing (${String(err)}) \u2014 mutations are rejected until state_data.bin is writable again`
|
|
2043
|
-
));
|
|
2044
|
-
}
|
|
2577
|
+
function voiceDeliveryLine(args, outcome) {
|
|
2578
|
+
const head = ` \u2022 \u{1F3A4} voice message from ${args.sender} (${args.sizeBytes} B)`;
|
|
2579
|
+
const tail = `audio saved \u2192 ${args.savedPath} {${args.wire}}`;
|
|
2580
|
+
switch (outcome.kind) {
|
|
2581
|
+
case "transcript":
|
|
2582
|
+
return `${head}: "${outcome.text}" \u2014 transcribed from voice message (STT); ${tail}`;
|
|
2583
|
+
case "unconfigured":
|
|
2584
|
+
return `${head}: cannot transcribe \u2014 ${outcome.reason}. Tell the user you can't listen to voice messages until the operator configures transcription on this ours-mcp server, and ask them to send text meanwhile; ${tail}`;
|
|
2585
|
+
case "failed":
|
|
2586
|
+
return `${head}: transcription failed (${outcome.error}). Tell the user their voice message could not be transcribed right now and ask them to send text or retry; ${tail}`;
|
|
2045
2587
|
}
|
|
2046
|
-
const envelope = object_to_adapt_value({ name, targ });
|
|
2047
|
-
return withLock(id, () => enqueueMutation(id, envelope, timeoutMs)).then(
|
|
2048
|
-
(payload) => {
|
|
2049
|
-
envelope.Destroy();
|
|
2050
|
-
if (id.persistFailed) {
|
|
2051
|
-
try {
|
|
2052
|
-
payload.Destroy();
|
|
2053
|
-
} catch {
|
|
2054
|
-
}
|
|
2055
|
-
throw new Error(
|
|
2056
|
-
`identity "${id.name}": state persist FAILED \u2014 this transaction's outbound was withheld (fail-closed); fix the state directory and retry`
|
|
2057
|
-
);
|
|
2058
|
-
}
|
|
2059
|
-
return lt ? payload.Attach(lt) : payload;
|
|
2060
|
-
},
|
|
2061
|
-
(err) => {
|
|
2062
|
-
try {
|
|
2063
|
-
envelope.Destroy();
|
|
2064
|
-
} catch {
|
|
2065
|
-
}
|
|
2066
|
-
throw err;
|
|
2067
|
-
}
|
|
2068
|
-
);
|
|
2069
2588
|
}
|
|
2070
2589
|
|
|
2071
|
-
// ../../src/
|
|
2072
|
-
function
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
return null;
|
|
2590
|
+
// ../../src/render/adapt-to-json.ts
|
|
2591
|
+
function renderContacts(v) {
|
|
2592
|
+
const out = [];
|
|
2593
|
+
if (v.IsNil()) return out;
|
|
2594
|
+
for (const key of v.GetKeys()) {
|
|
2595
|
+
const c = v.Reduce(key);
|
|
2596
|
+
if (c.IsNil()) continue;
|
|
2597
|
+
out.push({
|
|
2598
|
+
name: c.Reduce("name").Visualize(),
|
|
2599
|
+
container_id: c.Reduce("container_id").Visualize()
|
|
2600
|
+
});
|
|
2083
2601
|
}
|
|
2602
|
+
return out;
|
|
2084
2603
|
}
|
|
2085
|
-
function
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2604
|
+
function renderImportRenames(v) {
|
|
2605
|
+
const out = {};
|
|
2606
|
+
if (v.IsNil()) return out;
|
|
2607
|
+
for (const key of v.GetKeys()) {
|
|
2608
|
+
const n = v.Reduce(key);
|
|
2609
|
+
if (!n.IsNil()) out[typeof key === "string" ? key : key.Visualize()] = n.Visualize();
|
|
2610
|
+
}
|
|
2611
|
+
return out;
|
|
2090
2612
|
}
|
|
2091
|
-
function
|
|
2092
|
-
|
|
2613
|
+
function renderPending(v) {
|
|
2614
|
+
const out = [];
|
|
2615
|
+
if (v.IsNil()) return out;
|
|
2616
|
+
for (const key of v.GetKeys()) {
|
|
2617
|
+
const p = v.Reduce(key);
|
|
2618
|
+
if (p.IsNil()) continue;
|
|
2619
|
+
out.push({
|
|
2620
|
+
container_id: typeof key === "string" ? key : key.Visualize(),
|
|
2621
|
+
name: p.Reduce("name").Visualize(),
|
|
2622
|
+
queued: parseInt(p.Reduce("queued").Visualize(), 10) || 0
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
return out;
|
|
2093
2626
|
}
|
|
2094
|
-
function
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
monitoringEnabled: v.Reduce("monitoring_enabled").GetBoolean()
|
|
2627
|
+
function renderContactRoots(v) {
|
|
2628
|
+
const out = {};
|
|
2629
|
+
if (v.IsNil()) return out;
|
|
2630
|
+
for (const key of v.GetKeys()) {
|
|
2631
|
+
const r = v.Reduce(key);
|
|
2632
|
+
if (r.IsNil()) continue;
|
|
2633
|
+
out[typeof key === "string" ? key : key.Visualize()] = {
|
|
2634
|
+
root_cid: r.Reduce("root_cid").Visualize(),
|
|
2635
|
+
root_name: r.Reduce("root_name").Visualize(),
|
|
2636
|
+
role_id: r.Reduce("role_id").Visualize()
|
|
2105
2637
|
};
|
|
2106
|
-
}
|
|
2638
|
+
}
|
|
2639
|
+
return out;
|
|
2107
2640
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
const roleAd = exportAdBlob(role);
|
|
2111
|
-
const signed = await mutatingTx(root, "::actor::sign_delegation", {
|
|
2112
|
-
role_ad: root.pw.packet.NewBinaryFromBuffer(roleAd).Attach(lt),
|
|
2113
|
-
role_id: role.name
|
|
2114
|
-
}, lt);
|
|
2115
|
-
const certBlob = Buffer.from(signed.Reduce("cert").GetBinary());
|
|
2116
|
-
const profileData = await mutatingTx(root, "::actor::export_root_profile", {}, lt);
|
|
2117
|
-
const profileBlob = Buffer.from(profileData.Reduce("profile").GetBinary());
|
|
2118
|
-
const rootAdBlob = exportAdBlob(root);
|
|
2119
|
-
const roleAdV1 = Buffer.from(
|
|
2120
|
-
(await mutatingTx(role, "::actor::export_v1_address_document", {}, lt)).Reduce("ad").GetBinary()
|
|
2121
|
-
);
|
|
2122
|
-
const signedV1 = await mutatingTx(root, "::actor::sign_delegation", {
|
|
2123
|
-
role_ad: root.pw.packet.NewBinaryFromBuffer(roleAdV1).Attach(lt),
|
|
2124
|
-
role_id: role.name
|
|
2125
|
-
}, lt);
|
|
2126
|
-
const certV1Blob = Buffer.from(signedV1.Reduce("cert").GetBinary());
|
|
2127
|
-
await mutatingTx(role, "::actor::set_delegation", {
|
|
2128
|
-
cert: role.pw.packet.NewBinaryFromBuffer(certBlob).Attach(lt),
|
|
2129
|
-
root_ad: role.pw.packet.NewBinaryFromBuffer(rootAdBlob).Attach(lt),
|
|
2130
|
-
root_profile: role.pw.packet.NewBinaryFromBuffer(profileBlob).Attach(lt),
|
|
2131
|
-
cert_v1: role.pw.packet.NewBinaryFromBuffer(certV1Blob).Attach(lt)
|
|
2132
|
-
}, lt);
|
|
2133
|
-
});
|
|
2134
|
-
log(`[${role.name}] delegated as a role under root "${root.name}"`);
|
|
2641
|
+
function encodeWireBin(raw) {
|
|
2642
|
+
return brotliCompressSync(raw).toString("base64url");
|
|
2135
2643
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
writeRootMarker(id.name);
|
|
2139
|
-
const adopted = [];
|
|
2140
|
-
const failed = [];
|
|
2141
|
-
for (const other of identities.values()) {
|
|
2142
|
-
if (other.name === id.name) continue;
|
|
2143
|
-
if (other.temp) continue;
|
|
2144
|
-
try {
|
|
2145
|
-
await delegateRole(id, other);
|
|
2146
|
-
adopted.push(other.name);
|
|
2147
|
-
} catch (err) {
|
|
2148
|
-
log(`failed to adopt "${other.name}" as a role under new root "${id.name}":`, String(err));
|
|
2149
|
-
failed.push(other.name);
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
log(`[${id.name}] established as the host root${adopted.length ? ` (adopted ${adopted.length} role(s))` : ""}`);
|
|
2153
|
-
return { adopted, failed };
|
|
2644
|
+
function decodeWireBin(s) {
|
|
2645
|
+
return brotliDecompressSync(Buffer.from(s.replace(/\s+/g, ""), "base64url"));
|
|
2154
2646
|
}
|
|
2155
2647
|
|
|
2156
2648
|
// ../../src/identity/lifecycle.ts
|
|
2157
|
-
import { join as join9 } from "node:path";
|
|
2158
|
-
import * as fs10 from "node:fs";
|
|
2159
2649
|
function deleteIdentityCompletely(id) {
|
|
2650
|
+
try {
|
|
2651
|
+
closeHistory(id);
|
|
2652
|
+
} catch (err) {
|
|
2653
|
+
log(`closeHistory(${id.name}) failed:`, String(err));
|
|
2654
|
+
}
|
|
2160
2655
|
try {
|
|
2161
2656
|
wrapper.remove_packet(id.cid);
|
|
2162
2657
|
} catch (err) {
|
|
@@ -2177,7 +2672,7 @@ function deleteIdentityCompletely(id) {
|
|
|
2177
2672
|
clearRootMarker();
|
|
2178
2673
|
}
|
|
2179
2674
|
try {
|
|
2180
|
-
|
|
2675
|
+
fs11.rmSync(id.dir, { recursive: true, force: true });
|
|
2181
2676
|
reservedNames.delete(id.name);
|
|
2182
2677
|
} catch (err) {
|
|
2183
2678
|
return `deleting ${id.dir} failed: ${String(err)}`;
|
|
@@ -2242,14 +2737,14 @@ function sweepStaleTempIdentities() {
|
|
|
2242
2737
|
}
|
|
2243
2738
|
}
|
|
2244
2739
|
function sweepOrphanTempDirs() {
|
|
2245
|
-
if (!
|
|
2246
|
-
for (const d of
|
|
2740
|
+
if (!fs11.existsSync(STATE_DIR)) return;
|
|
2741
|
+
for (const d of fs11.readdirSync(STATE_DIR, { withFileTypes: true })) {
|
|
2247
2742
|
if (!d.isDirectory() || identities.has(d.name)) continue;
|
|
2248
2743
|
const dir = join9(STATE_DIR, d.name);
|
|
2249
2744
|
const meta = readTempMetaFile(dir);
|
|
2250
2745
|
if (!meta || pidAlive(meta.owner.pid)) continue;
|
|
2251
2746
|
try {
|
|
2252
|
-
|
|
2747
|
+
fs11.rmSync(dir, { recursive: true, force: true });
|
|
2253
2748
|
reservedNames.delete(d.name);
|
|
2254
2749
|
log(`[${d.name}] removed orphaned temporary-identity dir (owner pid ${meta.owner.pid} dead, no live packet)`);
|
|
2255
2750
|
} catch (err) {
|
|
@@ -2390,6 +2885,7 @@ async function bootWrapper() {
|
|
|
2390
2885
|
if (tempMeta) id.temp = tempMeta;
|
|
2391
2886
|
} catch (err) {
|
|
2392
2887
|
log(`failed to restore "${name}":`, String(err));
|
|
2888
|
+
throw err;
|
|
2393
2889
|
}
|
|
2394
2890
|
startupProgress?.update("identities", { completed: index + 1, total: names.length });
|
|
2395
2891
|
}
|
|
@@ -2465,6 +2961,16 @@ export {
|
|
|
2465
2961
|
log,
|
|
2466
2962
|
requireAuth,
|
|
2467
2963
|
validateName,
|
|
2964
|
+
consumeOutboundHistoryFailure,
|
|
2965
|
+
listIncomingMessages,
|
|
2966
|
+
takeUnreadMessages,
|
|
2967
|
+
listIncomingFiles,
|
|
2968
|
+
takeUnreadFiles,
|
|
2969
|
+
listMessageHistory,
|
|
2970
|
+
getMessageHistoryItem,
|
|
2971
|
+
getMessageHistorySummary,
|
|
2972
|
+
listFileHistory,
|
|
2973
|
+
getFileHistoryItem,
|
|
2468
2974
|
FILE_SELECTION_CAP,
|
|
2469
2975
|
identities,
|
|
2470
2976
|
registrar,
|
|
@@ -2481,21 +2987,9 @@ export {
|
|
|
2481
2987
|
persistBindings,
|
|
2482
2988
|
resolveBound,
|
|
2483
2989
|
bindSession,
|
|
2484
|
-
buildMessagesPayload,
|
|
2485
|
-
mimeFromExt,
|
|
2486
|
-
renderContacts,
|
|
2487
|
-
renderImportRenames,
|
|
2488
|
-
renderInbox,
|
|
2489
|
-
renderFileMetadata,
|
|
2490
|
-
writeIncomingFiles,
|
|
2491
|
-
renderPending,
|
|
2492
|
-
renderContactRoots,
|
|
2493
|
-
encodeWireBin,
|
|
2494
|
-
decodeWireBin,
|
|
2495
2990
|
setNotifyHook,
|
|
2496
2991
|
clearNotifyHook,
|
|
2497
2992
|
appendNotifyLog,
|
|
2498
|
-
readE2eWireIds,
|
|
2499
2993
|
serveNotifications,
|
|
2500
2994
|
refreshUnread,
|
|
2501
2995
|
unreadSummary,
|
|
@@ -2519,6 +3013,16 @@ export {
|
|
|
2519
3013
|
describeIdentity,
|
|
2520
3014
|
delegateRole,
|
|
2521
3015
|
establishRoot,
|
|
3016
|
+
sttStatus,
|
|
3017
|
+
transcribeVoice,
|
|
3018
|
+
structuredVoiceOutcome,
|
|
3019
|
+
voiceDeliveryLine,
|
|
3020
|
+
renderContacts,
|
|
3021
|
+
renderImportRenames,
|
|
3022
|
+
renderPending,
|
|
3023
|
+
renderContactRoots,
|
|
3024
|
+
encodeWireBin,
|
|
3025
|
+
decodeWireBin,
|
|
2522
3026
|
deleteIdentityCompletely,
|
|
2523
3027
|
closeTemporaryIdentity,
|
|
2524
3028
|
sweepStaleTempIdentities,
|