Files
Obsidian/Default/Service Worker/ScriptCache/f1cdccba37924bda_0
T

1616 lines
62 KiB
Plaintext
Raw Normal View History

2026-11-06 08:29:11 +08:00
0\r§mûü!Óˆ‚2// Copyright (C) Microsoft Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
function expect(b, message) {
if (!b) {
throw new Error(message);
}
}
function unexpected(message) {
throw new Error(message);
}
// Copyright (C) Microsoft Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const NAA_REQUEST_EVENT_NAME = 'Edge.Auth.NaaRequest';
const NAA_RESPONSE_EVENT_NAME = 'Edge.Auth.NaaResponse';
function unwrap(args) {
expect(args !== undefined);
if ('envelope' in args) {
return args.envelope;
}
if ('payload' in args) {
return JSON.parse(args.payload);
}
throw new Error();
}
async function handleNestedApplicationAuth(port, message) {
const response = await chrome.copilotBridgePrivate.auth.nestedAppAuth(unwrap(message.args));
port.postMessage(NAA_RESPONSE_EVENT_NAME, message.requestId, { payload: JSON.stringify(response) });
}
function handleUnexpectedMessage$1() {
// Should never be hit.
throw new Error('Unexpected message in AuthApiHandler.');
}
const messages$4 = new Map([
[NAA_REQUEST_EVENT_NAME, handleNestedApplicationAuth],
]);
class AuthApiHandler {
connect(_port) {
// Nothing to do.
}
isHandlerFor(message) {
return messages$4.has(message.eventName);
}
// Note: no part of args is validated before calling this.
async handle(message, port) {
const handler = messages$4.get(message.eventName) ?? handleUnexpectedMessage$1;
await handler(port, message);
}
disconnect(_port) {
// Nothing to do.
}
}
// Copyright (C) Microsoft Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const CHAT_READY_REQUEST_EVENT_NAME = 'Edge.Chat.Ready';
const CHAT_READY_RESPONSE_EVENT_NAME = 'Edge.OnGetCopilotSurface';
const HOST_METADATA_RESPONSE_EVENT_NAME = 'Edge.OnGetHostMetadata';
const SCREENSHOT_REQUEST_EVENT_NAME = 'Edge.Chat.Screenshot';
const SCREENSHOT_RESPONSE_EVENT_NAME = 'Edge.Chat.OnGetScreenshot';
async function handleChatReady(port, message) {
const documentId = port.sender?.documentId;
if (documentId === undefined) {
unexpected('ChatApiHandler: port has no sender documentId');
}
const [_, surfaceResult] = await Promise.all([
chrome.copilotBridgePrivate.chat.notifyReady(documentId),
chrome.copilotBridgePrivate.chat.getSurface(documentId),
]);
port.postMessage(CHAT_READY_RESPONSE_EVENT_NAME, message.requestId, surfaceResult);
}
async function handleScreenshot(port, message) {
const documentId = port.sender?.documentId;
if (documentId === undefined) {
unexpected('ChatApiHandler: port has no sender documentId');
}
const base64Image = await chrome.copilotBridgePrivate.chat.takeScreenshot(documentId, message.args);
if (base64Image) {
port.postMessage(SCREENSHOT_RESPONSE_EVENT_NAME, message.requestId, { data: base64Image });
}
}
const messages$3 = new Map([
[CHAT_READY_REQUEST_EVENT_NAME, handleChatReady],
[SCREENSHOT_REQUEST_EVENT_NAME, handleScreenshot],
]);
class ChatApiHandler {
#ports = new Set();
connect(port) {
// Track the port and start listening for events when the first port connects.
if (this.#ports.size === 0) {
chrome.copilotBridgePrivate.chat.onHostMetadata.addListener(this.#onHostMetadata);
}
this.#ports.add(port);
}
isHandlerFor(message) {
return messages$3.has(message.eventName);
}
async handle(message, port) {
const handler = messages$3.get(message.eventName);
if (!handler) {
unexpected('Unexpected message in ChatApiHandler.');
}
handler(port, message);
}
disconnect(port) {
this.#ports.delete(port);
// Stop listening when all ports have disconnected.
if (this.#ports.size === 0) {
chrome.copilotBridgePrivate.chat.onHostMetadata.removeListener(this.#onHostMetadata);
}
}
// Arrow function to preserve `this` when used as a listener callback.
#onHostMetadata = (metadata) => {
const requestId = crypto.randomUUID();
for (const port of this.#ports) {
port.postMessage(HOST_METADATA_RESPONSE_EVENT_NAME, requestId, metadata);
}
};
}
// Copyright (C) Microsoft Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const IS_FEATURE_ENABLED_REQUEST_EVENT_NAME = 'Edge.IsFeatureEnabled';
const IS_FEATURE_ENABLED_RESPONSE_EVENT_NAME = 'Edge.OnIsFeatureEnabled';
const LOAD_TIME_DATA_REQUEST_EVENT_NAME = 'Edge.BrowserContext.LoadTimeData';
async function handleGetFeatures(port, message) {
const enabledFeatures = await chrome.copilotBridgePrivate.config.getFeatures();
port.postMessage(IS_FEATURE_ENABLED_RESPONSE_EVENT_NAME, message.requestId, { featureNames: enabledFeatures });
}
async function handleGetLoadTimeData(port, message) {
const loadTimeData = await chrome.copilotBridgePrivate.config.getLoadTimeData();
port.postMessage(message.eventName, message.requestId, loadTimeData);
}
function handleUnexpectedMessage() {
throw new Error('Unexpected message in ConfigApiHandler.');
}
const messages$2 = new Map([
[IS_FEATURE_ENABLED_REQUEST_EVENT_NAME, handleGetFeatures],
[LOAD_TIME_DATA_REQUEST_EVENT_NAME, handleGetLoadTimeData],
]);
class ConfigApiHandler {
connect(_port) {
// Nothing to do.
}
isHandlerFor(message) {
return messages$2.has(message.eventName);
}
// Note: no part of args is validated before calling this.
async handle(message, port) {
const handler = messages$2.get(message.eventName) ?? handleUnexpectedMessage;
await handler(port, message);
}
disconnect(_port) {
// Nothing to do.
}
}
// Copyright (C) Microsoft Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const supportedAPIs = [
'chrome.tabs.',
'chrome.windows.',
'chrome.authPrivate.',
'chrome.metricsPrivate.',
'chrome.copilotAuthPrivate.',
'chrome.ntpSettingsPrivate.',
];
const extensionsAPIContract = 'chrome.';
const profileSettingsUrl = 'edge://settings/profiles';
const backgroundImageSettingsUrl = 'edge://settings/appearance#Themes&1';
const copilotNtpSettingsUrl = 'edge://settings/ai#CopilotNtp&1';
const allowedUrls = [
profileSettingsUrl,
backgroundImageSettingsUrl,
copilotNtpSettingsUrl
];
class ExtensionApiProxy {
#event_listeners = new Map();
connect(_port) {
// Nothing to do.
}
isHandlerFor(message) {
return message.eventName.startsWith(extensionsAPIContract);
}
// Handle a request from the page to execute an Extension API.
// This only executes APIs that we explicitly support and also
// ensures that the data passed to the API is allowed.
async handle(message, port) {
let error;
// First make sure the API is one we support and also that the provided
// data is allowed for that API
if (this.#IsApiSupported(message.eventName) && this.#IsAllowed(message)) {
const apiDomain = this.#GetApiDomain(message.eventName);
if (this.#HandleApi(apiDomain, message, port)) {
return;
}
else {
error = 'Error in calling the API.';
}
}
else {
error = 'API not supported.';
}
port.postMessage(message.eventName, message.requestId, message.args, error);
}
disconnect(port) {
for (const [event, listeners] of this.#event_listeners) {
if (listeners.ports.has(port)) {
this.#RemovePort(event, port);
}
}
}
// Internal methods
#GetApiDomain(actionName) {
for (const apiDomain of supportedAPIs) {
if (actionName.startsWith(apiDomain)) {
return apiDomain;
}
}
return '';
}
#IsApiSupported(eventName) {
const apiDomain = this.#GetApiDomain(eventName);
switch (apiDomain) {
case 'chrome.tabs.':
return this.#IsTabsApiSupported(eventName);
case 'chrome.windows.':
return this.#IsWindowsApiSupported(eventName);
case 'chrome.authPrivate.':
return this.#IsAuthPrivateApiSupported(eventName);
case 'chrome.metricsPrivate.':
return this.#IsMetricsPrivateApiSupported(eventName);
case 'chrome.copilotAuthPrivate.':
return this.#IsCopilotAuthPrivateApiSupported(eventName);
case 'chrome.ntpSettingsPrivate.':
return this.#IsNtpSettingsPrivateApiSupported(eventName);
default:
return false;
}
}
#IsTabsApiSupported(eventName) {
switch (eventName) {
case 'chrome.tabs.create':
case 'chrome.tabs.query':
case 'chrome.tabs.update':
case 'chrome.tabs.reload':
case 'chrome.tabs.onUpdated':
case 'chrome.tabs.onActivated':
return true;
default:
return false;
}
}
#IsWindowsApiSupported(eventName) {
switch (eventName) {
case 'chrome.windows.getAll':
case 'chrome.windows.getCurrent':
return true;
default:
return false;
}
}
#IsAuthPrivateApiSupported(eventName) {
switch (eventName) {
case 'chrome.authPrivate.getPrimaryAccountInfo':
case 'chrome.authPrivate.getIntegratedAccountInfo':
case 'chrome.authPrivate.acquireAccessTokenSilently':
case 'chrome.authPrivate.acquireAccessTokenWithUserInteraction':
case 'chrome.authPrivate.isUserSignedInToHost':
case 'chrome.authPrivate.subscribeToServiceAuthEvents':
case 'chrome.authPrivate.startEdgeSignIn':
case 'chrome.authPrivate.onSignInStateChanged':
case 'chrome.authPrivate.onServiceAuthStateChanged':
return true;
default:
return false;
}
}
#IsMetricsPrivateApiSupported(eventName) {
switch (eventName) {
case 'chrome.metricsPrivate.recordBoolean':
case 'chrome.metricsPrivate.recordMediumCount':
case 'chrome.metricsPrivate.recordSmallCount':
case 'chrome.metricsPrivate.recordMediumTime':
case 'chrome.metricsPrivate.recordTime':
case 'chrome.metricsPrivate.recordCount':
case 'chrome.metricsPrivate.recordPercentage':
case 'chrome.metricsPrivate.recordLongTime':
case 'chrome.metricsPrivate.recordEnumerationValue':
case 'chrome.metricsPrivate.recordSparseValueWithHashMetricName':
return true;
default:
return false;
}
}
#IsCopilotAuthPrivateApiSupported(eventName) {
switch (eventName) {
case 'chrome.copilotAuthPrivate.startSignIn':
case 'chrome.copilotAuthPrivate.startSignOut':
case 'chrome.copilotAuthPrivate.startSwitchAccount':
case 'chrome.copilotAuthPrivate.isNativeWorkCopilotInstalled':
case 'chrome.copilotAuthPrivate.openWorkCopilot':
case 'chrome.copilotAuthPrivate.onAgeVerificationRequiredForSignIn':
case 'chrome.copilotAuthPrivate.continueSignInWithAgeInfo':
return true;
default:
return false;
}
}
#IsNtpSettingsPrivateApiSupported(eventName) {
switch (eventName) {
case 'chrome.ntpSettingsPrivate.logTopSitesTileActivated':
return true;
default:
return false;
}
}
#IsUrlAllowed(url) {
return (url.match(/^https?:\/\/.*$/) !== null);
}
#IsEdgeSettingsUrlAllowed(url) {
return allowedUrls.includes(url);
}
#IsAllowed(message) {
switch (message.eventName) {
case 'chrome.tabs.create': {
const url = message.args?.createProperties?.url ?? message.args?.url ?? '';
return this.#IsUrlAllowed(url) || this.#IsEdgeSettingsUrlAllowed(url);
}
case 'chrome.tabs.update':
return this.#IsUrlAllowed(message.args?.updateProperties?.url ?? message.args?.url ?? '');
case 'chrome.tabs.reload': {
// Validate that tabId is provided and is a valid number
const tabId = message.args?.tabId ?? message.args;
return typeof tabId === 'number' && tabId >= 0;
}
default:
return true;
}
}
// Actually handle the API. This call does not validate that the API is
// supported
#HandleApi(apiDomain, message, port) {
switch (apiDomain) {
case 'chrome.tabs.':
case 'chrome.windows.':
case 'chrome.authPrivate.':
case 'chrome.metricsPrivate.':
case 'chrome.copilotAuthPrivate.':
// Check in it is event listener.
if (message.eventName.split('.')?.[2]?.startsWith('on')) {
return this.#RegisterEvent(message, port);
}
else {
return this.#CallChromeApiFunc(message, port);
}
case 'chrome.ntpSettingsPrivate.':
return this.#CallChromeApiFunc(message, port);
default:
return false;
}
}
// Add a listener for a specific extension event
#RegisterEvent(message, port) {
const eventName = message.eventName;
const eventObj = this.#GetChromeApiFunc(eventName);
if (typeof eventObj !== 'object') {
throw new Error(`Expected an object for event ${eventName}, but got ${typeof eventObj}`);
}
if (!this.#event_listeners.has(eventObj)) {
const listeners = {
ports: new Map(),
callback: (args) => {
for (const [listener, requestId] of listeners.ports) {
listener.postMessage(eventName, requestId, args);
}
},
};
this.#event_listeners.set(eventObj, listeners);
eventObj.addListener(listeners.callback);
}
// We just added the listeners above (if it didn't already exist).
const listeners = this.#event_listeners.get(eventObj);
listeners.ports.set(port, message.requestId);
// Use undefined args to unsubscribe.
if (message.args === undefined) {
this.#RemovePort(eventObj, port);
return false;
}
return true;
}
// Get from a string like "chrome.tabs.update" the actual function
// chrome.tabs.update. Does not validate whether an API is supported
#GetChromeApiFunc(actionName) {
let obj = globalThis;
const apiDomains = actionName.split('.');
for (const api of apiDomains) {
obj = obj[api];
if (obj === undefined) {
throw new Error(`Couldn't find ${actionName} in #GetChromeApiFunc`);
}
}
return obj;
}
// Called by HandleApi to actually execute an extension API.
// Validation should already have occurred before calling this
// method. See HandleApi for supported APIs.
#CallChromeApiFunc(message, port) {
const eventName = message.eventName;
const func = this.#GetChromeApiFunc(eventName);
if (typeof func !== 'function') {
return false;
}
const callback = (response) => {
port.postMessage(eventName, message.requestId, response);
};
switch (eventName) {
case 'chrome.tabs.reload':
func(message.args?.tabId ?? message.args, message.args?.props, callback);
return true;
case 'chrome.metricsPrivate.recordBoolean':
case 'chrome.metricsPrivate.recordMediumCount':
case 'chrome.metricsPrivate.recordSmallCount':
case 'chrome.metricsPrivate.recordMediumTime':
case 'chrome.metricsPrivate.recordTime':
case 'chrome.metricsPrivate.recordCount':
case 'chrome.metricsPrivate.recordLongTime':
case 'chrome.metricsPrivate.recordPercentage':
case 'chrome.metricsPrivate.recordSparseValueWithHashMetricName':
func(message.args?.eventName, message?.args?.value);
return true;
// This method takes a boolean value as args and will skip the following
// args existence check. We handle it here.
case 'chrome.copilotAuthPrivate.openWorkCopilot':
func(message.args);
return true;
case 'chrome.metricsPrivate.recordEnumerationValue':
func(message.args?.eventName, message.args?.value, message.args?.maxValue);
return true;
case 'chrome.ntpSettingsPrivate.logTopSitesTileActivated':
func(message.args?.tileSource, message.args?.slotIndex, message.args?.slotIndexByTileSource, message.args?.url);
return true;
}
if (message.args) {
func(message.args, callback);
}
else {
func(callback);
}
return true;
}
#RemovePort(event, port) {
const listeners = this.#event_listeners.get(event) ?? unexpected('No event listener found');
listeners.ports.delete(port);
if (listeners.ports.size === 0) {
event.removeListener(listeners.callback);
this.#event_listeners.delete(event);
}
}
}
// Copyright (C) Microsoft Corporation. All rights reserved.
// Message names matching edge_webui/packages/copilot_common/src/edge_journeys/consts.ts
const EDGE_GET_JOURNEYS_DATA = 'Edge.Journeys.GetData';
const EDGE_HIDE_JOURNEYS_CARD = 'Edge.Journeys.HideCard';
const EDGE_CLICK_JOURNEYS_CARD = 'Edge.Journeys.ClickCard';
const EDGE_SUBMIT_JOURNEYS_CARD_FEEDBACK = 'Edge.Journeys.SubmitCardFeedback';
const EDGE_GET_JOURNEYS_AVAILABILITY = 'Edge.Journeys.GetAvailability';
const EDGE_GET_BROWSER_PROFILES = 'Edge.Journeys.GetBrowserProfiles';
const EDGE_IMPORT_BROWSING_HISTORY = 'Edge.Journeys.ImportBrowsingHistory';
const EDGE_ENABLE_JOURNEYS = 'Edge.Journeys.Enable';
const EDGE_ON_JOURNEYS_DATA_RECEIVED = 'Edge.Journeys.OnDataReceived';
const EDGE_ON_JOURNEYS_DATA_UPDATED = 'Edge.Journeys.OnDataUpdated';
const EDGE_ON_GET_JOURNEYS_AVAILABILITY = 'Edge.Journeys.OnGetAvailability';
const EDGE_ON_GET_BROWSER_PROFILES = 'Edge.Journeys.OnGetBrowserProfiles';
const EDGE_ON_IMPORT_BROWSING_HISTORY = 'Edge.Journeys.OnImportBrowsingHistory';
const EDGE_ON_ASK_COPILOT = 'Edge.Chat.OnAskCopilot';
const MAX_CARDS_TO_DISPLAY$1 = 3;
const BACKFILL_IMAGE_INDEX_SIZE = 6;
const COLDSTART_CARD_ID_TYPE = 'coldstart';
const BACKFILL_CARD_ID_TYPE = 'backfill';
const JOURNEYS_CLICK_EVENT_TYPE_HISTOGRAM = 'Microsoft.Journeys.NewTabPage.ClickEventType';
const JOURNEYS_CARD_CLICK_HISTOGRAM = 'Microsoft.Journeys.NewTabPage.JourneysCard.Click';
const JOURNEYS_CARD_SHOW_HISTOGRAM = 'Microsoft.Journeys.NewTabPage.JourneysCard.Show';
const JOURNEYS_CARD_CATEGORY_CLICK_HISTOGRAM = 'Microsoft.Journeys.NewTabPage.JourneysCardCategory.Click';
const handledMessages = new Set([
EDGE_GET_JOURNEYS_DATA,
EDGE_GET_JOURNEYS_AVAILABILITY,
EDGE_GET_BROWSER_PROFILES,
EDGE_IMPORT_BROWSING_HISTORY,
EDGE_ENABLE_JOURNEYS,
EDGE_HIDE_JOURNEYS_CARD,
EDGE_CLICK_JOURNEYS_CARD,
EDGE_SUBMIT_JOURNEYS_CARD_FEEDBACK,
]);
// --- Telemetry helpers ---
function recordEnumHistogram$1(name, value, max) {
chrome.metricsPrivate?.recordEnumerationValue(name, value, max);
}
function recordSparseHistogram$1(name, value) {
chrome.metricsPrivate?.recordSparseValueWithHashMetricName(name, value);
}
function getClickEventType(type) {
switch (type) {
case 'open urls':
return 0 /* JourneysClickEventType.OPEN_URLS */;
case 'navigation':
return 1 /* JourneysClickEventType.NAVIGATION */;
case 'copilot action':
return 2 /* JourneysClickEventType.COPILOT_ACTION */;
case 'dismiss':
return 3 /* JourneysClickEventType.DISMISS */;
case 'menu':
return 4 /* JourneysClickEventType.MENU */;
case 'why':
return 5 /* JourneysClickEventType.WHY */;
default:
return null;
}
}
// --- Data parsing ---
// Extension API returns plain JS objects (base::Value serialized),
// not mojom Value format ({dictionaryValue: {storage: {...}}}).
function parseJourneysInfoData(data, now = Date.now()) {
if (!data || typeof data !== 'object') {
return [];
}
const journeysList = data.journeys;
if (!Array.isArray(journeysList)) {
return [];
}
// No version marker, but v1 always has status; in v2, it is removed.
const isSchemaV2 = data.status === undefined;
const cards = [];
for (const journey of journeysList) {
if (cards.length === MAX_CARDS_TO_DISPLAY$1) {
break;
}
if (!journey || typeof journey !== 'object') {
continue;
}
// Parse sourceInfos
const rawSourceInfos = isSchemaV2 ? journey.relatedPages : journey.sourceInfos;
const sourceInfoList = [];
if (Array.isArray(rawSourceInfos)) {
for (const info of rawSourceInfos) {
if (info && typeof info === 'object') {
sourceInfoList.push({
url: info.url ?? '',
title: info.title ?? '',
});
}
}
}
// Parse copilot prompts
const action = isSchemaV2 ? journey.action : undefined;
const copilotPrompts = [];
if (isSchemaV2) {
if (action?.prompt) {
copilotPrompts.push(action.prompt);
}
}
else if (Array.isArray(journey.copilotPrompts)) {
for (const p of journey.copilotPrompts) {
if (typeof p === 'string') {
copilotPrompts.push(p);
}
}
}
let cardType = isSchemaV2 ?
(journey.isContextual ? 'navigation' : 'copilot action') :
journey.cardType;
let buttonText = isSchemaV2 ? action?.label : journey.buttonText;
const description = isSchemaV2 ? journey.description : journey.summary;
// For v1 schema, if additionalData.actions exists, randomly pick one.
if (!isSchemaV2) {
const actions = journey.additionalData?.actions;
if (Array.isArray(actions) && actions.length > 0) {
const selected = actions[Math.floor(Math.random() * actions.length)];
if (selected) {
if (selected.buttonText) {
buttonText = selected.buttonText;
}
if (selected.cardType) {
cardType = selected.cardType;
}
if (selected.copilotPrompt) {
copilotPrompts.length = 0;
copilotPrompts.push(selected.copilotPrompt);
}
}
}
}
const backfillImageIndex = isSchemaV2 ?
(cards.length % BACKFILL_IMAGE_INDEX_SIZE + 1) :
journey.backfillImageIndex;
const activityTime = isSchemaV2 ? '' : (journey.activityTime ?? '');
// Parse validEndTime and drop expired journeys.
let validEndTime;
if (isSchemaV2) {
const raw = journey.expiresAt;
if (typeof raw === 'string') {
const date = new Date(raw);
validEndTime = isNaN(date.getTime()) ? undefined : date.toISOString();
}
}
else {
validEndTime = journey.validEndTime;
}
if (validEndTime) {
const endDate = new Date(validEndTime);
if (!isNaN(endDate.getTime()) && endDate.getTime() < now) {
continue;
}
}
recordSparseHistogram$1(JOURNEYS_CARD_SHOW_HISTOGRAM, journey.id ?? '');
cards.push({
id: journey.id ?? '',
title: journey.title ?? '',
buttonText: buttonText ?? '',
description: description ?? '',
activityTime,
cardType: cardType ?? '',
sourceInfoList,
copilotPrompts,
backfillImageIndex,
imageUrl: journey.imageUrl ?? '',
imageCategory: journey.imageCategory ?? '',
validEndTime,
});
}
return cards;
}
function parseJourneysImageData(data) {
const imageMap = new Map();
if (!data || typeof data !== 'object') {
return imageMap;
}
const images = data.images;
if (!Array.isArray(images)) {
return imageMap;
}
for (const img of images) {
if (img?.base64ImageData && img?.journeyId) {
imageMap.set(img.journeyId, img.base64ImageData);
}
}
return imageMap;
}
function buildCards(journeysData) {
if (!journeysData) {
return [{ id: COLDSTART_CARD_ID_TYPE, cardType: COLDSTART_CARD_ID_TYPE }];
}
const cards = parseJourneysInfoData(journeysData.journeysData);
const imageMap = parseJourneysImageData(journeysData.imageData);
// Merge image data into cards.
for (const card of cards) {
const img = imageMap.get(card.id);
if (img) {
card.base64ImageData = img;
}
}
// Add backfill/coldstart cards.
if (cards.length >= 1 && cards.length <= 2 &&
!cards.some(c => c.cardType === 'backfill')) {
cards.push({ id: BACKFILL_CARD_ID_TYPE, cardType: BACKFILL_CARD_ID_TYPE });
}
else if (cards.length === 0) {
cards.push({ id: COLDSTART_CARD_ID_TYPE, cardType: COLDSTART_CARD_ID_TYPE });
}
return cards;
}
// --- Handler ---
class JourneysApiHandler {
// port -> requestId: tracks which ports want data update notifications.
#dataWatchers = new Map();
connect(_port) {
// Nothing to do.
}
isHandlerFor(message) {
return handledMessages.has(message.eventName);
}
async handle(message, port) {
switch (message.eventName) {
case EDGE_GET_JOURNEYS_DATA:
await this.#handleGetData(message, port);
break;
case EDGE_GET_JOURNEYS_AVAILABILITY:
await this.#handleGetAvailability(message, port);
break;
case EDGE_HIDE_JOURNEYS_CARD:
await this.#handleHideCard(message.args);
break;
case EDGE_CLICK_JOURNEYS_CARD:
await this.#handleClickCard(message, port);
break;
case EDGE_SUBMIT_JOURNEYS_CARD_FEEDBACK:
await this.#handleSubmitFeedback(message.args);
break;
case EDGE_GET_BROWSER_PROFILES:
await this.#handleGetBrowserProfiles(message, port);
break;
case EDGE_IMPORT_BROWSING_HISTORY:
await this.#handleImportBrowsingHistory(message, port);
break;
case EDGE_ENABLE_JOURNEYS:
this.#handleEnableJourneys();
break;
default:
unexpected(`Unhandled message: ${message.eventName}`);
}
}
disconnect(port) {
this.#dataWatchers.delete(port);
if (this.#dataWatchers.size === 0) {
chrome.copilotBridgePrivate.journeys.onJourneysDataUpdated
.removeListener(this.#onDataUpdated);
}
}
// --- GetData ---
async #handleGetData(message, port) {
// Register this port for push updates.
const isEmpty = this.#dataWatchers.size === 0;
if (isEmpty) {
this.#dataWatchers.set(port, message.requestId);
chrome.copilotBridgePrivate.journeys.onJourneysDataUpdated
.addListener(this.#onDataUpdated);
}
const journeysData = await chrome.copilotBridgePrivate.journeys
.getLatestJourneysData();
const cards = buildCards(journeysData ?? null);
port.postMessage(EDGE_ON_JOURNEYS_DATA_RECEIVED, message.requestId, { cards });
}
// --- GetAvailability ---
async #handleGetAvailability(message, port) {
const isAvailable = await chrome.copilotBridgePrivate.journeys
.getJourneysAvailability();
port.postMessage(EDGE_ON_GET_JOURNEYS_AVAILABILITY, message.requestId, { isAvailable });
}
// --- HideCard ---
async #handleHideCard(args) {
const eventTime = new Date();
const clickEventType = getClickEventType('dismiss');
if (clickEventType !== null) {
recordEnumHistogram$1(JOURNEYS_CLICK_EVENT_TYPE_HISTOGRAM, clickEventType, 5 /* JourneysClickEventType.MAX */);
}
await chrome.copilotBridgePrivate.journeys.sendJourneysEventData({
timeStamp: eventTime.getTime(),
journeyId: args.id ?? '',
eventData: {
eventType: 'dismiss',
eventTime: eventTime.toISOString(),
prompt: '',
journeyTitle: '',
sourceInfos: (args.sourceInfoList ?? []).map(info => ({ url: info.url, title: info.title })),
},
});
}
// --- ClickCard ---
async #handleClickCard(message, port) {
const args = message.args;
const clickEventType = getClickEventType(args.cardType);
if (clickEventType !== null) {
recordEnumHistogram$1(JOURNEYS_CLICK_EVENT_TYPE_HISTOGRAM, clickEventType, 5 /* JourneysClickEventType.MAX */);
}
recordSparseHistogram$1(JOURNEYS_CARD_CLICK_HISTOGRAM, args.id);
if (args.imageCategory) {
recordSparseHistogram$1(JOURNEYS_CARD_CATEGORY_CLICK_HISTOGRAM, args.imageCategory);
}
const eventTime = new Date();
const query = args.copilotPrompts?.[0] ?? '';
const result = await chrome.copilotBridgePrivate.journeys
.sendJourneysEventData({
timeStamp: eventTime.getTime(),
journeyId: args.id ?? '',
eventData: {
eventType: args.cardType ?? '',
eventTime: eventTime.toISOString(),
prompt: query,
journeyTitle: args.title ?? '',
sourceInfos: (args.sourceInfoList ?? []).map(info => ({ url: info.url, title: info.title })),
},
});
port.postMessage(EDGE_ON_ASK_COPILOT, crypto.randomUUID(), {
additionalData: result || '{}',
bypassConsentCheck: true,
createNewChat: true,
imageData: '',
inputMethod: 'askCopilot',
pageContextStrategy: 'journeysNTP',
prepopulate: false,
prompt: query,
source: 'JOURNEYS_NTP',
});
}
// --- SubmitCardFeedback ---
async #handleSubmitFeedback(args) {
if (args.action !== 'thumbsUp' && args.action !== 'thumbsDown') {
unexpected(`Invalid feedback action: ${args.action}`);
}
const extraData = {};
for (const [key, value] of Object.entries(args)) {
try {
extraData[key] =
typeof value === 'string' ? value : JSON.stringify(value);
}
catch {
extraData[key] = String(value);
}
}
await chrome.edgeFeedbackPrivate.sendMicroFeedbackVote({
itemId: 0,
itemName: 'Journeys Card Feedback',
serviceId: '39d8231d-2a26-469e-99c4-ce8139f02bee',
vote: args.action === 'thumbsUp' ? 'like' : 'dislike',
extraData,
}, (_clientFeedbackId) => { });
}
// --- GetBrowserProfiles ---
async #handleGetBrowserProfiles(message, port) {
const profiles = await chrome.copilotBridgePrivate.journeys
.getAvailableBrowserProfiles();
port.postMessage(EDGE_ON_GET_BROWSER_PROFILES, message.requestId, { profiles });
}
// --- ImportBrowsingHistory ---
async #handleImportBrowsingHistory(message, port) {
const args = message.args;
const result = await chrome.copilotBridgePrivate.journeys
.importBrowsingHistory(args.browserType, args.profileIndex);
port.postMessage(EDGE_ON_IMPORT_BROWSING_HISTORY, message.requestId, {
success: result.success,
errorMessage: result.errorMessage,
});
}
// --- EnableJourneys ---
#handleEnableJourneys() {
chrome.copilotBridgePrivate.journeys.enable(chrome.copilotBridgePrivate.journeys.OptInSource.NTP);
}
#onDataUpdated = (journeysData) => {
const cards = buildCards(journeysData);
for (const [port, requestId] of this.#dataWatchers) {
port.postMessage(EDGE_ON_JOURNEYS_DATA_UPDATED, requestId, { cards });
}
};
}
// Copyright (C) Microsoft Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
class MessagingPort {
port;
disconnected;
constructor(port) {
this.port = port;
}
postMessage(eventName, requestId, args, error) {
if (this.disconnected)
return;
const msg = { eventName, requestId, args };
if (error !== undefined)
msg.error = error;
this.port.postMessage(msg);
}
get onDisconnect() {
return this.port.onDisconnect;
}
get onMessage() {
return this.port.onMessage;
}
get sender() {
return this.port.sender;
}
}
// Copyright (C) Microsoft Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const MOCK_EVENT_NAMES = [
'Edge.Chat.GetCopilotMode',
'Edge.Searchbox.FocusChanged',
'Edge.Searchbox.ShouldSendOnlyChatSuggestions',
];
function reply(port, eventName, requestId, args) {
console.info(`MockMessageHandler: response ${eventName}`, requestId, args);
port.postMessage(eventName, requestId, args);
}
// This is a mock message handler that returns mocked responses for testing
// purposes.
class MockMessageHandler {
connect(_port) {
// Nothing to do.
}
isHandlerFor(message) {
return MOCK_EVENT_NAMES.includes(message.eventName);
}
async handle(message, port) {
const { eventName, requestId, args } = message;
console.info(`MockMessageHandler: request ${eventName}`, requestId, args);
switch (eventName) {
case 'Edge.Chat.GetCopilotMode': {
reply(port, 'Edge.Chat.OnGetCopilotMode', requestId, { isCopilotModeEnabled: true });
break;
}
}
}
disconnect(_port) {
// Nothing to do.
}
}
// Copyright (C) Microsoft Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// CMC -> Edge notifications.
const trace = () => { };
// Future notifications:
const NOTIFY_CHAT_SETTINGS_CLOSED_EVENT_NAME = 'Edge.Notify.OnUserClosedChatSettings';
const NOTIFY_CLOSE_SIDEPANE_EVENT_NAME = 'Edge.Notify.HeaderCommand.ClosePane';
const NOTIFY_CONTENT_SIZE_EVENT_NAME = 'Edge.Notify.ContentSize';
const NOTIFY_CONVERSATION_EVENT_NAME = 'Edge.Notify.Conv';
const NOTIFY_ERROR_EVENT_NAME = 'Edge.Notify.Error';
const NOTIFY_FEATURES_EVENT_NAME = 'Edge.Notify.Features';
const NOTIFY_MESSAGE_SENT_EVENT_NAME = 'Edge.Notify.MessageSent';
const NOTIFY_OPEN_SIDEPANE_EVENT_NAME = 'Edge.Notify.OpenSidepane.WithConversation';
const NOTIFY_USER_EVENT_NAME = 'Edge.Notify.User';
const NOTIFY_SEND_FEEDBACK_EVENT_NAME = 'Edge.Notify.HeaderCommand.SendFeedback';
const NOTIFY_TOGGLE_PIN_EVENT_NAME = 'Edge.Notify.HeaderCommand.TogglePinState';
// Edge -> CMC notifications.
const NOTIFY_PROJECT_ATTACHED = 'Edge.NTP.OnProjectAttached';
const NOTIFY_PROJECT_DETACHED = 'Edge.NTP.OnProjectDetached';
const EventType = chrome.copilotBridgePrivate.notify.EventType;
function handleNotify(event) {
return async (port, message) => {
trace(message.eventName);
const documentId = port.sender?.documentId ?? unexpected('need document id');
await chrome.copilotBridgePrivate.notify.dispatch(documentId, event, message.args);
};
}
const messages$1 = new Map([
[NOTIFY_CHAT_SETTINGS_CLOSED_EVENT_NAME, handleNotify(EventType.CHAT_SETTINGS_CLOSED)],
[NOTIFY_CLOSE_SIDEPANE_EVENT_NAME, handleNotify(EventType.CLOSE_SIDEBAR_PANE)],
[NOTIFY_CONTENT_SIZE_EVENT_NAME, handleNotify(EventType.CONTENT_SIZE)],
[NOTIFY_CONVERSATION_EVENT_NAME, handleNotify(EventType.CONVERSATION)],
[NOTIFY_ERROR_EVENT_NAME, handleNotify(EventType.ERROR)],
[NOTIFY_FEATURES_EVENT_NAME, handleNotify(EventType.FEATURES)],
[NOTIFY_MESSAGE_SENT_EVENT_NAME, handleNotify(EventType.MESSAGE_SENT)],
[NOTIFY_OPEN_SIDEPANE_EVENT_NAME, handleNotify(EventType.OPEN_SIDEPANE)],
[NOTIFY_SEND_FEEDBACK_EVENT_NAME, handleNotify(EventType.SEND_FEEDBACK)],
[NOTIFY_USER_EVENT_NAME, handleNotify(EventType.USER)],
[NOTIFY_TOGGLE_PIN_EVENT_NAME, handleNotify(EventType.TOGGLE_SIDEBAR_PIN)],
]);
function dispatch(event) {
return async (port, requestId, payload) => {
port.postMessage(event, requestId, payload);
};
}
const NotificationType = chrome.copilotBridgePrivate.notify.NotificationType;
const notifications = new Map([
[NotificationType.PROJECT_ATTACHED, dispatch(NOTIFY_PROJECT_ATTACHED)],
[NotificationType.PROJECT_DETACHED, dispatch(NOTIFY_PROJECT_DETACHED)],
]);
class NotifyApiHandler {
#ports;
constructor(ports) {
this.#ports = ports;
chrome.copilotBridgePrivate.notify.onNotification.addListener(this.#onNotification.bind(this));
}
isHandlerFor(message) {
return messages$1.has(message.eventName);
}
// Note: no part of args is validated before calling this.
async handle(message, port) {
const handler = messages$1.get(message.eventName) ?? unexpected(`unexpected message ${message.eventName} for NotifyApiHandler`);
await handler(port, message);
}
#onNotification(documentId, event, payload) {
const notifier = notifications.get(event);
if (!notifier) {
return;
}
const requestId = crypto.randomUUID();
this.#ports.matchingPorts(documentId)
.forEach(p => notifier(p, requestId, payload));
}
}
// Copyright (C) Microsoft Corporation. All rights reserved.
class PortMap {
#ports = new Set();
add(port) {
this.#ports.add(port);
}
remove(port) {
this.#ports.delete(port);
}
get ports() {
return Array.from(this.#ports);
}
get(documentId) {
for (const port of this.#ports) {
if (port.sender?.documentId === documentId) {
return port;
}
}
return undefined;
}
// Yields all ports matching documentId or all ports.
*matchingPorts(documentId) {
for (const port of this.#ports) {
if (documentId === undefined || port.sender?.documentId === documentId) {
yield port;
}
}
}
}
// Copyright (C) Microsoft Corporation. All rights reserved.
const PREF_REQUEST_EVENT_NAME = 'Edge.Context.Pref';
const PREF_RESPONSE_EVENT_NAME = 'Edge.Context.OnPref';
// Wraps a bare primitive into the tagged-union PrefValue.
function toExtPrefValue(value) {
switch (typeof value) {
case 'string':
return { stringValue: value };
case 'number':
return { intValue: value };
case 'boolean':
return { boolValue: value };
default:
unexpected(`Unsupported pref value type: ${typeof value}`);
}
}
// Unwraps a tagged-union PrefValue into a bare primitive.
function fromExtPrefValue(pv) {
return pv.intValue ?? pv.stringValue ?? pv.boolValue ?? unexpected('PrefValue has no field set');
}
function validatePrefArgs(args) {
return !!args && typeof args === 'object' && 'key' in args &&
typeof args.key === 'string';
}
class PrefApiHandler {
connect(_port) {
// Nothing to do.
}
// key -> Map<port, requestId>: tracks which ports want change notifications
// for each pref key, and the requestId to use when sending the notification.
#prefsWatchers = new Map();
#listening = false;
isHandlerFor(message) {
return message.eventName === PREF_REQUEST_EVENT_NAME;
}
async handle(message, port) {
if (!validatePrefArgs(message.args)) {
unexpected('PrefArgs is missing or invalid');
}
const prefArgs = message.args;
// Register watcher if the caller wants change notifications.
if (prefArgs.notifyOnChange) {
if (!this.#prefsWatchers.has(prefArgs.key)) {
this.#prefsWatchers.set(prefArgs.key, new Map());
}
this.#prefsWatchers.get(prefArgs.key).set(port, message.requestId);
this.#maybeStartListening();
}
if (prefArgs.value !== undefined) {
// SET: no response needed.
await chrome.copilotBridgePrivate.prefs.set(prefArgs.key, toExtPrefValue(prefArgs.value));
}
else {
// GET: send result back to webpage.
const result = await chrome.copilotBridgePrivate.prefs.get(prefArgs.key);
port.postMessage(PREF_RESPONSE_EVENT_NAME, message.requestId, {
key: result.key,
value: fromExtPrefValue(result.value),
});
}
}
disconnect(_port) {
for (const [key, watchers] of this.#prefsWatchers) {
watchers.delete(_port);
if (watchers.size === 0) {
this.#prefsWatchers.delete(key);
}
}
this.#maybeStopListening();
}
#maybeStartListening() {
if (this.#listening) {
return;
}
this.#listening = true;
chrome.copilotBridgePrivate.prefs.onChanged.addListener(this.#onPrefChanged);
}
#maybeStopListening() {
if (this.#prefsWatchers.size > 0) {
return;
}
if (!this.#listening) {
return;
}
this.#listening = false;
chrome.copilotBridgePrivate.prefs.onChanged.removeListener(this.#onPrefChanged);
}
// Arrow function to preserve `this` when used as a listener callback.
#onPrefChanged = (prefChange) => {
const watchers = this.#prefsWatchers.get(prefChange.key);
if (!watchers) {
return;
}
for (const [port, requestId] of watchers) {
port.postMessage(PREF_RESPONSE_EVENT_NAME, requestId, {
key: prefChange.key,
value: fromExtPrefValue(prefChange.value),
});
}
};
}
// Copyright (C) Microsoft Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const QUICK_LINK_CLICKED_EVENT_NAME = 'Edge.NTP.QuickLinkClicked';
const TILE_SOURCE_CUSTOM = 3;
const TILE_SOURCE_MOST_VISITED = 0;
const HANDLED_EVENTS = [
QUICK_LINK_CLICKED_EVENT_NAME,
];
class QuickLinksApiHandler {
connect(_port) {
// Nothing to do.
}
disconnect(_port) {
// Nothing to do.
}
isHandlerFor(message) {
return HANDLED_EVENTS.includes(message.eventName);
}
async handle(message, port) {
switch (message.eventName) {
case QUICK_LINK_CLICKED_EVENT_NAME:
this.#handleQuickLinkClicked(port.sender?.tab?.id, message.args);
break;
}
}
#handleQuickLinkClicked(tabId, args) {
if (tabId === undefined || !args.url) {
return;
}
let parsedUrl;
try {
parsedUrl = new URL(args.url);
}
catch {
return;
}
if (parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') {
return;
}
chrome.tabs.update(tabId, { url: args.url, active: true });
let topsiteSource;
switch (args.source) {
case TILE_SOURCE_CUSTOM:
topsiteSource = 'CUSTOM_LINK';
break;
case TILE_SOURCE_MOST_VISITED:
topsiteSource = 'MOST_VISITED';
break;
default:
topsiteSource = 'OTHER';
}
chrome.ntpSettingsPrivate.logTopSitesTileActivated(topsiteSource, args.position, args.positionBySource, args.url);
}
}
// Copyright (C) Microsoft Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const ACTIVATE_REBATES_EVENT_NAME = 'Edge.Chat.ActivateRebates';
const SIGNIN_TO_REBATES_EVENT_NAME = 'Edge.Chat.SignInToRebates';
const ON_ACTIVATE_REBATES_EVENT_NAME = 'Edge.Chat.OnActivateRebates';
async function handleActivateRebates(port, message) {
const documentId = port.sender?.documentId ?? unexpected('need document id');
const success = await chrome.copilotBridgePrivate.rebates.activate(documentId);
port.postMessage(ON_ACTIVATE_REBATES_EVENT_NAME, message.requestId, { success });
}
async function handleSignInToRebates(port, _message) {
const documentId = port.sender?.documentId ?? unexpected('need document id');
chrome.copilotBridgePrivate.rebates.signIn(documentId);
}
const messages = new Map([
[ACTIVATE_REBATES_EVENT_NAME, handleActivateRebates],
[SIGNIN_TO_REBATES_EVENT_NAME, handleSignInToRebates],
]);
class RebatesApiHandler {
connect(_port) {
// Nothing to do.
}
isHandlerFor(message) {
return messages.has(message.eventName);
}
async handle(message, port) {
const handler = messages.get(message.eventName) ??
unexpected(`unexpected message ${message.eventName} for RebatesApiHandler`);
await handler(port, message);
}
disconnect(_port) {
// Nothing to do.
}
}
// Copyright (C) Microsoft Corporation. All rights reserved.
// Message names matching edge_webui/packages/copilot_common/src/edge_copilot_tips/consts.ts
const EDGE_COPILOT_TIPS_GET_DATA = 'Edge.CopilotTips.GetData';
const EDGE_COPILOT_TIPS_CLICK_CARD = 'Edge.CopilotTips.ClickCard';
const EDGE_COPILOT_TIPS_TIP_DISPLAYED = 'Edge.CopilotTips.TipDisplayed';
const EDGE_COPILOT_TIPS_ON_DATA_RECEIVED = 'Edge.CopilotTips.OnDataReceived';
const MAX_CARDS_TO_DISPLAY = 3;
// --- Telemetry helpers ---
function recordEnumHistogram(name, value, max) {
chrome.metricsPrivate.recordEnumerationValue(name, value, max);
}
function recordSparseHistogram(name, value) {
chrome.metricsPrivate.recordSparseValueWithHashMetricName(name, value);
}
function parseTipsData(tipsData) {
if (!tipsData?.tips?.length) {
return [];
}
const cards = [];
for (const tip of tipsData.tips) {
if (cards.length === MAX_CARDS_TO_DISPLAY) {
break;
}
cards.push({
id: tip.id,
title: tip.title,
description: tip.description,
buttonText: tip.cta,
prompt: tip.prompt,
imageUrl: tip.imageUrl ?? '',
composerMode: tip.composerMode,
});
}
return cards;
}
class TipsApiHandler {
#engagedByPort = new Set();
connect(_port) {
// Nothing to do.
}
isHandlerFor(message) {
return message.eventName === EDGE_COPILOT_TIPS_GET_DATA ||
message.eventName === EDGE_COPILOT_TIPS_CLICK_CARD ||
message.eventName === EDGE_COPILOT_TIPS_TIP_DISPLAYED;
}
async handle(message, port) {
switch (message.eventName) {
case EDGE_COPILOT_TIPS_GET_DATA:
await this.#handleGetData(message, port);
break;
case EDGE_COPILOT_TIPS_CLICK_CARD:
await this.#handleClickCard(message.args, port);
break;
case EDGE_COPILOT_TIPS_TIP_DISPLAYED:
this.#handleTipDisplayed(message.args);
break;
default:
unexpected(`Unhandled message: ${message.eventName}`);
}
}
disconnect(port) {
this.#engagedByPort.delete(port);
}
async #handleGetData(message, port) {
const tipsData = await chrome.copilotBridgePrivate.tips.getAll();
const cards = parseTipsData(tipsData ?? null);
port.postMessage(EDGE_COPILOT_TIPS_ON_DATA_RECEIVED, message.requestId, { cards });
}
async #handleClickCard(args, port) {
// Validate args at the trust boundary — message.args is untyped.
if (!args || typeof args.id !== 'string' || !args.id) {
return;
}
// Record engagement only once per port session.
if (!this.#engagedByPort.has(port)) {
this.#engagedByPort.add(port);
recordEnumHistogram('Microsoft.CopilotTips.Engaged', 0, 1);
}
// Record call-to-action per click.
recordSparseHistogram('Microsoft.CopilotTips.CallToAction', args.id);
const documentId = port.sender?.documentId;
if (documentId) {
chrome.copilotBridgePrivate.tips.notifyClick(documentId);
}
}
#handleTipDisplayed(args) {
if (!args || !Array.isArray(args.tipIds)) {
return;
}
for (const tipId of args.tipIds) {
if (typeof tipId !== 'string' || !tipId) {
continue;
}
recordSparseHistogram('Microsoft.CopilotTips.TipDisplayed', tipId);
}
}
}
// Copyright (C) Microsoft Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Event names that route to handleNativeToolAction via ParseActionName.
const NATIVE_TOOL_EVENT_NAMES = new Set([
'Edge.ToolAction',
'Edge.LockTabs',
'Edge.ReleaseTabs',
'Edge.ShowTabs',
'Edge.AllowActionForUrl',
]);
// Event names handled directly by this handler (not routed to native).
const CONSENT_EVENT_NAMES = new Set([
'Edge.SetCoworkActionConsent',
]);
// Origins allowed to invoke tool actions (defense-in-depth).
// Exact-match origins (scheme + host + port).
const TOOL_ACTION_ALLOWED_ORIGINS = new Set([
'https://m365.cloud.microsoft',
'https://scuprodprv.m365.cloud.microsoft',
]);
// When --cowork-local-dev injects loopback patterns into the manifest's
// externally_connectable.matches, also allow those origins for tool actions.
// This keeps a single source of truth (the C++ manifest injection) for which
// local-dev origins are permitted. Manifest patterns like "http://localhost/*"
// match any port, so we compare by scheme+hostname (ignoring port).
const COWORK_LOCAL_DEV_HOSTNAMES = [
'localhost',
'127.0.0.1',
'agentbuilder.local.microsoft.com',
];
const manifest = chrome.runtime.getManifest();
const ecMatches = manifest?.externally_connectable?.matches ?? [];
const coworkLocalDevAllowed = new Set();
for (const pattern of ecMatches) {
for (const host of COWORK_LOCAL_DEV_HOSTNAMES) {
if (pattern.includes(host)) {
// Extract scheme from pattern (e.g. "http://localhost/*" → "http").
const scheme = pattern.split('://')[0];
coworkLocalDevAllowed.add(`${scheme}://${host}`);
}
}
}
function isOriginAllowed(origin) {
if (TOOL_ACTION_ALLOWED_ORIGINS.has(origin)) {
return true;
}
// For cowork-local-dev origins, match by scheme+hostname (any port).
try {
const url = new URL(origin);
return coworkLocalDevAllowed.has(`${url.protocol}//${url.hostname}`);
}
catch (_e) {
return false;
}
}
/**
* Handles native tool action messages from external ports (cowork) and
* delegates to the edgeCopilotRemoteActionsPrivate.handleNativeToolAction API.
*
* Supports all event names dispatched by ParseActionName: Edge.ToolAction,
* Edge.LockTabs, Edge.ReleaseTabs, Edge.ShowTabs, Edge.AllowActionForUrl.
*/
class ToolActionApiHandler {
ports = new Set();
listenerRegistered = false;
toolActionsAllowed = false;
onToolEventListener;
connect(port) {
this.ports.add(port);
if (!this.listenerRegistered) {
this.onToolEventListener =
(event) => {
// Trusted single-origin assumption — events are broadcast to
// every connected port regardless of which port owns the tab
// or activity. This is safe today because all ports come from
// a single trusted M365 origin. If multi-origin surfaces
// expand (e.g. cowork-local-dev + scuprodprv connected
// concurrently), events should be routed by tabId ownership
// to avoid cross-origin information leakage.
for (const p of this.ports) {
p.postMessage(event.eventName, event.requestId ?? '', event);
}
};
chrome.edgeCopilotRemoteActionsPrivate.onToolEvent.addListener(this.onToolEventListener);
this.listenerRegistered = true;
}
}
disconnect(port) {
this.ports.delete(port);
if (this.ports.size === 0 && this.listenerRegistered &&
this.onToolEventListener) {
chrome.edgeCopilotRemoteActionsPrivate.onToolEvent.removeListener(this.onToolEventListener);
this.onToolEventListener = undefined;
this.listenerRegistered = false;
}
}
isHandlerFor(message) {
return NATIVE_TOOL_EVENT_NAMES.has(message.eventName) ||
CONSENT_EVENT_NAMES.has(message.eventName);
}
/** Fetches current tool actions allowed state from the native API. */
async refreshToolActionsAllowed() {
try {
const result = await chrome.edgeCopilotRemoteActionsPrivate.getToolActionConsent();
this.toolActionsAllowed = result.allowed;
}
catch (_e) {
this.toolActionsAllowed = false;
}
return this.toolActionsAllowed;
}
/** Returns the cached tool actions allowed state. */
getToolActionsAllowed() {
return this.toolActionsAllowed;
}
async handle(message, port) {
// All cowork messages require an allowed origin.
const origin = port.sender?.origin;
if (!origin || !isOriginAllowed(origin)) {
const { eventName, requestId } = message;
port.postMessage(eventName, requestId, null, 'Origin not allowed');
return;
}
// Handle consent-related messages (not routed to native).
// TODO(62644489): Move consent handling logic to the C++ API layer so
// the bridge extension remains a thin pass-through. Validation,
// pref-setting, and capabilities re-broadcast should be handled natively.
if (message.eventName === 'Edge.SetCoworkActionConsent') {
const { requestId, args } = message;
try {
const argsObj = args;
if (typeof argsObj?.consent !== 'boolean') {
port.postMessage(message.eventName, requestId, null, 'Invalid consent value: expected boolean');
return;
}
await chrome.edgeCopilotRemoteActionsPrivate.setToolActionConsent(argsObj.consent);
await this.refreshToolActionsAllowed();
// Re-broadcast capabilities to all connected ports with fresh state.
this.broadcastCapabilities();
port.postMessage(message.eventName, requestId, { success: true });
}
catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e);
port.postMessage(message.eventName, requestId, null, errorMessage);
}
return;
}
const { eventName, requestId, args } = message;
try {
const response = await chrome.edgeCopilotRemoteActionsPrivate.handleNativeToolAction({ eventName, requestId, args: args ?? {} });
port.postMessage(eventName, requestId, response);
}
catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e);
port.postMessage(eventName, requestId, null, errorMessage);
}
}
/** Broadcasts updated capabilities to all connected ports. */
broadcastCapabilities() {
const capabilities = {
toolAction: true,
toolActionAllowed: this.toolActionsAllowed,
};
for (const p of this.ports) {
p.postMessage('Edge.Capabilities', crypto.randomUUID(), capabilities);
}
}
}
// Copyright (C) Microsoft Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
class UnhandledMessageHandler {
connect(_port) {
// Nothing to do.
}
isHandlerFor(_message) {
return true;
}
// Note: no part of args is validated before calling this.
async handle(message, _port) {
throw new Error(`unhandled message: ${JSON.stringify(message)}`);
}
disconnect(_port) {
// Nothing to do.
}
}
// Copyright (C) Microsoft Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(function hookErrorReporting(component) {
function reportError(info, error) {
const errorInfo = {
column: info.column,
component,
line: info.line,
message: error?.message ?? '',
name: error?.name ?? '',
source_url: info.source,
stack: error?.stack,
};
chrome.errorReporting.reportError(errorInfo);
}
globalThis.onerror = (_message, source, lineno, columnNumber, error) => {
reportError({
column: columnNumber ?? 0,
line: lineno ?? 0,
source: source ?? 'unknown',
}, error);
};
globalThis.addEventListener('unhandledrejection', (e) => {
reportError({ column: 0, line: 0, source: 'unhandledrejection' }, e.reason instanceof Error ? e.reason : undefined);
});
})('copilot-extension-bridge');
const ports = new PortMap();
/** Wire-level message names used on the legacy `copilot-bridge` port. */
const CAPABILITIES_EVENT_NAME = 'Edge.Capabilities';
// Some APIs are platform-gated (e.g. chatInteractions, qvSettings are
// mac-only). Guard construction so the service worker does not crash on
// platforms where the API namespace is undefined.
const handlers = [
new ConfigApiHandler(),
new PrefApiHandler(),
new ChatApiHandler(),
new ExtensionApiProxy(),
new JourneysApiHandler(),
new AuthApiHandler(),
new QuickLinksApiHandler(),
new TipsApiHandler(),
new NotifyApiHandler(ports),
new RebatesApiHandler(),
...(typeof chrome.edgeCopilotRemoteActionsPrivate !== 'undefined' &&
chrome.runtime.getManifest()['edge_cowork_enabled'] === true ?
[new ToolActionApiHandler()] :
[]),
// For testing purposes. This should be removed before shipping.
new MockMessageHandler(),
// Must be last.
new UnhandledMessageHandler(),
];
function isMessage(message) {
if (typeof message !== 'object') {
return false;
}
if (!('eventName' in message) || typeof message.eventName !== 'string') {
return false;
}
if (!('requestId' in message) || typeof message.requestId !== 'string') {
return false;
}
return true;
}
function timeAndCall(eventName, f) {
{
return f();
}
}
chrome.runtime.onConnectExternal.addListener((rawPort) => {
if (rawPort.name !== 'copilot-bridge')
return;
const port = new MessagingPort(rawPort);
ports.add(port);
handlers.forEach(h => h.connect?.(port));
// Announce capabilities at connect time so page-side code can
// feature-detect. New entries here are opt-in by the receiver:
// unknown keys must be ignored. ToolActionApiHandler is conditionally
// registered above, so the instanceof check correctly reflects
// whether it is available on this build.
const toolActionHandler = handlers.find(h => h instanceof ToolActionApiHandler);
const capabilities = {
toolAction: !!toolActionHandler,
toolActionAllowed: false,
};
if (toolActionHandler) {
// Fetch fresh consent state from native API for the initial announcement.
toolActionHandler.refreshToolActionsAllowed().then(allowed => {
capabilities.toolActionAllowed = allowed;
}).catch(() => {
// On failure, send capabilities with default (false) state.
}).finally(() => {
port.postMessage(CAPABILITIES_EVENT_NAME, crypto.randomUUID(), capabilities);
});
}
else {
port.postMessage(CAPABILITIES_EVENT_NAME, crypto.randomUUID(), capabilities);
}
port.onMessage.addListener(async (message) => {
if (!isMessage(message)) {
throw new Error(`malformed message: ${JSON.stringify(message)}`);
}
const handler = handlers.find(h => h.isHandlerFor(message));
await timeAndCall(message.eventName, () => handler.handle(message, port));
});
port.onDisconnect.addListener(() => {
console.warn('Port disconnected.');
port.disconnected = true;
handlers.forEach(h => h.disconnect?.(port));
ports.remove(port);
});
});
ØA