@tonyruan/dsh-bridge-host 0.1.1-beta.4 → 0.1.1-beta.7

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
@@ -1,9 +1,14 @@
1
- # DSH Bridge host beta
2
-
3
- A community DSH-compatible phone-access plugin, not an official DeepSeek product. Requires Node.js >=24.9.0 and DSH0.2.2-rc.1.
4
-
5
- Package: @tonyruan/dsh-bridge-host. Use the official DSH plugin manager for installation; stop the target profile before package-name migration. Cordis ID dsh-phone-access, plugin name phone-access, API routes and existing profileDirectory/identity remain stable. The default profile is the existing .dsh-phone-profile; changing an npm name does not authorize replacing data.
6
-
7
- This generic build contains no personal relay preset or update root/feed. Import the original trusted deployment configuration or use the private migration bridge locally before replacing an old configured build. Existing paired profiles without deployment configuration fail recoverably and retain identity, pairing and login data. Strict LAN-only use does not enable update or diagnostic networking.
8
-
9
- Own project code is licensed under the standard PolyForm Noncommercial License1.0.0 in LICENSE. This is not OSI open-source; permitted purposes and exceptions are governed by that exact text, not a blanket promise about every business/payment scenario. Third-party components retain their own permission, copyright and NOTICE terms in THIRD-PARTY-NOTICES.txt and oss-licenses/. These own-code terms do not restrict upstream rights. The complete own development project/Git repository is not included; npm runtime JavaScript is publicly readable. Native platform download assets are not included in this npm release.
1
+ # DSH Bridge host beta
2
+
3
+ A community DSH-compatible phone-access plugin, not an official DeepSeek product. Requires Node.js >=24.9.0 and DSH0.2.2-rc.1.
4
+
5
+ Package: @tonyruan/dsh-bridge-host. Use the official DSH plugin manager for installation; stop the target profile before package-name migration. Cordis ID dsh-phone-access, plugin name phone-access, API routes and existing profileDirectory/identity remain stable. The default profile is the existing .dsh-phone-profile; changing an npm name does not authorize replacing data.
6
+
7
+ This generic build contains no personal relay preset or update root/feed. Import the original trusted deployment configuration or use the private migration bridge locally before replacing an old configured build. Existing paired profiles without deployment configuration fail recoverably and retain identity, pairing and login data. Strict LAN-only use does not enable update or diagnostic networking.
8
+
9
+ Own project code is licensed under the standard PolyForm Noncommercial License1.0.0 in LICENSE. This is not OSI open-source; permitted purposes and exceptions are governed by that exact text, not a blanket promise about every business/payment scenario. Third-party components retain their own permission, copyright and NOTICE terms in THIRD-PARTY-NOTICES.txt and oss-licenses/. These own-code terms do not restrict upstream rights. The complete own development project/Git repository is not included; npm runtime JavaScript is publicly readable. Native platform download assets are not included in this npm release.
10
+
11
+
12
+ Beta6/build26 retains the original validated persisted connection realm across upgrades and LAN-only restarts, including previously paired WSS realms without an unused relay leaf. Existing trusted deployment is reused without import or rewrite. AUTO still requires trusted TLS material. Legacy private code-only presets require the separately delivered Linux pre-upgrade migration helper before replacement; it statically validates the original local package and pin, never executes old code or fetches a new network leaf. No identity/pairing/schema reinitialization or client update-source JSON is required.
13
+
14
+ Beta7/build27 fixes the setting sidebar icon through the native settings.section.icon slot, without changing the phone-connection name or the general settings icon. An already-native-authenticated hostname may be used only for independently proven direct LAN sockets; the private internal flag cannot authorize relay, unknown, virtual/VPN or unverified interfaces, or bypass native Cookie/Host/Origin. The UI distinguishes native admission403 from the plugin fixed management403, and both remain blocked. Targeted raw-LAN positives use synthetic interface facts; the actual rc.1 browser/navigation checks use an isolated loopback profile. The real user Ubuntu version and route were not inspected, so site-specific resolution and a real physical-LAN success are not claimed.
package/lib/agent.mjs CHANGED
@@ -332,7 +332,13 @@ var require_is_electron = __commonJS({
332
332
  }
333
333
  });
334
334
 
335
- // npm-release-beta-r1/host/phone-access-plugin/src/agent.mjs
335
+ // host-config-migration-r1/host/phone-access-plugin/src/lan-provenance.mjs
336
+ var accepted = /* @__PURE__ */ new WeakMap();
337
+ function acceptedLanEndpoint(socket) {
338
+ return socket.destroyed ? null : accepted.get(socket) ?? null;
339
+ }
340
+
341
+ // host-config-migration-r1/host/phone-access-plugin/src/agent.mjs
336
342
  import net from "node:net";
337
343
  import { once } from "node:events";
338
344
 
@@ -15327,7 +15333,7 @@ function noise(init = {}) {
15327
15333
  return (components) => new Noise(components, init);
15328
15334
  }
15329
15335
 
15330
- // npm-release-beta-r1/host/phone-access-plugin/src/lan-trace.mjs
15336
+ // host-config-migration-r1/host/phone-access-plugin/src/lan-trace.mjs
15331
15337
  var LAN_DETAIL_CODES = Object.freeze(["LAN_TCP_ACCEPTED", "LAN_TCP_CLOSED_REJECTED", "LAN_TCP_DISABLED_REJECTED", "LAN_TCP_POLICY_REJECTED", "LAN_TCP_SUBNET_REJECTED", "LAN_TCP_LIMIT_REJECTED", "LAN_TCP_GLOBAL_RATE_REJECTED", "LAN_TCP_SOURCE_RATE_REJECTED", "LAN_TCP_RATE_KEYS_REJECTED", "LAN_AGENT_CLOSED_REJECTED", "LAN_AGENT_LIMIT_REJECTED", "LAN_AGENT_HANDSHAKE_LIMIT_REJECTED", "LAN_AGENT_PEER_LIMIT_REJECTED", "LAN_AGENT_ADMISSION_REJECTED", "LAN_AGENT_READMISSION_REJECTED", "LAN_AGENT_FIRST_FRAME_EOF", "LAN_AGENT_NOISE_FAILED", "LAN_AGENT_FIRST_FRAME_TIMEOUT", "LAN_AGENT_FIRST_FRAME_FAILED", "LAN_AGENT_BACKEND_CONNECT_FAILED", "LAN_AGENT_REGISTER_FAILED", "LAN_AGENT_FORWARD_FAILED", "LAN_AGENT_CHANNEL_READY", "LAN_AGENT_CHANNEL_CLOSED"]);
15332
15338
  var codes = new Set(LAN_DETAIL_CODES);
15333
15339
  var bounded = (v, max) => Number.isSafeInteger(v) && v >= 0 ? Math.min(max, v) : 0;
@@ -15395,7 +15401,7 @@ function createLanTrace(diagnostic = () => {
15395
15401
  return { sample, lifetime, now };
15396
15402
  }
15397
15403
 
15398
- // npm-release-beta-r1/host/noise-lab/core.mjs
15404
+ // host-config-migration-r1/host/noise-lab/core.mjs
15399
15405
  var silent = Object.assign(() => {
15400
15406
  }, { enabled: false, error: () => {
15401
15407
  }, trace: () => {
@@ -15435,7 +15441,7 @@ async function temporaryIdentity() {
15435
15441
  return { privateKey, peerId: peerIdFromPrivateKey(privateKey) };
15436
15442
  }
15437
15443
 
15438
- // npm-release-beta-r1/host/phone-access-plugin/src/agent.mjs
15444
+ // host-config-migration-r1/host/phone-access-plugin/src/agent.mjs
15439
15445
  async function createPcAgent({ backendPort, listenPort = 0, listen = true, identity: providedIdentity, pairing = () => ({ confirmed: false, pin: null }), acquireLease, registerBackend, diagnostic = () => {
15440
15446
  }, factories = {} }) {
15441
15447
  if (!Number.isInteger(backendPort) || backendPort < 1 || backendPort > 65535) throw Error("INVALID_FIXED_BACKEND");
@@ -15452,7 +15458,7 @@ async function createPcAgent({ backendPort, listenPort = 0, listen = true, ident
15452
15458
  let handshaking = 0, closed = false, attempts = 0;
15453
15459
  const stats = { handshakes: 0, rejected: 0, handshakeFailed: 0, dshConnections: 0, upBytes: 0, downBytes: 0 };
15454
15460
  const acceptWire = (wire, { route = "RELAY", pairId } = {}) => {
15455
- const attempt = ++attempts, began = trace.now();
15461
+ const attempt = ++attempts, began = trace.now(), lanEndpoint = route === "LAN" ? acceptedLanEndpoint(wire) : null;
15456
15462
  const event = (code3) => {
15457
15463
  try {
15458
15464
  diagnostic({ stage: route === "LAN" ? "LAN" : "AUTH", code: code3, attempt });
@@ -15629,7 +15635,7 @@ async function createPcAgent({ backendPort, listenPort = 0, listen = true, ident
15629
15635
  check();
15630
15636
  if (registerBackend) {
15631
15637
  phase = "REGISTER";
15632
- const registration = Promise.resolve(registerBackend({ localAddress: backend.localAddress, localPort: backend.localPort, remoteAddress: backend.remoteAddress, remotePort: backend.remotePort })).then((fn) => {
15638
+ const registration = Promise.resolve(registerBackend({ localAddress: backend.localAddress, localPort: backend.localPort, remoteAddress: backend.remoteAddress, remotePort: backend.remotePort }, lease && lease.valid() ? { schema: 1, route: route === "LAN" && lanEndpoint ? "LAN" : route === "RELAY" ? "RELAY" : "UNKNOWN", peerId: lease.peerId, pairId: lease.pairId, generation: lease.generation, lanEndpoint: route === "LAN" ? lanEndpoint : null } : null)).then((fn) => {
15633
15639
  let released = false;
15634
15640
  const once2 = () => {
15635
15641
  if (released) return;
package/lib/child.mjs CHANGED
@@ -1,38 +1,38 @@
1
- import {createPcAgent} from './agent.mjs';
2
- import {createPhoneSession} from './session.mjs';
3
- import {randomUUID} from 'node:crypto';
4
- let agent,closing=false,heartbeat;
5
- const registrations=new Map();
6
- function registerBackend(tuple){return new Promise((resolve,reject)=>{
7
- const registrationId=randomUUID(),timer=setTimeout(()=>{registrations.delete(registrationId);process.send?.({op:'backend.close',registrationId},()=>{});reject(Error('BACKEND_REGISTRATION_TIMEOUT'));},3000);
8
- registrations.set(registrationId,{resolve,reject,timer});
9
- process.send?.({op:'backend.open',registrationId,tuple},error=>{if(error){clearTimeout(timer);registrations.delete(registrationId);reject(Error('BACKEND_REGISTRATION_FAILED'));}});
10
- });}
11
- const finish=async()=>{if(closing)return;closing=true;clearInterval(heartbeat);await agent?.close();process.exit(0);};
12
- process.on('disconnect',finish);process.on('SIGTERM',finish);process.on('SIGINT',finish);
13
- // Parent IPC is the only controller. No unauthenticated control HTTP listener exists.
14
- process.on('message',async message=>{
15
- if(closing)return;
16
- try{
17
- if(message?.op==='backend.ack'){
18
- const r=registrations.get(message.registrationId);if(!r)return;registrations.delete(message.registrationId);clearTimeout(r.timer);
19
- if(!message.ok)r.reject(Error('BACKEND_REGISTRATION_REJECTED'));else {let released=false;r.resolve(()=>{if(released)return;released=true;process.send?.({op:'backend.close',registrationId:message.registrationId},()=>{});});}return;
20
- }
21
- if(message?.op==='init'){
22
- if(agent)throw Error('ALREADY_INITIALIZED');
23
- if(message.profileDirectory&&!message.relay)throw Error('PERSISTENT_PROFILE_REQUIRES_FIXED_RELAY');
24
- agent=message.relay?await createPhoneSession({backendPort:message.backendPort,connectionMode:message.connectionMode??'auto',lanAddress:message.lanAddress,relay:message.relay,relayCa:message.relayCa,relayLeaf:message.relayLeaf,profileDirectory:message.profileDirectory,lanEnabled:message.lanEnabled===true,registerBackend,diagnostics:{record:event=>{if(process.connected)process.send?.({op:'diagnostic',event:{...event,generation:Number.isInteger(message.diagnosticGeneration)?message.diagnosticGeneration:0}},()=>{});}}}):await createPcAgent({backendPort:message.backendPort,registerBackend});
25
- heartbeat=setInterval(()=>{if(process.connected)process.send?.({op:'diagnostic',event:{stage:'LOCAL',code:'COMPANION_HEARTBEAT',count:1,generation:Number.isInteger(message.diagnosticGeneration)?message.diagnosticGeneration:0}},()=>{});},30000);heartbeat.unref?.();
26
- process.send?.({id:message.id,ok:true,status:{pid:process.pid,...agent.status()}});
27
- }else if(message?.op==='status'&&agent)process.send?.({id:message.id,ok:true,status:{pid:process.pid,...agent.status()}});
28
- else if(message?.op==='confirmReverseScan'&&agent?.confirmReverseScan){await agent.confirmReverseScan(message.qr);process.send?.({id:message.id,ok:true,status:agent.status()});}
29
- else if(message?.op==='pair/start'&&agent?.beginPairing){await agent.beginPairing();process.send?.({id:message.id,ok:true,status:agent.status()});}
30
- else if(message?.op==='pair/cancel'&&agent?.cancelPairing){await agent.cancelPairing(message.sessionId);process.send?.({id:message.id,ok:true,status:agent.status()});}
31
- else if(message?.op==='revoke'&&agent?.revoke){await agent.revoke(message.target);process.send?.({id:message.id,ok:true,status:agent.status()});}
32
- else if(message?.op==='lan/settings'&&agent?.setLanEnabled){await agent.setLanEnabled(message.enabled);process.send?.({id:message.id,ok:true,status:agent.status()});}
33
- else if(message?.op==='reconnect'&&agent?.reconnect){await agent.reconnect();process.send?.({id:message.id,ok:true,status:agent.status()});}
34
- else if(message?.op==='stop')await finish();
35
- }catch(error){const code=error?.code??error?.message;process.send?.({id:message?.id,ok:false,error:/^[A-Z_]{3,70}$/.test(code??'')?code:'AGENT_OPERATION_FAILED'});if(!agent)await finish();}
36
- });
37
- // Failed owners must not leave a child waiting forever before init.
38
- setTimeout(()=>{if(!agent)finish();},10000).unref();
1
+ import {createPcAgent} from './agent.mjs';
2
+ import {createPhoneSession} from './session.mjs';
3
+ import {randomUUID} from 'node:crypto';
4
+ let agent,closing=false,heartbeat;
5
+ const registrations=new Map();
6
+ function registerBackend(tuple,provenance){return new Promise((resolve,reject)=>{
7
+ const registrationId=randomUUID(),timer=setTimeout(()=>{registrations.delete(registrationId);process.send?.({op:'backend.close',registrationId},()=>{});reject(Error('BACKEND_REGISTRATION_TIMEOUT'));},3000);
8
+ registrations.set(registrationId,{resolve,reject,timer});
9
+ process.send?.({op:'backend.open',registrationId,tuple,provenance},error=>{if(error){clearTimeout(timer);registrations.delete(registrationId);reject(Error('BACKEND_REGISTRATION_FAILED'));}});
10
+ });}
11
+ const finish=async()=>{if(closing)return;closing=true;clearInterval(heartbeat);await agent?.close();process.exit(0);};
12
+ process.on('disconnect',finish);process.on('SIGTERM',finish);process.on('SIGINT',finish);
13
+ // Parent IPC is the only controller. No unauthenticated control HTTP listener exists.
14
+ process.on('message',async message=>{
15
+ if(closing)return;
16
+ try{
17
+ if(message?.op==='backend.ack'){
18
+ const r=registrations.get(message.registrationId);if(!r)return;registrations.delete(message.registrationId);clearTimeout(r.timer);
19
+ if(!message.ok)r.reject(Error('BACKEND_REGISTRATION_REJECTED'));else {let released=false;r.resolve(()=>{if(released)return;released=true;process.send?.({op:'backend.close',registrationId:message.registrationId},()=>{});});}return;
20
+ }
21
+ if(message?.op==='init'){
22
+ if(agent)throw Error('ALREADY_INITIALIZED');
23
+ if(message.profileDirectory&&!message.relay)throw Error('PERSISTENT_PROFILE_REQUIRES_FIXED_RELAY');
24
+ agent=message.relay?await createPhoneSession({backendPort:message.backendPort,connectionMode:message.connectionMode??'auto',lanAddress:message.lanAddress,relay:message.relay,relayCa:message.relayCa,relayLeaf:message.relayLeaf,profileDirectory:message.profileDirectory,lanEnabled:message.lanEnabled===true,registerBackend,diagnostics:{record:event=>{if(process.connected)process.send?.({op:'diagnostic',event:{...event,generation:Number.isInteger(message.diagnosticGeneration)?message.diagnosticGeneration:0}},()=>{});}}}):await createPcAgent({backendPort:message.backendPort,registerBackend});
25
+ heartbeat=setInterval(()=>{if(process.connected)process.send?.({op:'diagnostic',event:{stage:'LOCAL',code:'COMPANION_HEARTBEAT',count:1,generation:Number.isInteger(message.diagnosticGeneration)?message.diagnosticGeneration:0}},()=>{});},30000);heartbeat.unref?.();
26
+ process.send?.({id:message.id,ok:true,status:{pid:process.pid,...agent.status()}});
27
+ }else if(message?.op==='status'&&agent)process.send?.({id:message.id,ok:true,status:{pid:process.pid,...agent.status()}});
28
+ else if(message?.op==='confirmReverseScan'&&agent?.confirmReverseScan){await agent.confirmReverseScan(message.qr);process.send?.({id:message.id,ok:true,status:agent.status()});}
29
+ else if(message?.op==='pair/start'&&agent?.beginPairing){await agent.beginPairing();process.send?.({id:message.id,ok:true,status:agent.status()});}
30
+ else if(message?.op==='pair/cancel'&&agent?.cancelPairing){await agent.cancelPairing(message.sessionId);process.send?.({id:message.id,ok:true,status:agent.status()});}
31
+ else if(message?.op==='revoke'&&agent?.revoke){await agent.revoke(message.target);process.send?.({id:message.id,ok:true,status:agent.status()});}
32
+ else if(message?.op==='lan/settings'&&agent?.setLanEnabled){await agent.setLanEnabled(message.enabled);process.send?.({id:message.id,ok:true,status:agent.status()});}
33
+ else if(message?.op==='reconnect'&&agent?.reconnect){await agent.reconnect();process.send?.({id:message.id,ok:true,status:agent.status()});}
34
+ else if(message?.op==='stop')await finish();
35
+ }catch(error){const code=error?.code??error?.message;process.send?.({id:message?.id,ok:false,error:/^[A-Z_]{3,70}$/.test(code??'')?code:'AGENT_OPERATION_FAILED'});if(!agent)await finish();}
36
+ });
37
+ // Failed owners must not leave a child waiting forever before init.
38
+ setTimeout(()=>{if(!agent)finish();},10000).unref();
package/lib/client.js CHANGED
@@ -31,8 +31,9 @@ window.__ModuleLoader__.load({ id: "@tonyruan/dsh-bridge-host", factory: (requir
31
31
  decidePairing:'/api/phone-access/pair/confirm', revoke:'/api/phone-access/revoke',
32
32
  reconnect:'/api/phone-access/start', diagnosticInitialize:'/api/phone-access/diagnostics/initialize', diagnosticSettings:'/api/phone-access/diagnostics/settings', diagnosticExport:'/api/phone-access/diagnostics/export', lanSettings:'/api/phone-access/lan/settings', startupSettings:'/api/phone-access/startup/settings'
33
33
  });
34
- const messages = { CLIENT_TARGET_REQUIRED:"请选择要撤销的客户端。",PAIRING_STALE:"身份或代次已变化,请刷新后重试。",PAIRING_BUSY:"已有配对正在进行。",PROFILE_CAPACITY:"已达到四个客户端上限。",PROFILE_UNAVAILABLE:"安全存储状态不确定,访问已关闭。请恢复存储并读取状态,不要删除配对或重新初始化。", STARTUP_SETTINGS_UNAVAILABLE:"自动启动设置无法保存,请检查本地存储。", AUTOSTART_PAIR_REQUIRED:"请先手动完成手机配对,自动启动不会创建身份。", DIAGNOSTICS_STORAGE_UNAVAILABLE:"日志存储不可用,业务连接可继续使用。", DIAGNOSTICS_INITIALIZE_REQUIRED:"请先初始化独立日志身份。", DIAGNOSTICS_CONFIRM_REQUIRED:"请确认日志操作。", CONFIG_REQUIRED: "未找到有效的预置连接配置,请安装已配置中继与公开证书的用户专用插件包。", TRUST_EXPIRED: "relay 证书已过期。请更新插件包中的公开证书配置后重试。", BACKEND_NOT_CONNECTED: "配对管理接口尚未接入。请更新插件后重试。", SAME_ORIGIN_REQUIRED: "管理请求被拒绝。请从这台电脑的 DSH 重新打开页面。", LOCAL_MANAGEMENT_REQUIRED: "请在电脑浏览器打开本机 DSH,或使用 SSH 转发打开管理页面。手机远程通道不能执行管理操作。", STALE_REQUEST: "手机请求已变化或过期,请刷新后重新核对指纹。", PAIRING_EXPIRED: "二维码已过期,请重新开始配对。", RELAY_UNREACHABLE: "无法连接 relay。请检查地址、网络和证书配置。", TRUST_MISMATCH: "relay 证书校验失败,请核对公开证书或 SHA-256 指纹。", TIMEOUT: "请求超时。操作结果尚未确认,请刷新状态后再决定是否重试。", NETWORK: "无法读取连接状态。请检查 DSH 是否在线,再刷新。" };
34
+ const messages = { CLIENT_TARGET_REQUIRED:"请选择要撤销的客户端。",PAIRING_STALE:"身份或代次已变化,请刷新后重试。",PAIRING_BUSY:"已有配对正在进行。",PROFILE_CAPACITY:"已达到四个客户端上限。",PROFILE_UNAVAILABLE:"安全存储状态不确定,访问已关闭。请恢复存储并读取状态,不要删除配对或重新初始化。", STARTUP_SETTINGS_UNAVAILABLE:"自动启动设置无法保存,请检查本地存储。", AUTOSTART_PAIR_REQUIRED:"请先手动完成手机配对,自动启动不会创建身份。", DIAGNOSTICS_STORAGE_UNAVAILABLE:"日志存储不可用,业务连接可继续使用。", DIAGNOSTICS_INITIALIZE_REQUIRED:"请先初始化独立日志身份。", DIAGNOSTICS_CONFIRM_REQUIRED:"请确认日志操作。", CONFIG_REQUIRED: "未找到有效的预置连接配置,请安装已配置中继与公开证书的用户专用插件包。", TRUST_EXPIRED: "relay 证书已过期。请更新插件包中的公开证书配置后重试。", BACKEND_NOT_CONNECTED: "配对管理接口尚未接入。请更新插件后重试。", SAME_ORIGIN_REQUIRED: "管理请求被拒绝。请从这台电脑的 DSH 重新打开页面。", LOCAL_MANAGEMENT_REQUIRED: "请在本机或已验证的局域网连接中管理插件;公网中继不允许管理。也可保留原有 SSH 转发方式。", STALE_REQUEST: "手机请求已变化或过期,请刷新后重新核对指纹。", PAIRING_EXPIRED: "二维码已过期,请重新开始配对。", RELAY_UNREACHABLE: "无法连接 relay。请检查地址、网络和证书配置。", TRUST_MISMATCH: "relay 证书校验失败,请核对公开证书或 SHA-256 指纹。", TIMEOUT: "请求超时。操作结果尚未确认,请刷新状态后再决定是否重试。", NETWORK: "无法读取连接状态。请检查 DSH 是否在线,再刷新。" };
35
35
  Object.assign(messages,{"CONFIGURATION_REQUIRED": "未找到有效的预置连接配置,请更新用户专用插件包。", "INITIALIZE_REQUIRED": "请先点击“初始化连接”,再开始配对。", "PAIR_REQUEST_CHANGED": "手机请求已变化或过期,请重新开始配对并核对指纹。", "ALREADY_PAIRED": "已配对一部手机,请先撤销现有配对。", "INVALID_ACTION_BODY": "请求未被接受,请刷新页面后重试。", "RELAY_OFFLINE": "中继离线或连接超时,请检查网络后重新诊断。", "RELAY_DISCONNECTED": "中继连接已断开,请重新连接后再试。", "TLS_LEAF_EXPIRED": "中继公开证书已过期或尚未生效,请更新用户专用插件包。", "TLS_LEAF_SAN_MISMATCH": "中继证书的域名或 IP 不匹配,请核对插件配置。", "TLS_LEAF_PIN_MISMATCH": "中继证书指纹不匹配,已拒绝连接。请更新正确的公开证书配置。", "TLS_LEAF_CA_FORBIDDEN": "需要固定中继的公开叶证书,请更新插件配置。", "PERSISTENCE_UNSUPPORTED_PLATFORM": "当前系统尚不支持本机身份存储。", "PROFILE_NOT_PROVISIONED": "请先初始化本机连接身份。", "PROFILE_RELAY_MISMATCH": "本机身份绑定了其他中继,请核对已安装的插件配置。", "LINUX_VAULT_UNSAFE_IN_USE_OR_RECOVERY_REQUIRED": "本机身份目录不可用、被占用或需要恢复。请关闭其他实例后检查目录。", "VAULT_UNAVAILABLE_OR_IN_USE": "Windows 安全存储不可用或被其他实例占用,请关闭其他实例后重试。", "LOCAL_AGENT_OPERATION_FAILED": "本机连接操作失败,请刷新并检查设备与中继状态。"});
36
+ messages.NATIVE_ACCESS_DENIED = "DSH 拒绝了此页面的认证或来源,请从原 DSH 入口重新打开。插件不会放宽认证或代理规则。";
36
37
  class UiError extends Error {
37
38
  constructor(code) {
38
39
  super(messages[code] || "操作未完成。请刷新状态;如仍失败,请检查插件和 relay 配置。");
@@ -96,13 +97,13 @@ window.__ModuleLoader__.load({ id: "@tonyruan/dsh-bridge-host", factory: (requir
96
97
  const abort = new AbortController(), timer = setTimeout(() => abort.abort(), 12e3);
97
98
  try {
98
99
  const r = await request(path, { method: body === void 0 ? "GET" : "POST", credentials: "same-origin", cache: "no-store", signal: abort.signal, headers: body === void 0 ? {} : { "content-type": "application/json" }, ...body === void 0 ? {} : { body: JSON.stringify(body) } });
99
- if (r.status === 403) throw new UiError('LOCAL_MANAGEMENT_REQUIRED');
100
100
  let data;
101
101
  try {
102
102
  data = await r.json();
103
103
  } catch {
104
- throw new UiError("INVALID_RESPONSE");
104
+ throw new UiError(r.status === 403 ? 'NATIVE_ACCESS_DENIED' : "INVALID_RESPONSE");
105
105
  }
106
+ if (r.status === 403) throw new UiError(data?.error?.code === 'LOCAL_MANAGEMENT_REQUIRED' ? 'LOCAL_MANAGEMENT_REQUIRED' : 'NATIVE_ACCESS_DENIED');
106
107
  if (!r.ok) throw new UiError(errorCode(data?.error) || 'REQUEST_FAILED');
107
108
  return data;
108
109
  } catch (e) {
@@ -122,9 +123,10 @@ window.__ModuleLoader__.load({ id: "@tonyruan/dsh-bridge-host", factory: (requir
122
123
  }
123
124
  };
124
125
  }
125
- function PhoneIcon({ size = 20, ...props } = {}) {
126
- const Icon = primitives.IconPhoneOutlineMedium || primitives.IconMobileOutlineMedium;
127
- return Icon ? h(Icon, { size, ...props }) : h("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.7, "aria-hidden": true, ...props }, h("rect", { x: 6, y: 2.5, width: 12, height: 19, rx: 2.5 }), h("path", { d: "M10 5h4M10.5 18.5h3", strokeLinecap: "round" }));
126
+ function PhoneIcon({ size = 20, ...props } = {}) {
127
+ return h("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, focusable: false, ...props },
128
+ h("rect", { x: 3, y: 3, width: 10, height: 18, rx: 2 }),
129
+ h("path", { d: "M6.5 6h3M7 18h2M16 8a5 5 0 0 1 0 8M19 5a10 10 0 0 1 0 14" }));
128
130
  }
129
131
  const css = `.dsh-phone{font-family:inherit;font-size:14px;line-height:1.6;color:var(--dsw-alias-label-primary,#202329);max-width:690px;min-width:0}.dsh-phone *{box-sizing:border-box}.dsh-phone h2{font-size:20px;line-height:1.4;font-weight:600;margin:0}.dsh-phone h3{font-size:15px;font-weight:600;margin:0 0 6px}.dsh-phone p{margin:5px 0 12px}.dsh-phone .muted{color:var(--dsw-alias-label-secondary,#69707b);font-size:13px}.dsh-phone .head,.dsh-phone .actions{display:flex;align-items:center;gap:9px;flex-wrap:wrap}.dsh-phone .head{margin-bottom:8px}.dsh-phone .state{border:1px solid var(--dsw-alias-border-main,#e1e4e9);border-radius:10px;padding:14px 16px;margin:18px 0}.dsh-phone .state strong{font-weight:600}.dsh-phone .dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:#959ba6;margin-right:8px}.dsh-phone .dot.online{background:#2b8b65}.dsh-phone .dot.wait{background:#b28122}.dsh-phone .block{padding:18px 0;border-top:1px solid var(--dsw-alias-border-main,#e7e9ed)}.dsh-phone label{display:block;font-size:13px;margin:12px 0 5px}.dsh-phone input,.dsh-phone select,.dsh-phone textarea{width:100%;font:inherit;color:inherit;background:var(--dsw-alias-bg-layer-2,#fff);border:1px solid var(--dsw-alias-border-main,#cfd4dd);border-radius:7px;padding:8px 10px;min-width:0}.dsh-phone textarea{resize:vertical}.dsh-phone button{font:inherit;font-size:13px;cursor:pointer;line-height:20px;padding:7px 12px;border:1px solid var(--dsw-alias-border-main,#d9dde4);border-radius:7px;background:var(--dsw-alias-bg-layer-2,#fff);color:inherit}.dsh-phone button.primary{background:var(--dsw-alias-brand-primary-new-colorprimary-new-color,#4d6bfe);color:white;border-color:transparent}.dsh-phone button.danger{color:#b13d3d}.dsh-phone button:disabled{opacity:.48;cursor:not-allowed}.dsh-phone :is(button,input,select,textarea):focus-visible{outline:2px solid #4d6bfe;outline-offset:2px}.dsh-phone .alert{border-left:3px solid #bb7830;padding:9px 12px;background:var(--dsw-alias-bg-layer-1,#faf7f2);margin:12px 0;overflow-wrap:anywhere}.dsh-phone .fingerprint,.dsh-phone pre{font:12px/1.7 ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere;white-space:pre-wrap;background:var(--dsw-alias-bg-layer-1,#f5f6f8);border-radius:7px;padding:10px;user-select:text;margin:8px 0}.dsh-phone .qr-layout{display:flex;align-items:flex-start;gap:20px;flex-wrap:wrap;margin:14px 0}.dsh-phone .qr{background:#fff;color:#111;max-width:100%;width:196px;height:auto;border:1px solid #e4e7ec;border-radius:7px;flex:none;image-rendering:pixelated}.dsh-phone .qr-copy{flex:1;min-width:180px}.dsh-phone .check{display:flex;align-items:flex-start;gap:9px;margin:12px 0}.dsh-phone .check input{width:16px;height:16px;flex:none;margin-top:3px}.dsh-phone .diagnostics{padding-left:20px}.dsh-phone .diagnostics li{padding:3px 0}.dsh-phone .tag{font-size:12px;font-weight:400;margin-left:8px}.dsh-phone .actions{margin-top:12px}.dsh-phone .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}@media(max-width:460px){.dsh-phone .state{padding:12px}.dsh-phone .qr-layout{gap:12px}.dsh-phone .qr{width:180px}.dsh-phone button{min-height:38px}}`;
130
132
  function createPhonePage(api) {
@@ -421,7 +423,8 @@ function installSubmissionGuard(conversation, sessions, enabled, options={}) {
421
423
  function apply(ctx) {
422
424
  installPhoneCompatibility(ctx);
423
425
  const Page = createPhonePage(createPhoneApi());
424
- ctx.slots.inject("settings.section", () => ctx.slots.register({ name: "settings.section", id: "phone-access", order: 18, label: "手机连接", icon: PhoneIcon }, Page));
426
+ ctx.slots.inject("settings.section", () => ctx.slots.register({ name: "settings.section", id: "phone-access", order: 18, label: "手机连接" }, Page));
427
+ ctx.slots.inject("settings.section.icon", () => ctx.slots.register({ name: "settings.section.icon", id: "phone-access" }, ({size = 16}) => h(PhoneIcon, {size})));
425
428
  }
426
429
  return { inject: ["slots"], apply, createPhoneApi, createPhonePage, PhoneIcon, publicStatus };
427
430
  } });
@@ -5,7 +5,7 @@ var __export = (target, all) => {
5
5
  __defProp(target, name2, { get: all[name2], enumerable: true });
6
6
  };
7
7
 
8
- // npm-release-beta-r1/host/diagnostics/lifecycle-trace.mjs
8
+ // host-config-migration-r1/host/diagnostics/lifecycle-trace.mjs
9
9
  var STOP_REASON = Object.freeze({ USER_STOP: 1, PLUGIN_SHUTDOWN: 2, USER_RESTART: 3, REPLACE_CHILD: 4, INIT_FAILURE: 5, OWNER_DISCONNECT: 6 });
10
10
  var SIGNAL = Object.freeze({ NONE: 0, SIGTERM: 1, SIGKILL: 2, SIGINT: 3, SIGABRT: 4, SIGSEGV: 5, SIGBUS: 6, SIGHUP: 7, OTHER: 8 });
11
11
  var CRITICAL_CODES = Object.freeze(["COMPANION_START_BEGIN", "COMPANION_START_READY", "COMPANION_START_FAILED", "COMPANION_STOP_REQUESTED", "COMPANION_EXIT", "COMPANION_SIGNAL", "COMPANION_IPC_LOST", "DIAG_UPLOAD_FAILED", "RELAY_CONTROL_ENDED", "RELAY_CONTROL_TIMEOUT", "RELAY_CONTROL_CONNECT_FAILED", "RELAY_RECOVERY_BLOCKED"]);
@@ -41,7 +41,7 @@ function createLifecycleTrace(record) {
41
41
  } };
42
42
  }
43
43
 
44
- // npm-release-beta-r1/host/phone-access-plugin/src/controller.mjs
44
+ // host-config-migration-r1/host/phone-access-plugin/src/controller.mjs
45
45
  import { fork } from "node:child_process";
46
46
 
47
47
  // ../../2026-10-22/task-2/node_modules/@libp2p/interface/dist/src/errors.js
@@ -8055,7 +8055,7 @@ function isSha256Multihash(multihash) {
8055
8055
  return multihash.code === sha256.code;
8056
8056
  }
8057
8057
 
8058
- // npm-release-beta-r1/host/phone-access-plugin/src/linux-vault.mjs
8058
+ // host-config-migration-r1/host/phone-access-plugin/src/linux-vault.mjs
8059
8059
  import * as nativeFs from "node:fs/promises";
8060
8060
  import { constants as nativeConstants } from "node:fs";
8061
8061
  import { posix } from "node:path";
@@ -8214,7 +8214,7 @@ async function openLinuxVault(directory, { platform = process.platform, fs = nat
8214
8214
  };
8215
8215
  }
8216
8216
 
8217
- // npm-release-beta-r1/host/phone-access-plugin/src/profile-store.mjs
8217
+ // host-config-migration-r1/host/phone-access-plugin/src/profile-store.mjs
8218
8218
  async function openWindowsVault() {
8219
8219
  throw Error("PERSISTENCE_UNSUPPORTED_PLATFORM");
8220
8220
  }
@@ -8250,7 +8250,7 @@ function openProfileVault(directory, options = {}) {
8250
8250
  throw Error("PERSISTENCE_UNSUPPORTED_PLATFORM");
8251
8251
  }
8252
8252
 
8253
- // npm-release-beta-r1/host/phone-access-plugin/src/multi-client-profile.mjs
8253
+ // host-config-migration-r1/host/phone-access-plugin/src/multi-client-profile.mjs
8254
8254
  var LIMITS = Object.freeze({ maxPaired: 4, maxPending: 1, maxRecords: 16, maxBytes: 32768, maxPairingMs: 12e4 });
8255
8255
  var copy = (value) => structuredClone(value);
8256
8256
  var keys = (value, expected) => value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).sort().join(",") === expected;
@@ -8646,7 +8646,7 @@ var MultiClientProfile = class {
8646
8646
  }
8647
8647
  };
8648
8648
 
8649
- // npm-release-beta-r1/host/phone-access-plugin/src/multi-client-runtime-profile.mjs
8649
+ // host-config-migration-r1/host/phone-access-plugin/src/multi-client-runtime-profile.mjs
8650
8650
  async function loadMultiClientProfile(directory, options = {}) {
8651
8651
  const vault = await (options.openVault ?? openProfileVault)(directory), profile = new MultiClientProfile(vault, options);
8652
8652
  try {
@@ -8673,43 +8673,165 @@ async function initializeMultiClientProfile(directory, relay, { openVault = open
8673
8673
  }
8674
8674
  }
8675
8675
 
8676
- // npm-release-beta-r1/host/phone-access-plugin/src/controller.mjs
8676
+ // host-config-migration-r1/host/phone-access-plugin/src/controller.mjs
8677
8677
  import { randomUUID as randomUUID2 } from "node:crypto";
8678
8678
  import { fileURLToPath } from "node:url";
8679
8679
 
8680
- // npm-release-beta-r1/host/phone-access-plugin/src/local-management.mjs
8680
+ // host-config-migration-r1/host/phone-access-plugin/src/local-management.mjs
8681
+ import { Server as HttpServer } from "node:http";
8682
+ import { Server as HttpsServer } from "node:https";
8681
8683
  import { isIP } from "node:net";
8682
- var ip = (value) => typeof value === "string" && value.startsWith("::ffff:") ? value.slice(7) : value;
8684
+
8685
+ // host-config-migration-r1/host/phone-access-plugin/src/lan-protocol.mjs
8686
+ var MAGIC = Buffer.from("DSHLAN01");
8687
+
8688
+ // host-config-migration-r1/host/phone-access-plugin/src/management-lan.mjs
8689
+ var normalizedAddress = (value) => typeof value === "string" && value.startsWith("::ffff:") ? value.slice(7) : value;
8690
+
8691
+ // host-config-migration-r1/host/phone-access-plugin/src/local-management.mjs
8683
8692
  var loopback = (value) => {
8684
- const a = ip(value);
8693
+ const a = normalizedAddress(value);
8685
8694
  return a === "::1" || isIP(a ?? "") === 4 && a.startsWith("127.");
8686
8695
  };
8687
8696
  var port = (value) => Number.isInteger(value) && value > 0 && value <= 65535;
8688
- var key = (s) => [ip(s.remoteAddress), s.remotePort, ip(s.localAddress), s.localPort].join("|");
8697
+ var key = (s) => [normalizedAddress(s.remoteAddress), s.remotePort, normalizedAddress(s.localAddress), s.localPort].join("|");
8698
+ var exact = (o, keys2) => o && typeof o === "object" && !Array.isArray(o) && Object.keys(o).sort().join(",") === keys2.slice().sort().join(",");
8699
+ function provenance(p) {
8700
+ if (!exact(p, ["schema", "route", "peerId", "pairId", "generation", "lanEndpoint"]) || p.schema !== 1 || !["LAN", "RELAY"].includes(p.route) || !/^\w{16,128}$/.test(p.peerId ?? "") || !/^[a-f0-9]{32}$/.test(p.pairId ?? "") || !Number.isSafeInteger(p.generation) || p.generation < 1) return null;
8701
+ if (p.route === "RELAY") return p.lanEndpoint === null ? Object.freeze({ ...p }) : null;
8702
+ const e = p.lanEndpoint;
8703
+ if (!exact(e, ["localAddress", "localPort", "remoteAddress", "remotePort"]) || isIP(normalizedAddress(e.localAddress)) !== 4 || isIP(normalizedAddress(e.remoteAddress)) !== 4 || e.localPort !== 38445 || !port(e.remotePort)) return null;
8704
+ return Object.freeze({ ...p, lanEndpoint: Object.freeze({ ...e }) });
8705
+ }
8689
8706
  function createBackendRegistry(backendPort) {
8690
- const connections = /* @__PURE__ */ new Map();
8707
+ const connections = /* @__PURE__ */ new Map(), tuples = /* @__PURE__ */ new Map(), sockets = /* @__PURE__ */ new WeakMap(), observed = /* @__PURE__ */ new Map(), waiting = /* @__PURE__ */ new Map(), denied = /* @__PURE__ */ new Set();
8708
+ let registryEpoch = 0, owner = /* @__PURE__ */ Symbol("standalone"), observer = null;
8709
+ const invalidate = () => {
8710
+ registryEpoch++;
8711
+ for (const w of waiting.values()) {
8712
+ clearTimeout(w.timer);
8713
+ w.reject(Error("BACKEND_OWNER_CLOSED"));
8714
+ }
8715
+ waiting.clear();
8716
+ for (const r of connections.values()) r.active = false;
8717
+ connections.clear();
8718
+ };
8691
8719
  return {
8692
- add(id, tuple) {
8693
- if (typeof id !== "string" || id.length > 80 || connections.has(id) || connections.size >= 64 || !tuple || !loopback(tuple.localAddress) || !loopback(tuple.remoteAddress) || !port(tuple.localPort) || tuple.remotePort !== backendPort) throw Error("BACKEND_REGISTRATION_REJECTED");
8694
- connections.set(id, [ip(tuple.localAddress), tuple.localPort, ip(tuple.remoteAddress), tuple.remotePort].join("|"));
8720
+ attachServer(server) {
8721
+ if (observer || !(server instanceof HttpServer || server instanceof HttpsServer) || !server.listening || server.address()?.port !== backendPort) throw Error("MANAGEMENT_SOCKET_OBSERVER_UNAVAILABLE");
8722
+ const event = server instanceof HttpsServer ? "secureConnection" : "connection";
8723
+ const accept = (socket) => {
8724
+ const k = key(socket);
8725
+ if (observed.size >= 2048 || observed.has(k)) return;
8726
+ observed.set(k, socket);
8727
+ const w = waiting.get(k);
8728
+ if (w) {
8729
+ waiting.delete(k);
8730
+ clearTimeout(w.timer);
8731
+ w.resolve(socket);
8732
+ }
8733
+ socket.once("close", () => {
8734
+ if (observed.get(k) === socket) observed.delete(k);
8735
+ const r = sockets.get(socket);
8736
+ if (r) {
8737
+ r.active = false;
8738
+ if (connections.get(r.id) === r) connections.delete(r.id);
8739
+ if (tuples.get(k) === r) tuples.delete(k);
8740
+ }
8741
+ });
8742
+ };
8743
+ server.on(event, accept);
8744
+ observer = { server, event, accept };
8745
+ return () => {
8746
+ if (observer?.accept !== accept) return;
8747
+ server.off(event, accept);
8748
+ observer = null;
8749
+ invalidate();
8750
+ owner = null;
8751
+ };
8752
+ },
8753
+ beginOwner(next) {
8754
+ invalidate();
8755
+ denied.clear();
8756
+ owner = next;
8757
+ },
8758
+ isOwner(value) {
8759
+ return owner === value && owner !== null;
8695
8760
  },
8696
- remove(id) {
8697
- connections.delete(id);
8761
+ endOwner(value) {
8762
+ if (owner !== value) return;
8763
+ invalidate();
8764
+ owner = null;
8765
+ },
8766
+ async add(id, tuple, p, source = owner) {
8767
+ if (!observer || source !== owner || owner === null || typeof id !== "string" || id.length > 80 || connections.has(id) || connections.size + waiting.size >= 64 || tuples.size >= 512 || !tuple || !loopback(tuple.localAddress) || !loopback(tuple.remoteAddress) || !port(tuple.localPort) || tuple.remotePort !== backendPort) throw Error("BACKEND_REGISTRATION_REJECTED");
8768
+ const began = registryEpoch, k = [normalizedAddress(tuple.localAddress), tuple.localPort, normalizedAddress(tuple.remoteAddress), tuple.remotePort].join("|");
8769
+ if (tuples.has(k) || waiting.has(k) || [...waiting.values()].some((w) => w.id === id)) throw Error("BACKEND_TUPLE_IN_USE");
8770
+ let socket = observed.get(k);
8771
+ if (!socket) socket = await new Promise((resolve, reject) => {
8772
+ const timer = setTimeout(() => {
8773
+ waiting.delete(k);
8774
+ reject(Error("BACKEND_NATIVE_SOCKET_TIMEOUT"));
8775
+ }, 2e3);
8776
+ waiting.set(k, { id, resolve, reject, timer, provenance: provenance(p) });
8777
+ });
8778
+ if (began !== registryEpoch || source !== owner || owner === null || !observer || tuples.has(k) || connections.has(id) || !socket || socket.destroyed || observed.get(k) !== socket) throw Error("BACKEND_NATIVE_SOCKET_REQUIRED");
8779
+ let meta = provenance(p);
8780
+ if (meta && denied.has(meta.peerId + "|" + meta.generation)) meta = null;
8781
+ const r = { id, key: k, owner, active: true, provenance: meta, socket };
8782
+ connections.set(id, r);
8783
+ tuples.set(k, r);
8784
+ sockets.set(socket, r);
8785
+ },
8786
+ remove(id, source = owner) {
8787
+ if (source !== owner) return;
8788
+ for (const [k, w] of waiting) if (w.id === id) {
8789
+ waiting.delete(k);
8790
+ clearTimeout(w.timer);
8791
+ w.reject(Error("BACKEND_CLOSED"));
8792
+ }
8793
+ const r = connections.get(id);
8794
+ if (r) {
8795
+ r.active = false;
8796
+ connections.delete(id);
8797
+ }
8698
8798
  },
8699
8799
  clear() {
8700
- connections.clear();
8800
+ invalidate();
8801
+ },
8802
+ lookup(socket) {
8803
+ if (!socket) return null;
8804
+ const r = sockets.get(socket);
8805
+ return r ?? (tuples.has(key(socket)) ? { active: false, provenance: null } : null);
8806
+ },
8807
+ active(r, socket) {
8808
+ return !!r && !!observer && r.active && r.owner === owner && owner !== null && r.socket === socket && !socket.destroyed && connections.get(r.id) === r;
8809
+ },
8810
+ denyPeer(peer2, generation) {
8811
+ if (peer2 && Number.isSafeInteger(generation)) denied.add(peer2 + "|" + generation);
8812
+ for (const [id, r] of connections) if (!peer2 || r.provenance?.peerId === peer2 && (generation === void 0 || r.provenance?.generation === generation)) {
8813
+ if (r.provenance) denied.add(r.provenance.peerId + "|" + r.provenance.generation);
8814
+ r.active = false;
8815
+ connections.delete(id);
8816
+ }
8817
+ for (const w of waiting.values()) if (w.provenance && (!peer2 || w.provenance.peerId === peer2 && (generation === void 0 || w.provenance.generation === generation))) denied.add(w.provenance.peerId + "|" + w.provenance.generation);
8818
+ },
8819
+ nativeObserved(socket) {
8820
+ return !!observer && !!socket && !socket.destroyed && observed.get(key(socket)) === socket;
8701
8821
  },
8702
8822
  has(socket) {
8703
- const k = key(socket);
8704
- return [...connections.values()].includes(k);
8823
+ return !!this.lookup(socket);
8705
8824
  },
8706
8825
  get size() {
8707
8826
  return connections.size;
8827
+ },
8828
+ get tracked() {
8829
+ return { observed: observed.size, tuples: tuples.size, waiting: waiting.size };
8708
8830
  }
8709
8831
  };
8710
8832
  }
8711
8833
 
8712
- // npm-release-beta-r1/host/phone-access-plugin/src/controller.mjs
8834
+ // host-config-migration-r1/host/phone-access-plugin/src/controller.mjs
8713
8835
  var AgentController = class {
8714
8836
  constructor({ backendPort, connectionMode = "auto", lanAddress, relay, relayCa, relayLeaf, profileDirectory, childEntry = new URL("./child.mjs", import.meta.url), onDiagnostic = () => {
8715
8837
  }, forkProcess = fork, timers = { setInterval, clearInterval } }) {
@@ -8801,7 +8923,9 @@ var AgentController = class {
8801
8923
  this.starts++;
8802
8924
  this.lifecycle.observe(child, this.starts);
8803
8925
  const generation = this.starts;
8804
- child.on("message", (message2) => {
8926
+ this.backendRegistry.beginOwner(child);
8927
+ child.on("message", async (message2) => {
8928
+ if (this.child !== child || this.starts !== generation || !this.backendRegistry.isOwner(child) || !["starting", "running"].includes(this.phase)) return;
8805
8929
  if (message2?.op === "diagnostic") {
8806
8930
  try {
8807
8931
  this.onDiagnostic(message2.event);
@@ -8812,21 +8936,22 @@ var AgentController = class {
8812
8936
  if (message2?.op === "backend.open") {
8813
8937
  let ok = false;
8814
8938
  try {
8815
- this.backendRegistry.add(message2.registrationId, message2.tuple);
8816
- ok = true;
8939
+ await this.backendRegistry.add(message2.registrationId, message2.tuple, message2.provenance, child);
8940
+ ok = this.child === child && this.starts === generation && this.backendRegistry.isOwner(child) && ["starting", "running"].includes(this.phase);
8817
8941
  } catch {
8818
8942
  }
8819
8943
  if (child.connected) child.send({ op: "backend.ack", registrationId: message2.registrationId, ok }, () => {
8820
8944
  });
8821
- } else if (message2?.op === "backend.close") this.backendRegistry.remove(message2.registrationId);
8945
+ } else if (message2?.op === "backend.close") this.backendRegistry.remove(message2.registrationId, child);
8822
8946
  });
8823
8947
  child.on("error", () => {
8948
+ if (this.child !== child || this.starts !== generation) return;
8824
8949
  this.lifecycle.emit("COMPANION_START_FAILED", generation, 1);
8825
8950
  this.lastError = "AGENT_SPAWN_FAILED";
8826
8951
  this.phase = "failed";
8827
8952
  });
8828
8953
  child.once("exit", () => {
8829
- this.backendRegistry.clear();
8954
+ this.backendRegistry.endOwner(child);
8830
8955
  if (this.child === child) {
8831
8956
  this.child = null;
8832
8957
  if (this.phase !== "stopping") {
@@ -8886,6 +9011,7 @@ var AgentController = class {
8886
9011
  return this.ask(this.child, "pair/cancel", { sessionId }).then((s) => this.snapshot(s));
8887
9012
  }
8888
9013
  revoke(target) {
9014
+ this.backendRegistry.denyPeer(target?.remotePeer, target?.generation);
8889
9015
  if (this.child && this.phase === "running") return this.ask(this.child, "revoke", { target }).then((s) => this.snapshot(s));
8890
9016
  return this.serialize(async () => {
8891
9017
  if (this.child) return this.snapshot(await this.ask(this.child, "revoke", { target }));
@@ -8911,6 +9037,7 @@ var AgentController = class {
8911
9037
  });
8912
9038
  }
8913
9039
  async stopChild(child, reason = "USER_STOP") {
9040
+ this.backendRegistry.endOwner(child);
8914
9041
  if (child.exitCode !== null || child.signalCode !== null) return;
8915
9042
  this.lifecycle.stop(child, reason);
8916
9043
  const exited = new Promise((resolve) => child.once("exit", resolve));
@@ -8935,6 +9062,7 @@ var AgentController = class {
8935
9062
  if (child.exitCode === null && child.signalCode === null) throw Error("OWNED_CHILD_NOT_STOPPED");
8936
9063
  }
8937
9064
  stop(reason = "USER_STOP") {
9065
+ if (this.child) this.backendRegistry.endOwner(this.child);
8938
9066
  return this.serialize(async () => {
8939
9067
  this.phase = "stopping";
8940
9068
  if (this.child) await this.stopChild(this.child, reason);