Add Brave CDP automation, replace Oracle browser mode

Connects to user's running Brave via Chrome DevTools Protocol
to automate ChatGPT interaction. Uses puppeteer-core to open a
tab, send the prompt, wait for response, and extract the result.

No cookies, no separate profiles, no copy/paste. Just connects
to the browser where the user is already logged in.

One-time setup: relaunch Brave with --remote-debugging-port=9222

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-07 16:16:41 -05:00
parent d776a266a8
commit e7882b917b
4163 changed files with 782828 additions and 148 deletions

View File

@@ -0,0 +1,37 @@
/**
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { EmptyResult, Emulation, UAClientHints } from '../../../protocol/protocol.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { UserContextStorage } from '../browser/UserContextStorage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
export declare class EmulationProcessor {
#private;
constructor(browsingContextStorage: BrowsingContextStorage, userContextStorage: UserContextStorage, contextConfigStorage: ContextConfigStorage);
setGeolocationOverride(params: Emulation.SetGeolocationOverrideParameters): Promise<EmptyResult>;
setLocaleOverride(params: Emulation.SetLocaleOverrideParameters): Promise<EmptyResult>;
setScriptingEnabled(params: Emulation.SetScriptingEnabledParameters): Promise<EmptyResult>;
setScreenOrientationOverride(params: Emulation.SetScreenOrientationOverrideParameters): Promise<EmptyResult>;
setScreenSettingsOverride(params: Emulation.SetScreenSettingsOverrideParameters): Promise<EmptyResult>;
setTimezoneOverride(params: Emulation.SetTimezoneOverrideParameters): Promise<EmptyResult>;
setTouchOverride(params: Emulation.SetTouchOverrideParameters): Promise<EmptyResult>;
setUserAgentOverrideParams(params: Emulation.SetUserAgentOverrideParameters): Promise<EmptyResult>;
setClientHintsOverride(params: UAClientHints.Emulation.SetClientHintsOverrideParameters): Promise<EmptyResult>;
setNetworkConditions(params: Emulation.SetNetworkConditionsParameters): Promise<EmptyResult>;
}
export declare function isValidLocale(locale: string): boolean;
export declare function isValidTimezone(timezone: string): boolean;
export declare function isTimeZoneOffsetString(timezone: string): boolean;

View File

@@ -0,0 +1,384 @@
"use strict";
/**
* Copyright 2025 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.EmulationProcessor = void 0;
exports.isValidLocale = isValidLocale;
exports.isValidTimezone = isValidTimezone;
exports.isTimeZoneOffsetString = isTimeZoneOffsetString;
const protocol_js_1 = require("../../../protocol/protocol.js");
class EmulationProcessor {
#userContextStorage;
#browsingContextStorage;
#contextConfigStorage;
constructor(browsingContextStorage, userContextStorage, contextConfigStorage) {
this.#userContextStorage = userContextStorage;
this.#browsingContextStorage = browsingContextStorage;
this.#contextConfigStorage = contextConfigStorage;
}
async setGeolocationOverride(params) {
if ('coordinates' in params && 'error' in params) {
// Unreachable. Handled by params parser.
throw new protocol_js_1.InvalidArgumentException('Coordinates and error cannot be set at the same time');
}
let geolocation = null;
if ('coordinates' in params) {
if ((params.coordinates?.altitude ?? null) === null &&
(params.coordinates?.altitudeAccuracy ?? null) !== null) {
throw new protocol_js_1.InvalidArgumentException('Geolocation altitudeAccuracy can be set only with altitude');
}
geolocation = params.coordinates;
}
else if ('error' in params) {
if (params.error.type !== 'positionUnavailable') {
// Unreachable. Handled by params parser.
throw new protocol_js_1.InvalidArgumentException(`Unknown geolocation error ${params.error.type}`);
}
geolocation = params.error;
}
else {
// Unreachable. Handled by params parser.
throw new protocol_js_1.InvalidArgumentException(`Coordinates or error should be set`);
}
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
geolocation,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
geolocation,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setGeolocationOverride(config.geolocation ?? null);
}));
return {};
}
async setLocaleOverride(params) {
const locale = params.locale ?? null;
if (locale !== null && !isValidLocale(locale)) {
throw new protocol_js_1.InvalidArgumentException(`Invalid locale "${locale}"`);
}
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
locale,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
locale,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await Promise.all([
context.setLocaleOverride(config.locale ?? null),
// Set `AcceptLanguage` to locale.
context.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints),
]);
}));
return {};
}
async setScriptingEnabled(params) {
const scriptingEnabled = params.enabled;
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
scriptingEnabled,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
scriptingEnabled,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setScriptingEnabled(config.scriptingEnabled ?? null);
}));
return {};
}
async setScreenOrientationOverride(params) {
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
screenOrientation: params.screenOrientation,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
screenOrientation: params.screenOrientation,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setViewport(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null);
}));
return {};
}
async setScreenSettingsOverride(params) {
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
screenArea: params.screenArea,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
screenArea: params.screenArea,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setViewport(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null);
}));
return {};
}
/**
* Returns a list of top-level browsing contexts.
*/
async #getRelatedTopLevelBrowsingContexts(browsingContextIds, userContextIds, allowGlobal = false) {
if (browsingContextIds === undefined && userContextIds === undefined) {
if (allowGlobal) {
return this.#browsingContextStorage.getTopLevelContexts();
}
throw new protocol_js_1.InvalidArgumentException('Either user contexts or browsing contexts must be provided');
}
if (browsingContextIds !== undefined && userContextIds !== undefined) {
throw new protocol_js_1.InvalidArgumentException('User contexts and browsing contexts are mutually exclusive');
}
const result = [];
if (browsingContextIds === undefined) {
// userContextIds !== undefined
if (userContextIds.length === 0) {
throw new protocol_js_1.InvalidArgumentException('user context should be provided');
}
// Verify that all user contexts exist.
await this.#userContextStorage.verifyUserContextIdList(userContextIds);
for (const userContextId of userContextIds) {
const topLevelBrowsingContexts = this.#browsingContextStorage
.getTopLevelContexts()
.filter((browsingContext) => browsingContext.userContext === userContextId);
result.push(...topLevelBrowsingContexts);
}
}
else {
if (browsingContextIds.length === 0) {
throw new protocol_js_1.InvalidArgumentException('browsing context should be provided');
}
for (const browsingContextId of browsingContextIds) {
const browsingContext = this.#browsingContextStorage.getContext(browsingContextId);
if (!browsingContext.isTopLevelContext()) {
throw new protocol_js_1.InvalidArgumentException('The command is only supported on the top-level context');
}
result.push(browsingContext);
}
}
// Remove duplicates. Compare `BrowsingContextImpl` by reference is correct here, as
// `browsingContextStorage` returns the same instance for the same id.
return [...new Set(result).values()];
}
async setTimezoneOverride(params) {
let timezone = params.timezone ?? null;
if (timezone !== null && !isValidTimezone(timezone)) {
throw new protocol_js_1.InvalidArgumentException(`Invalid timezone "${timezone}"`);
}
if (timezone !== null && isTimeZoneOffsetString(timezone)) {
// CDP supports offset timezone with `GMT` prefix.
timezone = `GMT${timezone}`;
}
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
timezone,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
timezone,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setTimezoneOverride(config.timezone ?? null);
}));
return {};
}
async setTouchOverride(params) {
const maxTouchPoints = params.maxTouchPoints;
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
maxTouchPoints,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
maxTouchPoints,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
maxTouchPoints,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setTouchOverride(config.maxTouchPoints ?? null);
}));
return {};
}
async setUserAgentOverrideParams(params) {
if (params.userAgent === '') {
throw new protocol_js_1.UnsupportedOperationException('empty user agent string is not supported');
}
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
userAgent: params.userAgent,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
userAgent: params.userAgent,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
userAgent: params.userAgent,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints);
}));
return {};
}
async setClientHintsOverride(params) {
const clientHints = params.clientHints ?? null;
// Get all relevant contexts to update:
// 1. Specific browsing contexts (if provided).
// 2. All contexts for specific user contexts (if provided).
// 3. All top-level contexts (if global).
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
clientHints,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
clientHints,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
clientHints,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints);
}));
return {};
}
async setNetworkConditions(params) {
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
emulatedNetworkConditions: params.networkConditions,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
emulatedNetworkConditions: params.networkConditions,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
emulatedNetworkConditions: params.networkConditions,
});
}
if (params.networkConditions !== null &&
params.networkConditions.type !== 'offline') {
throw new protocol_js_1.UnsupportedOperationException(`Unsupported network conditions ${params.networkConditions.type}`);
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setEmulatedNetworkConditions(config.emulatedNetworkConditions ?? null);
}));
return {};
}
}
exports.EmulationProcessor = EmulationProcessor;
// Export for testing.
function isValidLocale(locale) {
try {
new Intl.Locale(locale);
return true;
}
catch (e) {
if (e instanceof RangeError) {
return false;
}
// Re-throw other errors
throw e;
}
}
// Export for testing.
function isValidTimezone(timezone) {
try {
Intl.DateTimeFormat(undefined, { timeZone: timezone });
return true;
}
catch (e) {
if (e instanceof RangeError) {
return false;
}
// Re-throw other errors
throw e;
}
}
// Export for testing.
function isTimeZoneOffsetString(timezone) {
return /^[+-](?:2[0-3]|[01]\d)(?::[0-5]\d)?$/.test(timezone);
}
//# sourceMappingURL=EmulationProcessor.js.map

File diff suppressed because one or more lines are too long