edge-book 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/edge-book.js +374 -142
- package/package.json +1 -1
package/dist/edge-book.js
CHANGED
|
@@ -49,6 +49,9 @@ var FEED_FILE = "feed-items.json";
|
|
|
49
49
|
var APPROVALS_FILE = "approvals.json";
|
|
50
50
|
var ESCALATIONS_FILE = "escalations.json";
|
|
51
51
|
var CONTACT_MUTES_FILE = "contact-mutes.json";
|
|
52
|
+
var REPORTS_FILE = "reports.json";
|
|
53
|
+
var INVITE_CODES_FILE = "invite-codes.json";
|
|
54
|
+
var INBOUND_RATE_FILE = "inbound-rate.json";
|
|
52
55
|
var ATTESTATIONS_FILE = "attestations.json";
|
|
53
56
|
var ENDORSEMENTS_FILE = "endorsements.json";
|
|
54
57
|
var SIGNALS_FILE = "signals.json";
|
|
@@ -285,6 +288,10 @@ var EdgeBookStore = class {
|
|
|
285
288
|
if (input.direct_url !== void 0) next.direct_url = input.direct_url;
|
|
286
289
|
if (input.relay_url !== void 0) next.relay_url = input.relay_url;
|
|
287
290
|
if (input.notify_on_friend_request !== void 0) next.notify_on_friend_request = input.notify_on_friend_request;
|
|
291
|
+
if (input.open_friend_requests !== void 0) next.open_friend_requests = input.open_friend_requests;
|
|
292
|
+
if (input.inbound_max_per_peer !== void 0) next.inbound_max_per_peer = input.inbound_max_per_peer;
|
|
293
|
+
if (input.inbound_max_global !== void 0) next.inbound_max_global = input.inbound_max_global;
|
|
294
|
+
if (input.inbound_window_ms !== void 0) next.inbound_window_ms = input.inbound_window_ms;
|
|
288
295
|
await writeJson(this.file(CONFIG_FILE), next);
|
|
289
296
|
return next;
|
|
290
297
|
}
|
|
@@ -459,7 +466,7 @@ var EdgeBookStore = class {
|
|
|
459
466
|
await this.audit(`relationship.${type}`, peerAgentId, { previous, next: nextState, reason });
|
|
460
467
|
return event;
|
|
461
468
|
}
|
|
462
|
-
async createFriendRequest(targetCard, note = "") {
|
|
469
|
+
async createFriendRequest(targetCard, note = "", inviteCode = "") {
|
|
463
470
|
const identity = await this.identity();
|
|
464
471
|
validateCard(targetCard);
|
|
465
472
|
const existing = (await this.contacts())[targetCard.agent_id];
|
|
@@ -467,6 +474,7 @@ var EdgeBookStore = class {
|
|
|
467
474
|
await this.upsertContactFromCard(targetCard, "request_sent");
|
|
468
475
|
await this.setRelationship(targetCard.agent_id, "request_sent", "FriendRequest", note);
|
|
469
476
|
const card = await this.writeCard();
|
|
477
|
+
const body = { card, note, ...inviteCode ? { invite_code: inviteCode } : {} };
|
|
470
478
|
return this.signEnvelope({
|
|
471
479
|
type: "friend_request",
|
|
472
480
|
to_agent_id: targetCard.agent_id,
|
|
@@ -474,15 +482,56 @@ var EdgeBookStore = class {
|
|
|
474
482
|
capability_id: "",
|
|
475
483
|
ref: "",
|
|
476
484
|
transport: "local",
|
|
477
|
-
body
|
|
485
|
+
body
|
|
478
486
|
});
|
|
479
487
|
}
|
|
488
|
+
// NOTE — concurrency + sybil-defense assumptions (v1):
|
|
489
|
+
// The GLOBAL cap (inbound_max_global) is the real sybil defense: it limits total
|
|
490
|
+
// inbound load regardless of how many distinct identities an attacker mints.
|
|
491
|
+
// The per-peer cap only slows a single persistent identity; it provides weaker
|
|
492
|
+
// protection because `from_agent_id` is attacker-mintable (any key can be generated).
|
|
493
|
+
//
|
|
494
|
+
// The rate file is read-modify-write. This is safe under the assumption that the
|
|
495
|
+
// receive loop is effectively serial for a single-owner edge agent (one active
|
|
496
|
+
// session at a time). Concurrent receives — e.g. two simultaneous HTTP deliveries
|
|
497
|
+
// on a multi-machine deployment — could undercount hits, allowing bursts past the
|
|
498
|
+
// cap. A shared atomic lock or external counter store is the follow-up (ea-claude-090).
|
|
499
|
+
async enforceInboundRate(peerAgentId) {
|
|
500
|
+
const config = await this.config();
|
|
501
|
+
const windowMs = config.inbound_window_ms ?? 36e5;
|
|
502
|
+
const maxPeer = config.inbound_max_per_peer ?? 5;
|
|
503
|
+
const maxGlobal = config.inbound_max_global ?? 60;
|
|
504
|
+
const cutoff = Date.now() - windowMs;
|
|
505
|
+
const all = await readJson(this.file(INBOUND_RATE_FILE), {});
|
|
506
|
+
for (const k of Object.keys(all)) {
|
|
507
|
+
all[k] = all[k].filter((t) => t > cutoff);
|
|
508
|
+
if (!all[k].length) delete all[k];
|
|
509
|
+
}
|
|
510
|
+
const peerCount = (all[peerAgentId] ?? []).length;
|
|
511
|
+
const globalCount = Object.values(all).reduce((n, arr) => n + arr.length, 0);
|
|
512
|
+
if (peerCount >= maxPeer || globalCount >= maxGlobal) {
|
|
513
|
+
await this.audit("inbound.rate_limited", peerAgentId, { peerCount, globalCount });
|
|
514
|
+
throw new EdgeBookError("rate_limited", "Inbound request rate limit exceeded");
|
|
515
|
+
}
|
|
516
|
+
all[peerAgentId] = [...all[peerAgentId] ?? [], Date.now()];
|
|
517
|
+
await writeJson(this.file(INBOUND_RATE_FILE), all);
|
|
518
|
+
}
|
|
480
519
|
async receiveFriendRequest(envelope) {
|
|
481
520
|
await this.verifyEnvelope(envelope);
|
|
482
521
|
if (envelope.type !== "friend_request") throw new EdgeBookError("wrong_message_type", "Expected friend_request envelope");
|
|
522
|
+
await this.enforceInboundRate(envelope.from_agent_id);
|
|
483
523
|
const body = envelope.body;
|
|
484
524
|
validateCard(body.card);
|
|
485
525
|
if (body.card.agent_id !== envelope.from_agent_id) throw new EdgeBookError("agent_id_mismatch", "Friend request card does not match sender");
|
|
526
|
+
if ((await this.config()).open_friend_requests === false) {
|
|
527
|
+
const ALLOWED_INVITE_BYPASS = ["request_sent", "friend"];
|
|
528
|
+
const known = (await this.contacts())[envelope.from_agent_id]?.relationship_state;
|
|
529
|
+
const allowed = known !== void 0 && ALLOWED_INVITE_BYPASS.includes(known) || (body.invite_code ? await this.consumeInviteCode(body.invite_code) : false);
|
|
530
|
+
if (!allowed) {
|
|
531
|
+
await this.audit("inbound.unsolicited_dropped", envelope.from_agent_id, {});
|
|
532
|
+
throw new EdgeBookError("unsolicited_dropped", "Invite-only: unsolicited request without a valid invite code");
|
|
533
|
+
}
|
|
534
|
+
}
|
|
486
535
|
const contact = await this.upsertContactFromCard(body.card, "request_received");
|
|
487
536
|
await this.setRelationship(envelope.from_agent_id, "request_received", "FriendRequest", body.note);
|
|
488
537
|
await appendJsonl(this.file(INBOX_FILE), envelope);
|
|
@@ -658,6 +707,66 @@ var EdgeBookStore = class {
|
|
|
658
707
|
async block(peerAgentId) {
|
|
659
708
|
await this.setRelationship(peerAgentId, "blocked", "Block", "blocked");
|
|
660
709
|
}
|
|
710
|
+
async reports() {
|
|
711
|
+
return readJson(this.file(REPORTS_FILE), []);
|
|
712
|
+
}
|
|
713
|
+
async inviteCodes() {
|
|
714
|
+
return readJson(this.file(INVITE_CODES_FILE), []);
|
|
715
|
+
}
|
|
716
|
+
async mintInviteCode(opts = {}) {
|
|
717
|
+
const invite = {
|
|
718
|
+
code: randomId("invite"),
|
|
719
|
+
created_at: now(),
|
|
720
|
+
expires_at: opts.ttlMs ? new Date(Date.now() + opts.ttlMs).toISOString() : "",
|
|
721
|
+
max_uses: opts.maxUses ?? 0,
|
|
722
|
+
uses: 0
|
|
723
|
+
};
|
|
724
|
+
const codes = await this.inviteCodes();
|
|
725
|
+
codes.push(invite);
|
|
726
|
+
await writeJson(this.file(INVITE_CODES_FILE), codes);
|
|
727
|
+
return invite;
|
|
728
|
+
}
|
|
729
|
+
// NOTE — serial-receive assumption (v1):
|
|
730
|
+
// consumeInviteCode is read-modify-write. Under concurrent receives, two requests
|
|
731
|
+
// carrying the same single-use code could both read uses=0, both pass the max_uses
|
|
732
|
+
// check, and both increment — effectively spending the code twice. This is safe for
|
|
733
|
+
// a single-owner serial receive loop; a locking primitive is needed for concurrent
|
|
734
|
+
// multi-machine deployments (ties to the same ea-claude-090 follow-up).
|
|
735
|
+
async consumeInviteCode(code) {
|
|
736
|
+
const codes = await this.inviteCodes();
|
|
737
|
+
const idx = codes.findIndex((c) => c.code === code);
|
|
738
|
+
if (idx === -1) return false;
|
|
739
|
+
const invite = codes[idx];
|
|
740
|
+
if (invite.expires_at && new Date(invite.expires_at) < /* @__PURE__ */ new Date()) return false;
|
|
741
|
+
if (invite.max_uses > 0 && invite.uses >= invite.max_uses) return false;
|
|
742
|
+
invite.uses += 1;
|
|
743
|
+
codes[idx] = invite;
|
|
744
|
+
await writeJson(this.file(INVITE_CODES_FILE), codes);
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
async reportPeer(peerAgentId, reason = "", opts = {}) {
|
|
748
|
+
const auditRef = await this.audit("peer.reported", peerAgentId, { reason, block: Boolean(opts.block) });
|
|
749
|
+
let actuallyBlocked = false;
|
|
750
|
+
if (opts.block) {
|
|
751
|
+
const contacts = await this.contacts();
|
|
752
|
+
if (contacts[peerAgentId]) {
|
|
753
|
+
await this.block(peerAgentId);
|
|
754
|
+
actuallyBlocked = true;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const rec = {
|
|
758
|
+
report_id: randomId("report"),
|
|
759
|
+
peer_agent_id: peerAgentId,
|
|
760
|
+
reason,
|
|
761
|
+
blocked: actuallyBlocked,
|
|
762
|
+
created_at: now(),
|
|
763
|
+
audit_refs: [auditRef]
|
|
764
|
+
};
|
|
765
|
+
const existingReports = await readJson(this.file(REPORTS_FILE), []);
|
|
766
|
+
existingReports.push(rec);
|
|
767
|
+
await writeJson(this.file(REPORTS_FILE), existingReports);
|
|
768
|
+
return rec;
|
|
769
|
+
}
|
|
661
770
|
async issueGrant(subjectAgentId, scopes, expiresAt = "") {
|
|
662
771
|
const identity = await this.identity();
|
|
663
772
|
const unsigned = {
|
|
@@ -1245,6 +1354,7 @@ var EdgeBookStore = class {
|
|
|
1245
1354
|
async receiveObjectShare(envelope) {
|
|
1246
1355
|
await this.verifyEnvelope(envelope);
|
|
1247
1356
|
if (envelope.type !== "object_share") throw new EdgeBookError("wrong_message_type", "Expected object_share envelope");
|
|
1357
|
+
await this.enforceInboundRate(envelope.from_agent_id);
|
|
1248
1358
|
const identity = await this.identity();
|
|
1249
1359
|
const body = envelope.body;
|
|
1250
1360
|
const { object, grant } = body;
|
|
@@ -2193,6 +2303,159 @@ async function runTwoAgentHarness(baseDir) {
|
|
|
2193
2303
|
import fs2 from "fs/promises";
|
|
2194
2304
|
import http from "http";
|
|
2195
2305
|
import path2 from "path";
|
|
2306
|
+
|
|
2307
|
+
// src/resolver.ts
|
|
2308
|
+
function nextAction(result, target) {
|
|
2309
|
+
switch (result.status) {
|
|
2310
|
+
case "resolved":
|
|
2311
|
+
return `friend request ${target} --deliver`;
|
|
2312
|
+
case "approval_required":
|
|
2313
|
+
case "candidates": {
|
|
2314
|
+
const first = result.candidates?.[0];
|
|
2315
|
+
return first ? `candidates list # then: friend request ${first.candidate_id}` : "candidates list";
|
|
2316
|
+
}
|
|
2317
|
+
default:
|
|
2318
|
+
return "(no match \u2014 check the target)";
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
var localContactProvider = {
|
|
2322
|
+
name: "local",
|
|
2323
|
+
priority: 100,
|
|
2324
|
+
async resolve(store, target) {
|
|
2325
|
+
const contacts = await store.contacts();
|
|
2326
|
+
const match = Object.values(contacts).find(
|
|
2327
|
+
(c) => c.peer_agent_id === target || c.aliases.includes(target) || c.display_name === target
|
|
2328
|
+
);
|
|
2329
|
+
if (!match) return null;
|
|
2330
|
+
return {
|
|
2331
|
+
kind: "card",
|
|
2332
|
+
agent_id: match.peer_agent_id,
|
|
2333
|
+
provenance: {
|
|
2334
|
+
source: "local",
|
|
2335
|
+
confidence: "high",
|
|
2336
|
+
display_name: match.display_name,
|
|
2337
|
+
reason: `known contact (relationship_state=${match.relationship_state})`
|
|
2338
|
+
}
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
};
|
|
2342
|
+
function cardProvider(name, source, match) {
|
|
2343
|
+
return {
|
|
2344
|
+
name,
|
|
2345
|
+
priority: 90,
|
|
2346
|
+
async resolve(_store, target) {
|
|
2347
|
+
if (!match(target)) return null;
|
|
2348
|
+
const card = await loadCard(target);
|
|
2349
|
+
return {
|
|
2350
|
+
kind: "card",
|
|
2351
|
+
card,
|
|
2352
|
+
agent_id: card.agent_id,
|
|
2353
|
+
provenance: { source, confidence: "high", display_name: card.handle, reason: `${source} card verified` }
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
var inviteProvider = cardProvider("invite", "invite", (t) => t.startsWith("edgebook:invite:"));
|
|
2359
|
+
var cardUrlProvider = cardProvider("card_url", "card_url", (t) => /^https?:\/\//.test(t));
|
|
2360
|
+
var cardFileProvider = cardProvider(
|
|
2361
|
+
"card_file",
|
|
2362
|
+
"card_file",
|
|
2363
|
+
(t) => t.startsWith("file://") || t.startsWith("/") || t.startsWith("./") || t.endsWith(".json")
|
|
2364
|
+
);
|
|
2365
|
+
var CANDIDATES_FILE = "candidates.json";
|
|
2366
|
+
function candidateKey(c) {
|
|
2367
|
+
return `${c.source}:${c.card_url ?? c.agent_id ?? ""}`;
|
|
2368
|
+
}
|
|
2369
|
+
async function readCandidates(store) {
|
|
2370
|
+
return readJson(store.file(CANDIDATES_FILE), {});
|
|
2371
|
+
}
|
|
2372
|
+
async function listCandidates(store) {
|
|
2373
|
+
return Object.values(await readCandidates(store));
|
|
2374
|
+
}
|
|
2375
|
+
async function getCandidate(store, id) {
|
|
2376
|
+
return (await readCandidates(store))[id];
|
|
2377
|
+
}
|
|
2378
|
+
async function writeCandidate(store, input) {
|
|
2379
|
+
const map = await readCandidates(store);
|
|
2380
|
+
const existing = Object.values(map).find((c) => candidateKey(c) === candidateKey(input));
|
|
2381
|
+
if (existing) return existing;
|
|
2382
|
+
const candidate = {
|
|
2383
|
+
candidate_id: randomId("cand"),
|
|
2384
|
+
approved: false,
|
|
2385
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2386
|
+
...input
|
|
2387
|
+
};
|
|
2388
|
+
map[candidate.candidate_id] = candidate;
|
|
2389
|
+
await writeJson(store.file(CANDIDATES_FILE), map);
|
|
2390
|
+
await store.audit("candidate.write", candidate.agent_id ?? "", { candidate_id: candidate.candidate_id, source: candidate.source });
|
|
2391
|
+
return candidate;
|
|
2392
|
+
}
|
|
2393
|
+
function defaultProviders(registryLookup = async () => null) {
|
|
2394
|
+
return [localContactProvider, inviteProvider, cardUrlProvider, cardFileProvider, makeRegistryProvider(registryLookup)];
|
|
2395
|
+
}
|
|
2396
|
+
async function resolveTarget(store, target, opts) {
|
|
2397
|
+
const ordered = [...opts.providers].sort((a, b) => b.priority - a.priority);
|
|
2398
|
+
for (const provider of ordered) {
|
|
2399
|
+
const r = await provider.resolve(store, target);
|
|
2400
|
+
if (!r) continue;
|
|
2401
|
+
if (r.kind === "card") {
|
|
2402
|
+
const result2 = { status: "resolved", card: r.card, agent_id: r.agent_id, provenance: r.provenance, next_action: "" };
|
|
2403
|
+
result2.next_action = nextAction(result2, target);
|
|
2404
|
+
return result2;
|
|
2405
|
+
}
|
|
2406
|
+
const candidate = await writeCandidate(store, r.candidate);
|
|
2407
|
+
const result = { status: "approval_required", candidates: [candidate], provenance: r.provenance, next_action: "" };
|
|
2408
|
+
result.next_action = nextAction(result, target);
|
|
2409
|
+
return result;
|
|
2410
|
+
}
|
|
2411
|
+
return { status: "not_found", next_action: "(no match \u2014 check the target)" };
|
|
2412
|
+
}
|
|
2413
|
+
async function dropCandidate(store, candidateId) {
|
|
2414
|
+
const map = await readJson(store.file(CANDIDATES_FILE), {});
|
|
2415
|
+
if (!map[candidateId]) return;
|
|
2416
|
+
delete map[candidateId];
|
|
2417
|
+
await writeJson(store.file(CANDIDATES_FILE), map);
|
|
2418
|
+
}
|
|
2419
|
+
async function markCandidateApproved(store, candidateId, agentId) {
|
|
2420
|
+
const map = await readJson(store.file(CANDIDATES_FILE), {});
|
|
2421
|
+
if (!map[candidateId]) return;
|
|
2422
|
+
map[candidateId].approved = true;
|
|
2423
|
+
map[candidateId].agent_id = agentId;
|
|
2424
|
+
await writeJson(store.file(CANDIDATES_FILE), map);
|
|
2425
|
+
}
|
|
2426
|
+
async function promoteCandidate(store, candidateId, note = "") {
|
|
2427
|
+
const candidate = await getCandidate(store, candidateId);
|
|
2428
|
+
if (!candidate) throw new EdgeBookError("unknown_candidate", `No candidate ${candidateId}`);
|
|
2429
|
+
if (!candidate.card_url) {
|
|
2430
|
+
await store.audit("candidate.denied", "", { candidate_id: candidateId, reason: "no_card_url" });
|
|
2431
|
+
throw new EdgeBookError("candidate_not_resolvable", "Candidate has no card_url to verify; cannot promote");
|
|
2432
|
+
}
|
|
2433
|
+
const card = await loadCard(candidate.card_url);
|
|
2434
|
+
const envelope = await store.createFriendRequest(card, note);
|
|
2435
|
+
await markCandidateApproved(store, candidateId, card.agent_id);
|
|
2436
|
+
await store.audit("candidate.promoted", card.agent_id, { candidate_id: candidateId });
|
|
2437
|
+
return envelope;
|
|
2438
|
+
}
|
|
2439
|
+
function makeRegistryProvider(lookup) {
|
|
2440
|
+
return {
|
|
2441
|
+
name: "registry",
|
|
2442
|
+
priority: 50,
|
|
2443
|
+
async resolve(_store, target) {
|
|
2444
|
+
if (!target.startsWith("registry:")) return null;
|
|
2445
|
+
const cardTarget = await lookup(target);
|
|
2446
|
+
if (!cardTarget) return null;
|
|
2447
|
+
const card = await loadCard(cardTarget);
|
|
2448
|
+
return {
|
|
2449
|
+
kind: "card",
|
|
2450
|
+
card,
|
|
2451
|
+
agent_id: card.agent_id,
|
|
2452
|
+
provenance: { source: "registry", confidence: "medium", display_name: card.handle, reason: "registry handle lookup" }
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// src/http.ts
|
|
2196
2459
|
async function readJsonBody(req) {
|
|
2197
2460
|
const chunks = [];
|
|
2198
2461
|
for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
@@ -2369,6 +2632,13 @@ async function handleOwnerApi(req, res, url, adapters) {
|
|
|
2369
2632
|
sendJson(res, 200, { mute: await store.muteContact(decodeURIComponent(contactMuteMatch[1]), body.reason || "") });
|
|
2370
2633
|
return true;
|
|
2371
2634
|
}
|
|
2635
|
+
const contactReportMatch = /^\/api\/contacts\/([^/]+)\/report$/.exec(url.pathname);
|
|
2636
|
+
if (req.method === "POST" && contactReportMatch) {
|
|
2637
|
+
const body = await readJsonBody(req);
|
|
2638
|
+
const report = await store.reportPeer(decodeURIComponent(contactReportMatch[1]), body.reason || "", { block: Boolean(body.block) });
|
|
2639
|
+
sendJson(res, 200, { report });
|
|
2640
|
+
return true;
|
|
2641
|
+
}
|
|
2372
2642
|
const messagesMatch = /^\/api\/messages\/([^/]+)$/.exec(url.pathname);
|
|
2373
2643
|
if (req.method === "GET" && messagesMatch) {
|
|
2374
2644
|
const peerId = decodeURIComponent(messagesMatch[1]);
|
|
@@ -2493,6 +2763,32 @@ async function handleOwnerApi(req, res, url, adapters) {
|
|
|
2493
2763
|
sendJson(res, 200, { review: await store.reviewLocalDataImport(body) });
|
|
2494
2764
|
return true;
|
|
2495
2765
|
}
|
|
2766
|
+
if (req.method === "GET" && url.pathname === "/api/candidates") {
|
|
2767
|
+
sendJson(res, 200, { candidates: await listCandidates(store) });
|
|
2768
|
+
return true;
|
|
2769
|
+
}
|
|
2770
|
+
const candPromote = /^\/api\/candidates\/([^/]+)\/promote$/.exec(url.pathname);
|
|
2771
|
+
if (req.method === "POST" && candPromote) {
|
|
2772
|
+
const id = decodeURIComponent(candPromote[1]);
|
|
2773
|
+
const candidate = await getCandidate(store, id);
|
|
2774
|
+
if (!candidate) {
|
|
2775
|
+
sendJson(res, 404, { error: "unknown_candidate" });
|
|
2776
|
+
return true;
|
|
2777
|
+
}
|
|
2778
|
+
if (!candidate.card_url) {
|
|
2779
|
+
sendJson(res, 400, { error: "candidate_not_resolvable" });
|
|
2780
|
+
return true;
|
|
2781
|
+
}
|
|
2782
|
+
const response_envelope = await promoteCandidate(store, id);
|
|
2783
|
+
sendJson(res, 200, { candidate: await getCandidate(store, id), response_envelope });
|
|
2784
|
+
return true;
|
|
2785
|
+
}
|
|
2786
|
+
const candReject = /^\/api\/candidates\/([^/]+)\/reject$/.exec(url.pathname);
|
|
2787
|
+
if (req.method === "POST" && candReject) {
|
|
2788
|
+
await dropCandidate(store, decodeURIComponent(candReject[1]));
|
|
2789
|
+
sendJson(res, 200, { dropped: true });
|
|
2790
|
+
return true;
|
|
2791
|
+
}
|
|
2496
2792
|
sendJson(res, 404, { ok: false, error: "not_found" });
|
|
2497
2793
|
return true;
|
|
2498
2794
|
}
|
|
@@ -3130,6 +3426,7 @@ function dashboardHtml() {
|
|
|
3130
3426
|
<button data-view="messages">Messages <span id="messageCount">Total 0</span></button>
|
|
3131
3427
|
<button data-view="posts">Post history <span id="postCount">Drafts 0</span></button>
|
|
3132
3428
|
<button data-view="approvals">Approvals <span id="approvalCount">Pending 0</span></button>
|
|
3429
|
+
<button data-view="candidates">Candidates <span id="candidateCount">Pending 0</span></button>
|
|
3133
3430
|
<button data-view="escalations">Escalations <span id="escalationCount">Pending 0</span></button>
|
|
3134
3431
|
<button data-view="activity">Activity Log <span id="activityCount">Events 0</span></button>
|
|
3135
3432
|
<button data-view="inspector">Inspector <span>Details</span></button>
|
|
@@ -3193,6 +3490,7 @@ function dashboardHtml() {
|
|
|
3193
3490
|
posts: {},
|
|
3194
3491
|
feedItems: {},
|
|
3195
3492
|
approvals: {},
|
|
3493
|
+
candidates: [],
|
|
3196
3494
|
escalations: {},
|
|
3197
3495
|
messages: [],
|
|
3198
3496
|
audit: []
|
|
@@ -3204,6 +3502,7 @@ function dashboardHtml() {
|
|
|
3204
3502
|
messages: "Messages",
|
|
3205
3503
|
posts: "Post history",
|
|
3206
3504
|
approvals: "Approvals",
|
|
3505
|
+
candidates: "Candidates",
|
|
3207
3506
|
escalations: "Escalations",
|
|
3208
3507
|
activity: "Activity Log",
|
|
3209
3508
|
inspector: "Inspector"
|
|
@@ -3215,6 +3514,7 @@ function dashboardHtml() {
|
|
|
3215
3514
|
messages: "Friend-gated envelopes grouped by peer context.",
|
|
3216
3515
|
posts: "Drafts, approvals, visibility, source basis, and removal state.",
|
|
3217
3516
|
approvals: "Human gates for agent-authored changes and risk-bearing actions.",
|
|
3517
|
+
candidates: "Resolver-discovered first-contact candidates with provenance \u2014 approve to send a friend request, or reject to drop.",
|
|
3218
3518
|
escalations: "Questions your agent \u2014 or a collaborating agent \u2014 raised for you to answer.",
|
|
3219
3519
|
activity: "Owner-only audit trail for local decisions, relationship changes, posts, and messages.",
|
|
3220
3520
|
inspector: "Readable decision summary plus detailed local evidence."
|
|
@@ -3301,6 +3601,7 @@ function dashboardHtml() {
|
|
|
3301
3601
|
}
|
|
3302
3602
|
function pendingApprovals() { return values(state.approvals).filter((approval) => approval.status === "pending"); }
|
|
3303
3603
|
function pendingEscalations() { return values(state.escalations).filter((escalation) => escalation.status === "pending"); }
|
|
3604
|
+
function pendingCandidates() { return (state.candidates || []).filter((c) => !c.approved); }
|
|
3304
3605
|
function visibleFeedItems() { return values(state.feedItems).filter((feed) => !feed.hidden); }
|
|
3305
3606
|
function friendContacts() { return values(state.contacts).filter((contact) => contact.relationship_state === "friend"); }
|
|
3306
3607
|
function blockedContacts() { return values(state.contacts).filter((contact) => contact.relationship_state === "blocked"); }
|
|
@@ -3308,6 +3609,7 @@ function dashboardHtml() {
|
|
|
3308
3609
|
function renderAttentionQueue() {
|
|
3309
3610
|
const rows = [
|
|
3310
3611
|
["Approvals", pendingApprovals().length, pendingApprovals().length ? "attention" : "owned"],
|
|
3612
|
+
["Candidates", pendingCandidates().length, pendingCandidates().length ? "attention" : "neutral"],
|
|
3311
3613
|
["Escalations", pendingEscalations().length, pendingEscalations().length ? "attention" : "owned"],
|
|
3312
3614
|
["Unread feed", values(state.feedItems).filter((feed) => feed.read_state !== "read" && !feed.hidden).length, "neutral"],
|
|
3313
3615
|
["Blocked peers", blockedContacts().length, blockedContacts().length ? "risk" : "owned"],
|
|
@@ -3346,6 +3648,7 @@ function dashboardHtml() {
|
|
|
3346
3648
|
setText("contactCount", "Friends " + friendContacts().length);
|
|
3347
3649
|
setText("postCount", "Drafts " + draftPosts().length);
|
|
3348
3650
|
setText("approvalCount", "Pending " + pendingApprovals().length);
|
|
3651
|
+
setText("candidateCount", "Pending " + pendingCandidates().length);
|
|
3349
3652
|
setText("escalationCount", "Pending " + pendingEscalations().length);
|
|
3350
3653
|
setText("activityCount", "Events " + state.audit.length);
|
|
3351
3654
|
setText("messageCount", "Total " + state.messages.length);
|
|
@@ -3401,7 +3704,7 @@ function dashboardHtml() {
|
|
|
3401
3704
|
if (state.view === "contacts") {
|
|
3402
3705
|
html = values(state.contacts).map((contact) => item(contact.display_name || "Unnamed contact", contact.aliases?.[0] || contact.card_url || peerEndpointLabel(contact), [
|
|
3403
3706
|
state.mutes[contact.peer_agent_id] ? "muted" : "active",
|
|
3404
|
-
], contact, contact.relationship_state === "blocked" ? "risk" : "", state.mutes[contact.peer_agent_id] ? "" : action("Mute", "contact-mute", contact.peer_agent_id), [
|
|
3707
|
+
], contact, contact.relationship_state === "blocked" ? "risk" : "", (state.mutes[contact.peer_agent_id] ? "" : action("Mute", "contact-mute", contact.peer_agent_id)) + action("Report", "contact-report", contact.peer_agent_id, "risk"), [
|
|
3405
3708
|
["relationship", labelize(contact.relationship_state)],
|
|
3406
3709
|
["grants", (contact.capability_grants || []).length],
|
|
3407
3710
|
["endpoint", (contact.known_endpoints || []).length ? "known" : "missing"],
|
|
@@ -3448,6 +3751,22 @@ function dashboardHtml() {
|
|
|
3448
3751
|
], "Requested " + timeLabel(approval.created_at));
|
|
3449
3752
|
}).join("") || renderEmpty("No approval requests.");
|
|
3450
3753
|
}
|
|
3754
|
+
if (state.view === "candidates") {
|
|
3755
|
+
html = pendingCandidates().map((candidate) => {
|
|
3756
|
+
const actions = action("Approve", "candidate-approve", candidate.candidate_id) + action("Reject", "candidate-reject", candidate.candidate_id, "danger");
|
|
3757
|
+
return item(candidate.display_name || "Unknown candidate", candidate.reason, [
|
|
3758
|
+
"source: " + labelize(candidate.source),
|
|
3759
|
+
"confidence: " + labelize(candidate.confidence),
|
|
3760
|
+
candidate.network ? "network: " + candidate.network : "",
|
|
3761
|
+
"pending"
|
|
3762
|
+
], candidate, "", actions, [
|
|
3763
|
+
["source", labelize(candidate.source)],
|
|
3764
|
+
["confidence", labelize(candidate.confidence)],
|
|
3765
|
+
["network", candidate.network || "n/a"],
|
|
3766
|
+
["status", candidate.approved ? "approved" : "pending"]
|
|
3767
|
+
], "Discovered " + timeLabel(candidate.created_at));
|
|
3768
|
+
}).join("") || renderEmpty("No candidates discovered yet.");
|
|
3769
|
+
}
|
|
3451
3770
|
if (state.view === "escalations") {
|
|
3452
3771
|
html = values(state.escalations).map((escalation) => {
|
|
3453
3772
|
const isOption = (escalation.kind === "decision" || escalation.kind === "approval") && (escalation.options || []).length;
|
|
@@ -3523,6 +3842,11 @@ function dashboardHtml() {
|
|
|
3523
3842
|
if (name === "feed-read") await postJson("/api/feed/" + encodeURIComponent(id) + "/read");
|
|
3524
3843
|
if (name === "feed-hide") await postJson("/api/feed/" + encodeURIComponent(id) + "/hide", { reason: prompt("Reason", "hidden by owner") || "" });
|
|
3525
3844
|
if (name === "contact-mute") await postJson("/api/contacts/" + encodeURIComponent(id) + "/mute", { reason: prompt("Reason", "muted by owner") || "" });
|
|
3845
|
+
if (name === "contact-report") {
|
|
3846
|
+
const reason = prompt("Reason for report", "") || "";
|
|
3847
|
+
const blockStr = prompt("Also block this contact? (yes/no)", "no") || "no";
|
|
3848
|
+
await postJson("/api/contacts/" + encodeURIComponent(id) + "/report", { reason, block: blockStr.trim().toLowerCase() === "yes" });
|
|
3849
|
+
}
|
|
3526
3850
|
if (name === "post-approve") await postJson("/api/posts/" + encodeURIComponent(id) + "/approve");
|
|
3527
3851
|
if (name === "post-edit") {
|
|
3528
3852
|
const current = state.posts[id] || {};
|
|
@@ -3535,6 +3859,8 @@ function dashboardHtml() {
|
|
|
3535
3859
|
if (name === "post-remove") await postJson("/api/posts/" + encodeURIComponent(id) + "/remove", { reason: prompt("Reason", "removed by owner") || "" });
|
|
3536
3860
|
if (name === "approval-approve") await postJson("/api/approvals/" + encodeURIComponent(id) + "/resolve", { approved: true });
|
|
3537
3861
|
if (name === "approval-reject") await postJson("/api/approvals/" + encodeURIComponent(id) + "/resolve", { approved: false });
|
|
3862
|
+
if (name === "candidate-approve") await postJson("/api/candidates/" + encodeURIComponent(id) + "/promote", {});
|
|
3863
|
+
if (name === "candidate-reject") await postJson("/api/candidates/" + encodeURIComponent(id) + "/reject", {});
|
|
3538
3864
|
if (name === "escalation-answer") {
|
|
3539
3865
|
const text = prompt("Your answer", "");
|
|
3540
3866
|
if (text === null) return;
|
|
@@ -3574,11 +3900,12 @@ function dashboardHtml() {
|
|
|
3574
3900
|
setText("owner", publicOwnerLabel() + " | Local session active");
|
|
3575
3901
|
setText("ownerName", publicOwnerLabel());
|
|
3576
3902
|
setText("ownerShort", "local owner session");
|
|
3577
|
-
const [contacts, posts, feed, approvals, escalations, audit] = await Promise.all([
|
|
3903
|
+
const [contacts, posts, feed, approvals, candidates, escalations, audit] = await Promise.all([
|
|
3578
3904
|
api("/api/contacts"),
|
|
3579
3905
|
api("/api/posts"),
|
|
3580
3906
|
api("/api/feed"),
|
|
3581
3907
|
api("/api/approvals"),
|
|
3908
|
+
api("/api/candidates"),
|
|
3582
3909
|
api("/api/escalations"),
|
|
3583
3910
|
api("/api/audit")
|
|
3584
3911
|
]);
|
|
@@ -3587,6 +3914,7 @@ function dashboardHtml() {
|
|
|
3587
3914
|
state.posts = posts.posts;
|
|
3588
3915
|
state.feedItems = feed.feed_items;
|
|
3589
3916
|
state.approvals = approvals.approvals;
|
|
3917
|
+
state.candidates = candidates.candidates || [];
|
|
3590
3918
|
state.escalations = escalations.escalations || {};
|
|
3591
3919
|
state.audit = audit.audit || [];
|
|
3592
3920
|
const messageSets = await Promise.all(values(state.contacts).map((contact) => api("/api/messages/" + encodeURIComponent(contact.peer_agent_id)).catch(() => ({ messages: [] }))));
|
|
@@ -4258,138 +4586,6 @@ async function revokeOneSession(options) {
|
|
|
4258
4586
|
}
|
|
4259
4587
|
}
|
|
4260
4588
|
|
|
4261
|
-
// src/resolver.ts
|
|
4262
|
-
function nextAction(result, target) {
|
|
4263
|
-
switch (result.status) {
|
|
4264
|
-
case "resolved":
|
|
4265
|
-
return `friend request ${target} --deliver`;
|
|
4266
|
-
case "approval_required":
|
|
4267
|
-
case "candidates": {
|
|
4268
|
-
const first = result.candidates?.[0];
|
|
4269
|
-
return first ? `candidates list # then: friend request ${first.candidate_id}` : "candidates list";
|
|
4270
|
-
}
|
|
4271
|
-
default:
|
|
4272
|
-
return "(no match \u2014 check the target)";
|
|
4273
|
-
}
|
|
4274
|
-
}
|
|
4275
|
-
var localContactProvider = {
|
|
4276
|
-
name: "local",
|
|
4277
|
-
priority: 100,
|
|
4278
|
-
async resolve(store, target) {
|
|
4279
|
-
const contacts = await store.contacts();
|
|
4280
|
-
const match = Object.values(contacts).find(
|
|
4281
|
-
(c) => c.peer_agent_id === target || c.aliases.includes(target) || c.display_name === target
|
|
4282
|
-
);
|
|
4283
|
-
if (!match) return null;
|
|
4284
|
-
return {
|
|
4285
|
-
kind: "card",
|
|
4286
|
-
agent_id: match.peer_agent_id,
|
|
4287
|
-
provenance: {
|
|
4288
|
-
source: "local",
|
|
4289
|
-
confidence: "high",
|
|
4290
|
-
display_name: match.display_name,
|
|
4291
|
-
reason: `known contact (relationship_state=${match.relationship_state})`
|
|
4292
|
-
}
|
|
4293
|
-
};
|
|
4294
|
-
}
|
|
4295
|
-
};
|
|
4296
|
-
function cardProvider(name, source, match) {
|
|
4297
|
-
return {
|
|
4298
|
-
name,
|
|
4299
|
-
priority: 90,
|
|
4300
|
-
async resolve(_store, target) {
|
|
4301
|
-
if (!match(target)) return null;
|
|
4302
|
-
const card = await loadCard(target);
|
|
4303
|
-
return {
|
|
4304
|
-
kind: "card",
|
|
4305
|
-
card,
|
|
4306
|
-
agent_id: card.agent_id,
|
|
4307
|
-
provenance: { source, confidence: "high", display_name: card.handle, reason: `${source} card verified` }
|
|
4308
|
-
};
|
|
4309
|
-
}
|
|
4310
|
-
};
|
|
4311
|
-
}
|
|
4312
|
-
var inviteProvider = cardProvider("invite", "invite", (t) => t.startsWith("edgebook:invite:"));
|
|
4313
|
-
var cardUrlProvider = cardProvider("card_url", "card_url", (t) => /^https?:\/\//.test(t));
|
|
4314
|
-
var cardFileProvider = cardProvider(
|
|
4315
|
-
"card_file",
|
|
4316
|
-
"card_file",
|
|
4317
|
-
(t) => t.startsWith("file://") || t.startsWith("/") || t.startsWith("./") || t.endsWith(".json")
|
|
4318
|
-
);
|
|
4319
|
-
var CANDIDATES_FILE = "candidates.json";
|
|
4320
|
-
function candidateKey(c) {
|
|
4321
|
-
return `${c.source}:${c.card_url ?? c.agent_id ?? ""}`;
|
|
4322
|
-
}
|
|
4323
|
-
async function readCandidates(store) {
|
|
4324
|
-
return readJson(store.file(CANDIDATES_FILE), {});
|
|
4325
|
-
}
|
|
4326
|
-
async function listCandidates(store) {
|
|
4327
|
-
return Object.values(await readCandidates(store));
|
|
4328
|
-
}
|
|
4329
|
-
async function getCandidate(store, id) {
|
|
4330
|
-
return (await readCandidates(store))[id];
|
|
4331
|
-
}
|
|
4332
|
-
async function writeCandidate(store, input) {
|
|
4333
|
-
const map = await readCandidates(store);
|
|
4334
|
-
const existing = Object.values(map).find((c) => candidateKey(c) === candidateKey(input));
|
|
4335
|
-
if (existing) return existing;
|
|
4336
|
-
const candidate = {
|
|
4337
|
-
candidate_id: randomId("cand"),
|
|
4338
|
-
approved: false,
|
|
4339
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4340
|
-
...input
|
|
4341
|
-
};
|
|
4342
|
-
map[candidate.candidate_id] = candidate;
|
|
4343
|
-
await writeJson(store.file(CANDIDATES_FILE), map);
|
|
4344
|
-
await store.audit("candidate.write", candidate.agent_id ?? "", { candidate_id: candidate.candidate_id, source: candidate.source });
|
|
4345
|
-
return candidate;
|
|
4346
|
-
}
|
|
4347
|
-
function defaultProviders(registryLookup = async () => null) {
|
|
4348
|
-
return [localContactProvider, inviteProvider, cardUrlProvider, cardFileProvider, makeRegistryProvider(registryLookup)];
|
|
4349
|
-
}
|
|
4350
|
-
async function resolveTarget(store, target, opts) {
|
|
4351
|
-
const ordered = [...opts.providers].sort((a, b) => b.priority - a.priority);
|
|
4352
|
-
for (const provider of ordered) {
|
|
4353
|
-
const r = await provider.resolve(store, target);
|
|
4354
|
-
if (!r) continue;
|
|
4355
|
-
if (r.kind === "card") {
|
|
4356
|
-
const result2 = { status: "resolved", card: r.card, agent_id: r.agent_id, provenance: r.provenance, next_action: "" };
|
|
4357
|
-
result2.next_action = nextAction(result2, target);
|
|
4358
|
-
return result2;
|
|
4359
|
-
}
|
|
4360
|
-
const candidate = await writeCandidate(store, r.candidate);
|
|
4361
|
-
const result = { status: "approval_required", candidates: [candidate], provenance: r.provenance, next_action: "" };
|
|
4362
|
-
result.next_action = nextAction(result, target);
|
|
4363
|
-
return result;
|
|
4364
|
-
}
|
|
4365
|
-
return { status: "not_found", next_action: "(no match \u2014 check the target)" };
|
|
4366
|
-
}
|
|
4367
|
-
async function markCandidateApproved(store, candidateId, agentId) {
|
|
4368
|
-
const map = await readJson(store.file(CANDIDATES_FILE), {});
|
|
4369
|
-
if (!map[candidateId]) return;
|
|
4370
|
-
map[candidateId].approved = true;
|
|
4371
|
-
map[candidateId].agent_id = agentId;
|
|
4372
|
-
await writeJson(store.file(CANDIDATES_FILE), map);
|
|
4373
|
-
}
|
|
4374
|
-
function makeRegistryProvider(lookup) {
|
|
4375
|
-
return {
|
|
4376
|
-
name: "registry",
|
|
4377
|
-
priority: 50,
|
|
4378
|
-
async resolve(_store, target) {
|
|
4379
|
-
if (!target.startsWith("registry:")) return null;
|
|
4380
|
-
const cardTarget = await lookup(target);
|
|
4381
|
-
if (!cardTarget) return null;
|
|
4382
|
-
const card = await loadCard(cardTarget);
|
|
4383
|
-
return {
|
|
4384
|
-
kind: "card",
|
|
4385
|
-
card,
|
|
4386
|
-
agent_id: card.agent_id,
|
|
4387
|
-
provenance: { source: "registry", confidence: "medium", display_name: card.handle, reason: "registry handle lookup" }
|
|
4388
|
-
};
|
|
4389
|
-
}
|
|
4390
|
-
};
|
|
4391
|
-
}
|
|
4392
|
-
|
|
4393
4589
|
// src/cli.ts
|
|
4394
4590
|
function usage() {
|
|
4395
4591
|
return `Edge Book
|
|
@@ -4410,7 +4606,7 @@ Local agent:
|
|
|
4410
4606
|
edge-book doctor [--home <dir>]
|
|
4411
4607
|
edge-book card show [--home <dir>]
|
|
4412
4608
|
edge-book card export --path <file> [--home <dir>]
|
|
4413
|
-
edge-book card invite [--home <dir>]
|
|
4609
|
+
edge-book card invite [--ttl-ms <ms>] [--uses <n>] [--home <dir>] # "Add me" link; --uses/--ttl-ms mint a consumable code
|
|
4414
4610
|
edge-book friend request <card-path-or-url-or-invite> [--deliver] [--home <dir>]
|
|
4415
4611
|
edge-book friend receive <envelope-json-path> [--home <dir>]
|
|
4416
4612
|
edge-book friend accept <peer-agent-id> [--deliver] [--home <dir>]
|
|
@@ -4420,6 +4616,7 @@ Local agent:
|
|
|
4420
4616
|
edge-book friend pending [--json] [--home <dir>]
|
|
4421
4617
|
edge-book friend mark-notified <peer-agent-id> [--home <dir>]
|
|
4422
4618
|
edge-book friend notify-config --on|--off [--home <dir>]
|
|
4619
|
+
edge-book friend policy --open|--invite-only [--home <dir>]
|
|
4423
4620
|
edge-book contacts list [--home <dir>]
|
|
4424
4621
|
edge-book contacts refresh <card-path-or-url> [--home <dir>]
|
|
4425
4622
|
edge-book message send <peer-agent-id> --body <text> [--deliver] [--home <dir>]
|
|
@@ -4454,7 +4651,10 @@ Post taxonomy (spec-0021):
|
|
|
4454
4651
|
edge-book answer <query-id> --body <s>
|
|
4455
4652
|
edge-book query-delete <query-id>
|
|
4456
4653
|
edge-book ephemeral # list Class-2 ephemeral posts
|
|
4457
|
-
edge-book answers # list answers
|
|
4654
|
+
edge-book answers # list answers
|
|
4655
|
+
|
|
4656
|
+
Abuse floor:
|
|
4657
|
+
edge-book report <peer-agent-id> [--reason <r>] [--block] [--home <dir>]`;
|
|
4458
4658
|
}
|
|
4459
4659
|
function takeFlag(args, name) {
|
|
4460
4660
|
const idx = args.indexOf(name);
|
|
@@ -4668,9 +4868,18 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
|
|
|
4668
4868
|
return { text: `Exported Agent Card to ${path4.resolve(target)}`, json: card };
|
|
4669
4869
|
}
|
|
4670
4870
|
if (action === "invite") {
|
|
4871
|
+
const ttlMsStr = takeFlag(args, "--ttl-ms");
|
|
4872
|
+
const usesStr = takeFlag(args, "--uses");
|
|
4873
|
+
const ttlMs = ttlMsStr ? Number(ttlMsStr) : void 0;
|
|
4874
|
+
const maxUses = usesStr ? Number(usesStr) : void 0;
|
|
4671
4875
|
const card = await store.writeCard();
|
|
4672
|
-
const
|
|
4673
|
-
|
|
4876
|
+
const baseUrl = `edgebook:invite:${Buffer.from(JSON.stringify(card), "utf8").toString("base64url")}`;
|
|
4877
|
+
if (ttlMs !== void 0 || maxUses !== void 0) {
|
|
4878
|
+
const invite = await store.mintInviteCode({ ttlMs, maxUses });
|
|
4879
|
+
const inviteUrl = `${baseUrl}#code=${invite.code}`;
|
|
4880
|
+
return { text: inviteUrl, json: { invite_url: inviteUrl, agent_id: card.agent_id, invite_code: invite.code } };
|
|
4881
|
+
}
|
|
4882
|
+
return { text: baseUrl, json: { invite_url: baseUrl, agent_id: card.agent_id } };
|
|
4674
4883
|
}
|
|
4675
4884
|
}
|
|
4676
4885
|
if (command === "resolve") {
|
|
@@ -4692,13 +4901,20 @@ next: ${result.next_action}`, json: result };
|
|
|
4692
4901
|
const action = args.shift();
|
|
4693
4902
|
if (action === "request") {
|
|
4694
4903
|
const deliver = takeBoolFlag(args, "--deliver");
|
|
4695
|
-
const
|
|
4904
|
+
const rawTarget = requireArg(args.shift(), "card-path-url-or-candidate");
|
|
4905
|
+
let inviteCode = "";
|
|
4906
|
+
let target = rawTarget;
|
|
4907
|
+
const hashIdx = rawTarget.indexOf("#code=");
|
|
4908
|
+
if (hashIdx !== -1) {
|
|
4909
|
+
inviteCode = rawTarget.slice(hashIdx + 6);
|
|
4910
|
+
target = rawTarget.slice(0, hashIdx);
|
|
4911
|
+
}
|
|
4696
4912
|
const candidate = await getCandidate(store, target);
|
|
4697
4913
|
if (candidate && !candidate.card_url) {
|
|
4698
4914
|
throw new EdgeBookError("candidate_not_resolvable", "Candidate has no card_url to verify; cannot request");
|
|
4699
4915
|
}
|
|
4700
4916
|
const card = candidate ? await loadCard(candidate.card_url) : await loadCard(target);
|
|
4701
|
-
const envelope = await store.createFriendRequest(card);
|
|
4917
|
+
const envelope = await store.createFriendRequest(card, "", inviteCode);
|
|
4702
4918
|
if (candidate) await markCandidateApproved(store, candidate.candidate_id, card.agent_id);
|
|
4703
4919
|
if (deliver) {
|
|
4704
4920
|
const direct = card.transports.find((entry) => entry.mode === "direct")?.endpoint;
|
|
@@ -4796,6 +5012,15 @@ next: ${result.next_action}`, json: result };
|
|
|
4796
5012
|
const cfg = await store.updateConfig({ notify_on_friend_request: on ? true : false });
|
|
4797
5013
|
return { text: `notify_on_friend_request = ${cfg.notify_on_friend_request}`, json: cfg };
|
|
4798
5014
|
}
|
|
5015
|
+
if (action === "policy") {
|
|
5016
|
+
const open = takeBoolFlag(args, "--open");
|
|
5017
|
+
const inviteOnly = takeBoolFlag(args, "--invite-only");
|
|
5018
|
+
if (open && inviteOnly) throw new EdgeBookError("bad_flags", "policy takes either --open or --invite-only, not both");
|
|
5019
|
+
if (!open && !inviteOnly) throw new EdgeBookError("missing_arg", "policy needs --open or --invite-only");
|
|
5020
|
+
const cfg = await store.updateConfig({ open_friend_requests: open ? true : false });
|
|
5021
|
+
const mode = cfg.open_friend_requests === false ? "invite-only" : "open";
|
|
5022
|
+
return { text: `open_friend_requests = ${mode}`, json: cfg };
|
|
5023
|
+
}
|
|
4799
5024
|
}
|
|
4800
5025
|
if (command === "object") {
|
|
4801
5026
|
const action = args.shift();
|
|
@@ -5122,6 +5347,13 @@ ${JSON.stringify(result, null, 2)}`, json: result };
|
|
|
5122
5347
|
const all = await store.answers();
|
|
5123
5348
|
return { text: JSON.stringify(all, null, 2), json: all };
|
|
5124
5349
|
}
|
|
5350
|
+
if (command === "report") {
|
|
5351
|
+
const peer = requireArg(args.shift(), "peer-agent-id");
|
|
5352
|
+
const reason = takeFlag(args, "--reason") || "";
|
|
5353
|
+
const block = takeBoolFlag(args, "--block");
|
|
5354
|
+
const rec = await store.reportPeer(peer, reason, { block });
|
|
5355
|
+
return { text: `Reported ${peer}${block ? " and blocked" : ""} (report ${rec.report_id})`, json: rec };
|
|
5356
|
+
}
|
|
5125
5357
|
throw new EdgeBookError("unknown_command", usage());
|
|
5126
5358
|
}
|
|
5127
5359
|
async function runCli(args) {
|