Skip to content

Commit 756379b

Browse files
committed
refactor: centralize config mutations
1 parent c4c0b65 commit 756379b

13 files changed

Lines changed: 593 additions & 403 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { mutateConfigFile } from "../config/config.js";
2+
import type { BrowserProfileConfig } from "../config/config.js";
3+
import { deriveDefaultBrowserCdpPortRange } from "../config/port-defaults.js";
4+
import { formatErrorMessage } from "../infra/errors.js";
5+
import { assertCdpEndpointAllowed } from "./cdp.helpers.js";
6+
import { resolveBrowserConfig, type ResolvedBrowserConfig } from "./config.js";
7+
import {
8+
BrowserConflictError,
9+
BrowserResourceExhaustedError,
10+
BrowserValidationError,
11+
} from "./errors.js";
12+
import { allocateCdpPort, allocateColor, getUsedColors, getUsedPorts } from "./profiles.js";
13+
14+
type BrowserControlCredential =
15+
| {
16+
kind: "token";
17+
value: string;
18+
}
19+
| {
20+
kind: "password";
21+
value: string;
22+
};
23+
24+
const cdpPortRange = (resolved: {
25+
controlPort: number;
26+
cdpPortRangeStart?: number;
27+
cdpPortRangeEnd?: number;
28+
}): { start: number; end: number } => {
29+
const start = resolved.cdpPortRangeStart;
30+
const end = resolved.cdpPortRangeEnd;
31+
if (
32+
typeof start === "number" &&
33+
Number.isFinite(start) &&
34+
Number.isInteger(start) &&
35+
typeof end === "number" &&
36+
Number.isFinite(end) &&
37+
Number.isInteger(end) &&
38+
start > 0 &&
39+
end >= start &&
40+
end <= 65535
41+
) {
42+
return { start, end };
43+
}
44+
45+
return deriveDefaultBrowserCdpPortRange(resolved.controlPort);
46+
};
47+
48+
export async function persistBrowserControlCredential(
49+
credential: BrowserControlCredential,
50+
): Promise<void> {
51+
await mutateConfigFile({
52+
afterWrite: { mode: "auto" },
53+
mutate: (draft) => {
54+
draft.gateway = {
55+
...draft.gateway,
56+
auth: {
57+
...draft.gateway?.auth,
58+
[credential.kind]: credential.value,
59+
},
60+
};
61+
},
62+
});
63+
}
64+
65+
export async function createBrowserProfileConfig(params: {
66+
name: string;
67+
resolved: ResolvedBrowserConfig;
68+
color?: string;
69+
parsedCdpUrl?: string;
70+
userDataDir?: string;
71+
driver?: "openclaw" | "existing-session";
72+
}): Promise<BrowserProfileConfig | undefined> {
73+
const mutation = await mutateConfigFile<BrowserProfileConfig>({
74+
afterWrite: { mode: "auto" },
75+
mutate: async (draft) => {
76+
const latestResolved = resolveBrowserConfig({
77+
...params.resolved,
78+
...draft.browser,
79+
profiles: draft.browser?.profiles ?? params.resolved.profiles,
80+
});
81+
const latestProfiles = draft.browser?.profiles ?? {};
82+
if (params.name in latestProfiles || params.name in latestResolved.profiles) {
83+
throw new BrowserConflictError(`profile "${params.name}" already exists`);
84+
}
85+
86+
const profileColor = params.color ?? allocateColor(getUsedColors(latestResolved.profiles));
87+
88+
let nextProfileConfig: BrowserProfileConfig;
89+
if (params.parsedCdpUrl) {
90+
try {
91+
await assertCdpEndpointAllowed(params.parsedCdpUrl, latestResolved.ssrfPolicy);
92+
} catch (err) {
93+
throw new BrowserValidationError(formatErrorMessage(err));
94+
}
95+
nextProfileConfig = {
96+
cdpUrl: params.parsedCdpUrl,
97+
...(params.driver ? { driver: params.driver } : {}),
98+
color: profileColor,
99+
};
100+
} else if (params.driver === "existing-session") {
101+
nextProfileConfig = {
102+
driver: params.driver,
103+
attachOnly: true,
104+
...(params.userDataDir ? { userDataDir: params.userDataDir } : {}),
105+
color: profileColor,
106+
};
107+
} else {
108+
const usedPorts = getUsedPorts(latestResolved.profiles);
109+
const rangeStart = draft.browser?.cdpPortRangeStart ?? params.resolved.cdpPortRangeStart;
110+
const range = cdpPortRange({
111+
controlPort: params.resolved.controlPort,
112+
cdpPortRangeStart: rangeStart,
113+
cdpPortRangeEnd:
114+
draft.browser?.cdpPortRangeStart === undefined
115+
? params.resolved.cdpPortRangeEnd
116+
: latestResolved.cdpPortRangeEnd,
117+
});
118+
const cdpPort = allocateCdpPort(usedPorts, range);
119+
if (cdpPort === null) {
120+
throw new BrowserResourceExhaustedError("no available CDP ports in range");
121+
}
122+
nextProfileConfig = {
123+
cdpPort,
124+
...(params.driver ? { driver: params.driver } : {}),
125+
color: profileColor,
126+
};
127+
}
128+
129+
draft.browser = {
130+
...draft.browser,
131+
profiles: {
132+
...draft.browser?.profiles,
133+
[params.name]: nextProfileConfig,
134+
},
135+
};
136+
return nextProfileConfig;
137+
},
138+
});
139+
return mutation.result;
140+
}
141+
142+
export async function deleteBrowserProfileConfig(name: string): Promise<void> {
143+
await mutateConfigFile({
144+
afterWrite: { mode: "auto" },
145+
mutate: (draft) => {
146+
const { [name]: _removed, ...remainingProfiles } = draft.browser?.profiles ?? {};
147+
const nextBrowser = {
148+
...draft.browser,
149+
profiles: remainingProfiles,
150+
};
151+
if (nextBrowser.defaultProfile === name) {
152+
delete nextBrowser.defaultProfile;
153+
}
154+
draft.browser = nextBrowser;
155+
},
156+
});
157+
}

extensions/browser/src/browser/control-auth.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import {
33
normalizeLowercaseStringOrEmpty,
44
normalizeOptionalString,
55
} from "openclaw/plugin-sdk/string-coerce-runtime";
6-
import { getRuntimeConfig, mutateConfigFile } from "../config/config.js";
6+
import { getRuntimeConfig } from "../config/config.js";
77
import type { OpenClawConfig } from "../config/config.js";
88
import { resolveGatewayAuth } from "../gateway/auth.js";
99
import { ensureGatewayStartupAuth } from "../gateway/startup-auth.js";
10+
import { persistBrowserControlCredential } from "./config-mutations.js";
1011

1112
export type BrowserControlAuth = {
1213
token?: string;
@@ -77,18 +78,7 @@ async function generateAndPersistBrowserControlToken(params: {
7778
generatedToken?: string;
7879
}> {
7980
const token = generateBrowserControlToken();
80-
await mutateConfigFile({
81-
afterWrite: { mode: "auto" },
82-
mutate: (draft) => {
83-
draft.gateway = {
84-
...draft.gateway,
85-
auth: {
86-
...draft.gateway?.auth,
87-
token,
88-
},
89-
};
90-
},
91-
});
81+
await persistBrowserControlCredential({ kind: "token", value: token });
9282

9383
// Re-read to stay consistent with any concurrent config writer.
9484
const persistedAuth = resolveBrowserControlAuth(getRuntimeConfig(), params.env);
@@ -110,18 +100,7 @@ async function generateAndPersistBrowserControlPassword(params: {
110100
generatedToken?: string;
111101
}> {
112102
const password = generateBrowserControlToken();
113-
await mutateConfigFile({
114-
afterWrite: { mode: "auto" },
115-
mutate: (draft) => {
116-
draft.gateway = {
117-
...draft.gateway,
118-
auth: {
119-
...draft.gateway?.auth,
120-
password,
121-
},
122-
};
123-
},
124-
});
103+
await persistBrowserControlCredential({ kind: "password", value: password });
125104

126105
// Re-read to stay consistent with any concurrent config writer.
127106
const persistedAuth = resolveBrowserControlAuth(getRuntimeConfig(), params.env);

extensions/browser/src/browser/profiles-service.ts

Lines changed: 12 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,20 @@
11
import fs from "node:fs";
22
import path from "node:path";
33
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
4-
import type { BrowserProfileConfig } from "../config/config.js";
5-
import { getRuntimeConfig, mutateConfigFile } from "../config/config.js";
6-
import { deriveDefaultBrowserCdpPortRange } from "../config/port-defaults.js";
4+
import { getRuntimeConfig } from "../config/config.js";
75
import { formatErrorMessage } from "../infra/errors.js";
86
import { resolveUserPath } from "../utils.js";
97
import { assertCdpEndpointAllowed } from "./cdp.helpers.js";
108
import { resolveOpenClawUserDataDir } from "./chrome.js";
11-
import { parseHttpUrl, resolveBrowserConfig, resolveProfile } from "./config.js";
9+
import { createBrowserProfileConfig, deleteBrowserProfileConfig } from "./config-mutations.js";
10+
import { parseHttpUrl, resolveProfile } from "./config.js";
1211
import {
1312
BrowserConflictError,
1413
BrowserProfileNotFoundError,
15-
BrowserResourceExhaustedError,
1614
BrowserValidationError,
1715
} from "./errors.js";
1816
import { getBrowserProfileCapabilities } from "./profile-capabilities.js";
19-
import {
20-
allocateCdpPort,
21-
allocateColor,
22-
getUsedColors,
23-
getUsedPorts,
24-
isValidProfileName,
25-
} from "./profiles.js";
17+
import { isValidProfileName } from "./profiles.js";
2618
import type { BrowserRouteContext, ProfileStatus } from "./server-context.js";
2719
import { movePathToTrash } from "./trash.js";
2820

@@ -53,30 +45,6 @@ export type DeleteProfileResult = {
5345

5446
const HEX_COLOR_RE = /^#[0-9A-Fa-f]{6}$/;
5547

56-
const cdpPortRange = (resolved: {
57-
controlPort: number;
58-
cdpPortRangeStart?: number;
59-
cdpPortRangeEnd?: number;
60-
}): { start: number; end: number } => {
61-
const start = resolved.cdpPortRangeStart;
62-
const end = resolved.cdpPortRangeEnd;
63-
if (
64-
typeof start === "number" &&
65-
Number.isFinite(start) &&
66-
Number.isInteger(start) &&
67-
typeof end === "number" &&
68-
Number.isFinite(end) &&
69-
Number.isInteger(end) &&
70-
start > 0 &&
71-
end >= start &&
72-
end <= 65535
73-
) {
74-
return { start, end };
75-
}
76-
77-
return deriveDefaultBrowserCdpPortRange(resolved.controlPort);
78-
};
79-
8048
export function createBrowserProfilesService(ctx: BrowserRouteContext) {
8149
const listProfiles = async (): Promise<ProfileStatus[]> => {
8250
return await ctx.listProfiles();
@@ -138,76 +106,14 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) {
138106
parsedCdpUrl = parsed.normalized;
139107
}
140108

141-
const mutation = await mutateConfigFile<BrowserProfileConfig>({
142-
afterWrite: { mode: "auto" },
143-
mutate: async (draft) => {
144-
const latestResolved = resolveBrowserConfig({
145-
...state.resolved,
146-
...draft.browser,
147-
profiles: draft.browser?.profiles ?? state.resolved.profiles,
148-
});
149-
const latestProfiles = draft.browser?.profiles ?? {};
150-
if (name in latestProfiles || name in latestResolved.profiles) {
151-
throw new BrowserConflictError(`profile "${name}" already exists`);
152-
}
153-
154-
const profileColor =
155-
explicitProfileColor ?? allocateColor(getUsedColors(latestResolved.profiles));
156-
157-
let nextProfileConfig: BrowserProfileConfig;
158-
if (parsedCdpUrl) {
159-
try {
160-
await assertCdpEndpointAllowed(parsedCdpUrl, latestResolved.ssrfPolicy);
161-
} catch (err) {
162-
throw new BrowserValidationError(formatErrorMessage(err));
163-
}
164-
nextProfileConfig = {
165-
cdpUrl: parsedCdpUrl,
166-
...(driver ? { driver } : {}),
167-
color: profileColor,
168-
};
169-
} else if (driver === "existing-session") {
170-
// existing-session uses Chrome MCP auto-connect; no CDP port needed.
171-
nextProfileConfig = {
172-
driver,
173-
attachOnly: true,
174-
...(normalizedUserDataDir ? { userDataDir: normalizedUserDataDir } : {}),
175-
color: profileColor,
176-
};
177-
} else {
178-
const usedPorts = getUsedPorts(latestResolved.profiles);
179-
const rangeStart = draft.browser?.cdpPortRangeStart ?? state.resolved.cdpPortRangeStart;
180-
const range = cdpPortRange({
181-
controlPort: state.resolved.controlPort,
182-
cdpPortRangeStart: rangeStart,
183-
cdpPortRangeEnd:
184-
draft.browser?.cdpPortRangeStart === undefined
185-
? state.resolved.cdpPortRangeEnd
186-
: latestResolved.cdpPortRangeEnd,
187-
});
188-
const cdpPort = allocateCdpPort(usedPorts, range);
189-
if (cdpPort === null) {
190-
throw new BrowserResourceExhaustedError("no available CDP ports in range");
191-
}
192-
nextProfileConfig = {
193-
cdpPort,
194-
...(driver ? { driver } : {}),
195-
color: profileColor,
196-
};
197-
}
198-
199-
draft.browser = {
200-
...draft.browser,
201-
profiles: {
202-
...draft.browser?.profiles,
203-
[name]: nextProfileConfig,
204-
},
205-
};
206-
return nextProfileConfig;
207-
},
109+
const profileConfig = await createBrowserProfileConfig({
110+
name,
111+
resolved: state.resolved,
112+
...(explicitProfileColor ? { color: explicitProfileColor } : {}),
113+
...(parsedCdpUrl ? { parsedCdpUrl } : {}),
114+
...(normalizedUserDataDir ? { userDataDir: normalizedUserDataDir } : {}),
115+
...(driver ? { driver } : {}),
208116
});
209-
210-
const profileConfig = mutation.result;
211117
if (!profileConfig) {
212118
throw new BrowserProfileNotFoundError(`profile "${name}" not found after creation`);
213119
}
@@ -270,22 +176,7 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) {
270176
}
271177
}
272178

273-
await mutateConfigFile({
274-
afterWrite: { mode: "auto" },
275-
mutate: (draft) => {
276-
const { [name]: _removed, ...remainingProfiles } = draft.browser?.profiles ?? {};
277-
const nextBrowser = {
278-
...draft.browser,
279-
profiles: remainingProfiles,
280-
};
281-
if (nextBrowser.defaultProfile === name) {
282-
delete nextBrowser.defaultProfile;
283-
}
284-
draft.browser = {
285-
...nextBrowser,
286-
};
287-
},
288-
});
179+
await deleteBrowserProfileConfig(name);
289180

290181
delete state.resolved.profiles[name];
291182
state.profiles.delete(name);

0 commit comments

Comments
 (0)