#!/usr/bin/env bun /** * AnyClip Authentication Script * * Automates login to AnyClip and returns session credentials. * * Usage: * bun scripts/auth.ts * * 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 { return encryptString(password, PASS_CRYPTO_SALT); } /** * Step 1: Login to external API */ async function loginExternal( email: string, password: string ): Promise { 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 { 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 { // 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 { return { Accept: "application/json", "Content-Type": "application/json", Cookie: session.cookies, }; } /** * Make an authenticated GraphQL request */ export async function graphqlRequest( session: AuthSession, query: string, variables: Record = {} ): Promise { 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 "); 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); } }