peachScript.ts
1/**2 * peachScript.ts — PeachScript encoding/decoding.3 *4 * PeachScript is the device-to-device export format. Each byte of an5 * encrypted vault payload maps to one Unicode Private Use Area code point6 * in U+E000..U+E0FF. The bundled `PeachScript-Regular.otf` renders those7 * 256 code points as the visual script. Outside Peach the string renders8 * as boxes — that is the desired property: PeachScript is meant to be9 * read by Peach, not by humans.10 *11 * The encoding is trivially reversible: byte 0 → U+E000, byte 255 →12 * U+E0FF. No padding, no base conversion, no alignment fix-ups. The font13 * does the visual work; this module is the codec.14 */1516const BASE = 0xe000;1718/** Encode a binary payload to a PeachScript string (one byte per code point). */19export function encodePeachScript(bytes: Uint8Array): string {20 const out: string[] = [];21 for (let i = 0; i < bytes.length; i++) {22 out.push(String.fromCodePoint(BASE + bytes[i]));23 }24 return out.join("");25}2627/**28 * Decode a PeachScript string back to bytes. Throws29 * `INVALID_PEACHSCRIPT_CHARACTER` when any character is outside the30 * allotted PUA range — i.e. the string was edited, truncated mid-pair,31 * or is not PeachScript at all.32 */33export function decodePeachScript(str: string): Uint8Array {34 const codePoints = Array.from(str);35 const bytes = new Uint8Array(codePoints.length);36 for (let i = 0; i < codePoints.length; i++) {37 const cp = codePoints[i].codePointAt(0)!;38 if (cp < BASE || cp > BASE + 0xff) {39 throw new Error("INVALID_PEACHSCRIPT_CHARACTER");40 }41 bytes[i] = cp - BASE;42 }43 return bytes;44}4546/** Strip whitespace from a PeachScript string before decoding. */47export function normalisePeachScript(str: string): string {48 return str.replace(/\s+/g, "");49}5051/**52 * Cheap pre-flight: every non-whitespace char must be in the PUA range and53 * the string must be non-empty. Used by the import UI to gate the decode54 * button without throwing.55 */56export function isPeachScript(str: string): boolean {57 const trimmed = normalisePeachScript(str);58 if (trimmed.length === 0) return false;59 for (const ch of trimmed) {60 const cp = ch.codePointAt(0)!;61 if (cp < BASE || cp > BASE + 0xff) return false;62 }63 return true;64}