edge-book 0.3.0 → 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 +256 -6
  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) {
@@ -46,7 +52,10 @@ var ATTESTATIONS_FILE = "attestations.json";
46
52
  var ENDORSEMENTS_FILE = "endorsements.json";
47
53
  var SIGNALS_FILE = "signals.json";
48
54
  var CAPABILITIES_FILE = "capabilities.json";
55
+ var EPHEMERAL_FILE = "ephemeral-posts.json";
56
+ var ANSWERS_FILE = "answers.json";
49
57
  var DEFAULT_SIGNAL_TTL_MS = 6 * 60 * 60 * 1e3;
58
+ var DEFAULT_EPHEMERAL_TTL_MS = 24 * 60 * 60 * 1e3;
50
59
  function resolveHome(home) {
51
60
  if (home?.trim()) return path.resolve(home.trim());
52
61
  if (process.env.EDGE_BOOK_HOME?.trim()) return path.resolve(process.env.EDGE_BOOK_HOME.trim());
@@ -145,6 +154,13 @@ async function readJsonl(file) {
145
154
  function relationshipId(a, b) {
146
155
  return `rel_${crypto.createHash("sha256").update([a, b].sort().join("|")).digest("base64url").slice(0, 24)}`;
147
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
+ }
148
164
  var EdgeBookStore = class {
149
165
  home;
150
166
  constructor(options = {}) {
@@ -470,6 +486,7 @@ var EdgeBookStore = class {
470
486
  }
471
487
  const grant = await this.findUsableGrant(peerAgentId, scope);
472
488
  if (!grant) throw new EdgeBookError("missing_grant", `No active grant for ${scope}`);
489
+ await this.assertGrantSignature(grant);
473
490
  const envelope = await this.signEnvelope({
474
491
  type: "privileged_message",
475
492
  to_agent_id: peerAgentId,
@@ -497,6 +514,7 @@ var EdgeBookStore = class {
497
514
  if (!grant || grant.status !== "active" || grant.subject_agent_id !== envelope.from_agent_id || !grant.scopes.includes("message.friend")) {
498
515
  throw new EdgeBookError("missing_grant", "Message does not carry an active grant issued to sender");
499
516
  }
517
+ await this.assertGrantSignature(grant);
500
518
  await appendJsonl(this.file(INBOX_FILE), envelope);
501
519
  await this.audit("message.receive", envelope.from_agent_id, { message_id: envelope.message_id });
502
520
  }
@@ -614,6 +632,43 @@ var EdgeBookStore = class {
614
632
  const { signature, ...rest } = cap;
615
633
  return verifyPayload(rest, signature, pub);
616
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
+ }
617
672
  // Class 3: Endorse — actor-owned reified edge, strongRef parent, evidence link (R5, R8)
618
673
  async endorsements() {
619
674
  return readJson(this.file(ENDORSEMENTS_FILE), {});
@@ -650,8 +705,7 @@ var EdgeBookStore = class {
650
705
  }
651
706
  // Class 2: Signal — ephemeral, lifecycle + TTL (R4)
652
707
  signalLifecycle(sig) {
653
- if (sig.lifecycle === "expired") return "expired";
654
- return Date.parse(sig.expires_at) <= Date.now() ? "stale" : "active";
708
+ return computeLifecycle(sig.expires_at, false, sig.lifecycle);
655
709
  }
656
710
  async signals() {
657
711
  const raw = await readJson(this.file(SIGNALS_FILE), {});
@@ -669,11 +723,10 @@ var EdgeBookStore = class {
669
723
  schema: "edge-book/signal/0.1",
670
724
  from_agent: identity.agent_id,
671
725
  body: input.body,
672
- lifecycle: "active",
673
726
  created_at: created,
674
727
  expires_at
675
728
  };
676
- const signal = { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
729
+ const signal = { ...unsigned, lifecycle: "active", signature: signPayload(unsigned, identity.private_key_pem) };
677
730
  const all = await this.signals();
678
731
  all[signal_id] = signal;
679
732
  await this.saveSignals(all);
@@ -691,6 +744,111 @@ var EdgeBookStore = class {
691
744
  }
692
745
  if (changed) await this.saveSignals(all);
693
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
+ }
694
852
  // Class 1: Capability Advertisement — versioned, deprecate-not-delete (R3)
695
853
  async capabilities() {
696
854
  return readJson(this.file(CAPABILITIES_FILE), {});
@@ -745,8 +903,21 @@ var EdgeBookStore = class {
745
903
  }
746
904
  await this.saveCapabilities(caps);
747
905
  const sigs = await readJson(this.file(SIGNALS_FILE), {});
748
- for (const id of Object.keys(sigs)) sigs[id].lifecycle = "expired";
906
+ for (const id of Object.keys(sigs)) {
907
+ if (sigs[id].lifecycle !== "expired") sigs[id].lifecycle = "expired";
908
+ }
749
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);
750
921
  await this.audit("agent.deregister", (await this.identity()).agent_id, {});
751
922
  }
752
923
  // Issue an `object.read` grant binding ONE object to ONE subject (revocable).
@@ -917,6 +1088,34 @@ var EdgeBookStore = class {
917
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())
918
1089
  );
919
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
+ }
920
1119
  async signEnvelope(input) {
921
1120
  const identity = await this.identity();
922
1121
  const unsigned = {
@@ -1227,6 +1426,7 @@ var EdgeBookStore = class {
1227
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())
1228
1427
  );
1229
1428
  if (!grant) throw new EdgeBookError("missing_grant", "No active feed.read.friends grant for peer");
1429
+ await this.assertGrantSignature(grant);
1230
1430
  const posts = Object.values(await this.posts());
1231
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));
1232
1432
  }
@@ -1567,6 +1767,14 @@ async function handleOwnerApi(req, res, url, adapters) {
1567
1767
  sendJson(res, 200, { capabilities: await store.capabilities() });
1568
1768
  return true;
1569
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
+ }
1570
1778
  if (req.method === "GET" && url.pathname === "/api/shared-objects") {
1571
1779
  const objects = await store.sharedObjectsFor();
1572
1780
  sendJson(res, 200, { objects: objects.map((object) => ({ ...object, grant_scope: "object.read" })) });
@@ -3434,7 +3642,15 @@ Post taxonomy (spec-0021):
3434
3642
  edge-book signal --body <s> [--ttl-ms <ms>]
3435
3643
  edge-book capability advertise --name <n> --version <v> --summary <s>
3436
3644
  edge-book capability deprecate <capability-id>
3437
- edge-book capability list`;
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`;
3438
3654
  }
3439
3655
  function takeFlag(args, name) {
3440
3656
  const idx = args.indexOf(name);
@@ -3825,6 +4041,40 @@ ${JSON.stringify(result, null, 2)}`, json: result };
3825
4041
  }
3826
4042
  throw new EdgeBookError("unknown_action", `Unknown capability action: ${action}`);
3827
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
+ }
3828
4078
  throw new EdgeBookError("unknown_command", usage());
3829
4079
  }
3830
4080
  async function runCli(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edge-book",
3
- "version": "0.3.0",
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",