mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
1e3192c0b4
* refactor(ui): model gateway connection state as one closed phase union ApplicationGatewaySnapshot carried connected + reconnecting as parallel booleans whose illegal combination (both true) was representable, and every consumer re-derived the same flag logic. The snapshot now carries phase: "stopped" | "connecting" | "connected" | "reconnecting" | "offline"; offlineStable stays as the store-owned 2s debounced presentation boolean, and lastError/lastErrorCode are unchanged. Mapping preserves shipped behavior exactly: never-connected terminal closes return to "stopped" (login gate), established-connection drops with retry are "reconnecting" (shell stays mounted), without retry "offline". Component props stay boolean, derived once at the app-host boundary; no compat aliases remain on the snapshot. Closes #112741. * fix(ui): migrate rebased-in test fixtures to the gateway phase union * fix(ui): migrate post-rebase gateway snapshot readers to the phase union * fix(ui): migrate document-title test harness snapshot to the phase union
122 lines
3.9 KiB
TypeScript
122 lines
3.9 KiB
TypeScript
import type { GatewayBrowserClient } from "../api/gateway.ts";
|
|
import { t } from "../i18n/index.ts";
|
|
import { peekStoredDeviceIdentityId } from "../lib/nodes/index.ts";
|
|
import type { ApplicationGateway } from "./gateway.ts";
|
|
import "../components/device-auth-migration-banner.ts";
|
|
|
|
export type DeviceAuthMigrationSnapshot = {
|
|
requestId: string | null;
|
|
busy: boolean;
|
|
error: string | null;
|
|
};
|
|
|
|
export type DeviceAuthMigrationController = ReturnType<typeof createDeviceAuthMigrationController>;
|
|
|
|
const EMPTY_SNAPSHOT: DeviceAuthMigrationSnapshot = {
|
|
requestId: null,
|
|
busy: false,
|
|
error: null,
|
|
};
|
|
|
|
export function createDeviceAuthMigrationController(params: {
|
|
gateway: ApplicationGateway;
|
|
isCurrent: (client: GatewayBrowserClient, epoch: number) => boolean;
|
|
onChange: (snapshot: DeviceAuthMigrationSnapshot) => void;
|
|
}) {
|
|
let snapshot = EMPTY_SNAPSHOT;
|
|
let generation = 0;
|
|
let disposed = false;
|
|
|
|
const update = (patch: Partial<DeviceAuthMigrationSnapshot>) => {
|
|
snapshot = { ...snapshot, ...patch };
|
|
params.onChange(snapshot);
|
|
};
|
|
|
|
return {
|
|
reset() {
|
|
generation += 1;
|
|
update(EMPTY_SNAPSHOT);
|
|
},
|
|
async refresh(client: GatewayBrowserClient, epoch: number) {
|
|
const migrationPending = params.gateway.snapshot.hello?.deviceAuthMigration?.pending === true;
|
|
const deviceId = peekStoredDeviceIdentityId();
|
|
if (!migrationPending || !params.isCurrent(client, epoch)) {
|
|
generation += 1;
|
|
update(EMPTY_SNAPSHOT);
|
|
return;
|
|
}
|
|
if (!deviceId) {
|
|
generation += 1;
|
|
update({
|
|
...EMPTY_SNAPSHOT,
|
|
error: t("login.deviceAuthMigration.secureContextRequired"),
|
|
});
|
|
return;
|
|
}
|
|
const refreshGeneration = ++generation;
|
|
try {
|
|
const result = await client.request<{
|
|
pending?: Array<{ requestId?: unknown; deviceId?: unknown }>;
|
|
}>("device.pair.list", {});
|
|
if (disposed || refreshGeneration !== generation || !params.isCurrent(client, epoch)) {
|
|
return;
|
|
}
|
|
const ownRequest = result.pending?.find(
|
|
(entry) => entry.deviceId === deviceId && typeof entry.requestId === "string",
|
|
);
|
|
update({
|
|
requestId: typeof ownRequest?.requestId === "string" ? ownRequest.requestId : null,
|
|
error: ownRequest ? null : t("login.deviceAuthMigration.pendingUnavailable"),
|
|
});
|
|
} catch (error) {
|
|
if (refreshGeneration === generation && params.isCurrent(client, epoch)) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
update({
|
|
error: t("login.deviceAuthMigration.loadFailed", {
|
|
error: message,
|
|
}),
|
|
});
|
|
}
|
|
}
|
|
},
|
|
async secure(client: GatewayBrowserClient | null, epoch: number) {
|
|
const requestId = snapshot.requestId;
|
|
if (
|
|
!client ||
|
|
!requestId ||
|
|
params.gateway.snapshot.phase !== "connected" ||
|
|
!params.isCurrent(client, epoch) ||
|
|
snapshot.busy ||
|
|
disposed
|
|
) {
|
|
return;
|
|
}
|
|
update({ busy: true, error: null });
|
|
try {
|
|
await client.request("device.pair.approve", { requestId });
|
|
if (disposed || !params.isCurrent(client, epoch)) {
|
|
return;
|
|
}
|
|
update({ requestId: null, busy: false });
|
|
// Reconnect once so the newly approved browser receives and stores its
|
|
// device token; shared auth is no longer its baseline.
|
|
params.gateway.connect();
|
|
} catch (error) {
|
|
if (!disposed && params.isCurrent(client, epoch)) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
update({
|
|
busy: false,
|
|
error: t("login.deviceAuthMigration.approvalFailed", {
|
|
error: message,
|
|
}),
|
|
});
|
|
}
|
|
}
|
|
},
|
|
dispose() {
|
|
disposed = true;
|
|
generation += 1;
|
|
},
|
|
};
|
|
}
|