Add AnyClip integration tools and extracted source code

- 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)
This commit is contained in:
2026-01-21 10:36:51 +08:00
parent d4fe4800e6
commit e32d475aa9
3463 changed files with 184648 additions and 64341 deletions

275
scripts/auth.ts Normal file
View File

@@ -0,0 +1,275 @@
#!/usr/bin/env bun
/**
* AnyClip Authentication Script
*
* Automates login to AnyClip and returns session credentials.
*
* Usage:
* bun scripts/auth.ts <email> <password>
*
* Or import and use programmatically:
* import { login, getAuthHeaders } from './scripts/auth';
*/
// Load .env file
import { file } from "bun";
const envFile = file(".env");
if (await envFile.exists()) {
const envContent = await envFile.text();
for (const line of envContent.split("\n")) {
const [key, ...valueParts] = line.split("=");
if (key && valueParts.length) {
process.env[key.trim()] = valueParts.join("=").trim();
}
}
}
import { encryptString } from "./crypto-subtle";
const PASS_CRYPTO_SALT = "$2b$04$wwky7rvtr6BFNaCqntwyie";
const EXTERNAL_API = "https://videomanager-api.anyclip.com";
const MAIN_API = "https://videomanager.anyclip.com";
export interface LoginResponse {
cookieName: string;
cookieValue: string;
token: string;
user: string; // base64 encoded JSON
}
export interface AuthSession {
cookies: string; // Both cookies combined
anyclipCookie: string;
sessionCookie: string;
token: string;
user: DecodedUser;
}
export interface DecodedUser {
id: number;
firstName: string;
lastName: string;
email?: string;
roleId: number;
accountId: number;
role: {
id: number;
name: string;
displayName: string;
type: string;
};
account: {
id: number;
name: string;
publisherId?: number;
publishers?: Array<{ id: number; name: string }>;
};
permissions: string[];
publisherIds: number[];
publisherId: number;
slug: string;
}
/**
* Encrypt password using AnyClip's client-side encryption (SubtleCrypto)
*/
export async function encryptPassword(password: string): Promise<string> {
return encryptString(password, PASS_CRYPTO_SALT);
}
/**
* Step 1: Login to external API
*/
async function loginExternal(
email: string,
password: string
): Promise<LoginResponse> {
const encryptedPassword = await encryptPassword(password);
const response = await fetch(`${EXTERNAL_API}/public/auth/login`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password: encryptedPassword,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`External login failed: ${response.status} - ${error}`);
}
return response.json();
}
/**
* Step 2: Login to main app (returns session cookie)
*/
async function loginMain(
token: string,
cookieName: string,
cookieValue: string
): Promise<string> {
const response = await fetch(`${MAIN_API}/api/auth/login`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
token,
tcname: cookieName,
tcvalue: cookieValue,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Main login failed: ${response.status} - ${error}`);
}
// Extract session cookie from Set-Cookie header
const setCookies = response.headers.getSetCookie?.() || [];
const sessionCookie = setCookies
.find((c) => c.startsWith("session="))
?.split(";")[0];
if (!sessionCookie) {
throw new Error("No session cookie returned from main login");
}
return sessionCookie;
}
/**
* Decode the base64-encoded user object
*/
export function decodeUser(base64User: string): DecodedUser {
const json = Buffer.from(base64User, "base64").toString("utf-8");
return JSON.parse(json);
}
/**
* Full login flow - returns session credentials
*/
export async function login(
email: string,
password: string
): Promise<AuthSession> {
// Step 1: External API login
const externalResponse = await loginExternal(email, password);
const anyclipCookie = `${externalResponse.cookieName}=${externalResponse.cookieValue}`;
// Step 2: Main app login - get session cookie
const sessionCookie = await loginMain(
externalResponse.token,
externalResponse.cookieName,
externalResponse.cookieValue
);
// Decode user
const user = decodeUser(externalResponse.user);
return {
cookies: `${anyclipCookie}; ${sessionCookie}`,
anyclipCookie,
sessionCookie,
token: externalResponse.token,
user,
};
}
/**
* Get headers for authenticated API requests
*/
export function getAuthHeaders(session: AuthSession): Record<string, string> {
return {
Accept: "application/json",
"Content-Type": "application/json",
Cookie: session.cookies,
};
}
/**
* Make an authenticated GraphQL request
*/
export async function graphqlRequest<T = unknown>(
session: AuthSession,
query: string,
variables: Record<string, unknown> = {}
): Promise<T> {
const response = await fetch(`${MAIN_API}/api/graphql`, {
method: "POST",
headers: getAuthHeaders(session),
body: JSON.stringify({ query, variables }),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`GraphQL request failed: ${response.status} - ${error}`);
}
const result = await response.json();
if (result.errors) {
throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
}
return result.data;
}
// CLI usage
if (import.meta.main) {
const [emailArg, passwordArg] = process.argv.slice(2);
const email =
emailArg || process.env.ANYCLIP_EMAIL || process.env.ANYCLIP_USER;
const password = passwordArg || process.env.ANYCLIP_PASSWORD;
if (!email || !password) {
console.error("Usage: bun scripts/auth.ts <email> <password>");
console.error(" Or: set ANYCLIP_EMAIL and ANYCLIP_PASSWORD env vars");
process.exit(1);
}
try {
console.log("Logging in...");
const session = await login(email, password);
console.log("\n✅ Login successful!\n");
console.log("User:", session.user.firstName, session.user.lastName);
console.log("Account:", session.user.account?.name);
console.log("Role:", session.user.role?.displayName);
console.log("\nCookies:", session.cookies.substring(0, 80) + "...");
console.log("\nJWT Token:", session.token.substring(0, 50) + "...");
// Save session to file for other scripts
const sessionFile = "session.json";
await Bun.write(
sessionFile,
JSON.stringify(
{
cookies: session.cookies,
anyclipCookie: session.anyclipCookie,
sessionCookie: session.sessionCookie.substring(0, 100) + "...",
token: session.token,
user: {
id: session.user.id,
name: `${session.user.firstName} ${session.user.lastName}`,
accountId: session.user.accountId,
publisherId: session.user.publisherId,
},
},
null,
2
)
);
console.log(`\nSession saved to ${sessionFile}`);
} catch (error) {
console.error("Login failed:", error);
process.exit(1);
}
}