Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ When you interact with these AI providers through Panelize, you are subject to t

### Cookie Access

Panelize does not access cookie data directly and does not use the cookies API. Provider websites may use your existing browser session cookies when they load inside panels, just like in a normal tab.
Provider websites use your existing browser session cookies when they load inside panels. Because Chromium omits `SameSite=Lax` and `SameSite=Strict` cookies on WebSocket handshakes initiated from embedded extension panels, Panelize uses the `cookies` API locally in your browser to forward existing `grok.com` session cookies to Grok's own `wss://grok.com/*` WebSocket requests made from Panelize's Multi-Panel tabs. The forwarding applies only while the Multi-Panel page is the top-level page of a tab; it does not apply when another website embeds a Panelize page. The forwarding rule lasts only for the browser session; cookie values are not saved in Panelize's settings or sent to Panelize-operated servers or unrelated third parties.

Panelize also preserves your existing Grok cookie consent choices by setting the `OptanonAlertBoxClosed` and `OptanonConsent` cookies to `SameSite=None; Secure`, allowing Grok to read and save those choices inside its embedded panel. This happens only when you open Grok in the Multi-Panel page. This preserves the choice you make in Grok's cookie controls and does not automatically accept cookies on your behalf.

### Update Checks

Expand Down Expand Up @@ -155,6 +157,7 @@ Panelize requests the following Chrome extension permissions:
| Permission | Purpose |
|------------|---------|
| `storage` | Store your prompts and settings locally |
| `cookies` | Attach existing `grok.com` session cookies to Grok's embedded WebSocket connections and preserve your Grok cookie consent choices inside Panelize |
| `contextMenus` | Add "Send to Panelize" option when right-clicking |
| `declarativeNetRequest` | Allow AI provider websites to load in embedded panels (bypass X-Frame-Options) |
| `declarativeNetRequestWithHostAccess` | Apply header modifications for specific AI provider domains |
Expand Down
70 changes: 69 additions & 1 deletion background/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ import { removeDeprecatedClaudeModelMode } from '../modules/settings.js';
import {
reconcileOptionalProviderAccess
} from '../modules/optional-provider-access.js';
import {
GROK_WEBSOCKET_COOKIE_SYNC_ACTION,
createGrokWebSocketCookieReconcileScheduler,
handleGrokTabNavigationComplete,
handleGrokTabNavigationStart,
handleGrokTabRemoved,
handleMultiPanelSyncRequest,
isGrokWebSocketCookieChange,
isMultiPanelTopFrameSender,
reconcileGrokWebSocketCookies
} from '../modules/grok-websocket-cookies.js';

// Install event - setup context menus
const DEFAULT_SHORTCUT_SETTING = { keyboardShortcutEnabled: true };
Expand Down Expand Up @@ -126,19 +137,28 @@ function scheduleOptionalProviderReconcile() {
return optionalProviderReconcileQueue;
}

const scheduleGrokWebSocketCookieReconcile = createGrokWebSocketCookieReconcileScheduler(
reconcileGrokWebSocketCookies,
(error) => {
console.error('[Background] Failed to synchronize Grok WebSocket cookies:', error);
}
);

chrome.runtime.onInstalled.addListener(async (details) => {
await migrateProviderSettingsForUpdate(details);
if (details?.reason === 'update') {
await removeDeprecatedClaudeModelMode();
}
await scheduleOptionalProviderReconcile();
await scheduleGrokWebSocketCookieReconcile();
await createContextMenus();
await loadShortcutSetting();
await loadOpenModeSetting();
});

chrome.runtime.onStartup.addListener(async () => {
await scheduleOptionalProviderReconcile();
await scheduleGrokWebSocketCookieReconcile();
await loadShortcutSetting();
await loadOpenModeSetting();
});
Expand Down Expand Up @@ -172,9 +192,45 @@ chrome.storage.onChanged.addListener((changes, namespace) => {

if (namespace === 'sync' && changes.enabledProviders) {
scheduleOptionalProviderReconcile();
scheduleGrokWebSocketCookieReconcile();
}
});

if (chrome.cookies?.onChanged) {
chrome.cookies.onChanged.addListener((changeInfo) => {
if (isGrokWebSocketCookieChange(changeInfo)) {
scheduleGrokWebSocketCookieReconcile();
}
});
}

if (chrome.tabs?.onUpdated) {
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (changeInfo?.status === 'loading') {
handleGrokTabNavigationStart(tabId).catch((error) => {
console.error('[Background] Failed to drop navigating tab from Grok cookie rule:', error);
scheduleGrokWebSocketCookieReconcile();
});
} else if (changeInfo?.status === 'complete' && handleGrokTabNavigationComplete(tabId)) {
scheduleGrokWebSocketCookieReconcile();
}
});
}

if (chrome.tabs?.onRemoved) {
chrome.tabs.onRemoved.addListener((tabId) => {
handleGrokTabRemoved(tabId)
.then((inRule) => {
if (inRule) {
scheduleGrokWebSocketCookieReconcile();
}
})
.catch((error) => {
console.error('[Background] Failed to handle closed Grok tab:', error);
});
});
}

if (chrome.permissions?.onAdded) {
chrome.permissions.onAdded.addListener(() => {
scheduleOptionalProviderReconcile();
Expand Down Expand Up @@ -295,7 +351,19 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
handleFetchLatestCommit().then(sendResponse);
return true; // Keep channel open for async response
}
return true;

if (message.action === GROK_WEBSOCKET_COOKIE_SYNC_ACTION) {
if (!isMultiPanelTopFrameSender(sender)) {
sendResponse({ success: false, error: 'Grok cookie sync is only available to the Multi-Panel page' });
return false;
}

handleMultiPanelSyncRequest(sender.tab.id);
scheduleGrokWebSocketCookieReconcile({ upgradeConsentCookies: true }).then(sendResponse);
return true;
}

return false;
});

// Handle version check by fetching latest commit from GitHub API
Expand Down
60 changes: 60 additions & 0 deletions content-scripts/grok-cookie-consent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Ensure OneTrust cookie consent choices (OptanonAlertBoxClosed, OptanonConsent)
// written via document.cookie inside an embedded Grok iframe use SameSite=None; Secure
// so Chromium's cross-site script cookie check accepts and persists them.
(() => {
if (window.top === window.self) {
return;
}

// Only rewrite inside extension pages such as the Multi-Panel page. Grok
// frames embedded by ordinary websites (or by a website framing the
// web-accessible Multi-Panel page) keep the browser's default behavior.
const ancestorOrigins = location.ancestorOrigins;
const topOrigin = ancestorOrigins?.length ? ancestorOrigins[ancestorOrigins.length - 1] : '';
if (!topOrigin.startsWith('chrome-extension://')) {
return;
}

const OPTANON_COOKIE_PATTERN = /^\s*Optanon(?:AlertBoxClosed|Consent)=/;

function rewriteGrokConsentCookieString(cookieString) {
if (typeof cookieString !== 'string' || !OPTANON_COOKIE_PATTERN.test(cookieString)) {
return cookieString;
}

if (typeof location !== 'undefined' && location.protocol !== 'https:') {
return cookieString;
}

let rewritten = cookieString.replace(/;\s*SameSite=(?:Lax|Strict)/gi, '; SameSite=None');
if (!/;\s*SameSite=None\b/i.test(rewritten)) {
rewritten += '; SameSite=None';
}
if (!/;\s*Secure\b/i.test(rewritten)) {
rewritten += '; Secure';
}
return rewritten;
}

// Patch the prototype that actually owns the accessor (Document.prototype in
// Chromium) so no own property further down the chain shadows the override.
let owner = Object.getPrototypeOf(document);
while (owner && !Object.prototype.hasOwnProperty.call(owner, 'cookie')) {
owner = Object.getPrototypeOf(owner);
}
const descriptor = owner && Object.getOwnPropertyDescriptor(owner, 'cookie');
if (!descriptor || typeof descriptor.get !== 'function' || typeof descriptor.set !== 'function') {
return;
}

Object.defineProperty(owner, 'cookie', {
configurable: true,
enumerable: descriptor.enumerable ?? true,
get() {
return descriptor.get.call(this);
},
set(value) {
return descriptor.set.call(this, rewriteGrokConsentCookieString(value));
}
});
})();
13 changes: 13 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"default_locale": "en",
"permissions": [
"storage",
"cookies",
"contextMenus",
"activeTab",
"scripting",
Expand All @@ -18,6 +19,7 @@
"*://claude.ai/*",
"*://gemini.google.com/*",
"*://grok.com/*",
"wss://grok.com/*",
"*://chat.deepseek.com/*",
"*://www.kimi.com/*",
"*://kimi.com/*",
Expand Down Expand Up @@ -114,6 +116,17 @@
"run_at": "document_start",
"all_frames": true
},
{
"matches": [
"https://grok.com/*"
],
"js": [
"content-scripts/grok-cookie-consent.js"
],
"run_at": "document_start",
"all_frames": true,
"world": "MAIN"
},
{
"matches": [
"https://grok.com/*"
Expand Down
Loading
Loading