export class NaNNotAllowedError extends Error { constructor() { super('NaN is not allowed'); } } export class InfinityNotAllowedError extends Error { constructor() { super('Infinity is not allowed'); } } /** * JSON canonicalize function. * Creates crypto safe predictable canocalization of JSON as defined by RFC8785. * * @see https://tools.ietf.org/html/rfc8785 * @see https://www.rfc-editor.org/rfc/rfc8785 * * @example Primitive values * ```ts * import { canonicalize } from '@graph-framework/utils' * * console.log(canonicalize(null)) // 'null' * console.log(canonicalize(1)) // '1' * console.log(canonicalize("test")) // "string" * console.log(canonicalize(true)) // 'true' * ``` * * @example Objects * ``` * import { canonicalize } from '@graph-framework/utils' * * const json = { * from_account: '543 232 625-3', * to_account: '321 567 636-4', * amount: 500, * currency: 'USD', * }; * console.log(canonicalize(json)) // '{"amount":500,"currency":"USD","from_account":"543 232 625-3","to_account":"321 567 636-4"}' * ``` * * @example Arrays * ```ts * import { canonicalize } from '@graph-framework/utils' * * console.log(canonicalize([1, 'text', null, true, false])) // '[1,"text",null,true,false]' * ``` * * @param object object to JSC canonicalize * @throws NaNNotAllowedError if given object is of type number, but is not a valid number * @throws InfinityNotAllowedError if given object is of type number, but is the infinite number */ export function canonicalize(object: T): string { if (typeof object === 'number' && Number.isNaN(object)) { throw new NaNNotAllowedError(); } if (typeof object === 'number' && !Number.isFinite(object)) { throw new InfinityNotAllowedError(); } if (object === null || typeof object !== 'object') { return JSON.stringify(object); } // biome-ignore lint/suspicious/noExplicitAny: typeof T is unknown, cast to any to check if ((object as any).toJSON instanceof Function) { // biome-ignore lint/suspicious/noExplicitAny: typeof T is unknown, cast to any to check return canonicalize((object as any).toJSON()); } if (Array.isArray(object)) { const values = object.reduce((t, cv) => { if (cv === undefined || typeof cv === 'symbol') { return t; // Skip undefined and symbol values entirely } const comma = t.length === 0 ? '' : ','; return `${t}${comma}${canonicalize(cv)}`; }, ''); return `[${values}]`; } const values = Object.keys(object) .sort() .reduce((t, cv) => { // biome-ignore lint/suspicious/noExplicitAny: typeof T is unknown, cast to any to check if ((object as any)[cv] === undefined || typeof (object as any)[cv] === 'symbol') { return t; } const comma = t.length === 0 ? '' : ','; // biome-ignore lint/suspicious/noExplicitAny: typeof T is unknown, cast to any to check return `${t}${comma}${canonicalize(cv)}:${canonicalize((object as any)[cv])}`; }, ''); return `{${values}}`; }