Add TypeScript domain and contract bindings

This commit is contained in:
ms2k 2026-07-15 21:00:44 +09:00
commit fecb9a1a8d

View file

@ -0,0 +1,77 @@
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('')
}