@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.
@@ -89,7 +89,7 @@ function createStartupProgressReporter(stateDir, opts = {}) {
89
89
  }
90
90
 
91
91
  // ../../src/runtime/env.ts
92
- var VERSION = typeof __OURS_VERSION__ !== "undefined" ? __OURS_VERSION__ : "0.0.0-dev";
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 join3 } from "node:path";
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) => join3(STATE_DIR, name);
211
- var keyPath = (dir) => join3(dir, "identity.key");
212
- var dataPath = (dir) => join3(dir, "state_data.bin");
213
- var notifyLogPath = (dir) => join3(dir, "notifications.log");
214
- var unreadPath = (dir) => join3(dir, "unread.json");
215
- var filesDirFor = (id) => join3(id.dir, "files");
216
- var tempMetaPath = (dir) => join3(dir, "temp.json");
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
- fs3.writeFileSync(
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(fs3.readFileSync(tempMetaPath(dir), "utf8"));
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 = join3(STATE_DIR, name);
841
+ const dir = join4(STATE_DIR, name);
240
842
  try {
241
- fs3.chmodSync(dir, 448);
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 (!fs3.existsSync(f)) continue;
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
- fs3.chmodSync(f, 384);
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
- entries = fs3.readdirSync(dir);
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 (!fs3.existsSync(STATE_DIR)) return [];
272
- return fs3.readdirSync(STATE_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && fs3.existsSync(keyPath(join3(STATE_DIR, d.name)))).map((d) => d.name);
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 join4 } from "node:path";
277
- import * as fs4 from "node:fs";
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 = () => join4(STATE_DIR, "bindings.json");
910
+ var bindingsSnapshotPath = () => join5(STATE_DIR, "bindings.json");
296
911
  function persistBindings() {
297
912
  try {
298
- fs4.mkdirSync(STATE_DIR, { recursive: true });
913
+ fs5.mkdirSync(STATE_DIR, { recursive: true });
299
914
  const tmp = `${bindingsSnapshotPath()}.tmp`;
300
- fs4.writeFileSync(tmp, JSON.stringify({
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
- fs4.renameSync(tmp, bindingsSnapshotPath());
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 fs9 from "node:fs";
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 fs8 from "node:fs";
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 fs7 from "node:fs";
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 fs5 from "node:fs";
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/render/adapt-to-json.ts
474
- import { basename as basename2, join as join5 } from "node:path";
475
- import { createHash as createHash2, randomUUID } from "node:crypto";
476
- import { mkdir, open, rename, unlink, lstat } from "node:fs/promises";
477
- import { brotliCompressSync, brotliDecompressSync } from "node:zlib";
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
- // ../../src/transcribe.ts
514
- var VOICE_MESSAGE_MIME_PARAM = "x-ours-kind=voice-message";
515
- var VOICE_MESSAGE_FILENAME_PREFIX = "voice-message-";
516
- function isVoiceMessage(mime, filename) {
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 baseMime(mime) {
522
- return (mime ?? "").split(";")[0].trim() || "application/octet-stream";
1065
+ function fireNotify(identityName, summary) {
1066
+ emitDaemonNotification(identityName, summary);
523
1067
  }
524
- var STT_PROVIDERS = ["openai-compatible", "elevenlabs", "deepgram", "custom"];
525
- var STT_MAX_BYTES_DEFAULT = 5 * 1024 * 1024;
526
- var STT_TIMEOUT_MS_DEFAULT = 6e4;
527
- function sttStatus(cfg) {
528
- const hint = "configure it in config.json `stt: {}` or via OURS_STT_* env";
529
- if (!cfg?.provider) {
530
- return { ready: false, reason: `no STT provider configured (stt.provider: ${STT_PROVIDERS.join(" | ")}) \u2014 ${hint}` };
531
- }
532
- const provider = cfg.provider.trim().toLowerCase();
533
- if (!STT_PROVIDERS.includes(provider)) {
534
- return { ready: false, reason: `unknown STT provider "${cfg.provider}" (expected: ${STT_PROVIDERS.join(" | ")})` };
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
- if (!cfg.apiKey?.trim()) {
537
- return { ready: false, reason: `STT provider "${provider}" is set but the API key is missing (stt.apiKey / OURS_STT_API_KEY)` };
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
- if (provider === "openai-compatible") {
540
- if (!cfg.baseUrl?.trim()) {
541
- 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" };
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
- if (!cfg.model?.trim()) {
544
- return { ready: false, reason: "openai-compatible STT needs stt.model / OURS_STT_MODEL (passed to the provider verbatim) \u2014 no model is assumed" };
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 (provider === "elevenlabs" && !cfg.model?.trim()) {
548
- 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" };
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
- if (provider === "custom") {
551
- if (!cfg.custom?.url?.trim()) {
552
- return { ready: false, reason: "custom STT needs stt.custom.url (the full endpoint URL of your provider)" };
553
- }
554
- const wantsModel = cfg.custom.url.includes("{model}") || cfg.custom.modelField !== void 0 && cfg.custom.modelField !== "";
555
- if (wantsModel && !cfg.model?.trim()) {
556
- return { ready: false, reason: "the custom STT template references a model (url {model} or modelField) but stt.model / OURS_STT_MODEL is not set" };
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
- return { ready: true, provider };
1156
+ fireNotifyWaiters(id.name);
1157
+ emitDaemonEvent(id, contentFree);
560
1158
  }
561
- function textAtPath(obj, path) {
562
- let cur = obj;
563
- for (const seg of path.split(".")) {
564
- if (cur === null || typeof cur !== "object") return void 0;
565
- cur = cur[seg];
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
- async function request(url, init, timeoutMs, secrets = []) {
570
- const aborter = new AbortController();
571
- const timer = setTimeout(() => aborter.abort(), timeoutMs);
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 resp = await fetch(url, { ...init, signal: aborter.signal });
574
- if (!resp.ok) {
575
- let detail = await resp.text().catch(() => "");
576
- for (const secret of secrets) {
577
- if (secret) detail = detail.split(secret).join("[redacted]");
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
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
587
- } finally {
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 { unread, unreadFiles } = withScope((lt) => {
1155
- const inbox = renderInbox(readonlyTx(id, "::actor::list_incoming_messages", lt));
1156
- const unread2 = inbox.filter((m) => m.status === "unread");
1157
- const filesAv = readonlyTx(id, "::actor::list_incoming_files", lt);
1158
- const unreadFiles2 = [];
1159
- if (!filesAv.IsNil()) {
1160
- for (let i = 0; ; i++) {
1161
- const f = filesAv.Reduce(i);
1162
- if (f.IsNil()) break;
1163
- if (f.Reduce("status").Visualize() !== "unread") continue;
1164
- unreadFiles2.push({
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.sender_name, msg_id: m.msg_id, date: m.date })),
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
- fs5.mkdirSync(id.dir, { recursive: true, mode: 448 });
1271
+ fs6.mkdirSync(id.dir, { recursive: true, mode: 448 });
1185
1272
  const tmp = `${unreadPath(id.dir)}.tmp`;
1186
- fs5.writeFileSync(tmp, JSON.stringify(snapshot));
1187
- fs5.renameSync(tmp, unreadPath(id.dir));
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 = fs5.readdirSync(STATE_DIR, { withFileTypes: true });
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(fs5.readFileSync(unreadPath(join6(STATE_DIR, entry.name)), "utf8"));
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 === "message_received") {
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 fs6 from "node:fs";
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(fs6.readFileSync(bookPath(), "utf8"));
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
- fs6.mkdirSync(bookDir(), { recursive: true });
1851
+ fs7.mkdirSync(bookDir(), { recursive: true });
1659
1852
  const tmp = `${bookPath()}.tmp`;
1660
- fs6.writeFileSync(tmp, JSON.stringify({ v: 1, entries }, null, 2), { mode: 384 });
1661
- fs6.renameSync(tmp, bookPath());
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
- fs7.mkdirSync(dir, { recursive: true, mode: 448 });
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
- fs7.writeFileSync(keyPath(dir), exportSigningSecret(id), { mode: 384 });
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 = fs7.readFileSync(keyPath(dir), "utf8").trim();
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 = fs7.readFileSync(dataPath(dir));
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
- log(`[${name}] FAILED TO IMPORT SAVED STATE \u2014 continuing with the reseeded identity; surviving contacts (if the blob was partially migrated) self-heal via contact restore:`, String(err));
1839
- appendNotifyLog(id, { event: "state_import_failed", error: String(err).slice(0, 300) });
1840
- try {
1841
- const failedPath = `${dataPath(dir)}.failed-${Date.now()}`;
1842
- fs7.renameSync(dataPath(dir), failedPath);
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
- if (imported) {
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
- wrapper.expose_packet(id.cid);
1898
- identities.set(name, id);
1899
- log(`[${name}] EXPOSED (routing + broker registration) \u2014 import phase complete`);
1900
- await contactRestoreSweep(id);
1901
- refreshUnread(id);
1902
- return id;
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/state.ts
1906
- async function ensureRegistrar() {
1907
- fs8.mkdirSync(bookDir(), { recursive: true });
1908
- let secret;
1909
- try {
1910
- secret = fs8.readFileSync(registrarKeyPath(), "utf8").trim();
1911
- } catch {
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 seed = randomBytes2(24).toString("hex");
1914
- setRegistrar(await createPacket(BOOK_DIR_NAME, seed, bookDir(), false, secret));
1915
- if (!secret) {
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
- setRegistrarAdBlob(exportAdBlob(registrar));
1919
- log(`contact-book registrar ready (${registrar.cid})`);
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
- function saveState(id) {
1929
- const bytes = withScope(
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
- const dirFd = fs8.openSync(id.dir, "r");
1950
- try {
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
- } catch (err) {
1956
- if (fd !== void 0) {
1957
- try {
1958
- fs8.closeSync(fd);
1959
- } catch {
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
- try {
1963
- fs8.rmSync(tmp, { force: true });
1964
- } catch {
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 saveStateFailClosed(id) {
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
- saveState(id);
1972
- if (id.persistFailed) {
1973
- id.persistFailed = false;
1974
- log(`[${id.name}] persist recovered \u2014 quarantine lifted`);
1975
- appendNotifyLog(id, { event: "persist_recovered" });
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
- id.persistFailed = true;
1979
- log(`[${id.name}] PERSIST FAILED \u2014 identity quarantined (fail-closed), outbound of this txn withheld:`, String(err));
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
- process.nextTick(
1985
- () => fireNotify(id.name, `[${id.name}] PERSIST FAILED \u2014 messaging quarantined until the state file is writable again`)
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
- // ../../src/mufl/tx.ts
1992
- function withScope(fn) {
1993
- const lt = new AdaptObjectLifetime2();
1994
- try {
1995
- return fn(lt);
1996
- } finally {
1997
- lt.Finalize();
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 fn(lt);
2004
- } finally {
2005
- lt.Finalize();
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 readonlyTx(id, name, lt, targ) {
2009
- const envelope = object_to_adapt_value({ name, targ });
2010
- const result = id.pw.packet.ExecuteTransaction(envelope);
2011
- envelope.Destroy();
2012
- return lt ? result.Attach(lt) : result;
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
- async function withLock(id, fn) {
2015
- const prev = id.lock;
2016
- let release;
2017
- id.lock = new Promise((r) => release = r);
2018
- await prev;
2019
- try {
2020
- return await fn();
2021
- } finally {
2022
- release();
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 enqueueMutation(id, envelope, timeoutMs = 25e3) {
2026
- return new Promise((res, rej) => {
2027
- const timer = setTimeout(() => {
2028
- const i = id.pending.findIndex((p) => p.timer === timer);
2029
- if (i >= 0) id.pending.splice(i, 1);
2030
- rej(new Error("timed out waiting for the transaction result"));
2031
- }, timeoutMs);
2032
- id.pending.push({ resolve: res, reject: rej, timer });
2033
- id.pw.add_client_message(envelope);
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/identity/hierarchy.ts
2072
- function setRootName(name) {
2073
- rootName = name;
2074
- }
2075
- var rootMarkerPath = () => join8(STATE_DIR, "root.json");
2076
- var rootName = null;
2077
- function readRootMarker() {
2078
- try {
2079
- const parsed = JSON.parse(fs9.readFileSync(rootMarkerPath(), "utf8"));
2080
- return typeof parsed.name === "string" ? parsed.name : null;
2081
- } catch {
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 writeRootMarker(name) {
2086
- fs9.mkdirSync(STATE_DIR, { recursive: true });
2087
- const tmp = `${rootMarkerPath()}.tmp`;
2088
- fs9.writeFileSync(tmp, JSON.stringify({ v: 1, name }));
2089
- fs9.renameSync(tmp, rootMarkerPath());
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 clearRootMarker() {
2092
- fs9.rmSync(rootMarkerPath(), { force: true });
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 describeIdentity(id) {
2095
- return withScope((lt) => {
2096
- const v = readonlyTx(id, "::actor::describe_identity", lt);
2097
- return {
2098
- bio: v.Reduce("bio").Visualize(),
2099
- persona: v.Reduce("persona").Visualize(),
2100
- hasCert: v.Reduce("has_cert").GetBoolean(),
2101
- roleId: v.Reduce("role_id").Visualize(),
2102
- rootCid: v.Reduce("root_cid").Visualize(),
2103
- rootName: v.Reduce("root_name").Visualize(),
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
- async function delegateRole(root, role) {
2109
- await withScopeAsync(async (lt) => {
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
- async function establishRoot(id) {
2137
- rootName = id.name;
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
- fs10.rmSync(id.dir, { recursive: true, force: true });
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 (!fs10.existsSync(STATE_DIR)) return;
2246
- for (const d of fs10.readdirSync(STATE_DIR, { withFileTypes: true })) {
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
- fs10.rmSync(dir, { recursive: true, force: true });
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,