// Mint PRO unlock keys for RecipesCam. Dev-side only — never bundled. // node tools/keygen.mjs 5 mint 5 keys // node tools/keygen.mjs --check mint 3 keys and verify them (parity guard) // The key is 12 payload chars + 4 checksum chars, so any 12-char payload works. // Keep KEY_ALPHABET / KEY_SECRET / checksum identical to // src/utils/entitlement.ts — the app verifies with the same maths. import { randomInt } from 'node:crypto'; const KEY_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; const KEY_SECRET = 'recipes-cam-key-2026'; const checksum = (payload) => { let h = 0x811c9dc5; // FNV-1a 32 const src = KEY_SECRET + payload; for (let i = 0; i < src.length; i++) { h ^= src.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; } return h & 0xfffff; }; const keyIsValid = (raw) => { const s = (raw || '').toUpperCase().replace(/[^A-Z0-9]/g, ''); if (s.length !== 16) return false; for (let i = 0; i < 16; i++) if (KEY_ALPHABET.indexOf(s[i]) < 0) return false; const bits = checksum(s.slice(0, 12)); for (let i = 0; i < 4; i++) { if (KEY_ALPHABET[(bits >>> (i * 5)) & 31] !== s[12 + i]) return false; } return true; }; const mint = () => { let payload = ''; for (let i = 0; i < 12; i++) payload += KEY_ALPHABET[randomInt(KEY_ALPHABET.length)]; const bits = checksum(payload); let check = ''; for (let i = 0; i < 4; i++) check += KEY_ALPHABET[(bits >>> (i * 5)) & 31]; const s = payload + check; return s.replace(/(.{4})/g, '$1-').slice(0, -1); }; const args = process.argv.slice(2); if (args[0] === '--check') { const keys = [mint(), mint(), mint()]; for (const k of keys) { if (!keyIsValid(k)) throw new Error(`keygen/verify mismatch on ${k}`); if (keyIsValid(k.slice(0, -1) + (k.endsWith('A') ? 'B' : 'A'))) { throw new Error(`tampered key accepted: ${k}`); } } console.log('ok', keys.join(' ')); } else { const n = Number(args[0]) || 1; for (let i = 0; i < n; i++) console.log(mint()); }