edge-book 0.1.0 → 0.1.1

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.
@@ -0,0 +1,2965 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import fs4 from "fs/promises";
5
+ import { realpathSync } from "fs";
6
+ import path4 from "path";
7
+ import { fileURLToPath } from "url";
8
+
9
+ // src/dialout.ts
10
+ import crypto2 from "crypto";
11
+ import fs3 from "fs/promises";
12
+ import path3 from "path";
13
+
14
+ // src/edge-book.ts
15
+ import crypto from "crypto";
16
+ import fs from "fs/promises";
17
+ import os from "os";
18
+ import path from "path";
19
+ var EdgeBookError = class extends Error {
20
+ code;
21
+ constructor(code, message) {
22
+ super(message);
23
+ this.name = "EdgeBookError";
24
+ this.code = code;
25
+ }
26
+ };
27
+ var IDENTITY_FILE = "identity.json";
28
+ var CONTACTS_FILE = "contacts.json";
29
+ var GRANTS_FILE = "grants.json";
30
+ var SEEN_MESSAGES_FILE = "seen-messages.json";
31
+ var CONFIG_FILE = "config.json";
32
+ var RELATIONSHIP_EVENTS_FILE = "relationship-events.jsonl";
33
+ var MESSAGES_FILE = "messages.jsonl";
34
+ var AUDIT_FILE = "audit.jsonl";
35
+ var INBOX_FILE = "inbox.jsonl";
36
+ var CARD_FILE = "openclaw-agent.json";
37
+ var SESSIONS_FILE = "web-sessions.json";
38
+ var POSTS_FILE = "posts.json";
39
+ var FEED_FILE = "feed-items.json";
40
+ var APPROVALS_FILE = "approvals.json";
41
+ var CONTACT_MUTES_FILE = "contact-mutes.json";
42
+ function resolveHome(home) {
43
+ if (home?.trim()) return path.resolve(home.trim());
44
+ if (process.env.EDGE_BOOK_HOME?.trim()) return path.resolve(process.env.EDGE_BOOK_HOME.trim());
45
+ return path.join(os.homedir(), ".openclaw", "edge-book");
46
+ }
47
+ function now() {
48
+ return (/* @__PURE__ */ new Date()).toISOString();
49
+ }
50
+ function randomId(prefix) {
51
+ return `${prefix}_${crypto.randomBytes(16).toString("base64url")}`;
52
+ }
53
+ function stableIdFromPublicKey(publicKeyPem) {
54
+ const digest = crypto.createHash("sha256").update(publicKeyPem).digest("base64url").slice(0, 32);
55
+ return `did:openclaw:${digest}`;
56
+ }
57
+ function canonicalize(value) {
58
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
59
+ if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
60
+ const obj = value;
61
+ return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(obj[key])}`).join(",")}}`;
62
+ }
63
+ function withoutSignature(value) {
64
+ const clone = { ...value };
65
+ delete clone.signature;
66
+ return clone;
67
+ }
68
+ function signPayload(payload, privateKeyPem) {
69
+ return crypto.sign(null, Buffer.from(canonicalize(payload)), privateKeyPem).toString("base64url");
70
+ }
71
+ function verifyPayload(payload, signature, publicKeyPem) {
72
+ return crypto.verify(null, Buffer.from(canonicalize(payload)), publicKeyPem, Buffer.from(signature, "base64url"));
73
+ }
74
+ async function ensureHome(home) {
75
+ await fs.mkdir(home, { recursive: true });
76
+ await chmodBestEffort(home, 448);
77
+ }
78
+ async function readJson(file, fallback) {
79
+ try {
80
+ return JSON.parse(await fs.readFile(file, "utf8"));
81
+ } catch (error) {
82
+ if (error.code === "ENOENT") return fallback;
83
+ throw error;
84
+ }
85
+ }
86
+ async function chmodBestEffort(file, mode) {
87
+ if (process.platform === "win32") return;
88
+ try {
89
+ await fs.chmod(file, mode);
90
+ } catch {
91
+ }
92
+ }
93
+ async function writeJson(file, value, mode) {
94
+ await fs.mkdir(path.dirname(file), { recursive: true });
95
+ await fs.writeFile(file, `${JSON.stringify(value, null, 2)}
96
+ `, "utf8");
97
+ if (mode !== void 0) await chmodBestEffort(file, mode);
98
+ }
99
+ async function appendJsonl(file, value) {
100
+ await fs.mkdir(path.dirname(file), { recursive: true });
101
+ await fs.appendFile(file, `${JSON.stringify(value)}
102
+ `, "utf8");
103
+ }
104
+ async function readJsonl(file) {
105
+ try {
106
+ const text = await fs.readFile(file, "utf8");
107
+ return text.split(/\n/).filter(Boolean).map((line) => JSON.parse(line));
108
+ } catch (error) {
109
+ if (error.code === "ENOENT") return [];
110
+ throw error;
111
+ }
112
+ }
113
+ function relationshipId(a, b) {
114
+ return `rel_${crypto.createHash("sha256").update([a, b].sort().join("|")).digest("base64url").slice(0, 24)}`;
115
+ }
116
+ var EdgeBookStore = class {
117
+ home;
118
+ constructor(options = {}) {
119
+ this.home = resolveHome(options.home);
120
+ }
121
+ file(name) {
122
+ return path.join(this.home, name);
123
+ }
124
+ async init(input = {}) {
125
+ await ensureHome(this.home);
126
+ const existing = await readJson(this.file(IDENTITY_FILE), null);
127
+ if (existing) {
128
+ await this.updateConfig({ direct_url: input.directUrl, relay_url: input.relayUrl });
129
+ return existing;
130
+ }
131
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
132
+ const public_key_pem = publicKey.export({ type: "spki", format: "pem" }).toString();
133
+ const private_key_pem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
134
+ const identity = {
135
+ agent_id: stableIdFromPublicKey(public_key_pem),
136
+ handle: input.handle || "agent.openclaw.local",
137
+ display_name: input.displayName || "OpenClaw Agent",
138
+ owner_label: input.ownerLabel || "",
139
+ public_key_pem,
140
+ private_key_pem,
141
+ created_at: now(),
142
+ updated_at: now()
143
+ };
144
+ await writeJson(this.file(IDENTITY_FILE), identity, 384);
145
+ await writeJson(this.file(CONTACTS_FILE), {});
146
+ await writeJson(this.file(GRANTS_FILE), {});
147
+ await writeJson(this.file(SEEN_MESSAGES_FILE), []);
148
+ await this.updateConfig({ direct_url: input.directUrl, relay_url: input.relayUrl });
149
+ await this.audit("identity.init", identity.agent_id, { handle: identity.handle });
150
+ await this.writeCard(input.cardUrl);
151
+ return identity;
152
+ }
153
+ async identity() {
154
+ const identity = await readJson(this.file(IDENTITY_FILE), null);
155
+ if (!identity) throw new EdgeBookError("not_initialized", `Edge Book is not initialized at ${this.home}`);
156
+ return identity;
157
+ }
158
+ async config() {
159
+ return readJson(this.file(CONFIG_FILE), {});
160
+ }
161
+ async updateConfig(input) {
162
+ const current = await this.config();
163
+ const next = { ...current };
164
+ if (input.direct_url !== void 0) next.direct_url = input.direct_url;
165
+ if (input.relay_url !== void 0) next.relay_url = input.relay_url;
166
+ await writeJson(this.file(CONFIG_FILE), next);
167
+ return next;
168
+ }
169
+ async buildCard(cardUrl) {
170
+ const identity = await this.identity();
171
+ const config = await this.config();
172
+ const transports = [{ mode: "local", endpoint: this.home }];
173
+ if (config.direct_url) transports.push({ mode: "direct", endpoint: config.direct_url });
174
+ if (config.relay_url) transports.push({ mode: "relay", endpoint: config.relay_url });
175
+ const unsigned = {
176
+ schema: "openclaw-agent-card/0.1",
177
+ agent_id: identity.agent_id,
178
+ handle: identity.handle,
179
+ display_name: identity.display_name,
180
+ card_url: cardUrl || `file://${this.file(CARD_FILE)}`,
181
+ card_version: 1,
182
+ public_keys: [{ id: `${identity.agent_id}#main`, type: "ed25519", public_key_pem: identity.public_key_pem }],
183
+ capabilities: ["friend_request", "friend_gated_message", "feed_read_friends"],
184
+ transports,
185
+ refresh_after: new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString(),
186
+ expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString()
187
+ };
188
+ const card_hash = crypto.createHash("sha256").update(canonicalize(unsigned)).digest("base64url");
189
+ const withHash = { ...unsigned, card_hash };
190
+ return { ...withHash, signature: signPayload(withHash, identity.private_key_pem) };
191
+ }
192
+ async writeCard(cardUrl) {
193
+ const card = await this.buildCard(cardUrl);
194
+ await writeJson(this.file(CARD_FILE), card);
195
+ return card;
196
+ }
197
+ async doctor() {
198
+ const identity = await readJson(this.file(IDENTITY_FILE), null);
199
+ const config = await this.config();
200
+ const checks = {
201
+ home: this.home,
202
+ initialized: Boolean(identity),
203
+ config,
204
+ files: {}
205
+ };
206
+ const requiredFiles = [IDENTITY_FILE, CONTACTS_FILE, GRANTS_FILE, SEEN_MESSAGES_FILE, CARD_FILE];
207
+ const files = {};
208
+ for (const name of requiredFiles) {
209
+ try {
210
+ const stat = await fs.stat(this.file(name));
211
+ files[name] = {
212
+ exists: true,
213
+ mode: `0${(stat.mode & 511).toString(8)}`
214
+ };
215
+ } catch (error) {
216
+ if (error.code === "ENOENT") {
217
+ files[name] = { exists: false };
218
+ } else {
219
+ throw error;
220
+ }
221
+ }
222
+ }
223
+ checks.files = files;
224
+ let cardValid = false;
225
+ try {
226
+ const card = await loadCard(this.file(CARD_FILE));
227
+ cardValid = Boolean(identity && card.agent_id === identity.agent_id);
228
+ } catch {
229
+ cardValid = false;
230
+ }
231
+ const identityMode = files[IDENTITY_FILE].mode;
232
+ const privateKeyModeOk = process.platform === "win32" || identityMode === "0600";
233
+ checks.card_valid = cardValid;
234
+ checks.private_key_mode_ok = privateKeyModeOk;
235
+ checks.pass = Boolean(identity) && cardValid && privateKeyModeOk;
236
+ return checks;
237
+ }
238
+ async contacts() {
239
+ return readJson(this.file(CONTACTS_FILE), {});
240
+ }
241
+ async saveContacts(contacts) {
242
+ await writeJson(this.file(CONTACTS_FILE), contacts);
243
+ }
244
+ async grants() {
245
+ return readJson(this.file(GRANTS_FILE), {});
246
+ }
247
+ async saveGrants(grants) {
248
+ await writeJson(this.file(GRANTS_FILE), grants);
249
+ }
250
+ async upsertContactFromCard(card, state) {
251
+ validateCard(card);
252
+ const contacts = await this.contacts();
253
+ const existing = contacts[card.agent_id];
254
+ if (existing?.relationship_state === "blocked" && state !== "blocked") {
255
+ throw new EdgeBookError("blocked_peer", "Blocked peer cannot refresh privileged contact state");
256
+ }
257
+ const stamp = now();
258
+ const next = {
259
+ peer_agent_id: card.agent_id,
260
+ aliases: Array.from(new Set([...existing?.aliases ?? [], card.handle].filter(Boolean))),
261
+ display_name: card.display_name,
262
+ card_url: card.card_url,
263
+ known_endpoints: card.transports,
264
+ public_keys: card.public_keys,
265
+ relationship_state: state ?? existing?.relationship_state ?? "none",
266
+ capability_grants: existing?.capability_grants ?? [],
267
+ last_card_hash: card.card_hash,
268
+ last_card_version: card.card_version,
269
+ last_card_refresh_at: stamp,
270
+ last_successful_delivery_at: existing?.last_successful_delivery_at ?? "",
271
+ audit_refs: existing?.audit_refs ?? [],
272
+ created_at: existing?.created_at ?? stamp,
273
+ updated_at: stamp
274
+ };
275
+ contacts[card.agent_id] = next;
276
+ await this.saveContacts(contacts);
277
+ await this.audit("contact.upsert", card.agent_id, { state: next.relationship_state });
278
+ return next;
279
+ }
280
+ async setRelationship(peerAgentId, nextState, type, reason = "") {
281
+ const identity = await this.identity();
282
+ const contacts = await this.contacts();
283
+ const contact = contacts[peerAgentId];
284
+ if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
285
+ const previous = contact.relationship_state;
286
+ contact.relationship_state = nextState;
287
+ contact.updated_at = now();
288
+ contacts[peerAgentId] = contact;
289
+ await this.saveContacts(contacts);
290
+ const unsigned = {
291
+ event_id: randomId("evt"),
292
+ type,
293
+ from_agent_id: identity.agent_id,
294
+ to_agent_id: peerAgentId,
295
+ relationship_id: relationshipId(identity.agent_id, peerAgentId),
296
+ previous_state: previous,
297
+ next_state: nextState,
298
+ human_approval_ref: "local-test-harness-or-cli",
299
+ reason,
300
+ created_at: now()
301
+ };
302
+ const event = { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
303
+ await appendJsonl(this.file(RELATIONSHIP_EVENTS_FILE), event);
304
+ await this.audit(`relationship.${type}`, peerAgentId, { previous, next: nextState, reason });
305
+ return event;
306
+ }
307
+ async createFriendRequest(targetCard, note = "") {
308
+ const identity = await this.identity();
309
+ validateCard(targetCard);
310
+ const existing = (await this.contacts())[targetCard.agent_id];
311
+ if (existing?.relationship_state === "blocked") throw new EdgeBookError("blocked_peer", "Cannot request a blocked peer");
312
+ await this.upsertContactFromCard(targetCard, "request_sent");
313
+ await this.setRelationship(targetCard.agent_id, "request_sent", "FriendRequest", note);
314
+ const card = await this.writeCard();
315
+ return this.signEnvelope({
316
+ type: "friend_request",
317
+ to_agent_id: targetCard.agent_id,
318
+ relationship_id: relationshipId(identity.agent_id, targetCard.agent_id),
319
+ capability_id: "",
320
+ ref: "",
321
+ transport: "local",
322
+ body: { card, note }
323
+ });
324
+ }
325
+ async receiveFriendRequest(envelope) {
326
+ await this.verifyEnvelope(envelope);
327
+ if (envelope.type !== "friend_request") throw new EdgeBookError("wrong_message_type", "Expected friend_request envelope");
328
+ const body = envelope.body;
329
+ validateCard(body.card);
330
+ if (body.card.agent_id !== envelope.from_agent_id) throw new EdgeBookError("agent_id_mismatch", "Friend request card does not match sender");
331
+ const contact = await this.upsertContactFromCard(body.card, "request_received");
332
+ await this.setRelationship(envelope.from_agent_id, "request_received", "FriendRequest", body.note);
333
+ await appendJsonl(this.file(INBOX_FILE), envelope);
334
+ return contact;
335
+ }
336
+ async acceptFriend(peerAgentId, reason = "accepted") {
337
+ const identity = await this.identity();
338
+ const contacts = await this.contacts();
339
+ const contact = contacts[peerAgentId];
340
+ if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
341
+ if (contact.relationship_state === "blocked") throw new EdgeBookError("blocked_peer", "Cannot accept a blocked peer");
342
+ await this.setRelationship(peerAgentId, "friend", "Accept", reason);
343
+ const grant = await this.issueGrant(peerAgentId, ["message.friend", "feed.read.friends"]);
344
+ const card = await this.writeCard();
345
+ return this.signEnvelope({
346
+ type: "friend_response",
347
+ to_agent_id: peerAgentId,
348
+ relationship_id: relationshipId(identity.agent_id, peerAgentId),
349
+ capability_id: grant.grant_id,
350
+ ref: "",
351
+ transport: "local",
352
+ body: { accepted: true, card, grant, reason }
353
+ });
354
+ }
355
+ async applyFriendResponse(envelope) {
356
+ await this.verifyEnvelope(envelope);
357
+ if (envelope.type !== "friend_response") throw new EdgeBookError("wrong_message_type", "Expected friend_response envelope");
358
+ const body = envelope.body;
359
+ validateCard(body.card);
360
+ if (body.card.agent_id !== envelope.from_agent_id) throw new EdgeBookError("agent_id_mismatch", "Friend response card does not match sender");
361
+ await this.upsertContactFromCard(body.card, body.accepted ? "friend" : "rejected");
362
+ await this.setRelationship(envelope.from_agent_id, body.accepted ? "friend" : "rejected", body.accepted ? "Accept" : "Reject", body.reason);
363
+ if (body.grant) await this.storeGrant(body.grant);
364
+ }
365
+ async revoke(peerAgentId) {
366
+ await this.setRelationship(peerAgentId, "revoked", "Revoke", "revoked");
367
+ const grants = await this.grants();
368
+ for (const grant of Object.values(grants)) {
369
+ if (grant.subject_agent_id === peerAgentId || grant.issuer_agent_id === peerAgentId) {
370
+ grant.status = "revoked";
371
+ grant.revoked_at = now();
372
+ }
373
+ }
374
+ await this.saveGrants(grants);
375
+ }
376
+ async block(peerAgentId) {
377
+ await this.setRelationship(peerAgentId, "blocked", "Block", "blocked");
378
+ }
379
+ async issueGrant(subjectAgentId, scopes, expiresAt = "") {
380
+ const identity = await this.identity();
381
+ const unsigned = {
382
+ grant_id: randomId("grant"),
383
+ issuer_agent_id: identity.agent_id,
384
+ subject_agent_id: subjectAgentId,
385
+ relationship_id: relationshipId(identity.agent_id, subjectAgentId),
386
+ scopes,
387
+ status: "active",
388
+ issued_at: now(),
389
+ expires_at: expiresAt,
390
+ revoked_at: "",
391
+ audit_refs: []
392
+ };
393
+ const grant = { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
394
+ await this.storeGrant(grant);
395
+ await this.audit("grant.issue", subjectAgentId, { grant_id: grant.grant_id, scopes });
396
+ return grant;
397
+ }
398
+ async storeGrant(grant) {
399
+ const grants = await this.grants();
400
+ grants[grant.grant_id] = grant;
401
+ await this.saveGrants(grants);
402
+ const contacts = await this.contacts();
403
+ const peer = grant.issuer_agent_id === (await this.identity()).agent_id ? grant.subject_agent_id : grant.issuer_agent_id;
404
+ const contact = contacts[peer];
405
+ if (contact && !contact.capability_grants.includes(grant.grant_id)) {
406
+ contact.capability_grants.push(grant.grant_id);
407
+ contact.updated_at = now();
408
+ contacts[peer] = contact;
409
+ await this.saveContacts(contacts);
410
+ }
411
+ }
412
+ async sendPrivilegedMessage(peerAgentId, body, scope = "message.friend") {
413
+ const identity = await this.identity();
414
+ const contacts = await this.contacts();
415
+ const contact = contacts[peerAgentId];
416
+ if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
417
+ if (contact.relationship_state !== "friend") {
418
+ throw new EdgeBookError("not_friend", `Cannot send friend-gated message to relationship_state=${contact.relationship_state}`);
419
+ }
420
+ const grant = await this.findUsableGrant(peerAgentId, scope);
421
+ if (!grant) throw new EdgeBookError("missing_grant", `No active grant for ${scope}`);
422
+ const envelope = await this.signEnvelope({
423
+ type: "privileged_message",
424
+ to_agent_id: peerAgentId,
425
+ relationship_id: relationshipId(identity.agent_id, peerAgentId),
426
+ capability_id: grant.grant_id,
427
+ ref: "",
428
+ transport: "local",
429
+ body
430
+ });
431
+ await appendJsonl(this.file(MESSAGES_FILE), envelope);
432
+ await this.audit("message.send", peerAgentId, { message_id: envelope.message_id, scope });
433
+ return envelope;
434
+ }
435
+ async receivePrivilegedMessage(envelope) {
436
+ await this.verifyEnvelope(envelope);
437
+ if (envelope.type !== "privileged_message") throw new EdgeBookError("wrong_message_type", "Expected privileged_message envelope");
438
+ const contacts = await this.contacts();
439
+ const contact = contacts[envelope.from_agent_id];
440
+ if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${envelope.from_agent_id}`);
441
+ if (contact.relationship_state !== "friend") {
442
+ throw new EdgeBookError("not_friend", `Cannot receive friend-gated message from relationship_state=${contact.relationship_state}`);
443
+ }
444
+ const grants = await this.grants();
445
+ const grant = grants[envelope.capability_id];
446
+ if (!grant || grant.status !== "active" || grant.subject_agent_id !== envelope.from_agent_id || !grant.scopes.includes("message.friend")) {
447
+ throw new EdgeBookError("missing_grant", "Message does not carry an active grant issued to sender");
448
+ }
449
+ await appendJsonl(this.file(INBOX_FILE), envelope);
450
+ await this.audit("message.receive", envelope.from_agent_id, { message_id: envelope.message_id });
451
+ }
452
+ async findUsableGrant(peerAgentId, scope) {
453
+ const identity = await this.identity();
454
+ const grants = await this.grants();
455
+ return Object.values(grants).find(
456
+ (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())
457
+ );
458
+ }
459
+ async signEnvelope(input) {
460
+ const identity = await this.identity();
461
+ const unsigned = {
462
+ message_id: randomId("msg"),
463
+ from_agent_id: identity.agent_id,
464
+ created_at: now(),
465
+ expires_at: new Date(Date.now() + 10 * 60 * 1e3).toISOString(),
466
+ ...input
467
+ };
468
+ return { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
469
+ }
470
+ async verifyEnvelope(envelope) {
471
+ const identity = await this.identity();
472
+ if (envelope.to_agent_id !== identity.agent_id) throw new EdgeBookError("wrong_recipient", "Envelope recipient does not match local identity");
473
+ if (Date.parse(envelope.expires_at) <= Date.now()) throw new EdgeBookError("expired_message", "Message is expired");
474
+ const seen = await readJson(this.file(SEEN_MESSAGES_FILE), []);
475
+ if (seen.includes(envelope.message_id)) throw new EdgeBookError("replay", `Replay detected for ${envelope.message_id}`);
476
+ const contacts = await this.contacts();
477
+ let publicKey = contacts[envelope.from_agent_id]?.public_keys?.[0]?.public_key_pem;
478
+ if (!publicKey && envelope.type === "friend_request") {
479
+ const card = envelope.body.card;
480
+ publicKey = card?.public_keys?.[0]?.public_key_pem;
481
+ }
482
+ if (!publicKey && envelope.type === "friend_response") {
483
+ const card = envelope.body.card;
484
+ publicKey = card?.public_keys?.[0]?.public_key_pem;
485
+ }
486
+ if (!publicKey) throw new EdgeBookError("unknown_key", `Unknown sender key for ${envelope.from_agent_id}`);
487
+ if (!verifyPayload(withoutSignature(envelope), envelope.signature, publicKey)) {
488
+ throw new EdgeBookError("invalid_signature", "Message signature is invalid");
489
+ }
490
+ seen.push(envelope.message_id);
491
+ await writeJson(this.file(SEEN_MESSAGES_FILE), seen);
492
+ }
493
+ async inbox() {
494
+ return readJsonl(this.file(INBOX_FILE));
495
+ }
496
+ async receiveEnvelope(envelope) {
497
+ if (envelope.type === "friend_request") return this.receiveFriendRequest(envelope);
498
+ if (envelope.type === "friend_response") return this.applyFriendResponse(envelope);
499
+ if (envelope.type === "privileged_message") return this.receivePrivilegedMessage(envelope);
500
+ throw new EdgeBookError("unsupported_envelope", `Unsupported envelope type: ${envelope.type}`);
501
+ }
502
+ async audit(action, peerAgentId, details) {
503
+ const audit_id = randomId("audit");
504
+ await appendJsonl(this.file(AUDIT_FILE), {
505
+ audit_id,
506
+ created_at: now(),
507
+ action,
508
+ peer_agent_id: peerAgentId,
509
+ details
510
+ });
511
+ return audit_id;
512
+ }
513
+ async auditEvents() {
514
+ return readJsonl(this.file(AUDIT_FILE));
515
+ }
516
+ async sessions() {
517
+ return readJson(this.file(SESSIONS_FILE), {});
518
+ }
519
+ async saveSessions(sessions) {
520
+ await writeJson(this.file(SESSIONS_FILE), sessions);
521
+ }
522
+ async createSession(input = {}) {
523
+ const identity = await this.identity();
524
+ const stamp = now();
525
+ const session = {
526
+ session_id: randomId("session"),
527
+ owner_agent_id: identity.agent_id,
528
+ created_at: stamp,
529
+ expires_at: new Date(Date.now() + (input.ttlMs ?? 8 * 60 * 60 * 1e3)).toISOString(),
530
+ last_seen_at: stamp,
531
+ auth_method: input.authMethod ?? "local-owner-token",
532
+ csrf_token_hash: randomId("csrf"),
533
+ revoked_at: ""
534
+ };
535
+ const sessions = await this.sessions();
536
+ sessions[session.session_id] = session;
537
+ await this.saveSessions(sessions);
538
+ await this.audit("session.create", identity.agent_id, { session_id: session.session_id, auth_method: session.auth_method });
539
+ return session;
540
+ }
541
+ async requireSession(sessionId) {
542
+ const sessions = await this.sessions();
543
+ const session = sessions[sessionId];
544
+ if (!session) throw new EdgeBookError("unauthorized", "Missing or unknown web session");
545
+ if (session.revoked_at) throw new EdgeBookError("unauthorized", "Web session was revoked");
546
+ if (Date.parse(session.expires_at) <= Date.now()) throw new EdgeBookError("unauthorized", "Web session expired");
547
+ session.last_seen_at = now();
548
+ sessions[sessionId] = session;
549
+ await this.saveSessions(sessions);
550
+ return session;
551
+ }
552
+ async revokeSession(sessionId) {
553
+ const sessions = await this.sessions();
554
+ const session = sessions[sessionId];
555
+ if (!session) return;
556
+ session.revoked_at = now();
557
+ sessions[sessionId] = session;
558
+ await this.saveSessions(sessions);
559
+ await this.audit("session.revoke", session.owner_agent_id, { session_id: sessionId });
560
+ }
561
+ async posts() {
562
+ return readJson(this.file(POSTS_FILE), {});
563
+ }
564
+ async savePosts(posts) {
565
+ await writeJson(this.file(POSTS_FILE), posts);
566
+ }
567
+ async feedItems() {
568
+ return readJson(this.file(FEED_FILE), {});
569
+ }
570
+ async saveFeedItems(items) {
571
+ await writeJson(this.file(FEED_FILE), items);
572
+ }
573
+ async approvals() {
574
+ return readJson(this.file(APPROVALS_FILE), {});
575
+ }
576
+ async saveApprovals(approvals) {
577
+ await writeJson(this.file(APPROVALS_FILE), approvals);
578
+ }
579
+ async contactMutes() {
580
+ return readJson(this.file(CONTACT_MUTES_FILE), {});
581
+ }
582
+ async saveContactMutes(mutes) {
583
+ await writeJson(this.file(CONTACT_MUTES_FILE), mutes);
584
+ }
585
+ async createApproval(input) {
586
+ const identity = await this.identity();
587
+ const approval = {
588
+ approval_id: randomId("approval"),
589
+ type: input.type,
590
+ requested_by_agent_id: input.requestedByAgentId || identity.agent_id,
591
+ object_type: input.objectType,
592
+ object_id: input.objectId,
593
+ summary: input.summary,
594
+ risk_level: input.riskLevel || "medium",
595
+ status: "pending",
596
+ created_at: now(),
597
+ resolved_at: "",
598
+ resolved_by: "",
599
+ audit_refs: []
600
+ };
601
+ const approvals = await this.approvals();
602
+ approvals[approval.approval_id] = approval;
603
+ await this.saveApprovals(approvals);
604
+ approval.audit_refs.push(await this.audit("approval.create", approval.requested_by_agent_id, { approval_id: approval.approval_id, type: approval.type }));
605
+ approvals[approval.approval_id] = approval;
606
+ await this.saveApprovals(approvals);
607
+ return approval;
608
+ }
609
+ async resolveApproval(approvalId, approved) {
610
+ const approvals = await this.approvals();
611
+ const approval = approvals[approvalId];
612
+ if (!approval) throw new EdgeBookError("unknown_approval", `Unknown approval: ${approvalId}`);
613
+ if (approval.status !== "pending") throw new EdgeBookError("approval_resolved", `Approval already ${approval.status}`);
614
+ approval.status = approved ? "approved" : "rejected";
615
+ approval.resolved_at = now();
616
+ approval.resolved_by = "local-owner";
617
+ approvals[approvalId] = approval;
618
+ approval.audit_refs.push(await this.audit("approval.resolve", approval.requested_by_agent_id, { approval_id: approvalId, approved }));
619
+ approvals[approvalId] = approval;
620
+ await this.saveApprovals(approvals);
621
+ return approval;
622
+ }
623
+ async createPost(input) {
624
+ const identity = await this.identity();
625
+ const stamp = now();
626
+ const sourceBasis = input.sourceBasis || "human-authored";
627
+ const requestedStatus = input.status || (sourceBasis === "agent-authored" ? "pending_approval" : "draft");
628
+ const post = {
629
+ post_id: randomId("post"),
630
+ author_agent_id: identity.agent_id,
631
+ human_owner_id: identity.owner_label || identity.agent_id,
632
+ kind: input.kind || "note",
633
+ title: input.title,
634
+ body: input.body,
635
+ tags: input.tags || [],
636
+ visibility: input.visibility || "private",
637
+ source_basis: sourceBasis,
638
+ status: requestedStatus,
639
+ created_at: stamp,
640
+ updated_at: stamp,
641
+ published_at: requestedStatus === "published" ? stamp : "",
642
+ expires_at: input.expiresAt || "",
643
+ approval_ref: "",
644
+ permissions_used: [],
645
+ audit_refs: [],
646
+ reply_or_help_channel: input.replyOrHelpChannel || ""
647
+ };
648
+ const posts = await this.posts();
649
+ posts[post.post_id] = post;
650
+ await this.savePosts(posts);
651
+ if (post.status === "pending_approval") {
652
+ const approval = await this.createApproval({
653
+ type: "publish_post",
654
+ objectType: "post",
655
+ objectId: post.post_id,
656
+ summary: `Publish ${post.visibility} post: ${post.title}`,
657
+ riskLevel: post.visibility === "public_if_enabled" ? "high" : "medium"
658
+ });
659
+ post.approval_ref = approval.approval_id;
660
+ posts[post.post_id] = post;
661
+ await this.savePosts(posts);
662
+ }
663
+ post.audit_refs.push(await this.audit("post.create", identity.agent_id, { post_id: post.post_id, status: post.status, visibility: post.visibility }));
664
+ posts[post.post_id] = post;
665
+ await this.savePosts(posts);
666
+ if (post.status === "published") await this.ensureLocalFeedItem(post);
667
+ return post;
668
+ }
669
+ async approvePost(postId) {
670
+ const posts = await this.posts();
671
+ const post = posts[postId];
672
+ if (!post) throw new EdgeBookError("unknown_post", `Unknown post: ${postId}`);
673
+ if (post.status === "removed") throw new EdgeBookError("removed_post", "Cannot approve a removed post");
674
+ if (post.status === "expired") throw new EdgeBookError("expired_post", "Cannot approve an expired post");
675
+ if (post.approval_ref) await this.resolveApproval(post.approval_ref, true);
676
+ post.status = "published";
677
+ post.source_basis = post.source_basis === "agent-authored" ? "human-approved" : post.source_basis;
678
+ post.updated_at = now();
679
+ post.published_at = post.published_at || post.updated_at;
680
+ posts[postId] = post;
681
+ await this.savePosts(posts);
682
+ await this.ensureLocalFeedItem(post);
683
+ post.audit_refs.push(await this.audit("post.approve", post.author_agent_id, { post_id: postId, visibility: post.visibility }));
684
+ posts[postId] = post;
685
+ await this.savePosts(posts);
686
+ return post;
687
+ }
688
+ async editPost(postId, input) {
689
+ const posts = await this.posts();
690
+ const post = posts[postId];
691
+ if (!post) throw new EdgeBookError("unknown_post", `Unknown post: ${postId}`);
692
+ if (post.status === "removed") throw new EdgeBookError("removed_post", "Cannot edit a removed post");
693
+ if (input.title !== void 0) post.title = input.title;
694
+ if (input.body !== void 0) post.body = input.body;
695
+ if (input.tags !== void 0) post.tags = input.tags;
696
+ if (input.visibility !== void 0) post.visibility = input.visibility;
697
+ post.status = post.status === "published" ? "edited" : post.status;
698
+ post.updated_at = now();
699
+ post.audit_refs.push(await this.audit("post.edit", post.author_agent_id, { post_id: postId }));
700
+ posts[postId] = post;
701
+ await this.savePosts(posts);
702
+ return post;
703
+ }
704
+ async removePost(postId, reason = "removed by local owner") {
705
+ const posts = await this.posts();
706
+ const post = posts[postId];
707
+ if (!post) throw new EdgeBookError("unknown_post", `Unknown post: ${postId}`);
708
+ post.status = "removed";
709
+ post.updated_at = now();
710
+ post.audit_refs.push(await this.audit("post.remove", post.author_agent_id, { post_id: postId, reason }));
711
+ posts[postId] = post;
712
+ await this.savePosts(posts);
713
+ return post;
714
+ }
715
+ async expirePost(postId, reason = "expired") {
716
+ const posts = await this.posts();
717
+ const post = posts[postId];
718
+ if (!post) throw new EdgeBookError("unknown_post", `Unknown post: ${postId}`);
719
+ post.status = "expired";
720
+ post.updated_at = now();
721
+ post.audit_refs.push(await this.audit("post.expire", post.author_agent_id, { post_id: postId, reason }));
722
+ posts[postId] = post;
723
+ await this.savePosts(posts);
724
+ return post;
725
+ }
726
+ async ensureLocalFeedItem(post) {
727
+ const identity = await this.identity();
728
+ const items = await this.feedItems();
729
+ const existing = Object.values(items).find((item2) => item2.post_id === post.post_id && item2.origin_agent_id === identity.agent_id);
730
+ if (existing) return existing;
731
+ const item = {
732
+ feed_item_id: randomId("feed"),
733
+ post_id: post.post_id,
734
+ origin_agent_id: identity.agent_id,
735
+ origin_home: "local",
736
+ relationship_id: "",
737
+ visibility_checked_at: now(),
738
+ delivery_route: "local",
739
+ read_state: "unread",
740
+ hidden: false,
741
+ muted_reason: "",
742
+ received_at: now(),
743
+ audit_refs: []
744
+ };
745
+ item.audit_refs.push(await this.audit("feed.local_add", identity.agent_id, { feed_item_id: item.feed_item_id, post_id: post.post_id }));
746
+ items[item.feed_item_id] = item;
747
+ await this.saveFeedItems(items);
748
+ return item;
749
+ }
750
+ async visiblePostsForPeer(peerAgentId) {
751
+ const identity = await this.identity();
752
+ const contacts = await this.contacts();
753
+ const contact = contacts[peerAgentId];
754
+ if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
755
+ if (contact.relationship_state !== "friend") throw new EdgeBookError("not_friend", `Feed denied for relationship_state=${contact.relationship_state}`);
756
+ const grants = await this.grants();
757
+ const grant = Object.values(grants).find(
758
+ (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())
759
+ );
760
+ if (!grant) throw new EdgeBookError("missing_grant", "No active feed.read.friends grant for peer");
761
+ const posts = Object.values(await this.posts());
762
+ 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));
763
+ }
764
+ async importFeedPosts(peerAgentId, posts, route = "local") {
765
+ const contacts = await this.contacts();
766
+ const contact = contacts[peerAgentId];
767
+ if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
768
+ if (contact.relationship_state !== "friend") throw new EdgeBookError("not_friend", `Cannot import feed from relationship_state=${contact.relationship_state}`);
769
+ const items = await this.feedItems();
770
+ const imported = [];
771
+ for (const post of posts) {
772
+ const existing = Object.values(items).find((item2) => item2.post_id === post.post_id && item2.origin_agent_id === peerAgentId);
773
+ if (existing) {
774
+ imported.push(existing);
775
+ continue;
776
+ }
777
+ const item = {
778
+ feed_item_id: randomId("feed"),
779
+ post_id: post.post_id,
780
+ origin_agent_id: peerAgentId,
781
+ origin_home: route === "relay" ? "relay" : "direct",
782
+ relationship_id: relationshipId((await this.identity()).agent_id, peerAgentId),
783
+ visibility_checked_at: now(),
784
+ delivery_route: route,
785
+ read_state: "unread",
786
+ hidden: false,
787
+ muted_reason: "",
788
+ received_at: now(),
789
+ audit_refs: []
790
+ };
791
+ item.audit_refs.push(await this.audit("feed.import_item", peerAgentId, { feed_item_id: item.feed_item_id, post_id: post.post_id, route }));
792
+ items[item.feed_item_id] = item;
793
+ imported.push(item);
794
+ }
795
+ await this.saveFeedItems(items);
796
+ await this.audit("feed.import", peerAgentId, { count: imported.length, route });
797
+ return imported;
798
+ }
799
+ async markFeedItemRead(feedItemId) {
800
+ const items = await this.feedItems();
801
+ const item = items[feedItemId];
802
+ if (!item) throw new EdgeBookError("unknown_feed_item", `Unknown feed item: ${feedItemId}`);
803
+ item.read_state = "read";
804
+ item.audit_refs.push(await this.audit("feed.mark_read", item.origin_agent_id, { feed_item_id: feedItemId }));
805
+ items[feedItemId] = item;
806
+ await this.saveFeedItems(items);
807
+ return item;
808
+ }
809
+ async hideFeedItem(feedItemId, reason = "") {
810
+ const items = await this.feedItems();
811
+ const item = items[feedItemId];
812
+ if (!item) throw new EdgeBookError("unknown_feed_item", `Unknown feed item: ${feedItemId}`);
813
+ item.hidden = true;
814
+ item.muted_reason = reason;
815
+ item.audit_refs.push(await this.audit("feed.hide", item.origin_agent_id, { feed_item_id: feedItemId, reason }));
816
+ items[feedItemId] = item;
817
+ await this.saveFeedItems(items);
818
+ return item;
819
+ }
820
+ async muteContact(peerAgentId, reason = "") {
821
+ const contacts = await this.contacts();
822
+ if (!contacts[peerAgentId]) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
823
+ const mutes = await this.contactMutes();
824
+ const mute = {
825
+ peer_agent_id: peerAgentId,
826
+ muted_at: now(),
827
+ muted_reason: reason,
828
+ audit_refs: []
829
+ };
830
+ mute.audit_refs.push(await this.audit("contact.mute", peerAgentId, { reason }));
831
+ mutes[peerAgentId] = mute;
832
+ await this.saveContactMutes(mutes);
833
+ return mute;
834
+ }
835
+ async unmuteContact(peerAgentId) {
836
+ const mutes = await this.contactMutes();
837
+ if (!mutes[peerAgentId]) return;
838
+ delete mutes[peerAgentId];
839
+ await this.saveContactMutes(mutes);
840
+ await this.audit("contact.unmute", peerAgentId, {});
841
+ }
842
+ async reviewLocalDataImport(data) {
843
+ const objectCount = (key) => {
844
+ const value = data[key];
845
+ if (!value || typeof value !== "object" || Array.isArray(value)) return 0;
846
+ return Object.keys(value).length;
847
+ };
848
+ const audit = Array.isArray(data.audit) ? data.audit.length : 0;
849
+ return {
850
+ review_only: true,
851
+ activates_remote_endpoints: false,
852
+ counts: {
853
+ contacts: objectCount("contacts"),
854
+ grants: objectCount("grants"),
855
+ sessions: objectCount("sessions"),
856
+ posts: objectCount("posts"),
857
+ feed_items: objectCount("feed_items"),
858
+ approvals: objectCount("approvals"),
859
+ contact_mutes: objectCount("contact_mutes"),
860
+ audit
861
+ }
862
+ };
863
+ }
864
+ async exportLocalData() {
865
+ return {
866
+ identity: await this.identity(),
867
+ contacts: await this.contacts(),
868
+ grants: await this.grants(),
869
+ sessions: await this.sessions(),
870
+ posts: await this.posts(),
871
+ feed_items: await this.feedItems(),
872
+ approvals: await this.approvals(),
873
+ contact_mutes: await this.contactMutes(),
874
+ audit: await this.auditEvents()
875
+ };
876
+ }
877
+ };
878
+ function validateCard(card) {
879
+ if (card.schema !== "openclaw-agent-card/0.1") throw new EdgeBookError("invalid_card", "Unsupported Agent Card schema");
880
+ if (!card.agent_id || !card.public_keys?.[0]?.public_key_pem) throw new EdgeBookError("invalid_card", "Agent Card is missing identity key");
881
+ const expectedId = stableIdFromPublicKey(card.public_keys[0].public_key_pem);
882
+ if (card.agent_id !== expectedId) throw new EdgeBookError("invalid_card", "Agent Card agent_id does not match public key");
883
+ if (!verifyPayload(withoutSignature(card), card.signature, card.public_keys[0].public_key_pem)) {
884
+ throw new EdgeBookError("invalid_card", "Agent Card signature is invalid");
885
+ }
886
+ }
887
+ async function loadCard(cardPathOrUrl) {
888
+ if (/^https?:\/\//.test(cardPathOrUrl)) {
889
+ const response = await fetch(cardPathOrUrl);
890
+ if (!response.ok) throw new EdgeBookError("card_fetch_failed", `Failed to fetch card: ${response.status}`);
891
+ const card2 = await response.json();
892
+ validateCard(card2);
893
+ return card2;
894
+ }
895
+ const filePath = cardPathOrUrl.startsWith("file://") ? new URL(cardPathOrUrl) : path.resolve(cardPathOrUrl);
896
+ const card = JSON.parse(await fs.readFile(filePath, "utf8"));
897
+ validateCard(card);
898
+ return card;
899
+ }
900
+ async function runTwoAgentHarness(baseDir) {
901
+ const root = baseDir || await fs.mkdtemp(path.join(os.tmpdir(), "edge-book-"));
902
+ const alice = new EdgeBookStore({ home: path.join(root, "alice") });
903
+ const bob = new EdgeBookStore({ home: path.join(root, "bob") });
904
+ await alice.init({ handle: "alice.openclaw.local", displayName: "Alice Agent", ownerLabel: "Alice" });
905
+ await bob.init({ handle: "bob.openclaw.local", displayName: "Bob Agent", ownerLabel: "Bob" });
906
+ const aliceCard = await alice.writeCard();
907
+ const bobCard = await bob.writeCard();
908
+ const request = await alice.createFriendRequest(bobCard, "test harness request");
909
+ let deniedBeforeAccept = false;
910
+ try {
911
+ await alice.sendPrivilegedMessage(bobCard.agent_id, { text: "too soon" });
912
+ } catch (error) {
913
+ deniedBeforeAccept = error.code === "not_friend";
914
+ }
915
+ await bob.receiveFriendRequest(request);
916
+ const accept = await bob.acceptFriend(aliceCard.agent_id);
917
+ await alice.applyFriendResponse(accept);
918
+ const message = await alice.sendPrivilegedMessage(bobCard.agent_id, { text: "hello Bob" });
919
+ await bob.receivePrivilegedMessage(message);
920
+ let replayDenied = false;
921
+ try {
922
+ await bob.receivePrivilegedMessage(message);
923
+ } catch (error) {
924
+ replayDenied = error.code === "replay";
925
+ }
926
+ await bob.revoke(aliceCard.agent_id);
927
+ let revokedDenied = false;
928
+ try {
929
+ await bob.receivePrivilegedMessage(await alice.sendPrivilegedMessage(bobCard.agent_id, { text: "after revoke" }));
930
+ } catch (error) {
931
+ revokedDenied = ["not_friend", "replay", "missing_grant"].includes(error.code);
932
+ }
933
+ await bob.setRelationship(aliceCard.agent_id, "friend", "Accept", "reset for block test");
934
+ await bob.block(aliceCard.agent_id);
935
+ let blockedDenied = false;
936
+ try {
937
+ await bob.receivePrivilegedMessage(await alice.sendPrivilegedMessage(bobCard.agent_id, { text: "after block" }));
938
+ } catch (error) {
939
+ blockedDenied = error.code === "not_friend";
940
+ }
941
+ const rotatedBobCard = await bob.writeCard();
942
+ await alice.upsertContactFromCard(rotatedBobCard);
943
+ const aliceContacts = await alice.contacts();
944
+ const bobAudit = await bob.auditEvents();
945
+ const assertions = {
946
+ deniedBeforeAccept,
947
+ replayDenied,
948
+ revokedDenied,
949
+ blockedDenied,
950
+ aliceHasBobContact: Boolean(aliceContacts[bobCard.agent_id]),
951
+ bobAuditWritten: bobAudit.length > 0
952
+ };
953
+ const passed = Object.values(assertions).every(Boolean);
954
+ if (!passed) throw new EdgeBookError("harness_failed", `Harness failed: ${JSON.stringify(assertions)}`);
955
+ return { passed, root, assertions };
956
+ }
957
+
958
+ // src/http.ts
959
+ import fs2 from "fs/promises";
960
+ import http from "http";
961
+ import path2 from "path";
962
+ async function readJsonBody(req) {
963
+ const chunks = [];
964
+ for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
965
+ const text = Buffer.concat(chunks).toString("utf8");
966
+ return JSON.parse(text);
967
+ }
968
+ function headerValue(req, name) {
969
+ const value = req.headers[name.toLowerCase()];
970
+ if (Array.isArray(value)) return value[0] || "";
971
+ return value || "";
972
+ }
973
+ function sendJson(res, status, value) {
974
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
975
+ res.end(`${JSON.stringify(value, null, 2)}
976
+ `);
977
+ }
978
+ function sendHtml(res, value) {
979
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
980
+ res.end(value);
981
+ }
982
+ function sendError(res, error) {
983
+ const status = error instanceof EdgeBookError && error.code === "unauthorized" ? 401 : error instanceof EdgeBookError && error.code === "csrf_required" ? 403 : error instanceof EdgeBookError ? 400 : 500;
984
+ sendJson(res, status, {
985
+ ok: false,
986
+ error: error instanceof Error ? error.message : String(error),
987
+ code: error instanceof EdgeBookError ? error.code : "internal_error"
988
+ });
989
+ }
990
+ function compactPem(pem) {
991
+ return pem.replace(/-----BEGIN [^-]+-----/g, "").replace(/-----END [^-]+-----/g, "").replace(/\s+/g, "");
992
+ }
993
+ function publicIdentity(identity) {
994
+ return {
995
+ did: identity.agent_id,
996
+ handle: identity.handle,
997
+ name: identity.display_name,
998
+ display_name: identity.display_name,
999
+ public_key: compactPem(identity.public_key_pem)
1000
+ };
1001
+ }
1002
+ function publicApiExport(data) {
1003
+ const identity = data.identity;
1004
+ return {
1005
+ ...data,
1006
+ ...identity ? { identity: publicIdentity(identity) } : {},
1007
+ sessions: void 0
1008
+ };
1009
+ }
1010
+ async function publicApprovals(store) {
1011
+ try {
1012
+ const approvals = await store.approvals();
1013
+ if (!approvals || typeof approvals !== "object" || Array.isArray(approvals)) return {};
1014
+ return approvals;
1015
+ } catch {
1016
+ return {};
1017
+ }
1018
+ }
1019
+ function createDefaultApiAdapters(store) {
1020
+ return {
1021
+ store,
1022
+ async requireSession(req) {
1023
+ const sessionId = headerValue(req, "x-openclaw-session");
1024
+ await store.requireSession(sessionId);
1025
+ return sessionId;
1026
+ },
1027
+ async requireCsrf(req, sessionId) {
1028
+ const sessions = await store.sessions();
1029
+ const session = sessions[sessionId];
1030
+ if (!session) throw new EdgeBookError("unauthorized", "Missing or unknown web session");
1031
+ if (headerValue(req, "x-openclaw-csrf") !== session.csrf_token_hash) {
1032
+ throw new EdgeBookError("csrf_required", "Missing or invalid CSRF token");
1033
+ }
1034
+ }
1035
+ };
1036
+ }
1037
+ function methodMutates(method) {
1038
+ return method !== "GET" && method !== "HEAD" && method !== "OPTIONS";
1039
+ }
1040
+ async function requireApiAuth(req, adapters) {
1041
+ const sessionId = await adapters.requireSession(req);
1042
+ if (methodMutates(req.method)) await adapters.requireCsrf(req, sessionId);
1043
+ return sessionId;
1044
+ }
1045
+ async function handleOwnerApi(req, res, url, adapters) {
1046
+ const store = adapters.store;
1047
+ if (req.method === "POST" && url.pathname === "/auth/login") {
1048
+ const body = await readJsonBody(req);
1049
+ const session = await store.createSession({ authMethod: body.auth_method, ttlMs: body.ttl_ms });
1050
+ sendJson(res, 200, { ok: true, session_id: session.session_id, csrf_token: session.csrf_token_hash, expires_at: session.expires_at });
1051
+ return true;
1052
+ }
1053
+ if (req.method === "POST" && url.pathname === "/auth/logout") {
1054
+ const sessionId = await adapters.requireSession(req);
1055
+ await adapters.requireCsrf(req, sessionId);
1056
+ await store.revokeSession(sessionId);
1057
+ sendJson(res, 200, { ok: true });
1058
+ return true;
1059
+ }
1060
+ if (!url.pathname.startsWith("/api/")) return false;
1061
+ await requireApiAuth(req, adapters);
1062
+ if (req.method === "GET" && url.pathname === "/api/me") {
1063
+ sendJson(res, 200, { identity: publicIdentity(await store.identity()) });
1064
+ return true;
1065
+ }
1066
+ if (req.method === "GET" && url.pathname === "/api/contacts") {
1067
+ sendJson(res, 200, { contacts: await store.contacts(), mutes: await store.contactMutes() });
1068
+ return true;
1069
+ }
1070
+ const contactMuteMatch = /^\/api\/contacts\/([^/]+)\/mute$/.exec(url.pathname);
1071
+ if (req.method === "POST" && contactMuteMatch) {
1072
+ const body = await readJsonBody(req);
1073
+ sendJson(res, 200, { mute: await store.muteContact(decodeURIComponent(contactMuteMatch[1]), body.reason || "") });
1074
+ return true;
1075
+ }
1076
+ const messagesMatch = /^\/api\/messages\/([^/]+)$/.exec(url.pathname);
1077
+ if (req.method === "GET" && messagesMatch) {
1078
+ const peerId = decodeURIComponent(messagesMatch[1]);
1079
+ const inbox = (await store.inbox()).filter((message) => message.from_agent_id === peerId || message.to_agent_id === peerId);
1080
+ sendJson(res, 200, { messages: inbox });
1081
+ return true;
1082
+ }
1083
+ const messageSendMatch = /^\/api\/messages\/([^/]+)\/send$/.exec(url.pathname);
1084
+ if (req.method === "POST" && messageSendMatch) {
1085
+ const body = await readJsonBody(req);
1086
+ const envelope = await store.sendPrivilegedMessage(decodeURIComponent(messageSendMatch[1]), { text: body.text || "" });
1087
+ sendJson(res, 200, { envelope });
1088
+ return true;
1089
+ }
1090
+ if (req.method === "GET" && url.pathname === "/api/posts") {
1091
+ sendJson(res, 200, { posts: await store.posts() });
1092
+ return true;
1093
+ }
1094
+ if (req.method === "POST" && url.pathname === "/api/posts") {
1095
+ const body = await readJsonBody(req);
1096
+ const post = await store.createPost({
1097
+ title: body.title,
1098
+ body: body.body,
1099
+ kind: body.kind,
1100
+ tags: body.tags,
1101
+ visibility: body.visibility,
1102
+ sourceBasis: body.source_basis,
1103
+ status: body.status
1104
+ });
1105
+ sendJson(res, 200, { post });
1106
+ return true;
1107
+ }
1108
+ const postActionMatch = /^\/api\/posts\/([^/]+)\/(approve|edit|remove)$/.exec(url.pathname);
1109
+ if (req.method === "POST" && postActionMatch) {
1110
+ const postId = decodeURIComponent(postActionMatch[1]);
1111
+ const action = postActionMatch[2];
1112
+ if (action === "approve") sendJson(res, 200, { post: await store.approvePost(postId) });
1113
+ if (action === "edit") {
1114
+ const body = await readJsonBody(req);
1115
+ sendJson(res, 200, { post: await store.editPost(postId, body) });
1116
+ }
1117
+ if (action === "remove") {
1118
+ const body = await readJsonBody(req);
1119
+ sendJson(res, 200, { post: await store.removePost(postId, body.reason || "removed by local owner") });
1120
+ }
1121
+ return true;
1122
+ }
1123
+ if (req.method === "GET" && url.pathname === "/api/feed") {
1124
+ sendJson(res, 200, { feed_items: await store.feedItems() });
1125
+ return true;
1126
+ }
1127
+ const feedActionMatch = /^\/api\/feed\/([^/]+)\/(read|hide)$/.exec(url.pathname);
1128
+ if (req.method === "POST" && feedActionMatch) {
1129
+ const itemId = decodeURIComponent(feedActionMatch[1]);
1130
+ if (feedActionMatch[2] === "read") sendJson(res, 200, { feed_item: await store.markFeedItemRead(itemId) });
1131
+ if (feedActionMatch[2] === "hide") {
1132
+ const body = await readJsonBody(req);
1133
+ sendJson(res, 200, { feed_item: await store.hideFeedItem(itemId, body.reason || "") });
1134
+ }
1135
+ return true;
1136
+ }
1137
+ if (req.method === "GET" && url.pathname === "/api/approvals") {
1138
+ sendJson(res, 200, { approvals: await publicApprovals(store) });
1139
+ return true;
1140
+ }
1141
+ const approvalResolveMatch = /^\/api\/approvals\/([^/]+)\/resolve$/.exec(url.pathname);
1142
+ if (req.method === "POST" && approvalResolveMatch) {
1143
+ const body = await readJsonBody(req);
1144
+ sendJson(res, 200, { approval: await store.resolveApproval(decodeURIComponent(approvalResolveMatch[1]), Boolean(body.approved)) });
1145
+ return true;
1146
+ }
1147
+ const auditMatch = /^\/api\/audit\/([^/]+)\/([^/]+)$/.exec(url.pathname);
1148
+ if (req.method === "GET" && auditMatch) {
1149
+ const objectId = decodeURIComponent(auditMatch[2]);
1150
+ const audit = (await store.auditEvents()).filter((event) => JSON.stringify(event).includes(objectId));
1151
+ sendJson(res, 200, { audit });
1152
+ return true;
1153
+ }
1154
+ if (req.method === "GET" && url.pathname === "/api/audit") {
1155
+ sendJson(res, 200, { audit: await store.auditEvents() });
1156
+ return true;
1157
+ }
1158
+ if (req.method === "POST" && url.pathname === "/api/export") {
1159
+ sendJson(res, 200, { export: publicApiExport(await store.exportLocalData()) });
1160
+ return true;
1161
+ }
1162
+ if (req.method === "POST" && url.pathname === "/api/import") {
1163
+ const body = await readJsonBody(req);
1164
+ sendJson(res, 200, { review: await store.reviewLocalDataImport(body) });
1165
+ return true;
1166
+ }
1167
+ sendJson(res, 404, { ok: false, error: "not_found" });
1168
+ return true;
1169
+ }
1170
+ function dashboardHtml() {
1171
+ return `<!doctype html>
1172
+ <html lang="en">
1173
+ <head>
1174
+ <meta charset="utf-8">
1175
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1176
+ <title>Edge Book</title>
1177
+ <style>
1178
+ :root {
1179
+ color-scheme: light;
1180
+ --bg: #eef2f4;
1181
+ --panel: #ffffff;
1182
+ --line: #c7d1d6;
1183
+ --text: #1d2a31;
1184
+ --muted: #5f7079;
1185
+ --accent: #116466;
1186
+ --accent-dark: #0a4244;
1187
+ --accent-soft: #dcefee;
1188
+ --active: #1f7a4f;
1189
+ --active-soft: #e5f5ec;
1190
+ --active-line: #a8d5bd;
1191
+ --note: #345995;
1192
+ --note-soft: #e8eef9;
1193
+ --ink: #12343b;
1194
+ --warn: #9a3412;
1195
+ --warn-soft: #fff7ed;
1196
+ --warn-line: #fed7aa;
1197
+ --danger: #b42318;
1198
+ --danger-soft: #fff7f6;
1199
+ --danger-line: #f0b5ae;
1200
+ --neutral-soft: #f4f7f8;
1201
+ }
1202
+ * { box-sizing: border-box; }
1203
+ body {
1204
+ margin: 0;
1205
+ background: var(--bg);
1206
+ color: var(--text);
1207
+ font-family: "Lucida Grande", Tahoma, Verdana, Arial, sans-serif;
1208
+ font-size: 12px;
1209
+ letter-spacing: 0;
1210
+ }
1211
+ .app {
1212
+ min-height: 100vh;
1213
+ display: grid;
1214
+ grid-template-columns: minmax(0, 1fr);
1215
+ grid-template-rows: auto 1fr;
1216
+ }
1217
+ header {
1218
+ position: sticky;
1219
+ top: 0;
1220
+ z-index: 10;
1221
+ display: flex;
1222
+ align-items: center;
1223
+ justify-content: space-between;
1224
+ gap: 16px;
1225
+ padding: 0 16px;
1226
+ border-bottom: 1px solid #07383a;
1227
+ background: linear-gradient(#14797b, #0d5557);
1228
+ color: #ffffff;
1229
+ box-shadow: 0 1px 2px rgb(0 0 0 / 18%);
1230
+ }
1231
+ .top-inner {
1232
+ width: min(1220px, 100%);
1233
+ margin: 0 auto;
1234
+ display: grid;
1235
+ grid-template-columns: 220px minmax(240px, 1fr) auto;
1236
+ gap: 12px;
1237
+ align-items: center;
1238
+ }
1239
+ h1 {
1240
+ margin: 0;
1241
+ font-size: 20px;
1242
+ font-weight: 700;
1243
+ letter-spacing: 0;
1244
+ text-shadow: 0 -1px 0 rgb(0 0 0 / 25%);
1245
+ }
1246
+ .product-mark {
1247
+ display: grid;
1248
+ gap: 2px;
1249
+ min-width: 0;
1250
+ }
1251
+ .product-subtitle {
1252
+ color: #d8f1ef;
1253
+ font-size: 11px;
1254
+ overflow-wrap: anywhere;
1255
+ }
1256
+ h2 {
1257
+ margin: 0;
1258
+ font-size: 13px;
1259
+ font-weight: 700;
1260
+ }
1261
+ h3 { font-size: 14px; }
1262
+ .search {
1263
+ width: 100%;
1264
+ height: 25px;
1265
+ border: 1px solid #07383a;
1266
+ border-radius: 2px;
1267
+ padding: 4px 8px;
1268
+ font: inherit;
1269
+ background: #f7fbfb;
1270
+ color: var(--text);
1271
+ box-shadow: inset 0 1px 1px rgb(0 0 0 / 12%);
1272
+ }
1273
+ .status {
1274
+ display: flex;
1275
+ align-items: center;
1276
+ flex-wrap: wrap;
1277
+ gap: 12px;
1278
+ color: #eef8f8;
1279
+ min-width: 0;
1280
+ }
1281
+ .badge {
1282
+ border: 1px solid var(--line);
1283
+ border-radius: 3px;
1284
+ padding: 4px 7px;
1285
+ background: #f9fafb;
1286
+ color: var(--muted);
1287
+ white-space: nowrap;
1288
+ }
1289
+ .badge.owned {
1290
+ border-color: var(--active-line);
1291
+ background: var(--active-soft);
1292
+ color: var(--active);
1293
+ }
1294
+ .badge.attention {
1295
+ border-color: var(--warn-line);
1296
+ background: var(--warn-soft);
1297
+ color: var(--warn);
1298
+ }
1299
+ .badge.risk {
1300
+ border-color: var(--danger-line);
1301
+ background: var(--danger-soft);
1302
+ color: var(--danger);
1303
+ }
1304
+ .badge.neutral {
1305
+ border-color: var(--line);
1306
+ background: var(--neutral-soft);
1307
+ color: var(--muted);
1308
+ }
1309
+ header .badge {
1310
+ border-color: #0a4244;
1311
+ background: rgb(255 255 255 / 14%);
1312
+ color: #ffffff;
1313
+ }
1314
+ .page {
1315
+ width: min(1220px, 100%);
1316
+ margin: 0 auto;
1317
+ display: grid;
1318
+ grid-template-columns: 170px minmax(520px, 1fr) 250px;
1319
+ gap: 12px;
1320
+ padding: 14px 12px 28px;
1321
+ }
1322
+ nav, aside {
1323
+ align-self: start;
1324
+ position: sticky;
1325
+ top: 56px;
1326
+ }
1327
+ nav {
1328
+ padding: 0;
1329
+ }
1330
+ nav button {
1331
+ width: 100%;
1332
+ display: flex;
1333
+ justify-content: space-between;
1334
+ align-items: center;
1335
+ margin-bottom: 2px;
1336
+ border: 1px solid transparent;
1337
+ border-radius: 2px;
1338
+ background: transparent;
1339
+ color: var(--text);
1340
+ padding: 5px 6px;
1341
+ text-align: left;
1342
+ cursor: pointer;
1343
+ font-weight: 700;
1344
+ }
1345
+ nav button span { color: var(--muted); font-weight: 400; }
1346
+ nav button:hover { background: #e2ebef; }
1347
+ nav button.active {
1348
+ border-color: #b7c5cc;
1349
+ background: #dbe7eb;
1350
+ color: var(--accent-dark);
1351
+ }
1352
+ main {
1353
+ min-width: 0;
1354
+ }
1355
+ aside {
1356
+ background: #f8fafb;
1357
+ border: 1px solid var(--line);
1358
+ padding: 10px;
1359
+ min-width: 0;
1360
+ color: #40535c;
1361
+ }
1362
+ .toolbar {
1363
+ display: flex;
1364
+ align-items: center;
1365
+ justify-content: space-between;
1366
+ gap: 12px;
1367
+ border: 1px solid var(--line);
1368
+ border-bottom: 0;
1369
+ background: #f7f9fa;
1370
+ padding: 7px 9px;
1371
+ }
1372
+ .summary-grid {
1373
+ display: grid;
1374
+ grid-template-columns: repeat(5, minmax(0, 1fr));
1375
+ gap: 8px;
1376
+ margin-bottom: 10px;
1377
+ }
1378
+ .summary-card {
1379
+ min-height: 62px;
1380
+ border: 1px solid var(--line);
1381
+ border-radius: 4px;
1382
+ background: var(--panel);
1383
+ padding: 8px;
1384
+ display: grid;
1385
+ align-content: space-between;
1386
+ gap: 5px;
1387
+ }
1388
+ .summary-label {
1389
+ color: var(--muted);
1390
+ font-size: 11px;
1391
+ line-height: 1.25;
1392
+ overflow-wrap: anywhere;
1393
+ }
1394
+ .summary-value {
1395
+ font-size: 19px;
1396
+ font-weight: 700;
1397
+ color: var(--ink);
1398
+ }
1399
+ .summary-card.warn { background: var(--warn-soft) !important; }
1400
+ .summary-card.risk { background: var(--danger-soft) !important; }
1401
+ .summary-card.active { background: var(--active-soft); border-color: #b5ddc9; }
1402
+ .list {
1403
+ display: grid;
1404
+ gap: 10px;
1405
+ }
1406
+ .item {
1407
+ border: 1px solid var(--line);
1408
+ border-radius: 3px;
1409
+ background: var(--panel);
1410
+ padding: 10px 12px;
1411
+ box-shadow: 0 1px 1px rgb(0 0 0 / 4%);
1412
+ display: grid;
1413
+ gap: 8px;
1414
+ }
1415
+ .item[tabindex="0"] { cursor: pointer; }
1416
+ .item[tabindex="0"]:hover {
1417
+ border-color: #8fbec0;
1418
+ box-shadow: 0 1px 3px rgb(0 0 0 / 10%);
1419
+ }
1420
+ .item-head {
1421
+ display: flex;
1422
+ justify-content: space-between;
1423
+ gap: 10px;
1424
+ align-items: start;
1425
+ }
1426
+ .item h3 {
1427
+ margin: 0 0 6px;
1428
+ color: var(--accent-dark);
1429
+ font-size: 14px;
1430
+ line-height: 1.25;
1431
+ }
1432
+ .item-title-row {
1433
+ display: flex;
1434
+ align-items: start;
1435
+ gap: 8px;
1436
+ min-width: 0;
1437
+ }
1438
+ .item-body {
1439
+ color: var(--text);
1440
+ line-height: 1.45;
1441
+ }
1442
+ .item-time {
1443
+ color: var(--muted);
1444
+ font-size: 11px;
1445
+ white-space: nowrap;
1446
+ }
1447
+ .inspect-tag {
1448
+ color: var(--accent-dark);
1449
+ border: 1px solid #bfd8d9;
1450
+ background: #f1f8f8;
1451
+ border-radius: 2px;
1452
+ padding: 2px 5px;
1453
+ font-size: 11px;
1454
+ white-space: nowrap;
1455
+ }
1456
+ .meta {
1457
+ display: flex;
1458
+ flex-wrap: wrap;
1459
+ gap: 6px;
1460
+ color: var(--muted);
1461
+ font-size: 11px;
1462
+ margin-top: 8px;
1463
+ }
1464
+ .trust-strip {
1465
+ display: grid;
1466
+ grid-template-columns: repeat(4, minmax(0, 1fr));
1467
+ gap: 6px;
1468
+ margin-top: 2px;
1469
+ }
1470
+ .trust-pill {
1471
+ border: 1px solid var(--line);
1472
+ border-radius: 3px;
1473
+ background: #fbfcfd;
1474
+ padding: 5px 6px;
1475
+ min-width: 0;
1476
+ }
1477
+ .trust-label {
1478
+ display: block;
1479
+ color: var(--muted);
1480
+ font-size: 9px;
1481
+ font-weight: 400;
1482
+ text-transform: uppercase;
1483
+ }
1484
+ .trust-value {
1485
+ display: block;
1486
+ overflow-wrap: anywhere;
1487
+ font-weight: 700;
1488
+ font-size: 12px;
1489
+ color: var(--ink);
1490
+ }
1491
+ .meta span {
1492
+ border: 1px solid var(--line);
1493
+ border-radius: 2px;
1494
+ padding: 3px 5px;
1495
+ background: #fbfcfd;
1496
+ }
1497
+ .actions {
1498
+ display: flex;
1499
+ flex-wrap: wrap;
1500
+ gap: 6px;
1501
+ margin-top: 10px;
1502
+ }
1503
+ .view-copy {
1504
+ color: var(--muted);
1505
+ font-size: 11px;
1506
+ }
1507
+ .detail-panel {
1508
+ border: 1px solid var(--line);
1509
+ border-bottom: 0;
1510
+ background: #f7f9fa;
1511
+ padding: 9px;
1512
+ display: grid;
1513
+ gap: 6px;
1514
+ }
1515
+ .detail-title {
1516
+ font-weight: 700;
1517
+ color: var(--ink);
1518
+ overflow-wrap: anywhere;
1519
+ }
1520
+ .detail-grid {
1521
+ display: grid;
1522
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1523
+ gap: 6px;
1524
+ }
1525
+ .detail-grid div {
1526
+ border: 1px solid var(--line);
1527
+ background: #fff;
1528
+ padding: 5px;
1529
+ min-width: 0;
1530
+ overflow-wrap: anywhere;
1531
+ }
1532
+ .actions button, .composer button {
1533
+ border: 1px solid var(--line);
1534
+ border-radius: 2px;
1535
+ background: #f3f6f7;
1536
+ color: var(--text);
1537
+ padding: 5px 8px;
1538
+ cursor: pointer;
1539
+ font: inherit;
1540
+ font-weight: 700;
1541
+ }
1542
+ .actions button:hover, .composer button:hover {
1543
+ border-color: #9cc9ca;
1544
+ background: #eef7f7;
1545
+ }
1546
+ .actions button.danger {
1547
+ border-color: var(--danger-line);
1548
+ background: var(--danger-soft);
1549
+ color: var(--danger);
1550
+ }
1551
+ .actions button.primary, .composer button.primary, .empty-actions button.primary {
1552
+ border-color: var(--active-line);
1553
+ background: var(--active-soft);
1554
+ color: var(--active);
1555
+ }
1556
+ .composer {
1557
+ border: 1px solid var(--line);
1558
+ border-radius: 3px;
1559
+ background: var(--panel);
1560
+ padding: 10px;
1561
+ margin-bottom: 10px;
1562
+ display: grid;
1563
+ gap: 8px;
1564
+ }
1565
+ .composer input, .composer textarea, .composer select {
1566
+ width: 100%;
1567
+ border: 1px solid var(--line);
1568
+ border-radius: 2px;
1569
+ padding: 6px;
1570
+ font: inherit;
1571
+ background: #ffffff;
1572
+ color: var(--text);
1573
+ }
1574
+ .composer textarea {
1575
+ min-height: 72px;
1576
+ resize: vertical;
1577
+ }
1578
+ .empty, .loading, .error {
1579
+ border: 1px dashed var(--line);
1580
+ border-radius: 3px;
1581
+ background: var(--panel);
1582
+ color: var(--muted);
1583
+ padding: 16px;
1584
+ }
1585
+ .empty-actions {
1586
+ display: flex;
1587
+ flex-wrap: wrap;
1588
+ gap: 8px;
1589
+ margin-top: 12px;
1590
+ }
1591
+ .empty-actions button {
1592
+ border: 1px solid var(--line);
1593
+ border-radius: 2px;
1594
+ background: #f3f6f7;
1595
+ color: var(--text);
1596
+ padding: 5px 8px;
1597
+ cursor: pointer;
1598
+ font: inherit;
1599
+ font-weight: 700;
1600
+ }
1601
+ .skeleton {
1602
+ display: grid;
1603
+ gap: 8px;
1604
+ }
1605
+ .skeleton-line {
1606
+ height: 10px;
1607
+ border-radius: 2px;
1608
+ background: linear-gradient(90deg, #e7eef1, #f7fafb, #e7eef1);
1609
+ }
1610
+ .skeleton-line.short { width: 48%; }
1611
+ .error { border-color: #f3b4ad; color: var(--danger); }
1612
+ .risk {
1613
+ color: var(--danger);
1614
+ border-color: var(--danger-line) !important;
1615
+ background: var(--danger-soft) !important;
1616
+ }
1617
+ .warn {
1618
+ color: var(--warn);
1619
+ border-color: var(--warn-line) !important;
1620
+ background: var(--warn-soft) !important;
1621
+ }
1622
+ pre {
1623
+ margin: 0;
1624
+ white-space: pre-wrap;
1625
+ word-break: break-word;
1626
+ font-size: 11px;
1627
+ line-height: 1.4;
1628
+ }
1629
+ .module {
1630
+ border: 1px solid var(--line);
1631
+ background: var(--panel);
1632
+ margin-bottom: 10px;
1633
+ padding: 9px;
1634
+ }
1635
+ .module h2 { margin-bottom: 7px; }
1636
+ .owner-card {
1637
+ display: grid;
1638
+ grid-template-columns: 36px minmax(0, 1fr);
1639
+ gap: 8px;
1640
+ align-items: center;
1641
+ margin-bottom: 10px;
1642
+ padding: 6px;
1643
+ }
1644
+ .avatar {
1645
+ width: 36px;
1646
+ height: 36px;
1647
+ border-radius: 2px;
1648
+ display: grid;
1649
+ place-items: center;
1650
+ background: var(--accent);
1651
+ color: #ffffff;
1652
+ font-weight: 700;
1653
+ border: 1px solid var(--accent-dark);
1654
+ }
1655
+ .avatar.mini {
1656
+ width: 30px;
1657
+ height: 30px;
1658
+ font-size: 11px;
1659
+ background: var(--note);
1660
+ border-color: #274472;
1661
+ flex: 0 0 auto;
1662
+ }
1663
+ .owner-name {
1664
+ font-weight: 700;
1665
+ overflow-wrap: anywhere;
1666
+ }
1667
+ .owner-id {
1668
+ color: var(--muted);
1669
+ font-size: 11px;
1670
+ overflow-wrap: anywhere;
1671
+ }
1672
+ .queue {
1673
+ display: grid;
1674
+ gap: 6px;
1675
+ }
1676
+ .queue-row {
1677
+ display: flex;
1678
+ align-items: center;
1679
+ justify-content: space-between;
1680
+ gap: 8px;
1681
+ border-bottom: 1px solid #e4ebef;
1682
+ padding-bottom: 5px;
1683
+ }
1684
+ .queue-row:last-child { border-bottom: 0; padding-bottom: 0; }
1685
+ .queue-row strong { overflow-wrap: anywhere; }
1686
+ .profile-panel {
1687
+ border: 1px solid var(--line);
1688
+ background: var(--panel);
1689
+ margin-bottom: 10px;
1690
+ padding: 10px;
1691
+ display: grid;
1692
+ gap: 8px;
1693
+ }
1694
+ .profile-head {
1695
+ display: grid;
1696
+ grid-template-columns: 52px minmax(0, 1fr);
1697
+ gap: 10px;
1698
+ align-items: center;
1699
+ }
1700
+ .profile-head .avatar {
1701
+ width: 52px;
1702
+ height: 52px;
1703
+ font-size: 16px;
1704
+ }
1705
+ .profile-name {
1706
+ font-size: 16px;
1707
+ font-weight: 700;
1708
+ color: var(--ink);
1709
+ overflow-wrap: anywhere;
1710
+ }
1711
+ .profile-meta {
1712
+ color: var(--muted);
1713
+ overflow-wrap: anywhere;
1714
+ }
1715
+ .activity-list {
1716
+ display: grid;
1717
+ gap: 6px;
1718
+ }
1719
+ .activity-row {
1720
+ border-bottom: 1px solid #e4ebef;
1721
+ padding-bottom: 6px;
1722
+ display: grid;
1723
+ gap: 2px;
1724
+ cursor: pointer;
1725
+ }
1726
+ .activity-row:last-child { border-bottom: 0; padding-bottom: 0; }
1727
+ .activity-type {
1728
+ color: var(--ink);
1729
+ font-weight: 700;
1730
+ overflow-wrap: anywhere;
1731
+ }
1732
+ .activity-note {
1733
+ color: var(--muted);
1734
+ overflow-wrap: anywhere;
1735
+ }
1736
+ @media (max-width: 920px) {
1737
+ header { position: static; height: auto; min-height: 54px; }
1738
+ .top-inner {
1739
+ grid-template-columns: 1fr;
1740
+ padding: 8px 0 10px;
1741
+ }
1742
+ .status {
1743
+ display: grid;
1744
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1745
+ gap: 8px;
1746
+ }
1747
+ header .badge {
1748
+ min-width: 0;
1749
+ text-align: center;
1750
+ white-space: normal;
1751
+ }
1752
+ .page {
1753
+ grid-template-columns: 1fr;
1754
+ padding-top: 12px;
1755
+ }
1756
+ nav, aside {
1757
+ position: static;
1758
+ }
1759
+ nav {
1760
+ display: grid;
1761
+ grid-template-columns: 1fr;
1762
+ gap: 6px;
1763
+ }
1764
+ nav button { margin: 0; }
1765
+ .summary-grid {
1766
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1767
+ }
1768
+ .trust-strip,
1769
+ .detail-grid {
1770
+ grid-template-columns: 1fr;
1771
+ }
1772
+ }
1773
+ </style>
1774
+ </head>
1775
+ <body>
1776
+ <div class="app">
1777
+ <header>
1778
+ <div class="top-inner">
1779
+ <div class="product-mark">
1780
+ <h1>Edge Book</h1>
1781
+ <div class="product-subtitle">Local-first agent social workspace</div>
1782
+ </div>
1783
+ <input class="search" aria-label="Search local Edge Book data" placeholder="Search local friends, posts, messages">
1784
+ <div class="status">
1785
+ <span id="sessionBadge" class="badge">Local session</span>
1786
+ </div>
1787
+ </div>
1788
+ </header>
1789
+ <div class="page">
1790
+ <nav aria-label="Edge Book views">
1791
+ <div class="owner-card">
1792
+ <div class="avatar">EB</div>
1793
+ <div>
1794
+ <div id="ownerName" class="owner-name">Connecting...</div>
1795
+ <div id="ownerShort" class="owner-id">local owner session</div>
1796
+ </div>
1797
+ </div>
1798
+ <button data-view="profile">Profile <span id="profileCount">Owner</span></button>
1799
+ <button data-view="feed" class="active">Feed <span id="feedCount">Visible 0</span></button>
1800
+ <button data-view="contacts">Friends <span id="contactCount">Friends 0</span></button>
1801
+ <button data-view="messages">Messages <span id="messageCount">Total 0</span></button>
1802
+ <button data-view="posts">Post history <span id="postCount">Drafts 0</span></button>
1803
+ <button data-view="approvals">Approvals <span id="approvalCount">Pending 0</span></button>
1804
+ <button data-view="activity">Activity Log <span id="activityCount">Events 0</span></button>
1805
+ <button data-view="inspector">Inspector <span>Details</span></button>
1806
+ </nav>
1807
+ <main>
1808
+ <section id="summaryGrid" class="summary-grid" aria-label="Edge Book operational summary">
1809
+ <div class="summary-card active"><div class="summary-label">Visible feed</div><div id="summaryFeed" class="summary-value">0</div></div>
1810
+ <div class="summary-card"><div class="summary-label">Friends</div><div id="summaryFriends" class="summary-value">0</div></div>
1811
+ <div class="summary-card"><div class="summary-label">Messages</div><div id="summaryMessages" class="summary-value">0</div></div>
1812
+ <div class="summary-card warn"><div class="summary-label">Pending approvals</div><div id="summaryApprovals" class="summary-value">0</div></div>
1813
+ <div class="summary-card"><div class="summary-label">Drafts and pending posts</div><div id="summaryDrafts" class="summary-value">0</div></div>
1814
+ </section>
1815
+ <div class="toolbar">
1816
+ <div>
1817
+ <h2 id="viewTitle">Feed</h2>
1818
+ <div id="viewCopy" class="view-copy">Relationship-gated updates with delivery and provenance context.</div>
1819
+ </div>
1820
+ <span id="viewState" class="badge">Loading</span>
1821
+ </div>
1822
+ <section id="content" class="list">
1823
+ <div class="loading">Loading local Edge Book data...</div>
1824
+ </section>
1825
+ </main>
1826
+ <aside>
1827
+ <div class="module">
1828
+ <h2>Owner Console</h2>
1829
+ <div id="owner" class="owner-id">Connecting to local owner session...</div>
1830
+ </div>
1831
+ <div class="module">
1832
+ <h2>Attention Queue</h2>
1833
+ <div id="attentionQueue" class="queue">
1834
+ <div class="queue-row"><strong>Loading</strong><span class="badge">Local</span></div>
1835
+ </div>
1836
+ </div>
1837
+ <div class="module">
1838
+ <h2>Recent Activity</h2>
1839
+ <div id="activityRail" class="activity-list">
1840
+ <div class="activity-row"><div class="activity-type">Loading</div><div class="activity-note">Local audit trail</div></div>
1841
+ </div>
1842
+ </div>
1843
+ <div class="toolbar">
1844
+ <h2>Inspector</h2>
1845
+ <span class="badge">Inspect</span>
1846
+ </div>
1847
+ <div id="inspectorSummary" class="detail-panel">
1848
+ <div class="detail-title">No object selected</div>
1849
+ <div class="view-copy">Click a feed item, contact, message, post, or approval to inspect decision context.</div>
1850
+ </div>
1851
+ <pre id="inspector">Select an item to inspect source basis, visibility, grants, approvals, and audit refs.</pre>
1852
+ </aside>
1853
+ </div>
1854
+ </div>
1855
+ <script>
1856
+ const state = {
1857
+ view: "feed",
1858
+ sessionId: "",
1859
+ csrf: "",
1860
+ me: null,
1861
+ contacts: {},
1862
+ mutes: {},
1863
+ posts: {},
1864
+ feedItems: {},
1865
+ approvals: {},
1866
+ messages: [],
1867
+ audit: []
1868
+ };
1869
+ const titleByView = {
1870
+ profile: "Profile",
1871
+ feed: "Feed",
1872
+ contacts: "Friends and contacts",
1873
+ messages: "Messages",
1874
+ posts: "Post history",
1875
+ approvals: "Approvals",
1876
+ activity: "Activity Log",
1877
+ inspector: "Inspector"
1878
+ };
1879
+ const copyByView = {
1880
+ profile: "Owner identity, local session, relationship posture, and working history.",
1881
+ feed: "Relationship-gated updates with delivery and provenance context.",
1882
+ contacts: "Relationship state, grants, endpoints, and local moderation posture.",
1883
+ messages: "Friend-gated envelopes grouped by peer context.",
1884
+ posts: "Drafts, approvals, visibility, source basis, and removal state.",
1885
+ approvals: "Human gates for agent-authored changes and risk-bearing actions.",
1886
+ activity: "Owner-only audit trail for local decisions, relationship changes, posts, and messages.",
1887
+ inspector: "Readable decision summary plus detailed local evidence."
1888
+ };
1889
+ function headers(extra = {}) {
1890
+ return { "content-type": "application/json", "x-openclaw-session": state.sessionId, "x-openclaw-csrf": state.csrf, ...extra };
1891
+ }
1892
+ async function api(path, init = {}) {
1893
+ const response = await fetch(path, { ...init, headers: headers(init.headers || {}) });
1894
+ const body = await response.json();
1895
+ if (!response.ok) throw new Error(body.code || body.error || "request_failed");
1896
+ return body;
1897
+ }
1898
+ function values(obj) { return Object.values(obj || {}); }
1899
+ function setText(id, text) { document.getElementById(id).textContent = text; }
1900
+ function setInspector(value) {
1901
+ const summary = summarizePayload(value);
1902
+ document.getElementById("inspectorSummary").innerHTML = '<div class="detail-title">' + escapeHtml(summary.title) + '</div><div class="detail-grid">' +
1903
+ summary.facts.map((fact) => '<div><span class="trust-label">' + escapeHtml(fact[0]) + '</span><span class="trust-value">' + escapeHtml(fact[1]) + '</span></div>').join("") +
1904
+ '</div>';
1905
+ setText("inspector", JSON.stringify(value, null, 2));
1906
+ }
1907
+ function meta(parts) {
1908
+ return '<div class="meta">' + parts.filter(Boolean).map((part) => '<span>' + escapeHtml(part) + '</span>').join("") + '</div>';
1909
+ }
1910
+ function skeleton(label = "Loading local Edge Book data...") {
1911
+ return '<div class="loading"><div>' + escapeHtml(label) + '</div><div class="skeleton" aria-hidden="true"><div class="skeleton-line"></div><div class="skeleton-line"></div><div class="skeleton-line short"></div></div></div>';
1912
+ }
1913
+ function escapeHtml(value) {
1914
+ return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char]));
1915
+ }
1916
+ function action(label, name, id, variant = "") {
1917
+ return '<button type="button" class="' + escapeHtml(variant) + '" data-action="' + escapeHtml(name) + '" data-id="' + escapeHtml(id) + '">' + escapeHtml(label) + '</button>';
1918
+ }
1919
+ function trustStrip(entries) {
1920
+ return '<div class="trust-strip">' + entries.map((entry) => '<div class="trust-pill"><span class="trust-label">' + escapeHtml(entry[0]) + '</span><span class="trust-value">' + escapeHtml(entry[1]) + '</span></div>').join("") + '</div>';
1921
+ }
1922
+ function item(title, body, facts, payload, classes = "", actions = "", trust = [], timestamp = "", avatar = "") {
1923
+ const factHtml = facts.filter(Boolean).length ? meta(facts) : "";
1924
+ const timeHtml = timestamp ? '<span class="item-time">' + escapeHtml(timestamp) + '</span>' : "";
1925
+ const avatarHtml = avatar ? '<span class="avatar mini contact-avatar">' + escapeHtml(avatar) + '</span>' : "";
1926
+ return '<article class="item ' + classes + '" tabindex="0" data-payload="' + encodeURIComponent(JSON.stringify(payload)) + '"><div class="item-head"><div class="item-title-row">' + avatarHtml + '<div><h3>' + escapeHtml(title) + '</h3>' + timeHtml + '</div></div><span class="inspect-tag">Inspect</span></div><div class="item-body">' + escapeHtml(body || "") + '</div>' + (trust.length ? trustStrip(trust) : "") + factHtml + (actions ? '<div class="actions">' + actions + '</div>' : '') + '</article>';
1927
+ }
1928
+ function renderEmpty(label) {
1929
+ return '<div class="empty">' + label + '</div>';
1930
+ }
1931
+ function renderFeedEmpty() {
1932
+ return '<div class="empty">Nothing yet.<div class="empty-actions"><button type="button" class="primary" data-view-target="posts">Compose</button><button type="button" data-view-target="contacts">Invite a friend</button></div></div>';
1933
+ }
1934
+ function shortId(value) {
1935
+ const text = String(value || "");
1936
+ return text.length > 18 ? text.slice(0, 18) + "..." : text;
1937
+ }
1938
+ function labelize(value) {
1939
+ return String(value || "n/a").replace(/_/g, " ");
1940
+ }
1941
+ function publicOwnerLabel() {
1942
+ return state.me?.display_name || "Local owner";
1943
+ }
1944
+ function initials(label) {
1945
+ const words = String(label || "EB").replace(/[^a-z0-9 ]/gi, " ").trim().split(/s+/).filter(Boolean);
1946
+ const text = (words[0]?.[0] || "E") + (words[1]?.[0] || words[0]?.[1] || "B");
1947
+ return text.toUpperCase();
1948
+ }
1949
+ function contactFor(agentId) {
1950
+ return state.contacts[agentId] || {};
1951
+ }
1952
+ function agentLabel(agentId) {
1953
+ if (!agentId) return "Local owner";
1954
+ if ((state.me?.did || state.me?.agent_id) === agentId) return publicOwnerLabel();
1955
+ const contact = contactFor(agentId);
1956
+ return contact.display_name || contact.aliases?.[0] || shortId(agentId);
1957
+ }
1958
+ function peerEndpointLabel(contact) {
1959
+ const endpoints = contact.known_endpoints || [];
1960
+ if (!endpoints.length) return "No endpoint published";
1961
+ return endpoints.map((endpoint) => labelize(endpoint.mode)).join(", ");
1962
+ }
1963
+ function timeLabel(value) {
1964
+ if (!value) return "n/a";
1965
+ const date = new Date(value);
1966
+ if (Number.isNaN(date.getTime())) return String(value);
1967
+ return date.toLocaleString([], { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
1968
+ }
1969
+ function pendingApprovals() { return values(state.approvals).filter((approval) => approval.status === "pending"); }
1970
+ function visibleFeedItems() { return values(state.feedItems).filter((feed) => !feed.hidden); }
1971
+ function friendContacts() { return values(state.contacts).filter((contact) => contact.relationship_state === "friend"); }
1972
+ function blockedContacts() { return values(state.contacts).filter((contact) => contact.relationship_state === "blocked"); }
1973
+ function draftPosts() { return values(state.posts).filter((post) => post.status === "draft" || post.status === "pending_approval"); }
1974
+ function renderAttentionQueue() {
1975
+ const rows = [
1976
+ ["Approvals", pendingApprovals().length, pendingApprovals().length ? "attention" : "owned"],
1977
+ ["Unread feed", values(state.feedItems).filter((feed) => feed.read_state !== "read" && !feed.hidden).length, "neutral"],
1978
+ ["Blocked peers", blockedContacts().length, blockedContacts().length ? "risk" : "owned"],
1979
+ ["Draft/pending posts", draftPosts().length, draftPosts().length ? "attention" : "neutral"]
1980
+ ];
1981
+ document.getElementById("attentionQueue").innerHTML = rows.map((row) => '<div class="queue-row"><strong>' + escapeHtml(row[0]) + '</strong><span class="badge ' + escapeHtml(row[2]) + '">' + escapeHtml(row[1]) + '</span></div>').join("");
1982
+ }
1983
+ function renderActivityRail() {
1984
+ const recent = [...state.audit].reverse().slice(0, 6);
1985
+ document.getElementById("activityRail").innerHTML = recent.map((event) => '<div class="activity-row" tabindex="0" data-payload="' + encodeURIComponent(JSON.stringify(event)) + '"><div class="activity-type">' + escapeHtml(labelize(event.type || "event")) + '</div><div class="activity-note">' + escapeHtml(agentLabel(event.peer_agent_id) + " | " + timeLabel(event.created_at)) + '</div></div>').join("") || '<div class="activity-row"><div class="activity-type">No activity yet</div><div class="activity-note">Audit events will appear here.</div></div>';
1986
+ document.querySelectorAll("#activityRail [data-payload]").forEach((node) => {
1987
+ node.addEventListener("click", () => setInspector(JSON.parse(decodeURIComponent(node.dataset.payload))));
1988
+ node.addEventListener("keydown", (event) => { if (event.key === "Enter") node.click(); });
1989
+ });
1990
+ }
1991
+ function summarizePayload(value) {
1992
+ const data = value || {};
1993
+ const feed = data.feed || data;
1994
+ const post = data.post || data;
1995
+ const title = post.title || data.summary || data.display_name || labelize(data.type) || agentLabel(data.peer_agent_id) || "Selected object";
1996
+ const facts = [
1997
+ ["relationship", labelize(data.relationship_state || "local owner")],
1998
+ ["visibility", labelize(post.visibility || feed.visibility || "n/a")],
1999
+ ["source", labelize(post.source_basis || data.source_basis || data.transport || data.delivery_route || feed.delivery_route || "local")],
2000
+ ["approval", labelize(data.status || post.status || data.risk_level || "n/a")],
2001
+ ["audit evidence", (data.audit_refs || post.audit_refs || feed.audit_refs || []).length || (data.audit_id ? 1 : 0)]
2002
+ ];
2003
+ return { title, facts };
2004
+ }
2005
+ function render() {
2006
+ document.querySelectorAll("nav button").forEach((button) => button.classList.toggle("active", button.dataset.view === state.view));
2007
+ setText("viewTitle", titleByView[state.view]);
2008
+ setText("viewCopy", copyByView[state.view]);
2009
+ setText("viewState", "Current");
2010
+ setText("feedCount", "Visible " + visibleFeedItems().length);
2011
+ setText("contactCount", "Friends " + friendContacts().length);
2012
+ setText("postCount", "Drafts " + draftPosts().length);
2013
+ setText("approvalCount", "Pending " + pendingApprovals().length);
2014
+ setText("activityCount", "Events " + state.audit.length);
2015
+ setText("messageCount", "Total " + state.messages.length);
2016
+ setText("summaryFeed", visibleFeedItems().length);
2017
+ setText("summaryFriends", friendContacts().length);
2018
+ setText("summaryMessages", state.messages.length);
2019
+ setText("summaryApprovals", pendingApprovals().length);
2020
+ setText("summaryDrafts", draftPosts().length);
2021
+ renderAttentionQueue();
2022
+ renderActivityRail();
2023
+ const content = document.getElementById("content");
2024
+ let html = "";
2025
+ if (state.view === "profile") {
2026
+ html = '<section class="profile-panel"><div class="profile-head"><div class="avatar">EB</div><div><div class="profile-name">' + escapeHtml(publicOwnerLabel()) + '</div><div class="profile-meta">Local owner session</div></div></div>' +
2027
+ trustStrip([
2028
+ ["session", "local active"],
2029
+ ["friends", friendContacts().length],
2030
+ ["pending approvals", pendingApprovals().length],
2031
+ ["activity events", state.audit.length]
2032
+ ]) +
2033
+ '<div class="view-copy">Endpoint and key material are kept out of the main profile surface; inspect technical evidence only when needed.</div></section>' +
2034
+ values(state.posts).slice(0, 6).map((post) => item(post.title, post.body, [
2035
+ "status: " + labelize(post.status),
2036
+ "visibility: " + labelize(post.visibility),
2037
+ "source: " + labelize(post.source_basis),
2038
+ "updated: " + timeLabel(post.updated_at)
2039
+ ], post, post.status === "removed" ? "risk" : "", "", [
2040
+ ["status", labelize(post.status)],
2041
+ ["visibility", labelize(post.visibility)],
2042
+ ["source", labelize(post.source_basis)],
2043
+ ["audit refs", (post.audit_refs || []).length]
2044
+ ])).join("");
2045
+ }
2046
+ if (state.view === "feed") {
2047
+ const posts = state.posts;
2048
+ html = values(state.feedItems).map((feed) => {
2049
+ const post = posts[feed.post_id] || {};
2050
+ const actions = [
2051
+ feed.read_state === "read" ? "" : action("Mark read", "feed-read", feed.feed_item_id),
2052
+ feed.hidden ? "" : action("Hide", "feed-hide", feed.feed_item_id, "danger")
2053
+ ].join("");
2054
+ return item(post.title || "Untitled feed item", post.body || "No post body loaded for this feed item.", [
2055
+ feed.read_state !== "read" ? "unread" : "",
2056
+ feed.hidden ? "hidden" : ""
2057
+ ], { feed, post }, feed.hidden ? "warn" : "", actions, [
2058
+ ["relationship", labelize(contactFor(feed.origin_agent_id).relationship_state || "local")],
2059
+ ["visibility", labelize(post.visibility || "unknown")],
2060
+ ["source", labelize(post.source_basis || feed.origin_home || "unknown")],
2061
+ ["delivery", labelize(feed.delivery_route || "local")]
2062
+ ], "Posted " + timeLabel(post.published_at || post.updated_at || feed.received_at));
2063
+ }).join("") || renderFeedEmpty();
2064
+ }
2065
+ if (state.view === "contacts") {
2066
+ html = values(state.contacts).map((contact) => item(contact.display_name || "Unnamed contact", contact.aliases?.[0] || contact.card_url || peerEndpointLabel(contact), [
2067
+ state.mutes[contact.peer_agent_id] ? "muted" : "active",
2068
+ ], contact, contact.relationship_state === "blocked" ? "risk" : "", state.mutes[contact.peer_agent_id] ? "" : action("Mute", "contact-mute", contact.peer_agent_id), [
2069
+ ["relationship", labelize(contact.relationship_state)],
2070
+ ["grants", (contact.capability_grants || []).length],
2071
+ ["endpoint", (contact.known_endpoints || []).length ? "known" : "missing"],
2072
+ ["local posture", state.mutes[contact.peer_agent_id] ? "muted" : "active"]
2073
+ ], "", initials(contact.display_name || contact.aliases?.[0] || contact.peer_agent_id))).join("") || renderEmpty("No contacts yet.");
2074
+ }
2075
+ if (state.view === "messages") {
2076
+ html = state.messages.map((message) => item(labelize(message.type), message.body?.text || message.body?.note || JSON.stringify(message.body || {}), [
2077
+ ], message, "", "", [
2078
+ ["direction", message.to_agent_id === (state.me?.did || state.me?.agent_id) ? "inbound" : "outbound"],
2079
+ ["transport", labelize(message.transport || "local")],
2080
+ ["sender", agentLabel(message.from_agent_id)],
2081
+ ["recipient", agentLabel(message.to_agent_id)]
2082
+ ], "", initials(agentLabel(message.from_agent_id)))).join("") || renderEmpty("No messages for selected contacts yet.");
2083
+ }
2084
+ if (state.view === "posts") {
2085
+ html = '<form class="composer" data-action="post-create"><input name="title" placeholder="Post title" required><textarea name="body" placeholder="Post body" required></textarea><select name="visibility"><option value="private">private</option><option value="friends">friends</option><option value="public_if_enabled">public_if_enabled</option></select><button type="submit" class="primary">Create draft</button></form>' +
2086
+ (values(state.posts).map((post) => {
2087
+ const actions = [
2088
+ post.status === "pending_approval" ? action("Approve", "post-approve", post.post_id) : "",
2089
+ post.status === "removed" ? "" : action("Edit", "post-edit", post.post_id),
2090
+ post.status === "removed" ? "" : action("Remove", "post-remove", post.post_id, "danger")
2091
+ ].join("");
2092
+ return item(post.title, post.body, [
2093
+ post.approval_ref ? "approval linked" : ""
2094
+ ], post, post.status === "removed" ? "risk" : "", actions, [
2095
+ ["status", labelize(post.status)],
2096
+ ["visibility", labelize(post.visibility)],
2097
+ ["source", labelize(post.source_basis)],
2098
+ ["approval", post.approval_ref ? "linked" : "none"]
2099
+ ], "Updated " + timeLabel(post.updated_at));
2100
+ }).join("") || renderEmpty("No post history yet."));
2101
+ }
2102
+ if (state.view === "approvals") {
2103
+ html = values(state.approvals).map((approval) => {
2104
+ const actions = approval.status === "pending"
2105
+ ? action("Approve", "approval-approve", approval.approval_id) + action("Reject", "approval-reject", approval.approval_id, "danger")
2106
+ : "";
2107
+ return item(approval.summary, approval.object_type + " awaiting local owner decision", [], approval, approval.risk_level === "high" ? "risk" : approval.risk_level === "medium" ? "warn" : "", actions, [
2108
+ ["risk", labelize(approval.risk_level)],
2109
+ ["status", labelize(approval.status)],
2110
+ ["type", labelize(approval.type)],
2111
+ ["object", labelize(approval.object_type || "unknown")]
2112
+ ], "Requested " + timeLabel(approval.created_at));
2113
+ }).join("") || renderEmpty("No approval requests.");
2114
+ }
2115
+ if (state.view === "activity") {
2116
+ html = [...state.audit].reverse().map((event) => item(labelize(event.type || "audit event"), event.peer_agent_id ? agentLabel(event.peer_agent_id) : "Local owner action", [
2117
+ "when: " + timeLabel(event.created_at),
2118
+ "actor/context: " + agentLabel(event.peer_agent_id),
2119
+ "audit evidence available"
2120
+ ], event, "", "", [
2121
+ ["event", labelize(event.type || "unknown")],
2122
+ ["actor/context", agentLabel(event.peer_agent_id)],
2123
+ ["time", timeLabel(event.created_at)],
2124
+ ["audit evidence", event.audit_id ? "available" : "not recorded"]
2125
+ ])).join("") || renderEmpty("No activity log entries yet.");
2126
+ }
2127
+ if (state.view === "inspector") {
2128
+ html = item("Current API snapshot", "Local owner state loaded from /api routes.", [
2129
+ "contacts: " + values(state.contacts).length,
2130
+ "posts: " + values(state.posts).length,
2131
+ "feed: " + values(state.feedItems).length,
2132
+ "approvals: " + values(state.approvals).length,
2133
+ "activity: " + state.audit.length
2134
+ ], state, "", "", [
2135
+ ["owner", state.me?.display_name || "Local owner"],
2136
+ ["contacts", values(state.contacts).length],
2137
+ ["posts", values(state.posts).length],
2138
+ ["approvals", values(state.approvals).length]
2139
+ ]);
2140
+ }
2141
+ content.innerHTML = html;
2142
+ content.querySelectorAll("[data-payload]").forEach((node) => {
2143
+ node.addEventListener("click", () => setInspector(JSON.parse(decodeURIComponent(node.dataset.payload))));
2144
+ node.addEventListener("keydown", (event) => { if (event.key === "Enter") node.click(); });
2145
+ });
2146
+ content.querySelectorAll("button[data-view-target]").forEach((button) => {
2147
+ button.addEventListener("click", (event) => {
2148
+ event.stopPropagation();
2149
+ state.view = button.dataset.viewTarget;
2150
+ render();
2151
+ });
2152
+ });
2153
+ content.querySelectorAll("button[data-action]").forEach((button) => {
2154
+ button.addEventListener("click", (event) => {
2155
+ event.stopPropagation();
2156
+ runAction(button.dataset.action, button.dataset.id);
2157
+ });
2158
+ });
2159
+ const composer = content.querySelector("form[data-action='post-create']");
2160
+ if (composer) composer.addEventListener("submit", createPost);
2161
+ }
2162
+ async function postJson(path, body = {}) {
2163
+ return api(path, { method: "POST", body: JSON.stringify(body) });
2164
+ }
2165
+ async function runAction(name, id) {
2166
+ try {
2167
+ if (name === "feed-read") await postJson("/api/feed/" + encodeURIComponent(id) + "/read");
2168
+ if (name === "feed-hide") await postJson("/api/feed/" + encodeURIComponent(id) + "/hide", { reason: prompt("Reason", "hidden by owner") || "" });
2169
+ if (name === "contact-mute") await postJson("/api/contacts/" + encodeURIComponent(id) + "/mute", { reason: prompt("Reason", "muted by owner") || "" });
2170
+ if (name === "post-approve") await postJson("/api/posts/" + encodeURIComponent(id) + "/approve");
2171
+ if (name === "post-edit") {
2172
+ const current = state.posts[id] || {};
2173
+ await postJson("/api/posts/" + encodeURIComponent(id) + "/edit", {
2174
+ title: prompt("Title", current.title || "") || current.title || "",
2175
+ body: prompt("Body", current.body || "") || current.body || "",
2176
+ visibility: current.visibility || "private"
2177
+ });
2178
+ }
2179
+ if (name === "post-remove") await postJson("/api/posts/" + encodeURIComponent(id) + "/remove", { reason: prompt("Reason", "removed by owner") || "" });
2180
+ if (name === "approval-approve") await postJson("/api/approvals/" + encodeURIComponent(id) + "/resolve", { approved: true });
2181
+ if (name === "approval-reject") await postJson("/api/approvals/" + encodeURIComponent(id) + "/resolve", { approved: false });
2182
+ await refresh();
2183
+ } catch (error) {
2184
+ setInspector({ action: name, id, failure_reason: error.message || String(error) });
2185
+ }
2186
+ }
2187
+ async function createPost(event) {
2188
+ event.preventDefault();
2189
+ const form = event.currentTarget;
2190
+ const data = new FormData(form);
2191
+ try {
2192
+ await postJson("/api/posts", {
2193
+ title: data.get("title"),
2194
+ body: data.get("body"),
2195
+ visibility: data.get("visibility"),
2196
+ status: "draft"
2197
+ });
2198
+ form.reset();
2199
+ await refresh();
2200
+ } catch (error) {
2201
+ setInspector({ action: "post-create", failure_reason: error.message || String(error) });
2202
+ }
2203
+ }
2204
+ async function refresh() {
2205
+ const me = await api("/api/me");
2206
+ state.me = me.identity;
2207
+ setText("owner", publicOwnerLabel() + " | Local session active");
2208
+ setText("ownerName", publicOwnerLabel());
2209
+ setText("ownerShort", "local owner session");
2210
+ const [contacts, posts, feed, approvals, audit] = await Promise.all([
2211
+ api("/api/contacts"),
2212
+ api("/api/posts"),
2213
+ api("/api/feed"),
2214
+ api("/api/approvals"),
2215
+ api("/api/audit")
2216
+ ]);
2217
+ state.contacts = contacts.contacts;
2218
+ state.mutes = contacts.mutes;
2219
+ state.posts = posts.posts;
2220
+ state.feedItems = feed.feed_items;
2221
+ state.approvals = approvals.approvals;
2222
+ state.audit = audit.audit || [];
2223
+ const messageSets = await Promise.all(values(state.contacts).map((contact) => api("/api/messages/" + encodeURIComponent(contact.peer_agent_id)).catch(() => ({ messages: [] }))));
2224
+ state.messages = messageSets.flatMap((set) => set.messages || []);
2225
+ setText("sessionBadge", "Local session active");
2226
+ render();
2227
+ }
2228
+ async function boot() {
2229
+ try {
2230
+ document.getElementById("content").innerHTML = skeleton();
2231
+ setText("viewState", "Loading");
2232
+ const login = await fetch("/auth/login", {
2233
+ method: "POST",
2234
+ headers: { "content-type": "application/json" },
2235
+ body: JSON.stringify({ auth_method: "dev-bypass" })
2236
+ }).then((response) => response.json());
2237
+ state.sessionId = login.session_id;
2238
+ state.csrf = login.csrf_token;
2239
+ await refresh();
2240
+ } catch (error) {
2241
+ document.getElementById("content").innerHTML = '<div class="loading">Still connecting to local Edge Book data. Retrying shortly...</div>';
2242
+ setText("viewState", "Connecting");
2243
+ window.setTimeout(boot, 1200);
2244
+ }
2245
+ }
2246
+ document.querySelectorAll("nav button").forEach((button) => button.addEventListener("click", () => {
2247
+ state.view = button.dataset.view;
2248
+ render();
2249
+ }));
2250
+ boot();
2251
+ </script>
2252
+ </body>
2253
+ </html>`;
2254
+ }
2255
+ function createEdgeBookHttpServer(store, cardUrl) {
2256
+ const adapters = createDefaultApiAdapters(store);
2257
+ return http.createServer(async (req, res) => {
2258
+ try {
2259
+ const url = new URL(req.url || "/", "http://localhost");
2260
+ if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/app")) {
2261
+ sendHtml(res, dashboardHtml());
2262
+ return;
2263
+ }
2264
+ if (await handleOwnerApi(req, res, url, adapters)) return;
2265
+ if (req.method === "GET" && url.pathname === "/edge-book/card") {
2266
+ sendJson(res, 200, await store.writeCard(cardUrl));
2267
+ return;
2268
+ }
2269
+ if (req.method === "POST" && url.pathname === "/edge-book/envelopes") {
2270
+ const envelope = await readJsonBody(req);
2271
+ await store.receiveEnvelope(envelope);
2272
+ sendJson(res, 200, { ok: true, type: envelope.type, message_id: envelope.message_id });
2273
+ return;
2274
+ }
2275
+ sendJson(res, 404, { ok: false, error: "not_found" });
2276
+ } catch (error) {
2277
+ sendError(res, error);
2278
+ }
2279
+ });
2280
+ }
2281
+ async function startEdgeBookServer(options) {
2282
+ const store = new EdgeBookStore({ home: options.home });
2283
+ const host = options.host || "127.0.0.1";
2284
+ const port = options.port ?? 0;
2285
+ const server = createEdgeBookHttpServer(store, options.cardUrl);
2286
+ await new Promise((resolve) => server.listen(port, host, resolve));
2287
+ return server;
2288
+ }
2289
+ function relayFile(store, agentId) {
2290
+ return path2.join(store, `${encodeURIComponent(agentId)}.jsonl`);
2291
+ }
2292
+ async function appendRelayEnvelope(store, agentId, envelope) {
2293
+ await fs2.mkdir(store, { recursive: true });
2294
+ await fs2.appendFile(relayFile(store, agentId), `${JSON.stringify(envelope)}
2295
+ `, "utf8");
2296
+ }
2297
+ async function drainRelayEnvelopes(store, agentId) {
2298
+ const file = relayFile(store, agentId);
2299
+ try {
2300
+ const text = await fs2.readFile(file, "utf8");
2301
+ await fs2.writeFile(file, "", "utf8");
2302
+ return text.split(/\n/).filter(Boolean).map((line) => JSON.parse(line));
2303
+ } catch (error) {
2304
+ if (error.code === "ENOENT") return [];
2305
+ throw error;
2306
+ }
2307
+ }
2308
+ function createRelayServer(store) {
2309
+ return http.createServer(async (req, res) => {
2310
+ try {
2311
+ const url = new URL(req.url || "/", "http://localhost");
2312
+ const match = /^\/relay\/([^/]+)$/.exec(url.pathname);
2313
+ if (!match) {
2314
+ sendJson(res, 404, { ok: false, error: "not_found" });
2315
+ return;
2316
+ }
2317
+ const agentId = decodeURIComponent(match[1]);
2318
+ if (req.method === "POST") {
2319
+ const envelope = await readJsonBody(req);
2320
+ await appendRelayEnvelope(store, agentId, envelope);
2321
+ sendJson(res, 200, { ok: true, queued: 1 });
2322
+ return;
2323
+ }
2324
+ if (req.method === "GET") {
2325
+ const envelopes = await drainRelayEnvelopes(store, agentId);
2326
+ sendJson(res, 200, { ok: true, envelopes });
2327
+ return;
2328
+ }
2329
+ sendJson(res, 405, { ok: false, error: "method_not_allowed" });
2330
+ } catch (error) {
2331
+ sendError(res, error);
2332
+ }
2333
+ });
2334
+ }
2335
+ async function startRelayServer(options) {
2336
+ const host = options.host || "127.0.0.1";
2337
+ const port = options.port ?? 0;
2338
+ const server = createRelayServer(options.store);
2339
+ await new Promise((resolve) => server.listen(port, host, resolve));
2340
+ return server;
2341
+ }
2342
+ async function postEnvelope(endpoint, envelope) {
2343
+ const response = await fetch(endpoint, {
2344
+ method: "POST",
2345
+ headers: { "content-type": "application/json" },
2346
+ body: JSON.stringify(envelope)
2347
+ });
2348
+ if (!response.ok) throw new EdgeBookError("delivery_failed", `Delivery failed: ${response.status} ${await response.text()}`);
2349
+ }
2350
+ async function postRelayEnvelope(relayBaseUrl, recipientAgentId, envelope) {
2351
+ await postEnvelope(`${relayBaseUrl.replace(/\/$/, "")}/relay/${encodeURIComponent(recipientAgentId)}`, envelope);
2352
+ }
2353
+ async function pullRelayEnvelopes(relayBaseUrl, recipientAgentId) {
2354
+ const response = await fetch(`${relayBaseUrl.replace(/\/$/, "")}/relay/${encodeURIComponent(recipientAgentId)}`);
2355
+ if (!response.ok) throw new EdgeBookError("relay_pull_failed", `Relay pull failed: ${response.status}`);
2356
+ const body = await response.json();
2357
+ return body.envelopes || [];
2358
+ }
2359
+
2360
+ // src/dialout.ts
2361
+ var KEY_FILE = "host-dialout-key.json";
2362
+ var DEFAULT_PAIR_TTL_MS = 5 * 60 * 1e3;
2363
+ var DEFAULT_HEARTBEAT_MS = 25e3;
2364
+ var DEFAULT_BACKOFF_MS = 1e3;
2365
+ var MAX_BACKOFF_MS = 3e4;
2366
+ var PAIRING_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
2367
+ function now2() {
2368
+ return (/* @__PURE__ */ new Date()).toISOString();
2369
+ }
2370
+ function keyId(agentKey) {
2371
+ return `agent_${crypto2.createHash("sha256").update(agentKey).digest("base64url").slice(0, 32)}`;
2372
+ }
2373
+ async function chmodBestEffort2(file, mode) {
2374
+ if (process.platform === "win32") return;
2375
+ try {
2376
+ await fs3.chmod(file, mode);
2377
+ } catch {
2378
+ }
2379
+ }
2380
+ async function loadOrCreateDialoutKey(store) {
2381
+ const file = store.file(KEY_FILE);
2382
+ try {
2383
+ const existing = JSON.parse(await fs3.readFile(file, "utf8"));
2384
+ if (existing.agent_key && existing.public_key_pem && existing.private_key_pem && existing.key_id) return existing;
2385
+ if (existing.public_key_pem && existing.private_key_pem && existing.key_id) {
2386
+ const migrated = {
2387
+ ...existing,
2388
+ agent_key: `ed25519:${Buffer.from(existing.public_key_pem, "utf8").toString("base64")}`
2389
+ };
2390
+ migrated.key_id = keyId(migrated.agent_key);
2391
+ await fs3.writeFile(file, `${JSON.stringify(migrated, null, 2)}
2392
+ `, { encoding: "utf8", mode: 384 });
2393
+ await chmodBestEffort2(file, 384);
2394
+ return migrated;
2395
+ }
2396
+ } catch (error) {
2397
+ if (error.code !== "ENOENT") throw error;
2398
+ }
2399
+ const pair = crypto2.generateKeyPairSync("ed25519");
2400
+ const publicKeyPem = pair.publicKey.export({ type: "spki", format: "pem" }).toString();
2401
+ const privateKeyPem = pair.privateKey.export({ type: "pkcs8", format: "pem" }).toString();
2402
+ const publicKeyDer = pair.publicKey.export({ type: "spki", format: "der" });
2403
+ const agentKey = `ed25519:${Buffer.from(publicKeyDer).toString("base64")}`;
2404
+ const key = {
2405
+ schema: "edge-book-host-dialout-key/0.1",
2406
+ key_id: keyId(agentKey),
2407
+ agent_key: agentKey,
2408
+ public_key_pem: publicKeyPem,
2409
+ private_key_pem: privateKeyPem,
2410
+ created_at: now2()
2411
+ };
2412
+ await fs3.mkdir(path3.dirname(file), { recursive: true });
2413
+ await fs3.writeFile(file, `${JSON.stringify(key, null, 2)}
2414
+ `, { encoding: "utf8", mode: 384 });
2415
+ await chmodBestEffort2(file, 384);
2416
+ return key;
2417
+ }
2418
+ function generatePairingCode(length = 8) {
2419
+ let code = "";
2420
+ for (let i = 0; i < length; i += 1) {
2421
+ code += PAIRING_ALPHABET[crypto2.randomInt(PAIRING_ALPHABET.length)];
2422
+ }
2423
+ return code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;
2424
+ }
2425
+ async function createPairRegistration(store, ttlMs = DEFAULT_PAIR_TTL_MS) {
2426
+ const code = generatePairingCode();
2427
+ return {
2428
+ code,
2429
+ frame: {
2430
+ type: "pair_register",
2431
+ code,
2432
+ ttl_ms: ttlMs,
2433
+ request_id: crypto2.randomUUID()
2434
+ }
2435
+ };
2436
+ }
2437
+ async function createSessionsRevokeFrame(store) {
2438
+ await loadOrCreateDialoutKey(store);
2439
+ return {
2440
+ type: "sessions_revoke",
2441
+ request_id: crypto2.randomUUID()
2442
+ };
2443
+ }
2444
+ function socketFactory(url) {
2445
+ const SocketCtor = globalThis.WebSocket;
2446
+ if (!SocketCtor) throw new EdgeBookError("websocket_unavailable", "This Node runtime does not provide global WebSocket");
2447
+ return new SocketCtor(url);
2448
+ }
2449
+ function addSocketListener(socket, event, handler) {
2450
+ if (socket.addEventListener) {
2451
+ socket.addEventListener(event, handler);
2452
+ return;
2453
+ }
2454
+ const prop = `on${event}`;
2455
+ socket[prop] = handler;
2456
+ }
2457
+ function serverBaseUrl(server) {
2458
+ const address = server.address();
2459
+ if (!address || typeof address === "string") throw new EdgeBookError("local_api_unavailable", "Local API server did not expose a port");
2460
+ return `http://127.0.0.1:${address.port}`;
2461
+ }
2462
+ async function closeServer(server) {
2463
+ await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
2464
+ }
2465
+ async function openLocalApi(store) {
2466
+ const server = await startEdgeBookServer({ home: store.home, host: "127.0.0.1", port: 0 });
2467
+ const baseUrl = serverBaseUrl(server);
2468
+ const login = await fetch(`${baseUrl}/auth/login`, {
2469
+ method: "POST",
2470
+ headers: { "content-type": "application/json" },
2471
+ body: JSON.stringify({ auth_method: "future-remote-auth", ttl_ms: 24 * 60 * 60 * 1e3 })
2472
+ });
2473
+ if (!login.ok) {
2474
+ await closeServer(server);
2475
+ throw new EdgeBookError("local_api_login_failed", `Local API login failed: ${login.status}`);
2476
+ }
2477
+ const body = await login.json();
2478
+ return { server, baseUrl, sessionId: body.session_id, csrf: body.csrf_token };
2479
+ }
2480
+ function normalizeApiPath(value) {
2481
+ if (!value.startsWith("/api/")) throw new EdgeBookError("invalid_proxy_path", "Dial-out only proxies /api/* JSON requests");
2482
+ return value;
2483
+ }
2484
+ function apiUrl(baseUrl, frame) {
2485
+ return `${baseUrl}${normalizeApiPath(frame.path)}${frame.query || ""}`;
2486
+ }
2487
+ function requestBody(frame, method) {
2488
+ if (method === "GET" || method === "HEAD") return void 0;
2489
+ if (typeof frame.body_b64 === "string") return Buffer.from(frame.body_b64, "base64");
2490
+ return Buffer.from(JSON.stringify(frame.body ?? {}), "utf8");
2491
+ }
2492
+ var EdgeBookDialoutClient = class {
2493
+ options;
2494
+ store;
2495
+ socket;
2496
+ localApi;
2497
+ heartbeat;
2498
+ reconnectTimer;
2499
+ stopped = false;
2500
+ currentBackoff;
2501
+ opened;
2502
+ pendingSessionRevokes = /* @__PURE__ */ new Map();
2503
+ constructor(options) {
2504
+ this.options = {
2505
+ heartbeatMs: options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,
2506
+ reconnect: options.reconnect ?? true,
2507
+ backoffMs: options.backoffMs ?? DEFAULT_BACKOFF_MS,
2508
+ socketFactory: options.socketFactory ?? socketFactory,
2509
+ openLocalApi: options.openLocalApi ?? true,
2510
+ host: options.host,
2511
+ home: options.home
2512
+ };
2513
+ this.store = new EdgeBookStore({ home: options.home });
2514
+ this.currentBackoff = this.options.backoffMs;
2515
+ }
2516
+ async start() {
2517
+ this.stopped = false;
2518
+ await this.connect();
2519
+ }
2520
+ async stop() {
2521
+ this.stopped = true;
2522
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
2523
+ if (this.heartbeat) clearInterval(this.heartbeat);
2524
+ this.socket?.close();
2525
+ if (this.localApi) await closeServer(this.localApi.server);
2526
+ this.localApi = void 0;
2527
+ }
2528
+ async pair(ttlMs = DEFAULT_PAIR_TTL_MS) {
2529
+ const registration = await createPairRegistration(this.store, ttlMs);
2530
+ this.send(registration.frame);
2531
+ return registration;
2532
+ }
2533
+ async revokeSessions() {
2534
+ const frame = await createSessionsRevokeFrame(this.store);
2535
+ this.send(frame);
2536
+ return frame;
2537
+ }
2538
+ async revokeSessionsAndWait(timeoutMs = 5e3) {
2539
+ const frame = await createSessionsRevokeFrame(this.store);
2540
+ const ackPromise = new Promise((resolve, reject) => {
2541
+ const timer = setTimeout(() => {
2542
+ this.pendingSessionRevokes.delete(frame.request_id);
2543
+ reject(new EdgeBookError("host_revoke_timeout", "Timed out waiting for sessions_revoke_ok"));
2544
+ }, timeoutMs);
2545
+ this.pendingSessionRevokes.set(frame.request_id, { resolve, reject, timer });
2546
+ });
2547
+ this.send(frame);
2548
+ return { frame, ack: await ackPromise };
2549
+ }
2550
+ async connect() {
2551
+ if (this.options.openLocalApi && !this.localApi) this.localApi = await openLocalApi(this.store);
2552
+ const socket = this.options.socketFactory(this.options.host);
2553
+ this.socket = socket;
2554
+ const opened = new Promise((resolve, reject) => {
2555
+ this.opened = { resolve, reject };
2556
+ addSocketListener(socket, "open", async () => {
2557
+ try {
2558
+ this.currentBackoff = this.options.backoffMs;
2559
+ const key = await loadOrCreateDialoutKey(this.store);
2560
+ const identity = await this.store.identity();
2561
+ this.send({
2562
+ type: "hello",
2563
+ agent_key: key.agent_key,
2564
+ agent_did: identity.agent_id,
2565
+ version: "0.1.0",
2566
+ nonce: crypto2.randomUUID()
2567
+ });
2568
+ } catch (error) {
2569
+ this.opened = void 0;
2570
+ reject(error instanceof Error ? error : new Error(String(error)));
2571
+ }
2572
+ });
2573
+ });
2574
+ addSocketListener(socket, "message", (event) => {
2575
+ void this.handleMessage(event?.data);
2576
+ });
2577
+ addSocketListener(socket, "close", () => {
2578
+ if (this.heartbeat) clearInterval(this.heartbeat);
2579
+ if (!this.stopped && this.options.reconnect) this.scheduleReconnect();
2580
+ });
2581
+ await opened;
2582
+ }
2583
+ scheduleReconnect() {
2584
+ const delay = this.currentBackoff;
2585
+ this.currentBackoff = Math.min(MAX_BACKOFF_MS, Math.round(this.currentBackoff * 1.7));
2586
+ this.reconnectTimer = setTimeout(() => {
2587
+ void this.connect();
2588
+ }, delay);
2589
+ }
2590
+ send(value) {
2591
+ this.socket?.send(JSON.stringify(value));
2592
+ }
2593
+ async handleMessage(data) {
2594
+ const text = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
2595
+ const frame = JSON.parse(text);
2596
+ if (frame.type === "hello_ok") {
2597
+ this.opened?.resolve();
2598
+ this.opened = void 0;
2599
+ return;
2600
+ }
2601
+ if (frame.type === "hello_err") {
2602
+ const error = new EdgeBookError("host_hello_failed", frame.error || "Host rejected hello");
2603
+ this.opened?.reject(error);
2604
+ this.opened = void 0;
2605
+ return;
2606
+ }
2607
+ if (frame.type === "ping") {
2608
+ this.send({ type: "pong" });
2609
+ return;
2610
+ }
2611
+ if (frame.type === "pair_register_ok" || frame.type === "pair_register_err") return;
2612
+ if (frame.type === "sessions_revoke_ok") {
2613
+ const ack = frame;
2614
+ const pending = this.pendingSessionRevokes.get(ack.request_id || "");
2615
+ if (pending) {
2616
+ clearTimeout(pending.timer);
2617
+ this.pendingSessionRevokes.delete(ack.request_id || "");
2618
+ pending.resolve(ack);
2619
+ }
2620
+ return;
2621
+ }
2622
+ if (frame.type === "error") return;
2623
+ if (frame.type !== "host.api.request" && frame.type !== "api_request") return;
2624
+ const response = await this.handleApiRequest(frame);
2625
+ this.send(response);
2626
+ }
2627
+ async handleApiRequest(frame) {
2628
+ try {
2629
+ if (!this.localApi) {
2630
+ if (!this.options.openLocalApi) throw new EdgeBookError("local_api_disabled", "This dial-out client does not serve local API requests");
2631
+ this.localApi = await openLocalApi(this.store);
2632
+ }
2633
+ const method = (frame.method || "GET").toUpperCase();
2634
+ const response = await fetch(apiUrl(this.localApi.baseUrl, frame), {
2635
+ method,
2636
+ headers: {
2637
+ "content-type": "application/json",
2638
+ "x-openclaw-session": this.localApi.sessionId,
2639
+ "x-openclaw-csrf": this.localApi.csrf
2640
+ },
2641
+ body: requestBody(frame, method)
2642
+ });
2643
+ const bodyBuffer = Buffer.from(await response.arrayBuffer());
2644
+ return {
2645
+ type: "api_response",
2646
+ id: frame.id || frame.request_id || "",
2647
+ request_id: frame.request_id || frame.id || "",
2648
+ status: response.status,
2649
+ headers: { "content-type": response.headers.get("content-type") || "application/json; charset=utf-8" },
2650
+ body_b64: bodyBuffer.toString("base64")
2651
+ };
2652
+ } catch (error) {
2653
+ const body = {
2654
+ ok: false,
2655
+ code: error instanceof EdgeBookError ? error.code : "internal_error",
2656
+ error: error instanceof Error ? error.message : String(error)
2657
+ };
2658
+ return {
2659
+ type: "api_response",
2660
+ id: frame.id || frame.request_id || "",
2661
+ request_id: frame.request_id || frame.id || "",
2662
+ status: error instanceof EdgeBookError ? 400 : 500,
2663
+ headers: { "content-type": "application/json; charset=utf-8" },
2664
+ body_b64: Buffer.from(JSON.stringify(body), "utf8").toString("base64"),
2665
+ body
2666
+ };
2667
+ }
2668
+ }
2669
+ };
2670
+ async function sendPairRegistration(options) {
2671
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
2672
+ await client.start();
2673
+ await new Promise((resolve) => setTimeout(resolve, 0));
2674
+ const registration = await client.pair(options.ttlMs ?? DEFAULT_PAIR_TTL_MS);
2675
+ await client.stop();
2676
+ return registration;
2677
+ }
2678
+ async function sendSessionsRevoke(options) {
2679
+ const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
2680
+ await client.start();
2681
+ await new Promise((resolve) => setTimeout(resolve, 0));
2682
+ const { frame, ack } = await client.revokeSessionsAndWait();
2683
+ await client.stop();
2684
+ return { ...frame, channel_id: ack.channel_id };
2685
+ }
2686
+
2687
+ // src/cli.ts
2688
+ function usage() {
2689
+ return `Edge Book
2690
+
2691
+ Usage:
2692
+ edge-book init [--home <dir>] [--handle <handle>] [--name <display>]
2693
+
2694
+ 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>]
2698
+
2699
+ Local agent:
2700
+ edge-book doctor [--home <dir>]
2701
+ edge-book card show [--home <dir>]
2702
+ edge-book card export --path <file> [--home <dir>]
2703
+ edge-book friend request <card-path-or-url> [--deliver] [--home <dir>]
2704
+ edge-book friend receive <envelope-json-path> [--home <dir>]
2705
+ edge-book friend accept <peer-agent-id> [--deliver] [--home <dir>]
2706
+ edge-book friend apply-response <envelope-json-path> [--home <dir>]
2707
+ edge-book friend revoke <peer-agent-id> [--home <dir>]
2708
+ edge-book friend block <peer-agent-id> [--home <dir>]
2709
+ edge-book contacts list [--home <dir>]
2710
+ edge-book contacts refresh <card-path-or-url> [--home <dir>]
2711
+ edge-book message send <peer-agent-id> --body <text> [--deliver] [--home <dir>]
2712
+ edge-book message receive <envelope-json-path> [--home <dir>]
2713
+ edge-book inbox list [--home <dir>]
2714
+ edge-book inbox pull --relay <url> [--home <dir>]
2715
+ edge-book serve --host <host> --port <port> [--home <dir>]
2716
+ edge-book relay serve --host <host> --port <port> --store <dir>
2717
+ edge-book harness two-agent`;
2718
+ }
2719
+ function takeFlag(args, name) {
2720
+ const idx = args.indexOf(name);
2721
+ if (idx === -1) return void 0;
2722
+ const value = args[idx + 1];
2723
+ args.splice(idx, 2);
2724
+ return value;
2725
+ }
2726
+ function parseHome(args, ctx) {
2727
+ return takeFlag(args, "--home") || ctx.home;
2728
+ }
2729
+ function requireArg(value, label) {
2730
+ if (!value) throw new EdgeBookError("missing_arg", `Missing ${label}`);
2731
+ return value;
2732
+ }
2733
+ function takeBoolFlag(args, name) {
2734
+ const idx = args.indexOf(name);
2735
+ if (idx === -1) return false;
2736
+ args.splice(idx, 1);
2737
+ return true;
2738
+ }
2739
+ async function readEnvelope(filePath) {
2740
+ return JSON.parse(await fs4.readFile(path4.resolve(filePath), "utf8"));
2741
+ }
2742
+ async function deliverToEndpoint(envelope, endpoint) {
2743
+ await postEnvelope(endpoint, envelope);
2744
+ return `Delivered ${envelope.type} to ${endpoint}`;
2745
+ }
2746
+ async function deliverToPeer(store, envelope, peerAgentId) {
2747
+ const contacts = await store.contacts();
2748
+ const contact = contacts[peerAgentId];
2749
+ const direct = contact?.known_endpoints.find((entry) => entry.mode === "direct")?.endpoint;
2750
+ if (direct) return deliverToEndpoint(envelope, direct);
2751
+ const relay = contact?.known_endpoints.find((entry) => entry.mode === "relay")?.endpoint;
2752
+ if (relay) {
2753
+ await postRelayEnvelope(relay, peerAgentId, envelope);
2754
+ return `Queued ${envelope.type} via relay ${relay}`;
2755
+ }
2756
+ throw new EdgeBookError("no_route", `No direct or relay endpoint for ${peerAgentId}`);
2757
+ }
2758
+ function serverAddress(server) {
2759
+ const address = server.address();
2760
+ if (!address || typeof address === "string") return String(address);
2761
+ return `${address.address}:${address.port}`;
2762
+ }
2763
+ async function handleCli(inputArgs, ctx = {}) {
2764
+ const args = [...inputArgs];
2765
+ const home = parseHome(args, ctx);
2766
+ const command = args.shift() || "help";
2767
+ const store = new EdgeBookStore({ home });
2768
+ if (command === "help" || command === "--help" || command === "-h") {
2769
+ return { text: usage() };
2770
+ }
2771
+ if (command === "init") {
2772
+ const handle = takeFlag(args, "--handle");
2773
+ const displayName = takeFlag(args, "--name");
2774
+ const directUrl = takeFlag(args, "--direct-url");
2775
+ const relayUrl = takeFlag(args, "--relay-url");
2776
+ const identity = await store.init({ handle, displayName, directUrl, relayUrl });
2777
+ return { text: `Initialized ${identity.agent_id} at ${store.home}`, json: identity };
2778
+ }
2779
+ if (command === "doctor") {
2780
+ const result = await store.doctor();
2781
+ return { text: JSON.stringify(result, null, 2), json: result };
2782
+ }
2783
+ if (command === "card") {
2784
+ const action = args.shift() || "show";
2785
+ if (action === "show") {
2786
+ const card = await store.writeCard();
2787
+ return { text: JSON.stringify(card, null, 2), json: card };
2788
+ }
2789
+ if (action === "export") {
2790
+ const target = requireArg(takeFlag(args, "--path"), "--path");
2791
+ const card = await store.writeCard();
2792
+ await fs4.mkdir(path4.dirname(path4.resolve(target)), { recursive: true });
2793
+ await fs4.writeFile(path4.resolve(target), `${JSON.stringify(card, null, 2)}
2794
+ `, "utf8");
2795
+ return { text: `Exported Agent Card to ${path4.resolve(target)}`, json: card };
2796
+ }
2797
+ }
2798
+ if (command === "friend") {
2799
+ const action = args.shift();
2800
+ if (action === "request") {
2801
+ const deliver = takeBoolFlag(args, "--deliver");
2802
+ const target = requireArg(args.shift(), "card-path-or-url");
2803
+ const card = await loadCard(target);
2804
+ const envelope = await store.createFriendRequest(card);
2805
+ if (deliver) {
2806
+ const direct = card.transports.find((entry) => entry.mode === "direct")?.endpoint;
2807
+ if (direct) return { text: await deliverToEndpoint(envelope, direct), json: envelope };
2808
+ const relay = card.transports.find((entry) => entry.mode === "relay")?.endpoint;
2809
+ if (relay) {
2810
+ await postRelayEnvelope(relay, card.agent_id, envelope);
2811
+ return { text: `Queued friend_request via relay ${relay}`, json: envelope };
2812
+ }
2813
+ throw new EdgeBookError("no_route", `No direct or relay endpoint for ${card.agent_id}`);
2814
+ }
2815
+ return { text: JSON.stringify(envelope, null, 2), json: envelope };
2816
+ }
2817
+ if (action === "receive") {
2818
+ const source = requireArg(args.shift(), "envelope-json-path");
2819
+ const contact = await store.receiveFriendRequest(await readEnvelope(source));
2820
+ return { text: JSON.stringify(contact, null, 2), json: contact };
2821
+ }
2822
+ if (action === "accept") {
2823
+ const deliver = takeBoolFlag(args, "--deliver");
2824
+ const peer = requireArg(args.shift(), "peer-agent-id");
2825
+ const envelope = await store.acceptFriend(peer);
2826
+ if (deliver) return { text: await deliverToPeer(store, envelope, peer), json: envelope };
2827
+ return { text: JSON.stringify(envelope, null, 2), json: envelope };
2828
+ }
2829
+ if (action === "apply-response") {
2830
+ const source = requireArg(args.shift(), "envelope-json-path");
2831
+ await store.applyFriendResponse(await readEnvelope(source));
2832
+ return { text: `Applied friend response from ${path4.resolve(source)}` };
2833
+ }
2834
+ if (action === "revoke") {
2835
+ const peer = requireArg(args.shift(), "peer-agent-id");
2836
+ await store.revoke(peer);
2837
+ return { text: `Revoked ${peer}` };
2838
+ }
2839
+ if (action === "block") {
2840
+ const peer = requireArg(args.shift(), "peer-agent-id");
2841
+ await store.block(peer);
2842
+ return { text: `Blocked ${peer}` };
2843
+ }
2844
+ }
2845
+ if (command === "contacts") {
2846
+ const action = args.shift() || "list";
2847
+ if (action === "list") {
2848
+ const contacts = await store.contacts();
2849
+ return { text: JSON.stringify(Object.values(contacts), null, 2), json: contacts };
2850
+ }
2851
+ if (action === "refresh") {
2852
+ const target = requireArg(args.shift(), "card-path-or-url");
2853
+ const contact = await store.upsertContactFromCard(await loadCard(target));
2854
+ return { text: JSON.stringify(contact, null, 2), json: contact };
2855
+ }
2856
+ }
2857
+ if (command === "message") {
2858
+ const action = args.shift();
2859
+ if (action === "send") {
2860
+ const deliver = takeBoolFlag(args, "--deliver");
2861
+ const peer = requireArg(args.shift(), "peer-agent-id");
2862
+ const body = requireArg(takeFlag(args, "--body"), "--body");
2863
+ const envelope = await store.sendPrivilegedMessage(peer, { text: body });
2864
+ if (deliver) return { text: await deliverToPeer(store, envelope, peer), json: envelope };
2865
+ return { text: JSON.stringify(envelope, null, 2), json: envelope };
2866
+ }
2867
+ if (action === "receive") {
2868
+ const source = requireArg(args.shift(), "envelope-json-path");
2869
+ await store.receivePrivilegedMessage(await readEnvelope(source));
2870
+ return { text: `Received privileged message from ${path4.resolve(source)}` };
2871
+ }
2872
+ }
2873
+ if (command === "inbox") {
2874
+ const action = args.shift() || "list";
2875
+ if (action === "list") {
2876
+ const inbox = await store.inbox();
2877
+ return { text: JSON.stringify(inbox, null, 2), json: inbox };
2878
+ }
2879
+ if (action === "pull") {
2880
+ const relay = requireArg(takeFlag(args, "--relay"), "--relay");
2881
+ const identity = await store.identity();
2882
+ const envelopes = await pullRelayEnvelopes(relay, identity.agent_id);
2883
+ for (const envelope of envelopes) await store.receiveEnvelope(envelope);
2884
+ return { text: `Pulled ${envelopes.length} envelope(s) from ${relay}`, json: envelopes };
2885
+ }
2886
+ }
2887
+ if (command === "serve") {
2888
+ const host = takeFlag(args, "--host") || "127.0.0.1";
2889
+ const port = Number(takeFlag(args, "--port") || "0");
2890
+ const cardUrl = takeFlag(args, "--card-url");
2891
+ const server = await startEdgeBookServer({ home, host, port, cardUrl });
2892
+ console.log(`Edge Book server listening on ${serverAddress(server)}`);
2893
+ await new Promise(() => void 0);
2894
+ }
2895
+ if (command === "dialout") {
2896
+ const hostUrl = requireArg(takeFlag(args, "--host"), "--host");
2897
+ const client = new EdgeBookDialoutClient({ home, host: hostUrl, socketFactory: ctx.socketFactory });
2898
+ await client.start();
2899
+ console.log(`Edge Book dial-out connected to ${hostUrl}`);
2900
+ await new Promise(() => void 0);
2901
+ }
2902
+ if (command === "pair") {
2903
+ const hostUrl = requireArg(takeFlag(args, "--host"), "--host");
2904
+ const ttlMs = Number(takeFlag(args, "--ttl-ms") || `${5 * 60 * 1e3}`);
2905
+ if (!ctx.textOnly) {
2906
+ const client = new EdgeBookDialoutClient({ home, host: hostUrl, socketFactory: ctx.socketFactory, openLocalApi: false });
2907
+ await client.start();
2908
+ const registration2 = await client.pair(ttlMs);
2909
+ console.log(`Pairing code: ${registration2.code}`);
2910
+ console.log(`Expires in: ${ttlMs}ms`);
2911
+ console.log("Edge Book dial-out remains connected; leave this process running during the hosted reader session.");
2912
+ await new Promise(() => void 0);
2913
+ }
2914
+ const registration = await sendPairRegistration({ home, host: hostUrl, ttlMs, socketFactory: ctx.socketFactory });
2915
+ return { text: `Pairing code: ${registration.code}
2916
+ Expires in: ${registration.frame.ttl_ms}ms`, json: registration };
2917
+ }
2918
+ if (command === "sessions") {
2919
+ const action = args.shift();
2920
+ if (action === "revoke") {
2921
+ const hostUrl = requireArg(takeFlag(args, "--host"), "--host");
2922
+ const frame = await sendSessionsRevoke({ home, host: hostUrl, socketFactory: ctx.socketFactory });
2923
+ const channel = frame.channel_id || "unknown-channel";
2924
+ return { text: `Received sessions_revoke_ok for request ${frame.request_id} on ${channel}`, json: frame };
2925
+ }
2926
+ }
2927
+ if (command === "relay") {
2928
+ const action = args.shift();
2929
+ if (action === "serve") {
2930
+ const host = takeFlag(args, "--host") || "127.0.0.1";
2931
+ const port = Number(takeFlag(args, "--port") || "0");
2932
+ const relayStore = requireArg(takeFlag(args, "--store"), "--store");
2933
+ const server = await startRelayServer({ host, port, store: relayStore });
2934
+ console.log(`Edge Book relay listening on ${serverAddress(server)}`);
2935
+ await new Promise(() => void 0);
2936
+ }
2937
+ }
2938
+ if (command === "harness") {
2939
+ const action = args.shift();
2940
+ if (action === "two-agent") {
2941
+ const result = await runTwoAgentHarness();
2942
+ return { text: `PASS two-agent harness
2943
+ ${JSON.stringify(result, null, 2)}`, json: result };
2944
+ }
2945
+ }
2946
+ throw new EdgeBookError("unknown_command", usage());
2947
+ }
2948
+ async function runCli(args) {
2949
+ const result = await handleCli(args);
2950
+ console.log(result.text);
2951
+ }
2952
+ function isCliEntrypoint() {
2953
+ if (!process.argv[1]) return false;
2954
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
2955
+ }
2956
+ if (isCliEntrypoint()) {
2957
+ runCli(process.argv.slice(2)).catch((error) => {
2958
+ console.error(error?.message ?? String(error));
2959
+ process.exitCode = 1;
2960
+ });
2961
+ }
2962
+ export {
2963
+ handleCli,
2964
+ runCli
2965
+ };