1const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";23function base58Encode(data: Uint8Array): string {4 let num = BigInt("0x" + Array.from(data).map(b => b.toString(16).padStart(2, "0")).join(""));5 let result = "";6 while (num > 0n) {7 const remainder = num % 58n;8 num = num / 58n;9 result = BASE58_ALPHABET[Number(remainder)] + result;10 }11 for (let i = 0; i < data.length && data[i] === 0; i++) {12 result = "1" + result;13 }14 return result;15}1617function base58Decode(str: string): Uint8Array {18 let num = 0n;19 for (const char of str) {20 const idx = BASE58_ALPHABET.indexOf(char);21 if (idx === -1) throw new Error("Invalid base58 character");22 num = num * 58n + BigInt(idx);23 }24 const hex = num.toString(16).padStart(2, "0");25 const paddedHex = hex.length % 2 ? "0" + hex : hex;26 const result = new Uint8Array(paddedHex.length / 2);27 for (let i = 0; i < paddedHex.length; i += 2) {28 result[i / 2] = parseInt(paddedHex.slice(i, i + 2), 16);29 }30 return result;31}3233function toBase58Groups(encoded: string): string[] {34 const groups: string[] = [];35 for (let i = 0; i < encoded.length; i += 12) {36 groups.push(encoded.slice(i, i + 12));37 }38 return groups;39}4041function fromBase58Groups(groups: string[]): string {42 return groups.join("");43}4445export const PUBLIC_KEY_HEX =46 "29d0f82eb3527b99635336f5e9bef4ae07f80b32de5206b312e74eb3632d3599";4748export interface LicenseInfo {49 valid: boolean;50 plan: "free" | "pro";51 issued?: Date;52}5354const LICENSE_PREFIX = "PPK-";5556async function importPublicKey(): Promise<CryptoKey> {57 const keyData = hexToBytes(PUBLIC_KEY_HEX);58 return (crypto.subtle as any).importKey(59 "raw",60 keyData,61 { name: "Ed25519" },62 true,63 ["verify"]64 );65}6667function hexToBytes(hex: string): Uint8Array {68 const bytes = new Uint8Array(hex.length / 2);69 for (let i = 0; i < hex.length; i += 2) {70 bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);71 }72 return bytes;73}7475function bytesToHex(bytes: Uint8Array): string {76 return Array.from(bytes)77 .map(b => b.toString(16).padStart(2, "0"))78 .join("");79}8081export async function validateLicense(keyString: string): Promise<LicenseInfo> {82 if (!keyString || typeof keyString !== "string") {83 return { valid: false, plan: "free" };84 }8586 const trimmed = keyString.trim();87 if (!trimmed.startsWith(LICENSE_PREFIX)) {88 return { valid: false, plan: "free" };89 }9091 try {92 const withoutPrefix = trimmed.slice(LICENSE_PREFIX.length);93 const groups = withoutPrefix.split("-");94 if (groups.length !== 5) {95 return { valid: false, plan: "free" };96 }9798 const base58String = fromBase58Groups(groups);99 const decoded = base58Decode(base58String);100101 if (decoded.length !== 76) {102 return { valid: false, plan: "free" };103 }104105 const payload = decoded.slice(0, 12);106 const signature = decoded.slice(12, 76);107108 if (payload[0] !== 0x01) {109 return { valid: false, plan: "free" };110 }111 if (payload[1] !== 0x01) {112 return { valid: false, plan: "free" };113 }114 if (payload[2] !== 0x01) {115 return { valid: false, plan: "free" };116 }117118 const issuedTimestamp = new DataView(payload.buffer, payload.byteOffset + 3, 4).getUint32(0, true);119 const issuedDate = new Date(issuedTimestamp * 86400 * 1000);120121 const publicKey = await importPublicKey();122 const isValid = await (crypto.subtle as any).verify(123 "Ed25519",124 publicKey,125 signature,126 payload127 );128129 if (!isValid) {130 return { valid: false, plan: "free" };131 }132133 return { valid: true, plan: "pro", issued: issuedDate };134 } catch {135 return { valid: false, plan: "free" };136 }137}138139export async function getLicenseInfo(): Promise<LicenseInfo> {140 try {141 const result = await chrome.storage.local.get("peach_license") as Record<string, string | undefined>;142 const stored = result["peach_license"];143 if (!stored) {144 return { valid: false, plan: "free" };145 }146 return validateLicense(stored);147 } catch {148 return { valid: false, plan: "free" };149 }150}151152export async function activateLicense(keyString: string): Promise<LicenseInfo> {153 const validated = await validateLicense(keyString);154 if (validated.valid && validated.plan === "pro") {155 await chrome.storage.local.set({ peach_license: keyString.trim() });156 return validated;157 }158 return { valid: false, plan: "free" };159}160161export async function isProUser(): Promise<boolean> {162 const info = await getLicenseInfo();163 return info.valid && info.plan === "pro";164}165166167export async function getLicenseKey(): Promise<string | null> {168 try {169 const result = await chrome.storage.local.get("peach_license") as Record<string, string | undefined>;170 return result["peach_license"] ?? null;171 } catch {172 return null;173 }174}175176export async function deactivateLicense(): Promise<void> {177 await chrome.storage.local.remove("peach_license");178}179180const CELEBRATION_KEY = "peach_pro_celebrated";181182export async function shouldPlayCelebration(): Promise<boolean> {183 const info = await getLicenseInfo();184 if (!info.valid || info.plan !== "pro") return false;185 const result = await chrome.storage.local.get(CELEBRATION_KEY) as Record<string, boolean | undefined>;186 return !result[CELEBRATION_KEY];187}188189export async function markCelebrationPlayed(): Promise<void> {190 await chrome.storage.local.set({ [CELEBRATION_KEY]: true });191}