fix(pairing): align mobile setup with secure endpoints

This commit is contained in:
Ayaan Zaidi
2026-04-03 12:55:11 +05:30
parent c6f95a0c37
commit acd5734aa9
18 changed files with 412 additions and 73 deletions

View File

@@ -7,6 +7,7 @@ import {
resolveSecretInputRef,
} from "../config/types.secrets.js";
import { assertExplicitGatewayAuthModeWhenBothConfigured } from "../gateway/auth-mode-policy.js";
import { isLoopbackHost, isSecureWebSocketUrl } from "../gateway/net.js";
import { resolveRequiredConfiguredSecretRefInputString } from "../gateway/resolve-configured-secret-input-string.js";
import { issueDeviceBootstrapToken } from "../infra/device-bootstrap.js";
import {
@@ -62,6 +63,34 @@ type ResolveUrlResult = {
error?: string;
};
function describeSecureMobilePairingFix(source?: string): string {
const sourceNote = source ? ` Resolved source: ${source}.` : "";
return (
"Mobile pairing requires a secure remote gateway URL (wss://) or Tailscale Serve/Funnel." +
sourceNote +
" Fix: prefer gateway.tailscale.mode=serve, or set gateway.remote.url / " +
"plugins.entries.device-pair.config.publicUrl to a wss:// URL. ws:// is only valid for localhost."
);
}
function validateMobilePairingUrl(url: string, source?: string): string | null {
if (isSecureWebSocketUrl(url)) {
return null;
}
let parsed: URL;
try {
parsed = new URL(url);
} catch {
return "Resolved mobile pairing URL is invalid.";
}
const protocol =
parsed.protocol === "https:" ? "wss:" : parsed.protocol === "http:" ? "ws:" : parsed.protocol;
if (protocol !== "ws:" || isLoopbackHost(parsed.hostname)) {
return null;
}
return describeSecureMobilePairingFix(source);
}
type ResolveAuthLabelResult = {
label?: "token" | "password";
error?: string;
@@ -373,6 +402,10 @@ export async function resolvePairingSetupFromConfig(
if (!urlResult.url) {
return { ok: false, error: urlResult.error ?? "Gateway URL unavailable." };
}
const mobilePairingUrlError = validateMobilePairingUrl(urlResult.url, urlResult.source);
if (mobilePairingUrlError) {
return { ok: false, error: mobilePairingUrlError };
}
if (!authLabel.label) {
return { ok: false, error: "Gateway auth is not configured (no token or password)." };