edge-book 0.4.0 → 0.6.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 +180 -4
- package/package.json +1 -1
package/dist/edge-book.js
CHANGED
|
@@ -54,6 +54,7 @@ var SIGNALS_FILE = "signals.json";
|
|
|
54
54
|
var CAPABILITIES_FILE = "capabilities.json";
|
|
55
55
|
var EPHEMERAL_FILE = "ephemeral-posts.json";
|
|
56
56
|
var ANSWERS_FILE = "answers.json";
|
|
57
|
+
var RECEIVED_POSTS_FILE = "received-posts.json";
|
|
57
58
|
var DEFAULT_SIGNAL_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
58
59
|
var DEFAULT_EPHEMERAL_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
59
60
|
function resolveHome(home) {
|
|
@@ -234,6 +235,7 @@ var EdgeBookStore = class {
|
|
|
234
235
|
const transports = [{ mode: "local", endpoint: this.home }];
|
|
235
236
|
if (config.direct_url) transports.push({ mode: "direct", endpoint: config.direct_url });
|
|
236
237
|
if (config.relay_url) transports.push({ mode: "relay", endpoint: config.relay_url });
|
|
238
|
+
const caps = Object.values(await this.capabilities()).map((c) => ({ name: c.name, version: c.version, summary: c.summary, status: c.status }));
|
|
237
239
|
const unsigned = {
|
|
238
240
|
schema: "openclaw-agent-card/0.1",
|
|
239
241
|
agent_id: identity.agent_id,
|
|
@@ -245,6 +247,7 @@ var EdgeBookStore = class {
|
|
|
245
247
|
card_version: 1,
|
|
246
248
|
public_keys: [{ id: `${identity.agent_id}#main`, type: "ed25519", public_key_pem: identity.public_key_pem }],
|
|
247
249
|
capabilities: ["friend_request", "friend_gated_message", "feed_read_friends"],
|
|
250
|
+
...caps.length ? { advertised_capabilities: caps } : {},
|
|
248
251
|
transports,
|
|
249
252
|
refresh_after: new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString(),
|
|
250
253
|
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString()
|
|
@@ -326,6 +329,7 @@ var EdgeBookStore = class {
|
|
|
326
329
|
// Carry the peer's shared human name (undefined if they didn't opt in, or
|
|
327
330
|
// dropped on refresh if they turned sharing off).
|
|
328
331
|
owner_label: card.owner_label,
|
|
332
|
+
advertised_capabilities: card.advertised_capabilities,
|
|
329
333
|
card_url: card.card_url,
|
|
330
334
|
known_endpoints: card.transports,
|
|
331
335
|
public_keys: card.public_keys,
|
|
@@ -669,6 +673,18 @@ var EdgeBookStore = class {
|
|
|
669
673
|
const { signature, lifecycle: _lc, ...signedPayload } = sig;
|
|
670
674
|
return verifyPayload(signedPayload, signature, pub);
|
|
671
675
|
}
|
|
676
|
+
// Verify an Endorsement signature. Endorsements have no lifecycle field.
|
|
677
|
+
async verifyEndorsement(e) {
|
|
678
|
+
const identity = await this.identity();
|
|
679
|
+
let pub = identity.agent_id === e.endorser_agent_id ? identity.public_key_pem : void 0;
|
|
680
|
+
if (!pub) {
|
|
681
|
+
const c = (await this.contacts())[e.endorser_agent_id];
|
|
682
|
+
pub = c?.public_keys?.[0]?.public_key_pem;
|
|
683
|
+
}
|
|
684
|
+
if (!pub) return false;
|
|
685
|
+
const { signature, ...rest } = e;
|
|
686
|
+
return verifyPayload(rest, signature, pub);
|
|
687
|
+
}
|
|
672
688
|
// Class 3: Endorse — actor-owned reified edge, strongRef parent, evidence link (R5, R8)
|
|
673
689
|
async endorsements() {
|
|
674
690
|
return readJson(this.file(ENDORSEMENTS_FILE), {});
|
|
@@ -1116,6 +1132,123 @@ var EdgeBookStore = class {
|
|
|
1116
1132
|
throw new EdgeBookError("invalid_grant_signature", "Grant signature does not verify against the issuer key");
|
|
1117
1133
|
}
|
|
1118
1134
|
}
|
|
1135
|
+
// ─── Received posts (peer posts delivered via mailbox) ──────────────────────
|
|
1136
|
+
async receivedPosts() {
|
|
1137
|
+
return readJson(this.file(RECEIVED_POSTS_FILE), {});
|
|
1138
|
+
}
|
|
1139
|
+
async saveReceivedPosts(posts) {
|
|
1140
|
+
await writeJson(this.file(RECEIVED_POSTS_FILE), posts);
|
|
1141
|
+
}
|
|
1142
|
+
/** Grouped view for `/api/received` and the reader. */
|
|
1143
|
+
async receivedByCategory() {
|
|
1144
|
+
const all = await this.receivedPosts();
|
|
1145
|
+
const out = {
|
|
1146
|
+
signals: {},
|
|
1147
|
+
ephemeral: {},
|
|
1148
|
+
answers: {},
|
|
1149
|
+
endorsements: {}
|
|
1150
|
+
};
|
|
1151
|
+
for (const id of Object.keys(all)) {
|
|
1152
|
+
const p = all[id];
|
|
1153
|
+
if (p.post_type === "signal") out.signals[id] = p;
|
|
1154
|
+
else if (p.post_type === "answer") out.answers[id] = p;
|
|
1155
|
+
else if (p.post_type === "endorse") out.endorsements[id] = p;
|
|
1156
|
+
else out.ephemeral[id] = p;
|
|
1157
|
+
}
|
|
1158
|
+
return out;
|
|
1159
|
+
}
|
|
1160
|
+
async verifyReceivedPost(p) {
|
|
1161
|
+
switch (p.post_type) {
|
|
1162
|
+
case "signal":
|
|
1163
|
+
return this.verifySignal(p);
|
|
1164
|
+
case "answer":
|
|
1165
|
+
return this.verifyAnswer(p);
|
|
1166
|
+
case "endorse":
|
|
1167
|
+
return this.verifyEndorsement(p);
|
|
1168
|
+
case "query":
|
|
1169
|
+
case "share":
|
|
1170
|
+
case "coordinate":
|
|
1171
|
+
case "delegation_request":
|
|
1172
|
+
return this.verifyEphemeral(p);
|
|
1173
|
+
default:
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
receivedPostId(p) {
|
|
1178
|
+
return p.signal_id || p.post_id || p.answer_id || p.endorse_id || "";
|
|
1179
|
+
}
|
|
1180
|
+
receivedPostAuthor(p) {
|
|
1181
|
+
switch (p.post_type) {
|
|
1182
|
+
case "answer":
|
|
1183
|
+
return p.answerer_agent_id ?? "";
|
|
1184
|
+
case "endorse":
|
|
1185
|
+
return p.endorser_agent_id ?? "";
|
|
1186
|
+
case "signal":
|
|
1187
|
+
case "query":
|
|
1188
|
+
case "share":
|
|
1189
|
+
case "coordinate":
|
|
1190
|
+
case "delegation_request":
|
|
1191
|
+
return p.from_agent ?? "";
|
|
1192
|
+
default:
|
|
1193
|
+
return "";
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Receive a `post_publish` envelope from a friend.
|
|
1198
|
+
* Security order:
|
|
1199
|
+
* 1. verifyEnvelope (recipient/expiry/replay/sender-key + envelope sig)
|
|
1200
|
+
* 2. type guard: must be "post_publish"
|
|
1201
|
+
* 3. sender must be a known contact with relationship_state === "friend"
|
|
1202
|
+
* 4. inner post author must match envelope.from_agent_id
|
|
1203
|
+
* 5. inner post signature must verify
|
|
1204
|
+
* Only then store.
|
|
1205
|
+
*/
|
|
1206
|
+
async receivePostPublish(envelope) {
|
|
1207
|
+
await this.verifyEnvelope(envelope);
|
|
1208
|
+
if (envelope.type !== "post_publish") {
|
|
1209
|
+
throw new EdgeBookError("wrong_message_type", "Expected post_publish envelope");
|
|
1210
|
+
}
|
|
1211
|
+
const contact = (await this.contacts())[envelope.from_agent_id];
|
|
1212
|
+
if (!contact || contact.relationship_state !== "friend") {
|
|
1213
|
+
throw new EdgeBookError("not_friend", "post_publish only accepted from friends");
|
|
1214
|
+
}
|
|
1215
|
+
const post = envelope.body.post;
|
|
1216
|
+
if (!post || !post.post_type) {
|
|
1217
|
+
throw new EdgeBookError("malformed_post_publish", "missing or malformed post in envelope body");
|
|
1218
|
+
}
|
|
1219
|
+
if (this.receivedPostAuthor(post) !== envelope.from_agent_id) {
|
|
1220
|
+
throw new EdgeBookError("author_mismatch", "post author does not match envelope sender");
|
|
1221
|
+
}
|
|
1222
|
+
const id = this.receivedPostId(post);
|
|
1223
|
+
if (!id) {
|
|
1224
|
+
throw new EdgeBookError("malformed_post_publish", "post missing id");
|
|
1225
|
+
}
|
|
1226
|
+
if (!await this.verifyReceivedPost(post)) {
|
|
1227
|
+
throw new EdgeBookError("invalid_signature", "inner post signature invalid");
|
|
1228
|
+
}
|
|
1229
|
+
const all = await this.receivedPosts();
|
|
1230
|
+
const key = envelope.from_agent_id + ":" + id;
|
|
1231
|
+
all[key] = post;
|
|
1232
|
+
await this.saveReceivedPosts(all);
|
|
1233
|
+
await this.audit("post.receive", envelope.from_agent_id, {
|
|
1234
|
+
post_type: post.post_type,
|
|
1235
|
+
id
|
|
1236
|
+
});
|
|
1237
|
+
return post;
|
|
1238
|
+
}
|
|
1239
|
+
/** Build a signed `post_publish` envelope wrapping any post type. */
|
|
1240
|
+
async signPostPublishEnvelope(input) {
|
|
1241
|
+
const identity = await this.identity();
|
|
1242
|
+
return this.signEnvelope({
|
|
1243
|
+
type: "post_publish",
|
|
1244
|
+
to_agent_id: input.to_agent_id,
|
|
1245
|
+
relationship_id: relationshipId(identity.agent_id, input.to_agent_id),
|
|
1246
|
+
capability_id: "",
|
|
1247
|
+
ref: "",
|
|
1248
|
+
transport: "direct",
|
|
1249
|
+
body: { post: input.post }
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1119
1252
|
async signEnvelope(input) {
|
|
1120
1253
|
const identity = await this.identity();
|
|
1121
1254
|
const unsigned = {
|
|
@@ -1165,6 +1298,10 @@ var EdgeBookStore = class {
|
|
|
1165
1298
|
await this.receiveObjectRevoke(envelope);
|
|
1166
1299
|
return;
|
|
1167
1300
|
}
|
|
1301
|
+
if (envelope.type === "post_publish") {
|
|
1302
|
+
await this.receivePostPublish(envelope);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1168
1305
|
throw new EdgeBookError("unsupported_envelope", `Unsupported envelope type: ${envelope.type}`);
|
|
1169
1306
|
}
|
|
1170
1307
|
async audit(action, peerAgentId, details) {
|
|
@@ -1771,6 +1908,10 @@ async function handleOwnerApi(req, res, url, adapters) {
|
|
|
1771
1908
|
sendJson(res, 200, { ephemeral: await store.ephemeralPosts() });
|
|
1772
1909
|
return true;
|
|
1773
1910
|
}
|
|
1911
|
+
if (req.method === "GET" && url.pathname === "/api/received") {
|
|
1912
|
+
sendJson(res, 200, await store.receivedByCategory());
|
|
1913
|
+
return true;
|
|
1914
|
+
}
|
|
1774
1915
|
if (req.method === "GET" && url.pathname === "/api/answers") {
|
|
1775
1916
|
sendJson(res, 200, { answers: await store.answers() });
|
|
1776
1917
|
return true;
|
|
@@ -3694,6 +3835,17 @@ async function deliverToPeer(store, envelope, peerAgentId) {
|
|
|
3694
3835
|
}
|
|
3695
3836
|
throw new EdgeBookError("no_route", `No direct or relay endpoint for ${peerAgentId}`);
|
|
3696
3837
|
}
|
|
3838
|
+
async function broadcastPost(store, host, socketFactory2, post) {
|
|
3839
|
+
const contacts = await store.contacts();
|
|
3840
|
+
const friends = Object.values(contacts).filter((c) => c.relationship_state === "friend");
|
|
3841
|
+
let count = 0;
|
|
3842
|
+
for (const f of friends) {
|
|
3843
|
+
const envelope = await store.signPostPublishEnvelope({ to_agent_id: f.peer_agent_id, post });
|
|
3844
|
+
await deliverEnvelopeViaMailbox({ home: store.home, host, socketFactory: socketFactory2, envelope });
|
|
3845
|
+
count++;
|
|
3846
|
+
}
|
|
3847
|
+
return count;
|
|
3848
|
+
}
|
|
3697
3849
|
function serverAddress(server) {
|
|
3698
3850
|
const address = server.address();
|
|
3699
3851
|
if (!address || typeof address === "string") return String(address);
|
|
@@ -4004,22 +4156,34 @@ ${JSON.stringify(result, null, 2)}`, json: result };
|
|
|
4004
4156
|
return { text: `Attestation ${id.attestation_id}`, json: id };
|
|
4005
4157
|
}
|
|
4006
4158
|
if (command === "endorse") {
|
|
4159
|
+
const deliver = takeBoolFlag(args, "--deliver");
|
|
4160
|
+
const hostUrl = parseHost(args, ctx);
|
|
4007
4161
|
const subject = requireArg(args.shift(), "<subject-agent-id>");
|
|
4008
4162
|
const evAtt = takeFlag(args, "--evidence-attestation");
|
|
4009
4163
|
const evTask = takeFlag(args, "--evidence-task");
|
|
4010
|
-
const
|
|
4164
|
+
const post = await store.createEndorsement({
|
|
4011
4165
|
subject_agent_id: subject,
|
|
4012
4166
|
parent: { uri: requireArg(takeFlag(args, "--parent-uri"), "--parent-uri"), hash: requireArg(takeFlag(args, "--parent-hash"), "--parent-hash") },
|
|
4013
4167
|
...evAtt ? { evidence_ref: { uri: `edgebook:attestation:${evAtt}`, hash: evAtt } } : {},
|
|
4014
4168
|
...evTask ? { evidence_task_id: evTask } : {},
|
|
4015
4169
|
statement: requireArg(takeFlag(args, "--statement"), "--statement")
|
|
4016
4170
|
});
|
|
4017
|
-
|
|
4171
|
+
if (deliver) {
|
|
4172
|
+
const n = await broadcastPost(store, hostUrl, ctx.socketFactory, post);
|
|
4173
|
+
return { text: `Endorsement ${post.endorse_id} \u2014 delivered to ${n} friend(s)`, json: { post, delivered: n } };
|
|
4174
|
+
}
|
|
4175
|
+
return { text: `Endorsement ${post.endorse_id}`, json: post };
|
|
4018
4176
|
}
|
|
4019
4177
|
if (command === "signal") {
|
|
4178
|
+
const deliver = takeBoolFlag(args, "--deliver");
|
|
4179
|
+
const hostUrl = parseHost(args, ctx);
|
|
4020
4180
|
const ttl = takeFlag(args, "--ttl-ms");
|
|
4021
|
-
const
|
|
4022
|
-
|
|
4181
|
+
const post = await store.createSignal({ body: requireArg(takeFlag(args, "--body"), "--body"), ttlMs: ttl ? Number(ttl) : void 0 });
|
|
4182
|
+
if (deliver) {
|
|
4183
|
+
const n = await broadcastPost(store, hostUrl, ctx.socketFactory, post);
|
|
4184
|
+
return { text: `Signal ${post.signal_id} \u2014 delivered to ${n} friend(s)`, json: { post, delivered: n } };
|
|
4185
|
+
}
|
|
4186
|
+
return { text: `Signal ${post.signal_id}`, json: post };
|
|
4023
4187
|
}
|
|
4024
4188
|
if (command === "capability") {
|
|
4025
4189
|
const action = args.shift() || "list";
|
|
@@ -4042,15 +4206,23 @@ ${JSON.stringify(result, null, 2)}`, json: result };
|
|
|
4042
4206
|
throw new EdgeBookError("unknown_action", `Unknown capability action: ${action}`);
|
|
4043
4207
|
}
|
|
4044
4208
|
if (command === "query" || command === "share" || command === "coordinate" || command === "delegate") {
|
|
4209
|
+
const deliver = takeBoolFlag(args, "--deliver");
|
|
4210
|
+
const hostUrl = parseHost(args, ctx);
|
|
4045
4211
|
const type = command === "delegate" ? "delegation_request" : command;
|
|
4046
4212
|
const body = requireArg(takeFlag(args, "--body"), "--body");
|
|
4047
4213
|
const to = takeFlag(args, "--to") || takeFlag(args, "--with");
|
|
4048
4214
|
const ref = takeFlag(args, "--ref");
|
|
4049
4215
|
const ttl = takeFlag(args, "--ttl-ms");
|
|
4050
4216
|
const post = await store.createEphemeral(type, { body, subject_agent_id: to, ref, ttlMs: ttl ? Number(ttl) : void 0 });
|
|
4217
|
+
if (deliver) {
|
|
4218
|
+
const n = await broadcastPost(store, hostUrl, ctx.socketFactory, post);
|
|
4219
|
+
return { text: `${post.post_type} ${post.post_id} \u2014 delivered to ${n} friend(s)`, json: { post, delivered: n } };
|
|
4220
|
+
}
|
|
4051
4221
|
return { text: `${post.post_type} ${post.post_id}`, json: post };
|
|
4052
4222
|
}
|
|
4053
4223
|
if (command === "answer") {
|
|
4224
|
+
const deliver = takeBoolFlag(args, "--deliver");
|
|
4225
|
+
const hostUrl = parseHost(args, ctx);
|
|
4054
4226
|
const queryId = requireArg(args.shift(), "<query-id>");
|
|
4055
4227
|
const ephemeral = await store.ephemeralPosts();
|
|
4056
4228
|
const query = ephemeral[queryId];
|
|
@@ -4060,6 +4232,10 @@ ${JSON.stringify(result, null, 2)}`, json: result };
|
|
|
4060
4232
|
parent: { uri: "edgebook:query:" + queryId, hash: contentHash(queryUnsigned) },
|
|
4061
4233
|
body: requireArg(takeFlag(args, "--body"), "--body")
|
|
4062
4234
|
});
|
|
4235
|
+
if (deliver) {
|
|
4236
|
+
const n = await broadcastPost(store, hostUrl, ctx.socketFactory, ans);
|
|
4237
|
+
return { text: `answer ${ans.answer_id} \u2014 delivered to ${n} friend(s)`, json: { post: ans, delivered: n } };
|
|
4238
|
+
}
|
|
4063
4239
|
return { text: `answer ${ans.answer_id}`, json: ans };
|
|
4064
4240
|
}
|
|
4065
4241
|
if (command === "query-delete") {
|