akashyadav758
Dockerize stack, strip watermarking, add navigator monitor
c6e6dac
Raw
History Blame Contribute Delete
30.2 kB
/**
* Flow Agent β€” Chrome Extension Background Service Worker
*
* Connects to local Python agent via WebSocket (agent runs WS server).
* Captures bearer token, solves reCAPTCHA, proxies API calls through browser.
*/
const AGENT_WS_URL = 'ws://127.0.0.1:9227';
// NOTE: This is a browser-restricted public API key β€” safe to ship in extension bundles.
const API_KEY = 'AIzaSyBtrm0o5ab1c-Ec8ZuLcGt3oJAA5VWt3pY';
let ws = null;
let flowKey = null;
let callbackSecret = null; // Auth secret for HTTP callback, received from server on WS connect
let state = 'off'; // off | idle | running
let manualDisconnect = false;
let metrics = {
tokenCapturedAt: null,
requestCount: 0, // captcha-consuming requests only (gen image/video/upscale)
successCount: 0,
failedCount: 0,
lastError: null,
};
// ─── URL β†’ Log Type Classifier ─────────────────────────────
// Visible log types β€” only these appear in the request log
const _VISIBLE_TYPES = new Set(['GEN_IMG', 'GEN_VID', 'GEN_VID_REF', 'UPSCALE', 'TRACKING', 'URL_REFRESH']);
function _classifyApiUrl(url) {
if (url.includes('uploadImage')) return 'UPLOAD';
if (url.includes('batchGenerateImages')) return 'GEN_IMG';
if (url.includes('UpsampleVideo')) return 'UPSCALE';
if (url.includes('ReferenceImages')) return 'GEN_VID_REF';
if (url.includes('batchAsyncGenerateVideo')) return 'GEN_VID';
if (url.includes('batchCheckAsync')) return 'POLL';
if (url.includes('upsampleImage')) return 'UPS_IMG';
if (url.includes('/media/')) return 'MEDIA';
if (url.includes('/credits')) return 'CREDITS';
return 'API';
}
// ─── Request Log ────────────────────────────────────────────
let requestLog = [];
function addRequestLog(entry) {
requestLog.unshift(entry);
if (requestLog.length > 100) requestLog.pop();
broadcastRequestLog();
}
function updateRequestLog(id, updates) {
const entry = requestLog.find((e) => e.id === id);
if (entry) Object.assign(entry, updates);
broadcastRequestLog();
}
function broadcastRequestLog() {
chrome.runtime.sendMessage({ type: 'REQUEST_LOG_UPDATE', log: requestLog }).catch(() => {});
}
// ─── Startup ────────────────────────────────────────────────
chrome.runtime.onInstalled.addListener(init);
chrome.runtime.onStartup.addListener(init);
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === 'reconnect') connectToAgent();
if (alarm.name === 'keepAlive') keepAlive();
if (alarm.name === 'token-refresh') {
await captureTokenFromFlowTab();
}
});
async function init() {
const data = await chrome.storage.local.get(['flowKey', 'metrics', 'callbackSecret']);
if (data.flowKey) flowKey = data.flowKey;
if (data.metrics) Object.assign(metrics, data.metrics);
if (data.callbackSecret) callbackSecret = data.callbackSecret;
connectToAgent();
chrome.alarms.create('keepAlive', { periodInMinutes: 0.4 });
}
// ─── Token Capture ──────────────────────────────────────────
chrome.webRequest.onBeforeSendHeaders.addListener(
(details) => {
if (!details?.requestHeaders?.length) return;
const authHeader = details.requestHeaders.find(
(h) => h.name?.toLowerCase() === 'authorization',
);
const value = authHeader?.value || '';
if (!value.startsWith('Bearer ya29.')) return;
const token = value.replace(/^Bearer\s+/i, '').trim();
if (!token) return;
// Always update β€” even if same token string, refresh the timestamp
flowKey = token;
metrics.tokenCapturedAt = Date.now();
chrome.storage.local.set({ flowKey, metrics });
console.log('[Flow Agent] Bearer token captured');
// Notify agent
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'token_captured', flowKey }));
}
},
{ urls: ['https://aisandbox-pa.googleapis.com/*', 'https://labs.google/*'] },
['requestHeaders', 'extraHeaders'],
);
let _openingFlowTab = false;
async function captureTokenFromFlowTab() {
const tabs = await chrome.tabs.query({
url: ['https://labs.google/fx/tools/flow*', 'https://labs.google/fx/*/tools/flow*'],
});
if (!tabs.length) {
if (_openingFlowTab) {
console.log('[Flow Agent] Flow tab already opening, skipping');
return;
}
_openingFlowTab = true;
try {
console.log('[Flow Agent] No Flow tab found β€” opening one in background');
await chrome.tabs.create({ url: 'https://labs.google/fx/tools/flow', active: false });
await sleep(3000);
const retryTabs = await chrome.tabs.query({
url: ['https://labs.google/fx/tools/flow*', 'https://labs.google/fx/*/tools/flow*'],
});
if (!retryTabs.length) {
console.log('[Flow Agent] Flow tab not ready yet after open');
return;
}
await chrome.scripting.executeScript({
target: { tabId: retryTabs[0].id },
files: ['content.js'],
});
console.log('[Flow Agent] Token refresh triggered on newly opened Flow tab');
} catch (e) {
console.error('[Flow Agent] Token refresh failed after opening tab:', e);
} finally {
_openingFlowTab = false;
}
return;
}
try {
await chrome.scripting.executeScript({
target: { tabId: tabs[0].id },
files: ['content.js'],
});
console.log('[Flow Agent] Token refresh triggered on Flow tab');
} catch (e) {
console.error('[Flow Agent] Token refresh failed:', e);
}
}
// ─── WebSocket to Agent ─────────────────────────────────────
function connectToAgent() {
if (manualDisconnect) return;
if (ws?.readyState === WebSocket.CONNECTING) return;
if (ws?.readyState === WebSocket.OPEN) return;
try {
ws = new WebSocket(AGENT_WS_URL);
} catch (e) {
console.error('[Flow Agent] WS connect error:', e);
scheduleReconnect();
return;
}
ws.onopen = () => {
console.log('[Flow Agent] Connected to agent');
chrome.alarms.clear('reconnect');
setState('idle');
// Token refresh alarm β€” 45 min gives buffer before ~60 min expiry
chrome.alarms.create('token-refresh', { periodInMinutes: 45 });
// Send current state + resend token if we have one
ws.send(JSON.stringify({
type: 'extension_ready',
flowKeyPresent: !!flowKey,
tokenAge: flowKey && metrics.tokenCapturedAt ? Date.now() - metrics.tokenCapturedAt : null,
}));
if (flowKey) {
ws.send(JSON.stringify({ type: 'token_captured', flowKey }));
}
};
ws.onmessage = async ({ data }) => {
try {
const msg = JSON.parse(data);
if (msg.method === 'api_request') {
await handleApiRequest(msg);
} else if (msg.method === 'trpc_request') {
await handleTrpcRequest(msg);
} else if (msg.method === 'upload_video') {
await handleUploadVideo(msg);
} else if (msg.method === 'solve_captcha') {
await handleSolveCaptcha(msg);
} else if (msg.method === 'get_status') {
sendToAgent({
id: msg.id,
result: {
state,
flowKeyPresent: !!flowKey,
manualDisconnect,
tokenAge: metrics.tokenCapturedAt ? Date.now() - metrics.tokenCapturedAt : null,
metrics,
},
});
} else if (msg.method === 'open_flow_tab') {
// Python bridge asks us to open/focus a Flow tab
console.log('[Flow Agent] Agent requested: open Flow tab');
const tabs = await chrome.tabs.query({
url: ['https://labs.google/fx/tools/flow*', 'https://labs.google/fx/*/tools/flow*'],
});
if (tabs.length) {
// Tab exists β€” refresh it to trigger fresh API calls β†’ token capture
await chrome.tabs.reload(tabs[0].id);
console.log('[Flow Agent] Refreshed existing Flow tab');
} else {
// No tab β€” open one (active so it loads properly)
await chrome.tabs.create({ url: 'https://labs.google/fx/tools/flow', active: true });
console.log('[Flow Agent] Opened new Flow tab');
}
// Wait for page to load and make API calls that trigger token capture
await sleep(5000);
// If token was captured by webRequest during page load, send it
if (flowKey && ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'token_captured', flowKey }));
console.log('[Flow Agent] Sent stored token after tab open');
} else {
// Try reading from storage as fallback
const data = await chrome.storage.local.get(['flowKey']);
if (data.flowKey) {
flowKey = data.flowKey;
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'token_captured', flowKey }));
console.log('[Flow Agent] Sent token from storage after tab open');
}
}
}
} else if (msg.method === 'refresh_flow_tab') {
// Python bridge asks us to refresh token
console.log('[Flow Agent] Agent requested: refresh token');
await captureTokenFromFlowTab();
await sleep(3000);
// Actively send token if we have one
if (flowKey && ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'token_captured', flowKey }));
console.log('[Flow Agent] Sent token after refresh');
} else {
const data = await chrome.storage.local.get(['flowKey']);
if (data.flowKey) {
flowKey = data.flowKey;
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'token_captured', flowKey }));
console.log('[Flow Agent] Sent token from storage after refresh');
}
}
}
} else if (msg.type === 'callback_secret') {
callbackSecret = msg.secret;
chrome.storage.local.set({ callbackSecret: msg.secret });
console.log('[Flow Agent] Received callback secret');
} else if (msg.type === 'pong') {
// keepalive response
}
} catch (e) {
console.error('[Flow Agent] Message error:', e);
}
};
ws.onclose = () => {
setState('off');
chrome.alarms.clear('token-refresh');
if (!manualDisconnect) scheduleReconnect();
};
ws.onerror = (e) => {
console.error('[Flow Agent] WS error:', e);
metrics.lastError = 'WS_ERROR';
chrome.storage.local.set({ metrics });
};
}
function scheduleReconnect() {
chrome.alarms.create('reconnect', { delayInMinutes: 0.083 }); // ~5s
}
function keepAlive() {
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
} else {
connectToAgent();
}
}
function sendToAgent(msg) {
// API responses (with msg.id) go via HTTP β€” immune to WS disconnect
if (msg.id) {
fetch('http://127.0.0.1:8100/api/ext/callback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(msg),
}).catch(() => {
// HTTP failed β€” fallback to WS
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
});
return;
}
// Non-response messages (ping, status) or no secret yet β€” use WS
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(msg));
}
}
// ─── reCAPTCHA Solving ──────────────────────────────────────
async function requestCaptchaFromTab(tabId, requestId, pageAction) {
try {
return await chrome.tabs.sendMessage(tabId, {
type: 'GET_CAPTCHA',
requestId,
pageAction,
});
} catch (error) {
const msg = error?.message || '';
const shouldInject =
msg.includes('Receiving end does not exist') ||
msg.includes('Could not establish connection');
if (!shouldInject) throw error;
// Inject content script and retry
await chrome.scripting.executeScript({
target: { tabId },
files: ['content.js'],
});
await sleep(200);
return await chrome.tabs.sendMessage(tabId, {
type: 'GET_CAPTCHA',
requestId,
pageAction,
});
}
}
async function solveCaptcha(requestId, captchaAction) {
const tabs = await chrome.tabs.query({
url: ['https://labs.google/fx/tools/flow*', 'https://labs.google/fx/*/tools/flow*'],
});
if (!tabs.length) {
// Auto-open Flow tab and wait briefly before returning error
try {
await chrome.tabs.create({ url: 'https://labs.google/fx/tools/flow', active: false });
await sleep(3000);
// Retry tab query after opening
const retryTabs = await chrome.tabs.query({
url: ['https://labs.google/fx/tools/flow*', 'https://labs.google/fx/*/tools/flow*'],
});
if (!retryTabs.length) return { error: 'NO_FLOW_TAB' };
const resp = await Promise.race([
requestCaptchaFromTab(retryTabs[0].id, requestId, captchaAction),
new Promise((_, rej) => setTimeout(() => rej(new Error('CAPTCHA_TIMEOUT')), 30000)),
]);
return resp;
} catch (e) {
return { error: e.message || 'NO_FLOW_TAB' };
}
}
try {
const resp = await Promise.race([
requestCaptchaFromTab(tabs[0].id, requestId, captchaAction),
new Promise((_, rej) => setTimeout(() => rej(new Error('CAPTCHA_TIMEOUT')), 30000)),
]);
return resp;
} catch (e) {
return { error: e.message };
}
}
async function handleSolveCaptcha(msg) {
const { id, params } = msg;
const result = await solveCaptcha(id, params?.captchaAction || 'VIDEO_GENERATION');
// Standalone captcha solve counts as captcha-consuming
metrics.requestCount++;
if (result?.token) {
metrics.successCount++;
} else {
metrics.failedCount++;
metrics.lastError = result?.error || 'NO_TOKEN';
}
chrome.storage.local.set({ metrics });
sendToAgent({ id, result });
}
// ─── API Request Proxy ──────────────────────────────────────
async function handleTrpcRequest(msg) {
const { id, params } = msg;
const { url, method = 'POST', headers = {}, body } = params;
if (!url || !url.startsWith('https://labs.google/')) {
sendToAgent({ id, error: 'INVALID_TRPC_URL' });
return;
}
setState('running');
// TRPC calls don't consume captcha β€” don't count in metrics
const logId = id;
const logType = url.includes('createProject') ? 'CREATE_PROJECT' : 'TRPC';
// TRPC calls are silent β€” don't show in request log
const fetchHeaders = { 'Content-Type': 'application/json', ...headers };
if (flowKey) {
fetchHeaders['authorization'] = `Bearer ${flowKey}`;
}
try {
const resp = await fetch(url, {
method,
headers: fetchHeaders,
body: body ? JSON.stringify(body) : undefined,
credentials: 'include',
});
const data = await resp.json();
chrome.storage.local.set({ metrics });
updateRequestLog(logId, { status: 'success' });
sendToAgent({ id, status: resp.status, data });
} catch (e) {
console.error('[Flow Agent] tRPC request failed:', e);
chrome.storage.local.set({ metrics });
updateRequestLog(logId, { status: 'failed', error: e.message || 'TRPC_FETCH_FAILED' });
sendToAgent({ id, error: e.message || 'TRPC_FETCH_FAILED' });
} finally {
setState('idle');
}
}
async function handleUploadVideo(msg) {
const { id, params } = msg;
const { videoBase64, projectId, videoSize } = params;
try {
const tabs = await chrome.tabs.query({ url: '*://labs.google/*' });
if (!tabs.length) {
sendToAgent({ id, error: 'NO_FLOW_TAB' });
return;
}
const size = videoSize || (videoBase64 ? Math.floor(videoBase64.length * 3 / 4) : 0);
// Get session URL via page context XHR (needs session cookies)
const startResults = await chrome.scripting.executeScript({
target: { tabId: tabs[0].id },
world: 'MAIN',
func: (projId, sz) => {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/fx/api/upload-video?action=start');
xhr.setRequestHeader('X-Upload-Project-Id', projId);
xhr.setRequestHeader('X-Upload-Content-Type', 'video/mp4');
xhr.setRequestHeader('X-Upload-Content-Length', sz.toString());
xhr.withCredentials = true;
xhr.onload = () => {
let data;
try { data = JSON.parse(xhr.responseText); } catch { data = {}; }
resolve({
sessionUrl: data.sessionUrl || xhr.getResponseHeader('X-Upload-Session-Url') || '',
status: xhr.status,
});
};
xhr.onerror = () => resolve({ error: 'POST_FAILED' });
xhr.send();
});
},
args: [projectId, size],
});
const step1 = startResults?.[0]?.result;
if (!step1 || step1.error || !step1.sessionUrl) {
sendToAgent({ id, error: step1?.error || 'NO_SESSION_URL' });
return;
}
// Return sessionUrl + token β€” caller handles PUT
sendToAgent({
id,
result: {
sessionUrl: step1.sessionUrl,
token: flowKey || '',
},
});
} catch (e) {
sendToAgent({ id, error: `UPLOAD_ERROR: ${e.message}` });
}
}
async function handleApiRequest(msg) {
const { id, params } = msg;
const { url, method, headers, body, captchaAction } = params;
if (!url) {
sendToAgent({ id, error: 'MISSING_URL' });
return;
}
if (!url.startsWith('https://aisandbox-pa.googleapis.com/')) {
sendToAgent({ id, error: 'INVALID_URL' });
return;
}
setState('running');
const hasCaptcha = !!captchaAction;
if (hasCaptcha) metrics.requestCount++;
const logId = id;
const logType = _classifyApiUrl(url);
if (_VISIBLE_TYPES.has(logType)) {
const payloadSummary = body ? JSON.stringify(body).slice(0, 200) : null;
addRequestLog({ id: logId, type: logType, time: new Date().toISOString(), status: 'processing', error: null, outputUrl: null, url, payloadSummary });
}
try {
// Step 1: Solve captcha if needed
let captchaToken = null;
if (captchaAction) {
const captchaResult = await solveCaptcha(id, captchaAction);
captchaToken = captchaResult?.token || null;
if (!captchaToken) {
// Cannot proceed without captcha β€” API will 403
const err = captchaResult?.error || 'CAPTCHA_FAILED';
console.error(`[Flow Agent] Captcha failed for ${captchaAction}: ${err}`);
sendToAgent({ id, status: 403, error: `CAPTCHA_FAILED: ${err}` });
if (hasCaptcha) { metrics.failedCount++; metrics.lastError = `CAPTCHA_FAILED: ${err}`; }
chrome.storage.local.set({ metrics });
updateRequestLog(logId, { status: 'failed', error: `CAPTCHA_FAILED: ${err}` });
setState('idle');
return;
}
}
// Step 2: Inject captcha token into body
let finalBody = body;
if (captchaToken && finalBody) {
finalBody = JSON.parse(JSON.stringify(finalBody)); // deep clone
if (finalBody.clientContext?.recaptchaContext) {
finalBody.clientContext.recaptchaContext.token = captchaToken;
}
if (finalBody.requests && Array.isArray(finalBody.requests)) {
for (const req of finalBody.requests) {
if (req.clientContext?.recaptchaContext) {
req.clientContext.recaptchaContext.token = captchaToken;
}
}
}
}
// Step 3: Use flowKey for auth
const activeFlowKey = flowKey;
if (!activeFlowKey) {
sendToAgent({ id, status: 503, error: 'NO_FLOW_KEY' });
if (hasCaptcha) { metrics.failedCount++; metrics.lastError = 'NO_FLOW_KEY'; }
chrome.storage.local.set({ metrics });
updateRequestLog(logId, { status: 'failed', error: 'NO_FLOW_KEY' });
setState('idle');
return;
}
const fetchHeaders = { ...(headers || {}) };
fetchHeaders['authorization'] = `Bearer ${activeFlowKey}`;
// Step 4: Make the API call from browser context
const response = await fetch(url, {
method: method || 'POST',
headers: fetchHeaders,
credentials: 'include',
body: method === 'GET' ? undefined : JSON.stringify(finalBody),
});
let responseData;
const responseText = await response.text();
try {
responseData = JSON.parse(responseText);
} catch {
responseData = responseText;
}
sendToAgent({
id,
status: response.status,
data: responseData,
});
const responseSummary = responseText ? responseText.slice(0, 300) : null;
if (response.ok) {
if (hasCaptcha) { metrics.successCount++; metrics.lastError = null; }
updateRequestLog(logId, { status: 'success', httpStatus: response.status, responseSummary });
} else {
if (hasCaptcha) { metrics.failedCount++; metrics.lastError = `API_${response.status}`; }
updateRequestLog(logId, { status: 'failed', error: `API_${response.status}`, httpStatus: response.status, responseSummary });
}
} catch (e) {
sendToAgent({
id,
status: 500,
error: e.message || 'API_REQUEST_FAILED',
});
if (hasCaptcha) { metrics.failedCount++; metrics.lastError = e.message; }
updateRequestLog(logId, { status: 'failed', error: e.message || 'API_REQUEST_FAILED' });
}
chrome.storage.local.set({ metrics });
setState('idle');
}
// ─── State & Popup ──────────────────────────────────────────
function setState(newState) {
state = newState;
const badges = { idle: '●', running: 'β–Ά', off: 'β—‹' };
const colors = { idle: '#22c55e', running: '#f59e0b', off: '#6b7280' };
chrome.action.setBadgeText({ text: badges[state] || '' });
chrome.action.setBadgeBackgroundColor({ color: colors[state] || '#000' });
broadcastStatus();
}
function broadcastStatus() {
chrome.runtime.sendMessage({ type: 'STATUS_PUSH' }).catch(() => {});
}
chrome.runtime.onMessage.addListener((msg, _, reply) => {
if (msg.type === 'STATUS') {
reply({
connected: ws?.readyState === WebSocket.OPEN,
agentConnected: ws?.readyState === WebSocket.OPEN,
flowKeyPresent: !!flowKey,
manualDisconnect,
tokenAge: metrics.tokenCapturedAt ? Date.now() - metrics.tokenCapturedAt : null,
metrics: {
requestCount: metrics.requestCount,
successCount: metrics.successCount,
failedCount: metrics.failedCount,
lastError: metrics.lastError,
},
state,
});
}
if (msg.type === 'DISCONNECT') {
manualDisconnect = true;
if (ws) ws.close();
reply({ ok: true });
return true;
}
if (msg.type === 'RECONNECT') {
manualDisconnect = false;
connectToAgent();
reply({ ok: true });
return true;
}
if (msg.type === 'REQUEST_LOG') {
reply({ log: requestLog });
return true;
}
if (msg.type === 'OPEN_FLOW_TAB') {
chrome.tabs.query({
url: ['https://labs.google/fx/tools/flow*', 'https://labs.google/fx/*/tools/flow*'],
}).then((tabs) => {
if (tabs.length) {
chrome.tabs.update(tabs[0].id, { active: true });
reply({ ok: true, tabId: tabs[0].id });
} else {
chrome.tabs.create({ url: 'https://labs.google/fx/tools/flow' })
.then((tab) => reply({ ok: true, tabId: tab.id }))
.catch((e) => reply({ error: e.message }));
}
}).catch((e) => reply({ error: e.message }));
return true;
}
if (msg.type === 'REFRESH_TOKEN') {
captureTokenFromFlowTab()
.then(() => reply({ ok: true }))
.catch((e) => reply({ error: e.message }));
return true;
}
if (msg.type === 'TEST_CAPTCHA') {
solveCaptcha(`test-${Date.now()}`, msg.pageAction || 'IMAGE_GENERATION')
.then((r) => reply(r))
.catch((e) => reply({ error: e.message }));
return true;
}
if (msg.type === 'TRPC_MEDIA_URLS') {
handleTrpcMediaUrls(msg.trpcUrl, msg.body);
reply({ ok: true });
return true;
}
if (msg.type === 'SNIFFED_AISANDBOX_REQUEST') {
console.log('[Flow Agent] SNIFFED aisandbox request:', msg.url);
fetch('http://127.0.0.1:8100/api/ext/callback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'sniffed_video_request',
url: msg.url,
method: msg.method,
payload: msg.payload,
timestamp: msg.timestamp,
}),
}).catch((e) => console.error('[Flow Agent] Failed to forward sniffed request:', e));
reply({ ok: true });
return true;
}
return true;
});
// ─── TRPC Media URL Extractor ──────────────────────────────
function handleTrpcMediaUrls(trpcUrl, bodyText) {
try {
// Extract all fresh GCS signed URLs
const urlRegex = /https:\/\/storage\.googleapis\.com\/ai-sandbox-videofx\/(?:image|video)\/[0-9a-f-]{36}\?[^"'\s]+/g;
const matches = bodyText.match(urlRegex) || [];
if (!matches.length) return;
// Deduplicate and parse
const urlMap = {};
for (const rawUrl of matches) {
// Unescape JSON-escaped URLs
const url = rawUrl.replace(/\\u0026/g, '&').replace(/\\/g, '');
const mediaMatch = url.match(/\/(image|video)\/([0-9a-f-]{36})\?/);
if (mediaMatch) {
const [, mediaType, mediaId] = mediaMatch;
// Keep last occurrence (freshest)
urlMap[mediaId] = { mediaType, url, mediaId };
}
}
const entries = Object.values(urlMap);
if (!entries.length) return;
console.log(`[Flow Agent] Captured ${entries.length} fresh media URLs from TRPC`);
// URL refresh is silent β€” don't show in request log
// Forward to agent for DB update
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'media_urls_refresh',
urls: entries,
}));
}
} catch (e) {
console.error('[Flow Agent] Failed to extract TRPC media URLs:', e);
}
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
// ─── Human-like Telemetry ──────────────────────────────────
// Periodically send tracking events to Google's analytics endpoints
// to mimic normal browser behavior.
const _UA = navigator.userAgent;
let _telemetrySessionId = `;${Date.now()}`;
function _rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
function _buildBatchLogPayload() {
const events = [];
const types = ['FLOW_IMAGE_LATENCY', 'FLOW_VIDEO_LATENCY'];
const count = _rand(1, 3);
for (let i = 0; i < count; i++) {
events.push({
event: types[_rand(0, types.length - 1)],
eventProperties: [
{ key: 'CURRENT_TIME_MS', doubleValue: Date.now() },
{ key: 'DURATION_MS', doubleValue: _rand(150, 800) },
{ key: 'USER_AGENT', stringValue: _UA },
{ key: 'IS_DESKTOP', booleanValue: true },
],
eventMetadata: { sessionId: _telemetrySessionId },
eventTime: new Date().toISOString(),
});
}
return { appEvents: events };
}
function _buildFrontendEventsPayload() {
const eventTypes = [
'FLOW_IMAGE_LATENCY', 'FLOW_VIDEO_LATENCY', 'GRID_SCROLL_DEPTH',
'FLOW_PROJECT_OPEN', 'FLOW_SCENE_VIEW',
];
const count = _rand(1, 4);
const events = [];
for (let i = 0; i < count; i++) {
const et = eventTypes[_rand(0, eventTypes.length - 1)];
const params = {
USER_AGENT: { '@type': 'type.googleapis.com/google.protobuf.StringValue', value: _UA },
IS_DESKTOP: { '@type': 'type.googleapis.com/google.protobuf.StringValue', value: 'true' },
};
if (et.includes('LATENCY')) {
params.CURRENT_TIME_MS = { '@type': 'type.googleapis.com/google.protobuf.StringValue', value: String(Date.now()) };
params.DURATION_MS = { '@type': 'type.googleapis.com/google.protobuf.StringValue', value: String(_rand(100, 600)) };
}
if (et === 'GRID_SCROLL_DEPTH') {
params.MEDIA_GENERATION_PAYGATE_TIER = { '@type': 'type.googleapis.com/google.protobuf.StringValue', value: 'PAYGATE_TIER_TWO' };
}
events.push({
eventType: et,
metadata: {
sessionId: _telemetrySessionId,
createTime: new Date().toISOString(),
additionalParams: params,
},
});
}
return { events };
}
async function sendTelemetry() {
if (!flowKey || state === 'off') return;
const headers = {
'Content-Type': 'text/plain;charset=UTF-8',
'authorization': `Bearer ${flowKey}`,
};
// Telemetry is silent β€” don't show in request log
try {
if (Math.random() < 0.5) {
await fetch(`https://aisandbox-pa.googleapis.com/v1:batchLog`, {
method: 'POST', headers, credentials: 'include',
body: JSON.stringify(_buildBatchLogPayload()),
});
} else {
await fetch(`https://aisandbox-pa.googleapis.com/v1/flow:batchLogFrontendEvents`, {
method: 'POST', headers, credentials: 'include',
body: JSON.stringify(_buildFrontendEventsPayload()),
});
}
} catch {}
}
// Send telemetry at random intervals (45-120s) to look organic
function scheduleTelemetry() {
const delay = _rand(45, 120) * 1000;
setTimeout(async () => {
await sendTelemetry();
scheduleTelemetry(); // reschedule with new random interval
}, delay);
}
// Refresh session ID every ~30min like a real user
setInterval(() => { _telemetrySessionId = `;${Date.now()}`; }, _rand(25, 35) * 60 * 1000);
scheduleTelemetry();
console.log('[Flow Agent] Extension loaded');