edge-book 0.1.3 → 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.
Files changed (2) hide show
  1. package/dist/edge-book.js +442 -4
  2. package/package.json +2 -1
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);
@@ -2501,6 +2757,7 @@ var EdgeBookDialoutClient = class {
2501
2757
  currentBackoff;
2502
2758
  opened;
2503
2759
  pendingSessionRevokes = /* @__PURE__ */ new Map();
2760
+ pendingMailboxSends = /* @__PURE__ */ new Map();
2504
2761
  constructor(options) {
2505
2762
  this.options = {
2506
2763
  heartbeatMs: options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,
@@ -2509,6 +2766,8 @@ var EdgeBookDialoutClient = class {
2509
2766
  socketFactory: options.socketFactory ?? socketFactory,
2510
2767
  openLocalApi: options.openLocalApi ?? true,
2511
2768
  onStandDown: options.onStandDown ?? (() => void 0),
2769
+ autoApplyEnvelopes: options.autoApplyEnvelopes ?? true,
2770
+ onEnvelope: options.onEnvelope ?? (() => void 0),
2512
2771
  host: options.host,
2513
2772
  home: options.home
2514
2773
  };
@@ -2549,6 +2808,73 @@ var EdgeBookDialoutClient = class {
2549
2808
  this.send(frame);
2550
2809
  return { frame, ack: await ackPromise };
2551
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
+ }
2552
2878
  async connect() {
2553
2879
  if (this.stopped) return;
2554
2880
  if (this.options.openLocalApi && !this.localApi) this.localApi = await openLocalApi(this.store);
@@ -2627,7 +2953,32 @@ var EdgeBookDialoutClient = class {
2627
2953
  }
2628
2954
  return;
2629
2955
  }
2630
- 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;
2631
2982
  if (frame.type !== "host.api.request" && frame.type !== "api_request") return;
2632
2983
  const response = await this.handleApiRequest(frame);
2633
2984
  this.send(response);
@@ -2692,6 +3043,16 @@ async function sendPairRegistration(options) {
2692
3043
  await client.stop();
2693
3044
  return registration;
2694
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
+ }
2695
3056
  async function sendSessionsRevoke(options) {
2696
3057
  const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
2697
3058
  await client.start();
@@ -2717,7 +3078,8 @@ Local agent:
2717
3078
  edge-book doctor [--home <dir>]
2718
3079
  edge-book card show [--home <dir>]
2719
3080
  edge-book card export --path <file> [--home <dir>]
2720
- 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>]
2721
3083
  edge-book friend receive <envelope-json-path> [--home <dir>]
2722
3084
  edge-book friend accept <peer-agent-id> [--deliver] [--home <dir>]
2723
3085
  edge-book friend apply-response <envelope-json-path> [--home <dir>]
@@ -2727,6 +3089,11 @@ Local agent:
2727
3089
  edge-book contacts refresh <card-path-or-url> [--home <dir>]
2728
3090
  edge-book message send <peer-agent-id> --body <text> [--deliver] [--home <dir>]
2729
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>]
2730
3097
  edge-book inbox list [--home <dir>]
2731
3098
  edge-book inbox pull --relay <url> [--home <dir>]
2732
3099
  edge-book serve --host <host> --port <port> [--home <dir>]
@@ -2814,6 +3181,11 @@ async function handleCli(inputArgs, ctx = {}) {
2814
3181
  `, "utf8");
2815
3182
  return { text: `Exported Agent Card to ${path4.resolve(target)}`, json: card };
2816
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
+ }
2817
3189
  }
2818
3190
  if (command === "friend") {
2819
3191
  const action = args.shift();
@@ -2830,7 +3202,9 @@ async function handleCli(inputArgs, ctx = {}) {
2830
3202
  await postRelayEnvelope(relay, card.agent_id, envelope);
2831
3203
  return { text: `Queued friend_request via relay ${relay}`, json: envelope };
2832
3204
  }
2833
- 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 };
2834
3208
  }
2835
3209
  return { text: JSON.stringify(envelope, null, 2), json: envelope };
2836
3210
  }
@@ -2843,7 +3217,16 @@ async function handleCli(inputArgs, ctx = {}) {
2843
3217
  const deliver = takeBoolFlag(args, "--deliver");
2844
3218
  const peer = requireArg(args.shift(), "peer-agent-id");
2845
3219
  const envelope = await store.acceptFriend(peer);
2846
- 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
+ }
2847
3230
  return { text: JSON.stringify(envelope, null, 2), json: envelope };
2848
3231
  }
2849
3232
  if (action === "apply-response") {
@@ -2862,6 +3245,61 @@ async function handleCli(inputArgs, ctx = {}) {
2862
3245
  return { text: `Blocked ${peer}` };
2863
3246
  }
2864
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
+ }
2865
3303
  if (command === "contacts") {
2866
3304
  const action = args.shift() || "list";
2867
3305
  if (action === "list") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edge-book",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Run your own Edge Book agent and connect it to the hosted reader.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,6 +11,7 @@
11
11
  "build": "tsup",
12
12
  "test": "node --test test/*.test.ts",
13
13
  "harness": "node bin/edge-book.js harness two-agent",
14
+ "harness:e2e": "node scripts/convergence-e2e.ts",
14
15
  "prepublishOnly": "npm run build"
15
16
  },
16
17
  "openclaw": {