feat(provider-contract): compile governed L2 execution plans
This commit is contained in:
@@ -14,6 +14,21 @@ export {
|
||||
TELEMETRY_READINGS_MAX_ITEMS,
|
||||
isBoundedTelemetryReadings,
|
||||
} from "./telemetry-readings.mjs";
|
||||
export {
|
||||
TELEMETRY_FIELD_PROJECTION_SCHEMA_VERSION,
|
||||
TELEMETRY_FIELD_REGISTRY_SCHEMA_VERSION,
|
||||
compileTelemetryFieldProjection,
|
||||
digestTelemetryFieldRegistry,
|
||||
validateTelemetryFieldRegistry,
|
||||
} from "./telemetry-field-registry.mjs";
|
||||
export {
|
||||
L2_EXECUTION_PLAN_COMPILER_VERSION,
|
||||
L2_EXECUTION_PLAN_SCHEMA_VERSION,
|
||||
L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION,
|
||||
attestL2ExecutionPlanMaterialization,
|
||||
compileL2ExecutionPlan,
|
||||
validateL2ExecutionPlan,
|
||||
} from "./l2-execution-plan.mjs";
|
||||
export {
|
||||
ZONE_SOURCE_ADAPTERS,
|
||||
ZONE_SOURCE_GENERATION_SCHEMA_VERSION,
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { validateProviderPackage } from "./provider-package.mjs";
|
||||
import {
|
||||
compileTelemetryFieldProjection,
|
||||
validateTelemetryFieldRegistry,
|
||||
} from "./telemetry-field-registry.mjs";
|
||||
|
||||
export const L2_EXECUTION_PLAN_SCHEMA_VERSION = "nodedc.l2-execution-plan/v1";
|
||||
export const L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION = "nodedc.l2-materialization-receipt/v1";
|
||||
export const L2_EXECUTION_PLAN_COMPILER_VERSION = "1.0.0";
|
||||
|
||||
const HASH = /^(?:sha256:)?[a-f0-9]{64}$/;
|
||||
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
|
||||
const CREDENTIAL_REF = /^ndc-credref:[A-Za-z0-9._:-]{8,255}$/;
|
||||
const COMPILE_OPTION_KEYS = new Set(["telemetryFieldRegistry"]);
|
||||
const RECEIPT_OPTION_KEYS = new Set(["graphRevision", "graphDigest", "materializedStepIds"]);
|
||||
|
||||
/**
|
||||
* Resolve one immutable provider package plus one connection instance into a
|
||||
* deterministic, provider-neutral execution plan. Provider-specific knowledge
|
||||
* stays in package/registry data; compiler branching is only by generic step
|
||||
* kind.
|
||||
*/
|
||||
export function compileL2ExecutionPlan(providerPackage, connectionInstance, options = {}) {
|
||||
const packageValidation = validateProviderPackage(providerPackage);
|
||||
if (!packageValidation.ok) {
|
||||
throw new Error(`provider_package_invalid:${packageValidation.errors.join(",")}`);
|
||||
}
|
||||
validateCompileOptions(options);
|
||||
validateConnectionInstance(providerPackage, connectionInstance);
|
||||
|
||||
const profile = requireById(
|
||||
providerPackage.collectionProfiles,
|
||||
connectionInstance.collectionProfileId,
|
||||
"collection_profile",
|
||||
);
|
||||
const template = requireById(providerPackage.l2Templates, profile.l2TemplateId, "l2_template");
|
||||
if (template.id !== connectionInstance.l2TemplateId) {
|
||||
throw new Error("l2_execution_plan_connection_template_mismatch");
|
||||
}
|
||||
const mapping = requireById(providerPackage.mappingContracts, profile.mappingContractId, "mapping_contract");
|
||||
const fieldPolicy = requireById(providerPackage.fieldPolicies, profile.fieldPolicyId, "field_policy");
|
||||
const dataProduct = requireById(providerPackage.dataProducts, profile.dataProductId, "data_product");
|
||||
const requiresTelemetryRegistry = Object.values(dataProduct.fieldContracts || {})
|
||||
.some((contract) => contract?.type === "telemetry_readings");
|
||||
|
||||
let telemetryProjection;
|
||||
if (options.telemetryFieldRegistry !== undefined) {
|
||||
const registryValidation = validateTelemetryFieldRegistry(options.telemetryFieldRegistry);
|
||||
if (!registryValidation.ok) {
|
||||
throw new Error(`telemetry_field_registry_invalid:${registryValidation.errors.join(",")}`);
|
||||
}
|
||||
telemetryProjection = compileTelemetryFieldProjection(options.telemetryFieldRegistry, {
|
||||
providerPackageId: providerPackage.id,
|
||||
providerPackageVersion: providerPackage.version,
|
||||
dataProductId: dataProduct.id,
|
||||
surface: "data_product",
|
||||
});
|
||||
} else if (requiresTelemetryRegistry) {
|
||||
throw new Error("l2_execution_plan_telemetry_registry_required");
|
||||
}
|
||||
if (requiresTelemetryRegistry && telemetryProjection.entries.length === 0) {
|
||||
throw new Error("l2_execution_plan_telemetry_projection_empty");
|
||||
}
|
||||
if (!requiresTelemetryRegistry && telemetryProjection !== undefined) {
|
||||
throw new Error("l2_execution_plan_telemetry_registry_not_applicable");
|
||||
}
|
||||
|
||||
const artifacts = {
|
||||
packageDigest: canonicalDigest(providerPackage),
|
||||
collectionProfileDigest: canonicalDigest(profile),
|
||||
l2TemplateDigest: canonicalDigest(template),
|
||||
mappingContractDigest: canonicalDigest(mapping),
|
||||
fieldPolicyDigest: canonicalDigest(fieldPolicy),
|
||||
dataProductDigest: canonicalDigest(dataProduct),
|
||||
...(telemetryProjection ? { telemetryRegistryDigest: telemetryProjection.registry.digest } : {}),
|
||||
};
|
||||
|
||||
const steps = template.steps.map((step) => compileStep({
|
||||
step,
|
||||
providerPackage,
|
||||
profile,
|
||||
mapping,
|
||||
fieldPolicy,
|
||||
dataProduct,
|
||||
telemetryProjection,
|
||||
artifacts,
|
||||
}));
|
||||
|
||||
const planWithoutDigest = {
|
||||
schemaVersion: L2_EXECUTION_PLAN_SCHEMA_VERSION,
|
||||
compilerVersion: L2_EXECUTION_PLAN_COMPILER_VERSION,
|
||||
package: {
|
||||
id: providerPackage.id,
|
||||
providerId: providerPackage.providerId,
|
||||
version: providerPackage.version,
|
||||
},
|
||||
connection: {
|
||||
tenantId: connectionInstance.tenantId,
|
||||
connectionId: connectionInstance.connectionId,
|
||||
collectionProfileId: profile.id,
|
||||
l2TemplateId: template.id,
|
||||
},
|
||||
bindings: {
|
||||
provider: structuredClone(connectionInstance.credentialRefs.provider),
|
||||
publisher: structuredClone(connectionInstance.systemBindings.publisher),
|
||||
},
|
||||
collectionProfile: structuredClone(profile),
|
||||
artifacts,
|
||||
steps,
|
||||
};
|
||||
|
||||
return deepFreeze({
|
||||
...planWithoutDigest,
|
||||
executionPlanDigest: canonicalDigest(planWithoutDigest),
|
||||
});
|
||||
}
|
||||
|
||||
export function validateL2ExecutionPlan(value) {
|
||||
const errors = [];
|
||||
if (!isPlainObject(value)) return result(["executionPlan_must_be_object"]);
|
||||
if (value.schemaVersion !== L2_EXECUTION_PLAN_SCHEMA_VERSION) errors.push("executionPlan.schemaVersion_mismatch");
|
||||
if (value.compilerVersion !== L2_EXECUTION_PLAN_COMPILER_VERSION) errors.push("executionPlan.compilerVersion_mismatch");
|
||||
if (!Array.isArray(value.steps) || value.steps.length < 5) {
|
||||
errors.push("executionPlan.steps_invalid");
|
||||
} else {
|
||||
const ids = value.steps.map((step) => step?.id);
|
||||
if (ids.some((id) => typeof id !== "string" || !IDENTIFIER.test(id))) errors.push("executionPlan.steps_id_invalid");
|
||||
if (new Set(ids).size !== ids.length) errors.push("executionPlan.steps_id_must_be_unique");
|
||||
}
|
||||
if (!HASH.test(String(value.executionPlanDigest || ""))) {
|
||||
errors.push("executionPlan.executionPlanDigest_invalid");
|
||||
} else {
|
||||
const descriptor = structuredClone(value);
|
||||
delete descriptor.executionPlanDigest;
|
||||
if (normalizeDigest(value.executionPlanDigest) !== canonicalDigest(descriptor)) {
|
||||
errors.push("executionPlan.executionPlanDigest_mismatch");
|
||||
}
|
||||
}
|
||||
return result(errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a successful Engine graph materialization back to the exact compiled
|
||||
* plan without teaching this package how Engine stores or renders its graph.
|
||||
*/
|
||||
export function attestL2ExecutionPlanMaterialization(executionPlan, options) {
|
||||
const validation = validateL2ExecutionPlan(executionPlan);
|
||||
if (!validation.ok) {
|
||||
throw new Error(`l2_execution_plan_invalid:${validation.errors.join(",")}`);
|
||||
}
|
||||
if (!isPlainObject(options)) throw new Error("l2_materialization_options_must_be_object");
|
||||
const errors = [];
|
||||
rejectUnknownKeys(options, RECEIPT_OPTION_KEYS, "options", errors);
|
||||
if (typeof options.graphRevision !== "string" || !options.graphRevision.trim() || options.graphRevision.length > 160) {
|
||||
errors.push("options.graphRevision_invalid");
|
||||
}
|
||||
if (!HASH.test(String(options.graphDigest || ""))) errors.push("options.graphDigest_invalid");
|
||||
const expectedStepIds = executionPlan.steps.map((step) => step.id);
|
||||
if (!Array.isArray(options.materializedStepIds)
|
||||
|| options.materializedStepIds.length !== expectedStepIds.length
|
||||
|| options.materializedStepIds.some((id, index) => id !== expectedStepIds[index])) {
|
||||
errors.push("options.materializedStepIds_must_exactly_match_execution_plan");
|
||||
}
|
||||
if (errors.length) {
|
||||
throw new Error(`l2_materialization_invalid:${[...new Set(errors)].join(",")}`);
|
||||
}
|
||||
|
||||
const receiptWithoutDigest = {
|
||||
schemaVersion: L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION,
|
||||
compilerVersion: executionPlan.compilerVersion,
|
||||
executionPlanDigest: executionPlan.executionPlanDigest,
|
||||
graphRevision: options.graphRevision,
|
||||
graphDigest: normalizeDigest(options.graphDigest),
|
||||
materializedStepIds: [...options.materializedStepIds],
|
||||
};
|
||||
return deepFreeze({
|
||||
...receiptWithoutDigest,
|
||||
receiptDigest: canonicalDigest(receiptWithoutDigest),
|
||||
});
|
||||
}
|
||||
|
||||
function compileStep(context) {
|
||||
const { step, providerPackage, profile, mapping, fieldPolicy, dataProduct, telemetryProjection, artifacts } = context;
|
||||
const base = { id: step.id, kind: step.kind };
|
||||
if (step.kind === "collection_trigger") {
|
||||
return {
|
||||
...base,
|
||||
config: {
|
||||
mode: profile.mode,
|
||||
...(profile.schedule ? { schedule: structuredClone(profile.schedule) } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (step.kind === "provider_request") {
|
||||
const capability = requireById(providerPackage.capabilities, step.capabilityId, "capability");
|
||||
return {
|
||||
...base,
|
||||
config: {
|
||||
capabilityId: capability.id,
|
||||
capabilityDigest: canonicalDigest(capability),
|
||||
authModeId: capability.authModeId,
|
||||
credentialBinding: "provider",
|
||||
request: structuredClone(capability.request),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (step.kind === "extract_items") {
|
||||
const capability = requireById(providerPackage.capabilities, step.capabilityId, "capability");
|
||||
return {
|
||||
...base,
|
||||
config: {
|
||||
capabilityId: capability.id,
|
||||
capabilityDigest: canonicalDigest(capability),
|
||||
response: structuredClone(capability.request.response),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (step.kind === "semantic_mapping") {
|
||||
return {
|
||||
...base,
|
||||
config: {
|
||||
mappingContractId: mapping.id,
|
||||
mappingContractDigest: artifacts.mappingContractDigest,
|
||||
fieldPolicyId: fieldPolicy.id,
|
||||
fieldPolicyDigest: artifacts.fieldPolicyDigest,
|
||||
dataProductId: dataProduct.id,
|
||||
dataProductDigest: artifacts.dataProductDigest,
|
||||
mappingContract: structuredClone(mapping),
|
||||
...(telemetryProjection ? { telemetryProjection: structuredClone(telemetryProjection) } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (step.kind === "data_product_publish") {
|
||||
return {
|
||||
...base,
|
||||
config: {
|
||||
dataProductId: dataProduct.id,
|
||||
dataProductDigest: artifacts.dataProductDigest,
|
||||
nodeType: step.nodeType,
|
||||
credentialBinding: "publisher",
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error(`l2_execution_plan_step_kind_unsupported:${String(step.kind)}`);
|
||||
}
|
||||
|
||||
function validateConnectionInstance(providerPackage, value) {
|
||||
if (!isPlainObject(value)) throw new Error("l2_execution_plan_connection_must_be_object");
|
||||
if (value.schemaVersion !== "nodedc.l2-connection-instance/v1") {
|
||||
throw new Error("l2_execution_plan_connection_schema_mismatch");
|
||||
}
|
||||
if (value.package?.id !== providerPackage.id
|
||||
|| value.package?.providerId !== providerPackage.providerId
|
||||
|| value.package?.version !== providerPackage.version) {
|
||||
throw new Error("l2_execution_plan_connection_package_mismatch");
|
||||
}
|
||||
if (typeof value.tenantId !== "string" || !IDENTIFIER.test(value.tenantId)) {
|
||||
throw new Error("l2_execution_plan_tenant_id_invalid");
|
||||
}
|
||||
if (typeof value.connectionId !== "string" || !IDENTIFIER.test(value.connectionId)) {
|
||||
throw new Error("l2_execution_plan_connection_id_invalid");
|
||||
}
|
||||
if (!CREDENTIAL_REF.test(String(value.credentialRefs?.provider?.reference || ""))
|
||||
|| value.credentialRefs?.provider?.owner !== "ndc_l2_credentials") {
|
||||
throw new Error("l2_execution_plan_provider_credential_ref_invalid");
|
||||
}
|
||||
const publisher = value.systemBindings?.publisher;
|
||||
if (publisher?.owner !== "ndc_l2_credentials"
|
||||
|| publisher?.management !== "control_plane_managed"
|
||||
|| publisher?.desiredState !== "bound"
|
||||
|| publisher?.status !== "unresolved") {
|
||||
throw new Error("l2_execution_plan_publisher_binding_invalid");
|
||||
}
|
||||
}
|
||||
|
||||
function validateCompileOptions(options) {
|
||||
if (!isPlainObject(options)) throw new Error("l2_execution_plan_options_must_be_object");
|
||||
const errors = [];
|
||||
rejectUnknownKeys(options, COMPILE_OPTION_KEYS, "options", errors);
|
||||
if (errors.length) throw new Error(`l2_execution_plan_options_invalid:${errors.join(",")}`);
|
||||
}
|
||||
|
||||
function requireById(items, id, type) {
|
||||
const value = Array.isArray(items) ? items.find((item) => item?.id === id) : undefined;
|
||||
if (!value) throw new Error(`l2_execution_plan_${type}_missing:${String(id)}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function canonicalDigest(value) {
|
||||
return `sha256:${createHash("sha256").update(stableJson(value), "utf8").digest("hex")}`;
|
||||
}
|
||||
|
||||
function normalizeDigest(value) {
|
||||
const text = String(value);
|
||||
return text.startsWith("sha256:") ? text : `sha256:${text}`;
|
||||
}
|
||||
|
||||
function stableJson(value) {
|
||||
return JSON.stringify(stableValue(value));
|
||||
}
|
||||
|
||||
function stableValue(value) {
|
||||
if (Array.isArray(value)) return value.map(stableValue);
|
||||
if (!isPlainObject(value)) return value;
|
||||
return Object.fromEntries(
|
||||
Object.keys(value)
|
||||
.filter((key) => value[key] !== undefined)
|
||||
.sort()
|
||||
.map((key) => [key, stableValue(value[key])]),
|
||||
);
|
||||
}
|
||||
|
||||
function rejectUnknownKeys(value, allowed, path, errors) {
|
||||
for (const key of Object.keys(value)) {
|
||||
if (!allowed.has(key)) errors.push(`${path}.${key}_not_allowed`);
|
||||
}
|
||||
}
|
||||
|
||||
function result(errors) {
|
||||
return { ok: errors.length === 0, errors: [...new Set(errors)] };
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function deepFreeze(input) {
|
||||
if (!input || typeof input !== "object" || Object.isFrozen(input)) return input;
|
||||
Object.freeze(input);
|
||||
for (const child of Object.values(input)) deepFreeze(child);
|
||||
return input;
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
export const TELEMETRY_FIELD_REGISTRY_SCHEMA_VERSION = "nodedc.telemetry-field-registry/v1";
|
||||
export const TELEMETRY_FIELD_PROJECTION_SCHEMA_VERSION = "nodedc.telemetry-field-projection/v1";
|
||||
|
||||
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
|
||||
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?$/i;
|
||||
const SOURCE_KEY = /^[A-Za-z_][A-Za-z0-9_.:-]{0,127}$/;
|
||||
const VALUE_TYPES = new Set(["unknown", "number", "boolean", "string"]);
|
||||
const STATUSES = new Set(["observed", "classified", "approved", "restricted", "deprecated"]);
|
||||
const SENSITIVITIES = new Set(["operational", "unclassified", "identifier", "personal", "command", "restricted"]);
|
||||
const SURFACES = new Set(["audit", "data_product", "analytics", "foundry"]);
|
||||
const REGISTRY_KEYS = new Set([
|
||||
"schemaVersion",
|
||||
"id",
|
||||
"version",
|
||||
"providerPackage",
|
||||
"dataProductId",
|
||||
"sourceNamespace",
|
||||
"entries",
|
||||
]);
|
||||
const PACKAGE_KEYS = new Set(["id", "version"]);
|
||||
const ENTRY_KEYS = new Set([
|
||||
"id",
|
||||
"sourceKey",
|
||||
"status",
|
||||
"sensitivity",
|
||||
"valueType",
|
||||
"semanticReading",
|
||||
"allowedSurfaces",
|
||||
]);
|
||||
const READING_KEYS = new Set(["id", "labelSource", "label", "unitSource", "unit"]);
|
||||
const LABEL_SOURCES = new Set(["registry", "provider_configured"]);
|
||||
const UNIT_SOURCES = new Set(["none", "registry", "provider_configured"]);
|
||||
const PROJECTION_OPTION_KEYS = new Set([
|
||||
"providerPackageId",
|
||||
"providerPackageVersion",
|
||||
"dataProductId",
|
||||
"surface",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Validate a values-free, provider-scoped registry that classifies dynamic
|
||||
* telemetry parameters before any Data Product or interface can expose them.
|
||||
*/
|
||||
export function validateTelemetryFieldRegistry(value) {
|
||||
const errors = [];
|
||||
if (!isPlainObject(value)) return result(["registry_must_be_object"]);
|
||||
rejectUnknownKeys(value, REGISTRY_KEYS, "registry", errors);
|
||||
if (value.schemaVersion !== TELEMETRY_FIELD_REGISTRY_SCHEMA_VERSION) {
|
||||
errors.push("registry.schemaVersion_mismatch");
|
||||
}
|
||||
requiredIdentifier(value.id, "registry.id", errors);
|
||||
requiredSemver(value.version, "registry.version", errors);
|
||||
requiredIdentifier(value.dataProductId, "registry.dataProductId", errors);
|
||||
requiredIdentifier(value.sourceNamespace, "registry.sourceNamespace", errors);
|
||||
|
||||
if (!isPlainObject(value.providerPackage)) {
|
||||
errors.push("registry.providerPackage_must_be_object");
|
||||
} else {
|
||||
rejectUnknownKeys(value.providerPackage, PACKAGE_KEYS, "registry.providerPackage", errors);
|
||||
requiredIdentifier(value.providerPackage.id, "registry.providerPackage.id", errors);
|
||||
requiredSemver(value.providerPackage.version, "registry.providerPackage.version", errors);
|
||||
}
|
||||
|
||||
if (!Array.isArray(value.entries) || value.entries.length === 0 || value.entries.length > 512) {
|
||||
errors.push("registry.entries_must_be_nonempty_bounded_array");
|
||||
} else {
|
||||
const entryIds = new Set();
|
||||
const sourceKeys = new Set();
|
||||
const approvedReadingIds = new Set();
|
||||
value.entries.forEach((entry, index) => {
|
||||
const path = `registry.entries[${index}]`;
|
||||
if (!isPlainObject(entry)) {
|
||||
errors.push(`${path}_must_be_object`);
|
||||
return;
|
||||
}
|
||||
rejectUnknownKeys(entry, ENTRY_KEYS, path, errors);
|
||||
requiredIdentifier(entry.id, `${path}.id`, errors);
|
||||
if (entryIds.has(entry.id)) errors.push(`${path}.id_must_be_unique`);
|
||||
entryIds.add(entry.id);
|
||||
if (typeof entry.sourceKey !== "string" || !SOURCE_KEY.test(entry.sourceKey)) {
|
||||
errors.push(`${path}.sourceKey_invalid`);
|
||||
}
|
||||
if (sourceKeys.has(entry.sourceKey)) errors.push(`${path}.sourceKey_must_be_unique`);
|
||||
sourceKeys.add(entry.sourceKey);
|
||||
if (!STATUSES.has(entry.status)) errors.push(`${path}.status_invalid`);
|
||||
if (!SENSITIVITIES.has(entry.sensitivity)) errors.push(`${path}.sensitivity_invalid`);
|
||||
if (!VALUE_TYPES.has(entry.valueType)) errors.push(`${path}.valueType_invalid`);
|
||||
validateSurfaces(entry.allowedSurfaces, path, errors);
|
||||
validateSemanticReading(entry.semanticReading, path, errors);
|
||||
|
||||
const surfaces = new Set(Array.isArray(entry.allowedSurfaces) ? entry.allowedSurfaces : []);
|
||||
const externallyExposed = [...surfaces].some((surface) => surface !== "audit");
|
||||
if (entry.status === "approved") {
|
||||
if (!isPlainObject(entry.semanticReading)) errors.push(`${path}.approved_requires_semantic_reading`);
|
||||
if (entry.sensitivity !== "operational") errors.push(`${path}.approved_must_be_operational`);
|
||||
if (entry.valueType === "unknown") errors.push(`${path}.approved_valueType_must_be_classified`);
|
||||
if (!surfaces.has("data_product")) errors.push(`${path}.approved_requires_data_product_surface`);
|
||||
const readingId = entry.semanticReading?.id;
|
||||
if (typeof readingId === "string") {
|
||||
if (approvedReadingIds.has(readingId)) errors.push(`${path}.semanticReading.id_must_be_unique`);
|
||||
approvedReadingIds.add(readingId);
|
||||
}
|
||||
} else if (externallyExposed) {
|
||||
errors.push(`${path}.nonapproved_surface_must_be_audit_only`);
|
||||
}
|
||||
if (new Set(["observed", "restricted"]).has(entry.status) && entry.semanticReading !== undefined) {
|
||||
errors.push(`${path}.${entry.status}_must_not_claim_semantic_reading`);
|
||||
}
|
||||
if (entry.status === "observed" && entry.sensitivity !== "unclassified") {
|
||||
errors.push(`${path}.observed_must_be_unclassified`);
|
||||
}
|
||||
if (entry.status === "restricted" && !new Set(["identifier", "personal", "command", "restricted"]).has(entry.sensitivity)) {
|
||||
errors.push(`${path}.restricted_sensitivity_invalid`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result(errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile an exact, values-free allowlist for one consumer surface. Unknown,
|
||||
* observed, restricted and deprecated provider parameters are omitted.
|
||||
*/
|
||||
export function compileTelemetryFieldProjection(registry, options) {
|
||||
const validation = validateTelemetryFieldRegistry(registry);
|
||||
if (!validation.ok) {
|
||||
throw new Error(`telemetry_field_registry_invalid:${validation.errors.join(",")}`);
|
||||
}
|
||||
if (!isPlainObject(options)) throw new Error("telemetry_projection_options_must_be_object");
|
||||
const optionErrors = [];
|
||||
rejectUnknownKeys(options, PROJECTION_OPTION_KEYS, "options", optionErrors);
|
||||
requiredIdentifier(options.providerPackageId, "options.providerPackageId", optionErrors);
|
||||
requiredSemver(options.providerPackageVersion, "options.providerPackageVersion", optionErrors);
|
||||
requiredIdentifier(options.dataProductId, "options.dataProductId", optionErrors);
|
||||
if (!SURFACES.has(options.surface)) optionErrors.push("options.surface_invalid");
|
||||
if (options.providerPackageId !== registry.providerPackage.id) optionErrors.push("options.providerPackageId_mismatch");
|
||||
if (options.providerPackageVersion !== registry.providerPackage.version) optionErrors.push("options.providerPackageVersion_mismatch");
|
||||
if (options.dataProductId !== registry.dataProductId) optionErrors.push("options.dataProductId_mismatch");
|
||||
if (optionErrors.length) {
|
||||
throw new Error(`telemetry_projection_invalid:${[...new Set(optionErrors)].join(",")}`);
|
||||
}
|
||||
|
||||
const entries = registry.entries
|
||||
.filter((entry) => entry.status === "approved" && entry.allowedSurfaces.includes(options.surface))
|
||||
.map((entry) => ({
|
||||
registryEntryId: entry.id,
|
||||
sourceNamespace: registry.sourceNamespace,
|
||||
sourceKey: entry.sourceKey,
|
||||
valueType: entry.valueType,
|
||||
reading: structuredClone(entry.semanticReading),
|
||||
}))
|
||||
.sort((left, right) => left.sourceKey.localeCompare(right.sourceKey));
|
||||
|
||||
return deepFreeze({
|
||||
schemaVersion: TELEMETRY_FIELD_PROJECTION_SCHEMA_VERSION,
|
||||
registry: {
|
||||
id: registry.id,
|
||||
version: registry.version,
|
||||
digest: digestTelemetryFieldRegistry(registry),
|
||||
},
|
||||
providerPackage: structuredClone(registry.providerPackage),
|
||||
dataProductId: registry.dataProductId,
|
||||
surface: options.surface,
|
||||
entries,
|
||||
});
|
||||
}
|
||||
|
||||
export function digestTelemetryFieldRegistry(registry) {
|
||||
const validation = validateTelemetryFieldRegistry(registry);
|
||||
if (!validation.ok) {
|
||||
throw new Error(`telemetry_field_registry_invalid:${validation.errors.join(",")}`);
|
||||
}
|
||||
return canonicalDigest(registry);
|
||||
}
|
||||
|
||||
function validateSurfaces(value, path, errors) {
|
||||
if (!Array.isArray(value) || value.length === 0 || value.length > SURFACES.size) {
|
||||
errors.push(`${path}.allowedSurfaces_must_be_nonempty_bounded_array`);
|
||||
return;
|
||||
}
|
||||
if (new Set(value).size !== value.length) errors.push(`${path}.allowedSurfaces_must_be_unique`);
|
||||
value.forEach((surface) => {
|
||||
if (!SURFACES.has(surface)) errors.push(`${path}.allowedSurfaces_contains_invalid_surface`);
|
||||
});
|
||||
}
|
||||
|
||||
function validateSemanticReading(value, path, errors) {
|
||||
if (value === undefined) return;
|
||||
if (!isPlainObject(value)) {
|
||||
errors.push(`${path}.semanticReading_must_be_object`);
|
||||
return;
|
||||
}
|
||||
rejectUnknownKeys(value, READING_KEYS, `${path}.semanticReading`, errors);
|
||||
requiredIdentifier(value.id, `${path}.semanticReading.id`, errors);
|
||||
if (!LABEL_SOURCES.has(value.labelSource)) {
|
||||
errors.push(`${path}.semanticReading.labelSource_invalid`);
|
||||
}
|
||||
if (value.labelSource === "registry"
|
||||
&& (typeof value.label !== "string" || !value.label.trim() || value.label.length > 160)) {
|
||||
errors.push(`${path}.semanticReading.label_invalid`);
|
||||
}
|
||||
if (value.labelSource === "provider_configured"
|
||||
&& value.label !== undefined
|
||||
&& (typeof value.label !== "string" || !value.label.trim() || value.label.length > 160)) {
|
||||
errors.push(`${path}.semanticReading.label_invalid`);
|
||||
}
|
||||
if (!UNIT_SOURCES.has(value.unitSource)) {
|
||||
errors.push(`${path}.semanticReading.unitSource_invalid`);
|
||||
}
|
||||
if (value.unitSource === "registry"
|
||||
&& (typeof value.unit !== "string" || !value.unit.trim() || value.unit.length > 32)) {
|
||||
errors.push(`${path}.semanticReading.unit_invalid`);
|
||||
}
|
||||
if (value.unitSource === "none" && value.unit !== undefined) {
|
||||
errors.push(`${path}.semanticReading.unit_not_allowed`);
|
||||
}
|
||||
if (value.unit !== undefined && (typeof value.unit !== "string" || value.unit.length > 32)) {
|
||||
errors.push(`${path}.semanticReading.unit_invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
function canonicalDigest(value) {
|
||||
return `sha256:${createHash("sha256").update(stableJson(value), "utf8").digest("hex")}`;
|
||||
}
|
||||
|
||||
function stableJson(value) {
|
||||
return JSON.stringify(stableValue(value));
|
||||
}
|
||||
|
||||
function stableValue(value) {
|
||||
if (Array.isArray(value)) return value.map(stableValue);
|
||||
if (!isPlainObject(value)) return value;
|
||||
return Object.fromEntries(
|
||||
Object.keys(value)
|
||||
.filter((key) => value[key] !== undefined)
|
||||
.sort()
|
||||
.map((key) => [key, stableValue(value[key])]),
|
||||
);
|
||||
}
|
||||
|
||||
function rejectUnknownKeys(value, allowed, path, errors) {
|
||||
for (const key of Object.keys(value)) {
|
||||
if (!allowed.has(key)) errors.push(`${path}.${key}_not_allowed`);
|
||||
}
|
||||
}
|
||||
|
||||
function requiredIdentifier(value, path, errors) {
|
||||
if (typeof value !== "string" || !IDENTIFIER.test(value)) errors.push(`${path}_invalid`);
|
||||
}
|
||||
|
||||
function requiredSemver(value, path, errors) {
|
||||
if (typeof value !== "string" || !SEMVER.test(value)) errors.push(`${path}_invalid`);
|
||||
}
|
||||
|
||||
function result(errors) {
|
||||
return { ok: errors.length === 0, errors: [...new Set(errors)] };
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function deepFreeze(input) {
|
||||
if (!input || typeof input !== "object" || Object.isFrozen(input)) return input;
|
||||
Object.freeze(input);
|
||||
for (const child of Object.values(input)) deepFreeze(child);
|
||||
return input;
|
||||
}
|
||||
Reference in New Issue
Block a user