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

152
scripts/crypto-subtle.ts Normal file
View File

@@ -0,0 +1,152 @@
/**
* Reimplementation of string-crypto v2 using SubtleCrypto (Web Crypto API)
* Works in browsers, Bun, Deno, Cloudflare Workers, etc.
*/
const KEYLEN = 256; // bits
async function deriveKey(
password: string,
salt: string = "s41t",
iterations: number = 1
): Promise<CryptoKey> {
const enc = new TextEncoder();
// Import password as raw key material
const keyMaterial = await crypto.subtle.importKey(
"raw",
enc.encode(password),
"PBKDF2",
false,
["deriveKey"]
);
// Derive AES-GCM key using PBKDF2
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: enc.encode(salt),
iterations,
hash: "SHA-512",
},
keyMaterial,
{ name: "AES-GCM", length: KEYLEN },
false,
["encrypt", "decrypt"]
);
}
export async function encryptString(
str: string,
password: string
): Promise<string> {
const key = await deriveKey(password);
const enc = new TextEncoder();
// Generate random IV
const iv = crypto.getRandomValues(new Uint8Array(16));
// Encrypt
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
enc.encode(str)
);
// SubtleCrypto appends 16-byte auth tag, but v2 string-crypto ignores it
// Strip the last 16 bytes to match v2 behavior
const encryptedWithoutTag = new Uint8Array(encrypted).slice(0, -16);
// Convert to base64, then to hex (v2 quirk)
const encryptedBase64 = btoa(
String.fromCharCode(...encryptedWithoutTag)
);
const encryptedHex = Buffer.from(encryptedBase64).toString("hex");
// IV to hex
const ivHex = Array.from(iv)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return `${ivHex}:${encryptedHex}`;
}
export async function decryptString(
encryptedStr: string,
password: string
): Promise<string> {
const key = await deriveKey(password);
const [ivHex, encryptedHex] = encryptedStr.split(":");
// Hex to IV
const iv = new Uint8Array(
ivHex.match(/.{2}/g)!.map((byte) => parseInt(byte, 16))
);
// Hex to base64 to bytes
const encryptedBase64 = Buffer.from(encryptedHex, "hex").toString();
const encryptedBytes = Uint8Array.from(atob(encryptedBase64), (c) =>
c.charCodeAt(0)
);
// Decrypt
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
encryptedBytes
);
return new TextDecoder().decode(decrypted);
}
// Test
if (import.meta.main) {
// Load .env
const envFile = Bun.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();
}
}
}
const email = process.env.ANYCLIP_EMAIL || process.env.ANYCLIP_USER;
const plaintext = process.env.ANYCLIP_PASSWORD;
if (!email || !plaintext) {
console.error("Set ANYCLIP_EMAIL and ANYCLIP_PASSWORD in .env");
process.exit(1);
}
const salt = "$2b$04$wwky7rvtr6BFNaCqntwyie";
const encrypted = await encryptString(plaintext, salt);
console.log("Encrypted:", encrypted);
// Test with API
console.log("\nTesting with AnyClip API...");
const response = await fetch(
"https://videomanager-api.anyclip.com/public/auth/login",
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password: encrypted,
}),
}
);
console.log("Status:", response.status);
if (response.ok) {
console.log("✅ Works with SubtleCrypto!");
} else {
const text = await response.text();
console.log("Response:", text);
}
}