59e2719245
LITE previews every look and feature but export/render is blocked while a PRO look is in use, and every LITE export carries the RECIPESCAM mark. Keys are generated offline (FNV-1a checksum over a fixed secret, 16 chars from a no-ambiguity alphabet) and activate one machine or a thousand: nothing is bound to a device, so revoking a leaked key needs Play Billing plus server validation. CREATE, FAVORITED, the star chip and the TopBar + SAVE now answer with a PRO prompt instead of a silent no-op; CREATE still drafts and applies the look so LITE can try it, it just cannot persist. The SETTINGS card also lifts above the keyboard now: RN Modal is its own dialog window, so the IME never resizes or pans it and the UNLOCK row was sitting under the keys.
57 lines
1.9 KiB
JavaScript
57 lines
1.9 KiB
JavaScript
// 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());
|
|
}
|