super-dsh 0.3.0-d → 0.3.0-e

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/client/index.js CHANGED
@@ -331,7 +331,7 @@ function RuntimeMark({ runtimeId, agentKey, size }) {
331
331
  function statusColor(status) {
332
332
  if (status === "reachable") return "var(--dsw-alias-state-success-primary)";
333
333
  if (status === "auth-expired") return "var(--dsw-alias-state-error-primary)";
334
- if (status === "unreachable") return "var(--dsw-alias-state-warn-primary)";
334
+ if (status === "unreachable") return "var(--dsw-alias-label-caption)";
335
335
  return "var(--dsw-alias-label-caption)";
336
336
  }
337
337
  /** The word the icon replaces — tooltip and accessibility text. */
@@ -614,7 +614,8 @@ async function addMachine(input) {
614
614
  headers: { "content-type": "application/json" },
615
615
  body: JSON.stringify({
616
616
  name: input.name,
617
- tokenUrl: input.tokenUrl
617
+ tokenUrl: input.tokenUrl,
618
+ ...pageOrigin() === "" ? {} : { origin: pageOrigin() }
618
619
  })
619
620
  });
620
621
  if (!response.ok) throw new Error(await errorText(response));
@@ -623,9 +624,21 @@ async function addMachine(input) {
623
624
  const raw = body.mutual;
624
625
  return { mutual: {
625
626
  ok: raw.ok === true,
627
+ ...raw.skipped === "self" || raw.skipped === "already-paired" ? { skipped: raw.skipped } : {},
626
628
  ...typeof raw.error === "string" && raw.error !== "" ? { error: raw.error } : {}
627
629
  } };
628
630
  }
631
+ /** Preserve shell schemes whose WHATWG origin is opaque; this is an address, not a reachability verdict. */
632
+ function pageOrigin() {
633
+ if (typeof location === "undefined") return "";
634
+ if (location.origin !== "null" && location.origin !== "") return location.origin;
635
+ try {
636
+ const url = new URL(location.href);
637
+ return url.protocol + "//" + url.host;
638
+ } catch {
639
+ return "";
640
+ }
641
+ }
629
642
  /**
630
643
  * DELETE one remote row by id (`{removed: boolean}` answers even for
631
644
  * unknown ids — a false removal is a reload, not an error).
@@ -671,7 +684,7 @@ const AUTO_REFRESH_NOTE = "auto-refreshes every 5 min";
671
684
  * face's public `authenticatedUrl` — this client never sees the raw token
672
685
  * anywhere else.
673
686
  *
674
- * @param origin - the page's own origin (`location.origin`), a bare http(s) origin.
687
+ * @param origin - the page's own host address, including a desktop scheme.
675
688
  * @returns the tokenized pairing URL.
676
689
  * @throws Error(body.error) on a non-2xx answer (validation or missing host face).
677
690
  */
@@ -692,9 +705,9 @@ async function fetchPairingUrl(origin) {
692
705
  }
693
706
  /**
694
707
  * Client-side pre-check before a pairing submit: a parseable absolute
695
- * http(s) URL carrying a non-empty `token` query param. The server re-parses
708
+ * host-bearing URL carrying a non-empty `token` query param. The server re-parses
696
709
  * authoritatively (mesh-link's parseTokenUrl); this only keeps obviously
697
- * dead drafts off the wire.
710
+ * malformed drafts off the wire.
698
711
  *
699
712
  * @param input - the raw token-URL draft (trim before calling).
700
713
  * @returns whether the draft is submit-shaped.
@@ -707,7 +720,7 @@ function validTokenUrl(input) {
707
720
  } catch {
708
721
  return false;
709
722
  }
710
- if (url.protocol !== "http:" && url.protocol !== "https:") return false;
723
+ if (url.host === "") return false;
711
724
  const token = url.searchParams.get("token");
712
725
  return token !== null && token !== "";
713
726
  }
@@ -3143,7 +3156,7 @@ function HubPage() {
3143
3156
  });
3144
3157
  }, [fusion]);
3145
3158
  const here = currentMountPath();
3146
- const hereOrigin = typeof location === "undefined" ? "" : location.origin;
3159
+ const hereOrigin = pageOrigin();
3147
3160
  const isActive = (agent) => hubRowActive({
3148
3161
  fusion,
3149
3162
  agentKey: agent.key,
@@ -3254,8 +3267,9 @@ function HubPage() {
3254
3267
  setShowAdd(false);
3255
3268
  setMeshError(null);
3256
3269
  reloadMachines();
3257
- if (out.mutual !== void 0 && !out.mutual.ok) showSuperdToast(`Super D: paired with ${name}, but the mutual half failed (${out.mutual.error}) — add this machine from the peer manually.`);
3258
- else showSuperdToast(`Super D: paired with ${name} — the peer also added this machine (mutual).`, { tone: "success" });
3270
+ if (out.mutual !== void 0 && !out.mutual.ok) showSuperdToast(`Super D: paired with ${name}; reverse connection unavailable.`);
3271
+ else if (out.mutual?.ok && out.mutual.skipped !== "self") showSuperdToast(`Super D: paired with ${name} — the peer also added this machine (mutual).`, { tone: "success" });
3272
+ else showSuperdToast(`Super D: paired with ${name}.`, { tone: "success" });
3259
3273
  }).catch((err) => {
3260
3274
  setMeshError(errText(err));
3261
3275
  }).finally(() => {
@@ -3282,7 +3296,7 @@ function HubPage() {
3282
3296
  * GET), one clipboard write, the outcome on the shared toast. */
3283
3297
  const copyPairingUrl = () => {
3284
3298
  if (pairingBusy) return;
3285
- const origin = typeof location === "undefined" ? "" : location.origin;
3299
+ const origin = pageOrigin();
3286
3300
  if (origin === "") return;
3287
3301
  setPairingBusy(true);
3288
3302
  fetchPairingUrl(origin).then((url) => (0, __deepseek_ai_dsh_client_ui_primitives.writeClipboard)(url)).then((copied) => {
@@ -3463,7 +3477,7 @@ function HubPage() {
3463
3477
  color: "var(--dsw-alias-label-tertiary)"
3464
3478
  },
3465
3479
  children: [
3466
- "Add another DSH from remote machines, provide the token URL. Pairing is mutual — the peer adds this machine back automatically. Status ",
3480
+ "Add another DSH with its token URL. The peer attempts to connect back. Status ",
3467
3481
  AUTO_REFRESH_NOTE,
3468
3482
  "."
3469
3483
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "super-dsh",
3
- "version": "0.3.0-d",
3
+ "version": "0.3.0-e",
4
4
  "description": "Super D for the DeepSeek Harness (DSH) — one dsh plugin that mounts the superd hub (runtime selector, roster, per-runtime agents) and joins local foreign-agent runtimes (OMP, Codex, Claude, Pi, Hermes, Antigravity) into the DSH Web UI. Each runtime keeps its native home and session store; the hub only bridges.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -83,7 +83,7 @@
83
83
  }
84
84
  },
85
85
  "dependencies": {
86
- "better-dsh": "0.3.0-d",
86
+ "better-dsh": "0.3.0-e",
87
87
  "@earendil-works/pi-coding-agent": "0.84.2",
88
88
  "zod": "4.4.3",
89
89
  "@modelcontextprotocol/sdk": "^1.29.0"
@@ -87,6 +87,7 @@ export declare function fetchMachines(): Promise<MeshList>;
87
87
  * pairing itself succeeded either way and the row is in). */
88
88
  export interface MutualPairingOutcome {
89
89
  readonly ok: boolean;
90
+ readonly skipped?: 'self' | 'already-paired';
90
91
  readonly error?: string;
91
92
  }
92
93
  /**
@@ -100,6 +101,8 @@ export interface MutualPairingOutcome {
100
101
  export declare function addMachine(input: AddMachineInput): Promise<{
101
102
  mutual?: MutualPairingOutcome;
102
103
  }>;
104
+ /** Preserve shell schemes whose WHATWG origin is opaque; this is an address, not a reachability verdict. */
105
+ export declare function pageOrigin(): string;
103
106
  /**
104
107
  * DELETE one remote row by id (`{removed: boolean}` answers even for
105
108
  * unknown ids — a false removal is a reload, not an error).
@@ -128,16 +131,16 @@ export declare const AUTO_REFRESH_NOTE = "auto-refreshes every 5 min";
128
131
  * face's public `authenticatedUrl` — this client never sees the raw token
129
132
  * anywhere else.
130
133
  *
131
- * @param origin - the page's own origin (`location.origin`), a bare http(s) origin.
134
+ * @param origin - the page's own host address, including a desktop scheme.
132
135
  * @returns the tokenized pairing URL.
133
136
  * @throws Error(body.error) on a non-2xx answer (validation or missing host face).
134
137
  */
135
138
  export declare function fetchPairingUrl(origin: string): Promise<string>;
136
139
  /**
137
140
  * Client-side pre-check before a pairing submit: a parseable absolute
138
- * http(s) URL carrying a non-empty `token` query param. The server re-parses
141
+ * host-bearing URL carrying a non-empty `token` query param. The server re-parses
139
142
  * authoritatively (mesh-link's parseTokenUrl); this only keeps obviously
140
- * dead drafts off the wire.
143
+ * malformed drafts off the wire.
141
144
  *
142
145
  * @param input - the raw token-URL draft (trim before calling).
143
146
  * @returns whether the draft is submit-shaped.
@@ -128,7 +128,7 @@ export async function addMachine(input) {
128
128
  credentials: 'same-origin',
129
129
  method: 'POST',
130
130
  headers: { 'content-type': 'application/json' },
131
- body: JSON.stringify({ name: input.name, tokenUrl: input.tokenUrl }),
131
+ body: JSON.stringify({ name: input.name, tokenUrl: input.tokenUrl, ...(pageOrigin() === '' ? {} : { origin: pageOrigin() }) }),
132
132
  });
133
133
  if (!response.ok)
134
134
  throw new Error(await errorText(response));
@@ -139,10 +139,25 @@ export async function addMachine(input) {
139
139
  return {
140
140
  mutual: {
141
141
  ok: raw.ok === true,
142
+ ...(raw.skipped === 'self' || raw.skipped === 'already-paired' ? { skipped: raw.skipped } : {}),
142
143
  ...(typeof raw.error === 'string' && raw.error !== '' ? { error: raw.error } : {}),
143
144
  },
144
145
  };
145
146
  }
147
+ /** Preserve shell schemes whose WHATWG origin is opaque; this is an address, not a reachability verdict. */
148
+ export function pageOrigin() {
149
+ if (typeof location === 'undefined')
150
+ return '';
151
+ if (location.origin !== 'null' && location.origin !== '')
152
+ return location.origin;
153
+ try {
154
+ const url = new URL(location.href);
155
+ return url.protocol + '//' + url.host;
156
+ }
157
+ catch {
158
+ return '';
159
+ }
160
+ }
146
161
  /**
147
162
  * DELETE one remote row by id (`{removed: boolean}` answers even for
148
163
  * unknown ids — a false removal is a reload, not an error).
@@ -187,7 +202,7 @@ export const AUTO_REFRESH_NOTE = 'auto-refreshes every 5 min';
187
202
  * face's public `authenticatedUrl` — this client never sees the raw token
188
203
  * anywhere else.
189
204
  *
190
- * @param origin - the page's own origin (`location.origin`), a bare http(s) origin.
205
+ * @param origin - the page's own host address, including a desktop scheme.
191
206
  * @returns the tokenized pairing URL.
192
207
  * @throws Error(body.error) on a non-2xx answer (validation or missing host face).
193
208
  */
@@ -208,9 +223,9 @@ export async function fetchPairingUrl(origin) {
208
223
  }
209
224
  /**
210
225
  * Client-side pre-check before a pairing submit: a parseable absolute
211
- * http(s) URL carrying a non-empty `token` query param. The server re-parses
226
+ * host-bearing URL carrying a non-empty `token` query param. The server re-parses
212
227
  * authoritatively (mesh-link's parseTokenUrl); this only keeps obviously
213
- * dead drafts off the wire.
228
+ * malformed drafts off the wire.
214
229
  *
215
230
  * @param input - the raw token-URL draft (trim before calling).
216
231
  * @returns whether the draft is submit-shaped.
@@ -225,7 +240,7 @@ export function validTokenUrl(input) {
225
240
  catch {
226
241
  return false;
227
242
  }
228
- if (url.protocol !== 'http:' && url.protocol !== 'https:')
243
+ if (url.host === '')
229
244
  return false;
230
245
  const token = url.searchParams.get('token');
231
246
  return token !== null && token !== '';
@@ -106,13 +106,22 @@ try {
106
106
  const CONTROL_PREFIX = '/api/superd'
107
107
  const origin = () => {
108
108
  const loc = g.location
109
+ if (g.document !== undefined && typeof g.document.baseURI === 'string') return g.document.baseURI
110
+ if (loc !== undefined && typeof loc.href === 'string') return loc.href
109
111
  if (loc !== undefined && typeof loc.origin === 'string' && loc.origin !== 'null') return loc.origin
110
112
  return 'http://dsh.internal'
111
113
  }
112
- // Same authority as the page (scheme-agnostic: ws/wss/http/https share host).
114
+ // The document and the shell-declared stream carrier can name different
115
+ // authorities. Read both at call time; never infer a carrier from localhost.
113
116
  const sameAuthority = (url) => {
114
117
  try {
115
- return url.host === new URL(origin()).host
118
+ if (url.host === new URL(origin()).host) return true
119
+ const base = g.__DSH_TRANSPORT__ && g.__DSH_TRANSPORT__.streamBaseUrl
120
+ if (typeof base !== 'string') return false
121
+ const carrier = new URL(base)
122
+ return (carrier.protocol === 'http:' || carrier.protocol === 'https:')
123
+ && (url.protocol === 'http:' || url.protocol === 'https:' || url.protocol === 'ws:' || url.protocol === 'wss:')
124
+ && url.host === carrier.host
116
125
  } catch {
117
126
  return false
118
127
  }
@@ -169,7 +178,9 @@ try {
169
178
  const url = new URL(s, origin())
170
179
  if (!sameAuthority(url)) return s
171
180
  const next = retarget(url.pathname, url.search, url.hash)
172
- return next === null ? s : url.origin + next
181
+ // WHATWG URL.origin is 'null' for custom schemes such as dsh-app.
182
+ // Preserve that scheme/authority instead of manufacturing an HTTP URL.
183
+ return next === null ? s : url.protocol + '//' + url.host + next
173
184
  } catch {
174
185
  return s
175
186
  }
@@ -1,6 +1,7 @@
1
1
  import z from '@deepseek-ai/schemastery';
2
2
  import { registerSelectorRpc, registryTargets } from './rpc.js';
3
3
  import { registerMeshRpc } from './mesh-rpc.js';
4
+ import { registerPairingAuth } from './pairing-auth.js';
4
5
  import { startReprobeSweep } from './mesh-link.js';
5
6
  import { federatedGatewayOf, startMeshFederation } from './mesh-federate.js';
6
7
  import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
@@ -59,6 +60,7 @@ export function apply(ctx, config) {
59
60
  const real = host.get('webServer');
60
61
  if (real instanceof WorldWebServer)
61
62
  return;
63
+ registerPairingAuth(host);
62
64
  const connection = host.get('connection');
63
65
  // The gate is ctx0's authorize source (connection.authorizeIndex); the
64
66
  // addressed world resolves per request through the live foreign-target
@@ -15,6 +15,8 @@ export interface MachineRow {
15
15
  token?: string;
16
16
  /** Last minted request cookie (spec §4): persisted so hub restarts keep the remote's 30-day session instead of forcing a re-mint on every boot. */
17
17
  cookie?: string;
18
+ /** Received a reciprocal offer; without a verified cookie this is an incoming-only row. */
19
+ incoming?: boolean;
18
20
  enabled: boolean;
19
21
  /** P4 exec knob on the INITIATOR side (spec §5): undefined/true = this machine's agents may exec there; false = the tool leg refuses. */
20
22
  execEnabled?: boolean;
@@ -29,6 +29,7 @@ function isMachineRow(v) {
29
29
  typeof r.origin === 'string' && r.origin !== '' && typeof r.enabled === 'boolean' && typeof r.addedAt === 'number' &&
30
30
  (r.token === undefined || typeof r.token === 'string') &&
31
31
  (r.cookie === undefined || typeof r.cookie === 'string') &&
32
+ (r.incoming === undefined || typeof r.incoming === 'boolean') &&
32
33
  (r.execEnabled === undefined || r.execEnabled === true || r.execEnabled === false));
33
34
  }
34
35
  /** Mint a random short id over [a-z0-9], re-minting on collision with `taken`; loud after 32 attempts. */
@@ -80,6 +81,7 @@ export function loadMachines(home) {
80
81
  addedAt: r.addedAt,
81
82
  ...(r.token === undefined ? {} : { token: r.token }),
82
83
  ...(r.cookie === undefined ? {} : { cookie: r.cookie }),
84
+ ...(r.incoming === undefined ? {} : { incoming: r.incoming }),
83
85
  ...(r.execEnabled === undefined ? {} : { execEnabled: r.execEnabled }),
84
86
  })) };
85
87
  }
@@ -72,10 +72,10 @@ export interface MutualPairing {
72
72
  /** This machine's operator-facing name (hostname, port-qualified per instance). */
73
73
  selfName: string;
74
74
  }
75
- /** The reverse leg's outcome. `ok` is also true when the peer answered
76
- * duplicate-machine-id — it already holds us, so mutual already holds. */
75
+ /** The peer's explicitly confirmed reverse-leg outcome. */
77
76
  export interface MutualOutcome {
78
77
  ok: boolean;
78
+ skipped?: 'self' | 'already-paired';
79
79
  /** Why the offer failed; absent on success. The pairing itself never fails with it. */
80
80
  error?: string;
81
81
  }
@@ -204,6 +204,14 @@ export declare function pairMachine(home: string, input: {
204
204
  tokenUrl: string;
205
205
  mutual?: MutualPairing;
206
206
  }, deps?: LinkDeps): Promise<PairOutcome>;
207
+ /** Incoming offers name identity before dialing; address format is not reachability evidence. */
208
+ export declare function receivePairing(home: string, input: {
209
+ machineId: string;
210
+ name: string;
211
+ tokenUrl: string;
212
+ }, deps?: LinkDeps): Promise<MutualOutcome & {
213
+ row?: MachineRow;
214
+ }>;
207
215
  /**
208
216
  * Re-probe a paired machine: the jar's cookie, falling back to the cookie
209
217
  * persisted on the row (cold jar after a hub restart), rides the probe. On
@@ -20,8 +20,9 @@
20
20
  * records a `versionWarning` on the status entry (diff major/minor or
21
21
  * unparsable → warn; patch-only drift → clean).
22
22
  */
23
- import { loadMachines, saveMachines, upsertRemote, listRemotes } from './machines.js';
23
+ import { ensureSelf, loadMachines, saveMachines, upsertRemote, listRemotes } from './machines.js';
24
24
  import { PACK_VERSION } from './pack-version.js';
25
+ import { PAIRING_AUTH_PATH } from './pairing-auth.js';
25
26
  /**
26
27
  * The `ours` side of the I2 comparison — the super-dsh PACK version (D6: the
27
28
  * hub subpackage's constant 0.1.0 made the comparator dead; the pack number
@@ -68,6 +69,9 @@ export function versionWarningFor(peer, ours) {
68
69
  const FETCH_TIMEOUT_MS = 10_000;
69
70
  /** <machineId, cookie> — in-process jar, a front for the persisted `MachineRow.cookie` (same value; write-through on every mint). */
70
71
  const cookieJar = new Map();
72
+ /** Latest pairing/probe attempt owns status and federation updates. */
73
+ const probeAttempts = new Map();
74
+ const removalGenerations = new Map();
71
75
  const machineStatusTable = new Map();
72
76
  function errMsg(err) {
73
77
  return err instanceof Error ? err.message : String(err);
@@ -110,9 +114,20 @@ function firstCookiePair(raw) {
110
114
  * with the status/cause attached.
111
115
  */
112
116
  export async function mintCookie(origin, token, fetchImpl = globalThis.fetch) {
117
+ // Native peers exchange at /. Super peers also expose a dedicated route
118
+ // because Better Dash owns / for password login. Both use native auth.
119
+ const root = await exchangeCookie(origin, '/', token, fetchImpl);
120
+ if (root !== undefined)
121
+ return root;
122
+ const dedicated = await exchangeCookie(origin, PAIRING_AUTH_PATH, token, fetchImpl);
123
+ if (dedicated !== undefined)
124
+ return dedicated;
125
+ throw new Error(`mesh-link: token mint failed — no dsh-auth-* Set-Cookie in response from ${origin}`);
126
+ }
127
+ async function exchangeCookie(origin, path, token, fetchImpl) {
113
128
  let res;
114
129
  try {
115
- res = await fetchImpl(`${origin}/?token=${encodeURIComponent(token)}`, {
130
+ res = await fetchImpl(`${origin}${path}?token=${encodeURIComponent(token)}`, {
116
131
  redirect: 'manual',
117
132
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
118
133
  });
@@ -120,6 +135,8 @@ export async function mintCookie(origin, token, fetchImpl = globalThis.fetch) {
120
135
  catch (err) {
121
136
  throw new Error(`mesh-link: token mint to ${origin} failed — ${errMsg(err)}`);
122
137
  }
138
+ if (path === PAIRING_AUTH_PATH && res.status === 404)
139
+ return undefined;
123
140
  if (res.status !== 303 && !(res.status >= 200 && res.status < 300)) {
124
141
  throw new Error(`mesh-link: token mint failed — unexpected status ${res.status} from ${origin}`);
125
142
  }
@@ -131,7 +148,7 @@ export async function mintCookie(origin, token, fetchImpl = globalThis.fetch) {
131
148
  if (pair !== null && pair.startsWith('dsh-auth-'))
132
149
  return pair;
133
150
  }
134
- throw new Error(`mesh-link: token mint failed — no dsh-auth-* Set-Cookie in response (status ${res.status}) from ${origin}`);
151
+ return undefined;
135
152
  }
136
153
  function coerceAgent(v) {
137
154
  const a = (typeof v === 'object' && v !== null ? v : {});
@@ -203,6 +220,9 @@ export function machineStatus() {
203
220
  /** Test hook: drop all status entries. */
204
221
  export function resetMachineStatus() {
205
222
  machineStatusTable.clear();
223
+ cookieJar.clear();
224
+ probeAttempts.clear();
225
+ removalGenerations.clear();
206
226
  }
207
227
  const federationListeners = new Set();
208
228
  /** Subscribe to federation lifecycle events; returns an unsubscribe function. */
@@ -223,6 +243,10 @@ function emitFederation(event) {
223
243
  * mesh-link, the registrar subscribes at wiring time).
224
244
  */
225
245
  export function emitMachineRemoved(machineId) {
246
+ removalGenerations.set(machineId, (removalGenerations.get(machineId) ?? 0) + 1);
247
+ cookieJar.delete(machineId);
248
+ machineStatusTable.delete(machineId);
249
+ probeAttempts.set(machineId, Symbol());
226
250
  emitFederation({ type: 'defederated', machineId });
227
251
  }
228
252
  /**
@@ -233,12 +257,9 @@ export function emitMachineRemoved(machineId) {
233
257
  * in this process), else `''` (wire calls then simply omit the Cookie header).
234
258
  */
235
259
  export function cookieOf(machineId, home) {
236
- const jarred = cookieJar.get(machineId);
237
- if (jarred !== undefined)
238
- return jarred;
239
- if (home === undefined)
240
- return '';
241
- return listRemotes(home).find((r) => r.id === machineId)?.cookie ?? '';
260
+ if (home !== undefined)
261
+ return listRemotes(home).find((r) => r.id === machineId)?.cookie ?? '';
262
+ return cookieJar.get(machineId) ?? '';
242
263
  }
243
264
  /** Record one probe outcome; the I2 versionWarning (4th arg) lands only on
244
265
  * reachable probes — every entry is written FRESH, so a re-probe against a
@@ -278,14 +299,19 @@ function persistRow(home, row, deps) {
278
299
  export async function pairMachine(home, input, deps) {
279
300
  const fetchImpl = deps?.fetchImpl ?? globalThis.fetch;
280
301
  const load = deps?.load ?? loadMachines;
302
+ const removalsAtStart = new Map(removalGenerations);
281
303
  const { origin, token } = parseTokenUrl(input.tokenUrl);
282
304
  const cookie = await mintCookie(origin, token, fetchImpl);
283
305
  const probe = await probeMachine(origin, cookie, fetchImpl);
284
306
  if (!probe.ok) {
285
307
  throw new Error(`mesh-link: pairing ${origin} failed — ${probe.status}: ${probe.error}`);
286
308
  }
309
+ if ((removalGenerations.get(probe.machineId) ?? 0) !== (removalsAtStart.get(probe.machineId) ?? 0)) {
310
+ throw new Error('mesh-link: machine removed during pairing');
311
+ }
287
312
  const file = load(home);
288
- if (probe.machineId === file.self.id || file.remotes.some((r) => r.id === probe.machineId)) {
313
+ const prior = file.remotes.find((r) => r.id === probe.machineId);
314
+ if (probe.machineId === file.self.id || (prior !== undefined && !(prior.incoming && !prior.cookie))) {
289
315
  throw new Error(`mesh-link: duplicate-machine-id ${probe.machineId} — collides with self or an existing remote row; remove the stale row before re-pairing`);
290
316
  }
291
317
  const row = {
@@ -294,16 +320,19 @@ export async function pairMachine(home, input, deps) {
294
320
  origin,
295
321
  token,
296
322
  cookie,
297
- enabled: true,
298
- addedAt: Date.now(),
323
+ enabled: prior?.enabled ?? true,
324
+ addedAt: prior?.addedAt ?? Date.now(),
325
+ ...(prior?.incoming === undefined ? {} : { incoming: prior.incoming }),
326
+ ...(prior?.execEnabled === undefined ? {} : { execEnabled: prior.execEnabled }),
299
327
  };
300
328
  persistRow(home, row, deps);
329
+ probeAttempts.set(row.id, Symbol());
301
330
  cookieJar.set(row.id, cookie);
302
331
  recordStatus(row.id, 'reachable', undefined, versionWarningFor(probe.version, OWN_VERSION));
303
332
  emitFederation({ type: 'federated', machineId: row.id, machineName: row.name, origin, agents: probe.agents });
304
333
  const outcome = { machineId: row.id, row, probe };
305
334
  if (input.mutual !== undefined)
306
- outcome.mutual = await reversePair(row, input.mutual, fetchImpl);
335
+ outcome.mutual = await reversePair(row, input.mutual, ensureSelf(home, 'local').id, fetchImpl);
307
336
  return outcome;
308
337
  }
309
338
  const REVERSE_TIMEOUT_MS = 10_000;
@@ -311,18 +340,18 @@ const REVERSE_TIMEOUT_MS = 10_000;
311
340
  * The mutual-pairing reverse leg: offer THIS machine's pairing input to the
312
341
  * just-paired peer over the cookie minted seconds ago (the peer then pairs
313
342
  * back without its own reverse leg — the offer carries `mutual: true`). A
314
- * duplicate-machine-id answer means the peer already holds us: mutual is
315
- * already satisfied, reported as ok. EVERY failure lands on the outcome and
343
+ * peer must explicitly confirm the reverse outcome or a reachable row with
344
+ * our UID. EVERY failure lands on the outcome and
316
345
  * never fails the pairing itself — the user sees it and can still add
317
346
  * manually from the peer.
318
347
  */
319
- async function reversePair(row, mutual, fetchImpl) {
348
+ async function reversePair(row, mutual, machineId, fetchImpl) {
320
349
  let res;
321
350
  try {
322
351
  res = await fetchImpl(`${row.origin}/api/superd/machines`, {
323
352
  method: 'POST',
324
353
  headers: { cookie: row.cookie ?? '', 'content-type': 'application/json' },
325
- body: JSON.stringify({ name: mutual.selfName, tokenUrl: mutual.selfTokenUrl, mutual: true }),
354
+ body: JSON.stringify({ name: mutual.selfName, tokenUrl: mutual.selfTokenUrl, machineId, mutual: true }),
326
355
  signal: AbortSignal.timeout(REVERSE_TIMEOUT_MS),
327
356
  });
328
357
  }
@@ -337,14 +366,64 @@ async function reversePair(row, mutual, fetchImpl) {
337
366
  error = body.error;
338
367
  }
339
368
  catch { /* unreadable body */ }
340
- if (error.includes('duplicate-machine-id'))
341
- return { ok: true };
342
369
  return { ok: false, error: error === '' ? `the peer answered ${res.status}` : error };
343
370
  }
344
- if (res.status >= 200 && res.status < 300)
345
- return { ok: true };
371
+ if (res.status >= 200 && res.status < 300) {
372
+ const body = await res.json().catch(() => undefined);
373
+ const reciprocal = body?.reciprocal;
374
+ if (typeof reciprocal?.ok === 'boolean') {
375
+ return {
376
+ ok: reciprocal.ok,
377
+ ...(reciprocal.skipped === 'self' || reciprocal.skipped === 'already-paired' ? { skipped: reciprocal.skipped } : {}),
378
+ ...(typeof reciprocal.error === 'string' ? { error: reciprocal.error } : {}),
379
+ };
380
+ }
381
+ // Older peers confirm the paired identity through their public machine row.
382
+ if (body?.machine?.id === machineId && body.machine.status === 'reachable')
383
+ return { ok: true };
384
+ return { ok: false, error: 'the peer did not confirm a reverse connection' };
385
+ }
346
386
  return { ok: false, error: `the peer answered ${res.status}` };
347
387
  }
388
+ /** Incoming offers name identity before dialing; address format is not reachability evidence. */
389
+ export async function receivePairing(home, input, deps) {
390
+ const self = ensureSelf(home, 'local');
391
+ if (input.machineId === self.id)
392
+ return { ok: true, skipped: 'self' };
393
+ const load = deps?.load ?? loadMachines;
394
+ const existing = load(home).remotes.find((row) => row.id === input.machineId);
395
+ if (existing?.cookie) {
396
+ persistRow(home, { ...existing, incoming: true }, deps);
397
+ const probe = await reprobeMachine(home, existing.id, deps);
398
+ return {
399
+ ok: probe.ok, ...(probe.ok ? { skipped: 'already-paired' } : { error: probe.error }),
400
+ row: load(home).remotes.find((row) => row.id === existing.id),
401
+ };
402
+ }
403
+ let url;
404
+ try {
405
+ url = new URL(input.tokenUrl);
406
+ }
407
+ catch {
408
+ return { ok: false, error: 'mesh-link: invalid reciprocal URL' };
409
+ }
410
+ const origin = url.protocol + '//' + url.host;
411
+ const token = url.searchParams.get('token') ?? '';
412
+ const row = {
413
+ id: input.machineId, name: existing?.name ?? input.name, origin, token,
414
+ incoming: true, enabled: existing?.enabled ?? true, addedAt: existing?.addedAt ?? Date.now(),
415
+ ...(existing?.execEnabled === undefined ? {} : { execEnabled: existing.execEnabled }),
416
+ };
417
+ // Retain the identity even if the address cannot be dialed from this Host.
418
+ persistRow(home, row, deps);
419
+ recordStatus(row.id, 'unreachable', 'reciprocal connection has not been established');
420
+ const probe = await reprobeMachine(home, row.id, deps);
421
+ return {
422
+ ok: probe.ok,
423
+ row: load(home).remotes.find((current) => current.id === row.id),
424
+ ...(probe.ok ? {} : { error: probe.error }),
425
+ };
426
+ }
348
427
  /**
349
428
  * Re-probe a paired machine: the jar's cookie, falling back to the cookie
350
429
  * persisted on the row (cold jar after a hub restart), rides the probe. On
@@ -361,23 +440,48 @@ export async function reprobeMachine(home, id, deps) {
361
440
  throw new Error(`mesh-link: unknown machine id "${id}" — no remote row in the registry`);
362
441
  }
363
442
  const origin = row.origin;
364
- let cookie = cookieJar.get(id) ?? row.cookie ?? '';
365
- let probe = await probeMachine(origin, cookie, fetchImpl);
366
- if (!probe.ok && probe.status === 'auth-expired' && row.token) {
443
+ const attempt = Symbol();
444
+ probeAttempts.set(id, attempt);
445
+ let cookie = row.cookie ?? '';
446
+ let probe;
447
+ if (cookie === '' && row.token) {
448
+ try {
449
+ cookie = await mintCookie(origin, row.token, fetchImpl);
450
+ probe = await probeMachine(origin, cookie, fetchImpl);
451
+ }
452
+ catch (err) {
453
+ probe = { ok: false, status: 'unreachable', error: `connection failed — ${errMsg(err)}` };
454
+ }
455
+ }
456
+ else
457
+ probe = await probeMachine(origin, cookie, fetchImpl);
458
+ if (!probe.ok && probe.status === 'auth-expired' && row.token && cookie !== '') {
367
459
  try {
368
460
  cookie = await mintCookie(origin, row.token, fetchImpl);
369
- cookieJar.set(id, cookie);
370
- persistRow(home, { ...row, cookie }, deps);
371
461
  probe = await probeMachine(origin, cookie, fetchImpl);
372
462
  }
373
463
  catch (err) {
374
464
  probe = { ok: false, status: 'auth-expired', error: `re-mint failed — ${errMsg(err)}` };
375
465
  }
376
466
  }
467
+ if (probe.ok && probe.machineId !== id) {
468
+ probe = { ok: false, status: 'unreachable', error: 'peer Hub UID does not match the paired machine' };
469
+ }
470
+ if (probe.ok && row.incoming && cookie === '') {
471
+ probe = { ok: false, status: 'unreachable', error: 'reciprocal connection has no authenticated credentials' };
472
+ }
473
+ if (!probe.ok && row.incoming && !row.cookie) {
474
+ probe = { ...probe, status: 'unreachable' };
475
+ }
476
+ const current = (deps?.load ? deps.load(home).remotes : listRemotes(home)).find((r) => r.id === id);
477
+ if (probeAttempts.get(id) !== attempt || current === undefined || current.origin !== row.origin || current.token !== row.token || current.cookie !== row.cookie || current.addedAt !== row.addedAt) {
478
+ return { ok: false, status: 'unreachable', error: 'pairing changed during probe' };
479
+ }
377
480
  if (probe.ok) {
481
+ persistRow(home, { ...current, cookie }, deps);
378
482
  cookieJar.set(id, cookie);
379
483
  recordStatus(id, 'reachable', undefined, versionWarningFor(probe.version, OWN_VERSION));
380
- emitFederation({ type: 'federated', machineId: id, machineName: row.name, origin, agents: probe.agents });
484
+ emitFederation({ type: 'federated', machineId: id, machineName: current.name, origin, agents: probe.agents });
381
485
  }
382
486
  else if (probe.status === 'auth-expired') {
383
487
  recordStatus(id, 'auth-expired', probe.error);
@@ -98,7 +98,7 @@ export declare function tokenPreviewOf(tokenUrl: string): string | undefined;
98
98
  */
99
99
  export declare function handleMeshMachines(request: Request, deps: MeshDeps): Promise<Response>;
100
100
  /**
101
- * Validate a token-url request origin: an absolute http(s) URL that is ONLY
101
+ * Validate a token-url request origin: an absolute host-bearing URL that is ONLY
102
102
  * an origin — no path, query, hash, or credentials. The pairing URL a user
103
103
  * copies must address the server root (`authorizeIndex` answers `/` only),
104
104
  * and anything beyond the origin would smuggle parts the mint would overwrite
@@ -1,6 +1,6 @@
1
1
  import { hostname } from 'node:os';
2
2
  import { ensureSelf, listRemotes, removeRemote } from './machines.js';
3
- import { machineStatus, pairMachine, reprobeMachine, emitMachineRemoved } from './mesh-link.js';
3
+ import { machineStatus, pairMachine, receivePairing, reprobeMachine, emitMachineRemoved } from './mesh-link.js';
4
4
  import { A2A_ENDPOINT_PATH, A2A_OUTBOX_PATH, A2A_WIRE_VERSION, decodeA2aWireFrame } from './a2a-wire.js';
5
5
  import { EXEC_API_BASE, parseOneShotExecBody, runOneShotExec } from './exec.js';
6
6
  /** The shared-channel Fetch route path this API owns. */
@@ -37,7 +37,7 @@ function publicRow(row) {
37
37
  function projectRow(row, status) {
38
38
  const entry = status.get(row.id);
39
39
  if (entry === undefined)
40
- return { id: row.id, name: row.name, origin: row.origin, enabled: row.enabled, addedAt: row.addedAt, status: 'pending' };
40
+ return { id: row.id, name: row.name, origin: row.origin, enabled: row.enabled, addedAt: row.addedAt, status: row.incoming && !row.cookie ? 'unreachable' : 'pending' };
41
41
  const out = { id: row.id, name: row.name, origin: row.origin, enabled: row.enabled, addedAt: row.addedAt, status: entry.status, lastProbeAt: entry.lastProbeAt };
42
42
  if (entry.error !== undefined)
43
43
  out.error = entry.error;
@@ -101,7 +101,7 @@ function handleList(deps) {
101
101
  });
102
102
  }
103
103
  /**
104
- * Validate a token-url request origin: an absolute http(s) URL that is ONLY
104
+ * Validate a token-url request origin: an absolute host-bearing URL that is ONLY
105
105
  * an origin — no path, query, hash, or credentials. The pairing URL a user
106
106
  * copies must address the server root (`authorizeIndex` answers `/` only),
107
107
  * and anything beyond the origin would smuggle parts the mint would overwrite
@@ -119,15 +119,14 @@ export function parsePairingOrigin(origin) {
119
119
  catch {
120
120
  return { ok: false, error: 'mesh-api: tokenUrl origin is not a parseable absolute URL' };
121
121
  }
122
- if (url.protocol !== 'http:' && url.protocol !== 'https:') {
123
- return { ok: false, error: `mesh-api: tokenUrl origin must be http(s), got "${url.protocol}"` };
124
- }
122
+ if (url.host === '')
123
+ return { ok: false, error: 'mesh-api: tokenUrl origin must name a host' };
125
124
  if (url.username !== '' || url.password !== '')
126
125
  return { ok: false, error: 'mesh-api: tokenUrl origin must not carry credentials' };
127
- if (url.pathname !== '/' || url.search !== '' || url.hash !== '') {
126
+ if ((url.pathname !== '/' && url.pathname !== '') || url.search !== '' || url.hash !== '') {
128
127
  return { ok: false, error: 'mesh-api: tokenUrl origin must be a bare origin (no path, query or hash)' };
129
128
  }
130
- return { ok: true, origin: url.origin };
129
+ return { ok: true, origin: url.protocol + '//' + url.host };
131
130
  }
132
131
  /** POST op `tokenUrl`: tokenize the caller's origin with the live launch
133
132
  * token (spec hub-roster-identity §3). The answer rides a POST the user
@@ -153,14 +152,18 @@ function handleTokenUrl(body, deps) {
153
152
  * (no mint face, no Host header) — the peer can then only be paired
154
153
  * manually from its own UI.
155
154
  */
156
- function buildMutual(request, deps) {
155
+ function buildMutual(request, deps, pageOrigin) {
157
156
  if (deps.tokenUrlOf === undefined)
158
157
  return undefined;
159
158
  const host = request.headers.get('host');
160
- if (host === null || host === '')
159
+ if ((host === null || host === '') && typeof pageOrigin !== 'string')
161
160
  return undefined;
162
161
  const proto = request.headers.get('x-forwarded-proto')?.split(',')[0]?.trim() ?? 'http';
163
- const origin = `${proto}://${host}`;
162
+ const raw = typeof pageOrigin === 'string' ? pageOrigin : `${proto}://${host}`;
163
+ const parsed = parsePairingOrigin(raw);
164
+ if (!parsed.ok)
165
+ return undefined;
166
+ const origin = parsed.origin;
164
167
  const port = new URL(origin).port;
165
168
  const name = hostname();
166
169
  return {
@@ -207,9 +210,14 @@ async function handlePost(request, deps) {
207
210
  if (name === '' || tokenUrl === '') {
208
211
  return json(400, { error: 'mesh-api: pair body must be {name, tokenUrl} with non-empty name and tokenUrl' });
209
212
  }
213
+ if (body.mutual === true && typeof body.machineId === 'string' && body.machineId !== '') {
214
+ const received = await receivePairing(deps.home, { machineId: body.machineId, name, tokenUrl }, { fetchImpl: deps.fetchImpl });
215
+ const { row, ...reciprocal } = received;
216
+ return json(201, { ...(row === undefined ? {} : { machine: publicRow(row) }), reciprocal });
217
+ }
210
218
  // The reverse leg rides USER-initiated pairs only: an offer that arrived
211
219
  // with `mutual: true` (the peer's own reverse leg) pairs without one.
212
- const mutual = body.mutual === true ? undefined : buildMutual(request, deps);
220
+ const mutual = body.mutual === true ? undefined : buildMutual(request, deps, body.origin);
213
221
  let outcome;
214
222
  try {
215
223
  outcome = await pairMachine(deps.home, { name, tokenUrl, ...(mutual === undefined ? {} : { mutual }) }, { fetchImpl: deps.fetchImpl });
@@ -0,0 +1,7 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { HostConnectionHandle, ConnectionIndexRequest, ConnectionIndexResponse } from '@deepseek-ai/dsh-client-connection';
3
+ export declare const PAIRING_AUTH_PATH = "/superd/pair-auth";
4
+ /** Exchange the native launch token without taking ownership of the password page. */
5
+ export declare function handlePairingAuth(request: ConnectionIndexRequest, response: ConnectionIndexResponse, connection: Pick<HostConnectionHandle, 'authorizeIndex'>): void;
6
+ /** Called only in ctx0's real WebServer/Connection injection scope. */
7
+ export declare function registerPairingAuth(ctx: Context): void;
@@ -0,0 +1,29 @@
1
+ export const PAIRING_AUTH_PATH = '/superd/pair-auth';
2
+ /** Exchange the native launch token without taking ownership of the password page. */
3
+ export function handlePairingAuth(request, response, connection) {
4
+ if (request.method !== 'GET') {
5
+ response.writeHead(405, { allow: 'GET', 'cache-control': 'no-store' });
6
+ response.end();
7
+ return;
8
+ }
9
+ const url = new URL(request.url ?? PAIRING_AUTH_PATH, 'http://dsh.invalid');
10
+ const tokens = url.searchParams.getAll('token');
11
+ if (tokens.length !== 1 || tokens[0] === '') {
12
+ response.writeHead(401, { 'cache-control': 'no-store' });
13
+ response.end();
14
+ return;
15
+ }
16
+ // Host headers keep native authority binding. Connection alone checks the
17
+ // process token and signs the cookie; no credential or verifier is copied.
18
+ if (connection.authorizeIndex({ method: 'GET', url: '/' + url.search, headers: request.headers }, response)) {
19
+ response.writeHead(401, { 'cache-control': 'no-store' });
20
+ response.end();
21
+ }
22
+ }
23
+ /** Called only in ctx0's real WebServer/Connection injection scope. */
24
+ export function registerPairingAuth(ctx) {
25
+ ctx.effect(() => ctx.webServer.register({
26
+ kind: 'exact', path: PAIRING_AUTH_PATH,
27
+ handler: (request, response) => handlePairingAuth(request, response, ctx.connection),
28
+ }), 'superd-hub: native pairing authentication');
29
+ }
@@ -331,7 +331,7 @@ function RuntimeMark({ runtimeId, agentKey, size }) {
331
331
  function statusColor(status) {
332
332
  if (status === "reachable") return "var(--dsw-alias-state-success-primary)";
333
333
  if (status === "auth-expired") return "var(--dsw-alias-state-error-primary)";
334
- if (status === "unreachable") return "var(--dsw-alias-state-warn-primary)";
334
+ if (status === "unreachable") return "var(--dsw-alias-label-caption)";
335
335
  return "var(--dsw-alias-label-caption)";
336
336
  }
337
337
  /** The word the icon replaces — tooltip and accessibility text. */
@@ -614,7 +614,8 @@ async function addMachine(input) {
614
614
  headers: { "content-type": "application/json" },
615
615
  body: JSON.stringify({
616
616
  name: input.name,
617
- tokenUrl: input.tokenUrl
617
+ tokenUrl: input.tokenUrl,
618
+ ...pageOrigin() === "" ? {} : { origin: pageOrigin() }
618
619
  })
619
620
  });
620
621
  if (!response.ok) throw new Error(await errorText(response));
@@ -623,9 +624,21 @@ async function addMachine(input) {
623
624
  const raw = body.mutual;
624
625
  return { mutual: {
625
626
  ok: raw.ok === true,
627
+ ...raw.skipped === "self" || raw.skipped === "already-paired" ? { skipped: raw.skipped } : {},
626
628
  ...typeof raw.error === "string" && raw.error !== "" ? { error: raw.error } : {}
627
629
  } };
628
630
  }
631
+ /** Preserve shell schemes whose WHATWG origin is opaque; this is an address, not a reachability verdict. */
632
+ function pageOrigin() {
633
+ if (typeof location === "undefined") return "";
634
+ if (location.origin !== "null" && location.origin !== "") return location.origin;
635
+ try {
636
+ const url = new URL(location.href);
637
+ return url.protocol + "//" + url.host;
638
+ } catch {
639
+ return "";
640
+ }
641
+ }
629
642
  /**
630
643
  * DELETE one remote row by id (`{removed: boolean}` answers even for
631
644
  * unknown ids — a false removal is a reload, not an error).
@@ -671,7 +684,7 @@ const AUTO_REFRESH_NOTE = "auto-refreshes every 5 min";
671
684
  * face's public `authenticatedUrl` — this client never sees the raw token
672
685
  * anywhere else.
673
686
  *
674
- * @param origin - the page's own origin (`location.origin`), a bare http(s) origin.
687
+ * @param origin - the page's own host address, including a desktop scheme.
675
688
  * @returns the tokenized pairing URL.
676
689
  * @throws Error(body.error) on a non-2xx answer (validation or missing host face).
677
690
  */
@@ -692,9 +705,9 @@ async function fetchPairingUrl(origin) {
692
705
  }
693
706
  /**
694
707
  * Client-side pre-check before a pairing submit: a parseable absolute
695
- * http(s) URL carrying a non-empty `token` query param. The server re-parses
708
+ * host-bearing URL carrying a non-empty `token` query param. The server re-parses
696
709
  * authoritatively (mesh-link's parseTokenUrl); this only keeps obviously
697
- * dead drafts off the wire.
710
+ * malformed drafts off the wire.
698
711
  *
699
712
  * @param input - the raw token-URL draft (trim before calling).
700
713
  * @returns whether the draft is submit-shaped.
@@ -707,7 +720,7 @@ function validTokenUrl(input) {
707
720
  } catch {
708
721
  return false;
709
722
  }
710
- if (url.protocol !== "http:" && url.protocol !== "https:") return false;
723
+ if (url.host === "") return false;
711
724
  const token = url.searchParams.get("token");
712
725
  return token !== null && token !== "";
713
726
  }
@@ -3143,7 +3156,7 @@ function HubPage() {
3143
3156
  });
3144
3157
  }, [fusion]);
3145
3158
  const here = currentMountPath();
3146
- const hereOrigin = typeof location === "undefined" ? "" : location.origin;
3159
+ const hereOrigin = pageOrigin();
3147
3160
  const isActive = (agent) => hubRowActive({
3148
3161
  fusion,
3149
3162
  agentKey: agent.key,
@@ -3254,8 +3267,9 @@ function HubPage() {
3254
3267
  setShowAdd(false);
3255
3268
  setMeshError(null);
3256
3269
  reloadMachines();
3257
- if (out.mutual !== void 0 && !out.mutual.ok) showSuperdToast(`Super D: paired with ${name}, but the mutual half failed (${out.mutual.error}) — add this machine from the peer manually.`);
3258
- else showSuperdToast(`Super D: paired with ${name} — the peer also added this machine (mutual).`, { tone: "success" });
3270
+ if (out.mutual !== void 0 && !out.mutual.ok) showSuperdToast(`Super D: paired with ${name}; reverse connection unavailable.`);
3271
+ else if (out.mutual?.ok && out.mutual.skipped !== "self") showSuperdToast(`Super D: paired with ${name} — the peer also added this machine (mutual).`, { tone: "success" });
3272
+ else showSuperdToast(`Super D: paired with ${name}.`, { tone: "success" });
3259
3273
  }).catch((err) => {
3260
3274
  setMeshError(errText(err));
3261
3275
  }).finally(() => {
@@ -3282,7 +3296,7 @@ function HubPage() {
3282
3296
  * GET), one clipboard write, the outcome on the shared toast. */
3283
3297
  const copyPairingUrl = () => {
3284
3298
  if (pairingBusy) return;
3285
- const origin = typeof location === "undefined" ? "" : location.origin;
3299
+ const origin = pageOrigin();
3286
3300
  if (origin === "") return;
3287
3301
  setPairingBusy(true);
3288
3302
  fetchPairingUrl(origin).then((url) => (0, __deepseek_ai_dsh_client_ui_primitives.writeClipboard)(url)).then((copied) => {
@@ -3463,7 +3477,7 @@ function HubPage() {
3463
3477
  color: "var(--dsw-alias-label-tertiary)"
3464
3478
  },
3465
3479
  children: [
3466
- "Add another DSH from remote machines, provide the token URL. Pairing is mutual — the peer adds this machine back automatically. Status ",
3480
+ "Add another DSH with its token URL. The peer attempts to connect back. Status ",
3467
3481
  AUTO_REFRESH_NOTE,
3468
3482
  "."
3469
3483
  ]