license.ts

4.9 KB · raw

1const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";23function base58Decode(str: string): Uint8Array {4  let num = 0n;5  for (const char of str) {6    const idx = BASE58_ALPHABET.indexOf(char);7    if (idx === -1) throw new Error("Invalid base58 character");8    num = num * 58n + BigInt(idx);9  }10  const hex = num.toString(16).padStart(2, "0");11  const paddedHex = hex.length % 2 ? "0" + hex : hex;12  const result = new Uint8Array(paddedHex.length / 2);13  for (let i = 0; i < paddedHex.length; i += 2) {14    result[i / 2] = parseInt(paddedHex.slice(i, i + 2), 16);15  }16  return result;17}1819function fromBase58Groups(groups: string[]): string {20  return groups.join("");21}2223export const PUBLIC_KEY_HEX =24  "c7dc52d2c484696cf8566dbe6cfe69025d114cb47f6cf53c25088ecfd539bdfa";2526export interface LicenseInfo {27  valid: boolean;28  plan: "free" | "pro";29  issued?: Date;30}3132const LICENSE_PREFIX = "PPK-";3334async function importPublicKey(): Promise<CryptoKey> {35  const keyData = hexToBytes(PUBLIC_KEY_HEX);36  return (crypto.subtle as any).importKey(37    "raw",38    keyData,39    { name: "Ed25519" },40    true,41    ["verify"]42  );43}4445function hexToBytes(hex: string): Uint8Array {46  const bytes = new Uint8Array(hex.length / 2);47  for (let i = 0; i < hex.length; i += 2) {48    bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);49  }50  return bytes;51}5253export async function validateLicense(keyString: string): Promise<LicenseInfo> {54  if (!keyString || typeof keyString !== "string") {55    return { valid: false, plan: "free" };56  }5758  const trimmed = keyString.trim();59  if (!trimmed.startsWith(LICENSE_PREFIX)) {60    return { valid: false, plan: "free" };61  }6263  try {64    const withoutPrefix = trimmed.slice(LICENSE_PREFIX.length);65    const groups = withoutPrefix.split("-");66    if (groups.length !== 5) {67      return { valid: false, plan: "free" };68    }6970    const base58String = fromBase58Groups(groups);71    const decoded = base58Decode(base58String);7273    if (decoded.length !== 76) {74      return { valid: false, plan: "free" };75    }7677    const payload = decoded.slice(0, 12);78    const signature = decoded.slice(12, 76);7980    if (payload[0] !== 0x01) {81      return { valid: false, plan: "free" };82    }83    if (payload[1] !== 0x01) {84      return { valid: false, plan: "free" };85    }86    if (payload[2] !== 0x01) {87      return { valid: false, plan: "free" };88    }8990    const issuedTimestamp = new DataView(payload.buffer, payload.byteOffset + 3, 4).getUint32(0, true);91    const issuedDate = new Date(issuedTimestamp * 86400 * 1000);9293    const publicKey = await importPublicKey();94    const isValid = await (crypto.subtle as any).verify(95      "Ed25519",96      publicKey,97      signature,98      payload99    );100101    if (!isValid) {102      return { valid: false, plan: "free" };103    }104105    return { valid: true, plan: "pro", issued: issuedDate };106  } catch {107    return { valid: false, plan: "free" };108  }109}110111export async function getLicenseInfo(): Promise<LicenseInfo> {112  try {113    const result = await chrome.storage.local.get("peach_license") as Record<string, string | undefined>;114    const stored = result["peach_license"];115    if (!stored) {116      return { valid: false, plan: "free" };117    }118    return validateLicense(stored);119  } catch {120    return { valid: false, plan: "free" };121  }122}123124const CELEBRATION_KEY = "peach_pro_celebrated";125126export async function activateLicense(keyString: string): Promise<LicenseInfo> {127  const validated = await validateLicense(keyString);128  if (validated.valid && validated.plan === "pro") {129    // Reset the celebration flag so every new license activation triggers130    // the full-screen Pro celebration in the popup, even on devices that131    // have celebrated before (e.g., testers who activate repeatedly).132    await chrome.storage.local.set({133      peach_license: keyString.trim(),134      [CELEBRATION_KEY]: false,135    });136    return validated;137  }138  return { valid: false, plan: "free" };139}140141export async function isProUser(): Promise<boolean> {142  const info = await getLicenseInfo();143  return info.valid && info.plan === "pro";144}145146/** Returns the raw stored license key string, or null if none. */147export async function getLicenseKey(): Promise<string | null> {148  try {149    const result = await chrome.storage.local.get("peach_license") as Record<string, string | undefined>;150    return result["peach_license"] ?? null;151  } catch {152    return null;153  }154}155156export async function deactivateLicense(): Promise<void> {157  await chrome.storage.local.remove("peach_license");158}159160export async function shouldPlayCelebration(): Promise<boolean> {161  const info = await getLicenseInfo();162  if (!info.valid || info.plan !== "pro") return false;163  const result = await chrome.storage.local.get(CELEBRATION_KEY) as Record<string, boolean | undefined>;164  return !result[CELEBRATION_KEY];165}166167export async function markCelebrationPlayed(): Promise<void> {168  await chrome.storage.local.set({ [CELEBRATION_KEY]: true });169}