- Add authentication scripts with SubtleCrypto password encryption - Add sourcemap extraction pipeline (update-urls, download-sourcemaps, extract-sources) - Add Playwright API interception script for monetization endpoints - Document two-step auth flow with JWT tokens and dual cookies - Move extracted source from root to anyclip/ directory - Add project configuration (.env.example, .gitignore, CLAUDE.md)
245 lines
6.3 KiB
TypeScript
245 lines
6.3 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* AnyClip Monetization API Interception Script
|
|
*
|
|
* This script launches a visible browser, allows manual login,
|
|
* and captures all API traffic on the monetization page.
|
|
*
|
|
* Usage: bun scripts/intercept-monetization.ts
|
|
*
|
|
* Press Ctrl+C to stop and save captured data.
|
|
*/
|
|
|
|
import { chromium, type Page, type Request, type Response } from "playwright";
|
|
import { writeFileSync } from "fs";
|
|
|
|
interface CapturedRequest {
|
|
timestamp: string;
|
|
url: string;
|
|
method: string;
|
|
headers: Record<string, string>;
|
|
requestBody: string | null;
|
|
responseStatus: number | null;
|
|
responseHeaders: Record<string, string> | null;
|
|
responseBody: string | null;
|
|
duration: number | null;
|
|
}
|
|
|
|
const capturedRequests: CapturedRequest[] = [];
|
|
const pendingRequests = new Map<
|
|
Request,
|
|
{ startTime: number; captured: CapturedRequest }
|
|
>();
|
|
|
|
const OUTPUT_FILE = "captured-apis.json";
|
|
const BASE_URL = "https://videomanager.anyclip.com";
|
|
const MONETIZATION_PATH = "/analytics-new/monetization";
|
|
|
|
// Filter for API requests we care about
|
|
function isApiRequest(url: string): boolean {
|
|
// Capture GraphQL, REST API calls, and data endpoints
|
|
const apiPatterns = [
|
|
"/graphql",
|
|
"/api/",
|
|
"/analytics",
|
|
"/monetization",
|
|
"/v1/",
|
|
"/v2/",
|
|
"anyclip.com",
|
|
];
|
|
|
|
// Exclude static assets
|
|
const excludePatterns = [
|
|
".js",
|
|
".css",
|
|
".png",
|
|
".jpg",
|
|
".jpeg",
|
|
".gif",
|
|
".svg",
|
|
".ico",
|
|
".woff",
|
|
".woff2",
|
|
".ttf",
|
|
".eot",
|
|
"/static/",
|
|
"fonts.googleapis.com",
|
|
"google-analytics.com",
|
|
"googletagmanager.com",
|
|
];
|
|
|
|
const urlLower = url.toLowerCase();
|
|
|
|
if (excludePatterns.some((pattern) => urlLower.includes(pattern))) {
|
|
return false;
|
|
}
|
|
|
|
return apiPatterns.some((pattern) => urlLower.includes(pattern));
|
|
}
|
|
|
|
function saveCaptures(): void {
|
|
const output = {
|
|
capturedAt: new Date().toISOString(),
|
|
totalRequests: capturedRequests.length,
|
|
requests: capturedRequests,
|
|
};
|
|
|
|
writeFileSync(OUTPUT_FILE, JSON.stringify(output, null, 2));
|
|
console.log(`\n✅ Saved ${capturedRequests.length} requests to ${OUTPUT_FILE}`);
|
|
}
|
|
|
|
async function handleRequest(request: Request): Promise<void> {
|
|
const url = request.url();
|
|
|
|
if (!isApiRequest(url)) {
|
|
return;
|
|
}
|
|
|
|
const captured: CapturedRequest = {
|
|
timestamp: new Date().toISOString(),
|
|
url,
|
|
method: request.method(),
|
|
headers: request.headers(),
|
|
requestBody: request.postData() || null,
|
|
responseStatus: null,
|
|
responseHeaders: null,
|
|
responseBody: null,
|
|
duration: null,
|
|
};
|
|
|
|
pendingRequests.set(request, {
|
|
startTime: Date.now(),
|
|
captured,
|
|
});
|
|
|
|
console.log(`📤 ${request.method()} ${url}`);
|
|
}
|
|
|
|
async function handleResponse(response: Response): Promise<void> {
|
|
const request = response.request();
|
|
const pending = pendingRequests.get(request);
|
|
|
|
if (!pending) {
|
|
return;
|
|
}
|
|
|
|
const { startTime, captured } = pending;
|
|
|
|
captured.responseStatus = response.status();
|
|
captured.responseHeaders = response.headers();
|
|
captured.duration = Date.now() - startTime;
|
|
|
|
try {
|
|
const contentType = response.headers()["content-type"] || "";
|
|
if (
|
|
contentType.includes("application/json") ||
|
|
contentType.includes("text/")
|
|
) {
|
|
captured.responseBody = await response.text();
|
|
} else {
|
|
captured.responseBody = `[Binary content: ${contentType}]`;
|
|
}
|
|
} catch (error) {
|
|
captured.responseBody = `[Error reading response: ${error}]`;
|
|
}
|
|
|
|
capturedRequests.push(captured);
|
|
pendingRequests.delete(request);
|
|
|
|
console.log(
|
|
`📥 ${response.status()} ${captured.url} (${captured.duration}ms)`
|
|
);
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
console.log("🚀 AnyClip Monetization API Interceptor");
|
|
console.log("=========================================\n");
|
|
|
|
console.log("Launching browser...");
|
|
const browser = await chromium.launch({
|
|
headless: false,
|
|
args: ["--start-maximized"],
|
|
});
|
|
|
|
const context = await browser.newContext({
|
|
viewport: null, // Use full window size
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
|
|
// Set up request/response interception
|
|
page.on("request", handleRequest);
|
|
page.on("response", handleResponse);
|
|
|
|
// Navigate to AnyClip
|
|
console.log(`\nNavigating to ${BASE_URL}...`);
|
|
await page.goto(BASE_URL);
|
|
|
|
console.log("\n" + "=".repeat(50));
|
|
console.log("👤 Please log in manually in the browser window");
|
|
console.log("=".repeat(50) + "\n");
|
|
|
|
// Wait for login - user must navigate to a protected page
|
|
console.log("Waiting for authentication...");
|
|
console.log("(Navigate to any page after logging in, or the script will auto-detect)\n");
|
|
|
|
// Wait for URL to change to a protected route (not login/logout)
|
|
await page.waitForFunction(
|
|
() => {
|
|
const path = window.location.pathname;
|
|
// Must be on a real app page, not login/logout/root
|
|
const protectedRoutes = [
|
|
"/dashboard",
|
|
"/analytics",
|
|
"/video",
|
|
"/library",
|
|
"/settings",
|
|
"/playlists",
|
|
"/players",
|
|
];
|
|
return protectedRoutes.some((route) => path.startsWith(route));
|
|
},
|
|
{ timeout: 300000 } // 5 minute timeout for login
|
|
);
|
|
|
|
console.log("✅ Authentication detected!\n");
|
|
|
|
// Navigate to monetization page
|
|
const monetizationUrl = `${BASE_URL}${MONETIZATION_PATH}`;
|
|
console.log(`Navigating to ${monetizationUrl}...`);
|
|
|
|
try {
|
|
await page.goto(monetizationUrl, { waitUntil: "domcontentloaded" });
|
|
} catch (error) {
|
|
// Navigation might fail if page redirects - that's okay
|
|
console.log("Navigation completed (may have redirected)");
|
|
}
|
|
|
|
// Wait for page to stabilize
|
|
await page.waitForTimeout(2000);
|
|
await page.waitForLoadState("networkidle").catch(() => {});
|
|
|
|
console.log("\n" + "=".repeat(50));
|
|
console.log("📊 Now capturing API traffic on the Monetization page");
|
|
console.log(" Interact with filters, date ranges, etc.");
|
|
console.log(" Press Ctrl+C when done to save captured data");
|
|
console.log("=".repeat(50) + "\n");
|
|
|
|
// Handle Ctrl+C gracefully
|
|
process.on("SIGINT", async () => {
|
|
console.log("\n\n🛑 Stopping capture...");
|
|
saveCaptures();
|
|
await browser.close();
|
|
process.exit(0);
|
|
});
|
|
|
|
// Keep the script running
|
|
await new Promise(() => {}); // Never resolves - keeps script alive
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error("Error:", error);
|
|
saveCaptures();
|
|
process.exit(1);
|
|
});
|