- 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)
276 lines
6.9 KiB
TypeScript
276 lines
6.9 KiB
TypeScript
#!/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);
|
|
}
|
|
}
|