704 lines
24 KiB
JavaScript
704 lines
24 KiB
JavaScript
const SCHEMA_VERSION = "dashboard_data_v1";
|
|
const APPROVED_PARTITIONS = Object.freeze(["train", "tune", "calibrate"]);
|
|
const APPROVED_TARGET = "first-attempt next-episode binary non-pass rate";
|
|
const REQUIRED_CALIBRATION_MODELS = Object.freeze([
|
|
"hist_gradient_boosting_platt",
|
|
"logistic_platt",
|
|
]);
|
|
const ENVELOPE_KEYS = Object.freeze([
|
|
"development_preview",
|
|
"population_estimate_allowed",
|
|
"schema_version",
|
|
]);
|
|
const SHA256_INITIAL_STATE = Object.freeze([
|
|
0x6a09e667,
|
|
0xbb67ae85,
|
|
0x3c6ef372,
|
|
0xa54ff53a,
|
|
0x510e527f,
|
|
0x9b05688c,
|
|
0x1f83d9ab,
|
|
0x5be0cd19,
|
|
]);
|
|
const SHA256_ROUND_CONSTANTS = Object.freeze([
|
|
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
|
|
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
|
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
|
|
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
|
|
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
|
|
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
|
|
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
|
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
]);
|
|
|
|
export const REQUIRED_ASSETS = Object.freeze({
|
|
manifest: "data_manifest.json",
|
|
overview: "overview_period_county.json",
|
|
ageRisk: "age_risk_curve.json",
|
|
scorecards: "cohort_scorecard.json",
|
|
diagnostics: "model_diagnostics.json",
|
|
coverage: "coverage_quality.json",
|
|
filters: "filter_catalog.json",
|
|
shaManifest: "sha256_manifest.json",
|
|
});
|
|
|
|
const SENSITIVE_KEY_PARTS = new Set([
|
|
"address",
|
|
"certificate",
|
|
"credential",
|
|
"email",
|
|
"inspector",
|
|
"internal",
|
|
"ip",
|
|
"owner",
|
|
"password",
|
|
"pid",
|
|
"plate",
|
|
"prediction",
|
|
"probability",
|
|
"raw",
|
|
"secret",
|
|
"session",
|
|
"station",
|
|
"technician",
|
|
"token",
|
|
"user",
|
|
"vin",
|
|
"zip",
|
|
]);
|
|
|
|
const ASSET_VALIDATORS = {
|
|
overview: (value) => validateRows(value, validateOverviewRow, "overview_period_county"),
|
|
ageRisk: (value) => validateRows(value, validateAgeRiskRow, "age_risk_curve"),
|
|
scorecards: (value) => validateRows(value, validateScorecardRow, "cohort_scorecard"),
|
|
diagnostics: (value) => validateRows(value, validateDiagnosticRow, "model_diagnostics"),
|
|
coverage: (value) => validateRows(value, validateCoverageRow, "coverage_quality"),
|
|
filters: validateFilterCatalog,
|
|
shaManifest: validateShaManifest,
|
|
};
|
|
|
|
export class DataContractError extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = "DataContractError";
|
|
}
|
|
}
|
|
|
|
function isPlainObject(value) {
|
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
const prototype = Object.getPrototypeOf(value);
|
|
return prototype === Object.prototype || prototype === null;
|
|
}
|
|
|
|
function requirePlainObject(value, label) {
|
|
if (!isPlainObject(value)) {
|
|
throw new DataContractError(`${label} must be a JSON object.`);
|
|
}
|
|
}
|
|
|
|
function requireExactKeys(value, expectedKeys, label) {
|
|
const observed = Object.keys(value).sort();
|
|
const expected = [...expectedKeys].sort();
|
|
if (JSON.stringify(observed) !== JSON.stringify(expected)) {
|
|
throw new DataContractError(`${label} does not match the approved field schema.`);
|
|
}
|
|
}
|
|
|
|
function requireExactStringSet(value, expectedValues, label) {
|
|
validateStringArray(value, label);
|
|
const observed = [...value].sort();
|
|
const expected = [...expectedValues].sort();
|
|
if (JSON.stringify(observed) !== JSON.stringify(expected)) {
|
|
throw new DataContractError(`${label} does not match the approved values.`);
|
|
}
|
|
}
|
|
|
|
function requireBoolean(value, label) {
|
|
if (typeof value !== "boolean") {
|
|
throw new DataContractError(`${label} must be true or false.`);
|
|
}
|
|
}
|
|
|
|
function requireString(value, label) {
|
|
if (typeof value !== "string" || value.trim() === "") {
|
|
throw new DataContractError(`${label} must be a non-empty string.`);
|
|
}
|
|
}
|
|
|
|
function requireSafeModelVersion(value, label) {
|
|
requireString(value, label);
|
|
if (!/^[A-Za-z0-9._-]{1,64}$/.test(value)) {
|
|
throw new DataContractError(`${label} is not an approved model version.`);
|
|
}
|
|
}
|
|
|
|
function requireNumber(value, label, { min = -Infinity, max = Infinity } = {}) {
|
|
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
|
|
throw new DataContractError(`${label} must be a finite number in the approved range.`);
|
|
}
|
|
}
|
|
|
|
function requireInteger(value, label, { min = -Infinity, max = Infinity } = {}) {
|
|
requireNumber(value, label, { min, max });
|
|
if (!Number.isInteger(value)) {
|
|
throw new DataContractError(`${label} must be an integer.`);
|
|
}
|
|
}
|
|
|
|
function validateEnvelope(value, label) {
|
|
requirePlainObject(value, label);
|
|
if (value.schema_version !== SCHEMA_VERSION) {
|
|
throw new DataContractError(`${label} uses an unsupported schema version.`);
|
|
}
|
|
requireBoolean(value.development_preview, `${label}.development_preview`);
|
|
requireBoolean(
|
|
value.population_estimate_allowed,
|
|
`${label}.population_estimate_allowed`,
|
|
);
|
|
rejectSensitiveKeys(value, label);
|
|
}
|
|
|
|
function validateRows(value, rowValidator, label) {
|
|
validateEnvelope(value, label);
|
|
requireExactKeys(value, [...ENVELOPE_KEYS, "rows"], label);
|
|
if (!Array.isArray(value.rows)) {
|
|
throw new DataContractError(`${label}.rows must be an array.`);
|
|
}
|
|
value.rows.forEach((row, index) => {
|
|
requirePlainObject(row, `${label}.rows[${index}]`);
|
|
rowValidator(row, `${label}.rows[${index}]`);
|
|
});
|
|
return value;
|
|
}
|
|
|
|
function validateOverviewRow(row, label) {
|
|
requireExactKeys(
|
|
row,
|
|
["year", "quarter", "public_county", "support_rounded", "nonpass_rate"],
|
|
label,
|
|
);
|
|
requireInteger(row.year, `${label}.year`, { min: 2010, max: 2100 });
|
|
requireInteger(row.quarter, `${label}.quarter`, { min: 1, max: 4 });
|
|
requireString(row.public_county, `${label}.public_county`);
|
|
requireInteger(row.support_rounded, `${label}.support_rounded`, { min: 0 });
|
|
requireNumber(row.nonpass_rate, `${label}.nonpass_rate`, { min: 0, max: 1 });
|
|
}
|
|
|
|
function validateAgeRiskRow(row, label) {
|
|
requireExactKeys(row, ["age_band", "support_rounded", "nonpass_rate"], label);
|
|
requireString(row.age_band, `${label}.age_band`);
|
|
requireInteger(row.support_rounded, `${label}.support_rounded`, { min: 0 });
|
|
requireNumber(row.nonpass_rate, `${label}.nonpass_rate`, { min: 0, max: 1 });
|
|
}
|
|
|
|
function validateScorecardRow(row, label) {
|
|
requireExactKeys(
|
|
row,
|
|
["prior_make", "prior_model", "support_rounded", "nonpass_rate"],
|
|
label,
|
|
);
|
|
requireString(row.prior_make, `${label}.prior_make`);
|
|
requireString(row.prior_model, `${label}.prior_model`);
|
|
requireInteger(row.support_rounded, `${label}.support_rounded`, { min: 0 });
|
|
requireNumber(row.nonpass_rate, `${label}.nonpass_rate`, { min: 0, max: 1 });
|
|
}
|
|
|
|
function validateDiagnosticRow(row, label) {
|
|
requireExactKeys(
|
|
row,
|
|
[
|
|
"model",
|
|
"partition",
|
|
"average_precision",
|
|
"brier",
|
|
"log_loss",
|
|
"roc_auc",
|
|
"top_10_capture",
|
|
],
|
|
label,
|
|
);
|
|
requireString(row.model, `${label}.model`);
|
|
requireString(row.partition, `${label}.partition`);
|
|
if (!APPROVED_PARTITIONS.includes(row.partition)) {
|
|
throw new DataContractError(`${label}.partition is not approved for this public schema.`);
|
|
}
|
|
for (const metric of [
|
|
"average_precision",
|
|
"brier",
|
|
"log_loss",
|
|
"roc_auc",
|
|
"top_10_capture",
|
|
]) {
|
|
requireNumber(row[metric], `${label}.${metric}`, { min: 0 });
|
|
}
|
|
if (row.average_precision > 1 || row.brier > 1 || row.roc_auc > 1 || row.top_10_capture > 1) {
|
|
throw new DataContractError(`${label} contains a probability metric above 1.`);
|
|
}
|
|
}
|
|
|
|
function validateCoverageRow(row, label) {
|
|
requireExactKeys(
|
|
row,
|
|
[
|
|
"year",
|
|
"source_era",
|
|
"support_rounded",
|
|
"labeled_rate",
|
|
"overall_result_share",
|
|
"utah_obd_proxy_share",
|
|
],
|
|
label,
|
|
);
|
|
requireInteger(row.year, `${label}.year`, { min: 2010, max: 2100 });
|
|
requireString(row.source_era, `${label}.source_era`);
|
|
requireInteger(row.support_rounded, `${label}.support_rounded`, { min: 0 });
|
|
for (const metric of ["labeled_rate", "overall_result_share", "utah_obd_proxy_share"]) {
|
|
requireNumber(row[metric], `${label}.${metric}`, { min: 0, max: 1 });
|
|
}
|
|
}
|
|
|
|
function validateStringArray(value, label) {
|
|
if (!Array.isArray(value)) throw new DataContractError(`${label} must be an array.`);
|
|
value.forEach((item, index) => requireString(item, `${label}[${index}]`));
|
|
}
|
|
|
|
function validateFilterCatalog(value) {
|
|
const label = "filter_catalog";
|
|
validateEnvelope(value, label);
|
|
requireExactKeys(
|
|
value,
|
|
[
|
|
...ENVELOPE_KEYS,
|
|
"public_counties",
|
|
"age_bands",
|
|
"prior_make_models",
|
|
"periods",
|
|
"models",
|
|
"partitions",
|
|
],
|
|
label,
|
|
);
|
|
validateStringArray(value.public_counties, `${label}.public_counties`);
|
|
validateStringArray(value.age_bands, `${label}.age_bands`);
|
|
validateStringArray(value.models, `${label}.models`);
|
|
requireExactStringSet(value.partitions, APPROVED_PARTITIONS, `${label}.partitions`);
|
|
if (!Array.isArray(value.prior_make_models)) {
|
|
throw new DataContractError(`${label}.prior_make_models must be an array.`);
|
|
}
|
|
value.prior_make_models.forEach((row, index) => {
|
|
requirePlainObject(row, `${label}.prior_make_models[${index}]`);
|
|
requireExactKeys(
|
|
row,
|
|
["prior_make", "prior_model"],
|
|
`${label}.prior_make_models[${index}]`,
|
|
);
|
|
requireString(row.prior_make, `${label}.prior_make_models[${index}].prior_make`);
|
|
requireString(row.prior_model, `${label}.prior_make_models[${index}].prior_model`);
|
|
});
|
|
if (!Array.isArray(value.periods)) {
|
|
throw new DataContractError(`${label}.periods must be an array.`);
|
|
}
|
|
value.periods.forEach((period, index) => {
|
|
requirePlainObject(period, `${label}.periods[${index}]`);
|
|
requireExactKeys(period, ["year", "quarters"], `${label}.periods[${index}]`);
|
|
requireInteger(period.year, `${label}.periods[${index}].year`, {
|
|
min: 2010,
|
|
max: 2100,
|
|
});
|
|
if (!Array.isArray(period.quarters) || period.quarters.length === 0) {
|
|
throw new DataContractError(`${label}.periods[${index}].quarters must be non-empty.`);
|
|
}
|
|
period.quarters.forEach((quarter, quarterIndex) =>
|
|
requireInteger(quarter, `${label}.periods[${index}].quarters[${quarterIndex}]`, {
|
|
min: 1,
|
|
max: 4,
|
|
}),
|
|
);
|
|
});
|
|
return value;
|
|
}
|
|
|
|
function validateShaManifest(value) {
|
|
const label = "sha256_manifest";
|
|
validateEnvelope(value, label);
|
|
requireExactKeys(value, [...ENVELOPE_KEYS, "files"], label);
|
|
if (!Array.isArray(value.files)) {
|
|
throw new DataContractError(`${label}.files must be an array.`);
|
|
}
|
|
const expectedNames = Object.entries(REQUIRED_ASSETS)
|
|
.filter(([name]) => name !== "shaManifest")
|
|
.map(([, filename]) => filename)
|
|
.sort();
|
|
const observedNames = [];
|
|
for (const [index, file] of value.files.entries()) {
|
|
requirePlainObject(file, `${label}.files[${index}]`);
|
|
requireExactKeys(file, ["name", "sha256"], `${label}.files[${index}]`);
|
|
requireString(file.name, `${label}.files[${index}].name`);
|
|
requireString(file.sha256, `${label}.files[${index}].sha256`);
|
|
if (!/^[0-9a-f]{64}$/.test(file.sha256)) {
|
|
throw new DataContractError(`${label}.files[${index}].sha256 is invalid.`);
|
|
}
|
|
observedNames.push(file.name);
|
|
}
|
|
observedNames.sort();
|
|
if (JSON.stringify(observedNames) !== JSON.stringify(expectedNames)) {
|
|
throw new DataContractError(`${label} does not cover the exact approved asset set.`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function rejectSensitiveKeys(value, label, seen = new WeakSet()) {
|
|
if (value === null || typeof value !== "object") return;
|
|
if (seen.has(value)) return;
|
|
seen.add(value);
|
|
if (Array.isArray(value)) {
|
|
value.forEach((item, index) => rejectSensitiveKeys(item, `${label}[${index}]`, seen));
|
|
return;
|
|
}
|
|
for (const [key, child] of Object.entries(value)) {
|
|
const normalizedParts = key.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
if (normalizedParts.some((part) => SENSITIVE_KEY_PARTS.has(part))) {
|
|
throw new DataContractError(`${label} contains a field outside the public data contract.`);
|
|
}
|
|
rejectSensitiveKeys(child, `${label}.${key}`, seen);
|
|
}
|
|
}
|
|
|
|
export function validateAssetSet(rawAssets) {
|
|
requirePlainObject(rawAssets, "asset set");
|
|
requireExactKeys(rawAssets, Object.keys(REQUIRED_ASSETS), "asset set");
|
|
const manifest = rawAssets.manifest;
|
|
validateEnvelope(manifest, "data_manifest");
|
|
if (!manifest.development_preview || manifest.population_estimate_allowed) {
|
|
throw new DataContractError(
|
|
"The dashboard accepts development-sample, non-population assets only.",
|
|
);
|
|
}
|
|
requireExactKeys(
|
|
manifest,
|
|
[
|
|
...ENVELOPE_KEYS,
|
|
"assets",
|
|
"data_scope",
|
|
"definitions",
|
|
"model_versions",
|
|
"release_id",
|
|
],
|
|
"data_manifest",
|
|
);
|
|
if (Object.hasOwn(manifest, "rows")) {
|
|
throw new DataContractError("data_manifest must not contain row data.");
|
|
}
|
|
requirePlainObject(manifest.data_scope, "data_manifest.data_scope");
|
|
requirePlainObject(manifest.definitions, "data_manifest.definitions");
|
|
requireString(manifest.release_id, "data_manifest.release_id");
|
|
if (!/^[0-9a-f]{64}$/.test(manifest.release_id)) {
|
|
throw new DataContractError("data_manifest.release_id is invalid.");
|
|
}
|
|
if (!Array.isArray(manifest.model_versions) || manifest.model_versions.length === 0) {
|
|
throw new DataContractError("data_manifest.model_versions must be a non-empty array.");
|
|
}
|
|
manifest.model_versions.forEach((version, index) =>
|
|
requireSafeModelVersion(version, `data_manifest.model_versions[${index}]`),
|
|
);
|
|
if (
|
|
JSON.stringify(manifest.model_versions) !==
|
|
JSON.stringify([...new Set(manifest.model_versions)].sort())
|
|
) {
|
|
throw new DataContractError("data_manifest.model_versions must be sorted and unique.");
|
|
}
|
|
requireExactKeys(
|
|
manifest.data_scope,
|
|
["first_year", "last_year", "model_names", "partitions"],
|
|
"data_manifest.data_scope",
|
|
);
|
|
requireInteger(manifest.data_scope.first_year, "data_manifest.data_scope.first_year", {
|
|
min: 2010,
|
|
max: 2100,
|
|
});
|
|
requireInteger(manifest.data_scope.last_year, "data_manifest.data_scope.last_year", {
|
|
min: manifest.data_scope.first_year,
|
|
max: 2100,
|
|
});
|
|
validateStringArray(manifest.data_scope.model_names, "data_manifest.data_scope.model_names");
|
|
requireExactStringSet(
|
|
manifest.data_scope.partitions,
|
|
APPROVED_PARTITIONS,
|
|
"data_manifest.data_scope.partitions",
|
|
);
|
|
requireExactKeys(
|
|
manifest.definitions,
|
|
[
|
|
"episode_gap_days",
|
|
"locked_test_metrics_published",
|
|
"support_rounding",
|
|
"suppression_min_nonpass",
|
|
"suppression_min_pass",
|
|
"suppression_min_support",
|
|
"suppression_min_distinct_vehicles",
|
|
"suppression_min_distinct_pass_vehicles",
|
|
"suppression_min_distinct_nonpass_vehicles",
|
|
"target",
|
|
],
|
|
"data_manifest.definitions",
|
|
);
|
|
requireInteger(manifest.definitions.episode_gap_days, "data_manifest.definitions.episode_gap_days", {
|
|
min: 1,
|
|
});
|
|
requireBoolean(
|
|
manifest.definitions.locked_test_metrics_published,
|
|
"data_manifest.definitions.locked_test_metrics_published",
|
|
);
|
|
if (manifest.definitions.locked_test_metrics_published !== false) {
|
|
throw new DataContractError("Locked-test metrics are not approved for this public preview.");
|
|
}
|
|
for (const key of [
|
|
"support_rounding",
|
|
"suppression_min_nonpass",
|
|
"suppression_min_pass",
|
|
"suppression_min_support",
|
|
"suppression_min_distinct_vehicles",
|
|
"suppression_min_distinct_pass_vehicles",
|
|
"suppression_min_distinct_nonpass_vehicles",
|
|
]) {
|
|
requireInteger(manifest.definitions[key], `data_manifest.definitions.${key}`, { min: 1 });
|
|
}
|
|
requireString(manifest.definitions.target, "data_manifest.definitions.target");
|
|
if (manifest.definitions.target !== APPROVED_TARGET) {
|
|
throw new DataContractError("data_manifest.definitions.target is outside the approved scope.");
|
|
}
|
|
if (!Array.isArray(manifest.assets)) {
|
|
throw new DataContractError("data_manifest.assets must be an array.");
|
|
}
|
|
validateStringArray(manifest.assets, "data_manifest.assets");
|
|
const expectedManifestAssets = Object.entries(REQUIRED_ASSETS)
|
|
.filter(([name]) => !["manifest", "shaManifest"].includes(name))
|
|
.map(([, filename]) => filename)
|
|
.sort();
|
|
const declaredAssets = [...manifest.assets].sort();
|
|
if (JSON.stringify(declaredAssets) !== JSON.stringify(expectedManifestAssets)) {
|
|
throw new DataContractError("data_manifest.assets does not match the approved asset set.");
|
|
}
|
|
|
|
const validated = { manifest };
|
|
for (const [name, validator] of Object.entries(ASSET_VALIDATORS)) {
|
|
if (!Object.hasOwn(rawAssets, name)) {
|
|
throw new DataContractError(`A required approved aggregate is missing: ${name}.`);
|
|
}
|
|
validated[name] = validator(rawAssets[name]);
|
|
if (
|
|
validated[name].development_preview !== manifest.development_preview ||
|
|
validated[name].population_estimate_allowed !== manifest.population_estimate_allowed
|
|
) {
|
|
throw new DataContractError(`${name} publication flags disagree with data_manifest.`);
|
|
}
|
|
}
|
|
requireExactStringSet(
|
|
validated.filters.models,
|
|
manifest.data_scope.model_names,
|
|
"filter_catalog.models",
|
|
);
|
|
for (const [index, row] of validated.diagnostics.rows.entries()) {
|
|
if (!validated.filters.models.includes(row.model)) {
|
|
throw new DataContractError(`model_diagnostics.rows[${index}].model is not cataloged.`);
|
|
}
|
|
}
|
|
for (const model of REQUIRED_CALIBRATION_MODELS) {
|
|
const matches = validated.diagnostics.rows.filter(
|
|
(row) => row.model === model && row.partition === "calibrate",
|
|
);
|
|
if (matches.length !== 1) {
|
|
throw new DataContractError(
|
|
`model_diagnostics must contain exactly one approved calibration row for ${model}.`,
|
|
);
|
|
}
|
|
}
|
|
const minimumSupport = manifest.definitions.suppression_min_support;
|
|
const supportRounding = manifest.definitions.support_rounding;
|
|
for (const name of ["overview", "ageRisk", "scorecards", "coverage"]) {
|
|
for (const [index, row] of validated[name].rows.entries()) {
|
|
if (
|
|
row.support_rounded < minimumSupport ||
|
|
row.support_rounded % supportRounding !== 0
|
|
) {
|
|
throw new DataContractError(
|
|
`${name}.rows[${index}].support_rounded violates the publication thresholds.`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return Object.freeze(validated);
|
|
}
|
|
|
|
async function fetchBytes(path, fetchImplementation) {
|
|
let response;
|
|
try {
|
|
response = await fetchImplementation(path, {
|
|
cache: "no-store",
|
|
credentials: "same-origin",
|
|
});
|
|
} catch {
|
|
throw new DataContractError("Approved dashboard assets could not be reached.");
|
|
}
|
|
if (!response.ok) {
|
|
throw new DataContractError("One or more approved dashboard assets are unavailable.");
|
|
}
|
|
try {
|
|
return await response.arrayBuffer();
|
|
} catch {
|
|
throw new DataContractError("An approved dashboard asset could not be read safely.");
|
|
}
|
|
}
|
|
|
|
function parseJsonBytes(bytes, label) {
|
|
if (typeof TextDecoder !== "function") {
|
|
throw new DataContractError("This browser cannot decode dashboard assets safely.");
|
|
}
|
|
try {
|
|
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
} catch {
|
|
throw new DataContractError(`${label} is not valid UTF-8 JSON.`);
|
|
}
|
|
}
|
|
|
|
function rotateRight(value, shift) {
|
|
return ((value >>> shift) | (value << (32 - shift))) >>> 0;
|
|
}
|
|
|
|
function sha256HexFallback(bytes) {
|
|
const input = new Uint8Array(bytes);
|
|
const bitLength = input.byteLength * 8;
|
|
if (!Number.isSafeInteger(bitLength)) {
|
|
throw new DataContractError("Dashboard asset integrity verification failed.");
|
|
}
|
|
|
|
const paddedLength = Math.ceil((input.byteLength + 9) / 64) * 64;
|
|
const padded = new Uint8Array(paddedLength);
|
|
padded.set(input);
|
|
padded[input.byteLength] = 0x80;
|
|
|
|
const paddedView = new DataView(padded.buffer);
|
|
paddedView.setUint32(paddedLength - 8, Math.floor(bitLength / 0x100000000), false);
|
|
paddedView.setUint32(paddedLength - 4, bitLength >>> 0, false);
|
|
|
|
const state = [...SHA256_INITIAL_STATE];
|
|
const words = new Uint32Array(64);
|
|
for (let offset = 0; offset < paddedLength; offset += 64) {
|
|
for (let index = 0; index < 16; index += 1) {
|
|
words[index] = paddedView.getUint32(offset + index * 4, false);
|
|
}
|
|
for (let index = 16; index < 64; index += 1) {
|
|
const sigma0 =
|
|
rotateRight(words[index - 15], 7) ^
|
|
rotateRight(words[index - 15], 18) ^
|
|
(words[index - 15] >>> 3);
|
|
const sigma1 =
|
|
rotateRight(words[index - 2], 17) ^
|
|
rotateRight(words[index - 2], 19) ^
|
|
(words[index - 2] >>> 10);
|
|
words[index] =
|
|
(words[index - 16] + sigma0 + words[index - 7] + sigma1) >>> 0;
|
|
}
|
|
|
|
let [a, b, c, d, e, f, g, h] = state;
|
|
for (let index = 0; index < 64; index += 1) {
|
|
const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
|
const choose = (e & f) ^ (~e & g);
|
|
const temporary1 =
|
|
(h + sum1 + choose + SHA256_ROUND_CONSTANTS[index] + words[index]) >>> 0;
|
|
const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
|
const majority = (a & b) ^ (a & c) ^ (b & c);
|
|
const temporary2 = (sum0 + majority) >>> 0;
|
|
h = g;
|
|
g = f;
|
|
f = e;
|
|
e = (d + temporary1) >>> 0;
|
|
d = c;
|
|
c = b;
|
|
b = a;
|
|
a = (temporary1 + temporary2) >>> 0;
|
|
}
|
|
|
|
state[0] = (state[0] + a) >>> 0;
|
|
state[1] = (state[1] + b) >>> 0;
|
|
state[2] = (state[2] + c) >>> 0;
|
|
state[3] = (state[3] + d) >>> 0;
|
|
state[4] = (state[4] + e) >>> 0;
|
|
state[5] = (state[5] + f) >>> 0;
|
|
state[6] = (state[6] + g) >>> 0;
|
|
state[7] = (state[7] + h) >>> 0;
|
|
}
|
|
|
|
return state.map((word) => word.toString(16).padStart(8, "0")).join("");
|
|
}
|
|
|
|
async function sha256Hex(bytes, cryptoImplementation) {
|
|
if (!cryptoImplementation?.subtle || typeof cryptoImplementation.subtle.digest !== "function") {
|
|
return sha256HexFallback(bytes);
|
|
}
|
|
let digest;
|
|
try {
|
|
digest = await cryptoImplementation.subtle.digest("SHA-256", bytes);
|
|
} catch {
|
|
throw new DataContractError("Dashboard asset integrity verification failed.");
|
|
}
|
|
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
|
|
export async function loadDashboardData({
|
|
basePath = "./public/data/",
|
|
fetchImplementation = globalThis.fetch,
|
|
cryptoImplementation = globalThis.crypto,
|
|
} = {}) {
|
|
if (typeof fetchImplementation !== "function") {
|
|
throw new DataContractError("This browser cannot load dashboard assets safely.");
|
|
}
|
|
const baseUrl = new URL(basePath, globalThis.location?.href ?? "http://local/");
|
|
const shaBytes = await fetchBytes(
|
|
new URL(REQUIRED_ASSETS.shaManifest, baseUrl),
|
|
fetchImplementation,
|
|
);
|
|
const shaManifest = validateShaManifest(
|
|
parseJsonBytes(shaBytes, "The checksum manifest"),
|
|
);
|
|
const expectedDigests = new Map(
|
|
shaManifest.files.map((entry) => [entry.name, entry.sha256]),
|
|
);
|
|
const assetEntries = Object.entries(REQUIRED_ASSETS).filter(
|
|
([name]) => name !== "shaManifest",
|
|
);
|
|
const rawEntries = await Promise.all(
|
|
assetEntries.map(async ([name, filename]) => [
|
|
name,
|
|
filename,
|
|
await fetchBytes(new URL(filename, baseUrl), fetchImplementation),
|
|
]),
|
|
);
|
|
|
|
await Promise.all(
|
|
rawEntries.map(async ([, filename, bytes]) => {
|
|
const observed = await sha256Hex(bytes, cryptoImplementation);
|
|
if (observed !== expectedDigests.get(filename)) {
|
|
throw new DataContractError(`Integrity verification failed for ${filename}.`);
|
|
}
|
|
}),
|
|
);
|
|
|
|
const parsedEntries = rawEntries.map(([name, filename, bytes]) => [
|
|
name,
|
|
parseJsonBytes(bytes, filename),
|
|
]);
|
|
parsedEntries.push(["shaManifest", shaManifest]);
|
|
return validateAssetSet(Object.fromEntries(parsedEntries));
|
|
}
|
|
|
|
export { SCHEMA_VERSION };
|