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.
- package/dist/edge-book.js +442 -4
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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.
|
|
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": {
|