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/LICENSE +674 -0
- package/dist/edge-book.js +470 -10
- package/index.js +73 -0
- package/package.json +4 -2
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
};
|