edge-book 0.5.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 +177 -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) {
|
|
@@ -672,6 +673,18 @@ var EdgeBookStore = class {
|
|
|
672
673
|
const { signature, lifecycle: _lc, ...signedPayload } = sig;
|
|
673
674
|
return verifyPayload(signedPayload, signature, pub);
|
|
674
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
|
+
}
|
|
675
688
|
// Class 3: Endorse — actor-owned reified edge, strongRef parent, evidence link (R5, R8)
|
|
676
689
|
async endorsements() {
|
|
677
690
|
return readJson(this.file(ENDORSEMENTS_FILE), {});
|
|
@@ -1119,6 +1132,123 @@ var EdgeBookStore = class {
|
|
|
1119
1132
|
throw new EdgeBookError("invalid_grant_signature", "Grant signature does not verify against the issuer key");
|
|
1120
1133
|
}
|
|
1121
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
|
+
}
|
|
1122
1252
|
async signEnvelope(input) {
|
|
1123
1253
|
const identity = await this.identity();
|
|
1124
1254
|
const unsigned = {
|
|
@@ -1168,6 +1298,10 @@ var EdgeBookStore = class {
|
|
|
1168
1298
|
await this.receiveObjectRevoke(envelope);
|
|
1169
1299
|
return;
|
|
1170
1300
|
}
|
|
1301
|
+
if (envelope.type === "post_publish") {
|
|
1302
|
+
await this.receivePostPublish(envelope);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1171
1305
|
throw new EdgeBookError("unsupported_envelope", `Unsupported envelope type: ${envelope.type}`);
|
|
1172
1306
|
}
|
|
1173
1307
|
async audit(action, peerAgentId, details) {
|
|
@@ -1774,6 +1908,10 @@ async function handleOwnerApi(req, res, url, adapters) {
|
|
|
1774
1908
|
sendJson(res, 200, { ephemeral: await store.ephemeralPosts() });
|
|
1775
1909
|
return true;
|
|
1776
1910
|
}
|
|
1911
|
+
if (req.method === "GET" && url.pathname === "/api/received") {
|
|
1912
|
+
sendJson(res, 200, await store.receivedByCategory());
|
|
1913
|
+
return true;
|
|
1914
|
+
}
|
|
1777
1915
|
if (req.method === "GET" && url.pathname === "/api/answers") {
|
|
1778
1916
|
sendJson(res, 200, { answers: await store.answers() });
|
|
1779
1917
|
return true;
|
|
@@ -3697,6 +3835,17 @@ async function deliverToPeer(store, envelope, peerAgentId) {
|
|
|
3697
3835
|
}
|
|
3698
3836
|
throw new EdgeBookError("no_route", `No direct or relay endpoint for ${peerAgentId}`);
|
|
3699
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
|
+
}
|
|
3700
3849
|
function serverAddress(server) {
|
|
3701
3850
|
const address = server.address();
|
|
3702
3851
|
if (!address || typeof address === "string") return String(address);
|
|
@@ -4007,22 +4156,34 @@ ${JSON.stringify(result, null, 2)}`, json: result };
|
|
|
4007
4156
|
return { text: `Attestation ${id.attestation_id}`, json: id };
|
|
4008
4157
|
}
|
|
4009
4158
|
if (command === "endorse") {
|
|
4159
|
+
const deliver = takeBoolFlag(args, "--deliver");
|
|
4160
|
+
const hostUrl = parseHost(args, ctx);
|
|
4010
4161
|
const subject = requireArg(args.shift(), "<subject-agent-id>");
|
|
4011
4162
|
const evAtt = takeFlag(args, "--evidence-attestation");
|
|
4012
4163
|
const evTask = takeFlag(args, "--evidence-task");
|
|
4013
|
-
const
|
|
4164
|
+
const post = await store.createEndorsement({
|
|
4014
4165
|
subject_agent_id: subject,
|
|
4015
4166
|
parent: { uri: requireArg(takeFlag(args, "--parent-uri"), "--parent-uri"), hash: requireArg(takeFlag(args, "--parent-hash"), "--parent-hash") },
|
|
4016
4167
|
...evAtt ? { evidence_ref: { uri: `edgebook:attestation:${evAtt}`, hash: evAtt } } : {},
|
|
4017
4168
|
...evTask ? { evidence_task_id: evTask } : {},
|
|
4018
4169
|
statement: requireArg(takeFlag(args, "--statement"), "--statement")
|
|
4019
4170
|
});
|
|
4020
|
-
|
|
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 };
|
|
4021
4176
|
}
|
|
4022
4177
|
if (command === "signal") {
|
|
4178
|
+
const deliver = takeBoolFlag(args, "--deliver");
|
|
4179
|
+
const hostUrl = parseHost(args, ctx);
|
|
4023
4180
|
const ttl = takeFlag(args, "--ttl-ms");
|
|
4024
|
-
const
|
|
4025
|
-
|
|
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 };
|
|
4026
4187
|
}
|
|
4027
4188
|
if (command === "capability") {
|
|
4028
4189
|
const action = args.shift() || "list";
|
|
@@ -4045,15 +4206,23 @@ ${JSON.stringify(result, null, 2)}`, json: result };
|
|
|
4045
4206
|
throw new EdgeBookError("unknown_action", `Unknown capability action: ${action}`);
|
|
4046
4207
|
}
|
|
4047
4208
|
if (command === "query" || command === "share" || command === "coordinate" || command === "delegate") {
|
|
4209
|
+
const deliver = takeBoolFlag(args, "--deliver");
|
|
4210
|
+
const hostUrl = parseHost(args, ctx);
|
|
4048
4211
|
const type = command === "delegate" ? "delegation_request" : command;
|
|
4049
4212
|
const body = requireArg(takeFlag(args, "--body"), "--body");
|
|
4050
4213
|
const to = takeFlag(args, "--to") || takeFlag(args, "--with");
|
|
4051
4214
|
const ref = takeFlag(args, "--ref");
|
|
4052
4215
|
const ttl = takeFlag(args, "--ttl-ms");
|
|
4053
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
|
+
}
|
|
4054
4221
|
return { text: `${post.post_type} ${post.post_id}`, json: post };
|
|
4055
4222
|
}
|
|
4056
4223
|
if (command === "answer") {
|
|
4224
|
+
const deliver = takeBoolFlag(args, "--deliver");
|
|
4225
|
+
const hostUrl = parseHost(args, ctx);
|
|
4057
4226
|
const queryId = requireArg(args.shift(), "<query-id>");
|
|
4058
4227
|
const ephemeral = await store.ephemeralPosts();
|
|
4059
4228
|
const query = ephemeral[queryId];
|
|
@@ -4063,6 +4232,10 @@ ${JSON.stringify(result, null, 2)}`, json: result };
|
|
|
4063
4232
|
parent: { uri: "edgebook:query:" + queryId, hash: contentHash(queryUnsigned) },
|
|
4064
4233
|
body: requireArg(takeFlag(args, "--body"), "--body")
|
|
4065
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
|
+
}
|
|
4066
4239
|
return { text: `answer ${ans.answer_id}`, json: ans };
|
|
4067
4240
|
}
|
|
4068
4241
|
if (command === "query-delete") {
|