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 31 identifier?: number;32}3334const LICENSE_PREFIX = "PPK-";35const LICENSE_REVOCATIONS_KEY = "peach_license_revocations_v1";36const LICENSE_PAYLOAD_LENGTH = 12;3738export function generateLicenseIdentifier(): number {39 const bytes = new Uint32Array(1);40 crypto.getRandomValues(bytes);41 return bytes[0];42}4344export async function revokeLicenseIdentifier(identifier: number): Promise<void> {45 if (!Number.isSafeInteger(identifier) || identifier < 0 || identifier > 0xffffffff) {46 throw new Error("invalid license identifier");47 }48 const result = await chrome.storage.local.get(LICENSE_REVOCATIONS_KEY) as Record<string, unknown>;49 const current = Array.isArray(result[LICENSE_REVOCATIONS_KEY])50 ? result[LICENSE_REVOCATIONS_KEY].filter((value): value is number => Number.isSafeInteger(value))51 : [];52 if (!current.includes(identifier)) current.push(identifier);53 await chrome.storage.local.set({ [LICENSE_REVOCATIONS_KEY]: current });54}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}7475export async function validateLicense(keyString: string): Promise<LicenseInfo> {76 if (!keyString || typeof keyString !== "string") {77 return { valid: false, plan: "free" };78 }7980 const trimmed = keyString.trim();81 if (!trimmed.startsWith(LICENSE_PREFIX)) {82 return { valid: false, plan: "free" };83 }8485 try {86 const withoutPrefix = trimmed.slice(LICENSE_PREFIX.length);87 const groups = withoutPrefix.split("-");88 if (groups.length !== 5) {89 return { valid: false, plan: "free" };90 }91 const decoded = base58Decode(fromBase58Groups(groups));92 if (decoded.length !== 76) {93 return { valid: false, plan: "free" };94 }9596 const payload = decoded.slice(0, LICENSE_PAYLOAD_LENGTH);97 const signature = decoded.slice(LICENSE_PAYLOAD_LENGTH, 76);9899 if (payload[0] !== 0x01 || payload[1] !== 0x01 || payload[2] !== 0x01) {100 return { valid: false, plan: "free" };101 }102103 const issuedTimestamp = new DataView(payload.buffer, payload.byteOffset + 3, 4).getUint32(0, true);104 const issuedDate = new Date(issuedTimestamp * 86400 * 1000);105 const identifier = new DataView(payload.buffer, payload.byteOffset + 7, 4).getUint32(0, true);106107 const publicKey = await importPublicKey();108 const isValid = await (crypto.subtle as any).verify(109 "Ed25519",110 publicKey,111 signature,112 payload113 );114115 if (!isValid) {116 return { valid: false, plan: "free" };117 }118119 const revocations = await chrome.storage.local.get(LICENSE_REVOCATIONS_KEY) as Record<string, unknown>;120 const revoked = Array.isArray(revocations[LICENSE_REVOCATIONS_KEY]) &&121 revocations[LICENSE_REVOCATIONS_KEY].some((value) => value === identifier);122 if (revoked) return { valid: false, plan: "free", identifier };123124 return { valid: true, plan: "pro", issued: issuedDate, identifier };125 } catch {126 return { valid: false, plan: "free" };127 }128}129130export async function getLicenseInfo(): Promise<LicenseInfo> {131 try {132 const result = await chrome.storage.local.get("peach_license") as Record<string, string | undefined>;133 const stored = result["peach_license"];134 if (!stored) {135 return { valid: false, plan: "free" };136 }137 return validateLicense(stored);138 } catch {139 return { valid: false, plan: "free" };140 }141}142143const CELEBRATION_KEY = "peach_pro_celebrated";144145export async function activateLicense(keyString: string): Promise<LicenseInfo> {146 const validated = await validateLicense(keyString);147 if (validated.valid && validated.plan === "pro") {148 149 150 151 await chrome.storage.local.set({152 peach_license: keyString.trim(),153 [CELEBRATION_KEY]: false,154 });155 return validated;156 }157 return { valid: false, plan: "free" };158}159160export async function isProUser(): Promise<boolean> {161 const info = await getLicenseInfo();162 return info.valid && info.plan === "pro";163}164165166export async function getLicenseKey(): Promise<string | null> {167 try {168 const result = await chrome.storage.local.get("peach_license") as Record<string, string | undefined>;169 return result["peach_license"] ?? null;170 } catch {171 return null;172 }173}174175export async function deactivateLicense(): Promise<void> {176 await chrome.storage.local.remove("peach_license");177}178179export async function shouldPlayCelebration(): Promise<boolean> {180 const info = await getLicenseInfo();181 if (!info.valid || info.plan !== "pro") return false;182 const result = await chrome.storage.local.get(CELEBRATION_KEY) as Record<string, boolean | undefined>;183 return !result[CELEBRATION_KEY];184}185186export async function markCelebrationPlayed(): Promise<void> {187 await chrome.storage.local.set({ [CELEBRATION_KEY]: true });188}