openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
209 lines (208 loc) • 9.61 kB
JavaScript
import { i as extractErrorCode } from "./error-coercion-1P2One0X.mjs";
import { t as createSubsystemLogger } from "./subsystem-BlrgtxyM.mjs";
import "./security-runtime-9ynw5sNI.mjs";
import "./logging-core-D09a-xJP.mjs";
import { s as resolveProfile } from "./config-HztiUSlV.mjs";
import { a as getOrCreateProfileRuntime, h as withProfileOperationLease, m as waitForProfileOperation, o as getProfileLifecycle, s as isBrowserRuntimeRunning } from "./server-context.lifecycle-Cs0fdql1.mjs";
import { n as registerBorrowedRelayCdpAccess } from "./relay-access-B8A24s4S.mjs";
import { r as startExtensionRelayServer } from "./relay-server-CKH6HaoN.mjs";
import { t as RelayOwnerClient } from "./owner-client-Zyb6dni0.mjs";
//#region extensions/browser/src/browser/extension-relay/relay-lifecycle.ts
/**
* Extension relay lifecycle: one owned listener or authenticated borrowed lease
* per extension-driver profile in the browser control runtime.
*/
const log = createSubsystemLogger("browser").child("extension-relay");
const pendingRelayEnsures = /* @__PURE__ */ new WeakMap();
const EXTENSION_PAIRING_HINT = "Run `openclaw browser extension install`, load the printed unpacked directory once, and wait for automatic setup.";
function applyInternalRelayToken(state, profileName, internalToken) {
const tokens = { ...state.resolved.extensionRelayInternalTokens };
if (internalToken) tokens[profileName] = internalToken;
else delete tokens[profileName];
state.resolved = {
...state.resolved,
extensionRelayInternalTokens: tokens
};
const resolved = resolveProfile(state.resolved, profileName);
const runtime = state.profiles.get(profileName);
if (resolved?.driver === "extension" && runtime?.profile.driver === "extension") Object.assign(runtime.profile, resolved);
return resolved;
}
/**
* Reconcile each profile's relay against its current port, host key, and auth policy.
* Key rotation and config changes retire the old relay before binding its replacement.
*/
async function ensureExtensionRelayForProfile(state, profile, signal) {
for (;;) {
signal?.throwIfAborted();
if (!isBrowserRuntimeRunning(state)) throw new Error("Browser runtime is stopping");
const { ensureExtensionRelayToken } = await import("./relay-auth-BVujdECj.mjs");
const token = await ensureExtensionRelayToken();
if (state.resolved.extensionRelayToken !== token) state.resolved = {
...state.resolved,
extensionRelayToken: token
};
const desiredProfile = resolveProfile(state.resolved, profile.name);
if (profile.driver !== "extension" || desiredProfile?.driver !== "extension" || desiredProfile.cdpPort !== profile.cdpPort) throw new Error(`Extension relay profile "${profile.name}" changed during startup.`);
Object.assign(profile, desiredProfile);
const runtime = getOrCreateProfileRuntime(state, desiredProfile);
if (runtime.profile !== profile && runtime.profile.driver === "extension" && runtime.profile.cdpPort === desiredProfile.cdpPort) Object.assign(runtime.profile, desiredProfile);
const pending = pendingRelayEnsures.get(runtime);
if (pending && (pending.port !== desiredProfile.cdpPort || pending.token !== token || pending.allowLegacyAuth !== state.resolved.extensionRelay.allowLegacyAuth)) {
try {
await waitForProfileOperation(pending.promise, signal);
} catch (err) {
signal?.throwIfAborted();
if (getProfileLifecycle(runtime).blockedReason) throw err;
}
continue;
}
const promise = pending?.promise ?? ensureDesiredRelay({
state,
runtime,
profile: desiredProfile,
token
});
if (!pending) {
const owned = {
port: desiredProfile.cdpPort,
token,
allowLegacyAuth: state.resolved.extensionRelay.allowLegacyAuth,
promise
};
pendingRelayEnsures.set(runtime, owned);
const settlePending = () => {
if (pendingRelayEnsures.get(runtime) === owned) pendingRelayEnsures.delete(runtime);
};
promise.then(settlePending, settlePending);
}
const handle = await waitForProfileOperation(promise, signal);
const current = resolveProfile(state.resolved, profile.name);
if (current) Object.assign(profile, current);
return handle;
}
}
async function ensureDesiredRelay(params) {
const { state, runtime, profile, token } = params;
return await withProfileOperationLease({
state,
runtime,
configRevision: getProfileLifecycle(runtime).configRevision,
ownership: "lifecycle",
run: async (signal) => {
const map = state.extensionRelays ??= /* @__PURE__ */ new Map();
const actor = getProfileLifecycle(runtime);
const existing = map.get(profile.name);
if (existing) {
const matches = existing.port === profile.cdpPort && existing.token === token && existing.allowLegacyAuth === state.resolved.extensionRelay.allowLegacyAuth;
if (matches && existing.ownership === "borrowed" && existing.client.connected) try {
await existing.client.status();
} catch (error) {
if (existing.client.connected) throw error;
}
if (matches && (existing.ownership !== "borrowed" || existing.client.connected)) {
const current = applyInternalRelayToken(state, profile.name, existing.ownership === "borrowed" ? null : existing.internalToken);
if (current) Object.assign(profile, current);
return existing;
}
actor.cleanupRelays.add(existing);
await existing.close();
actor.cleanupRelays.delete(existing);
if (map.get(profile.name) === existing) map.delete(profile.name);
applyInternalRelayToken(state, profile.name, null);
}
let handle;
try {
try {
handle = await startExtensionRelayServer({
port: profile.cdpPort,
profileName: profile.name,
token,
allowLegacyAuth: state.resolved.extensionRelay.allowLegacyAuth
});
} catch (error) {
if (extractErrorCode(error) !== "EADDRINUSE") throw error;
}
if (!handle) {
const client = await RelayOwnerClient.connect({
port: profile.cdpPort,
profile: profile.name,
token,
signal
});
let unregister = () => {};
handle = {
ownership: "borrowed",
port: profile.cdpPort,
token,
allowLegacyAuth: state.resolved.extensionRelay.allowLegacyAuth,
client,
close: async () => {
await client.close();
unregister();
}
};
actor.cleanupRelays.add(handle);
const status = await client.status();
if (!handle.allowLegacyAuth && status.allowLegacyAuth) throw new Error("Existing relay permits legacy authentication; its owner must retire it before this stricter profile can use it.");
const borrowed = handle;
const assertCurrent = () => {
if (map.get(profile.name) !== borrowed || state.profiles.get(profile.name) !== runtime || actor.transitionReason || actor.terminal || actor.cleanupRelays.has(borrowed) || state.resolved.extensionRelayToken !== token || resolveProfile(state.resolved, profile.name)?.cdpPort !== borrowed.port) throw new Error("Borrowed relay profile lease was superseded");
};
client.adoptProfileLease(assertCurrent);
unregister = registerBorrowedRelayCdpAccess(`http://127.0.0.1:${profile.cdpPort}`, borrowed);
}
actor.cleanupRelays.add(handle);
signal.throwIfAborted();
const currentProfile = resolveProfile(state.resolved, profile.name);
if (state.profiles.get(profile.name) !== runtime || currentProfile?.driver !== "extension" || currentProfile.cdpPort !== profile.cdpPort || state.resolved.extensionRelayToken !== token) throw new Error(`Extension relay profile "${profile.name}" changed during startup.`);
map.set(profile.name, handle);
const currentWithInternalAuth = applyInternalRelayToken(state, profile.name, handle.ownership === "borrowed" ? null : handle.internalToken);
if (!currentWithInternalAuth) throw new Error(`Extension relay profile "${profile.name}" disappeared during startup.`);
Object.assign(profile, currentWithInternalAuth);
actor.cleanupRelays.delete(handle);
log.info(`extension relay for profile "${profile.name}" listening on 127.0.0.1:${handle.port}`);
return handle;
} catch (err) {
if (handle) try {
await handle.close();
actor.cleanupRelays.delete(handle);
} catch (closeError) {
actor.blockedReason = "extension relay cleanup failed";
throw closeError;
}
throw err;
}
}
});
}
/** Start relays for every extension-driver profile (control service startup). */
async function startConfiguredExtensionRelays(state, resolveProfileByName, onWarn) {
for (const [name, profile] of Object.entries(state.resolved.profiles)) {
if (profile.driver !== "extension") continue;
const resolved = resolveProfileByName(name);
if (!resolved) continue;
try {
await ensureExtensionRelayForProfile(state, resolved);
} catch (err) {
onWarn(`extension relay for profile "${name}" failed to start: ${String(err)}`);
}
}
}
/** Stop every running relay (runtime shutdown). */
async function stopExtensionRelays(state) {
const map = state.extensionRelays;
if (!map) return;
let firstError;
for (const [name, handle] of map) try {
await handle.close();
if (map.get(name) === handle) map.delete(name);
applyInternalRelayToken(state, name, null);
} catch (err) {
log.warn(`extension relay for profile "${name}" failed to stop: ${String(err)}`);
firstError ??= err instanceof Error ? err : new Error("Extension relay cleanup failed.", { cause: err });
}
if (firstError) throw firstError;
}
//#endregion
export { stopExtensionRelays as i, ensureExtensionRelayForProfile as n, startConfiguredExtensionRelays as r, EXTENSION_PAIRING_HINT as t };