77 lines
2.5 KiB
TypeScript
77 lines
2.5 KiB
TypeScript
export type JsonValue =
|
|
null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
|
|
|
|
export function jsonValue(value: unknown): JsonValue {
|
|
if (value === null || typeof value === 'boolean') return value
|
|
if (typeof value === 'string') {
|
|
assertValidUnicode(value)
|
|
return value
|
|
}
|
|
if (typeof value === 'number') {
|
|
if (!Number.isFinite(value))
|
|
throw new Error('canonical JSON does not permit non-finite numbers')
|
|
return value
|
|
}
|
|
if (Array.isArray(value)) return value.map(jsonValue)
|
|
if (typeof value === 'object') {
|
|
const prototype = Object.getPrototypeOf(value)
|
|
if (prototype !== Object.prototype && prototype !== null)
|
|
throw new Error('value is not JSON')
|
|
return Object.fromEntries(
|
|
Object.entries(value).map(([key, member]) => {
|
|
assertValidUnicode(key)
|
|
return [key, jsonValue(member)]
|
|
}),
|
|
)
|
|
}
|
|
throw new Error('value is not JSON')
|
|
}
|
|
|
|
export function canonicalJson(value: JsonValue): string {
|
|
if (
|
|
value === null ||
|
|
typeof value === 'boolean' ||
|
|
typeof value === 'string'
|
|
) {
|
|
if (typeof value === 'string') assertValidUnicode(value)
|
|
return JSON.stringify(value)
|
|
}
|
|
if (typeof value === 'number') {
|
|
if (!Number.isFinite(value))
|
|
throw new Error('canonical JSON does not permit non-finite numbers')
|
|
return JSON.stringify(value)
|
|
}
|
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`
|
|
return `{${Object.keys(value)
|
|
.map((key) => {
|
|
assertValidUnicode(key)
|
|
return key
|
|
})
|
|
.sort()
|
|
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
.join(',')}}`
|
|
}
|
|
|
|
function assertValidUnicode(value: string): void {
|
|
for (let index = 0; index < value.length; index += 1) {
|
|
const codeUnit = value.charCodeAt(index)
|
|
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
const next = value.charCodeAt(index + 1)
|
|
if (index + 1 >= value.length || next < 0xdc00 || next > 0xdfff)
|
|
throw new Error('canonical JSON does not permit lone surrogates')
|
|
index += 1
|
|
} else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
|
|
throw new Error('canonical JSON does not permit lone surrogates')
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function requestFingerprint(value: JsonValue): Promise<string> {
|
|
const digest = await crypto.subtle.digest(
|
|
'SHA-256',
|
|
new TextEncoder().encode(canonicalJson(value)),
|
|
)
|
|
return Array.from(new Uint8Array(digest), (byte) =>
|
|
byte.toString(16).padStart(2, '0'),
|
|
).join('')
|
|
}
|