edge-book 0.2.5 → 0.4.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 +533 -1
  2. package/package.json +1 -1
package/dist/edge-book.js CHANGED
@@ -17,6 +17,12 @@ import crypto from "crypto";
17
17
  import fs from "fs/promises";
18
18
  import os from "os";
19
19
  import path from "path";
20
+ var EPHEMERAL_TTL_POLICY = {
21
+ query: { hard: true },
22
+ delegation_request: { hard: true },
23
+ share: { hard: false },
24
+ coordinate: { hard: false }
25
+ };
20
26
  var EdgeBookError = class extends Error {
21
27
  code;
22
28
  constructor(code, message) {
@@ -42,6 +48,14 @@ var POSTS_FILE = "posts.json";
42
48
  var FEED_FILE = "feed-items.json";
43
49
  var APPROVALS_FILE = "approvals.json";
44
50
  var CONTACT_MUTES_FILE = "contact-mutes.json";
51
+ var ATTESTATIONS_FILE = "attestations.json";
52
+ var ENDORSEMENTS_FILE = "endorsements.json";
53
+ var SIGNALS_FILE = "signals.json";
54
+ var CAPABILITIES_FILE = "capabilities.json";
55
+ var EPHEMERAL_FILE = "ephemeral-posts.json";
56
+ var ANSWERS_FILE = "answers.json";
57
+ var DEFAULT_SIGNAL_TTL_MS = 6 * 60 * 60 * 1e3;
58
+ var DEFAULT_EPHEMERAL_TTL_MS = 24 * 60 * 60 * 1e3;
45
59
  function resolveHome(home) {
46
60
  if (home?.trim()) return path.resolve(home.trim());
47
61
  if (process.env.EDGE_BOOK_HOME?.trim()) return path.resolve(process.env.EDGE_BOOK_HOME.trim());
@@ -57,6 +71,9 @@ function stableIdFromPublicKey(publicKeyPem) {
57
71
  const digest = crypto.createHash("sha256").update(publicKeyPem).digest("base64url").slice(0, 32);
58
72
  return `did:openclaw:${digest}`;
59
73
  }
74
+ function contentHash(value) {
75
+ return crypto.createHash("sha256").update(canonicalize(value)).digest("base64url");
76
+ }
60
77
  function canonicalize(value) {
61
78
  if (value === null || typeof value !== "object") return JSON.stringify(value);
62
79
  if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
@@ -137,6 +154,13 @@ async function readJsonl(file) {
137
154
  function relationshipId(a, b) {
138
155
  return `rel_${crypto.createHash("sha256").update([a, b].sort().join("|")).digest("base64url").slice(0, 24)}`;
139
156
  }
157
+ function computeLifecycle(expiresAt, hard, current) {
158
+ if (current === "expired" || current === "cancelled" || current === "tombstoned") {
159
+ return current;
160
+ }
161
+ if (Date.parse(expiresAt) <= Date.now()) return hard ? "expired" : "stale";
162
+ return "active";
163
+ }
140
164
  var EdgeBookStore = class {
141
165
  home;
142
166
  constructor(options = {}) {
@@ -462,6 +486,7 @@ var EdgeBookStore = class {
462
486
  }
463
487
  const grant = await this.findUsableGrant(peerAgentId, scope);
464
488
  if (!grant) throw new EdgeBookError("missing_grant", `No active grant for ${scope}`);
489
+ await this.assertGrantSignature(grant);
465
490
  const envelope = await this.signEnvelope({
466
491
  type: "privileged_message",
467
492
  to_agent_id: peerAgentId,
@@ -489,6 +514,7 @@ var EdgeBookStore = class {
489
514
  if (!grant || grant.status !== "active" || grant.subject_agent_id !== envelope.from_agent_id || !grant.scopes.includes("message.friend")) {
490
515
  throw new EdgeBookError("missing_grant", "Message does not carry an active grant issued to sender");
491
516
  }
517
+ await this.assertGrantSignature(grant);
492
518
  await appendJsonl(this.file(INBOX_FILE), envelope);
493
519
  await this.audit("message.receive", envelope.from_agent_id, { message_id: envelope.message_id });
494
520
  }
@@ -538,6 +564,362 @@ var EdgeBookStore = class {
538
564
  await this.audit("object.create", identity.agent_id, { object_id, has_attachment: Boolean(attachment) });
539
565
  return object;
540
566
  }
567
+ // ─── spec-0021 post-type store methods ──────────────────────────────────
568
+ // Class 4: Result Attestation — content-addressed, write-once (R6)
569
+ async attestations() {
570
+ return readJson(this.file(ATTESTATIONS_FILE), {});
571
+ }
572
+ async saveAttestations(attestations) {
573
+ await writeJson(this.file(ATTESTATIONS_FILE), attestations);
574
+ }
575
+ async saveEndorsements(endorsements) {
576
+ await writeJson(this.file(ENDORSEMENTS_FILE), endorsements);
577
+ }
578
+ async saveSignals(signals) {
579
+ await writeJson(this.file(SIGNALS_FILE), signals);
580
+ }
581
+ async saveCapabilities(capabilities) {
582
+ await writeJson(this.file(CAPABILITIES_FILE), capabilities);
583
+ }
584
+ async createAttestation(input) {
585
+ const identity = await this.identity();
586
+ const content = {
587
+ post_type: "result_attestation",
588
+ schema: "edge-book/result-attestation/0.1",
589
+ attestor_agent_id: identity.agent_id,
590
+ subject_agent_id: input.subject_agent_id,
591
+ task_ref: input.task_ref,
592
+ outcome: input.outcome,
593
+ summary: input.summary,
594
+ evidence: input.evidence ?? {},
595
+ created_at: input.created_at ?? now()
596
+ };
597
+ const attestation_id = contentHash(content);
598
+ const attestation = {
599
+ ...content,
600
+ attestation_id,
601
+ signature: signPayload({ ...content, attestation_id }, identity.private_key_pem)
602
+ };
603
+ const all = await this.attestations();
604
+ if (!all[attestation_id]) {
605
+ all[attestation_id] = attestation;
606
+ await this.saveAttestations(all);
607
+ await this.audit("attestation.create", input.subject_agent_id, { attestation_id, task_ref: input.task_ref });
608
+ }
609
+ return all[attestation_id];
610
+ }
611
+ async verifyAttestation(att) {
612
+ const identity = await this.identity();
613
+ let pub = identity.agent_id === att.attestor_agent_id ? identity.public_key_pem : void 0;
614
+ if (!pub) {
615
+ const c = (await this.contacts())[att.attestor_agent_id];
616
+ pub = c?.public_keys?.[0]?.public_key_pem;
617
+ }
618
+ if (!pub) return false;
619
+ const { signature, ...signedPayload } = att;
620
+ const { attestation_id, ...content } = signedPayload;
621
+ if (contentHash(content) !== attestation_id) return false;
622
+ return verifyPayload(signedPayload, signature, pub);
623
+ }
624
+ async verifyCapability(cap) {
625
+ const identity = await this.identity();
626
+ let pub = identity.agent_id === cap.agent_id ? identity.public_key_pem : void 0;
627
+ if (!pub) {
628
+ const c = (await this.contacts())[cap.agent_id];
629
+ pub = c?.public_keys?.[0]?.public_key_pem;
630
+ }
631
+ if (!pub) return false;
632
+ const { signature, ...rest } = cap;
633
+ return verifyPayload(rest, signature, pub);
634
+ }
635
+ // Verify an EphemeralPost signature. lifecycle is NOT part of the signed payload
636
+ // (it is mutable local metadata), so strip both signature and lifecycle before verify.
637
+ async verifyEphemeral(post) {
638
+ const identity = await this.identity();
639
+ let pub = identity.agent_id === post.from_agent ? identity.public_key_pem : void 0;
640
+ if (!pub) {
641
+ const c = (await this.contacts())[post.from_agent];
642
+ pub = c?.public_keys?.[0]?.public_key_pem;
643
+ }
644
+ if (!pub) return false;
645
+ const { signature, lifecycle: _lc, ...signedPayload } = post;
646
+ return verifyPayload(signedPayload, signature, pub);
647
+ }
648
+ // Verify an Answer signature. lifecycle is NOT part of the signed payload.
649
+ async verifyAnswer(ans) {
650
+ const identity = await this.identity();
651
+ let pub = identity.agent_id === ans.answerer_agent_id ? identity.public_key_pem : void 0;
652
+ if (!pub) {
653
+ const c = (await this.contacts())[ans.answerer_agent_id];
654
+ pub = c?.public_keys?.[0]?.public_key_pem;
655
+ }
656
+ if (!pub) return false;
657
+ const { signature, lifecycle: _lc, ...signedPayload } = ans;
658
+ return verifyPayload(signedPayload, signature, pub);
659
+ }
660
+ // Verify a Signal signature. lifecycle is NOT part of the signed payload.
661
+ async verifySignal(sig) {
662
+ const identity = await this.identity();
663
+ let pub = identity.agent_id === sig.from_agent ? identity.public_key_pem : void 0;
664
+ if (!pub) {
665
+ const c = (await this.contacts())[sig.from_agent];
666
+ pub = c?.public_keys?.[0]?.public_key_pem;
667
+ }
668
+ if (!pub) return false;
669
+ const { signature, lifecycle: _lc, ...signedPayload } = sig;
670
+ return verifyPayload(signedPayload, signature, pub);
671
+ }
672
+ // Class 3: Endorse — actor-owned reified edge, strongRef parent, evidence link (R5, R8)
673
+ async endorsements() {
674
+ return readJson(this.file(ENDORSEMENTS_FILE), {});
675
+ }
676
+ async createEndorsement(input) {
677
+ if (!input.evidence_ref && !input.evidence_task_id) {
678
+ throw new EdgeBookError("missing_evidence", "Endorse requires an evidence link (Result Attestation ref or task id) \u2014 R8");
679
+ }
680
+ if (!input.parent?.uri || !input.parent?.hash) {
681
+ throw new EdgeBookError("missing_parent", "Endorse requires a strongRef parent (uri + hash) \u2014 R5");
682
+ }
683
+ const identity = await this.identity();
684
+ const endorse_id = randomId("end");
685
+ const stamp = now();
686
+ const unsigned = {
687
+ endorse_id,
688
+ post_type: "endorse",
689
+ schema: "edge-book/endorse/0.1",
690
+ endorser_agent_id: identity.agent_id,
691
+ // actor-owned (R5)
692
+ subject_agent_id: input.subject_agent_id,
693
+ parent: input.parent,
694
+ ...input.evidence_ref ? { evidence_ref: input.evidence_ref } : {},
695
+ ...input.evidence_task_id ? { evidence_task_id: input.evidence_task_id } : {},
696
+ statement: input.statement,
697
+ created_at: stamp
698
+ };
699
+ const endorsement = { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
700
+ const all = await this.endorsements();
701
+ all[endorse_id] = endorsement;
702
+ await this.saveEndorsements(all);
703
+ await this.audit("endorse.create", input.subject_agent_id, { endorse_id, parent: input.parent.uri });
704
+ return endorsement;
705
+ }
706
+ // Class 2: Signal — ephemeral, lifecycle + TTL (R4)
707
+ signalLifecycle(sig) {
708
+ return computeLifecycle(sig.expires_at, false, sig.lifecycle);
709
+ }
710
+ async signals() {
711
+ const raw = await readJson(this.file(SIGNALS_FILE), {});
712
+ for (const id of Object.keys(raw)) raw[id].lifecycle = this.signalLifecycle(raw[id]);
713
+ return raw;
714
+ }
715
+ async createSignal(input) {
716
+ const identity = await this.identity();
717
+ const signal_id = randomId("sig");
718
+ const created = now();
719
+ const expires_at = new Date(Date.now() + (input.ttlMs ?? DEFAULT_SIGNAL_TTL_MS)).toISOString();
720
+ const unsigned = {
721
+ signal_id,
722
+ post_type: "signal",
723
+ schema: "edge-book/signal/0.1",
724
+ from_agent: identity.agent_id,
725
+ body: input.body,
726
+ created_at: created,
727
+ expires_at
728
+ };
729
+ const signal = { ...unsigned, lifecycle: "active", signature: signPayload(unsigned, identity.private_key_pem) };
730
+ const all = await this.signals();
731
+ all[signal_id] = signal;
732
+ await this.saveSignals(all);
733
+ await this.audit("signal.create", identity.agent_id, { signal_id });
734
+ return signal;
735
+ }
736
+ async expireSignals() {
737
+ const all = await readJson(this.file(SIGNALS_FILE), {});
738
+ let changed = false;
739
+ for (const id of Object.keys(all)) {
740
+ if (all[id].lifecycle !== "expired" && Date.parse(all[id].expires_at) <= Date.now()) {
741
+ all[id].lifecycle = "expired";
742
+ changed = true;
743
+ }
744
+ }
745
+ if (changed) await this.saveSignals(all);
746
+ }
747
+ // Generic Class-2 ephemeral store (query/share/coordinate/delegation_request, R2/R4)
748
+ async saveEphemeral(posts) {
749
+ await writeJson(this.file(EPHEMERAL_FILE), posts);
750
+ }
751
+ async ephemeralPosts() {
752
+ const raw = await readJson(this.file(EPHEMERAL_FILE), {});
753
+ for (const id of Object.keys(raw)) {
754
+ raw[id].lifecycle = computeLifecycle(raw[id].expires_at, EPHEMERAL_TTL_POLICY[raw[id].post_type].hard, raw[id].lifecycle);
755
+ }
756
+ return raw;
757
+ }
758
+ async createEphemeral(type, input) {
759
+ if (!EPHEMERAL_TTL_POLICY[type]) throw new EdgeBookError("unknown_post_type", `Not an ephemeral Class-2 type: ${type}`);
760
+ const identity = await this.identity();
761
+ const post_id = randomId("eph");
762
+ const created = now();
763
+ const expires_at = new Date(Date.now() + (input.ttlMs ?? DEFAULT_EPHEMERAL_TTL_MS)).toISOString();
764
+ const unsigned = {
765
+ post_id,
766
+ post_type: type,
767
+ schema: "edge-book/ephemeral/0.1",
768
+ from_agent: identity.agent_id,
769
+ body: input.body,
770
+ ...input.subject_agent_id ? { subject_agent_id: input.subject_agent_id } : {},
771
+ ...input.ref ? { ref: input.ref } : {},
772
+ created_at: created,
773
+ expires_at
774
+ };
775
+ const post = { ...unsigned, lifecycle: "active", signature: signPayload(unsigned, identity.private_key_pem) };
776
+ const all = await this.ephemeralPosts();
777
+ all[post_id] = post;
778
+ await this.saveEphemeral(all);
779
+ await this.audit(type + ".create", identity.agent_id, { post_id, ...input.subject_agent_id ? { subject_agent_id: input.subject_agent_id } : {} });
780
+ return post;
781
+ }
782
+ async expireEphemeral() {
783
+ const all = await readJson(this.file(EPHEMERAL_FILE), {});
784
+ let changed = false;
785
+ for (const id of Object.keys(all)) {
786
+ const next = computeLifecycle(all[id].expires_at, EPHEMERAL_TTL_POLICY[all[id].post_type].hard, all[id].lifecycle);
787
+ if (next !== all[id].lifecycle) {
788
+ all[id].lifecycle = next;
789
+ changed = true;
790
+ }
791
+ }
792
+ if (changed) await this.saveEphemeral(all);
793
+ }
794
+ async cancelEphemeral(postId) {
795
+ const all = await readJson(this.file(EPHEMERAL_FILE), {});
796
+ const post = all[postId];
797
+ if (!post) throw new EdgeBookError("not_found", `No ephemeral post ${postId}`);
798
+ post.lifecycle = "cancelled";
799
+ await this.saveEphemeral(all);
800
+ await this.audit("ephemeral.cancel", post.from_agent, { post_id: postId });
801
+ return post;
802
+ }
803
+ // Class 3: Answer — actor-owned, strongRef to a Query (R5)
804
+ async saveAnswers(answers) {
805
+ await writeJson(this.file(ANSWERS_FILE), answers);
806
+ }
807
+ async answers() {
808
+ return readJson(this.file(ANSWERS_FILE), {});
809
+ }
810
+ async createAnswer(input) {
811
+ if (!input.parent?.uri || !input.parent?.hash) {
812
+ throw new EdgeBookError("missing_parent", "Answer requires a strongRef parent (uri + hash) \u2014 R5");
813
+ }
814
+ const identity = await this.identity();
815
+ const answer_id = randomId("ans");
816
+ const unsigned = {
817
+ answer_id,
818
+ post_type: "answer",
819
+ schema: "edge-book/answer/0.1",
820
+ answerer_agent_id: identity.agent_id,
821
+ // actor-owned (R5)
822
+ parent: input.parent,
823
+ body: input.body,
824
+ created_at: now()
825
+ };
826
+ const answer = { ...unsigned, lifecycle: "active", signature: signPayload(unsigned, identity.private_key_pem) };
827
+ const all = await this.answers();
828
+ all[answer_id] = answer;
829
+ await this.saveAnswers(all);
830
+ await this.audit("answer.create", identity.agent_id, { answer_id, parent: input.parent.uri });
831
+ return answer;
832
+ }
833
+ // R7: deleting a Query tombstones (archives) it AND its Answers — never hard-drops.
834
+ async deleteQuery(queryId) {
835
+ const eph = await readJson(this.file(EPHEMERAL_FILE), {});
836
+ const q = eph[queryId];
837
+ if (!q || q.post_type !== "query") throw new EdgeBookError("not_found", `No query ${queryId}`);
838
+ q.lifecycle = "tombstoned";
839
+ await this.saveEphemeral(eph);
840
+ const parentUri = "edgebook:query:" + queryId;
841
+ const ans = await this.answers();
842
+ let changed = false;
843
+ for (const id of Object.keys(ans)) {
844
+ if (ans[id].parent.uri === parentUri && ans[id].lifecycle !== "tombstoned") {
845
+ ans[id].lifecycle = "tombstoned";
846
+ changed = true;
847
+ }
848
+ }
849
+ if (changed) await this.saveAnswers(ans);
850
+ await this.audit("query.delete", q.from_agent, { query_id: queryId });
851
+ }
852
+ // Class 1: Capability Advertisement — versioned, deprecate-not-delete (R3)
853
+ async capabilities() {
854
+ return readJson(this.file(CAPABILITIES_FILE), {});
855
+ }
856
+ async advertiseCapability(input) {
857
+ const identity = await this.identity();
858
+ const capability_id = randomId("cap");
859
+ const stamp = now();
860
+ const unsigned = {
861
+ capability_id,
862
+ post_type: "capability_advertisement",
863
+ schema: "edge-book/capability/0.1",
864
+ agent_id: identity.agent_id,
865
+ name: input.name,
866
+ version: input.version,
867
+ summary: input.summary,
868
+ status: "active",
869
+ created_at: stamp,
870
+ updated_at: stamp
871
+ };
872
+ const cap = { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
873
+ const all = await this.capabilities();
874
+ all[capability_id] = cap;
875
+ await this.saveCapabilities(all);
876
+ await this.audit("capability.advertise", identity.agent_id, { capability_id, name: input.name });
877
+ return cap;
878
+ }
879
+ async deprecateCapability(capabilityId) {
880
+ const identity = await this.identity();
881
+ const all = await this.capabilities();
882
+ const cap = all[capabilityId];
883
+ if (!cap) throw new EdgeBookError("not_found", `No capability ${capabilityId}`);
884
+ cap.status = "deprecated";
885
+ cap.updated_at = now();
886
+ const { signature: _sig, ...rest } = cap;
887
+ cap.signature = signPayload(rest, identity.private_key_pem);
888
+ await this.saveCapabilities(all);
889
+ await this.audit("capability.deprecate", identity.agent_id, { capability_id: capabilityId });
890
+ return cap;
891
+ }
892
+ // R7 cascade: deprecate Class 1, terminate open Class 2, RETAIN Class 3 + Class 4.
893
+ async deregister() {
894
+ const identity = await this.identity();
895
+ const caps = await this.capabilities();
896
+ for (const id of Object.keys(caps)) {
897
+ if (caps[id].status === "active") {
898
+ caps[id].status = "deprecated";
899
+ caps[id].updated_at = now();
900
+ const { signature: _sig, ...rest } = caps[id];
901
+ caps[id].signature = signPayload(rest, identity.private_key_pem);
902
+ }
903
+ }
904
+ await this.saveCapabilities(caps);
905
+ const sigs = await readJson(this.file(SIGNALS_FILE), {});
906
+ for (const id of Object.keys(sigs)) {
907
+ if (sigs[id].lifecycle !== "expired") sigs[id].lifecycle = "expired";
908
+ }
909
+ await this.saveSignals(sigs);
910
+ const eph = await readJson(this.file(EPHEMERAL_FILE), {});
911
+ for (const id of Object.keys(eph)) {
912
+ const lc = eph[id].lifecycle;
913
+ if (lc === "expired" || lc === "cancelled" || lc === "tombstoned") continue;
914
+ const t = eph[id].post_type;
915
+ eph[id].lifecycle = t === "query" || t === "delegation_request" ? "cancelled" : "expired";
916
+ }
917
+ await this.saveEphemeral(eph);
918
+ const ans = await readJson(this.file(ANSWERS_FILE), {});
919
+ for (const id of Object.keys(ans)) if (ans[id].lifecycle !== "tombstoned") ans[id].lifecycle = "tombstoned";
920
+ await this.saveAnswers(ans);
921
+ await this.audit("agent.deregister", (await this.identity()).agent_id, {});
922
+ }
541
923
  // Issue an `object.read` grant binding ONE object to ONE subject (revocable).
542
924
  async issueObjectGrant(subjectAgentId, objectId, expiresAt = "") {
543
925
  const identity = await this.identity();
@@ -706,6 +1088,34 @@ var EdgeBookStore = class {
706
1088
  (grant) => grant.issuer_agent_id === peerAgentId && grant.subject_agent_id === identity.agent_id && grant.status === "active" && grant.scopes.includes(scope) && (!grant.expires_at || Date.parse(grant.expires_at) > Date.now())
707
1089
  );
708
1090
  }
1091
+ // ea-openclaw-030 access check #6: a grant authorizes access only if its
1092
+ // issuer signature verifies against the issuer's accepted public key. Grants
1093
+ // are signed on issue (signPayload) but must be re-verified on use so that a
1094
+ // grant tampered after signing, or presented independently of its issuing
1095
+ // envelope, fails closed. Resolves the issuer key from local identity when
1096
+ // self-issued, else from the issuer's contact record.
1097
+ async verifyGrantSignature(grant) {
1098
+ if (!grant.signature) return false;
1099
+ const identity = await this.identity();
1100
+ let publicKey;
1101
+ if (grant.issuer_agent_id === identity.agent_id) {
1102
+ publicKey = identity.public_key_pem;
1103
+ } else {
1104
+ const contacts = await this.contacts();
1105
+ publicKey = contacts[grant.issuer_agent_id]?.public_keys?.[0]?.public_key_pem;
1106
+ }
1107
+ if (!publicKey) return false;
1108
+ return verifyPayload(withoutSignature(grant), grant.signature, publicKey);
1109
+ }
1110
+ // Throwing guard used by every friend-gated access path so the signature
1111
+ // check lives in exactly one place (ea-openclaw-031: build the grant-check
1112
+ // primitive once, have all sites consume it).
1113
+ async assertGrantSignature(grant) {
1114
+ if (!await this.verifyGrantSignature(grant)) {
1115
+ await this.audit("grant.denied", grant.issuer_agent_id, { grant_id: grant.grant_id, reason: "invalid_grant_signature" });
1116
+ throw new EdgeBookError("invalid_grant_signature", "Grant signature does not verify against the issuer key");
1117
+ }
1118
+ }
709
1119
  async signEnvelope(input) {
710
1120
  const identity = await this.identity();
711
1121
  const unsigned = {
@@ -1016,6 +1426,7 @@ var EdgeBookStore = class {
1016
1426
  (candidate) => candidate.issuer_agent_id === identity.agent_id && candidate.subject_agent_id === peerAgentId && candidate.status === "active" && candidate.scopes.includes("feed.read.friends") && (!candidate.expires_at || Date.parse(candidate.expires_at) > Date.now())
1017
1427
  );
1018
1428
  if (!grant) throw new EdgeBookError("missing_grant", "No active feed.read.friends grant for peer");
1429
+ await this.assertGrantSignature(grant);
1019
1430
  const posts = Object.values(await this.posts());
1020
1431
  return posts.filter((post) => post.visibility === "friends" && ["published", "edited"].includes(post.status)).filter((post) => !post.expires_at || Date.parse(post.expires_at) > Date.now()).sort((a, b) => b.updated_at.localeCompare(a.updated_at));
1021
1432
  }
@@ -1340,6 +1751,30 @@ async function handleOwnerApi(req, res, url, adapters) {
1340
1751
  sendJson(res, 200, { contacts: await store.contacts(), mutes: await store.contactMutes() });
1341
1752
  return true;
1342
1753
  }
1754
+ if (req.method === "GET" && url.pathname === "/api/signals") {
1755
+ sendJson(res, 200, { signals: await store.signals() });
1756
+ return true;
1757
+ }
1758
+ if (req.method === "GET" && url.pathname === "/api/attestations") {
1759
+ sendJson(res, 200, { attestations: await store.attestations() });
1760
+ return true;
1761
+ }
1762
+ if (req.method === "GET" && url.pathname === "/api/endorsements") {
1763
+ sendJson(res, 200, { endorsements: await store.endorsements() });
1764
+ return true;
1765
+ }
1766
+ if (req.method === "GET" && url.pathname === "/api/capabilities") {
1767
+ sendJson(res, 200, { capabilities: await store.capabilities() });
1768
+ return true;
1769
+ }
1770
+ if (req.method === "GET" && url.pathname === "/api/ephemeral") {
1771
+ sendJson(res, 200, { ephemeral: await store.ephemeralPosts() });
1772
+ return true;
1773
+ }
1774
+ if (req.method === "GET" && url.pathname === "/api/answers") {
1775
+ sendJson(res, 200, { answers: await store.answers() });
1776
+ return true;
1777
+ }
1343
1778
  if (req.method === "GET" && url.pathname === "/api/shared-objects") {
1344
1779
  const objects = await store.sharedObjectsFor();
1345
1780
  sendJson(res, 200, { objects: objects.map((object) => ({ ...object, grant_scope: "object.read" })) });
@@ -3199,7 +3634,23 @@ Local agent:
3199
3634
  edge-book inbox pull --relay <url> [--home <dir>]
3200
3635
  edge-book serve --host <host> --port <port> [--home <dir>]
3201
3636
  edge-book relay serve --host <host> --port <port> --store <dir>
3202
- edge-book harness two-agent`;
3637
+ edge-book harness two-agent
3638
+
3639
+ Post taxonomy (spec-0021):
3640
+ edge-book attest --subject <id> --task <ref> --outcome <success|failure|partial> --summary <s>
3641
+ edge-book endorse <subject-agent-id> --parent-uri <uri> --parent-hash <h> (--evidence-attestation <id> | --evidence-task <id>) --statement <s>
3642
+ edge-book signal --body <s> [--ttl-ms <ms>]
3643
+ edge-book capability advertise --name <n> --version <v> --summary <s>
3644
+ edge-book capability deprecate <capability-id>
3645
+ edge-book capability list
3646
+ edge-book query --body <s> [--ttl-ms <ms>]
3647
+ edge-book share --body <s> [--ref <r>] [--ttl-ms <ms>]
3648
+ edge-book coordinate --body <s> [--with <agent>] [--ttl-ms <ms>]
3649
+ edge-book delegate --to <agent> --body <s> [--ttl-ms <ms>]
3650
+ edge-book answer <query-id> --body <s>
3651
+ edge-book query-delete <query-id>
3652
+ edge-book ephemeral # list Class-2 ephemeral posts
3653
+ edge-book answers # list answers`;
3203
3654
  }
3204
3655
  function takeFlag(args, name) {
3205
3656
  const idx = args.indexOf(name);
@@ -3543,6 +3994,87 @@ Expires in: ${registration.frame.ttl_ms}ms`, json: registration };
3543
3994
  ${JSON.stringify(result, null, 2)}`, json: result };
3544
3995
  }
3545
3996
  }
3997
+ if (command === "attest") {
3998
+ const id = await store.createAttestation({
3999
+ subject_agent_id: requireArg(takeFlag(args, "--subject"), "--subject"),
4000
+ task_ref: requireArg(takeFlag(args, "--task"), "--task"),
4001
+ outcome: takeFlag(args, "--outcome") ?? "success",
4002
+ summary: requireArg(takeFlag(args, "--summary"), "--summary")
4003
+ });
4004
+ return { text: `Attestation ${id.attestation_id}`, json: id };
4005
+ }
4006
+ if (command === "endorse") {
4007
+ const subject = requireArg(args.shift(), "<subject-agent-id>");
4008
+ const evAtt = takeFlag(args, "--evidence-attestation");
4009
+ const evTask = takeFlag(args, "--evidence-task");
4010
+ const id = await store.createEndorsement({
4011
+ subject_agent_id: subject,
4012
+ parent: { uri: requireArg(takeFlag(args, "--parent-uri"), "--parent-uri"), hash: requireArg(takeFlag(args, "--parent-hash"), "--parent-hash") },
4013
+ ...evAtt ? { evidence_ref: { uri: `edgebook:attestation:${evAtt}`, hash: evAtt } } : {},
4014
+ ...evTask ? { evidence_task_id: evTask } : {},
4015
+ statement: requireArg(takeFlag(args, "--statement"), "--statement")
4016
+ });
4017
+ return { text: `Endorsement ${id.endorse_id}`, json: id };
4018
+ }
4019
+ if (command === "signal") {
4020
+ const ttl = takeFlag(args, "--ttl-ms");
4021
+ const id = await store.createSignal({ body: requireArg(takeFlag(args, "--body"), "--body"), ttlMs: ttl ? Number(ttl) : void 0 });
4022
+ return { text: `Signal ${id.signal_id}`, json: id };
4023
+ }
4024
+ if (command === "capability") {
4025
+ const action = args.shift() || "list";
4026
+ if (action === "advertise") {
4027
+ const id = await store.advertiseCapability({
4028
+ name: requireArg(takeFlag(args, "--name"), "--name"),
4029
+ version: requireArg(takeFlag(args, "--version"), "--version"),
4030
+ summary: requireArg(takeFlag(args, "--summary"), "--summary")
4031
+ });
4032
+ return { text: `Capability ${id.capability_id}`, json: id };
4033
+ }
4034
+ if (action === "deprecate") {
4035
+ const id = await store.deprecateCapability(requireArg(args.shift(), "<capability-id>"));
4036
+ return { text: `Deprecated ${id.capability_id}`, json: id };
4037
+ }
4038
+ if (action === "list") {
4039
+ const all = await store.capabilities();
4040
+ return { text: JSON.stringify(all, null, 2), json: all };
4041
+ }
4042
+ throw new EdgeBookError("unknown_action", `Unknown capability action: ${action}`);
4043
+ }
4044
+ if (command === "query" || command === "share" || command === "coordinate" || command === "delegate") {
4045
+ const type = command === "delegate" ? "delegation_request" : command;
4046
+ const body = requireArg(takeFlag(args, "--body"), "--body");
4047
+ const to = takeFlag(args, "--to") || takeFlag(args, "--with");
4048
+ const ref = takeFlag(args, "--ref");
4049
+ const ttl = takeFlag(args, "--ttl-ms");
4050
+ const post = await store.createEphemeral(type, { body, subject_agent_id: to, ref, ttlMs: ttl ? Number(ttl) : void 0 });
4051
+ return { text: `${post.post_type} ${post.post_id}`, json: post };
4052
+ }
4053
+ if (command === "answer") {
4054
+ const queryId = requireArg(args.shift(), "<query-id>");
4055
+ const ephemeral = await store.ephemeralPosts();
4056
+ const query = ephemeral[queryId];
4057
+ if (!query) throw new EdgeBookError("not_found", `No local query ${queryId} to answer`);
4058
+ const { signature: _sig, lifecycle: _lc, ...queryUnsigned } = query;
4059
+ const ans = await store.createAnswer({
4060
+ parent: { uri: "edgebook:query:" + queryId, hash: contentHash(queryUnsigned) },
4061
+ body: requireArg(takeFlag(args, "--body"), "--body")
4062
+ });
4063
+ return { text: `answer ${ans.answer_id}`, json: ans };
4064
+ }
4065
+ if (command === "query-delete") {
4066
+ const queryId = requireArg(args.shift(), "<query-id>");
4067
+ await store.deleteQuery(queryId);
4068
+ return { text: `Tombstoned query ${queryId} and its answers`, json: { query_id: queryId } };
4069
+ }
4070
+ if (command === "ephemeral") {
4071
+ const all = await store.ephemeralPosts();
4072
+ return { text: JSON.stringify(all, null, 2), json: all };
4073
+ }
4074
+ if (command === "answers") {
4075
+ const all = await store.answers();
4076
+ return { text: JSON.stringify(all, null, 2), json: all };
4077
+ }
3546
4078
  throw new EdgeBookError("unknown_command", usage());
3547
4079
  }
3548
4080
  async function runCli(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edge-book",
3
- "version": "0.2.5",
3
+ "version": "0.4.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",