Add TypeScript domain and contract bindings
This commit is contained in:
parent
4da5a980ed
commit
fecb9a1a8d
1 changed files with 1553 additions and 1 deletions
77
web/src/domain/fingerprint.ts
Normal file
77
web/src/domain/fingerprint.ts
Normal 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('')
|
||||
}
|
||||
Loading…
Reference in a new issue