@ours.network/cli 1.0.0 → 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.
- package/README.md +16 -4
- package/dist/{boot-35BOHNSZ.js → boot-LJ2V4NSS.js} +1 -1
- package/dist/{chunk-ZFE47KH3.js → chunk-FKSRG5UJ.js} +3 -3
- package/dist/{chunk-MHVYJY3H.js → chunk-LJ35FDQH.js} +1500 -1023
- package/dist/{chunk-QRNZFPNJ.js → chunk-MS4TDUZY.js} +41 -42
- package/dist/{chunk-QZIL3IT7.js → chunk-NRPPG3WH.js} +1 -1
- package/dist/{chunk-SDNTPCKW.js → chunk-NWRXKQBD.js} +10 -12
- package/dist/commands.d.ts +10 -12
- package/dist/commands.js +1 -1
- package/dist/{daemon-UUG6P6GK.js → daemon-YCC24SVU.js} +2 -2
- package/dist/help.js +3 -3
- package/dist/lifecycle.js +1 -1
- package/dist/main.js +5 -5
- package/dist/mufl_code/351F16D444E6DAA252D4A8D829D17204360D9015FEF670ACE68FB4747BB3B14E.muflo +0 -0
- package/dist/operations.d.ts +32 -57
- package/dist/operations.js +1 -1
- package/dist/{server-TDJBEWTU.js → server-2RIEOKDA.js} +198 -145
- package/package.json +3 -2
|
@@ -89,7 +89,7 @@ function createStartupProgressReporter(stateDir, opts = {}) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// ../../src/runtime/env.ts
|
|
92
|
-
var VERSION =
|
|
92
|
+
var VERSION = true ? "3.0.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
|
|
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) =>
|
|
211
|
-
var keyPath = (dir) =>
|
|
212
|
-
var dataPath = (dir) =>
|
|
213
|
-
var notifyLogPath = (dir) =>
|
|
214
|
-
var unreadPath = (dir) =>
|
|
215
|
-
var
|
|
216
|
-
var
|
|
217
|
-
var hashLeaseToken = (token) => createHash("sha256").update(token).digest("hex");
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
815
|
+
const dir = join4(STATE_DIR, name);
|
|
240
816
|
try {
|
|
241
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
272
|
-
return
|
|
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
|
|
277
|
-
import * as
|
|
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 = () =>
|
|
884
|
+
var bindingsSnapshotPath = () => join5(STATE_DIR, "bindings.json");
|
|
296
885
|
function persistBindings() {
|
|
297
886
|
try {
|
|
298
|
-
|
|
887
|
+
fs5.mkdirSync(STATE_DIR, { recursive: true });
|
|
299
888
|
const tmp = `${bindingsSnapshotPath()}.tmp`;
|
|
300
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
// ../../src/files.ts
|
|
480
|
-
import { basename, extname } from "node:path";
|
|
481
|
-
var FILE_MIME = {
|
|
482
|
-
".png": "image/png",
|
|
483
|
-
".jpg": "image/jpeg",
|
|
484
|
-
".jpeg": "image/jpeg",
|
|
485
|
-
".gif": "image/gif",
|
|
486
|
-
".webp": "image/webp",
|
|
487
|
-
".svg": "image/svg+xml",
|
|
488
|
-
".bmp": "image/bmp",
|
|
489
|
-
".ico": "image/x-icon",
|
|
490
|
-
".pdf": "application/pdf",
|
|
491
|
-
".txt": "text/plain",
|
|
492
|
-
".md": "text/markdown",
|
|
493
|
-
".json": "application/json",
|
|
494
|
-
".csv": "text/csv",
|
|
495
|
-
".html": "text/html",
|
|
496
|
-
".xml": "application/xml",
|
|
497
|
-
".zip": "application/zip",
|
|
498
|
-
".gz": "application/gzip",
|
|
499
|
-
".tar": "application/x-tar",
|
|
500
|
-
".mp3": "audio/mpeg",
|
|
501
|
-
".wav": "audio/wav",
|
|
502
|
-
".mp4": "video/mp4",
|
|
503
|
-
".mov": "video/quicktime"
|
|
504
|
-
};
|
|
505
|
-
function mimeFromExt(p) {
|
|
506
|
-
return FILE_MIME[extname(p).toLowerCase()] ?? "application/octet-stream";
|
|
507
|
-
}
|
|
508
|
-
function sanitizeFilename(name) {
|
|
509
|
-
const base = basename(name).replace(/[^A-Za-z0-9._-]/g, "_");
|
|
510
|
-
return base.length ? base.slice(0, 200) : "file";
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const parts = (mime ?? "").toLowerCase().split(";").map((p) => p.trim());
|
|
518
|
-
if (parts[0].startsWith("audio/") && parts.slice(1).includes(VOICE_MESSAGE_MIME_PARAM)) return true;
|
|
519
|
-
return filename.toLowerCase().startsWith(VOICE_MESSAGE_FILENAME_PREFIX) && parts[0].startsWith("audio/");
|
|
1033
|
+
function clearNotifyHook(fn) {
|
|
1034
|
+
if (installed?.hook === fn) {
|
|
1035
|
+
installed.uninstall();
|
|
1036
|
+
installed = null;
|
|
1037
|
+
}
|
|
520
1038
|
}
|
|
521
|
-
function
|
|
522
|
-
|
|
1039
|
+
function fireNotify(identityName, summary) {
|
|
1040
|
+
emitDaemonNotification(identityName, summary);
|
|
523
1041
|
}
|
|
524
|
-
var
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
537
|
-
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
544
|
-
|
|
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 (
|
|
548
|
-
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
1130
|
+
fireNotifyWaiters(id.name);
|
|
1131
|
+
emitDaemonEvent(id, contentFree);
|
|
560
1132
|
}
|
|
561
|
-
function
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
const
|
|
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
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
file_id: f.Reduce("file_id").Visualize(),
|
|
1166
|
-
sender_id: f.Reduce("sender_id").Visualize(),
|
|
1167
|
-
from: f.Reduce("sender_name").Visualize(),
|
|
1168
|
-
filename: f.Reduce("filename").Visualize(),
|
|
1169
|
-
mime: f.Reduce("mime").Visualize(),
|
|
1170
|
-
bytes: f.Reduce("size").Visualize(),
|
|
1171
|
-
date: f.Reduce("date").Visualize(),
|
|
1172
|
-
wire_id: f.Reduce("wire_id").Visualize()
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
return { unread: unread2, unreadFiles: unreadFiles2 };
|
|
1177
|
-
});
|
|
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.
|
|
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
|
-
|
|
1245
|
+
fs6.mkdirSync(id.dir, { recursive: true, mode: 448 });
|
|
1185
1246
|
const tmp = `${unreadPath(id.dir)}.tmp`;
|
|
1186
|
-
|
|
1187
|
-
|
|
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 =
|
|
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(
|
|
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 === "
|
|
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
|
|
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(
|
|
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
|
-
|
|
1825
|
+
fs7.mkdirSync(bookDir(), { recursive: true });
|
|
1659
1826
|
const tmp = `${bookPath()}.tmp`;
|
|
1660
|
-
|
|
1661
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
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/
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
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
|
|
1914
|
-
|
|
1915
|
-
|
|
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
|
-
|
|
1919
|
-
|
|
1920
|
-
}
|
|
1921
|
-
function hasSavedState(dir) {
|
|
1922
|
-
try {
|
|
1923
|
-
return fs8.existsSync(dataPath(dir)) && fs8.statSync(dataPath(dir)).size > 0;
|
|
1924
|
-
} catch {
|
|
1925
|
-
return false;
|
|
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
|
-
|
|
1929
|
-
|
|
1930
|
-
(lt) => Buffer.from(readonlyTx(id, "::actor::export_state", lt).Serialize())
|
|
1931
|
-
);
|
|
1932
|
-
fs8.mkdirSync(id.dir, { recursive: true, mode: 448 });
|
|
1933
|
-
const final = dataPath(id.dir);
|
|
1934
|
-
const tmp = `${final}.tmp`;
|
|
1935
|
-
let fd;
|
|
1936
|
-
try {
|
|
1937
|
-
fd = fs8.openSync(tmp, "w", 384);
|
|
1938
|
-
fs8.fchmodSync(fd, 384);
|
|
1939
|
-
fs8.writeFileSync(fd, bytes);
|
|
1940
|
-
fs8.fsyncSync(fd);
|
|
1941
|
-
fs8.closeSync(fd);
|
|
1942
|
-
fd = void 0;
|
|
1943
|
-
fs8.renameSync(tmp, final);
|
|
1944
|
-
fs8.chmodSync(final, 384);
|
|
1945
|
-
try {
|
|
1946
|
-
fs8.chmodSync(keyPath(id.dir), 384);
|
|
1947
|
-
} catch {
|
|
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
|
-
|
|
1950
|
-
|
|
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
|
-
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
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
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
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
|
|
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
|
-
|
|
1972
|
-
if (
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
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
|
-
|
|
1979
|
-
|
|
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
|
-
|
|
1985
|
-
|
|
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
|
-
//
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
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
|
|
2004
|
-
}
|
|
2005
|
-
|
|
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
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
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
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
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
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
}
|
|
2036
|
-
function mutatingTx(id, name, targ, lt, timeoutMs) {
|
|
2037
|
-
if (id.persistFailed) {
|
|
2038
|
-
try {
|
|
2039
|
-
saveStateFailClosed(id);
|
|
2040
|
-
} catch (err) {
|
|
2041
|
-
return Promise.reject(new Error(
|
|
2042
|
-
`identity "${id.name}" is quarantined: state persist is failing (${String(err)}) \u2014 mutations are rejected until state_data.bin is writable again`
|
|
2043
|
-
));
|
|
2044
|
-
}
|
|
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/
|
|
2072
|
-
function
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
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
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
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
|
|
2092
|
-
|
|
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
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
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
|
-
|
|
2109
|
-
|
|
2110
|
-
const roleAd = exportAdBlob(role);
|
|
2111
|
-
const signed = await mutatingTx(root, "::actor::sign_delegation", {
|
|
2112
|
-
role_ad: root.pw.packet.NewBinaryFromBuffer(roleAd).Attach(lt),
|
|
2113
|
-
role_id: role.name
|
|
2114
|
-
}, lt);
|
|
2115
|
-
const certBlob = Buffer.from(signed.Reduce("cert").GetBinary());
|
|
2116
|
-
const profileData = await mutatingTx(root, "::actor::export_root_profile", {}, lt);
|
|
2117
|
-
const profileBlob = Buffer.from(profileData.Reduce("profile").GetBinary());
|
|
2118
|
-
const rootAdBlob = exportAdBlob(root);
|
|
2119
|
-
const roleAdV1 = Buffer.from(
|
|
2120
|
-
(await mutatingTx(role, "::actor::export_v1_address_document", {}, lt)).Reduce("ad").GetBinary()
|
|
2121
|
-
);
|
|
2122
|
-
const signedV1 = await mutatingTx(root, "::actor::sign_delegation", {
|
|
2123
|
-
role_ad: root.pw.packet.NewBinaryFromBuffer(roleAdV1).Attach(lt),
|
|
2124
|
-
role_id: role.name
|
|
2125
|
-
}, lt);
|
|
2126
|
-
const certV1Blob = Buffer.from(signedV1.Reduce("cert").GetBinary());
|
|
2127
|
-
await mutatingTx(role, "::actor::set_delegation", {
|
|
2128
|
-
cert: role.pw.packet.NewBinaryFromBuffer(certBlob).Attach(lt),
|
|
2129
|
-
root_ad: role.pw.packet.NewBinaryFromBuffer(rootAdBlob).Attach(lt),
|
|
2130
|
-
root_profile: role.pw.packet.NewBinaryFromBuffer(profileBlob).Attach(lt),
|
|
2131
|
-
cert_v1: role.pw.packet.NewBinaryFromBuffer(certV1Blob).Attach(lt)
|
|
2132
|
-
}, lt);
|
|
2133
|
-
});
|
|
2134
|
-
log(`[${role.name}] delegated as a role under root "${root.name}"`);
|
|
2615
|
+
function encodeWireBin(raw) {
|
|
2616
|
+
return brotliCompressSync(raw).toString("base64url");
|
|
2135
2617
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
writeRootMarker(id.name);
|
|
2139
|
-
const adopted = [];
|
|
2140
|
-
const failed = [];
|
|
2141
|
-
for (const other of identities.values()) {
|
|
2142
|
-
if (other.name === id.name) continue;
|
|
2143
|
-
if (other.temp) continue;
|
|
2144
|
-
try {
|
|
2145
|
-
await delegateRole(id, other);
|
|
2146
|
-
adopted.push(other.name);
|
|
2147
|
-
} catch (err) {
|
|
2148
|
-
log(`failed to adopt "${other.name}" as a role under new root "${id.name}":`, String(err));
|
|
2149
|
-
failed.push(other.name);
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
log(`[${id.name}] established as the host root${adopted.length ? ` (adopted ${adopted.length} role(s))` : ""}`);
|
|
2153
|
-
return { adopted, failed };
|
|
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
|
-
|
|
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 (!
|
|
2246
|
-
for (const d of
|
|
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
|
-
|
|
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,
|