feat: add core contracts

This commit is contained in:
chungyeong
2026-05-09 22:45:44 +09:00
parent 42f0fb193d
commit 44103839af
11 changed files with 377 additions and 0 deletions

74
packages/core/src/hash.ts Normal file
View File

@@ -0,0 +1,74 @@
import { createHash } from "node:crypto";
type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
export function canonicalize(value: unknown): string {
return renderCanonical(assertJsonValue(value));
}
export function hash(value: unknown): string {
return createHash("sha256").update(canonicalize(value)).digest("hex");
}
function renderCanonical(value: JsonValue): string {
if (value === null || typeof value === "boolean" || typeof value === "string") {
return JSON.stringify(value);
}
if (typeof value === "number") {
return JSON.stringify(value);
}
if (Array.isArray(value)) {
return `[${value.map((item) => renderCanonical(item)).join(",")}]`;
}
return `{${Object.keys(value)
.sort()
.map((key) => `${JSON.stringify(key)}:${renderCanonical(value[key] as JsonValue)}`)
.join(",")}}`;
}
function assertJsonValue(value: unknown): JsonValue {
if (
value === null ||
typeof value === "boolean" ||
typeof value === "string" ||
Array.isArray(value)
) {
if (Array.isArray(value)) {
return value.map((item) => assertJsonValue(item));
}
return value;
}
if (typeof value === "number") {
if (!Number.isFinite(value)) {
throw new TypeError("Cannot canonicalize non-finite numbers");
}
return value;
}
if (typeof value === "object") {
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) {
throw new TypeError("Cannot canonicalize non-plain object");
}
const objectValue: Record<string, JsonValue> = {};
for (const [key, childValue] of Object.entries(value as Record<string, unknown>)) {
if (childValue === undefined) {
throw new TypeError(`Cannot canonicalize undefined at key ${key}`);
}
objectValue[key] = assertJsonValue(childValue);
}
return objectValue;
}
throw new TypeError(`Cannot canonicalize ${typeof value}`);
}