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 +25 -11
- package/package.json +2 -2
- package/superd-hub/dist/client/mesh-api.d.ts +6 -3
- package/superd-hub/dist/client/mesh-api.js +20 -5
- package/superd-hub/dist/fusion-transport.js +14 -3
- package/superd-hub/dist/gateway.js +2 -0
- package/superd-hub/dist/machines.d.ts +2 -0
- package/superd-hub/dist/machines.js +2 -0
- package/superd-hub/dist/mesh-link.d.ts +10 -2
- package/superd-hub/dist/mesh-link.js +131 -27
- package/superd-hub/dist/mesh-rpc.d.ts +1 -1
- package/superd-hub/dist/mesh-rpc.js +20 -12
- package/superd-hub/dist/pairing-auth.d.ts +7 -0
- package/superd-hub/dist/pairing-auth.js +29 -0
- package/superd-hub/lib/client/index.js +25 -11
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-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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 =
|
|
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}
|
|
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 =
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
315
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
123
|
-
return { ok: false, error:
|
|
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.
|
|
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
|
|
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-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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 =
|
|
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}
|
|
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 =
|
|
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
|
|
3480
|
+
"Add another DSH with its token URL. The peer attempts to connect back. Status ",
|
|
3467
3481
|
AUTO_REFRESH_NOTE,
|
|
3468
3482
|
"."
|
|
3469
3483
|
]
|