// src/share/encodePreset.ts
// Round-trip a preset through msgpack + base64url so it can live in a URL
// fragment (#p=…). The decoder validates against the canonical schema so an
// outdated or hand-crafted link can never poison the running engine.
import { decode, encode } from '@msgpack/msgpack';
import type { Preset } from '~/engine/types';
import { PresetSchema } from '~/presets/schema';
function b64urlFromBytes(bytes: Uint8Array): string {
let bin = '';
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i] ?? 0);
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function bytesFromB64url(s: string): Uint8Array {
const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4));
const bin = atob(s.replace(/-/g, '+').replace(/_/g, '/') + pad);
const out = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
return out;
}
export function encodePreset(preset: Preset): string {
// Shareable copy: always demote builtIn so the recipient treats it as custom.
const payload = { ...preset, builtIn: false };
return b64urlFromBytes(encode(payload));
}
export function decodePreset(token: string): Preset | null {
try {
const decoded = decode(bytesFromB64url(token));
const result = PresetSchema.safeParse(decoded);
if (!result.success) return null;
return { ...(result.data as Preset), builtIn: false };
} catch {
return null;
}
}
/** Builds a full https://… URL for sharing a preset. */
export function shareUrl(preset: Preset): string {
const { origin, pathname } = window.location;
return `${origin}${pathname}#p=${encodePreset(preset)}`;
}
/** Reads `#p=…` from the current URL and returns the decoded preset, if any. */
export function readPresetFromHash(): Preset | null {
const hash = window.location.hash;
if (!hash.startsWith('#p=')) return null;
return decodePreset(hash.slice(3));
}
/** Removes the `#p=…` fragment without triggering a navigation. */
export function clearPresetHash(): void {
if (!window.location.hash.startsWith('#p=')) return;
const { origin, pathname, search } = window.location;
window.history.replaceState(null, '', `${origin}${pathname}${search}`);
}