/** Детерминированная JSON-сериализация для подписи (должна совпадать с `app/shared/license/canonicalJson.ts` в клиенте DNDGamePlayer). */ export function canonicalJson(value) { if (value === null) return 'null'; const t = typeof value; if (t === 'number') { if (!Number.isFinite(value)) throw new TypeError('non-finite number in license payload'); return JSON.stringify(value); } if (t === 'string' || t === 'boolean') return JSON.stringify(value); if (Array.isArray(value)) { return `[${value.map((x) => canonicalJson(x)).join(',')}]`; } if (t === 'object') { const keys = Object.keys(value).sort(); return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(',')}}`; } throw new TypeError(`unsupported type: ${t}`); }