Files
openclaw/ui/src/app/device-auth-migration-loader.ts
Jason (Json) 7eec1345f9 fix(gateway): preserve Control UI access across device-auth upgrades (#112558)
* fix(gateway): preserve device auth upgrade recovery

* fix(gateway): satisfy device auth upgrade gates

* fix(doctor): clean disabled device auth bypass

* fix(gateway): recheck migration operator boundary

* fix(gateway): keep migration guard internal

* fix(gateway): preserve insecure migration access

* fix(gateway): reject stale migration handshakes

* fix(gateway): revoke legacy migration sessions

* fix(gateway): bound device-less migration authority

* fix(gateway): require explicit migration pairing

* fix(gateway): revoke alternate migration sessions

* fix(gateway): close migration admission races

* style(gateway): format migration pairing import

* fix(security): audit pending device-auth migration

* fix(gateway): reconcile migration on startup

* fix(gateway): cap device auth migration scopes

* fix(gateway): retain migration socket restrictions

* perf(ui): trim migration startup bundle

* perf(ui): lazy-load device auth migration

* fix(gateway): bind migration completion to approved key

* fix(gateway): preserve migration authorization bounds

* fix(gateway): grant migrated device pairing capability

* fix(ui): preserve device migration bundle budget

* fix(ui): stabilize migration startup budget

* chore(ui): retain startup budget headroom

* fix(ui): split migration overlay helpers
2026-07-22 18:44:48 -06:00

75 lines
2.4 KiB
TypeScript

import type { GatewayBrowserClient } from "../api/gateway.ts";
import { t } from "../i18n/index.ts";
import type {
DeviceAuthMigrationController,
DeviceAuthMigrationSnapshot,
} from "./device-auth-migration.ts";
import type { ApplicationGateway } from "./gateway.ts";
export const EMPTY_DEVICE_AUTH_MIGRATION: DeviceAuthMigrationSnapshot = {
requestId: null,
busy: false,
error: null,
};
export function createDeviceAuthMigrationLoader(params: {
gateway: ApplicationGateway;
isCurrent: (client: GatewayBrowserClient, epoch: number) => boolean;
onChange: (snapshot: DeviceAuthMigrationSnapshot) => void;
}) {
let controllerPromise: Promise<DeviceAuthMigrationController | null> | null = null;
let disposed = false;
const reset = () => {
void controllerPromise?.then((controller) => controller?.reset());
params.onChange(EMPTY_DEVICE_AUTH_MIGRATION);
};
const load = (client: GatewayBrowserClient, epoch: number) => {
// This is a rare upgrade-only flow. Load it once when hello reports the
// pending transition so ordinary Control UI startup does not pay its cost.
controllerPromise ??= import("./device-auth-migration.ts")
.then(({ createDeviceAuthMigrationController }) => {
if (disposed) {
return null;
}
return createDeviceAuthMigrationController(params);
})
.catch((error: unknown) => {
controllerPromise = null;
if (params.isCurrent(client, epoch)) {
params.onChange({
...EMPTY_DEVICE_AUTH_MIGRATION,
error: t("login.deviceAuthMigration.loadFailed", {
error: error instanceof Error ? error.message : String(error),
}),
});
}
return null;
});
return controllerPromise;
};
return {
reset,
async refresh(client: GatewayBrowserClient, epoch: number) {
if (params.gateway.snapshot.hello?.deviceAuthMigration?.pending !== true) {
reset();
return;
}
const controller = await load(client, epoch);
if (!controller || !params.isCurrent(client, epoch)) {
controller?.reset();
return;
}
await controller.refresh(client, epoch);
},
async secure(client: GatewayBrowserClient | null, epoch: number) {
await (await controllerPromise)?.secure(client, epoch);
},
dispose() {
disposed = true;
void controllerPromise?.then((controller) => controller?.dispose());
},
};
}