edge-book 0.10.0 → 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 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(registryLookup = async () => null) {
2394
- return [localContactProvider, inviteProvider, cardUrlProvider, cardFileProvider, makeRegistryProvider(registryLookup)];
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
- if (!target.startsWith("registry:")) return null;
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
- const card = await loadCard(cardTarget);
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 handle lookup" }
2526
+ provenance: { source: "registry", confidence: "medium", display_name: card.handle, reason: "handle registry lookup" }
2453
2527
  };
2454
2528
  }
2455
2529
  };
@@ -4096,6 +4170,9 @@ function now2() {
4096
4170
  function keyId(agentKey) {
4097
4171
  return `agent_${crypto2.createHash("sha256").update(agentKey).digest("base64url").slice(0, 32)}`;
4098
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
+ }
4099
4176
  async function chmodBestEffort2(file, mode) {
4100
4177
  if (process.platform === "win32") return;
4101
4178
  try {
@@ -4423,6 +4500,21 @@ var EdgeBookDialoutClient = class {
4423
4500
  if (frame.type === "hello_ok") {
4424
4501
  this.opened?.resolve();
4425
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
+ }
4426
4518
  return;
4427
4519
  }
4428
4520
  if (frame.type === "hello_err") {
@@ -4485,6 +4577,7 @@ var EdgeBookDialoutClient = class {
4485
4577
  await this.handleMailboxDeliver(frame);
4486
4578
  return;
4487
4579
  }
4580
+ if (frameType === "handle_claim_ok" || frameType === "handle_claim_err") return;
4488
4581
  if (frameType === "error") return;
4489
4582
  if (frame.type !== "host.api.request" && frame.type !== "api_request") return;
4490
4583
  const response = await this.handleApiRequest(frame);
@@ -4625,6 +4718,27 @@ var COMMAND_GROUPS = [
4625
4718
  }
4626
4719
  ]
4627
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
+ },
4628
4742
  {
4629
4743
  title: "Profile",
4630
4744
  rows: [
@@ -4961,6 +5075,9 @@ function parseHome(args, ctx) {
4961
5075
  function parseHost(args, ctx) {
4962
5076
  return takeFlag(args, "--host") || ctx.defaultHost || process.env.EDGE_BOOK_HOST || DEFAULT_DIALOUT_HOST;
4963
5077
  }
5078
+ function relayBaseFromHost(host) {
5079
+ return host.replace(/\/agent\/ws\/?$/, "").replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://");
5080
+ }
4964
5081
  function requireArg(value, label) {
4965
5082
  if (!value) throw new EdgeBookError("missing_arg", `Missing ${label}`);
4966
5083
  return value;
@@ -5036,7 +5153,8 @@ async function handleCli(inputArgs, ctx = {}) {
5036
5153
  return { text: usage() };
5037
5154
  }
5038
5155
  if (command === "init") {
5039
- const handle = takeFlag(args, "--handle");
5156
+ const rawHandle = takeFlag(args, "--handle");
5157
+ const handle = rawHandle !== void 0 ? slugifyHandle(rawHandle) : void 0;
5040
5158
  const displayName = takeFlag(args, "--name");
5041
5159
  const ownerLabel = takeFlag(args, "--owner");
5042
5160
  const shareOwner = takeBoolFlag(args, "--share-owner");
@@ -5052,6 +5170,42 @@ Set it: edge-book profile set --name "<you>" --bio "..." --social telegram=@you
5052
5170
  Tune visibility: edge-book profile visibility bio=off telegram=public name=public`;
5053
5171
  return { text: note, json: identity };
5054
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
+ }
5055
5209
  if (command === "profile") {
5056
5210
  const action = args.shift() || "show";
5057
5211
  if (action === "show") {
@@ -5176,7 +5330,8 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
5176
5330
  }
5177
5331
  if (command === "resolve") {
5178
5332
  const target = requireArg(args.shift(), "target");
5179
- const result = await resolveTarget(store, target, { providers: defaultProviders() });
5333
+ const relayBase = relayBaseFromHost(parseHost(args, ctx));
5334
+ const result = await resolveTarget(store, target, { providers: defaultProviders(relayBase) });
5180
5335
  const label = result.agent_id ?? result.candidates?.[0]?.candidate_id ?? "";
5181
5336
  return { text: `${result.status} ${label}
5182
5337
  next: ${result.next_action}`, json: result };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edge-book",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Run your own Edge Book agent and connect it to the hosted reader.",
5
5
  "license": "MIT",
6
6
  "type": "module",