fix(desktop): recognize the LAN-bound local server when tagging client tokens
The client-create gate added in 1.13.9 rejects client tokens without the desktop-local kind, but the kind was only attached when the runtime origin exactly matched the injected local origin — an empty (same-origin) api base, loopback aliases, and the embedded server addressed via a LAN interface (0.0.0.0 binds) all minted untagged tokens, which then hit 403 and surfaced as "Local — Auth required" plus the unreachable-server screen. The renderer now treats same-origin and loopback targets as local, the Electron main additionally matches any of the machine's own interface addresses on the local server's port, and a deduped kind-tagged mint migrates away legacy same-label tokens that predate client kinds. The client-create gate itself is unchanged.
This commit is contained in:
@@ -537,9 +537,37 @@ const readDesktopLocalClientToken = () => {
|
||||
return sanitizeClientTokenForStorage(readSettingsRoot().desktopLocalClientToken) || '';
|
||||
};
|
||||
|
||||
const isMachineLocalHostname = (hostname) => {
|
||||
const clean = String(hostname || '').replace(/^\[|\]$/g, '');
|
||||
if (!clean) return false;
|
||||
if (clean === 'localhost' || clean === '127.0.0.1' || clean === '::1' || clean === '0.0.0.0' || clean === '::') {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return Object.values(os.networkInterfaces()).some((entries) =>
|
||||
(entries || []).some((entry) => entry?.address === clean));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isLocalRuntimeUrl = (targetUrl) => {
|
||||
const localUrl = state.sidecarUrl || state.localOrigin || '';
|
||||
return Boolean(localUrl && sameOrigin(targetUrl, localUrl));
|
||||
if (!localUrl) return false;
|
||||
if (sameOrigin(targetUrl, localUrl)) return true;
|
||||
// The embedded server bound to 0.0.0.0 for LAN access is still THIS
|
||||
// machine's server when addressed via any of its own interfaces on the same
|
||||
// port — the minted client token must carry the desktop-local kind, or the
|
||||
// server's client-create gate rejects it (the "Local — Auth required" +
|
||||
// unreachable-screen regression).
|
||||
try {
|
||||
const target = new URL(targetUrl);
|
||||
const local = new URL(localUrl);
|
||||
const portOf = (url) => url.port || (url.protocol === 'https:' ? '443' : '80');
|
||||
return portOf(target) === portOf(local) && isMachineLocalHostname(target.hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const readDesktopHostsConfig = () => {
|
||||
|
||||
@@ -51,11 +51,30 @@ const shouldIssueDesktopClientToken = (): boolean => {
|
||||
return isDesktopShell();
|
||||
};
|
||||
|
||||
const isLoopbackHostname = (hostname: string): boolean => {
|
||||
const clean = hostname.replace(/^\[|\]$/g, '');
|
||||
return clean === 'localhost' || clean === '127.0.0.1' || clean === '::1';
|
||||
};
|
||||
|
||||
const isLocalDesktopRuntime = (): boolean => {
|
||||
if (!isDesktopShell()) return false;
|
||||
const apiBaseUrl = getRuntimeApiBaseUrl();
|
||||
const localOrigin = readLocalOrigin();
|
||||
return Boolean(localOrigin && sameOrigin(localOrigin, apiBaseUrl));
|
||||
if (!localOrigin) return false;
|
||||
// An empty api base means same-origin requests against the page itself —
|
||||
// which on desktop IS the embedded local server. Requiring an exact origin
|
||||
// match here used to leave local client tokens untagged (no desktop-local
|
||||
// clientKind), and the server's client-create gate then 403'd them.
|
||||
const apiBaseUrl = getRuntimeApiBaseUrl();
|
||||
const effectiveTarget = apiBaseUrl || (typeof window !== 'undefined' ? window.location.origin : '');
|
||||
if (sameOrigin(localOrigin, effectiveTarget)) return true;
|
||||
// Loopback aliases (localhost vs 127.0.0.1) still address this machine's
|
||||
// own server.
|
||||
try {
|
||||
const normalized = normalizeHostUrl(effectiveTarget);
|
||||
return Boolean(normalized && isLoopbackHostname(new URL(normalized).hostname));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const desktopClientAuthMetadata = (): { clientKind?: string; dedupeKey?: string } => {
|
||||
|
||||
@@ -135,6 +135,14 @@ export const createRemoteClientAuthRuntime = ({ fsPromises, path, crypto, storeP
|
||||
};
|
||||
if (normalizedDedupeKey) {
|
||||
store.clients = store.clients.filter((entry) => entry.dedupeKey !== normalizedDedupeKey);
|
||||
// Migrate pre-clientKind desktop tokens: a deduped, kind-tagged mint
|
||||
// supersedes legacy records with the same label that carry neither a
|
||||
// kind nor a dedupe key — those tokens can no longer pass the
|
||||
// desktop-local client-create gate and would otherwise linger forever.
|
||||
if (client.clientKind) {
|
||||
store.clients = store.clients.filter((entry) =>
|
||||
!(entry.label === client.label && !entry.clientKind && !entry.dedupeKey));
|
||||
}
|
||||
}
|
||||
store.clients.push(client);
|
||||
await writeStore(store);
|
||||
|
||||
Reference in New Issue
Block a user