edge-book 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/edge-book.js CHANGED
@@ -28,6 +28,8 @@ var EdgeBookError = class extends Error {
28
28
  var IDENTITY_FILE = "identity.json";
29
29
  var CONTACTS_FILE = "contacts.json";
30
30
  var GRANTS_FILE = "grants.json";
31
+ var OBJECTS_FILE = "objects.json";
32
+ var ATTACHMENTS_DIR = "attachments";
31
33
  var SEEN_MESSAGES_FILE = "seen-messages.json";
32
34
  var CONFIG_FILE = "config.json";
33
35
  var RELATIONSHIP_EVENTS_FILE = "relationship-events.jsonl";
@@ -450,6 +452,213 @@ var EdgeBookStore = class {
450
452
  await appendJsonl(this.file(INBOX_FILE), envelope);
451
453
  await this.audit("message.receive", envelope.from_agent_id, { message_id: envelope.message_id });
452
454
  }
455
+ // ──────────────────────────────────────────────────────────────────────
456
+ // Edge Book MVP: single shared object + object.read grant (spec-0020 R2/R3)
457
+ // ea-claude-066. One object type ("request"), ≤1 attachment, fail-closed
458
+ // access, append-only audit on create/grant/access/revoke. No R4 fields.
459
+ // ──────────────────────────────────────────────────────────────────────
460
+ async objects() {
461
+ return readJson(this.file(OBJECTS_FILE), {});
462
+ }
463
+ async saveObjects(objects) {
464
+ await writeJson(this.file(OBJECTS_FILE), objects);
465
+ }
466
+ async getObject(objectId) {
467
+ return (await this.objects())[objectId];
468
+ }
469
+ // Create one shared object (a request + at most one attachment). Signed and
470
+ // stored locally; writes an `object.create` audit event.
471
+ async createObject(input) {
472
+ const identity = await this.identity();
473
+ const object_id = randomId("obj");
474
+ let attachment;
475
+ if (input.attachment) {
476
+ const ref = path.join(ATTACHMENTS_DIR, `${object_id}-${input.attachment.filename}`);
477
+ await fs.mkdir(this.file(ATTACHMENTS_DIR), { recursive: true });
478
+ await fs.writeFile(this.file(ref), input.attachment.bytes);
479
+ attachment = {
480
+ filename: input.attachment.filename,
481
+ mime: input.attachment.mime,
482
+ size: input.attachment.bytes.length,
483
+ ref
484
+ };
485
+ }
486
+ const unsigned = {
487
+ object_id,
488
+ type: "request",
489
+ from_agent: identity.agent_id,
490
+ request: { title: input.title, body: input.body },
491
+ ...attachment ? { attachment } : {},
492
+ created_at: now()
493
+ };
494
+ const object = { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
495
+ const objects = await this.objects();
496
+ objects[object_id] = object;
497
+ await this.saveObjects(objects);
498
+ await this.audit("object.create", identity.agent_id, { object_id, has_attachment: Boolean(attachment) });
499
+ return object;
500
+ }
501
+ // Issue an `object.read` grant binding ONE object to ONE subject (revocable).
502
+ async issueObjectGrant(subjectAgentId, objectId, expiresAt = "") {
503
+ const identity = await this.identity();
504
+ if (!await this.getObject(objectId)) throw new EdgeBookError("unknown_object", `Unknown object: ${objectId}`);
505
+ const unsigned = {
506
+ grant_id: randomId("grant"),
507
+ issuer_agent_id: identity.agent_id,
508
+ subject_agent_id: subjectAgentId,
509
+ relationship_id: relationshipId(identity.agent_id, subjectAgentId),
510
+ scopes: ["object.read"],
511
+ status: "active",
512
+ issued_at: now(),
513
+ expires_at: expiresAt,
514
+ revoked_at: "",
515
+ audit_refs: [],
516
+ object_id: objectId
517
+ };
518
+ const grant = { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
519
+ await this.storeGrant(grant);
520
+ await this.audit("grant.issue", subjectAgentId, { grant_id: grant.grant_id, object_id: objectId, scope: "object.read" });
521
+ return grant;
522
+ }
523
+ // Fail-closed predicate (spec-0020 R3): readable IFF an active, unexpired
524
+ // `object.read` grant exists for (object_id, subject). Does NOT audit — use
525
+ // readObject() for an audited access. The object's owner may always read it.
526
+ async canReadObject(objectId, subjectAgentId, at = Date.now()) {
527
+ const object = await this.getObject(objectId);
528
+ if (object && object.from_agent === subjectAgentId) return true;
529
+ const grants = await this.grants();
530
+ return Object.values(grants).some(
531
+ (grant) => grant.object_id === objectId && grant.subject_agent_id === subjectAgentId && grant.scopes.includes("object.read") && grant.status === "active" && (!grant.expires_at || Date.parse(grant.expires_at) > at)
532
+ );
533
+ }
534
+ // Audited read. Returns the object iff canReadObject; else fails closed.
535
+ async readObject(objectId, subjectAgentId) {
536
+ const object = await this.getObject(objectId);
537
+ if (!object || !await this.canReadObject(objectId, subjectAgentId)) {
538
+ throw new EdgeBookError("access_denied", `No active object.read grant for (${objectId}, ${subjectAgentId})`);
539
+ }
540
+ await this.audit("object.access", subjectAgentId, { object_id: objectId });
541
+ return object;
542
+ }
543
+ // Raw bytes of an object's (single) attachment, agent-held under attachments/.
544
+ // Caller is responsible for the access check (readObject) first.
545
+ async readAttachmentBytes(objectId) {
546
+ const object = await this.getObject(objectId);
547
+ if (!object?.attachment) throw new EdgeBookError("no_attachment", `No attachment for ${objectId}`);
548
+ return fs.readFile(this.file(object.attachment.ref));
549
+ }
550
+ // Objects the given subject (default: me) may currently read — the data behind
551
+ // the reader's "Shared with me" surface. Read-through is unaudited (listing);
552
+ // readObject() audits the actual open.
553
+ async sharedObjectsFor(subjectAgentId) {
554
+ const subject = subjectAgentId ?? (await this.identity()).agent_id;
555
+ const objects = await this.objects();
556
+ const out = [];
557
+ for (const object of Object.values(objects)) {
558
+ if (object.from_agent === subject) continue;
559
+ if (await this.canReadObject(object.object_id, subject)) out.push(object);
560
+ }
561
+ return out.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
562
+ }
563
+ // Build a signed `object_share` envelope (object + grant + inline attachment)
564
+ // to deliver to a friend over the mailbox transport (ea-claude-065).
565
+ async shareObjectEnvelope(peerAgentId, objectId, expiresAt = "") {
566
+ const identity = await this.identity();
567
+ const contact = (await this.contacts())[peerAgentId];
568
+ if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
569
+ if (contact.relationship_state !== "friend") {
570
+ throw new EdgeBookError("not_friend", `Cannot share to relationship_state=${contact.relationship_state}`);
571
+ }
572
+ const object = await this.getObject(objectId);
573
+ if (!object) throw new EdgeBookError("unknown_object", `Unknown object: ${objectId}`);
574
+ const grant = await this.issueObjectGrant(peerAgentId, objectId, expiresAt);
575
+ let attachment_b64;
576
+ if (object.attachment) {
577
+ attachment_b64 = (await fs.readFile(this.file(object.attachment.ref))).toString("base64");
578
+ }
579
+ return this.signEnvelope({
580
+ type: "object_share",
581
+ to_agent_id: peerAgentId,
582
+ relationship_id: relationshipId(identity.agent_id, peerAgentId),
583
+ capability_id: grant.grant_id,
584
+ ref: objectId,
585
+ transport: "local",
586
+ body: { object, grant, ...attachment_b64 ? { attachment_b64 } : {} }
587
+ });
588
+ }
589
+ // Apply a received `object_share`: store the object (+ attachment) and grant,
590
+ // after verifying the envelope signature and that the grant matches.
591
+ async receiveObjectShare(envelope) {
592
+ await this.verifyEnvelope(envelope);
593
+ if (envelope.type !== "object_share") throw new EdgeBookError("wrong_message_type", "Expected object_share envelope");
594
+ const identity = await this.identity();
595
+ const body = envelope.body;
596
+ const { object, grant } = body;
597
+ if (!object || !grant) throw new EdgeBookError("malformed_object_share", "object_share missing object or grant");
598
+ if (object.from_agent !== envelope.from_agent_id) throw new EdgeBookError("agent_id_mismatch", "Shared object author does not match sender");
599
+ if (grant.object_id !== object.object_id || grant.subject_agent_id !== identity.agent_id || !grant.scopes.includes("object.read")) {
600
+ throw new EdgeBookError("grant_mismatch", "Grant does not bind this object to me with object.read");
601
+ }
602
+ if (body.attachment_b64 && object.attachment) {
603
+ await fs.mkdir(this.file(ATTACHMENTS_DIR), { recursive: true });
604
+ await fs.writeFile(this.file(object.attachment.ref), Buffer.from(body.attachment_b64, "base64"));
605
+ }
606
+ const objects = await this.objects();
607
+ objects[object.object_id] = object;
608
+ await this.saveObjects(objects);
609
+ await this.storeGrant(grant);
610
+ await this.audit("object.receive", envelope.from_agent_id, { object_id: object.object_id, grant_id: grant.grant_id });
611
+ return object;
612
+ }
613
+ // Revoke an object.read grant (forward-looking; does not claw back delivered
614
+ // data). Writes a `grant.revoke` audit event. Returns the revoked grant_ids.
615
+ async revokeObjectGrant(objectId, subjectAgentId) {
616
+ const grants = await this.grants();
617
+ const revoked = [];
618
+ for (const grant of Object.values(grants)) {
619
+ if (grant.object_id === objectId && grant.subject_agent_id === subjectAgentId && grant.status === "active") {
620
+ grant.status = "revoked";
621
+ grant.revoked_at = now();
622
+ revoked.push(grant.grant_id);
623
+ }
624
+ }
625
+ if (revoked.length) {
626
+ await this.saveGrants(grants);
627
+ await this.audit("grant.revoke", subjectAgentId, { object_id: objectId, grant_ids: revoked });
628
+ }
629
+ return revoked;
630
+ }
631
+ // Build a signed `object_revoke` envelope to forward the revoke to the peer.
632
+ async revokeObjectEnvelope(peerAgentId, objectId) {
633
+ const identity = await this.identity();
634
+ const revoked = await this.revokeObjectGrant(objectId, peerAgentId);
635
+ return this.signEnvelope({
636
+ type: "object_revoke",
637
+ to_agent_id: peerAgentId,
638
+ relationship_id: relationshipId(identity.agent_id, peerAgentId),
639
+ capability_id: revoked[0] || "",
640
+ ref: objectId,
641
+ transport: "local",
642
+ body: { object_id: objectId, grant_id: revoked[0] || "" }
643
+ });
644
+ }
645
+ // Apply a received `object_revoke`: mark the matching grant revoked locally.
646
+ async receiveObjectRevoke(envelope) {
647
+ await this.verifyEnvelope(envelope);
648
+ if (envelope.type !== "object_revoke") throw new EdgeBookError("wrong_message_type", "Expected object_revoke envelope");
649
+ const body = envelope.body;
650
+ const grants = await this.grants();
651
+ let changed = false;
652
+ for (const grant of Object.values(grants)) {
653
+ if (grant.object_id === body.object_id && grant.issuer_agent_id === envelope.from_agent_id && grant.status === "active") {
654
+ grant.status = "revoked";
655
+ grant.revoked_at = now();
656
+ changed = true;
657
+ }
658
+ }
659
+ if (changed) await this.saveGrants(grants);
660
+ await this.audit("object.revoke.receive", envelope.from_agent_id, { object_id: body.object_id });
661
+ }
453
662
  async findUsableGrant(peerAgentId, scope) {
454
663
  const identity = await this.identity();
455
664
  const grants = await this.grants();
@@ -498,6 +707,14 @@ var EdgeBookStore = class {
498
707
  if (envelope.type === "friend_request") return this.receiveFriendRequest(envelope);
499
708
  if (envelope.type === "friend_response") return this.applyFriendResponse(envelope);
500
709
  if (envelope.type === "privileged_message") return this.receivePrivilegedMessage(envelope);
710
+ if (envelope.type === "object_share") {
711
+ await this.receiveObjectShare(envelope);
712
+ return;
713
+ }
714
+ if (envelope.type === "object_revoke") {
715
+ await this.receiveObjectRevoke(envelope);
716
+ return;
717
+ }
501
718
  throw new EdgeBookError("unsupported_envelope", `Unsupported envelope type: ${envelope.type}`);
502
719
  }
503
720
  async audit(action, peerAgentId, details) {
@@ -886,6 +1103,12 @@ function validateCard(card) {
886
1103
  }
887
1104
  }
888
1105
  async function loadCard(cardPathOrUrl) {
1106
+ if (cardPathOrUrl.startsWith("edgebook:invite:")) {
1107
+ const encoded = cardPathOrUrl.slice("edgebook:invite:".length);
1108
+ const card2 = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
1109
+ validateCard(card2);
1110
+ return card2;
1111
+ }
889
1112
  if (/^https?:\/\//.test(cardPathOrUrl)) {
890
1113
  const response = await fetch(cardPathOrUrl);
891
1114
  if (!response.ok) throw new EdgeBookError("card_fetch_failed", `Failed to fetch card: ${response.status}`);
@@ -980,6 +1203,14 @@ function sendHtml(res, value) {
980
1203
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
981
1204
  res.end(value);
982
1205
  }
1206
+ function sendBinary(res, status, mime, filename, body) {
1207
+ res.writeHead(status, {
1208
+ "content-type": mime || "application/octet-stream",
1209
+ "content-disposition": `inline; filename="${filename.replace(/[^\w.\- ]/g, "_")}"`,
1210
+ "content-length": String(body.length)
1211
+ });
1212
+ res.end(body);
1213
+ }
983
1214
  function sendError(res, error) {
984
1215
  const status = error instanceof EdgeBookError && error.code === "unauthorized" ? 401 : error instanceof EdgeBookError && error.code === "csrf_required" ? 403 : error instanceof EdgeBookError ? 400 : 500;
985
1216
  sendJson(res, status, {
@@ -1068,6 +1299,31 @@ async function handleOwnerApi(req, res, url, adapters) {
1068
1299
  sendJson(res, 200, { contacts: await store.contacts(), mutes: await store.contactMutes() });
1069
1300
  return true;
1070
1301
  }
1302
+ if (req.method === "GET" && url.pathname === "/api/shared-objects") {
1303
+ const objects = await store.sharedObjectsFor();
1304
+ sendJson(res, 200, { objects: objects.map((object) => ({ ...object, grant_scope: "object.read" })) });
1305
+ return true;
1306
+ }
1307
+ if (req.method === "GET" && url.pathname === "/api/invite") {
1308
+ const card = await store.writeCard();
1309
+ const identity = await store.identity();
1310
+ const invite_url = `edgebook:invite:${Buffer.from(JSON.stringify(card), "utf8").toString("base64url")}`;
1311
+ sendJson(res, 200, { agent_id: identity.agent_id, display_name: identity.display_name, card_url: card.card_url, card, invite_url });
1312
+ return true;
1313
+ }
1314
+ const attachmentMatch = /^\/api\/shared-objects\/([^/]+)\/attachment$/.exec(url.pathname);
1315
+ if (req.method === "GET" && attachmentMatch) {
1316
+ const objectId = decodeURIComponent(attachmentMatch[1]);
1317
+ const me = (await store.identity()).agent_id;
1318
+ const object = await store.readObject(objectId, me);
1319
+ if (!object.attachment) {
1320
+ sendJson(res, 404, { ok: false, code: "no_attachment", error: "Object has no attachment" });
1321
+ return true;
1322
+ }
1323
+ const bytes = await store.readAttachmentBytes(object.object_id);
1324
+ sendBinary(res, 200, object.attachment.mime, object.attachment.filename, bytes);
1325
+ return true;
1326
+ }
1071
1327
  const contactMuteMatch = /^\/api\/contacts\/([^/]+)\/mute$/.exec(url.pathname);
1072
1328
  if (req.method === "POST" && contactMuteMatch) {
1073
1329
  const body = await readJsonBody(req);
@@ -2360,6 +2616,7 @@ async function pullRelayEnvelopes(relayBaseUrl, recipientAgentId) {
2360
2616
 
2361
2617
  // src/dialout.ts
2362
2618
  var KEY_FILE = "host-dialout-key.json";
2619
+ var DEFAULT_DIALOUT_HOST = "wss://edge-book-host.fly.dev/agent/ws";
2363
2620
  var DEFAULT_PAIR_TTL_MS = 5 * 60 * 1e3;
2364
2621
  var DEFAULT_HEARTBEAT_MS = 25e3;
2365
2622
  var DEFAULT_BACKOFF_MS = 1e3;
@@ -2500,6 +2757,7 @@ var EdgeBookDialoutClient = class {
2500
2757
  currentBackoff;
2501
2758
  opened;
2502
2759
  pendingSessionRevokes = /* @__PURE__ */ new Map();
2760
+ pendingMailboxSends = /* @__PURE__ */ new Map();
2503
2761
  constructor(options) {
2504
2762
  this.options = {
2505
2763
  heartbeatMs: options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,
@@ -2507,6 +2765,9 @@ var EdgeBookDialoutClient = class {
2507
2765
  backoffMs: options.backoffMs ?? DEFAULT_BACKOFF_MS,
2508
2766
  socketFactory: options.socketFactory ?? socketFactory,
2509
2767
  openLocalApi: options.openLocalApi ?? true,
2768
+ onStandDown: options.onStandDown ?? (() => void 0),
2769
+ autoApplyEnvelopes: options.autoApplyEnvelopes ?? true,
2770
+ onEnvelope: options.onEnvelope ?? (() => void 0),
2510
2771
  host: options.host,
2511
2772
  home: options.home
2512
2773
  };
@@ -2547,7 +2808,75 @@ var EdgeBookDialoutClient = class {
2547
2808
  this.send(frame);
2548
2809
  return { frame, ack: await ackPromise };
2549
2810
  }
2811
+ // ── Mailbox transport (Contract 1 / ea-claude-065) ─────────────────────────
2812
+ // Low-level: hand an opaque blob to the host for delivery to `to` (a peer DID
2813
+ // or channel_id). Resolves with the host-assigned message id once enqueued.
2814
+ async sendMailbox(to, blob, timeoutMs = 5e3) {
2815
+ const request_id = crypto2.randomUUID();
2816
+ const blob_b64 = Buffer.from(blob).toString("base64");
2817
+ const ack = new Promise((resolve, reject) => {
2818
+ const timer = setTimeout(() => {
2819
+ this.pendingMailboxSends.delete(request_id);
2820
+ reject(new EdgeBookError("mailbox_send_timeout", "Timed out waiting for mailbox_send_ok"));
2821
+ }, timeoutMs);
2822
+ this.pendingMailboxSends.set(request_id, { resolve, reject, timer });
2823
+ });
2824
+ this.send({ type: "mailbox_send", request_id, to, blob_b64 });
2825
+ return ack;
2826
+ }
2827
+ // High-level: deliver a signed envelope to its recipient (envelope.to_agent_id;
2828
+ // the host resolves the DID to a channel). Used to route friend requests,
2829
+ // object shares, and revokes through the mailbox instead of a manual file hop.
2830
+ async sendEnvelope(envelope) {
2831
+ return this.sendMailbox(envelope.to_agent_id, Buffer.from(JSON.stringify(envelope), "utf8"));
2832
+ }
2833
+ async handleMailboxDeliver(frame) {
2834
+ let envelope;
2835
+ let applied = false;
2836
+ let error;
2837
+ try {
2838
+ envelope = JSON.parse(Buffer.from(frame.blob_b64, "base64").toString("utf8"));
2839
+ if (this.mailboxQueue) {
2840
+ this.pushMailbox({ id: frame.id, to: envelope.to_agent_id, from: frame.from, blob: frame.blob_b64, ts: frame.ts });
2841
+ } else if (this.options.autoApplyEnvelopes) {
2842
+ await this.store.receiveEnvelope(envelope);
2843
+ applied = true;
2844
+ }
2845
+ } catch (e) {
2846
+ error = e instanceof Error ? e.message : String(e);
2847
+ }
2848
+ if (!this.mailboxQueue) this.send({ type: "mailbox_ack", id: frame.id });
2849
+ if (envelope) await this.options.onEnvelope?.(envelope, { applied, error });
2850
+ }
2851
+ // Contract-1 Transport facade. Enabling it switches deliver handling to manual
2852
+ // (queue) mode so the consumer drives apply + ack via receive()/ack().
2853
+ mailboxQueue;
2854
+ mailboxWaiters = [];
2855
+ pushMailbox(m) {
2856
+ this.mailboxQueue?.push(m);
2857
+ this.mailboxWaiters.splice(0).forEach((w) => w());
2858
+ }
2859
+ transport() {
2860
+ this.mailboxQueue ||= [];
2861
+ const self = this;
2862
+ return {
2863
+ send: (recipient, bytes) => self.sendMailbox(recipient, bytes),
2864
+ ack: async (id) => {
2865
+ self.send({ type: "mailbox_ack", id });
2866
+ },
2867
+ receive: async function* () {
2868
+ for (; ; ) {
2869
+ while (self.mailboxQueue && self.mailboxQueue.length === 0) {
2870
+ await new Promise((r) => self.mailboxWaiters.push(r));
2871
+ }
2872
+ const next = self.mailboxQueue?.shift();
2873
+ if (next) yield next;
2874
+ }
2875
+ }
2876
+ };
2877
+ }
2550
2878
  async connect() {
2879
+ if (this.stopped) return;
2551
2880
  if (this.options.openLocalApi && !this.localApi) this.localApi = await openLocalApi(this.store);
2552
2881
  const socket = this.options.socketFactory(this.options.host);
2553
2882
  this.socket = socket;
@@ -2581,6 +2910,7 @@ var EdgeBookDialoutClient = class {
2581
2910
  await opened;
2582
2911
  }
2583
2912
  scheduleReconnect() {
2913
+ if (this.stopped) return;
2584
2914
  const delay = this.currentBackoff;
2585
2915
  this.currentBackoff = Math.min(MAX_BACKOFF_MS, Math.round(this.currentBackoff * 1.7));
2586
2916
  this.reconnectTimer = setTimeout(() => {
@@ -2608,6 +2938,10 @@ var EdgeBookDialoutClient = class {
2608
2938
  this.send({ type: "pong" });
2609
2939
  return;
2610
2940
  }
2941
+ if (frame.type === "stand_down" || frame.type === "dialout_idle") {
2942
+ await this.standDown(frame);
2943
+ return;
2944
+ }
2611
2945
  if (frame.type === "pair_register_ok" || frame.type === "pair_register_err") return;
2612
2946
  if (frame.type === "sessions_revoke_ok") {
2613
2947
  const ack = frame;
@@ -2619,11 +2953,45 @@ var EdgeBookDialoutClient = class {
2619
2953
  }
2620
2954
  return;
2621
2955
  }
2622
- if (frame.type === "error") return;
2956
+ const frameType = frame.type;
2957
+ if (frameType === "mailbox_send_ok") {
2958
+ const ack = frame;
2959
+ const pending = this.pendingMailboxSends.get(ack.request_id || "");
2960
+ if (pending) {
2961
+ clearTimeout(pending.timer);
2962
+ this.pendingMailboxSends.delete(ack.request_id || "");
2963
+ pending.resolve({ id: ack.id || "" });
2964
+ }
2965
+ return;
2966
+ }
2967
+ if (frameType === "mailbox_send_err") {
2968
+ const err = frame;
2969
+ const pending = this.pendingMailboxSends.get(err.request_id || "");
2970
+ if (pending) {
2971
+ clearTimeout(pending.timer);
2972
+ this.pendingMailboxSends.delete(err.request_id || "");
2973
+ pending.reject(new EdgeBookError("mailbox_send_failed", err.error || "mailbox_send rejected"));
2974
+ }
2975
+ return;
2976
+ }
2977
+ if (frameType === "mailbox_deliver") {
2978
+ await this.handleMailboxDeliver(frame);
2979
+ return;
2980
+ }
2981
+ if (frameType === "error") return;
2623
2982
  if (frame.type !== "host.api.request" && frame.type !== "api_request") return;
2624
2983
  const response = await this.handleApiRequest(frame);
2625
2984
  this.send(response);
2626
2985
  }
2986
+ async standDown(frame) {
2987
+ this.stopped = true;
2988
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
2989
+ if (this.heartbeat) clearInterval(this.heartbeat);
2990
+ this.socket?.close();
2991
+ if (this.localApi) await closeServer(this.localApi.server);
2992
+ this.localApi = void 0;
2993
+ await this.options.onStandDown?.(frame);
2994
+ }
2627
2995
  async handleApiRequest(frame) {
2628
2996
  try {
2629
2997
  if (!this.localApi) {
@@ -2675,6 +3043,16 @@ async function sendPairRegistration(options) {
2675
3043
  await client.stop();
2676
3044
  return registration;
2677
3045
  }
3046
+ async function deliverEnvelopeViaMailbox(options) {
3047
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
3048
+ await client.start();
3049
+ await new Promise((resolve) => setTimeout(resolve, 0));
3050
+ try {
3051
+ return await client.sendEnvelope(options.envelope);
3052
+ } finally {
3053
+ await client.stop();
3054
+ }
3055
+ }
2678
3056
  async function sendSessionsRevoke(options) {
2679
3057
  const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
2680
3058
  await client.start();
@@ -2692,15 +3070,16 @@ Usage:
2692
3070
  edge-book init [--home <dir>] [--handle <handle>] [--name <display>]
2693
3071
 
2694
3072
  Hosted reader:
2695
- edge-book dialout --host <ws-url> [--home <dir>]
2696
- edge-book pair --host <ws-url> [--ttl-ms <ms>] [--home <dir>]
2697
- edge-book sessions revoke --host <ws-url> [--home <dir>]
3073
+ edge-book dialout [--host <ws-url>] [--home <dir>]
3074
+ edge-book pair [--host <ws-url>] [--ttl-ms <ms>] [--home <dir>]
3075
+ edge-book sessions revoke [--host <ws-url>] [--home <dir>]
2698
3076
 
2699
3077
  Local agent:
2700
3078
  edge-book doctor [--home <dir>]
2701
3079
  edge-book card show [--home <dir>]
2702
3080
  edge-book card export --path <file> [--home <dir>]
2703
- edge-book friend request <card-path-or-url> [--deliver] [--home <dir>]
3081
+ edge-book card invite [--home <dir>] # "Add me" link (edgebook:invite:...)
3082
+ edge-book friend request <card-path-or-url-or-invite> [--deliver] [--home <dir>]
2704
3083
  edge-book friend receive <envelope-json-path> [--home <dir>]
2705
3084
  edge-book friend accept <peer-agent-id> [--deliver] [--home <dir>]
2706
3085
  edge-book friend apply-response <envelope-json-path> [--home <dir>]
@@ -2710,6 +3089,11 @@ Local agent:
2710
3089
  edge-book contacts refresh <card-path-or-url> [--home <dir>]
2711
3090
  edge-book message send <peer-agent-id> --body <text> [--deliver] [--home <dir>]
2712
3091
  edge-book message receive <envelope-json-path> [--home <dir>]
3092
+ edge-book object create --title <t> --body <b> [--file <path>] [--mime <type>] [--home <dir>]
3093
+ edge-book object share <peer-agent-id> <object-id> [--deliver] [--host <ws-url>] [--home <dir>]
3094
+ edge-book object revoke <peer-agent-id> <object-id> [--deliver] [--host <ws-url>] [--home <dir>]
3095
+ edge-book object list [--home <dir>]
3096
+ edge-book object read <object-id> [--home <dir>]
2713
3097
  edge-book inbox list [--home <dir>]
2714
3098
  edge-book inbox pull --relay <url> [--home <dir>]
2715
3099
  edge-book serve --host <host> --port <port> [--home <dir>]
@@ -2726,6 +3110,9 @@ function takeFlag(args, name) {
2726
3110
  function parseHome(args, ctx) {
2727
3111
  return takeFlag(args, "--home") || ctx.home;
2728
3112
  }
3113
+ function parseHost(args, ctx) {
3114
+ return takeFlag(args, "--host") || ctx.defaultHost || process.env.EDGE_BOOK_HOST || DEFAULT_DIALOUT_HOST;
3115
+ }
2729
3116
  function requireArg(value, label) {
2730
3117
  if (!value) throw new EdgeBookError("missing_arg", `Missing ${label}`);
2731
3118
  return value;
@@ -2794,6 +3181,11 @@ async function handleCli(inputArgs, ctx = {}) {
2794
3181
  `, "utf8");
2795
3182
  return { text: `Exported Agent Card to ${path4.resolve(target)}`, json: card };
2796
3183
  }
3184
+ if (action === "invite") {
3185
+ const card = await store.writeCard();
3186
+ const inviteUrl = `edgebook:invite:${Buffer.from(JSON.stringify(card), "utf8").toString("base64url")}`;
3187
+ return { text: inviteUrl, json: { invite_url: inviteUrl, agent_id: card.agent_id } };
3188
+ }
2797
3189
  }
2798
3190
  if (command === "friend") {
2799
3191
  const action = args.shift();
@@ -2810,7 +3202,9 @@ async function handleCli(inputArgs, ctx = {}) {
2810
3202
  await postRelayEnvelope(relay, card.agent_id, envelope);
2811
3203
  return { text: `Queued friend_request via relay ${relay}`, json: envelope };
2812
3204
  }
2813
- throw new EdgeBookError("no_route", `No direct or relay endpoint for ${card.agent_id}`);
3205
+ const hostUrl = parseHost(args, ctx);
3206
+ const ack = await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope });
3207
+ return { text: `Delivered friend_request to ${card.agent_id} over the mailbox (host id ${ack.id})`, json: envelope };
2814
3208
  }
2815
3209
  return { text: JSON.stringify(envelope, null, 2), json: envelope };
2816
3210
  }
@@ -2823,7 +3217,16 @@ async function handleCli(inputArgs, ctx = {}) {
2823
3217
  const deliver = takeBoolFlag(args, "--deliver");
2824
3218
  const peer = requireArg(args.shift(), "peer-agent-id");
2825
3219
  const envelope = await store.acceptFriend(peer);
2826
- if (deliver) return { text: await deliverToPeer(store, envelope, peer), json: envelope };
3220
+ if (deliver) {
3221
+ try {
3222
+ return { text: await deliverToPeer(store, envelope, peer), json: envelope };
3223
+ } catch (error) {
3224
+ if (!(error instanceof EdgeBookError) || error.code !== "no_route") throw error;
3225
+ const hostUrl = parseHost(args, ctx);
3226
+ const ack = await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope });
3227
+ return { text: `Delivered friend_response to ${peer} over the mailbox (host id ${ack.id})`, json: envelope };
3228
+ }
3229
+ }
2827
3230
  return { text: JSON.stringify(envelope, null, 2), json: envelope };
2828
3231
  }
2829
3232
  if (action === "apply-response") {
@@ -2842,6 +3245,61 @@ async function handleCli(inputArgs, ctx = {}) {
2842
3245
  return { text: `Blocked ${peer}` };
2843
3246
  }
2844
3247
  }
3248
+ if (command === "object") {
3249
+ const action = args.shift();
3250
+ if (action === "create") {
3251
+ const title = requireArg(takeFlag(args, "--title"), "--title");
3252
+ const body = requireArg(takeFlag(args, "--body"), "--body");
3253
+ const file = takeFlag(args, "--file");
3254
+ let attachment;
3255
+ if (file) {
3256
+ const bytes = await fs4.readFile(path4.resolve(file));
3257
+ attachment = { filename: path4.basename(file), mime: takeFlag(args, "--mime") || "application/octet-stream", bytes };
3258
+ }
3259
+ const object = await store.createObject({ title, body, attachment });
3260
+ return { text: `Created object ${object.object_id}`, json: object };
3261
+ }
3262
+ if (action === "share") {
3263
+ const deliver = takeBoolFlag(args, "--deliver");
3264
+ const hostUrl = parseHost(args, ctx);
3265
+ const peer = requireArg(args.shift(), "peer-agent-id");
3266
+ const objectId = requireArg(args.shift(), "object-id");
3267
+ const envelope = await store.shareObjectEnvelope(peer, objectId);
3268
+ if (deliver) {
3269
+ const ack = await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope });
3270
+ return { text: `Shared object ${objectId} to ${peer} over the mailbox (host id ${ack.id})`, json: envelope };
3271
+ }
3272
+ return { text: JSON.stringify(envelope, null, 2), json: envelope };
3273
+ }
3274
+ if (action === "revoke") {
3275
+ const deliver = takeBoolFlag(args, "--deliver");
3276
+ const hostUrl = parseHost(args, ctx);
3277
+ const peer = requireArg(args.shift(), "peer-agent-id");
3278
+ const objectId = requireArg(args.shift(), "object-id");
3279
+ const envelope = await store.revokeObjectEnvelope(peer, objectId);
3280
+ if (deliver) {
3281
+ const ack = await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope });
3282
+ return { text: `Revoked object ${objectId} for ${peer}; forwarded over the mailbox (host id ${ack.id})`, json: envelope };
3283
+ }
3284
+ return { text: JSON.stringify(envelope, null, 2), json: envelope };
3285
+ }
3286
+ if (action === "receive") {
3287
+ const source = requireArg(args.shift(), "envelope-json-path");
3288
+ await store.receiveEnvelope(await readEnvelope(source));
3289
+ return { text: `Applied object envelope from ${path4.resolve(source)}` };
3290
+ }
3291
+ if (action === "list") {
3292
+ const objects = await store.sharedObjectsFor();
3293
+ return { text: JSON.stringify(objects, null, 2), json: objects };
3294
+ }
3295
+ if (action === "read") {
3296
+ const objectId = requireArg(args.shift(), "object-id");
3297
+ const me = (await store.identity()).agent_id;
3298
+ const object = await store.readObject(objectId, me);
3299
+ return { text: JSON.stringify(object, null, 2), json: object };
3300
+ }
3301
+ throw new EdgeBookError("unknown_action", `Unknown object action: ${action}`);
3302
+ }
2845
3303
  if (command === "contacts") {
2846
3304
  const action = args.shift() || "list";
2847
3305
  if (action === "list") {
@@ -2893,14 +3351,14 @@ async function handleCli(inputArgs, ctx = {}) {
2893
3351
  await new Promise(() => void 0);
2894
3352
  }
2895
3353
  if (command === "dialout") {
2896
- const hostUrl = requireArg(takeFlag(args, "--host"), "--host");
3354
+ const hostUrl = parseHost(args, ctx);
2897
3355
  const client = new EdgeBookDialoutClient({ home, host: hostUrl, socketFactory: ctx.socketFactory });
2898
3356
  await client.start();
2899
3357
  console.log(`Edge Book dial-out connected to ${hostUrl}`);
2900
3358
  await new Promise(() => void 0);
2901
3359
  }
2902
3360
  if (command === "pair") {
2903
- const hostUrl = requireArg(takeFlag(args, "--host"), "--host");
3361
+ const hostUrl = parseHost(args, ctx);
2904
3362
  const ttlMs = Number(takeFlag(args, "--ttl-ms") || `${5 * 60 * 1e3}`);
2905
3363
  if (!ctx.textOnly) {
2906
3364
  const client = new EdgeBookDialoutClient({ home, host: hostUrl, socketFactory: ctx.socketFactory, openLocalApi: false });
@@ -2918,7 +3376,7 @@ Expires in: ${registration.frame.ttl_ms}ms`, json: registration };
2918
3376
  if (command === "sessions") {
2919
3377
  const action = args.shift();
2920
3378
  if (action === "revoke") {
2921
- const hostUrl = requireArg(takeFlag(args, "--host"), "--host");
3379
+ const hostUrl = parseHost(args, ctx);
2922
3380
  const frame = await sendSessionsRevoke({ home, host: hostUrl, socketFactory: ctx.socketFactory });
2923
3381
  const channel = frame.channel_id || "unknown-channel";
2924
3382
  return { text: `Received sessions_revoke_ok for request ${frame.request_id} on ${channel}`, json: frame };
@@ -2960,6 +3418,8 @@ if (isCliEntrypoint()) {
2960
3418
  });
2961
3419
  }
2962
3420
  export {
3421
+ DEFAULT_DIALOUT_HOST,
3422
+ EdgeBookDialoutClient,
2963
3423
  handleCli,
2964
3424
  runCli
2965
3425
  };