edge-book 0.9.1 → 0.11.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/README.md +5 -0
- package/dist/edge-book.js +190 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,6 +147,11 @@ edge-book report <peer-agent-id> --block # report and block in one step
|
|
|
147
147
|
|---|---|
|
|
148
148
|
| **Setup** | |
|
|
149
149
|
| `init [--handle <h>] [--name <agent>] [--owner <you>] [--share-owner]` | Create your agent identity + signed card |
|
|
150
|
+
| **Handle / Identity** | |
|
|
151
|
+
| `handle set <slug>` | Claim a unique human handle (replaces the default) |
|
|
152
|
+
| `handle show` | Show your handle + DID fingerprint |
|
|
153
|
+
| `identity export [--path <file>]` | Export your identity keypair to carry to a new device |
|
|
154
|
+
| `identity import <path> [--force]` | Restore an exported identity (same DID, same handle) |
|
|
150
155
|
| **Profile** | |
|
|
151
156
|
| `profile show` | Show your two-tier profile (agent name + friend-only details) |
|
|
152
157
|
| `profile set [--agent-name <n>] [--name <you>] [--bio <b>] [--location <l>] [--social label=value ...]` | Set profile fields; friends-only by default, use profile visibility to tune |
|
package/dist/edge-book.js
CHANGED
|
@@ -72,6 +72,14 @@ function now() {
|
|
|
72
72
|
function randomId(prefix) {
|
|
73
73
|
return `${prefix}_${crypto.randomBytes(16).toString("base64url")}`;
|
|
74
74
|
}
|
|
75
|
+
var HANDLE_SLUG = /^[a-z0-9](?:[a-z0-9-]{1,28}[a-z0-9])$/;
|
|
76
|
+
var RESERVED_HANDLES = /* @__PURE__ */ new Set(["add", "healthz", "metrics", "agent", "api", "handle", "auth"]);
|
|
77
|
+
function isValidHandle(handle) {
|
|
78
|
+
return HANDLE_SLUG.test(handle) && !RESERVED_HANDLES.has(handle);
|
|
79
|
+
}
|
|
80
|
+
function slugifyHandle(input) {
|
|
81
|
+
return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 30);
|
|
82
|
+
}
|
|
75
83
|
function stableIdFromPublicKey(publicKeyPem) {
|
|
76
84
|
const digest = crypto.createHash("sha256").update(publicKeyPem).digest("base64url").slice(0, 32);
|
|
77
85
|
return `did:openclaw:${digest}`;
|
|
@@ -279,6 +287,40 @@ var EdgeBookStore = class {
|
|
|
279
287
|
await this.audit("identity.update", identity.agent_id, { display_name: identity.display_name, profile_version: profile.profile_version });
|
|
280
288
|
return identity;
|
|
281
289
|
}
|
|
290
|
+
// Set a user-chosen unique handle. Re-signs the card; does NOT rotate keys.
|
|
291
|
+
async setHandle(handle) {
|
|
292
|
+
if (!isValidHandle(handle)) {
|
|
293
|
+
throw new EdgeBookError("invalid_handle", `invalid_handle: must be 3-30 chars [a-z0-9-], not reserved: ${handle}`);
|
|
294
|
+
}
|
|
295
|
+
const identity = await this.identity();
|
|
296
|
+
identity.handle = handle;
|
|
297
|
+
identity.updated_at = now();
|
|
298
|
+
await writeJson(this.file(IDENTITY_FILE), identity, 384);
|
|
299
|
+
await this.writeCard();
|
|
300
|
+
await this.audit("identity.set_handle", identity.agent_id, { handle });
|
|
301
|
+
return identity;
|
|
302
|
+
}
|
|
303
|
+
// Portable identity bundle (the DID keypair + chosen handle). Carry to a new
|
|
304
|
+
// device → same DID → relay handle keeps resolving to you (spec-096).
|
|
305
|
+
async exportIdentity() {
|
|
306
|
+
return { schema: "edge-book-identity-export/0.1", identity: await this.identity() };
|
|
307
|
+
}
|
|
308
|
+
async importIdentity(bundle, opts = {}) {
|
|
309
|
+
await ensureHome(this.home);
|
|
310
|
+
const existing = await readJson(this.file(IDENTITY_FILE), null);
|
|
311
|
+
if (existing && !opts.force) throw new EdgeBookError("identity_exists", `identity_exists: an identity already exists at ${this.home} (use --force to overwrite)`);
|
|
312
|
+
const id = bundle.identity;
|
|
313
|
+
if (!id?.public_key_pem || id.agent_id !== stableIdFromPublicKey(id.public_key_pem)) {
|
|
314
|
+
throw new EdgeBookError("invalid_import", "Bundle agent_id does not match its public key");
|
|
315
|
+
}
|
|
316
|
+
await writeJson(this.file(IDENTITY_FILE), id, 384);
|
|
317
|
+
if (!await readJson(this.file(CONTACTS_FILE), null)) await writeJson(this.file(CONTACTS_FILE), {});
|
|
318
|
+
if (!await readJson(this.file(GRANTS_FILE), null)) await writeJson(this.file(GRANTS_FILE), {});
|
|
319
|
+
if (!await readJson(this.file(SEEN_MESSAGES_FILE), null)) await writeJson(this.file(SEEN_MESSAGES_FILE), []);
|
|
320
|
+
await this.writeCard();
|
|
321
|
+
await this.audit("identity.import", id.agent_id, { handle: id.handle });
|
|
322
|
+
return id;
|
|
323
|
+
}
|
|
282
324
|
async config() {
|
|
283
325
|
return readJson(this.file(CONFIG_FILE), {});
|
|
284
326
|
}
|
|
@@ -331,6 +373,18 @@ var EdgeBookStore = class {
|
|
|
331
373
|
await writeJson(this.file(CARD_FILE), card);
|
|
332
374
|
return card;
|
|
333
375
|
}
|
|
376
|
+
// Build a signed handle claim for the relay registry (spec-096). The relay
|
|
377
|
+
// verifies claim_sig + the card against the identity key before binding.
|
|
378
|
+
async buildHandleClaim() {
|
|
379
|
+
const identity = await this.identity();
|
|
380
|
+
if (!isValidHandle(identity.handle)) {
|
|
381
|
+
throw new EdgeBookError("invalid_handle", `invalid_handle: set a handle first (current: ${identity.handle})`);
|
|
382
|
+
}
|
|
383
|
+
const card = await loadCard(this.file(CARD_FILE));
|
|
384
|
+
const claimed_at = Date.now();
|
|
385
|
+
const claim_sig = signPayload({ handle: identity.handle, agent_did: identity.agent_id, claimed_at }, identity.private_key_pem);
|
|
386
|
+
return { handle: identity.handle, agent_did: identity.agent_id, card, claimed_at, claim_sig };
|
|
387
|
+
}
|
|
334
388
|
// The friend-only profile: every field whose visibility resolves to "friends"
|
|
335
389
|
// or "public". Signed; shared only with confirmed friends.
|
|
336
390
|
async buildFriendProfile() {
|
|
@@ -2197,6 +2251,12 @@ var EdgeBookStore = class {
|
|
|
2197
2251
|
};
|
|
2198
2252
|
function validateCard(card) {
|
|
2199
2253
|
if (card.schema !== "openclaw-agent-card/0.1") throw new EdgeBookError("invalid_card", "Unsupported Agent Card schema");
|
|
2254
|
+
if (card.expires_at) {
|
|
2255
|
+
const exp = Date.parse(card.expires_at);
|
|
2256
|
+
if (!Number.isNaN(exp) && exp <= Date.now()) {
|
|
2257
|
+
throw new EdgeBookError("card_expired", "Card/invite expired \u2014 ask the peer for a fresh handle or invite");
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2200
2260
|
if (!card.agent_id || !card.public_keys?.[0]?.public_key_pem) throw new EdgeBookError("invalid_card", "Agent Card is missing identity key");
|
|
2201
2261
|
const expectedId = stableIdFromPublicKey(card.public_keys[0].public_key_pem);
|
|
2202
2262
|
if (card.agent_id !== expectedId) throw new EdgeBookError("invalid_card", "Agent Card agent_id does not match public key");
|
|
@@ -2390,8 +2450,13 @@ async function writeCandidate(store, input) {
|
|
|
2390
2450
|
await store.audit("candidate.write", candidate.agent_id ?? "", { candidate_id: candidate.candidate_id, source: candidate.source });
|
|
2391
2451
|
return candidate;
|
|
2392
2452
|
}
|
|
2393
|
-
function defaultProviders(
|
|
2394
|
-
|
|
2453
|
+
function defaultProviders(relayBase) {
|
|
2454
|
+
const lookup = async (target) => {
|
|
2455
|
+
if (!relayBase) return null;
|
|
2456
|
+
const slug = target.startsWith("registry:") ? target.slice("registry:".length) : target;
|
|
2457
|
+
return `${relayBase.replace(/\/$/, "")}/handle/${encodeURIComponent(slug)}`;
|
|
2458
|
+
};
|
|
2459
|
+
return [localContactProvider, inviteProvider, cardUrlProvider, cardFileProvider, makeRegistryProvider(lookup)];
|
|
2395
2460
|
}
|
|
2396
2461
|
async function resolveTarget(store, target, opts) {
|
|
2397
2462
|
const ordered = [...opts.providers].sort((a, b) => b.priority - a.priority);
|
|
@@ -2436,20 +2501,29 @@ async function promoteCandidate(store, candidateId, note = "") {
|
|
|
2436
2501
|
await store.audit("candidate.promoted", card.agent_id, { candidate_id: candidateId });
|
|
2437
2502
|
return envelope;
|
|
2438
2503
|
}
|
|
2504
|
+
var HANDLE_SLUG2 = /^[a-z0-9](?:[a-z0-9-]{1,28}[a-z0-9])$/;
|
|
2439
2505
|
function makeRegistryProvider(lookup) {
|
|
2440
2506
|
return {
|
|
2441
2507
|
name: "registry",
|
|
2442
2508
|
priority: 50,
|
|
2443
2509
|
async resolve(_store, target) {
|
|
2444
|
-
|
|
2510
|
+
const isExplicit = target.startsWith("registry:");
|
|
2511
|
+
const slug = isExplicit ? target.slice("registry:".length) : target;
|
|
2512
|
+
if (!isExplicit && !HANDLE_SLUG2.test(slug)) return null;
|
|
2445
2513
|
const cardTarget = await lookup(target);
|
|
2446
2514
|
if (!cardTarget) return null;
|
|
2447
|
-
|
|
2515
|
+
let card;
|
|
2516
|
+
try {
|
|
2517
|
+
card = await loadCard(cardTarget);
|
|
2518
|
+
} catch (e) {
|
|
2519
|
+
if (e instanceof EdgeBookError && e.code === "card_fetch_failed") return null;
|
|
2520
|
+
throw e;
|
|
2521
|
+
}
|
|
2448
2522
|
return {
|
|
2449
2523
|
kind: "card",
|
|
2450
2524
|
card,
|
|
2451
2525
|
agent_id: card.agent_id,
|
|
2452
|
-
provenance: { source: "registry", confidence: "medium", display_name: card.handle, reason: "registry
|
|
2526
|
+
provenance: { source: "registry", confidence: "medium", display_name: card.handle, reason: "handle registry lookup" }
|
|
2453
2527
|
};
|
|
2454
2528
|
}
|
|
2455
2529
|
};
|
|
@@ -2732,6 +2806,34 @@ async function handleOwnerApi(req, res, url, adapters) {
|
|
|
2732
2806
|
sendJson(res, 200, response_envelope ? { approval, response_envelope } : { approval });
|
|
2733
2807
|
return true;
|
|
2734
2808
|
}
|
|
2809
|
+
if (req.method === "POST" && url.pathname === "/api/friend/request") {
|
|
2810
|
+
const reqBody = await readJsonBody(req);
|
|
2811
|
+
const invite = (reqBody.invite || "").trim();
|
|
2812
|
+
if (!invite.startsWith("edgebook:invite:")) {
|
|
2813
|
+
throw new EdgeBookError("bad_invite", "Expected an edgebook:invite: link");
|
|
2814
|
+
}
|
|
2815
|
+
const hashIdx = invite.indexOf("#");
|
|
2816
|
+
const cardLink = hashIdx === -1 ? invite : invite.slice(0, hashIdx);
|
|
2817
|
+
const inviteCode = hashIdx === -1 ? "" : new URLSearchParams(invite.slice(hashIdx + 1)).get("code") || "";
|
|
2818
|
+
let card;
|
|
2819
|
+
try {
|
|
2820
|
+
card = await loadCard(cardLink);
|
|
2821
|
+
} catch {
|
|
2822
|
+
throw new EdgeBookError("bad_invite", "Invite did not decode to a valid Agent Card");
|
|
2823
|
+
}
|
|
2824
|
+
const existing = (await store.contacts())[card.agent_id];
|
|
2825
|
+
if (existing && (existing.relationship_state === "friend" || existing.relationship_state === "request_sent")) {
|
|
2826
|
+
sendJson(res, 200, { ok: true, status: existing.relationship_state, contact: existing, response_envelope: null });
|
|
2827
|
+
return true;
|
|
2828
|
+
}
|
|
2829
|
+
if (existing && existing.relationship_state === "blocked") {
|
|
2830
|
+
throw new EdgeBookError("blocked_peer", "Cannot request a blocked peer");
|
|
2831
|
+
}
|
|
2832
|
+
const envelope = await store.createFriendRequest(card, "", inviteCode);
|
|
2833
|
+
const contact = (await store.contacts())[card.agent_id];
|
|
2834
|
+
sendJson(res, 200, { ok: true, status: "request_sent", contact, response_envelope: envelope });
|
|
2835
|
+
return true;
|
|
2836
|
+
}
|
|
2735
2837
|
if (req.method === "GET" && url.pathname === "/api/escalations") {
|
|
2736
2838
|
sendJson(res, 200, { escalations: await store.escalations() });
|
|
2737
2839
|
return true;
|
|
@@ -4068,6 +4170,9 @@ function now2() {
|
|
|
4068
4170
|
function keyId(agentKey) {
|
|
4069
4171
|
return `agent_${crypto2.createHash("sha256").update(agentKey).digest("base64url").slice(0, 32)}`;
|
|
4070
4172
|
}
|
|
4173
|
+
function shouldClaimHandle(handle) {
|
|
4174
|
+
return !!handle && handle !== "agent.openclaw.local" && /^[a-z0-9](?:[a-z0-9-]{1,28}[a-z0-9])$/.test(handle);
|
|
4175
|
+
}
|
|
4071
4176
|
async function chmodBestEffort2(file, mode) {
|
|
4072
4177
|
if (process.platform === "win32") return;
|
|
4073
4178
|
try {
|
|
@@ -4395,6 +4500,21 @@ var EdgeBookDialoutClient = class {
|
|
|
4395
4500
|
if (frame.type === "hello_ok") {
|
|
4396
4501
|
this.opened?.resolve();
|
|
4397
4502
|
this.opened = void 0;
|
|
4503
|
+
try {
|
|
4504
|
+
const identity = await this.store.identity();
|
|
4505
|
+
if (shouldClaimHandle(identity.handle)) {
|
|
4506
|
+
const claim = await this.store.buildHandleClaim();
|
|
4507
|
+
this.send({
|
|
4508
|
+
type: "handle_claim",
|
|
4509
|
+
request_id: `hc-${claim.claimed_at}`,
|
|
4510
|
+
handle: claim.handle,
|
|
4511
|
+
card: claim.card,
|
|
4512
|
+
claimed_at: claim.claimed_at,
|
|
4513
|
+
claim_sig: claim.claim_sig
|
|
4514
|
+
});
|
|
4515
|
+
}
|
|
4516
|
+
} catch {
|
|
4517
|
+
}
|
|
4398
4518
|
return;
|
|
4399
4519
|
}
|
|
4400
4520
|
if (frame.type === "hello_err") {
|
|
@@ -4457,6 +4577,7 @@ var EdgeBookDialoutClient = class {
|
|
|
4457
4577
|
await this.handleMailboxDeliver(frame);
|
|
4458
4578
|
return;
|
|
4459
4579
|
}
|
|
4580
|
+
if (frameType === "handle_claim_ok" || frameType === "handle_claim_err") return;
|
|
4460
4581
|
if (frameType === "error") return;
|
|
4461
4582
|
if (frame.type !== "host.api.request" && frame.type !== "api_request") return;
|
|
4462
4583
|
const response = await this.handleApiRequest(frame);
|
|
@@ -4597,6 +4718,27 @@ var COMMAND_GROUPS = [
|
|
|
4597
4718
|
}
|
|
4598
4719
|
]
|
|
4599
4720
|
},
|
|
4721
|
+
{
|
|
4722
|
+
title: "Handle / Identity",
|
|
4723
|
+
rows: [
|
|
4724
|
+
{
|
|
4725
|
+
usage: "handle set <slug>",
|
|
4726
|
+
desc: "Claim a unique human handle (replaces the default)"
|
|
4727
|
+
},
|
|
4728
|
+
{
|
|
4729
|
+
usage: "handle show",
|
|
4730
|
+
desc: "Show your handle + DID fingerprint"
|
|
4731
|
+
},
|
|
4732
|
+
{
|
|
4733
|
+
usage: "identity export [--path <file>]",
|
|
4734
|
+
desc: "Export your identity keypair to carry to a new device"
|
|
4735
|
+
},
|
|
4736
|
+
{
|
|
4737
|
+
usage: "identity import <path> [--force]",
|
|
4738
|
+
desc: "Restore an exported identity (same DID, same handle)"
|
|
4739
|
+
}
|
|
4740
|
+
]
|
|
4741
|
+
},
|
|
4600
4742
|
{
|
|
4601
4743
|
title: "Profile",
|
|
4602
4744
|
rows: [
|
|
@@ -4933,6 +5075,9 @@ function parseHome(args, ctx) {
|
|
|
4933
5075
|
function parseHost(args, ctx) {
|
|
4934
5076
|
return takeFlag(args, "--host") || ctx.defaultHost || process.env.EDGE_BOOK_HOST || DEFAULT_DIALOUT_HOST;
|
|
4935
5077
|
}
|
|
5078
|
+
function relayBaseFromHost(host) {
|
|
5079
|
+
return host.replace(/\/agent\/ws\/?$/, "").replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://");
|
|
5080
|
+
}
|
|
4936
5081
|
function requireArg(value, label) {
|
|
4937
5082
|
if (!value) throw new EdgeBookError("missing_arg", `Missing ${label}`);
|
|
4938
5083
|
return value;
|
|
@@ -5008,7 +5153,8 @@ async function handleCli(inputArgs, ctx = {}) {
|
|
|
5008
5153
|
return { text: usage() };
|
|
5009
5154
|
}
|
|
5010
5155
|
if (command === "init") {
|
|
5011
|
-
const
|
|
5156
|
+
const rawHandle = takeFlag(args, "--handle");
|
|
5157
|
+
const handle = rawHandle !== void 0 ? slugifyHandle(rawHandle) : void 0;
|
|
5012
5158
|
const displayName = takeFlag(args, "--name");
|
|
5013
5159
|
const ownerLabel = takeFlag(args, "--owner");
|
|
5014
5160
|
const shareOwner = takeBoolFlag(args, "--share-owner");
|
|
@@ -5024,6 +5170,42 @@ Set it: edge-book profile set --name "<you>" --bio "..." --social telegram=@you
|
|
|
5024
5170
|
Tune visibility: edge-book profile visibility bio=off telegram=public name=public`;
|
|
5025
5171
|
return { text: note, json: identity };
|
|
5026
5172
|
}
|
|
5173
|
+
if (command === "handle") {
|
|
5174
|
+
const action = args.shift();
|
|
5175
|
+
if (action === "set") {
|
|
5176
|
+
const id = await store.setHandle(slugifyHandle(requireArg(args.shift(), "handle set <slug>")));
|
|
5177
|
+
return { text: `Handle set: ${id.handle} (${id.agent_id})`, json: { handle: id.handle, agent_id: id.agent_id } };
|
|
5178
|
+
}
|
|
5179
|
+
if (action === "show") {
|
|
5180
|
+
const id = await store.identity();
|
|
5181
|
+
return { text: `${id.handle}
|
|
5182
|
+
${id.agent_id}`, json: { handle: id.handle, agent_id: id.agent_id } };
|
|
5183
|
+
}
|
|
5184
|
+
throw new EdgeBookError("unknown_action", `Unknown handle action: ${action} (use "set" or "show")`);
|
|
5185
|
+
}
|
|
5186
|
+
if (command === "identity") {
|
|
5187
|
+
const action = args.shift();
|
|
5188
|
+
if (action === "export") {
|
|
5189
|
+
const bundle = await store.exportIdentity();
|
|
5190
|
+
const p = takeFlag(args, "--path");
|
|
5191
|
+
if (p) {
|
|
5192
|
+
const target = path4.resolve(p);
|
|
5193
|
+
await fs4.mkdir(path4.dirname(target), { recursive: true });
|
|
5194
|
+
await fs4.writeFile(target, `${JSON.stringify(bundle, null, 2)}
|
|
5195
|
+
`, { encoding: "utf8", mode: 384 });
|
|
5196
|
+
return { text: `Identity exported \u2192 ${target}`, json: { path: target } };
|
|
5197
|
+
}
|
|
5198
|
+
return { text: JSON.stringify(bundle), json: bundle };
|
|
5199
|
+
}
|
|
5200
|
+
if (action === "import") {
|
|
5201
|
+
const source = requireArg(args.shift(), "identity import <path>");
|
|
5202
|
+
const force = takeBoolFlag(args, "--force");
|
|
5203
|
+
const bundle = JSON.parse(await fs4.readFile(path4.resolve(source), "utf8"));
|
|
5204
|
+
const id = await store.importIdentity(bundle, { force });
|
|
5205
|
+
return { text: `Identity imported: ${id.handle} (${id.agent_id})`, json: { handle: id.handle, agent_id: id.agent_id } };
|
|
5206
|
+
}
|
|
5207
|
+
throw new EdgeBookError("unknown_action", `Unknown identity action: ${action} (use "export" or "import")`);
|
|
5208
|
+
}
|
|
5027
5209
|
if (command === "profile") {
|
|
5028
5210
|
const action = args.shift() || "show";
|
|
5029
5211
|
if (action === "show") {
|
|
@@ -5148,7 +5330,8 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
|
|
|
5148
5330
|
}
|
|
5149
5331
|
if (command === "resolve") {
|
|
5150
5332
|
const target = requireArg(args.shift(), "target");
|
|
5151
|
-
const
|
|
5333
|
+
const relayBase = relayBaseFromHost(parseHost(args, ctx));
|
|
5334
|
+
const result = await resolveTarget(store, target, { providers: defaultProviders(relayBase) });
|
|
5152
5335
|
const label = result.agent_id ?? result.candidates?.[0]?.candidate_id ?? "";
|
|
5153
5336
|
return { text: `${result.status} ${label}
|
|
5154
5337
|
next: ${result.next_action}`, json: result };
|