fix(ui): restore pairing connect error formatting

This commit is contained in:
Ayaan Zaidi
2026-04-20 14:15:20 +05:30
parent 042c117342
commit 94e2bf258d
2 changed files with 52 additions and 1 deletions

View File

@@ -26,4 +26,17 @@ describe("formatConnectError", () => {
}),
).toBe("gateway pairing required: device is not approved yet");
});
it("preserves surfaced pending approvals", () => {
expect(
formatConnectError({
message: "scope upgrade pending approval (requestId: req-123)",
details: {
code: "PAIRING_REQUIRED",
reason: "scope-upgrade",
requestId: "req-123",
},
}),
).toBe("scope upgrade pending approval (requestId: req-123)");
});
});

View File

@@ -1,6 +1,9 @@
import {
ConnectErrorDetailCodes,
describePairingConnectRequirement,
formatConnectPairingRequiredMessage,
readConnectPairingRequiredMessage,
readPairingConnectErrorDetails,
} from "../../../src/gateway/protocol/connect-error-details.js";
import { resolveGatewayErrorDetailCode } from "./gateway.ts";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
@@ -20,6 +23,41 @@ function normalizeErrorMessage(message: unknown): string {
return "unknown error";
}
function formatPairingRequiredError(error: ErrorWithMessageAndDetails): string {
const message = normalizeErrorMessage(error.message);
const normalizedMessage = normalizeLowercaseStringOrEmpty(message);
const pairing = readPairingConnectErrorDetails(error.details);
const pairingMessage = readConnectPairingRequiredMessage(message);
const pairingReason = pairing?.reason ?? pairingMessage?.reason;
if (normalizedMessage.startsWith("pairing required:") && pairingReason) {
return `gateway pairing required: ${describePairingConnectRequirement(pairingReason)}`;
}
if (pairingMessage && normalizedMessage !== "pairing required") {
return message;
}
const approvedRoles = pairing?.approvedRoles?.join(", ") ?? "none";
const requestedRole = pairing?.requestedRole ?? "none";
const approvedScopes = pairing?.approvedScopes?.join(", ") ?? "none";
const requestedScopes = pairing?.requestedScopes?.join(", ") ?? "none";
switch (pairing?.reason) {
case "scope-upgrade":
if (pairing.approvedScopes || pairing.requestedScopes) {
return `device scope upgrade requires approval (approved: ${approvedScopes}; requested: ${requestedScopes})`;
}
return formatConnectPairingRequiredMessage(error.details);
case "role-upgrade":
if (pairing.approvedRoles || pairing.requestedRole) {
return `device role upgrade requires approval (approved: ${approvedRoles}; requested: ${requestedRole})`;
}
return formatConnectPairingRequiredMessage(error.details);
case "metadata-upgrade":
return "device reconnect details changed and require approval";
default:
return "gateway pairing required";
}
}
function formatErrorFromMessageAndDetails(error: ErrorWithMessageAndDetails): string {
const message = normalizeErrorMessage(error.message);
const detailCode = resolveGatewayErrorDetailCode(error);
@@ -32,7 +70,7 @@ function formatErrorFromMessageAndDetails(error: ErrorWithMessageAndDetails): st
case ConnectErrorDetailCodes.AUTH_RATE_LIMITED:
return "too many failed authentication attempts";
case ConnectErrorDetailCodes.PAIRING_REQUIRED:
return formatConnectPairingRequiredMessage(error.details);
return formatPairingRequiredError(error);
case ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED:
return "device identity required (use HTTPS/localhost or allow insecure auth explicitly)";
case ConnectErrorDetailCodes.CONTROL_UI_ORIGIN_NOT_ALLOWED: