initial code
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
const SCHEMA_VERSION = "dashboard_data_v1";
|
||||
const APPROVED_PARTITIONS = Object.freeze(["train", "tune", "calibrate"]);
|
||||
const ENVELOPE_KEYS = Object.freeze([
|
||||
"development_preview",
|
||||
"population_estimate_allowed",
|
||||
"schema_version",
|
||||
]);
|
||||
|
||||
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",
|
||||
"email",
|
||||
"internal",
|
||||
"ip",
|
||||
"owner",
|
||||
"pid",
|
||||
"plate",
|
||||
"raw",
|
||||
"session",
|
||||
"station",
|
||||
"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");
|
||||
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 (!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.`);
|
||||
}
|
||||
}
|
||||
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.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function sha256Hex(bytes, cryptoImplementation) {
|
||||
if (!cryptoImplementation?.subtle || typeof cryptoImplementation.subtle.digest !== "function") {
|
||||
throw new DataContractError("This browser cannot verify dashboard asset integrity.");
|
||||
}
|
||||
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 };
|
||||
Reference in New Issue
Block a user