@ours.network/cli 1.0.1 → 2.0.1

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.1" : "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,586 @@ 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
+ return db.transaction(() => {
643
+ const rows = db.prepare(`
644
+ SELECT * FROM messages
645
+ WHERE direction = 'in' AND inbox_state = 'unread'
646
+ ORDER BY seq ASC LIMIT ?
647
+ `).all(limit);
648
+ const mark = db.prepare(`
649
+ UPDATE messages SET inbox_state = 'read'
650
+ WHERE seq = ? AND direction = 'in' AND inbox_state = 'unread'
651
+ `);
652
+ for (const row of rows) {
653
+ if (mark.run(row.seq).changes !== 1) throw new Error(`message row ${row.seq} changed during read batch`);
654
+ row.inbox_state = "read";
655
+ }
656
+ const remaining = Number(db.prepare(`
657
+ SELECT COUNT(*) FROM messages WHERE direction = 'in' AND inbox_state = 'unread'
658
+ `).pluck().get());
659
+ return { messages: rows.map(messageFromRow), remaining };
660
+ })();
661
+ }
662
+ function listIncomingFiles(id) {
663
+ const rows = openHistory(id).prepare(`
664
+ SELECT * FROM files WHERE direction = 'in' AND inbox_state = 'unread' ORDER BY seq ASC
665
+ `).all();
666
+ return rows.map((row) => fileFromRow(id, row));
667
+ }
668
+ function takeUnreadFiles(id, input = {}) {
669
+ const db = openHistory(id);
670
+ const limit = batchLimit(input.limit);
671
+ const requested = input.wire_ids;
672
+ return db.transaction(() => {
673
+ let rows;
674
+ if (requested !== void 0) {
675
+ if (requested.length < 1 || requested.length > MAX_BATCH) throw new Error(`wire_ids must contain 1-${MAX_BATCH} items`);
676
+ if (new Set(requested).size !== requested.length) throw new Error("wire_ids must not contain duplicates");
677
+ const get = db.prepare(`
678
+ SELECT * FROM files WHERE wire_id = ? AND direction = 'in' AND inbox_state = 'unread'
679
+ `);
680
+ rows = requested.map((wireId) => get.get(wireId)).filter((row) => !!row);
681
+ if (rows.length !== requested.length) throw new Error("selected file wire_id is unknown, stale, or no longer unread");
682
+ } else {
683
+ rows = db.prepare(`
684
+ SELECT * FROM files WHERE direction = 'in' AND inbox_state = 'unread'
685
+ ORDER BY seq ASC LIMIT ?
686
+ `).all(limit);
687
+ }
688
+ const mark = db.prepare(`
689
+ UPDATE files SET inbox_state = 'read'
690
+ WHERE seq = ? AND direction = 'in' AND inbox_state = 'unread'
691
+ `);
692
+ for (const row of rows) {
693
+ if (mark.run(row.seq).changes !== 1) throw new Error(`file row ${row.seq} changed during read batch`);
694
+ row.inbox_state = "read";
695
+ }
696
+ const remaining = Number(db.prepare(`
697
+ SELECT COUNT(*) FROM files WHERE direction = 'in' AND inbox_state = 'unread'
698
+ `).pluck().get());
699
+ return { files: rows.map((row) => fileFromRow(id, row)), remaining };
700
+ })();
701
+ }
702
+ function historyWhere(query) {
703
+ const clauses = [];
704
+ const args = [];
705
+ if (query.peer_cid !== void 0) {
706
+ clauses.push("peer_cid = ?");
707
+ args.push(query.peer_cid);
708
+ }
709
+ if (query.direction !== void 0) {
710
+ clauses.push("direction = ?");
711
+ args.push(query.direction);
712
+ }
713
+ if (query.before_seq !== void 0) {
714
+ clauses.push("seq < ?");
715
+ args.push(query.before_seq);
716
+ }
717
+ return { sql: clauses.length ? `WHERE ${clauses.join(" AND ")}` : "", args };
718
+ }
719
+ function listMessageHistory(id, query = {}) {
720
+ const db = openHistory(id);
721
+ const limit = batchLimit(query.limit);
722
+ const where = historyWhere(query);
723
+ const rows = db.prepare(`SELECT * FROM messages ${where.sql} ORDER BY seq DESC LIMIT ?`).all(...where.args, limit + 1);
724
+ const hasMore = rows.length > limit;
725
+ if (hasMore) rows.pop();
726
+ return { items: rows.map(messageFromRow), next_cursor: hasMore ? rows.at(-1).seq : null };
727
+ }
728
+ function getMessageHistoryItem(id, wireId) {
729
+ const row = openHistory(id).prepare("SELECT * FROM messages WHERE wire_id = ?").get(wireId);
730
+ return row ? messageFromRow(row) : null;
731
+ }
732
+ function listFileHistory(id, query = {}) {
733
+ const db = openHistory(id);
734
+ const limit = batchLimit(query.limit);
735
+ const where = historyWhere(query);
736
+ const rows = db.prepare(`SELECT * FROM files ${where.sql} ORDER BY seq DESC LIMIT ?`).all(...where.args, limit + 1);
737
+ const hasMore = rows.length > limit;
738
+ if (hasMore) rows.pop();
739
+ return { items: rows.map((row) => fileFromRow(id, row)), next_cursor: hasMore ? rows.at(-1).seq : null };
740
+ }
741
+ function getFileHistoryItem(id, wireId) {
742
+ const row = openHistory(id).prepare("SELECT * FROM files WHERE wire_id = ?").get(wireId);
743
+ return row ? fileFromRow(id, row) : null;
744
+ }
745
+ function resolveStoredFile(id, wireId) {
746
+ const item = getFileHistoryItem(id, wireId);
747
+ if (!item || !fs3.existsSync(item.blob_path)) return null;
748
+ const identityRoot = resolve2(id.dir);
749
+ const resolvedPath = resolve2(item.blob_path);
750
+ return resolvedPath.startsWith(`${identityRoot}${sep}`) ? resolvedPath : null;
751
+ }
752
+ function updateDeliveryState(id, peerCid, kind, wireIds) {
753
+ const db = openHistory(id);
754
+ const rank = kind === "read" ? 2 : 1;
755
+ const tx = db.transaction(() => {
756
+ for (const wireId of wireIds) {
757
+ for (const table of ["messages", "files"]) {
758
+ db.prepare(`
759
+ UPDATE ${table}
760
+ SET delivery_state = ?, human_read_at_ms = CASE WHEN ? = 'read' THEN COALESCE(human_read_at_ms, ?) ELSE human_read_at_ms END
761
+ WHERE wire_id = ? AND peer_cid = ? AND direction = 'out'
762
+ AND (CASE delivery_state WHEN 'read' THEN 2 WHEN 'delivered' THEN 1 WHEN 'sent' THEN 0 ELSE -1 END) < ?
763
+ `).run(kind, kind, Date.now(), wireId, peerCid, rank);
764
+ }
765
+ }
766
+ });
767
+ tx();
768
+ }
192
769
 
193
770
  // ../../src/constants.ts
194
771
  var FILE_SELECTION_CAP = 32;
@@ -207,16 +784,15 @@ function setRegistrar(id) {
207
784
  function setRegistrarAdBlob(blob) {
208
785
  registrarAdBlob = blob;
209
786
  }
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");
787
+ var identityDir = (name) => join4(STATE_DIR, name);
788
+ var keyPath = (dir) => join4(dir, "identity.key");
789
+ var dataPath = (dir) => join4(dir, "state_data.bin");
790
+ var notifyLogPath = (dir) => join4(dir, "notifications.log");
791
+ var unreadPath = (dir) => join4(dir, "unread.json");
792
+ var tempMetaPath = (dir) => join4(dir, "temp.json");
793
+ var hashLeaseToken = (token) => createHash2("sha256").update(token).digest("hex");
218
794
  function writeTempMetaFile(dir, meta) {
219
- fs3.writeFileSync(
795
+ fs4.writeFileSync(
220
796
  tempMetaPath(dir),
221
797
  JSON.stringify({ v: 1, owner: { token_sha256: meta.owner.tokenHash, pid: meta.owner.pid }, created_at: meta.createdAt }),
222
798
  { mode: 384 }
@@ -224,7 +800,7 @@ function writeTempMetaFile(dir, meta) {
224
800
  }
225
801
  function readTempMetaFile(dir) {
226
802
  try {
227
- const raw = JSON.parse(fs3.readFileSync(tempMetaPath(dir), "utf8"));
803
+ const raw = JSON.parse(fs4.readFileSync(tempMetaPath(dir), "utf8"));
228
804
  if (raw.v !== 1 || typeof raw.owner?.token_sha256 !== "string" || typeof raw.owner?.pid !== "number") return null;
229
805
  return {
230
806
  owner: { tokenHash: raw.owner.token_sha256, pid: raw.owner.pid },
@@ -236,45 +812,58 @@ function readTempMetaFile(dir) {
236
812
  }
237
813
  function tightenIdentityPerms() {
238
814
  for (const name of listPersistedNames()) {
239
- const dir = join3(STATE_DIR, name);
815
+ const dir = join4(STATE_DIR, name);
240
816
  try {
241
- fs3.chmodSync(dir, 448);
817
+ fs4.chmodSync(dir, 448);
242
818
  } catch (err) {
243
819
  log(`[${name}] chmod 0700 failed:`, String(err));
244
820
  }
245
- for (const f of [dataPath(dir), keyPath(dir)]) {
246
- if (!fs3.existsSync(f)) continue;
821
+ for (const f of [dataPath(dir), keyPath(dir), historyPath({ dir }), `${historyPath({ dir })}-wal`, `${historyPath({ dir })}-shm`, notifyLogPath(dir), unreadPath(dir)]) {
822
+ if (!fs4.existsSync(f)) continue;
247
823
  try {
248
- fs3.chmodSync(f, 384);
824
+ fs4.chmodSync(f, 384);
249
825
  } catch (err) {
250
826
  log(`[${name}] chmod 0600 ${f} failed:`, String(err));
251
827
  }
252
828
  }
829
+ const blobs = join4(dir, "blobs");
830
+ if (fs4.existsSync(blobs)) {
831
+ const tightenTree = (path) => {
832
+ for (const entry of fs4.readdirSync(path, { withFileTypes: true })) {
833
+ const child = join4(path, entry.name);
834
+ if (entry.isDirectory()) {
835
+ fs4.chmodSync(child, 448);
836
+ tightenTree(child);
837
+ } else if (entry.isFile()) fs4.chmodSync(child, 384);
838
+ }
839
+ };
840
+ try {
841
+ fs4.chmodSync(blobs, 448);
842
+ tightenTree(blobs);
843
+ } catch (err) {
844
+ log(`[${name}] chmod history blobs failed:`, String(err));
845
+ }
846
+ }
253
847
  }
254
848
  }
255
849
  var isWireId = (s) => /^[A-Za-z0-9]+$/.test(s) && s.length > 0 && s.length <= 128;
256
850
  var isSelectableWireId = (s) => /^[A-Fa-f0-9]{64}$/.test(s);
257
851
  function findIdentityFile(id, wireId) {
258
852
  if (!isWireId(wireId)) return null;
259
- const dir = filesDirFor(id);
260
- let entries;
261
853
  try {
262
- entries = fs3.readdirSync(dir);
854
+ return resolveStoredFile(id, wireId);
263
855
  } catch {
264
856
  return null;
265
857
  }
266
- const prefix = `${wireId}-`;
267
- const match = entries.find((name) => name.startsWith(prefix));
268
- return match ? join3(dir, match) : null;
269
858
  }
270
859
  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);
860
+ if (!fs4.existsSync(STATE_DIR)) return [];
861
+ return fs4.readdirSync(STATE_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && fs4.existsSync(keyPath(join4(STATE_DIR, d.name)))).map((d) => d.name);
273
862
  }
274
863
 
275
864
  // ../../src/identity/lease.ts
276
- import { join as join4 } from "node:path";
277
- import * as fs4 from "node:fs";
865
+ import { join as join5 } from "node:path";
866
+ import * as fs5 from "node:fs";
278
867
  var leases = /* @__PURE__ */ new Map();
279
868
  var tombstones = /* @__PURE__ */ new Set();
280
869
  var sessionHeaders = /* @__PURE__ */ new Map();
@@ -292,17 +881,17 @@ function leaseByToken(token) {
292
881
  for (const l of leases.values()) if (l.token === token) return l;
293
882
  return void 0;
294
883
  }
295
- var bindingsSnapshotPath = () => join4(STATE_DIR, "bindings.json");
884
+ var bindingsSnapshotPath = () => join5(STATE_DIR, "bindings.json");
296
885
  function persistBindings() {
297
886
  try {
298
- fs4.mkdirSync(STATE_DIR, { recursive: true });
887
+ fs5.mkdirSync(STATE_DIR, { recursive: true });
299
888
  const tmp = `${bindingsSnapshotPath()}.tmp`;
300
- fs4.writeFileSync(tmp, JSON.stringify({
889
+ fs5.writeFileSync(tmp, JSON.stringify({
301
890
  pid: process.pid,
302
891
  bound: [...leases.keys()],
303
892
  holders: [...leases.values()].map((l) => ({ identity: l.identity, pid: l.pid }))
304
893
  }));
305
- fs4.renameSync(tmp, bindingsSnapshotPath());
894
+ fs5.renameSync(tmp, bindingsSnapshotPath());
306
895
  } catch (err) {
307
896
  log("failed to persist bindings snapshot:", String(err));
308
897
  }
@@ -349,18 +938,18 @@ function bindSession(sid, name) {
349
938
 
350
939
  // ../../src/identity/hierarchy.ts
351
940
  import { join as join8 } from "node:path";
352
- import * as fs9 from "node:fs";
941
+ import * as fs10 from "node:fs";
353
942
 
354
943
  // ../../src/mufl/tx.ts
355
944
  import { AdaptObjectLifetime as AdaptObjectLifetime2 } from "@adapt-toolkit/sdk/common";
356
945
  import { object_to_adapt_value } from "@adapt-toolkit/sdk/wrapper";
357
946
 
358
947
  // ../../src/state.ts
359
- import * as fs8 from "node:fs";
948
+ import * as fs9 from "node:fs";
360
949
  import { randomBytes as randomBytes2 } from "node:crypto";
361
950
 
362
951
  // ../../src/identity/provision.ts
363
- import * as fs7 from "node:fs";
952
+ import * as fs8 from "node:fs";
364
953
  import { randomBytes } from "node:crypto";
365
954
  import { PacketWrapperConfigurator } from "@adapt-toolkit/sdk/wrappers";
366
955
 
@@ -369,42 +958,7 @@ import { AdaptObjectLifetime } from "@adapt-toolkit/sdk/common";
369
958
 
370
959
  // ../../src/notify.ts
371
960
  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
- }
961
+ import * as fs6 from "node:fs";
408
962
 
409
963
  // ../../src/events.ts
410
964
  var nonEmptyString = (value) => typeof value === "string" && value.length > 0;
@@ -470,628 +1024,148 @@ function emitDaemonNotification(identityName, summary) {
470
1024
  handlers.notification(identityName, summary);
471
1025
  }
472
1026
 
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";
1027
+ // ../../src/notify.ts
1028
+ var installed = null;
1029
+ function setNotifyHook(fn) {
1030
+ installed?.uninstall();
1031
+ installed = fn === null ? null : { hook: fn, uninstall: setDaemonEventHandler("notification", fn) };
511
1032
  }
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/");
1033
+ function clearNotifyHook(fn) {
1034
+ if (installed?.hook === fn) {
1035
+ installed.uninstall();
1036
+ installed = null;
1037
+ }
520
1038
  }
521
- function baseMime(mime) {
522
- return (mime ?? "").split(";")[0].trim() || "application/octet-stream";
1039
+ function fireNotify(identityName, summary) {
1040
+ emitDaemonNotification(identityName, summary);
523
1041
  }
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(" | ")})` };
1042
+ var notifyHooks = {
1043
+ onNotify: (identityName, summary) => fireNotify(identityName, summary)
1044
+ };
1045
+ var NOTIFY_KIND_SETS = {
1046
+ // Genuine inbound arrivals something addressed to this identity landed.
1047
+ inbound: ["message_received", "file_received"],
1048
+ // Introduction bookkeeping not an arrival, but an away agent must act on it.
1049
+ intro: ["local_contact_request", "pending_message"],
1050
+ // What `ours-mcp watch` surfaces: arrivals plus the intro events. This is
1051
+ // 7a7ec16's whitelist exactly, and it is a UNION rather than a third list so
1052
+ // the two cannot drift apart.
1053
+ get wake() {
1054
+ return [...NOTIFY_KIND_SETS.inbound, ...NOTIFY_KIND_SETS.intro];
535
1055
  }
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)` };
1056
+ };
1057
+ function parseNotifyKinds(kindsParam) {
1058
+ if (kindsParam === null || kindsParam === "") return null;
1059
+ const keep = /* @__PURE__ */ new Set();
1060
+ for (const raw of kindsParam.split(",")) {
1061
+ const kind = raw.trim();
1062
+ if (kind === "") continue;
1063
+ const events = NOTIFY_KIND_SETS[kind];
1064
+ if (!events) throw new Error(kind);
1065
+ for (const e of events) keep.add(e);
538
1066
  }
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" };
1067
+ return keep;
1068
+ }
1069
+ function notifyEventMatches(event, keep) {
1070
+ if (keep === null) return true;
1071
+ return typeof event.event === "string" && keep.has(event.event);
1072
+ }
1073
+ var notifyWaiters = /* @__PURE__ */ new Map();
1074
+ function fireNotifyWaiters(name) {
1075
+ const set = notifyWaiters.get(name);
1076
+ if (!set || set.size === 0) return;
1077
+ for (const w of [...set]) {
1078
+ try {
1079
+ w();
1080
+ } catch {
542
1081
  }
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" };
1082
+ }
1083
+ }
1084
+ function waitForNotify(name, ms) {
1085
+ return new Promise((resolve3) => {
1086
+ let set = notifyWaiters.get(name);
1087
+ if (!set) {
1088
+ set = /* @__PURE__ */ new Set();
1089
+ notifyWaiters.set(name, set);
545
1090
  }
1091
+ let done = false;
1092
+ const finish = () => {
1093
+ if (done) return;
1094
+ done = true;
1095
+ set.delete(fn);
1096
+ if (set.size === 0) notifyWaiters.delete(name);
1097
+ clearTimeout(timer);
1098
+ resolve3();
1099
+ };
1100
+ const fn = finish;
1101
+ const timer = setTimeout(finish, ms);
1102
+ set.add(fn);
1103
+ });
1104
+ }
1105
+ var NOTIFY_BODY_KEYS = /* @__PURE__ */ new Set(["body", "text", "payload", "data", "bytes", "content"]);
1106
+ function bodyFreeNotifyValue(value) {
1107
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array) return void 0;
1108
+ if (Array.isArray(value)) {
1109
+ return value.map(bodyFreeNotifyValue).filter((item) => item !== void 0);
546
1110
  }
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" };
1111
+ if (!value || typeof value !== "object") return value;
1112
+ const clean = {};
1113
+ for (const [key, item] of Object.entries(value)) {
1114
+ if (NOTIFY_BODY_KEYS.has(key.toLowerCase())) continue;
1115
+ const sanitized = bodyFreeNotifyValue(item);
1116
+ if (sanitized !== void 0) clean[key] = sanitized;
549
1117
  }
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
- }
1118
+ return clean;
1119
+ }
1120
+ function appendNotifyLog(id, event) {
1121
+ const contentFree = bodyFreeNotifyValue(event);
1122
+ try {
1123
+ fs6.mkdirSync(id.dir, { recursive: true, mode: 448 });
1124
+ const path = notifyLogPath(id.dir);
1125
+ fs6.appendFileSync(path, JSON.stringify(contentFree) + "\n", { mode: 384 });
1126
+ fs6.chmodSync(path, 384);
1127
+ } catch (err) {
1128
+ log(`[${id.name}] failed to append notifications.log:`, String(err));
558
1129
  }
559
- return { ready: true, provider };
1130
+ fireNotifyWaiters(id.name);
1131
+ emitDaemonEvent(id, contentFree);
560
1132
  }
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];
1133
+ function binHexField(av, field) {
1134
+ const x = av.Reduce(field);
1135
+ return x.IsNil() ? "" : Buffer.from(x.GetBinary()).toString("hex");
1136
+ }
1137
+ var NOTIFY_LONGPOLL_MS = Number(process.env.OURS_NOTIFY_LONGPOLL_MS) > 0 ? Number(process.env.OURS_NOTIFY_LONGPOLL_MS) : 25e3;
1138
+ var NOTIFY_RECHECK_MS = 250;
1139
+ function notifyLogSize(logPath) {
1140
+ try {
1141
+ return fs6.statSync(logPath).size;
1142
+ } catch {
1143
+ return 0;
566
1144
  }
567
- return typeof cur === "string" ? cur : void 0;
568
1145
  }
569
- async function request(url, init, timeoutMs, secrets = []) {
570
- const aborter = new AbortController();
571
- const timer = setTimeout(() => aborter.abort(), timeoutMs);
1146
+ function readNotifyRange(logPath, from, to) {
1147
+ if (to <= from) return { events: [], cursor: from };
1148
+ const buf = Buffer.alloc(to - from);
1149
+ let read = 0;
572
1150
  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)}` : ""}` };
1151
+ const fd = fs6.openSync(logPath, "r");
1152
+ try {
1153
+ read = fs6.readSync(fd, buf, 0, buf.length, from);
1154
+ } finally {
1155
+ fs6.closeSync(fd);
580
1156
  }
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` };
585
- }
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 };
1085
- }
1086
- const slice = buf.subarray(0, read);
1087
- const lastNl = slice.lastIndexOf(10);
1088
- if (lastNl === -1) return { events: [], cursor: from };
1089
- const events = [];
1090
- for (const line of slice.subarray(0, lastNl + 1).toString("utf8").split("\n")) {
1091
- if (!line.trim()) continue;
1092
- try {
1093
- events.push(JSON.parse(line));
1094
- } catch {
1157
+ } catch {
1158
+ return { events: [], cursor: from };
1159
+ }
1160
+ const slice = buf.subarray(0, read);
1161
+ const lastNl = slice.lastIndexOf(10);
1162
+ if (lastNl === -1) return { events: [], cursor: from };
1163
+ const events = [];
1164
+ for (const line of slice.subarray(0, lastNl + 1).toString("utf8").split("\n")) {
1165
+ if (!line.trim()) continue;
1166
+ try {
1167
+ events.push(JSON.parse(line));
1168
+ } catch {
1095
1169
  }
1096
1170
  }
1097
1171
  return { events, cursor: from + lastNl + 1 };
@@ -1151,40 +1225,28 @@ async function serveNotifications(req, res, name, sinceParam, kindsParam = null)
1151
1225
  }
1152
1226
  function refreshUnread(id) {
1153
1227
  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
- });
1228
+ const unread = listIncomingMessages(id);
1229
+ const unreadFiles = listIncomingFiles(id).map((file) => ({
1230
+ file_id: String(file.file_id),
1231
+ sender_id: file.from.id,
1232
+ from: file.from.name,
1233
+ filename: file.filename,
1234
+ mime: file.mime,
1235
+ bytes: String(file.byte_length),
1236
+ date: file.date,
1237
+ wire_id: file.wire_id
1238
+ }));
1178
1239
  const snapshot = {
1179
1240
  count: unread.length,
1180
- recent: unread.slice(-10).map((m) => ({ from: m.sender_name, msg_id: m.msg_id, date: m.date })),
1241
+ recent: unread.slice(-10).map((m) => ({ from: m.from.name, msg_id: m.msg_id, date: m.date })),
1181
1242
  files: unreadFiles.length,
1182
1243
  unread_files: unreadFiles.slice(-10)
1183
1244
  };
1184
- fs5.mkdirSync(id.dir, { recursive: true, mode: 448 });
1245
+ fs6.mkdirSync(id.dir, { recursive: true, mode: 448 });
1185
1246
  const tmp = `${unreadPath(id.dir)}.tmp`;
1186
- fs5.writeFileSync(tmp, JSON.stringify(snapshot));
1187
- fs5.renameSync(tmp, unreadPath(id.dir));
1247
+ fs6.writeFileSync(tmp, JSON.stringify(snapshot), { mode: 384 });
1248
+ fs6.chmodSync(tmp, 384);
1249
+ fs6.renameSync(tmp, unreadPath(id.dir));
1188
1250
  } catch (err) {
1189
1251
  log(`[${id.name}] failed to refresh unread snapshot:`, String(err));
1190
1252
  }
@@ -1193,7 +1255,7 @@ function unreadSummary() {
1193
1255
  const out = [];
1194
1256
  let entries = [];
1195
1257
  try {
1196
- entries = fs5.readdirSync(STATE_DIR, { withFileTypes: true });
1258
+ entries = fs6.readdirSync(STATE_DIR, { withFileTypes: true });
1197
1259
  } catch {
1198
1260
  return { identities: out };
1199
1261
  }
@@ -1201,7 +1263,7 @@ function unreadSummary() {
1201
1263
  if (!entry.isDirectory() || validateName(entry.name) !== null) continue;
1202
1264
  let value;
1203
1265
  try {
1204
- value = JSON.parse(fs5.readFileSync(unreadPath(join6(STATE_DIR, entry.name)), "utf8"));
1266
+ value = JSON.parse(fs6.readFileSync(unreadPath(join6(STATE_DIR, entry.name)), "utf8"));
1205
1267
  } catch {
1206
1268
  continue;
1207
1269
  }
@@ -1355,6 +1417,28 @@ async function e2eRecoverySweep(id) {
1355
1417
  }
1356
1418
 
1357
1419
  // ../../src/mufl/handlers.ts
1420
+ function eventTimeMs(value) {
1421
+ const shown = value.Visualize();
1422
+ const numeric = Number(shown);
1423
+ if (Number.isFinite(numeric) && numeric >= 0) return numeric;
1424
+ const parsed = Date.parse(shown);
1425
+ if (!Number.isFinite(parsed)) throw new Error("invalid host-event date");
1426
+ return parsed;
1427
+ }
1428
+ function replyFields(payload) {
1429
+ const reply2 = payload.Reduce("reply_to");
1430
+ if (reply2.IsNil()) return {};
1431
+ const wireId = reply2.Reduce("wire_id").Visualize();
1432
+ const sentenceValue = reply2.Reduce("sentence");
1433
+ return sentenceValue.IsNil() ? { replyToWireId: wireId } : { replyToWireId: wireId, replyToSentence: Number(sentenceValue.Visualize()) };
1434
+ }
1435
+ function appendPostInsertWake(id, event) {
1436
+ try {
1437
+ appendNotifyLog(id, event);
1438
+ } catch (error) {
1439
+ log(`[${id.name}] post-insert history wake failed event=${String(event.event ?? "")}:`, String(error));
1440
+ }
1441
+ }
1358
1442
  function wireHandlers(id, hooks) {
1359
1443
  id.pw.on_return_data = (data) => {
1360
1444
  const lt = new AdaptObjectLifetime();
@@ -1365,31 +1449,133 @@ function wireHandlers(id, hooks) {
1365
1449
  saveStateFailClosed(id);
1366
1450
  return;
1367
1451
  }
1452
+ if (kind === "host_event") {
1453
+ const payload = data.Reduce("payload");
1454
+ const event = payload.Reduce("event").Visualize();
1455
+ if (event === "application_payload") {
1456
+ const itemKind = payload.Reduce("item_kind").Visualize();
1457
+ const direction = payload.Reduce("direction").Visualize();
1458
+ const wireId = payload.Reduce("wire_id").Visualize();
1459
+ const peerCid = payload.Reduce("peer_cid").Visualize();
1460
+ const peerName = payload.Reduce("peer_name").Visualize();
1461
+ const encryption = payload.Reduce("encryption").Visualize();
1462
+ const inboxValue = payload.Reduce("inbox_state");
1463
+ const inboxState = direction === "in" && !inboxValue.IsNil() ? inboxValue.Visualize() : void 0;
1464
+ try {
1465
+ const inserted = itemKind === "message" ? ingestApplicationEvent(id, {
1466
+ kind: "message",
1467
+ wireId,
1468
+ peerCid,
1469
+ peerName,
1470
+ direction,
1471
+ inboxState,
1472
+ body: payload.Reduce("body").Visualize(),
1473
+ occurredAtMs: eventTimeMs(payload.Reduce("date")),
1474
+ encryption,
1475
+ ...replyFields(payload)
1476
+ }) : ingestApplicationEvent(id, {
1477
+ kind: "file",
1478
+ wireId,
1479
+ peerCid,
1480
+ peerName,
1481
+ direction,
1482
+ inboxState,
1483
+ filename: payload.Reduce("filename").Visualize(),
1484
+ mime: payload.Reduce("mime").Visualize(),
1485
+ bytes: Buffer.from(payload.Reduce("data").GetBinary()),
1486
+ occurredAtMs: eventTimeMs(payload.Reduce("date")),
1487
+ encryption,
1488
+ ...replyFields(payload)
1489
+ });
1490
+ if (inserted && direction === "in" && inboxState === "unread") {
1491
+ if (itemKind === "message") {
1492
+ const item = getMessageHistoryItem(id, wireId);
1493
+ appendPostInsertWake(id, {
1494
+ event: "message_received",
1495
+ from: peerName,
1496
+ sender_name: peerName,
1497
+ sender_id: peerCid,
1498
+ msg_id: String(item.msg_id),
1499
+ wire_id: wireId,
1500
+ date: item.date
1501
+ });
1502
+ refreshUnread(id);
1503
+ process.nextTick(
1504
+ () => hooks.onNotify(id.name, `[${id.name}] new message from ${peerName} (#${item.msg_id})`)
1505
+ );
1506
+ } else {
1507
+ const item = getFileHistoryItem(id, wireId);
1508
+ appendPostInsertWake(id, {
1509
+ event: "file_received",
1510
+ from: peerName,
1511
+ sender_id: peerCid,
1512
+ file_id: String(item.file_id),
1513
+ wire_id: wireId,
1514
+ filename: item.filename,
1515
+ mime: item.mime,
1516
+ bytes: String(item.byte_length),
1517
+ date: item.date
1518
+ });
1519
+ refreshUnread(id);
1520
+ process.nextTick(() => hooks.onNotify(
1521
+ id.name,
1522
+ `[${id.name}] new file ${item.filename} (${item.byte_length} B) from ${peerName} (sender_id ${peerCid}, file_id ${item.file_id}, wire_id ${wireId})`
1523
+ ));
1524
+ }
1525
+ }
1526
+ } catch (error) {
1527
+ const failureKind = itemKind === "file" ? "file" : "message";
1528
+ noteHistoryFailure(id, failureKind, wireId, direction);
1529
+ log(`[${id.name}] history ${failureKind} write failed direction=${direction} wire_id=${wireId}:`, String(error));
1530
+ }
1531
+ } else if (event === "pending_introduction_decision") {
1532
+ const peerCid = payload.Reduce("peer_cid").Visualize();
1533
+ const action = payload.Reduce("action").Visualize();
1534
+ try {
1535
+ if (action === "approve") promotePendingIntroduction(id, peerCid);
1536
+ else if (action === "reject") rejectPendingIntroduction(id, peerCid);
1537
+ refreshUnread(id);
1538
+ } catch (error) {
1539
+ log(`[${id.name}] pending-introduction history update failed action=${action} peer_cid=${peerCid}:`, String(error));
1540
+ }
1541
+ } else if (event === "monitoring_payload") {
1542
+ const itemId = payload.Reduce("item_id").Visualize();
1543
+ try {
1544
+ if (ingestMonitoringEvent(id, {
1545
+ kind: "monitoring_copy",
1546
+ itemId,
1547
+ sourceCid: payload.Reduce("source_cid").Visualize(),
1548
+ sourceName: payload.Reduce("source_name").Visualize(),
1549
+ direction: payload.Reduce("direction").Visualize(),
1550
+ peerCid: payload.Reduce("peer_cid").Visualize(),
1551
+ peerName: payload.Reduce("peer_name").Visualize(),
1552
+ body: payload.Reduce("body").Visualize(),
1553
+ occurredAtMs: eventTimeMs(payload.Reduce("date"))
1554
+ })) appendPostInsertWake(id, { event: "monitoring_copy", item_id: itemId });
1555
+ } catch (error) {
1556
+ log(`[${id.name}] monitoring history write failed item_id=${itemId}:`, String(error));
1557
+ }
1558
+ } else if (event === "control_payload") {
1559
+ const itemId = payload.Reduce("item_id").Visualize();
1560
+ try {
1561
+ if (ingestControlEvent(id, {
1562
+ kind: "control_request",
1563
+ itemId,
1564
+ senderCid: payload.Reduce("sender_cid").Visualize(),
1565
+ senderName: payload.Reduce("sender_name").Visualize(),
1566
+ payload: payload.Reduce("payload").Visualize(),
1567
+ occurredAtMs: eventTimeMs(payload.Reduce("date"))
1568
+ })) appendPostInsertWake(id, { event: "control_request", item_id: itemId });
1569
+ } catch (error) {
1570
+ log(`[${id.name}] control history write failed item_id=${itemId}:`, String(error));
1571
+ }
1572
+ }
1573
+ return;
1574
+ }
1368
1575
  if (kind === "notify_agent") {
1369
1576
  const payload = data.Reduce("payload");
1370
1577
  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") {
1578
+ if (event === "receipt_received") {
1393
1579
  const senderIdValue = payload.Reduce("sender_id");
1394
1580
  const senderId = senderIdValue.IsNil() ? "" : String(senderIdValue.Visualize());
1395
1581
  const kindValue = payload.Reduce("kind");
@@ -1405,6 +1591,14 @@ function wireHandlers(id, hooks) {
1405
1591
  }
1406
1592
  const dateValue = payload.Reduce("date");
1407
1593
  const date = dateValue.IsNil() ? (/* @__PURE__ */ new Date()).toISOString() : String(dateValue.Visualize());
1594
+ if (receiptKind === "delivered" || receiptKind === "read") {
1595
+ try {
1596
+ updateDeliveryState(id, senderId, receiptKind, wireIds);
1597
+ } catch (error) {
1598
+ noteHistoryFailure(id, "receipt");
1599
+ log(`[${id.name}] history receipt update failed sender_id=${senderId} kind=${receiptKind} count=${wireIds.length}:`, String(error));
1600
+ }
1601
+ }
1408
1602
  appendNotifyLog(id, {
1409
1603
  event: "receipt_received",
1410
1604
  sender_id: senderId,
@@ -1412,33 +1606,6 @@ function wireHandlers(id, hooks) {
1412
1606
  wire_ids: wireIds,
1413
1607
  date
1414
1608
  });
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
1609
  } else if (event === "contact_accepted") {
1443
1610
  const name = payload.Reduce("name").Visualize();
1444
1611
  const cid = payload.Reduce("container_id").Visualize();
@@ -1642,23 +1809,23 @@ function wireHandlers(id, hooks) {
1642
1809
 
1643
1810
  // ../../src/book.ts
1644
1811
  import { join as join7 } from "node:path";
1645
- import * as fs6 from "node:fs";
1812
+ import * as fs7 from "node:fs";
1646
1813
  var bookDir = () => join7(STATE_DIR, BOOK_DIR_NAME);
1647
1814
  var registrarKeyPath = () => join7(bookDir(), "registrar.key");
1648
1815
  var bookPath = () => join7(bookDir(), "book.json");
1649
1816
  function readBook() {
1650
1817
  try {
1651
- const parsed = JSON.parse(fs6.readFileSync(bookPath(), "utf8"));
1818
+ const parsed = JSON.parse(fs7.readFileSync(bookPath(), "utf8"));
1652
1819
  return parsed && typeof parsed.entries === "object" ? parsed.entries : {};
1653
1820
  } catch {
1654
1821
  return {};
1655
1822
  }
1656
1823
  }
1657
1824
  function writeBook(entries) {
1658
- fs6.mkdirSync(bookDir(), { recursive: true });
1825
+ fs7.mkdirSync(bookDir(), { recursive: true });
1659
1826
  const tmp = `${bookPath()}.tmp`;
1660
- fs6.writeFileSync(tmp, JSON.stringify({ v: 1, entries }, null, 2), { mode: 384 });
1661
- fs6.renameSync(tmp, bookPath());
1827
+ fs7.writeFileSync(tmp, JSON.stringify({ v: 1, entries }, null, 2), { mode: 384 });
1828
+ fs7.renameSync(tmp, bookPath());
1662
1829
  }
1663
1830
  function exportAdBlob(id) {
1664
1831
  return withScope(
@@ -1762,9 +1929,10 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
1762
1929
  );
1763
1930
  }
1764
1931
  reservedNames.add(name);
1932
+ let provisioned;
1765
1933
  try {
1766
1934
  const dir = identityDir(name);
1767
- fs7.mkdirSync(dir, { recursive: true, mode: 448 });
1935
+ fs8.mkdirSync(dir, { recursive: true, mode: 448 });
1768
1936
  let tempMeta;
1769
1937
  if (opts.temp) {
1770
1938
  tempMeta = { owner: { tokenHash: opts.temp.tokenHash, pid: opts.temp.pid }, createdAt: Date.now() };
@@ -1772,8 +1940,10 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
1772
1940
  }
1773
1941
  const seed = randomBytes(24).toString("hex");
1774
1942
  const id = await createPacket(name, seed, dir);
1943
+ provisioned = id;
1944
+ openHistory(id);
1775
1945
  if (tempMeta) id.temp = tempMeta;
1776
- fs7.writeFileSync(keyPath(dir), exportSigningSecret(id), { mode: 384 });
1946
+ fs8.writeFileSync(keyPath(dir), exportSigningSecret(id), { mode: 384 });
1777
1947
  await withScopeAsync(async (lt) => {
1778
1948
  await mutatingTx(id, "::a2a_messaging::set_my_name", { name }, lt);
1779
1949
  });
@@ -1789,13 +1959,14 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
1789
1959
  saveStateFailClosed(id);
1790
1960
  return id;
1791
1961
  } catch (err) {
1962
+ if (provisioned) closeHistory(provisioned);
1792
1963
  reservedNames.delete(name);
1793
1964
  throw err;
1794
1965
  }
1795
1966
  }
1796
1967
  async function restoreIdentity(name) {
1797
1968
  const dir = identityDir(name);
1798
- const secret = fs7.readFileSync(keyPath(dir), "utf8").trim();
1969
+ const secret = fs8.readFileSync(keyPath(dir), "utf8").trim();
1799
1970
  const id = await createPacket(name, "", dir, false, secret, true);
1800
1971
  log(`[${name}] created QUARANTINED (no routing/broker registration, not client-bindable) \u2014 importing state before exposure`);
1801
1972
  const holdMs = Number(process.env.OURS_TEST_RESTORE_HOLD_MS || "") || 0;
@@ -1806,10 +1977,6 @@ async function restoreIdentity(name) {
1806
1977
  const isTimeout = (err) => /timed out waiting for the transaction result/.test(String(err));
1807
1978
  const tearDownUnexposed = (step, why, err) => {
1808
1979
  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
1980
  identities.delete(name);
1814
1981
  try {
1815
1982
  wrapper.remove_packet(id.cid);
@@ -1820,32 +1987,26 @@ async function restoreIdentity(name) {
1820
1987
  };
1821
1988
  const failClosed = (step, err) => tearDownUnexposed(step, "outcome UNKNOWN (timeout): the transaction may still execute and exposure would race it", err);
1822
1989
  if (hasSavedState(dir)) {
1823
- let imported = false;
1824
1990
  try {
1825
1991
  if (process.env.OURS_TEST_FORCE_IMPORT_TIMEOUT === "1") {
1826
1992
  throw new Error("timed out waiting for the transaction result (forced by OURS_TEST_FORCE_IMPORT_TIMEOUT)");
1827
1993
  }
1828
- const buf = fs7.readFileSync(dataPath(dir));
1994
+ const buf = fs8.readFileSync(dataPath(dir));
1829
1995
  await withScopeAsync(async (lt) => {
1830
1996
  const adaptData = id.pw.packet.ParseValue(new Uint8Array(buf)).Attach(lt);
1831
1997
  const importTimeoutMs = Number(process.env.OURS_IMPORT_TIMEOUT_MS || "") || void 0;
1832
1998
  await mutatingTx(id, "::actor::import_state", adaptData, lt, importTimeoutMs);
1833
1999
  });
1834
- imported = true;
1835
2000
  log(`[${name}] state import completed (positively observed)`);
1836
2001
  } catch (err) {
1837
2002
  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
- }
2003
+ tearDownUnexposed(
2004
+ "import_state",
2005
+ "REFUSED: this release accepts only direct-host-storage app format 2; existing state was left untouched",
2006
+ err
2007
+ );
1847
2008
  }
1848
- if (imported) {
2009
+ {
1849
2010
  try {
1850
2011
  const st = await withScopeAsync(async (lt) => {
1851
2012
  const r = await mutatingTx(id, "::a2a_messaging::commit_e2e_restore", {}, lt);
@@ -1894,269 +2055,577 @@ async function restoreIdentity(name) {
1894
2055
  }
1895
2056
  }
1896
2057
  }
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;
2058
+ try {
2059
+ openHistory(id);
2060
+ } catch (err) {
2061
+ tearDownUnexposed("history_open", "FAILED (SQLite history unavailable)", err);
2062
+ }
2063
+ wrapper.expose_packet(id.cid);
2064
+ identities.set(name, id);
2065
+ log(`[${name}] EXPOSED (routing + broker registration) \u2014 import phase complete`);
2066
+ await contactRestoreSweep(id);
2067
+ refreshUnread(id);
2068
+ return id;
2069
+ }
2070
+
2071
+ // ../../src/state.ts
2072
+ async function ensureRegistrar() {
2073
+ fs9.mkdirSync(bookDir(), { recursive: true });
2074
+ let secret;
2075
+ try {
2076
+ secret = fs9.readFileSync(registrarKeyPath(), "utf8").trim();
2077
+ } catch {
2078
+ }
2079
+ const seed = randomBytes2(24).toString("hex");
2080
+ setRegistrar(await createPacket(BOOK_DIR_NAME, seed, bookDir(), false, secret));
2081
+ if (!secret) {
2082
+ fs9.writeFileSync(registrarKeyPath(), exportSigningSecret(registrar), { mode: 384 });
2083
+ }
2084
+ setRegistrarAdBlob(exportAdBlob(registrar));
2085
+ log(`contact-book registrar ready (${registrar.cid})`);
2086
+ }
2087
+ function hasSavedState(dir) {
2088
+ try {
2089
+ return fs9.existsSync(dataPath(dir)) && fs9.statSync(dataPath(dir)).size > 0;
2090
+ } catch {
2091
+ return false;
2092
+ }
2093
+ }
2094
+ function saveState(id) {
2095
+ const bytes = withScope(
2096
+ (lt) => Buffer.from(readonlyTx(id, "::actor::export_state", lt).Serialize())
2097
+ );
2098
+ fs9.mkdirSync(id.dir, { recursive: true, mode: 448 });
2099
+ const final = dataPath(id.dir);
2100
+ const tmp = `${final}.tmp`;
2101
+ let fd;
2102
+ try {
2103
+ fd = fs9.openSync(tmp, "w", 384);
2104
+ fs9.fchmodSync(fd, 384);
2105
+ fs9.writeFileSync(fd, bytes);
2106
+ fs9.fsyncSync(fd);
2107
+ fs9.closeSync(fd);
2108
+ fd = void 0;
2109
+ fs9.renameSync(tmp, final);
2110
+ fs9.chmodSync(final, 384);
2111
+ try {
2112
+ fs9.chmodSync(keyPath(id.dir), 384);
2113
+ } catch {
2114
+ }
2115
+ const dirFd = fs9.openSync(id.dir, "r");
2116
+ try {
2117
+ fs9.fsyncSync(dirFd);
2118
+ } finally {
2119
+ fs9.closeSync(dirFd);
2120
+ }
2121
+ } catch (err) {
2122
+ if (fd !== void 0) {
2123
+ try {
2124
+ fs9.closeSync(fd);
2125
+ } catch {
2126
+ }
2127
+ }
2128
+ try {
2129
+ fs9.rmSync(tmp, { force: true });
2130
+ } catch {
2131
+ }
2132
+ throw err;
2133
+ }
2134
+ }
2135
+ function saveStateFailClosed(id) {
2136
+ try {
2137
+ saveState(id);
2138
+ if (id.persistFailed) {
2139
+ id.persistFailed = false;
2140
+ log(`[${id.name}] persist recovered \u2014 quarantine lifted`);
2141
+ appendNotifyLog(id, { event: "persist_recovered" });
2142
+ }
2143
+ } catch (err) {
2144
+ id.persistFailed = true;
2145
+ log(`[${id.name}] PERSIST FAILED \u2014 identity quarantined (fail-closed), outbound of this txn withheld:`, String(err));
2146
+ try {
2147
+ appendNotifyLog(id, { event: "persist_failed", error: String(err).slice(0, 300) });
2148
+ } catch {
2149
+ }
2150
+ process.nextTick(
2151
+ () => fireNotify(id.name, `[${id.name}] PERSIST FAILED \u2014 messaging quarantined until the state file is writable again`)
2152
+ );
2153
+ throw err;
2154
+ }
2155
+ }
2156
+
2157
+ // ../../src/mufl/tx.ts
2158
+ function withScope(fn) {
2159
+ const lt = new AdaptObjectLifetime2();
2160
+ try {
2161
+ return fn(lt);
2162
+ } finally {
2163
+ lt.Finalize();
2164
+ }
2165
+ }
2166
+ async function withScopeAsync(fn) {
2167
+ const lt = new AdaptObjectLifetime2();
2168
+ try {
2169
+ return await fn(lt);
2170
+ } finally {
2171
+ lt.Finalize();
2172
+ }
2173
+ }
2174
+ function readonlyTx(id, name, lt, targ) {
2175
+ const envelope = object_to_adapt_value({ name, targ });
2176
+ const result = id.pw.packet.ExecuteTransaction(envelope);
2177
+ envelope.Destroy();
2178
+ return lt ? result.Attach(lt) : result;
2179
+ }
2180
+ async function withLock(id, fn) {
2181
+ const prev = id.lock;
2182
+ let release;
2183
+ id.lock = new Promise((r) => release = r);
2184
+ await prev;
2185
+ try {
2186
+ return await fn();
2187
+ } finally {
2188
+ release();
2189
+ }
2190
+ }
2191
+ function enqueueMutation(id, envelope, timeoutMs = 25e3) {
2192
+ return new Promise((res, rej) => {
2193
+ const timer = setTimeout(() => {
2194
+ const i = id.pending.findIndex((p) => p.timer === timer);
2195
+ if (i >= 0) id.pending.splice(i, 1);
2196
+ rej(new Error("timed out waiting for the transaction result"));
2197
+ }, timeoutMs);
2198
+ id.pending.push({ resolve: res, reject: rej, timer });
2199
+ id.pw.add_client_message(envelope);
2200
+ });
2201
+ }
2202
+ function mutatingTx(id, name, targ, lt, timeoutMs) {
2203
+ if (id.persistFailed) {
2204
+ try {
2205
+ saveStateFailClosed(id);
2206
+ } catch (err) {
2207
+ return Promise.reject(new Error(
2208
+ `identity "${id.name}" is quarantined: state persist is failing (${String(err)}) \u2014 mutations are rejected until state_data.bin is writable again`
2209
+ ));
2210
+ }
2211
+ }
2212
+ const envelope = object_to_adapt_value({ name, targ });
2213
+ return withLock(id, () => enqueueMutation(id, envelope, timeoutMs)).then(
2214
+ (payload) => {
2215
+ envelope.Destroy();
2216
+ if (id.persistFailed) {
2217
+ try {
2218
+ payload.Destroy();
2219
+ } catch {
2220
+ }
2221
+ throw new Error(
2222
+ `identity "${id.name}": state persist FAILED \u2014 this transaction's outbound was withheld (fail-closed); fix the state directory and retry`
2223
+ );
2224
+ }
2225
+ return lt ? payload.Attach(lt) : payload;
2226
+ },
2227
+ (err) => {
2228
+ try {
2229
+ envelope.Destroy();
2230
+ } catch {
2231
+ }
2232
+ throw err;
2233
+ }
2234
+ );
2235
+ }
2236
+
2237
+ // ../../src/identity/hierarchy.ts
2238
+ function setRootName(name) {
2239
+ rootName = name;
2240
+ }
2241
+ var rootMarkerPath = () => join8(STATE_DIR, "root.json");
2242
+ var rootName = null;
2243
+ function readRootMarker() {
2244
+ try {
2245
+ const parsed = JSON.parse(fs10.readFileSync(rootMarkerPath(), "utf8"));
2246
+ return typeof parsed.name === "string" ? parsed.name : null;
2247
+ } catch {
2248
+ return null;
2249
+ }
2250
+ }
2251
+ function writeRootMarker(name) {
2252
+ fs10.mkdirSync(STATE_DIR, { recursive: true });
2253
+ const tmp = `${rootMarkerPath()}.tmp`;
2254
+ fs10.writeFileSync(tmp, JSON.stringify({ v: 1, name }));
2255
+ fs10.renameSync(tmp, rootMarkerPath());
2256
+ }
2257
+ function clearRootMarker() {
2258
+ fs10.rmSync(rootMarkerPath(), { force: true });
2259
+ }
2260
+ function describeIdentity(id) {
2261
+ return withScope((lt) => {
2262
+ const v = readonlyTx(id, "::actor::describe_identity", lt);
2263
+ return {
2264
+ bio: v.Reduce("bio").Visualize(),
2265
+ persona: v.Reduce("persona").Visualize(),
2266
+ hasCert: v.Reduce("has_cert").GetBoolean(),
2267
+ roleId: v.Reduce("role_id").Visualize(),
2268
+ rootCid: v.Reduce("root_cid").Visualize(),
2269
+ rootName: v.Reduce("root_name").Visualize(),
2270
+ monitoringEnabled: v.Reduce("monitoring_enabled").GetBoolean()
2271
+ };
2272
+ });
2273
+ }
2274
+ async function delegateRole(root, role) {
2275
+ await withScopeAsync(async (lt) => {
2276
+ const roleAd = exportAdBlob(role);
2277
+ const signed = await mutatingTx(root, "::actor::sign_delegation", {
2278
+ role_ad: root.pw.packet.NewBinaryFromBuffer(roleAd).Attach(lt),
2279
+ role_id: role.name
2280
+ }, lt);
2281
+ const certBlob = Buffer.from(signed.Reduce("cert").GetBinary());
2282
+ const profileData = await mutatingTx(root, "::actor::export_root_profile", {}, lt);
2283
+ const profileBlob = Buffer.from(profileData.Reduce("profile").GetBinary());
2284
+ const rootAdBlob = exportAdBlob(root);
2285
+ const roleAdV1 = Buffer.from(
2286
+ (await mutatingTx(role, "::actor::export_v1_address_document", {}, lt)).Reduce("ad").GetBinary()
2287
+ );
2288
+ const signedV1 = await mutatingTx(root, "::actor::sign_delegation", {
2289
+ role_ad: root.pw.packet.NewBinaryFromBuffer(roleAdV1).Attach(lt),
2290
+ role_id: role.name
2291
+ }, lt);
2292
+ const certV1Blob = Buffer.from(signedV1.Reduce("cert").GetBinary());
2293
+ await mutatingTx(role, "::actor::set_delegation", {
2294
+ cert: role.pw.packet.NewBinaryFromBuffer(certBlob).Attach(lt),
2295
+ root_ad: role.pw.packet.NewBinaryFromBuffer(rootAdBlob).Attach(lt),
2296
+ root_profile: role.pw.packet.NewBinaryFromBuffer(profileBlob).Attach(lt),
2297
+ cert_v1: role.pw.packet.NewBinaryFromBuffer(certV1Blob).Attach(lt)
2298
+ }, lt);
2299
+ });
2300
+ log(`[${role.name}] delegated as a role under root "${root.name}"`);
2301
+ }
2302
+ async function establishRoot(id) {
2303
+ rootName = id.name;
2304
+ writeRootMarker(id.name);
2305
+ const adopted = [];
2306
+ const failed = [];
2307
+ for (const other of identities.values()) {
2308
+ if (other.name === id.name) continue;
2309
+ if (other.temp) continue;
2310
+ try {
2311
+ await delegateRole(id, other);
2312
+ adopted.push(other.name);
2313
+ } catch (err) {
2314
+ log(`failed to adopt "${other.name}" as a role under new root "${id.name}":`, String(err));
2315
+ failed.push(other.name);
2316
+ }
2317
+ }
2318
+ log(`[${id.name}] established as the host root${adopted.length ? ` (adopted ${adopted.length} role(s))` : ""}`);
2319
+ return { adopted, failed };
1903
2320
  }
1904
2321
 
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 {
2322
+ // ../../src/identity/lifecycle.ts
2323
+ import { join as join9 } from "node:path";
2324
+ import * as fs11 from "node:fs";
2325
+
2326
+ // ../../src/render/adapt-to-json.ts
2327
+ import { brotliCompressSync, brotliDecompressSync } from "node:zlib";
2328
+
2329
+ // ../../src/transcribe.ts
2330
+ function baseMime(mime) {
2331
+ return (mime ?? "").split(";")[0].trim() || "application/octet-stream";
2332
+ }
2333
+ var STT_PROVIDERS = ["openai-compatible", "elevenlabs", "deepgram", "custom"];
2334
+ var STT_MAX_BYTES_DEFAULT = 5 * 1024 * 1024;
2335
+ var STT_TIMEOUT_MS_DEFAULT = 6e4;
2336
+ function sttStatus(cfg) {
2337
+ const hint = "configure it in config.json `stt: {}` or via OURS_STT_* env";
2338
+ if (!cfg?.provider) {
2339
+ return { ready: false, reason: `no STT provider configured (stt.provider: ${STT_PROVIDERS.join(" | ")}) \u2014 ${hint}` };
1912
2340
  }
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 });
2341
+ const provider = cfg.provider.trim().toLowerCase();
2342
+ if (!STT_PROVIDERS.includes(provider)) {
2343
+ return { ready: false, reason: `unknown STT provider "${cfg.provider}" (expected: ${STT_PROVIDERS.join(" | ")})` };
1917
2344
  }
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;
2345
+ if (!cfg.apiKey?.trim()) {
2346
+ return { ready: false, reason: `STT provider "${provider}" is set but the API key is missing (stt.apiKey / OURS_STT_API_KEY)` };
1926
2347
  }
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 {
2348
+ if (provider === "openai-compatible") {
2349
+ if (!cfg.baseUrl?.trim()) {
2350
+ 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
2351
  }
1949
- const dirFd = fs8.openSync(id.dir, "r");
1950
- try {
1951
- fs8.fsyncSync(dirFd);
1952
- } finally {
1953
- fs8.closeSync(dirFd);
2352
+ if (!cfg.model?.trim()) {
2353
+ return { ready: false, reason: "openai-compatible STT needs stt.model / OURS_STT_MODEL (passed to the provider verbatim) \u2014 no model is assumed" };
1954
2354
  }
1955
- } catch (err) {
1956
- if (fd !== void 0) {
1957
- try {
1958
- fs8.closeSync(fd);
1959
- } catch {
1960
- }
2355
+ }
2356
+ if (provider === "elevenlabs" && !cfg.model?.trim()) {
2357
+ 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" };
2358
+ }
2359
+ if (provider === "custom") {
2360
+ if (!cfg.custom?.url?.trim()) {
2361
+ return { ready: false, reason: "custom STT needs stt.custom.url (the full endpoint URL of your provider)" };
1961
2362
  }
1962
- try {
1963
- fs8.rmSync(tmp, { force: true });
1964
- } catch {
2363
+ const wantsModel = cfg.custom.url.includes("{model}") || cfg.custom.modelField !== void 0 && cfg.custom.modelField !== "";
2364
+ if (wantsModel && !cfg.model?.trim()) {
2365
+ 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
2366
  }
1966
- throw err;
1967
2367
  }
2368
+ return { ready: true, provider };
1968
2369
  }
1969
- function saveStateFailClosed(id) {
2370
+ function textAtPath(obj, path) {
2371
+ let cur = obj;
2372
+ for (const seg of path.split(".")) {
2373
+ if (cur === null || typeof cur !== "object") return void 0;
2374
+ cur = cur[seg];
2375
+ }
2376
+ return typeof cur === "string" ? cur : void 0;
2377
+ }
2378
+ async function request(url, init, timeoutMs, secrets = []) {
2379
+ const aborter = new AbortController();
2380
+ const timer = setTimeout(() => aborter.abort(), timeoutMs);
1970
2381
  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" });
2382
+ const resp = await fetch(url, { ...init, signal: aborter.signal });
2383
+ if (!resp.ok) {
2384
+ let detail = await resp.text().catch(() => "");
2385
+ for (const secret of secrets) {
2386
+ if (secret) detail = detail.split(secret).join("[redacted]");
2387
+ }
2388
+ return { ok: false, error: `STT HTTP ${resp.status}${detail ? `: ${detail.slice(0, 200)}` : ""}` };
1976
2389
  }
2390
+ return { ok: true, json: await resp.json() };
1977
2391
  } 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 {
2392
+ if (err?.name === "AbortError") {
2393
+ return { ok: false, error: `STT timeout after ${timeoutMs}ms` };
1983
2394
  }
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;
2395
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
2396
+ } finally {
2397
+ clearTimeout(timer);
1988
2398
  }
1989
2399
  }
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();
2400
+ var audioBlob = (bytes, mime) => (
2401
+ // Copy into a plain Uint8Array — a Buffer is not a valid BlobPart.
2402
+ new Blob([new Uint8Array(bytes)], { type: baseMime(mime) })
2403
+ );
2404
+ var openaiCompatible = async (bytes, filename, mime, cfg, timeoutMs) => {
2405
+ const form = new FormData();
2406
+ form.set("file", audioBlob(bytes, mime), filename);
2407
+ form.set("model", cfg.model.trim());
2408
+ form.set("response_format", "json");
2409
+ if (cfg.language?.trim()) form.set("language", cfg.language.trim());
2410
+ const r = await request(
2411
+ `${cfg.baseUrl.trim().replace(/\/$/, "")}/audio/transcriptions`,
2412
+ { method: "POST", headers: { Authorization: `Bearer ${cfg.apiKey.trim()}` }, body: form },
2413
+ timeoutMs,
2414
+ [cfg.apiKey.trim()]
2415
+ );
2416
+ if (!r.ok) return r;
2417
+ const text = textAtPath(r.json, "text");
2418
+ return text !== void 0 ? { ok: true, text } : { ok: false, error: "STT response missing text" };
2419
+ };
2420
+ var elevenlabs = async (bytes, filename, mime, cfg, timeoutMs) => {
2421
+ const form = new FormData();
2422
+ form.set("file", audioBlob(bytes, mime), filename);
2423
+ form.set("model_id", cfg.model.trim());
2424
+ if (cfg.language?.trim()) form.set("language_code", cfg.language.trim());
2425
+ const r = await request(
2426
+ `${(cfg.baseUrl?.trim() || "https://api.elevenlabs.io").replace(/\/$/, "")}/v1/speech-to-text`,
2427
+ { method: "POST", headers: { "xi-api-key": cfg.apiKey.trim() }, body: form },
2428
+ timeoutMs,
2429
+ [cfg.apiKey.trim()]
2430
+ );
2431
+ if (!r.ok) return r;
2432
+ const text = textAtPath(r.json, "text");
2433
+ return text !== void 0 ? { ok: true, text } : { ok: false, error: "STT response missing text" };
2434
+ };
2435
+ var deepgram = async (bytes, _filename, mime, cfg, timeoutMs) => {
2436
+ const params = new URLSearchParams();
2437
+ if (cfg.model?.trim()) params.set("model", cfg.model.trim());
2438
+ if (cfg.language?.trim()) params.set("language", cfg.language.trim());
2439
+ const q = params.toString();
2440
+ const r = await request(
2441
+ `${(cfg.baseUrl?.trim() || "https://api.deepgram.com").replace(/\/$/, "")}/v1/listen${q ? `?${q}` : ""}`,
2442
+ {
2443
+ method: "POST",
2444
+ headers: { Authorization: `Token ${cfg.apiKey.trim()}`, "Content-Type": baseMime(mime) },
2445
+ body: new Uint8Array(bytes)
2446
+ },
2447
+ timeoutMs,
2448
+ [cfg.apiKey.trim()]
2449
+ );
2450
+ if (!r.ok) return r;
2451
+ const text = textAtPath(r.json, "results.channels.0.alternatives.0.transcript");
2452
+ return text !== void 0 ? { ok: true, text } : { ok: false, error: "STT response missing transcript" };
2453
+ };
2454
+ var custom = async (bytes, filename, mime, cfg, timeoutMs) => {
2455
+ const t = cfg.custom;
2456
+ const model = cfg.model?.trim() ?? "";
2457
+ const url = t.url.replaceAll("{model}", encodeURIComponent(model));
2458
+ const headers = {};
2459
+ const authName = t.authHeaderName?.trim() || "Authorization";
2460
+ const authValue = (t.authHeaderTemplate ?? "Bearer {key}").replaceAll("{key}", cfg.apiKey.trim());
2461
+ if (authValue) headers[authName] = authValue;
2462
+ const mode = t.bodyMode ?? "multipart";
2463
+ const fileField = t.fileField?.trim() || "file";
2464
+ const modelField = t.modelField === void 0 ? "model" : t.modelField.trim();
2465
+ let body;
2466
+ if (mode === "multipart") {
2467
+ const form = new FormData();
2468
+ form.set(fileField, audioBlob(bytes, mime), filename);
2469
+ if (modelField && model) form.set(modelField, model);
2470
+ for (const [k, v] of Object.entries(t.extraFields ?? {})) form.set(k, v);
2471
+ body = form;
2472
+ } else if (mode === "raw") {
2473
+ headers["Content-Type"] = baseMime(mime);
2474
+ body = new Uint8Array(bytes);
2475
+ } else {
2476
+ headers["Content-Type"] = "application/json";
2477
+ body = JSON.stringify({
2478
+ [fileField]: bytes.toString("base64"),
2479
+ ...modelField && model ? { [modelField]: model } : {},
2480
+ ...t.extraFields ?? {}
2481
+ });
2482
+ }
2483
+ const r = await request(url, { method: t.method?.trim() || "POST", headers, body }, timeoutMs, [cfg.apiKey.trim()]);
2484
+ if (!r.ok) return r;
2485
+ const text = textAtPath(r.json, t.responseTextPath?.trim() || "text");
2486
+ return text !== void 0 ? { ok: true, text } : { ok: false, error: `STT response has no text at "${t.responseTextPath?.trim() || "text"}"` };
2487
+ };
2488
+ var ADAPTERS = {
2489
+ "openai-compatible": openaiCompatible,
2490
+ elevenlabs,
2491
+ deepgram,
2492
+ custom
2493
+ };
2494
+ async function transcribeVoice(bytes, filename, mime, cfg) {
2495
+ const status = sttStatus(cfg);
2496
+ if (!status.ready) return { ok: false, error: status.reason };
2497
+ const maxBytes = cfg.maxBytes ?? STT_MAX_BYTES_DEFAULT;
2498
+ if (bytes.length > maxBytes) {
2499
+ return { ok: false, error: `voice message is ${bytes.length} B \u2014 over the ${maxBytes} B STT limit (stt.maxBytes)` };
1998
2500
  }
1999
- }
2000
- async function withScopeAsync(fn) {
2001
- const lt = new AdaptObjectLifetime2();
2501
+ const timeoutMs = cfg.timeoutMs ?? STT_TIMEOUT_MS_DEFAULT;
2002
2502
  try {
2003
- return await fn(lt);
2004
- } finally {
2005
- lt.Finalize();
2503
+ return await ADAPTERS[status.provider](bytes, filename, mime, cfg, timeoutMs);
2504
+ } catch (err) {
2505
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
2006
2506
  }
2007
2507
  }
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;
2508
+ function voiceErrorCategory(error) {
2509
+ if (/limit|over .* bytes/i.test(error)) return "size_limit";
2510
+ if (/timeout/i.test(error)) return "timeout";
2511
+ if (/HTTP \d+/i.test(error)) return "provider_http";
2512
+ if (/missing|no text|no transcript/i.test(error)) return "invalid_response";
2513
+ return "provider_error";
2013
2514
  }
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();
2515
+ function structuredVoiceOutcome(readiness, outcome, association) {
2516
+ if (!readiness.ready || outcome.kind === "unconfigured") {
2517
+ return {
2518
+ configured: false,
2519
+ attempted: false,
2520
+ status: "unavailable",
2521
+ provider: null,
2522
+ text: null,
2523
+ error_category: "not_configured",
2524
+ audio_path: association.audioPath,
2525
+ file_wire_id: association.wireId
2526
+ };
2023
2527
  }
2528
+ if (outcome.kind === "transcript") {
2529
+ return {
2530
+ configured: true,
2531
+ attempted: true,
2532
+ status: "succeeded",
2533
+ provider: readiness.provider,
2534
+ text: outcome.text,
2535
+ error_category: null,
2536
+ audio_path: association.audioPath,
2537
+ file_wire_id: association.wireId
2538
+ };
2539
+ }
2540
+ return {
2541
+ configured: true,
2542
+ attempted: true,
2543
+ status: "failed",
2544
+ provider: readiness.provider,
2545
+ text: null,
2546
+ error_category: voiceErrorCategory(outcome.error),
2547
+ audio_path: association.audioPath,
2548
+ file_wire_id: association.wireId
2549
+ };
2024
2550
  }
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
- }
2551
+ function voiceDeliveryLine(args, outcome) {
2552
+ const head = ` \u2022 \u{1F3A4} voice message from ${args.sender} (${args.sizeBytes} B)`;
2553
+ const tail = `audio saved \u2192 ${args.savedPath} {${args.wire}}`;
2554
+ switch (outcome.kind) {
2555
+ case "transcript":
2556
+ return `${head}: "${outcome.text}" \u2014 transcribed from voice message (STT); ${tail}`;
2557
+ case "unconfigured":
2558
+ 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}`;
2559
+ case "failed":
2560
+ 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
2561
  }
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
2562
  }
2070
2563
 
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;
2564
+ // ../../src/render/adapt-to-json.ts
2565
+ function renderContacts(v) {
2566
+ const out = [];
2567
+ if (v.IsNil()) return out;
2568
+ for (const key of v.GetKeys()) {
2569
+ const c = v.Reduce(key);
2570
+ if (c.IsNil()) continue;
2571
+ out.push({
2572
+ name: c.Reduce("name").Visualize(),
2573
+ container_id: c.Reduce("container_id").Visualize()
2574
+ });
2083
2575
  }
2576
+ return out;
2084
2577
  }
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());
2578
+ function renderImportRenames(v) {
2579
+ const out = {};
2580
+ if (v.IsNil()) return out;
2581
+ for (const key of v.GetKeys()) {
2582
+ const n = v.Reduce(key);
2583
+ if (!n.IsNil()) out[typeof key === "string" ? key : key.Visualize()] = n.Visualize();
2584
+ }
2585
+ return out;
2090
2586
  }
2091
- function clearRootMarker() {
2092
- fs9.rmSync(rootMarkerPath(), { force: true });
2587
+ function renderPending(v) {
2588
+ const out = [];
2589
+ if (v.IsNil()) return out;
2590
+ for (const key of v.GetKeys()) {
2591
+ const p = v.Reduce(key);
2592
+ if (p.IsNil()) continue;
2593
+ out.push({
2594
+ container_id: typeof key === "string" ? key : key.Visualize(),
2595
+ name: p.Reduce("name").Visualize(),
2596
+ queued: parseInt(p.Reduce("queued").Visualize(), 10) || 0
2597
+ });
2598
+ }
2599
+ return out;
2093
2600
  }
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()
2601
+ function renderContactRoots(v) {
2602
+ const out = {};
2603
+ if (v.IsNil()) return out;
2604
+ for (const key of v.GetKeys()) {
2605
+ const r = v.Reduce(key);
2606
+ if (r.IsNil()) continue;
2607
+ out[typeof key === "string" ? key : key.Visualize()] = {
2608
+ root_cid: r.Reduce("root_cid").Visualize(),
2609
+ root_name: r.Reduce("root_name").Visualize(),
2610
+ role_id: r.Reduce("role_id").Visualize()
2105
2611
  };
2106
- });
2612
+ }
2613
+ return out;
2107
2614
  }
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}"`);
2615
+ function encodeWireBin(raw) {
2616
+ return brotliCompressSync(raw).toString("base64url");
2135
2617
  }
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 };
2618
+ function decodeWireBin(s) {
2619
+ return brotliDecompressSync(Buffer.from(s.replace(/\s+/g, ""), "base64url"));
2154
2620
  }
2155
2621
 
2156
2622
  // ../../src/identity/lifecycle.ts
2157
- import { join as join9 } from "node:path";
2158
- import * as fs10 from "node:fs";
2159
2623
  function deleteIdentityCompletely(id) {
2624
+ try {
2625
+ closeHistory(id);
2626
+ } catch (err) {
2627
+ log(`closeHistory(${id.name}) failed:`, String(err));
2628
+ }
2160
2629
  try {
2161
2630
  wrapper.remove_packet(id.cid);
2162
2631
  } catch (err) {
@@ -2177,7 +2646,7 @@ function deleteIdentityCompletely(id) {
2177
2646
  clearRootMarker();
2178
2647
  }
2179
2648
  try {
2180
- fs10.rmSync(id.dir, { recursive: true, force: true });
2649
+ fs11.rmSync(id.dir, { recursive: true, force: true });
2181
2650
  reservedNames.delete(id.name);
2182
2651
  } catch (err) {
2183
2652
  return `deleting ${id.dir} failed: ${String(err)}`;
@@ -2242,14 +2711,14 @@ function sweepStaleTempIdentities() {
2242
2711
  }
2243
2712
  }
2244
2713
  function sweepOrphanTempDirs() {
2245
- if (!fs10.existsSync(STATE_DIR)) return;
2246
- for (const d of fs10.readdirSync(STATE_DIR, { withFileTypes: true })) {
2714
+ if (!fs11.existsSync(STATE_DIR)) return;
2715
+ for (const d of fs11.readdirSync(STATE_DIR, { withFileTypes: true })) {
2247
2716
  if (!d.isDirectory() || identities.has(d.name)) continue;
2248
2717
  const dir = join9(STATE_DIR, d.name);
2249
2718
  const meta = readTempMetaFile(dir);
2250
2719
  if (!meta || pidAlive(meta.owner.pid)) continue;
2251
2720
  try {
2252
- fs10.rmSync(dir, { recursive: true, force: true });
2721
+ fs11.rmSync(dir, { recursive: true, force: true });
2253
2722
  reservedNames.delete(d.name);
2254
2723
  log(`[${d.name}] removed orphaned temporary-identity dir (owner pid ${meta.owner.pid} dead, no live packet)`);
2255
2724
  } catch (err) {
@@ -2390,6 +2859,7 @@ async function bootWrapper() {
2390
2859
  if (tempMeta) id.temp = tempMeta;
2391
2860
  } catch (err) {
2392
2861
  log(`failed to restore "${name}":`, String(err));
2862
+ throw err;
2393
2863
  }
2394
2864
  startupProgress?.update("identities", { completed: index + 1, total: names.length });
2395
2865
  }
@@ -2465,6 +2935,15 @@ export {
2465
2935
  log,
2466
2936
  requireAuth,
2467
2937
  validateName,
2938
+ consumeOutboundHistoryFailure,
2939
+ listIncomingMessages,
2940
+ takeUnreadMessages,
2941
+ listIncomingFiles,
2942
+ takeUnreadFiles,
2943
+ listMessageHistory,
2944
+ getMessageHistoryItem,
2945
+ listFileHistory,
2946
+ getFileHistoryItem,
2468
2947
  FILE_SELECTION_CAP,
2469
2948
  identities,
2470
2949
  registrar,
@@ -2481,21 +2960,9 @@ export {
2481
2960
  persistBindings,
2482
2961
  resolveBound,
2483
2962
  bindSession,
2484
- buildMessagesPayload,
2485
- mimeFromExt,
2486
- renderContacts,
2487
- renderImportRenames,
2488
- renderInbox,
2489
- renderFileMetadata,
2490
- writeIncomingFiles,
2491
- renderPending,
2492
- renderContactRoots,
2493
- encodeWireBin,
2494
- decodeWireBin,
2495
2963
  setNotifyHook,
2496
2964
  clearNotifyHook,
2497
2965
  appendNotifyLog,
2498
- readE2eWireIds,
2499
2966
  serveNotifications,
2500
2967
  refreshUnread,
2501
2968
  unreadSummary,
@@ -2519,6 +2986,16 @@ export {
2519
2986
  describeIdentity,
2520
2987
  delegateRole,
2521
2988
  establishRoot,
2989
+ sttStatus,
2990
+ transcribeVoice,
2991
+ structuredVoiceOutcome,
2992
+ voiceDeliveryLine,
2993
+ renderContacts,
2994
+ renderImportRenames,
2995
+ renderPending,
2996
+ renderContactRoots,
2997
+ encodeWireBin,
2998
+ decodeWireBin,
2522
2999
  deleteIdentityCompletely,
2523
3000
  closeTemporaryIdentity,
2524
3001
  sweepStaleTempIdentities,