@bolloon/bolloon-agent 0.4.29 → 0.4.30

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.
Files changed (47) hide show
  1. package/dist/agents/contacts/chain.js +579 -0
  2. package/dist/agents/contacts/cli.js +158 -0
  3. package/dist/agents/contacts/consent.js +124 -0
  4. package/dist/agents/contacts/grant-payload.js +45 -0
  5. package/dist/agents/contacts/grants.js +451 -0
  6. package/dist/agents/contacts/policy.js +264 -0
  7. package/dist/agents/contacts/preview-types.js +6 -0
  8. package/dist/agents/contacts/providers.js +341 -0
  9. package/dist/agents/contacts/store.js +326 -0
  10. package/dist/agents/contacts/tools.js +154 -0
  11. package/dist/agents/contacts/types.js +148 -0
  12. package/dist/agents/pi-sdk.js +10 -0
  13. package/dist/electron-build/agents/contacts/chain.js +619 -0
  14. package/dist/electron-build/agents/contacts/chain.js.map +1 -0
  15. package/dist/electron-build/agents/contacts/consent.js +163 -0
  16. package/dist/electron-build/agents/contacts/consent.js.map +1 -0
  17. package/dist/electron-build/agents/contacts/grant-payload.js +52 -0
  18. package/dist/electron-build/agents/contacts/grant-payload.js.map +1 -0
  19. package/dist/electron-build/agents/contacts/grants.js +500 -0
  20. package/dist/electron-build/agents/contacts/grants.js.map +1 -0
  21. package/dist/electron-build/agents/contacts/policy.js +271 -0
  22. package/dist/electron-build/agents/contacts/policy.js.map +1 -0
  23. package/dist/electron-build/agents/contacts/preview-types.js +8 -0
  24. package/dist/electron-build/agents/contacts/preview-types.js.map +1 -0
  25. package/dist/electron-build/agents/contacts/providers.js +384 -0
  26. package/dist/electron-build/agents/contacts/providers.js.map +1 -0
  27. package/dist/electron-build/agents/contacts/store.js +370 -0
  28. package/dist/electron-build/agents/contacts/store.js.map +1 -0
  29. package/dist/electron-build/agents/contacts/tools.js +159 -0
  30. package/dist/electron-build/agents/contacts/tools.js.map +1 -0
  31. package/dist/electron-build/agents/contacts/types.js +166 -0
  32. package/dist/electron-build/agents/contacts/types.js.map +1 -0
  33. package/dist/electron-build/agents/external-events.js.map +1 -1
  34. package/dist/electron-build/agents/goal-store.js.map +1 -1
  35. package/dist/electron-build/agents/pi-sdk.js +10 -0
  36. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  37. package/dist/electron-build/security/tool-gate.js +4 -0
  38. package/dist/electron-build/security/tool-gate.js.map +1 -1
  39. package/dist/index.js +30 -0
  40. package/dist/security/tool-gate.js +4 -0
  41. package/dist/web/mobile-contacts.js +329 -0
  42. package/dist/web/mobile-core.js +481 -15
  43. package/dist/web/mobile.html +32 -0
  44. package/dist/web/mobile.js +139 -0
  45. package/dist/web/routes-contacts.js +420 -0
  46. package/dist/web/server.js +5 -0
  47. package/package.json +1 -1
@@ -71270,7 +71270,7 @@ ${[...listenStats.errors.entries()].map(([addr, err2]) => {
71270
71270
  i = this._head + i & this._capacityMask;
71271
71271
  return this._list[i];
71272
71272
  };
71273
- Denque2.prototype.get = function get2(i) {
71273
+ Denque2.prototype.get = function get3(i) {
71274
71274
  return this.peekAt(i);
71275
71275
  };
71276
71276
  Denque2.prototype.peek = function peek() {
@@ -103541,7 +103541,7 @@ ${r.steps.map((s3) => `${s3.ok ? "\u2713" : "\u2717"} ${s3.step}: ${s3.note}`).j
103541
103541
  }
103542
103542
  }
103543
103543
  }
103544
- function get(source, path) {
103544
+ function get2(source, path) {
103545
103545
  let node2 = source;
103546
103546
  for (const [index, key] of path.entries()) {
103547
103547
  node2 = node2[key];
@@ -103597,7 +103597,7 @@ ${r.steps.map((s3) => `${s3.ok ? "\u2713" : "\u2717"} ${s3.step}: ${s3.note}`).j
103597
103597
  return tree(this.value, []);
103598
103598
  }
103599
103599
  get(path = "/") {
103600
- return get(this.value, path.split("/").filter(Boolean));
103600
+ return get2(this.value, path.split("/").filter(Boolean));
103601
103601
  }
103602
103602
  };
103603
103603
  }
@@ -119947,46 +119947,46 @@ ${values.join("\n")}` : `${blockName} :`;
119947
119947
  var len;
119948
119948
  revLookup["-".charCodeAt(0)] = 62;
119949
119949
  revLookup["_".charCodeAt(0)] = 63;
119950
- function getLens(b64) {
119951
- var len2 = b64.length;
119950
+ function getLens(b642) {
119951
+ var len2 = b642.length;
119952
119952
  if (len2 % 4 > 0) {
119953
119953
  throw new Error("Invalid string. Length must be a multiple of 4");
119954
119954
  }
119955
- var validLen = b64.indexOf("=");
119955
+ var validLen = b642.indexOf("=");
119956
119956
  if (validLen === -1) validLen = len2;
119957
119957
  var placeHoldersLen = validLen === len2 ? 0 : 4 - validLen % 4;
119958
119958
  return [validLen, placeHoldersLen];
119959
119959
  }
119960
- function byteLength2(b64) {
119961
- var lens = getLens(b64);
119960
+ function byteLength2(b642) {
119961
+ var lens = getLens(b642);
119962
119962
  var validLen = lens[0];
119963
119963
  var placeHoldersLen = lens[1];
119964
119964
  return (validLen + placeHoldersLen) * 3 / 4 - placeHoldersLen;
119965
119965
  }
119966
- function _byteLength(b64, validLen, placeHoldersLen) {
119966
+ function _byteLength(b642, validLen, placeHoldersLen) {
119967
119967
  return (validLen + placeHoldersLen) * 3 / 4 - placeHoldersLen;
119968
119968
  }
119969
- function toByteArray(b64) {
119969
+ function toByteArray(b642) {
119970
119970
  var tmp;
119971
- var lens = getLens(b64);
119971
+ var lens = getLens(b642);
119972
119972
  var validLen = lens[0];
119973
119973
  var placeHoldersLen = lens[1];
119974
- var arr = new Arr(_byteLength(b64, validLen, placeHoldersLen));
119974
+ var arr = new Arr(_byteLength(b642, validLen, placeHoldersLen));
119975
119975
  var curByte = 0;
119976
119976
  var len2 = placeHoldersLen > 0 ? validLen - 4 : validLen;
119977
119977
  var i2;
119978
119978
  for (i2 = 0; i2 < len2; i2 += 4) {
119979
- tmp = revLookup[b64.charCodeAt(i2)] << 18 | revLookup[b64.charCodeAt(i2 + 1)] << 12 | revLookup[b64.charCodeAt(i2 + 2)] << 6 | revLookup[b64.charCodeAt(i2 + 3)];
119979
+ tmp = revLookup[b642.charCodeAt(i2)] << 18 | revLookup[b642.charCodeAt(i2 + 1)] << 12 | revLookup[b642.charCodeAt(i2 + 2)] << 6 | revLookup[b642.charCodeAt(i2 + 3)];
119980
119980
  arr[curByte++] = tmp >> 16 & 255;
119981
119981
  arr[curByte++] = tmp >> 8 & 255;
119982
119982
  arr[curByte++] = tmp & 255;
119983
119983
  }
119984
119984
  if (placeHoldersLen === 2) {
119985
- tmp = revLookup[b64.charCodeAt(i2)] << 2 | revLookup[b64.charCodeAt(i2 + 1)] >> 4;
119985
+ tmp = revLookup[b642.charCodeAt(i2)] << 2 | revLookup[b642.charCodeAt(i2 + 1)] >> 4;
119986
119986
  arr[curByte++] = tmp & 255;
119987
119987
  }
119988
119988
  if (placeHoldersLen === 1) {
119989
- tmp = revLookup[b64.charCodeAt(i2)] << 10 | revLookup[b64.charCodeAt(i2 + 1)] << 4 | revLookup[b64.charCodeAt(i2 + 2)] >> 2;
119989
+ tmp = revLookup[b642.charCodeAt(i2)] << 10 | revLookup[b642.charCodeAt(i2 + 1)] << 4 | revLookup[b642.charCodeAt(i2 + 2)] >> 2;
119990
119990
  arr[curByte++] = tmp >> 8 & 255;
119991
119991
  arr[curByte++] = tmp & 255;
119992
119992
  }
@@ -162923,6 +162923,365 @@ ${error.stack}` : head;
162923
162923
  return result;
162924
162924
  }
162925
162925
 
162926
+ // src/agents/contacts/grant-payload.ts
162927
+ function canonicalGrantPayload(g) {
162928
+ return JSON.stringify({
162929
+ grantId: g.grantId,
162930
+ identityId: g.identityId,
162931
+ ownerDid: g.ownerDid,
162932
+ level: g.level,
162933
+ channels: g.channels,
162934
+ contactScope: g.contactScope,
162935
+ taskScope: g.taskScope,
162936
+ contentScope: g.contentScope,
162937
+ replyWakeAllowed: g.replyWakeAllowed,
162938
+ autoSend: g.autoSend,
162939
+ sensitiveContentAllowed: g.sensitiveContentAllowed,
162940
+ newRecipientAllowed: g.newRecipientAllowed,
162941
+ grantedAt: g.grantedAt,
162942
+ grantedBy: g.grantedBy,
162943
+ grantedVia: g.grantedVia,
162944
+ deviceIds: [...g.deviceIds].sort(),
162945
+ status: g.status,
162946
+ revokedAt: g.revokedAt || "",
162947
+ grantVersion: g.grantVersion
162948
+ });
162949
+ }
162950
+ function canonicalRevocationPayload(r) {
162951
+ return JSON.stringify({ grantId: r.grantId, grantVersion: r.grantVersion, revokedAt: r.revokedAt, by: r.by });
162952
+ }
162953
+ function presetForChoice(choice) {
162954
+ const base6 = { channels: "both", replyWakeAllowed: true, autoSend: true, newRecipientAllowed: false };
162955
+ switch (choice) {
162956
+ case "task_once":
162957
+ return { ...base6, level: "task_once", contactScope: "verified_contacts", taskScope: "current_goal", contentScope: "normal", autoSend: false, sensitiveContentAllowed: false };
162958
+ case "persistent":
162959
+ return { ...base6, level: "persistent", contactScope: "verified_contacts", taskScope: "all_future_goals", contentScope: "normal", sensitiveContentAllowed: false };
162960
+ case "full_contact_access":
162961
+ return { ...base6, level: "full_contact_access", contactScope: "all_contacts", taskScope: "all_future_goals", contentScope: "sensitive", sensitiveContentAllowed: true };
162962
+ }
162963
+ }
162964
+
162965
+ // src/web/mobile-contacts.ts
162966
+ var K_DEVICE = "bolloon.contacts.device.v1";
162967
+ var K_QUEUE = "bolloon.contacts.queue.v1";
162968
+ var K_CAPS = "bolloon.contacts.capabilities.v1";
162969
+ var cryptoOf = (c) => c || globalThis.crypto || null;
162970
+ function ed25519Available(c) {
162971
+ const cobj = cryptoOf(c);
162972
+ if (!cobj?.subtle) return false;
162973
+ try {
162974
+ const alg = { name: "Ed25519" };
162975
+ return typeof cobj.subtle.importKey === "function" && !!alg;
162976
+ } catch {
162977
+ return false;
162978
+ }
162979
+ }
162980
+ function bytesOf(s2) {
162981
+ const src4 = new TextEncoder().encode(s2);
162982
+ const out = new Uint8Array(new ArrayBuffer(src4.byteLength));
162983
+ out.set(src4);
162984
+ return out;
162985
+ }
162986
+ function b64(buf) {
162987
+ const arr = new Uint8Array(buf);
162988
+ let out = "";
162989
+ for (let i = 0; i < arr.length; i++) out += String.fromCharCode(arr[i]);
162990
+ return btoa(out);
162991
+ }
162992
+ function pemFromSpki(spki) {
162993
+ const b = b64(spki).replace(/(.{64})/g, "$1\n").trim();
162994
+ return `-----BEGIN PUBLIC KEY-----
162995
+ ${b}
162996
+ -----END PUBLIC KEY-----
162997
+ `;
162998
+ }
162999
+ async function sha256Hex(c, text) {
163000
+ const d2 = await c.subtle.digest("SHA-256", bytesOf(text));
163001
+ return [...new Uint8Array(d2)].map((x) => x.toString(16).padStart(2, "0")).join("");
163002
+ }
163003
+ async function loadOrCreateDeviceKey(storage2, c) {
163004
+ const cobj = cryptoOf(c);
163005
+ if (!cobj?.subtle) throw new Error("device_signing_unavailable: \u672C\u673A\u6CA1\u6709 WebCrypto");
163006
+ const raw = storage2.getItem(K_DEVICE);
163007
+ if (raw) {
163008
+ try {
163009
+ const parsed = JSON.parse(raw);
163010
+ if (parsed?.privateKeyJwk && parsed?.publicKeyPem && parsed?.deviceId) return parsed;
163011
+ } catch {
163012
+ }
163013
+ }
163014
+ let pair;
163015
+ try {
163016
+ pair = await cobj.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]);
163017
+ } catch (err2) {
163018
+ throw new Error(`device_signing_unavailable: ${String(err2?.message || err2)}`);
163019
+ }
163020
+ const publicKeyPem = pemFromSpki(await cobj.subtle.exportKey("spki", pair.publicKey));
163021
+ const privateKeyJwk = await cobj.subtle.exportKey("jwk", pair.privateKey);
163022
+ const deviceId = `dev-${(await sha256Hex(cobj, publicKeyPem)).slice(0, 12)}`;
163023
+ const material = { deviceId, publicKeyPem, privateKeyJwk };
163024
+ storage2.setItem(K_DEVICE, JSON.stringify(material));
163025
+ return material;
163026
+ }
163027
+ async function importPrivate(storage2, c) {
163028
+ const k = await loadOrCreateDeviceKey(storage2, c);
163029
+ return c.subtle.importKey("jwk", k.privateKeyJwk, { name: "Ed25519" }, false, ["sign"]);
163030
+ }
163031
+ async function signGrantOnDevice(grant, storage2, c) {
163032
+ const cobj = cryptoOf(c);
163033
+ const key = await importPrivate(storage2, cobj);
163034
+ const payload = canonicalGrantPayload(grant);
163035
+ const sig = await cobj.subtle.sign({ name: "Ed25519" }, key, bytesOf(payload));
163036
+ const dev = await loadOrCreateDeviceKey(storage2, cobj);
163037
+ return { deviceId: dev.deviceId, alg: "ed25519", payloadHash: await sha256Hex(cobj, payload), sig: b64(sig) };
163038
+ }
163039
+ async function signRevocationOnDevice(rev, storage2, c) {
163040
+ const cobj = cryptoOf(c);
163041
+ const key = await importPrivate(storage2, cobj);
163042
+ const payload = canonicalRevocationPayload(rev);
163043
+ const sig = await cobj.subtle.sign({ name: "Ed25519" }, key, bytesOf(payload));
163044
+ const dev = await loadOrCreateDeviceKey(storage2, cobj);
163045
+ return { deviceId: dev.deviceId, alg: "ed25519", payloadHash: await sha256Hex(cobj, payload), sig: b64(sig) };
163046
+ }
163047
+ async function post(base6, path, body, f) {
163048
+ const res = await f(`${String(base6).replace(/\/+$/, "")}${path}`, {
163049
+ method: "POST",
163050
+ headers: { "Content-Type": "application/json" },
163051
+ body: JSON.stringify(body ?? {})
163052
+ });
163053
+ let json = null;
163054
+ try {
163055
+ json = await res.json();
163056
+ } catch {
163057
+ }
163058
+ return { status: res.status, json };
163059
+ }
163060
+ async function get(base6, path, f) {
163061
+ const res = await f(`${String(base6).replace(/\/+$/, "")}${path}`);
163062
+ let json = null;
163063
+ try {
163064
+ json = await res.json();
163065
+ } catch {
163066
+ }
163067
+ return { status: res.status, json };
163068
+ }
163069
+ async function registerDeviceOnDesktop(base6, storage2, opts = {}) {
163070
+ const f = opts.fetchImpl || fetch;
163071
+ const dev = await loadOrCreateDeviceKey(storage2, opts.cryptoObj);
163072
+ const r = await post(base6, "/api/contacts/devices", { deviceId: dev.deviceId, publicKeyPem: dev.publicKeyPem, label: opts.label || "\u624B\u673A" }, f);
163073
+ if (r.status !== 200 || !r.json?.ok) return { ok: false, error: `\u684C\u9762\u672A\u63A5\u53D7\u8BBE\u5907\u767B\u8BB0: ${r.json?.error || r.status}`, data: dev };
163074
+ return { ok: true, data: dev };
163075
+ }
163076
+ function saveCapabilityCopy(storage2, items) {
163077
+ const clean8 = (items || []).map((c) => ({
163078
+ contactId: String(c.contactId || ""),
163079
+ kind: c.kind,
163080
+ displayValue: String(c.displayValue || ""),
163081
+ verificationStatus: String(c.verificationStatus || ""),
163082
+ capabilities: [...c.capabilities || []],
163083
+ provider: c.provider ? String(c.provider) : void 0,
163084
+ state: c.state ? String(c.state) : void 0
163085
+ }));
163086
+ storage2.setItem(K_CAPS, JSON.stringify(clean8));
163087
+ }
163088
+ function loadCapabilityCopy(storage2) {
163089
+ try {
163090
+ const v = JSON.parse(storage2.getItem(K_CAPS) || "[]");
163091
+ return Array.isArray(v) ? v : [];
163092
+ } catch {
163093
+ return [];
163094
+ }
163095
+ }
163096
+ function pendingQueue(storage2) {
163097
+ try {
163098
+ const v = JSON.parse(storage2.getItem(K_QUEUE) || "[]");
163099
+ return Array.isArray(v) ? v : [];
163100
+ } catch {
163101
+ return [];
163102
+ }
163103
+ }
163104
+ function setQueue(storage2, q) {
163105
+ storage2.setItem(K_QUEUE, JSON.stringify(q.slice(-20)));
163106
+ }
163107
+ async function flushQueuedGrants(base6, storage2, opts = {}) {
163108
+ const f = opts.fetchImpl || fetch;
163109
+ const q = pendingQueue(storage2);
163110
+ const keep = [];
163111
+ const results = [];
163112
+ let sent = 0, failed = 0;
163113
+ for (const item of q) {
163114
+ try {
163115
+ const r = await post(base6, "/api/contacts/grants/sync", { grant: { ...item.grant, signature: item.signature } }, f);
163116
+ if (r.status === 200 && r.json?.ok) {
163117
+ sent++;
163118
+ results.push({ grantId: item.grant.grantId, ok: true });
163119
+ continue;
163120
+ }
163121
+ failed++;
163122
+ keep.push(item);
163123
+ results.push({ grantId: item.grant.grantId, ok: false, error: r.json?.code || r.status });
163124
+ } catch {
163125
+ failed++;
163126
+ keep.push(item);
163127
+ results.push({ grantId: item.grant.grantId, ok: false, error: "network" });
163128
+ }
163129
+ }
163130
+ setQueue(storage2, keep);
163131
+ return { sent, failed, results };
163132
+ }
163133
+ async function authorizeFromPhone(input) {
163134
+ const c = cryptoOf(input.cryptoObj);
163135
+ if (!ed25519Available(c || void 0)) {
163136
+ return { ok: false, error: "device_signing_unavailable: \u672C\u673A WebView \u4E0D\u652F\u6301 Ed25519 \u8BBE\u5907\u7B7E\u540D \u2014\u2014 \u8BF7\u5728\u684C\u9762\u7AEF\u6388\u6743 (\u6216\u5347\u7EA7\u7CFB\u7EDF WebView)" };
163137
+ }
163138
+ const dev = await loadOrCreateDeviceKey(input.storage, c);
163139
+ const preset = presetForChoice(input.choice);
163140
+ const grant = {
163141
+ grantId: `gr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
163142
+ identityId: input.identityId || `sid-phone-${dev.deviceId.slice(-6)}`,
163143
+ ownerDid: input.ownerDid,
163144
+ ...preset,
163145
+ grantedAt: (/* @__PURE__ */ new Date()).toISOString(),
163146
+ grantedBy: input.grantedBy || "leo",
163147
+ grantedVia: "mobile",
163148
+ deviceIds: [dev.deviceId],
163149
+ status: "active",
163150
+ grantVersion: 1
163151
+ };
163152
+ const signature = await signGrantOnDevice(grant, input.storage, c);
163153
+ const f = input.fetchImpl || fetch;
163154
+ const reg = await registerDeviceOnDesktop(input.base, input.storage, { fetchImpl: f, cryptoObj: c }).catch(() => ({ ok: false }));
163155
+ if (!reg.ok) {
163156
+ const q2 = pendingQueue(input.storage);
163157
+ q2.push({ grant, signature, queuedAt: (/* @__PURE__ */ new Date()).toISOString() });
163158
+ setQueue(input.storage, q2);
163159
+ return {
163160
+ ok: false,
163161
+ queued: true,
163162
+ error: `\u684C\u9762\u6682\u4E0D\u53EF\u8FBE \u2192 \u6388\u6743\u5DF2\u5B58\u5728\u624B\u673A, \u5F85\u684C\u9762\u5728\u7EBF\u65F6\u81EA\u52A8\u540C\u6B65 (${reg.error || "offline"})`,
163163
+ data: { grantId: grant.grantId, level: grant.level, signature },
163164
+ note: "\u684C\u9762\u4E0D\u5728\u7EBF\u65F6\u4E0D\u5047\u88C5\u5DF2\u751F\u6548: \u6388\u6743\u5728\u672C\u5730\u961F\u5217\u91CC, \u684C\u9762\u4E00\u4E0A\u7EBF\u5C31\u8865\u540C\u6B65"
163165
+ };
163166
+ }
163167
+ const r = await post(input.base, "/api/contacts/grants/sync", { grant: { ...grant, signature } }, f);
163168
+ if (r.status === 200 && r.json?.ok) {
163169
+ return { ok: true, data: { grantId: grant.grantId, level: grant.level, signature }, note: "\u684C\u9762\u5DF2\u9A8C\u7B7E\u63A5\u53D7" };
163170
+ }
163171
+ const q = pendingQueue(input.storage);
163172
+ q.push({ grant, signature, queuedAt: (/* @__PURE__ */ new Date()).toISOString() });
163173
+ setQueue(input.storage, q);
163174
+ return {
163175
+ ok: false,
163176
+ queued: true,
163177
+ error: `\u684C\u9762\u62D2\u7EDD/\u672A\u63A5\u53D7: ${r.json?.code || r.json?.reason || r.status}`,
163178
+ data: { grantId: grant.grantId, level: grant.level, signature },
163179
+ note: "\u5DF2\u4FDD\u5B58\u5728\u624B\u673A\u672C\u5730\u961F\u5217 (\u4E0D\u4F1A\u56E0\u4E3A\u5931\u8D25\u5C31\u5F53\u6210\u5DF2\u6388\u6743)"
163180
+ };
163181
+ }
163182
+ async function revokeFromPhone(opts) {
163183
+ const c = cryptoOf(opts.cryptoObj);
163184
+ if (!ed25519Available(c || void 0)) return { ok: false, error: "device_signing_unavailable: \u672C\u673A\u4E0D\u652F\u6301\u8BBE\u5907\u7B7E\u540D, \u64A4\u9500\u9700\u5728\u684C\u9762\u7AEF\u8FDB\u884C" };
163185
+ const revokedAt = (/* @__PURE__ */ new Date()).toISOString();
163186
+ const payload = { grantId: opts.grantId, grantVersion: opts.version || 1, revokedAt, by: "mobile" };
163187
+ const signature = await signRevocationOnDevice(payload, opts.storage, c);
163188
+ const f = opts.fetchImpl || fetch;
163189
+ const r = await post(opts.base, "/api/contacts/grants/revoke-sync", { grantId: opts.grantId, by: "mobile", reason: opts.reason, version: payload.grantVersion, revokedAt, signature }, f);
163190
+ if (r.status === 200 && r.json?.ok) return { ok: true, data: { grantId: opts.grantId }, note: "\u684C\u9762\u5DF2\u9A8C\u7B7E\u5E76\u7ACB\u5373\u5931\u6548" };
163191
+ return { ok: false, error: `\u64A4\u9500\u672A\u9001\u8FBE: ${r.json?.code || r.json?.error || r.status}`, note: "\u684C\u9762\u4E0D\u5728\u7EBF\u65F6\u64A4\u9500**\u4E0D\u4F1A**\u88AB\u5F53\u4F5C\u5DF2\u5B8C\u6210 \u2014\u2014 \u8BF7\u5728\u684C\u9762\u5728\u7EBF\u540E\u91CD\u8BD5 (\u6216\u684C\u9762\u7AEF\u64A4\u9500)" };
163192
+ }
163193
+ async function loadFromDesktop(base6, storage2, opts = {}) {
163194
+ const f = opts.fetchImpl || fetch;
163195
+ if (!base6) {
163196
+ const caps = loadCapabilityCopy(storage2);
163197
+ return {
163198
+ ok: false,
163199
+ error: "desktop_offline",
163200
+ data: { contacts: caps, grants: [], effective: "\u684C\u9762\u4E0D\u5728\u7EBF (\u663E\u793A\u624B\u673A\u672C\u5730\u526F\u672C)", approvals: [], waiting: [] },
163201
+ note: "\u4EE5\u4E0B\u662F\u4F60\u624B\u673A\u4E0A\u4FDD\u5B58\u7684\u80FD\u529B\u526F\u672C (\u4E0D\u662F\u684C\u9762\u5B9E\u65F6\u72B6\u6001)"
163202
+ };
163203
+ }
163204
+ try {
163205
+ const a = await get(base6, "/api/contacts", f);
163206
+ const b = await get(base6, "/api/contacts/grants", f);
163207
+ if (a.status !== 200 && b.status !== 200) return { ok: false, error: `desktop_http_${a.status || b.status}` };
163208
+ const view = {
163209
+ contacts: a.json?.contacts || [],
163210
+ grants: b.json?.grants || [],
163211
+ effective: b.json?.effective || "",
163212
+ approvals: a.json?.approvals || [],
163213
+ waiting: a.json?.waiting || []
163214
+ };
163215
+ saveCapabilityCopy(storage2, (view.contacts || []).filter((c) => c.capabilities?.includes("send")).map((c) => ({
163216
+ contactId: c.contactId,
163217
+ kind: c.kind,
163218
+ displayValue: c.displayValue,
163219
+ verificationStatus: c.status,
163220
+ capabilities: c.capabilities,
163221
+ provider: c.provider,
163222
+ state: c.status
163223
+ })));
163224
+ return { ok: true, data: view };
163225
+ } catch (err2) {
163226
+ const caps = loadCapabilityCopy(storage2);
163227
+ return {
163228
+ ok: false,
163229
+ error: "desktop_offline",
163230
+ data: { contacts: caps, grants: [], effective: "\u684C\u9762\u4E0D\u5728\u7EBF (\u663E\u793A\u624B\u673A\u672C\u5730\u526F\u672C)", approvals: [], waiting: [] },
163231
+ note: `\u8FDE\u4E0D\u4E0A\u684C\u9762 (${String(err2?.message || err2).slice(0, 60)}) \u2014\u2014 \u663E\u793A\u672C\u5730\u526F\u672C`
163232
+ };
163233
+ }
163234
+ }
163235
+ async function bindFromPhone(base6, opts, storage2, f) {
163236
+ const impl = f || fetch;
163237
+ if (!base6) return { ok: false, error: "desktop_offline: \u7ED1\u5B9A\u9700\u8981\u684C\u9762\u7AEF\u5728\u7EBF (\u684C\u9762\u8D1F\u8D23\u843D\u76D8\u4E0E\u9A8C\u8BC1\u7801\u901A\u9053)" };
163238
+ const r = await post(base6, "/api/contacts/bind", { kind: opts.kind, value: opts.value, region: opts.region }, impl);
163239
+ if (r.status === 200 && r.json?.ok) return { ok: true, data: r.json };
163240
+ return { ok: false, error: r.json?.error || `HTTP ${r.status}` };
163241
+ }
163242
+ async function verifyFromPhone(base6, opts, storage2, f) {
163243
+ const impl = f || fetch;
163244
+ if (!base6) return { ok: false, error: "desktop_offline: \u9A8C\u8BC1\u9700\u8981\u684C\u9762\u7AEF\u5728\u7EBF" };
163245
+ const r = await post(base6, "/api/contacts/verify", { contactId: opts.contactId, challengeId: opts.challengeId, code: opts.code }, impl);
163246
+ if (r.status === 200 && r.json?.ok) return { ok: true, data: r.json };
163247
+ return { ok: false, error: r.json?.error || `HTTP ${r.status}` };
163248
+ }
163249
+ async function decideApprovalFromPhone(base6, opts, f) {
163250
+ const impl = f || fetch;
163251
+ if (!base6) return { ok: false, error: "desktop_offline: \u6279\u51C6\u9700\u8981\u684C\u9762\u7AEF\u5728\u7EBF (\u624B\u673A\u4E0D\u5728\u7EBF\u65F6\u4E0D\u4F1A\u5047\u88C5\u5DF2\u6279\u51C6)" };
163252
+ const r = await post(base6, `/api/contacts/approvals/${encodeURIComponent(opts.consentId)}/${opts.action}`, { by: "leo", via: "mobile", body: opts.body, reason: opts.reason }, impl);
163253
+ if (r.status === 200 && r.json?.ok) return { ok: true, data: r.json };
163254
+ return { ok: false, error: r.json?.error || `HTTP ${r.status}` };
163255
+ }
163256
+ function buildPhoneCard(view, opts) {
163257
+ return {
163258
+ title: "\u8BA9 Bolloon \u4EE3\u8868\u4F60\u8054\u7CFB\u5916\u90E8\u7684\u4EBA",
163259
+ effective: view?.effective || opts.offline || "\u672A\u77E5",
163260
+ contacts: (view?.contacts || []).map((c) => ({
163261
+ displayValue: c.displayValue || "",
163262
+ kind: c.kind || "",
163263
+ state: c.status || c.verificationStatus || "",
163264
+ label: c.capabilities?.includes("send") ? "\u53EF\u8054\u7CFB" : "\u4E0D\u53EF\u8054\u7CFB"
163265
+ })),
163266
+ approvals: (view?.approvals || []).map((a) => ({
163267
+ consentId: a.consentId,
163268
+ contactName: a.contactName,
163269
+ channel: a.channel,
163270
+ subject: a.subject,
163271
+ bodyPreview: a.bodyPreview || "",
163272
+ reason: a.reason,
163273
+ reallySent: !!a.reallySent
163274
+ })),
163275
+ choices: [
163276
+ { id: "task_once", label: "\u4EC5\u672C\u6B21\u4EFB\u52A1" },
163277
+ { id: "persistent", label: "\u957F\u671F\u4F7F\u7528 (\u63A8\u8350)" },
163278
+ { id: "full_contact_access", label: "\u5B8C\u5168\u6388\u6743\u8054\u7CFB\u65B9\u5F0F\u80FD\u529B" }
163279
+ ],
163280
+ willNotGet: ["\u8BFB\u53D6\u5168\u90E8\u90AE\u7BB1", "\u8BFB\u53D6\u901A\u8BAF\u5F55", "\u7FA4\u53D1\u6D88\u606F", "\u81EA\u52A8\u652F\u4ED8/\u8F6C\u8D26", "\u4EE3\u8868\u4F60\u7B7E\u7F72\u5408\u540C", "\u62FF\u5230\u624B\u673A\u53F7/\u90AE\u7BB1\u660E\u6587"],
163281
+ deviceSigning: opts.deviceSigning
163282
+ };
163283
+ }
163284
+
162926
163285
  // src/web/mobile-core.ts
162927
163286
  var busHandlers = /* @__PURE__ */ new Set();
162928
163287
  function busBroadcast(msg) {
@@ -163033,6 +163392,8 @@ ${error.stack}` : head;
163033
163392
  if (p === "/api/peers") return () => core.peers.list();
163034
163393
  if (p === "/api/mcp/tools") return () => core.mcp.tools();
163035
163394
  if (p === "/api/skills") return () => core.skills.list();
163395
+ if (p === "/api/contacts") return () => core.contacts.view();
163396
+ if (p === "/api/contacts/grants") return () => core.contacts.grants();
163036
163397
  if (p === "/api/auth/status") return () => core.identity.status();
163037
163398
  if (p === "/api/payments/pending") return () => core.payments.pending();
163038
163399
  if (p === "/api/llm-config") return () => core.data.getLlmConfig();
@@ -163786,6 +164147,111 @@ ${error.stack}` : head;
163786
164147
  return mobileGatewayTool2(String(name10 || ""), args || {});
163787
164148
  }
163788
164149
  },
164150
+ /**
164151
+ * 联系方式与持久授权 (2026-09-19, Phase 5/10)
164152
+ * 手机负责: 输入 · OTP 确认 · 生物识别/系统确认 · 展示待发内容 · 批准高风险联系 · **签名长期授权**
164153
+ * 桌面负责: 落盘 · 执行 · 等待回复 · 证据
164154
+ * 铁律: 明文不进手机存储 (只存 capability 副本), 桌面离线不假装成功, 不支持 Ed25519 就明说。
164155
+ */
164156
+ contacts: {
164157
+ /** 本机是否能做设备签名 (老 WebView 可能没有 Ed25519) */
164158
+ deviceSigning() {
164159
+ return ed25519Available();
164160
+ },
164161
+ /** 桌面 HTTP 地址 (拿不到 → null, 不猜) */
164162
+ async desktopBase() {
164163
+ try {
164164
+ const u = await core.desktop?.url?.();
164165
+ const raw = u && typeof u === "object" ? u.url || u.baseUrl || "" : u;
164166
+ const s2 = String(raw || "").replace(/\/+$/, "");
164167
+ return s2 || null;
164168
+ } catch {
164169
+ return null;
164170
+ }
164171
+ },
164172
+ /** 手机端本地存储 (只放: 设备私钥 JWK / capability 副本 / 离线队列) */
164173
+ storage() {
164174
+ try {
164175
+ if (typeof localStorage !== "undefined") return localStorage;
164176
+ } catch {
164177
+ }
164178
+ const mem2 = /* @__PURE__ */ new Map();
164179
+ return { getItem: (k) => mem2.has(k) ? mem2.get(k) : null, setItem: (k, v) => {
164180
+ mem2.set(k, v);
164181
+ }, removeItem: (k) => {
164182
+ mem2.delete(k);
164183
+ } };
164184
+ },
164185
+ /** 从桌面读脱敏视图 (桌面离线 → 退回本地 capability 副本并标注) */
164186
+ async view() {
164187
+ const base6 = await core.contacts.desktopBase();
164188
+ const r = await loadFromDesktop(base6, core.contacts.storage());
164189
+ return { ...r.data || { contacts: [], grants: [], effective: "", approvals: [], waiting: [] }, offline: !r.ok, note: r.note, error: r.error };
164190
+ },
164191
+ async grants() {
164192
+ const v = await core.contacts.view();
164193
+ return { grants: v.grants || [], effective: v.effective || "", offline: v.offline };
164194
+ },
164195
+ /** 手机端摘要卡 (给 UI 直接渲染) */
164196
+ async card() {
164197
+ const base6 = await core.contacts.desktopBase();
164198
+ const r = await loadFromDesktop(base6, core.contacts.storage());
164199
+ return buildPhoneCard(r.data || null, { deviceSigning: ed25519Available(), offline: r.error });
164200
+ },
164201
+ /** 确认授权 (三个选项之一) → 设备签名 → 桌面验签; 桌面离线则进本地队列 */
164202
+ async authorize(choice, ownerDid) {
164203
+ const base6 = await core.contacts.desktopBase();
164204
+ let did = ownerDid;
164205
+ if (!did) {
164206
+ try {
164207
+ const st = await core.identity?.status?.();
164208
+ did = st?.did;
164209
+ } catch {
164210
+ }
164211
+ }
164212
+ return authorizeFromPhone({
164213
+ base: base6 || "",
164214
+ choice,
164215
+ ownerDid: did || "did:bolln:local",
164216
+ storage: core.contacts.storage(),
164217
+ grantedBy: "leo"
164218
+ });
164219
+ },
164220
+ /** 手机撤销 (带签名; 桌面离线不假装已撤销) */
164221
+ async revoke(grantId, reason) {
164222
+ const base6 = await core.contacts.desktopBase();
164223
+ if (!base6) return { ok: false, error: "desktop_offline: \u64A4\u9500\u9700\u8981\u684C\u9762\u5728\u7EBF" };
164224
+ return revokeFromPhone({ base: base6, grantId, reason, storage: core.contacts.storage(), version: Date.now() });
164225
+ },
164226
+ async bind(kind, value2, region) {
164227
+ const base6 = await core.contacts.desktopBase();
164228
+ return bindFromPhone(base6, { kind, value: value2, region }, core.contacts.storage());
164229
+ },
164230
+ async verify(contactId, challengeId, code10) {
164231
+ const base6 = await core.contacts.desktopBase();
164232
+ return verifyFromPhone(base6, { contactId, challengeId, code: code10 }, core.contacts.storage());
164233
+ },
164234
+ /** 手机上批准/拒绝高风险联系 (展示待发内容后) */
164235
+ async decide(consentId, action, opts = {}) {
164236
+ const base6 = await core.contacts.desktopBase();
164237
+ return decideApprovalFromPhone(base6, { consentId, action, ...opts });
164238
+ },
164239
+ /** 桌面回来后补同步排队中的授权 */
164240
+ async flushQueue() {
164241
+ const base6 = await core.contacts.desktopBase();
164242
+ if (!base6) return { sent: 0, failed: pendingQueue(core.contacts.storage()).length, results: [] };
164243
+ return flushQueuedGrants(base6, core.contacts.storage());
164244
+ },
164245
+ queue() {
164246
+ return pendingQueue(core.contacts.storage());
164247
+ },
164248
+ capabilities() {
164249
+ return loadCapabilityCopy(core.contacts.storage());
164250
+ },
164251
+ device() {
164252
+ return loadOrCreateDeviceKey(core.contacts.storage());
164253
+ }
164254
+ },
163789
164255
  skills: {
163790
164256
  async list() {
163791
164257
  const s2 = await Promise.resolve().then(() => (init_mobile_sync(), mobile_sync_exports));
@@ -121,6 +121,7 @@
121
121
  <div class="list-item" id="item-settings"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M4 7.5h6M14 7.5h6M4 16.5h9M17 16.5h3"/><circle cx="12" cy="7.5" r="2.2"/><circle cx="15" cy="16.5" r="2.2"/></svg></span><span>设置</span><span class="list-arrow">›</span></div>
122
122
  <div class="list-item" id="item-wallet"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="3" y="6.5" width="18" height="12" rx="3"/><path d="M3 10.5h18"/><circle cx="16.8" cy="14.5" r="1.3" fill="currentColor" stroke="none"/></svg></span><span>钱包</span><span class="list-arrow">›</span></div>
123
123
  <div class="list-item" id="item-judgments"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M11 3.5l1.8 4.7 4.7 1.8-4.7 1.8L11 16.5l-1.8-4.7L4.5 10l4.7-1.8z"/><path d="M18.5 16l.6 1.6 1.6.6-1.6.6-.6 1.6-.6-1.6-1.6-.6 1.6-.6z"/></svg></span><span>判断力 API</span><span class="list-arrow">›</span></div>
124
+ <div class="list-item" id="item-contacts"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5"/><path d="M3.5 9h17"/><circle cx="8.5" cy="14" r="1.6"/><path d="M12 13h5M12 15.5h3"/></svg></span><span>联系方式与授权</span><span class="list-arrow" id="contacts-arrow">›</span></div>
124
125
  <div class="list-item" id="item-login"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="4.5" y="10.5" width="15" height="9.5" rx="2.5"/><path d="M8 10.5V7.5a4 4 0 0 1 8 0v3"/></svg></span><span id="login-label">登录</span><span class="list-arrow">›</span></div>
125
126
  <div class="list-item" id="item-logout"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M14.5 4.5h3.5a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2h-3.5"/><path d="M10 8.5l-3.5 3.5L10 15.5M6.5 12h8"/></svg></span><span>注销</span></div>
126
127
  </div>
@@ -235,3 +236,34 @@ if ('serviceWorker' in navigator) {
235
236
  </script>
236
237
  </body>
237
238
  </html>
239
+
240
+ <!-- 联系方式与持久授权 (2026-09-19): 手机确认一次, Agent 长期自动使用 -->
241
+ <div class="sheet" id="contacts-sheet" hidden>
242
+ <div class="sheet-inner">
243
+ <div class="sheet-title">联系方式与授权</div>
244
+ <div class="sheet-text" id="contacts-effective">加载中...</div>
245
+ <div class="sheet-text" id="contacts-signing" style="font-size:12px;color:var(--text-secondary)"></div>
246
+
247
+ <div id="contacts-approvals"></div>
248
+
249
+ <div class="sheet-title" style="margin-top:10px">我的联系方式</div>
250
+ <div id="contacts-list"><div class="sheet-text">加载中...</div></div>
251
+
252
+ <div style="margin-top:10px">
253
+ <input id="contacts-value" placeholder="手机号 (+86...) 或 邮箱" style="width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" />
254
+ <div style="display:flex;gap:8px;margin-top:8px">
255
+ <button class="sheet-choice" id="contacts-bind" style="flex:1">绑定并发送验证码</button>
256
+ </div>
257
+ <input id="contacts-code" placeholder="收到的验证码" inputmode="numeric" style="width:100%;margin-top:8px;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" />
258
+ <button class="sheet-choice" id="contacts-verify" style="margin-top:8px;width:100%">验证</button>
259
+ </div>
260
+
261
+ <div class="sheet-title" style="margin-top:12px">让 Bolloon 代表你联系外部的人</div>
262
+ <div class="sheet-text" id="contacts-willnot" style="font-size:12px"></div>
263
+ <div class="sheet-inner-choices" id="contacts-choices" style="margin-top:8px"></div>
264
+ <div class="sheet-inner-choices">
265
+ <button class="sheet-choice" id="contacts-revoke-all">撤销全部授权</button>
266
+ <button class="sheet-choice sheet-cancel" id="contacts-close">关闭</button>
267
+ </div>
268
+ </div>
269
+ </div>