Files
NODEDC_PLATFORM/packages/external-provider-contract/src/provider-package.mjs
T

1458 lines
68 KiB
JavaScript

export const PROVIDER_PACKAGE_SCHEMA_VERSION = "nodedc.external-provider-package/v1";
export const L2_TEMPLATE_SCHEMA_VERSION = "nodedc.l2-template/v1";
export const L2_CONNECTION_INSTANCE_SCHEMA_VERSION = "nodedc.l2-connection-instance/v1";
export const SEMANTIC_MAPPING_SCHEMA_VERSION = "nodedc.semantic-mapping/v1";
import { SECRET_LIKE_VALUE as SECRET_VALUE } from "./sensitive-field-policy.mjs";
import { isNdcCredentialReferenceValue } from "./credential-reference.mjs";
import { isBoundedTelemetryReadings } from "./telemetry-readings.mjs";
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?$/i;
const SOURCE_PATH = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
const SAFE_PATH_SEGMENT = /^(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[A-Fa-f0-9]{2})*$/;
const MAGIC_PATH_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
const SECRET_QUERY_KEY = /(?:token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key)/i;
const SECRET_FIELD_NAME = /(?:token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key)/i;
const CAPABILITY_CLASSIFICATIONS = new Set(["read", "metadata", "write", "destructive"]);
const CAPABILITY_STATUSES = new Set(["implemented", "catalogued", "disabled"]);
const AUTH_KINDS = new Set(["api_token", "oauth2", "basic", "custom"]);
const TOKEN_ARTIFACTS = new Set(["access", "refresh"]);
const TOKEN_REFRESH_MODES = new Set(["not_applicable", "operator_managed", "runtime_managed"]);
const COLLECTION_MODES = new Set(["realtime", "manual", "history", "weekly"]);
const DELIVERY_MODES = new Set(["snapshot", "snapshot+patch", "query"]);
const HISTORY_MODES = new Set(["none", "all", "sampled"]);
const FIELD_CONTRACT_TYPES = new Set(["string", "number", "boolean", "string_array", "telemetry_readings", "point", "geometry"]);
const L2_STEP_KINDS = new Set([
"collection_trigger",
"provider_request",
"extract_items",
"semantic_mapping",
"data_product_publish",
]);
const L2_CONNECTION_PARAMETERS = Object.freeze([
"tenant_id",
"connection_id",
"collection_profile_id",
"provider_credential_ref",
]);
const PACKAGE_KEYS = new Set([
"schemaVersion",
"id",
"providerId",
"version",
"manifest",
"authModes",
"capabilities",
"fieldPolicies",
"collectionProfiles",
"dataProducts",
"mappingContracts",
"l2Templates",
]);
const MANIFEST_KEYS = new Set([
"id",
"providerId",
"version",
"ontology",
"authModeIds",
"capabilityIds",
"fieldPolicyIds",
"collectionProfileIds",
"dataProductIds",
"mappingContractIds",
"l2TemplateIds",
]);
const AUTH_MODE_KEYS = new Set([
"id",
"kind",
"credentialOwner",
"bindingCardinality",
"transport",
"tokenLifecycle",
"secretHandling",
]);
const AUTH_TRANSPORT_KEYS = new Set(["placement", "name"]);
const TOKEN_LIFECYCLE_KEYS = new Set(["artifacts", "requestArtifact", "refreshMode"]);
const SECRET_HANDLING_KEYS = new Set(["store", "exposeToGraph", "persistInPackage"]);
const CAPABILITY_KEYS = new Set([
"id",
"classification",
"status",
"authModeId",
"request",
"entityScope",
]);
const REQUEST_KEYS = new Set(["method", "baseUrl", "path", "query", "response"]);
const RESPONSE_KEYS = new Set(["collectionPaths", "pagination"]);
const OFFSET_PAGINATION_KEYS = new Set([
"mode", "limitParameter", "offsetParameter", "pageSize", "itemsPath", "totalPath",
"maxPages", "maxItems", "maxResponseBytes",
]);
const ENTITY_SCOPE_KEYS = new Set(["mode", "refresh", "businessEntityFilter"]);
const FIELD_POLICY_KEYS = new Set([
"id",
"version",
"dataProductId",
"targetFields",
"unknownSourceFields",
"dynamicSourceFields",
"restrictedSourcePaths",
]);
const COLLECTION_PROFILE_KEYS = new Set([
"id",
"version",
"mode",
"schedule",
"capabilityIds",
"dataProductId",
"mappingContractId",
"fieldPolicyId",
"l2TemplateId",
"entityScope",
"batching",
"cardinality",
"retry",
]);
const DATA_PRODUCT_KEYS = new Set([
"id",
"version",
"ontologyRevision",
"deliveryMode",
"semanticTypes",
"fields",
"fieldContracts",
"history",
]);
const FIELD_CONTRACT_KEYS = new Set(["type", "required", "enum", "minimum", "maximum"]);
const HISTORY_KEYS = new Set(["mode", "intervalMs", "strategy", "retentionDays"]);
const MAPPING_KEYS = new Set([
"schemaVersion",
"id",
"version",
"sourceCapabilityId",
"fieldPolicyId",
"target",
"derivations",
"fact",
]);
const MAPPING_TARGET_KEYS = new Set(["dataProductId", "version", "ontologyRevision", "semanticType"]);
const FACT_KEYS = new Set(["sourceId", "semanticType", "observedAt", "geometry", "attributes"]);
const EXPRESSION_KEYS = new Set([
"strategy", "paths", "coerce", "prefix", "fallback", "constant", "derive",
"omitIfMissing", "omitIfInvalid", "minimum", "maximum", "guardPath", "guardEquals",
]);
const GEOMETRY_KEYS = new Set([
"type", "longitude", "latitude", "strategy", "paths", "allowedTypes", "omitIfInvalid",
"kindPath", "polygonPointsPath", "corridorPointsPath", "radiusPath",
"coordinateOrder", "circleSegments",
]);
const DERIVATION_KEYS = new Set(["kind", "rules", "default", "parameters"]);
const TEMPLATE_KEYS = new Set([
"schemaVersion",
"id",
"version",
"runtime",
"instanceMode",
"connectionParameters",
"credentialBindings",
"steps",
"invariants",
]);
const CREDENTIAL_BINDING_KEYS = new Set(["role", "owner", "management", "authModeId", "nodeType"]);
const STEP_KEYS = new Set([
"id",
"kind",
"collectionProfileDriven",
"capabilityId",
"mappingContractId",
"dataProductId",
"nodeType",
]);
const INVARIANT_KEYS = new Set([
"oneConnectionPerInstance",
"providerSecretByReferenceOnly",
"publisherBindingControlPlaneManaged",
"callerScopeForbidden",
"providerSpecificServiceForbidden",
]);
const INSTANCE_OPTION_KEYS = new Set([
"tenantId",
"connectionId",
"collectionProfileId",
"providerCredentialRef",
]);
/**
* Validates one immutable provider package. Runtime account state and secret
* values deliberately live outside this artifact.
*/
export function validateProviderPackage(value) {
const errors = [];
if (!isPlainObject(value)) return result(["providerPackage_must_be_object"]);
rejectUnknownKeys(value, PACKAGE_KEYS, "providerPackage", errors);
if (value.schemaVersion !== PROVIDER_PACKAGE_SCHEMA_VERSION) errors.push("schemaVersion_mismatch");
requiredIdentifier(value.id, "id", errors);
requiredIdentifier(value.providerId, "providerId", errors);
requiredSemver(value.version, "version", errors);
validateManifest(value.manifest, errors);
const authModes = validateArtifactArray(value.authModes, "authModes", validateAuthMode, errors);
const capabilities = validateArtifactArray(value.capabilities, "capabilities", validateCapability, errors);
const fieldPolicies = validateArtifactArray(value.fieldPolicies, "fieldPolicies", validateFieldPolicy, errors);
const collectionProfiles = validateArtifactArray(value.collectionProfiles, "collectionProfiles", validateCollectionProfileTemplate, errors);
const dataProducts = validateArtifactArray(value.dataProducts, "dataProducts", validateDataProductDefinition, errors);
const mappingContracts = validateArtifactArray(value.mappingContracts, "mappingContracts", validateMappingContract, errors);
const l2Templates = validateArtifactArray(value.l2Templates, "l2Templates", validateL2Template, errors);
if (isPlainObject(value.manifest)) {
if (value.manifest.providerId !== value.providerId) errors.push("manifest.providerId_must_match_package");
if (value.manifest.version !== value.version) errors.push("manifest.version_must_match_package");
assertExactReferences(value.manifest.authModeIds, authModes, "manifest.authModeIds", errors);
assertExactReferences(value.manifest.capabilityIds, capabilities, "manifest.capabilityIds", errors);
assertExactReferences(value.manifest.fieldPolicyIds, fieldPolicies, "manifest.fieldPolicyIds", errors);
assertExactReferences(value.manifest.collectionProfileIds, collectionProfiles, "manifest.collectionProfileIds", errors);
assertExactReferences(value.manifest.dataProductIds, dataProducts, "manifest.dataProductIds", errors);
assertExactReferences(value.manifest.mappingContractIds, mappingContracts, "manifest.mappingContractIds", errors);
assertExactReferences(value.manifest.l2TemplateIds, l2Templates, "manifest.l2TemplateIds", errors);
}
const authModeIds = ids(authModes);
const capabilityIds = ids(capabilities);
const fieldPolicyIds = ids(fieldPolicies);
const collectionProfileIds = ids(collectionProfiles);
const dataProductIds = ids(dataProducts);
const mappingContractIds = ids(mappingContracts);
const l2TemplateIds = ids(l2Templates);
for (const capability of capabilities) {
assertReference(capability.authModeId, authModeIds, `capability.${capability.id}.authModeId`, errors);
const authMode = authModes.find((item) => item.id === capability.authModeId);
if (authMode && Object.hasOwn(capability.request?.query || {}, authMode.transport?.name)) {
errors.push(`capability.${capability.id}.request.query_must_not_embed_credential_parameter`);
}
}
for (const fieldPolicy of fieldPolicies) {
assertReference(fieldPolicy.dataProductId, dataProductIds, `fieldPolicy.${fieldPolicy.id}.dataProductId`, errors);
const product = dataProducts.find((item) => item.id === fieldPolicy.dataProductId);
if (product && !sameSet(fieldPolicy.targetFields, product.fields)) {
errors.push(`fieldPolicy.${fieldPolicy.id}.targetFields_must_match_data_product_fields`);
}
}
for (const profile of collectionProfiles) {
const selectedCapabilityIds = Array.isArray(profile.capabilityIds) ? profile.capabilityIds : [];
selectedCapabilityIds.forEach((id) => {
assertReference(id, capabilityIds, `collectionProfile.${profile.id}.capabilityIds`, errors);
const capability = capabilities.find((item) => item.id === id);
if (capability && (capability.status !== "implemented" || !new Set(["read", "metadata"]).has(capability.classification))) {
errors.push(`collectionProfile.${profile.id}.capabilityIds_must_reference_implemented_safe_read`);
}
});
assertReference(profile.dataProductId, dataProductIds, `collectionProfile.${profile.id}.dataProductId`, errors);
assertReference(profile.fieldPolicyId, fieldPolicyIds, `collectionProfile.${profile.id}.fieldPolicyId`, errors);
assertReference(profile.mappingContractId, mappingContractIds, `collectionProfile.${profile.id}.mappingContractId`, errors);
assertReference(profile.l2TemplateId, l2TemplateIds, `collectionProfile.${profile.id}.l2TemplateId`, errors);
const mapping = mappingContracts.find((item) => item.id === profile.mappingContractId);
const fieldPolicy = fieldPolicies.find((item) => item.id === profile.fieldPolicyId);
const template = l2Templates.find((item) => item.id === profile.l2TemplateId);
if (mapping) {
if (!selectedCapabilityIds.includes(mapping.sourceCapabilityId)) {
errors.push(`collectionProfile.${profile.id}.mapping_source_capability_must_be_selected`);
}
for (const derivation of Object.values(mapping.derivations || {})) {
if (
derivation?.kind === "bounded_response_lookup"
&& !selectedCapabilityIds.includes(derivation.parameters?.responseCapabilityId)
) {
errors.push(`collectionProfile.${profile.id}.lookup_response_capability_must_be_selected`);
}
}
if (mapping.fieldPolicyId !== profile.fieldPolicyId) errors.push(`collectionProfile.${profile.id}.mapping_field_policy_mismatch`);
if (mapping.target?.dataProductId !== profile.dataProductId) errors.push(`collectionProfile.${profile.id}.mapping_data_product_mismatch`);
}
if (fieldPolicy && fieldPolicy.dataProductId !== profile.dataProductId) {
errors.push(`collectionProfile.${profile.id}.field_policy_data_product_mismatch`);
}
if (template) {
const steps = Array.isArray(template.steps) ? template.steps : [];
const requestSteps = steps.filter((step) => step?.kind === "provider_request");
const extractSteps = steps.filter((step) => step?.kind === "extract_items");
const mappingSteps = steps.filter((step) => step?.kind === "semantic_mapping");
const publishSteps = steps.filter((step) => step?.kind === "data_product_publish");
const executedCapabilityIds = requestSteps.map((step) => step.capabilityId);
if (!sameSet(selectedCapabilityIds, executedCapabilityIds)
|| extractSteps.length !== 1
|| extractSteps[0]?.capabilityId !== mapping?.sourceCapabilityId) {
errors.push(`collectionProfile.${profile.id}.template_capability_chain_mismatch`);
}
if (mappingSteps.length !== 1 || mappingSteps[0]?.mappingContractId !== profile.mappingContractId) errors.push(`collectionProfile.${profile.id}.template_mapping_mismatch`);
if (publishSteps.length !== 1 || publishSteps[0]?.dataProductId !== profile.dataProductId) errors.push(`collectionProfile.${profile.id}.template_data_product_mismatch`);
}
}
for (const mapping of mappingContracts) {
assertReference(mapping.sourceCapabilityId, capabilityIds, `mappingContract.${mapping.id}.sourceCapabilityId`, errors);
for (const [derivationId, derivation] of Object.entries(mapping.derivations || {})) {
if (derivation?.kind === "bounded_response_lookup") {
assertReference(
derivation.parameters?.responseCapabilityId,
capabilityIds,
`mappingContract.${mapping.id}.derivations.${derivationId}.parameters.responseCapabilityId`,
errors,
);
}
}
assertReference(mapping.fieldPolicyId, fieldPolicyIds, `mappingContract.${mapping.id}.fieldPolicyId`, errors);
assertReference(mapping.target?.dataProductId, dataProductIds, `mappingContract.${mapping.id}.target.dataProductId`, errors);
const product = dataProducts.find((item) => item.id === mapping.target?.dataProductId);
const fieldPolicy = fieldPolicies.find((item) => item.id === mapping.fieldPolicyId);
if (product) validateMappingAgainstProduct(mapping, product, errors);
if (fieldPolicy) {
if (fieldPolicy.dataProductId !== mapping.target?.dataProductId) {
errors.push(`mappingContract.${mapping.id}.field_policy_data_product_mismatch`);
}
const restricted = Array.isArray(fieldPolicy.restrictedSourcePaths)
? fieldPolicy.restrictedSourcePaths.filter(isCanonicalSourcePath)
: [];
if (mappingSourcePaths({
fact: mapping.fact,
derivations: mapping.derivations,
}).some((path) => (
restricted.some((restrictedPath) => (
path === restrictedPath
|| path.startsWith(`${restrictedPath}.`)
|| restrictedPath.startsWith(`${path}.`)
))
))) {
errors.push(`mappingContract.${mapping.id}.fact_source_path_restricted`);
}
}
}
for (const template of l2Templates) {
for (const binding of Array.isArray(template.credentialBindings) ? template.credentialBindings : []) {
if (!isPlainObject(binding)) continue;
if (binding.authModeId !== undefined) {
assertReference(binding.authModeId, authModeIds, `l2Template.${template.id}.credentialBindings.authModeId`, errors);
}
}
for (const step of Array.isArray(template.steps) ? template.steps : []) {
if (!isPlainObject(step)) continue;
if (step.capabilityId !== undefined) {
assertReference(step.capabilityId, capabilityIds, `l2Template.${template.id}.steps.capabilityId`, errors);
if (step.kind === "provider_request") {
const capability = capabilities.find((item) => item.id === step.capabilityId);
if (capability && (capability.status !== "implemented" || !new Set(["read", "metadata"]).has(capability.classification))) {
errors.push(`l2Template.${template.id}.provider_request_must_reference_implemented_safe_read`);
}
}
}
if (step.mappingContractId !== undefined) assertReference(step.mappingContractId, mappingContractIds, `l2Template.${template.id}.steps.mappingContractId`, errors);
if (step.dataProductId !== undefined) assertReference(step.dataProductId, dataProductIds, `l2Template.${template.id}.steps.dataProductId`, errors);
}
const providerBinding = Array.isArray(template.credentialBindings)
? template.credentialBindings.find((binding) => binding?.role === "provider")
: undefined;
const providerRequests = Array.isArray(template.steps)
? template.steps.filter((step) => step?.kind === "provider_request")
: [];
for (const providerRequest of providerRequests) {
const requestCapability = capabilities.find((capability) => capability.id === providerRequest?.capabilityId);
if (providerBinding && requestCapability
&& providerBinding.authModeId !== requestCapability.authModeId) {
errors.push(`l2Template.${template.id}.provider_credential_auth_mode_mismatch`);
}
}
}
if (value.tenantId !== undefined || value.connectionId !== undefined || value.credentialRef !== undefined) {
errors.push("providerPackage_must_not_contain_connection_instance_state");
}
if (containsSecretValue(value)) errors.push("providerPackage_must_not_contain_secret_material");
return result(errors);
}
/**
* Materializes secret-free NDC L2 connection metadata from a provider package.
* A new account is data, not source code: callers supply only the provider
* credential reference. Internal publish binding resolution belongs to the
* NDC L2 control plane and never crosses this input boundary.
*/
export function instantiateL2Connection(providerPackage, options) {
const packageValidation = validateProviderPackage(providerPackage);
if (!packageValidation.ok) {
throw new Error(`provider_package_invalid:${packageValidation.errors.join(",")}`);
}
const errors = [];
if (!isPlainObject(options)) throw new Error("l2_connection_options_must_be_object");
rejectUnknownKeys(options, INSTANCE_OPTION_KEYS, "options", errors);
requiredIdentifier(options.tenantId, "tenantId", errors);
requiredIdentifier(options.connectionId, "connectionId", errors);
requiredIdentifier(options.collectionProfileId, "collectionProfileId", errors);
requiredOpaqueReference(options.providerCredentialRef, "providerCredentialRef", errors);
if (containsSecretValue(options)) errors.push("l2_connection_options_must_use_opaque_references");
const profile = providerPackage.collectionProfiles.find((item) => item.id === options.collectionProfileId);
if (!profile) errors.push("collectionProfileId_not_in_provider_package");
const template = providerPackage.l2Templates.find((item) => item.id === profile?.l2TemplateId);
if (!template) errors.push("l2Template_missing");
if (errors.length) throw new Error(`l2_connection_invalid:${[...new Set(errors)].join(",")}`);
const publisherBinding = template.credentialBindings.find((binding) => binding.role === "publisher");
return deepFreeze({
schemaVersion: L2_CONNECTION_INSTANCE_SCHEMA_VERSION,
package: {
id: providerPackage.id,
providerId: providerPackage.providerId,
version: providerPackage.version,
},
tenantId: options.tenantId,
connectionId: options.connectionId,
l2TemplateId: template.id,
collectionProfileId: profile.id,
credentialRefs: {
provider: { owner: "ndc_l2_credentials", reference: options.providerCredentialRef },
},
systemBindings: {
publisher: {
role: "publisher",
owner: publisherBinding.owner,
management: publisherBinding.management,
nodeType: publisherBinding.nodeType,
desiredState: "bound",
status: "unresolved",
},
},
scope: {
mode: "all_visible_to_credential",
refresh: "each_collection_run",
capabilityIds: [...profile.capabilityIds],
},
});
}
function validateManifest(value, errors) {
if (!isPlainObject(value)) {
errors.push("manifest_must_be_object");
return;
}
rejectUnknownKeys(value, MANIFEST_KEYS, "manifest", errors);
requiredIdentifier(value.id, "manifest.id", errors);
requiredIdentifier(value.providerId, "manifest.providerId", errors);
requiredSemver(value.version, "manifest.version", errors);
if (!isPlainObject(value.ontology)) {
errors.push("manifest.ontology_must_be_object");
} else {
rejectUnknownKeys(value.ontology, new Set(["packageId", "revision"]), "manifest.ontology", errors);
requiredIdentifier(value.ontology.packageId, "manifest.ontology.packageId", errors);
requiredIdentifier(value.ontology.revision, "manifest.ontology.revision", errors);
}
for (const key of ["authModeIds", "capabilityIds", "fieldPolicyIds", "collectionProfileIds", "dataProductIds", "mappingContractIds", "l2TemplateIds"]) {
requiredUniqueIdentifierArray(value[key], `manifest.${key}`, errors);
}
}
function validateAuthMode(value, path, errors) {
rejectUnknownKeys(value, AUTH_MODE_KEYS, path, errors);
requiredIdentifier(value.id, `${path}.id`, errors);
if (!AUTH_KINDS.has(value.kind)) errors.push(`${path}.kind_invalid`);
if (value.credentialOwner !== "ndc_l2_credentials") errors.push(`${path}.credentialOwner_must_be_ndc_l2_credentials`);
if (value.bindingCardinality !== "one_per_connection") errors.push(`${path}.bindingCardinality_must_be_one_per_connection`);
if (!isPlainObject(value.transport)) {
errors.push(`${path}.transport_must_be_object`);
} else {
rejectUnknownKeys(value.transport, AUTH_TRANSPORT_KEYS, `${path}.transport`, errors);
if (!new Set(["header", "query", "body"]).has(value.transport.placement)) errors.push(`${path}.transport.placement_invalid`);
requiredString(value.transport.name, `${path}.transport.name`, errors);
}
if (value.tokenLifecycle !== undefined) {
validateTokenLifecycle(value.tokenLifecycle, `${path}.tokenLifecycle`, errors);
}
if (!isPlainObject(value.secretHandling)) {
errors.push(`${path}.secretHandling_must_be_object`);
} else {
rejectUnknownKeys(value.secretHandling, SECRET_HANDLING_KEYS, `${path}.secretHandling`, errors);
if (value.secretHandling.store !== "ndc_l2_credentials") errors.push(`${path}.secretHandling.store_must_be_ndc_l2_credentials`);
if (value.secretHandling.exposeToGraph !== false) errors.push(`${path}.secretHandling.exposeToGraph_must_be_false`);
if (value.secretHandling.persistInPackage !== false) errors.push(`${path}.secretHandling.persistInPackage_must_be_false`);
}
}
function validateTokenLifecycle(value, path, errors) {
if (!isPlainObject(value)) {
errors.push(`${path}_must_be_object`);
return;
}
rejectUnknownKeys(value, TOKEN_LIFECYCLE_KEYS, path, errors);
if (!Array.isArray(value.artifacts) || value.artifacts.length === 0) {
errors.push(`${path}.artifacts_must_be_nonempty_array`);
} else {
for (const artifact of value.artifacts) {
if (!TOKEN_ARTIFACTS.has(artifact)) errors.push(`${path}.artifacts_invalid`);
}
if (new Set(value.artifacts).size !== value.artifacts.length) {
errors.push(`${path}.artifacts_must_not_contain_duplicates`);
}
}
if (value.requestArtifact !== "access") {
errors.push(`${path}.requestArtifact_must_be_access`);
} else if (Array.isArray(value.artifacts) && !value.artifacts.includes(value.requestArtifact)) {
errors.push(`${path}.requestArtifact_must_reference_artifact`);
}
const hasRefresh = Array.isArray(value.artifacts) && value.artifacts.includes("refresh");
if (!TOKEN_REFRESH_MODES.has(value.refreshMode)) {
errors.push(`${path}.refreshMode_invalid`);
} else if (hasRefresh && value.refreshMode === "not_applicable") {
errors.push(`${path}.refreshMode_required_for_refresh_artifact`);
} else if (!hasRefresh && value.refreshMode !== "not_applicable") {
errors.push(`${path}.refreshMode_must_be_not_applicable_without_refresh_artifact`);
}
}
function validateCapability(value, path, errors) {
rejectUnknownKeys(value, CAPABILITY_KEYS, path, errors);
requiredIdentifier(value.id, `${path}.id`, errors);
if (!CAPABILITY_CLASSIFICATIONS.has(value.classification)) errors.push(`${path}.classification_invalid`);
if (!CAPABILITY_STATUSES.has(value.status)) errors.push(`${path}.status_invalid`);
requiredIdentifier(value.authModeId, `${path}.authModeId`, errors);
if (!isPlainObject(value.request)) {
errors.push(`${path}.request_must_be_object`);
} else {
rejectUnknownKeys(value.request, REQUEST_KEYS, `${path}.request`, errors);
if (value.request.method !== "GET") errors.push(`${path}.request.method_must_be_GET_for_v1`);
validateProviderBaseUrl(value.request.baseUrl, `${path}.request.baseUrl`, errors);
validateProviderRequestPath(value.request.path, `${path}.request.path`, errors);
validateStaticQuery(value.request.query, `${path}.request.query`, errors);
if (!isPlainObject(value.request.response)) {
errors.push(`${path}.request.response_must_be_object`);
} else {
rejectUnknownKeys(value.request.response, RESPONSE_KEYS, `${path}.request.response`, errors);
requiredCollectionPathArray(value.request.response.collectionPaths, `${path}.request.response.collectionPaths`, errors);
validateResponsePagination(value.request.response.pagination, value.request.query, `${path}.request.response.pagination`, errors);
}
}
validateCredentialVisibleScope(value.entityScope, `${path}.entityScope`, errors);
}
function validateResponsePagination(value, query, path, errors) {
if (value === "single_bounded_response") return;
if (!isPlainObject(value)) {
errors.push(`${path}_invalid`);
return;
}
rejectUnknownKeys(value, OFFSET_PAGINATION_KEYS, path, errors);
if (value.mode !== "offset") errors.push(`${path}.mode_must_be_offset`);
for (const field of ["limitParameter", "offsetParameter"]) {
if (typeof value[field] !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(value[field])) {
errors.push(`${path}.${field}_invalid`);
}
}
if (!Number.isInteger(value.pageSize) || value.pageSize < 1 || value.pageSize > 500) {
errors.push(`${path}.pageSize_invalid`);
}
if (!Number.isInteger(value.maxPages) || value.maxPages < 1 || value.maxPages > 1000) {
errors.push(`${path}.maxPages_invalid`);
}
if (!Number.isInteger(value.maxItems) || value.maxItems < 1 || value.maxItems > 5000) {
errors.push(`${path}.maxItems_invalid`);
}
if (!Number.isInteger(value.maxResponseBytes) || value.maxResponseBytes < 1024 || value.maxResponseBytes > 32 * 1024 * 1024) {
errors.push(`${path}.maxResponseBytes_invalid`);
}
for (const field of ["itemsPath", "totalPath"]) {
if (!isCanonicalSourcePath(value[field])) errors.push(`${path}.${field}_invalid`);
}
if (isPlainObject(query)) {
if (query[value.limitParameter] !== value.pageSize) errors.push(`${path}.pageSize_must_match_static_query_limit`);
if (query[value.offsetParameter] !== 0) errors.push(`${path}.offset_query_must_start_at_zero`);
}
if (Number.isInteger(value.maxPages) && Number.isInteger(value.pageSize)
&& Number.isInteger(value.maxItems) && value.maxPages * value.pageSize < value.maxItems) {
errors.push(`${path}.maxPages_cannot_cover_maxItems`);
}
}
function validateFieldPolicy(value, path, errors) {
rejectUnknownKeys(value, FIELD_POLICY_KEYS, path, errors);
requiredIdentifier(value.id, `${path}.id`, errors);
requiredSemver(value.version, `${path}.version`, errors);
requiredIdentifier(value.dataProductId, `${path}.dataProductId`, errors);
requiredUniqueIdentifierArray(value.targetFields, `${path}.targetFields`, errors);
if (Array.isArray(value.targetFields) && value.targetFields.some((field) => SECRET_FIELD_NAME.test(String(field)))) {
errors.push(`${path}.targetFields_must_not_contain_secret_fields`);
}
if (value.unknownSourceFields !== "drop") errors.push(`${path}.unknownSourceFields_must_be_drop`);
if (value.dynamicSourceFields !== "drop_until_classified") errors.push(`${path}.dynamicSourceFields_must_be_drop_until_classified`);
requiredSourcePathArray(value.restrictedSourcePaths, `${path}.restrictedSourcePaths`, errors);
}
function validateCollectionProfileTemplate(value, path, errors) {
rejectUnknownKeys(value, COLLECTION_PROFILE_KEYS, path, errors);
requiredIdentifier(value.id, `${path}.id`, errors);
requiredSemver(value.version, `${path}.version`, errors);
if (!COLLECTION_MODES.has(value.mode)) errors.push(`${path}.mode_invalid`);
requiredUniqueIdentifierArray(value.capabilityIds, `${path}.capabilityIds`, errors);
requiredIdentifier(value.dataProductId, `${path}.dataProductId`, errors);
requiredIdentifier(value.mappingContractId, `${path}.mappingContractId`, errors);
requiredIdentifier(value.fieldPolicyId, `${path}.fieldPolicyId`, errors);
requiredIdentifier(value.l2TemplateId, `${path}.l2TemplateId`, errors);
if (value.mode === "realtime") {
if (!isPlainObject(value.schedule)) errors.push(`${path}.schedule_must_be_object`);
else if (!Number.isInteger(value.schedule.intervalMs) || value.schedule.intervalMs < 1000) errors.push(`${path}.schedule.intervalMs_invalid`);
} else if (value.schedule !== undefined) {
errors.push(`${path}.schedule_not_allowed_for_non_realtime_profile`);
}
validateCredentialVisibleScope(value.entityScope, `${path}.entityScope`, errors);
if (!isPlainObject(value.batching) || !Number.isInteger(value.batching.maxFacts) || value.batching.maxFacts < 1 || value.batching.maxFacts > 5000) {
errors.push(`${path}.batching.maxFacts_must_be_integer_1_to_5000`);
} else {
rejectUnknownKeys(value.batching, new Set(["maxFacts"]), `${path}.batching`, errors);
}
if (!isPlainObject(value.cardinality)) {
errors.push(`${path}.cardinality_must_be_object`);
} else {
rejectUnknownKeys(value.cardinality, new Set(["maxCurrentEntities", "onExceed"]), `${path}.cardinality`, errors);
if (value.cardinality.maxCurrentEntities !== 5000) errors.push(`${path}.cardinality.maxCurrentEntities_must_be_5000`);
if (value.cardinality.onExceed !== "require_partitioned_data_product") errors.push(`${path}.cardinality.onExceed_invalid`);
}
if (!isPlainObject(value.retry)) {
errors.push(`${path}.retry_must_be_object`);
} else {
rejectUnknownKeys(value.retry, new Set(["maxAttempts", "backoff", "delayMs"]), `${path}.retry`, errors);
if (!Number.isInteger(value.retry.maxAttempts) || value.retry.maxAttempts < 1 || value.retry.maxAttempts > 10) errors.push(`${path}.retry.maxAttempts_invalid`);
if (!["exponential_with_jitter", "fixed_delay"].includes(value.retry.backoff)) {
errors.push(`${path}.retry.backoff_invalid`);
}
if (value.retry.backoff === "fixed_delay") {
if (!Number.isInteger(value.retry.delayMs) || value.retry.delayMs < 100 || value.retry.delayMs > 60_000) {
errors.push(`${path}.retry.delayMs_invalid`);
}
} else if (value.retry.delayMs !== undefined) {
errors.push(`${path}.retry.delayMs_requires_fixed_delay`);
}
}
}
function validateDataProductDefinition(value, path, errors) {
rejectUnknownKeys(value, DATA_PRODUCT_KEYS, path, errors);
requiredIdentifier(value.id, `${path}.id`, errors);
requiredSemver(value.version, `${path}.version`, errors);
requiredIdentifier(value.ontologyRevision, `${path}.ontologyRevision`, errors);
if (!DELIVERY_MODES.has(value.deliveryMode)) errors.push(`${path}.deliveryMode_invalid`);
requiredUniqueIdentifierArray(value.semanticTypes, `${path}.semanticTypes`, errors);
requiredUniqueIdentifierArray(value.fields, `${path}.fields`, errors);
if (Array.isArray(value.fields) && value.fields.some((field) => SECRET_FIELD_NAME.test(String(field)))) {
errors.push(`${path}.fields_must_not_contain_secret_fields`);
}
validateFieldContracts(value.fieldContracts, value.fields, `${path}.fieldContracts`, errors);
if (!isPlainObject(value.history)) {
errors.push(`${path}.history_must_be_object`);
} else {
rejectUnknownKeys(value.history, HISTORY_KEYS, `${path}.history`, errors);
if (!HISTORY_MODES.has(value.history.mode)) errors.push(`${path}.history.mode_invalid`);
if (value.history.mode === "sampled") {
if (!Number.isInteger(value.history.intervalMs)
|| value.history.intervalMs < 1000
|| value.history.intervalMs > 24 * 60 * 60 * 1000) {
errors.push(`${path}.history.intervalMs_invalid`);
}
if (value.history.strategy !== "latest-per-entity-per-bucket") {
errors.push(`${path}.history.strategy_invalid`);
}
} else if (value.history.intervalMs !== undefined || value.history.strategy !== undefined) {
errors.push(`${path}.history.sampling_fields_forbidden`);
}
if (!Number.isInteger(value.history.retentionDays)
|| value.history.retentionDays < 1
|| value.history.retentionDays > 3650) {
errors.push(`${path}.history.retentionDays_invalid`);
}
}
}
function validateMappingContract(value, path, errors) {
rejectUnknownKeys(value, MAPPING_KEYS, path, errors);
if (value.schemaVersion !== SEMANTIC_MAPPING_SCHEMA_VERSION) errors.push(`${path}.schemaVersion_mismatch`);
requiredIdentifier(value.id, `${path}.id`, errors);
requiredSemver(value.version, `${path}.version`, errors);
requiredIdentifier(value.sourceCapabilityId, `${path}.sourceCapabilityId`, errors);
requiredIdentifier(value.fieldPolicyId, `${path}.fieldPolicyId`, errors);
if (!isPlainObject(value.target)) {
errors.push(`${path}.target_must_be_object`);
} else {
rejectUnknownKeys(value.target, MAPPING_TARGET_KEYS, `${path}.target`, errors);
requiredIdentifier(value.target.dataProductId, `${path}.target.dataProductId`, errors);
requiredSemver(value.target.version, `${path}.target.version`, errors);
requiredIdentifier(value.target.ontologyRevision, `${path}.target.ontologyRevision`, errors);
requiredIdentifier(value.target.semanticType, `${path}.target.semanticType`, errors);
}
if (!isPlainObject(value.fact)) {
errors.push(`${path}.fact_must_be_object`);
return;
}
rejectUnknownKeys(value.fact, FACT_KEYS, `${path}.fact`, errors);
validateExpression(value.fact.sourceId, `${path}.fact.sourceId`, errors);
validateExpression(value.fact.semanticType, `${path}.fact.semanticType`, errors);
validateExpression(value.fact.observedAt, `${path}.fact.observedAt`, errors);
if (value.fact.geometry !== undefined && !isPlainObject(value.fact.geometry)) {
errors.push(`${path}.fact.geometry_must_be_object`);
} else if (isPlainObject(value.fact.geometry)) {
rejectUnknownKeys(value.fact.geometry, GEOMETRY_KEYS, `${path}.fact.geometry`, errors);
if (value.fact.geometry.type === "Point") {
validateExpression(value.fact.geometry.longitude, `${path}.fact.geometry.longitude`, errors);
validateExpression(value.fact.geometry.latitude, `${path}.fact.geometry.latitude`, errors);
if (value.fact.geometry.omitIfInvalid !== true) errors.push(`${path}.fact.geometry.omitIfInvalid_must_be_true`);
if (value.fact.geometry.strategy !== undefined || value.fact.geometry.paths !== undefined || value.fact.geometry.allowedTypes !== undefined) {
errors.push(`${path}.fact.geometry.point_shape_invalid`);
}
} else if (value.fact.geometry.type === "GeoJSON") {
if (value.fact.geometry.strategy !== "bounded_zone_geometry_v1") errors.push(`${path}.fact.geometry.strategy_invalid`);
for (const key of ["kindPath", "polygonPointsPath", "corridorPointsPath", "radiusPath"]) {
if (!isCanonicalSourcePath(value.fact.geometry[key])) {
errors.push(`${path}.fact.geometry.${key}_invalid`);
}
}
if (!["latitude_longitude", "longitude_latitude"].includes(value.fact.geometry.coordinateOrder)) {
errors.push(`${path}.fact.geometry.coordinateOrder_invalid`);
}
if (!Number.isInteger(value.fact.geometry.circleSegments)
|| value.fact.geometry.circleSegments < 16
|| value.fact.geometry.circleSegments > 256) {
errors.push(`${path}.fact.geometry.circleSegments_invalid`);
}
if (!sameSet(value.fact.geometry.allowedTypes, ["Polygon", "MultiPolygon"])) {
errors.push(`${path}.fact.geometry.allowedTypes_must_be_polygon_or_multipolygon`);
}
if (value.fact.geometry.omitIfInvalid !== false) errors.push(`${path}.fact.geometry.omitIfInvalid_must_be_false`);
if (
value.fact.geometry.longitude !== undefined
|| value.fact.geometry.latitude !== undefined
|| value.fact.geometry.paths !== undefined
) {
errors.push(`${path}.fact.geometry.geojson_shape_invalid`);
}
} else {
errors.push(`${path}.fact.geometry.type_invalid`);
}
}
if (value.fact.attributes !== undefined && !isPlainObject(value.fact.attributes)) {
errors.push(`${path}.fact.attributes_must_be_object`);
} else if (isPlainObject(value.fact.attributes)) {
for (const [attribute, expression] of Object.entries(value.fact.attributes)) {
requiredIdentifier(attribute, `${path}.fact.attributes.${attribute}`, errors);
if (SECRET_FIELD_NAME.test(attribute)) errors.push(`${path}.fact.attributes.${attribute}_secret_field_not_allowed`);
validateExpression(expression, `${path}.fact.attributes.${attribute}`, errors);
}
}
const derivationReferences = mappingDerivationReferences(value.fact);
if (value.derivations !== undefined && !isPlainObject(value.derivations)) {
errors.push(`${path}.derivations_must_be_object`);
} else if (isPlainObject(value.derivations)) {
for (const [id, derivation] of Object.entries(value.derivations)) {
requiredIdentifier(id, `${path}.derivations.${id}`, errors);
validateDerivation(derivation, `${path}.derivations.${id}`, errors);
}
const derivationIds = new Set(Object.keys(value.derivations));
for (const id of derivationReferences) {
if (!derivationIds.has(id)) errors.push(`${path}.derive_${id}_missing_definition`);
}
for (const id of derivationIds) {
if (!derivationReferences.has(id)) errors.push(`${path}.derivation_${id}_unused`);
}
} else if (derivationReferences.size) {
for (const id of derivationReferences) errors.push(`${path}.derive_${id}_missing_definition`);
}
}
function validateL2Template(value, path, errors) {
rejectUnknownKeys(value, TEMPLATE_KEYS, path, errors);
if (value.schemaVersion !== L2_TEMPLATE_SCHEMA_VERSION) errors.push(`${path}.schemaVersion_mismatch`);
requiredIdentifier(value.id, `${path}.id`, errors);
requiredSemver(value.version, `${path}.version`, errors);
if (value.runtime !== "ndc_l2") errors.push(`${path}.runtime_must_be_ndc_l2`);
if (value.instanceMode !== "one_connection_per_workflow") errors.push(`${path}.instanceMode_must_be_one_connection_per_workflow`);
requiredStringArray(value.connectionParameters, `${path}.connectionParameters`, errors);
if (!sameArray(value.connectionParameters, L2_CONNECTION_PARAMETERS)) {
errors.push(`${path}.connectionParameters_must_match_v1_contract`);
}
if (!Array.isArray(value.credentialBindings) || value.credentialBindings.length !== 2) {
errors.push(`${path}.credentialBindings_must_have_provider_and_publisher`);
} else {
const roles = new Set();
value.credentialBindings.forEach((binding, index) => {
const bindingPath = `${path}.credentialBindings[${index}]`;
if (!isPlainObject(binding)) return errors.push(`${bindingPath}_must_be_object`);
rejectUnknownKeys(binding, CREDENTIAL_BINDING_KEYS, bindingPath, errors);
if (!new Set(["provider", "publisher"]).has(binding.role)) errors.push(`${bindingPath}.role_invalid`);
roles.add(binding.role);
if (binding.owner !== "ndc_l2_credentials") errors.push(`${bindingPath}.owner_must_be_ndc_l2_credentials`);
if (!new Set(["connection_input", "control_plane_managed"]).has(binding.management)) {
errors.push(`${bindingPath}.management_invalid`);
}
if (binding.authModeId !== undefined) requiredIdentifier(binding.authModeId, `${bindingPath}.authModeId`, errors);
if (binding.nodeType !== undefined) requiredString(binding.nodeType, `${bindingPath}.nodeType`, errors);
if (binding.role === "provider" && (
binding.authModeId === undefined
|| binding.nodeType !== undefined
|| binding.management !== "connection_input"
)) {
errors.push(`${bindingPath}.provider_binding_shape_invalid`);
}
if (binding.role === "publisher" && (
binding.authModeId !== undefined
|| binding.nodeType !== "n8n-nodes-ndc.ndcDataProductPublish"
|| binding.management !== "control_plane_managed"
)) {
errors.push(`${bindingPath}.publisher_binding_shape_invalid`);
}
});
if (!roles.has("provider") || !roles.has("publisher")) errors.push(`${path}.credentialBindings_roles_invalid`);
}
if (!Array.isArray(value.steps) || value.steps.length < 5) {
errors.push(`${path}.steps_must_define_complete_boundary`);
} else {
const stepIds = new Set();
value.steps.forEach((step, index) => {
const stepPath = `${path}.steps[${index}]`;
if (!isPlainObject(step)) return errors.push(`${stepPath}_must_be_object`);
rejectUnknownKeys(step, STEP_KEYS, stepPath, errors);
requiredIdentifier(step.id, `${stepPath}.id`, errors);
if (stepIds.has(step.id)) errors.push(`${path}.steps_ids_must_be_unique`);
stepIds.add(step.id);
if (!L2_STEP_KINDS.has(step.kind)) errors.push(`${stepPath}.kind_invalid`);
});
const requestSteps = value.steps.filter((step) => step?.kind === "provider_request");
const extractSteps = value.steps.filter((step) => step?.kind === "extract_items");
const mappingSteps = value.steps.filter((step) => step?.kind === "semantic_mapping");
const publishSteps = value.steps.filter((step) => step?.kind === "data_product_publish");
const requestStart = 1;
const extractIndex = value.steps.findIndex((step) => step?.kind === "extract_items");
const expectedSequence = (
value.steps[0]?.kind === "collection_trigger"
&& requestSteps.length >= 1
&& value.steps.slice(requestStart, extractIndex).every((step) => step?.kind === "provider_request")
&& extractSteps.length === 1
&& mappingSteps.length === 1
&& publishSteps.length === 1
&& value.steps.at(-3)?.kind === "extract_items"
&& value.steps.at(-2)?.kind === "semantic_mapping"
&& value.steps.at(-1)?.kind === "data_product_publish"
);
if (!expectedSequence) errors.push(`${path}.steps_sequence_invalid`);
if (value.steps[0]?.collectionProfileDriven !== true) errors.push(`${path}.collection_trigger_must_be_profile_driven`);
for (const step of requestSteps) {
if (!step?.capabilityId || step?.mappingContractId !== undefined || step?.dataProductId !== undefined) {
errors.push(`${path}.provider_request_shape_invalid`);
}
}
const extractStep = extractSteps[0];
if (!extractStep?.capabilityId || extractStep?.mappingContractId !== undefined || extractStep?.dataProductId !== undefined) {
errors.push(`${path}.extract_items_shape_invalid`);
}
if (!requestSteps.some((step) => step.capabilityId === extractStep?.capabilityId)) errors.push(`${path}.request_extract_capability_mismatch`);
const mappingStep = mappingSteps[0];
if (!mappingStep?.mappingContractId || mappingStep?.capabilityId !== undefined || mappingStep?.dataProductId !== undefined) {
errors.push(`${path}.semantic_mapping_shape_invalid`);
}
const publishStep = publishSteps[0];
if (!publishStep?.dataProductId || publishStep?.nodeType !== "n8n-nodes-ndc.ndcDataProductPublish") {
errors.push(`${path}.data_product_publish_shape_invalid`);
}
}
if (!isPlainObject(value.invariants)) {
errors.push(`${path}.invariants_must_be_object`);
} else {
rejectUnknownKeys(value.invariants, INVARIANT_KEYS, `${path}.invariants`, errors);
for (const key of INVARIANT_KEYS) {
if (value.invariants[key] !== true) errors.push(`${path}.invariants.${key}_must_be_true`);
}
}
}
function validateMappingAgainstProduct(mapping, product, errors) {
const path = `mappingContract.${mapping.id}`;
if (mapping.target.version !== product.version) errors.push(`${path}.target.version_must_match_data_product`);
if (mapping.target.ontologyRevision !== product.ontologyRevision) errors.push(`${path}.target.ontologyRevision_must_match_data_product`);
const semanticTypes = Array.isArray(product.semanticTypes) ? product.semanticTypes : [];
if (!semanticTypes.includes(mapping.target.semanticType)) errors.push(`${path}.target.semanticType_not_allowed`);
if (mapping.fact?.semanticType?.constant !== mapping.target.semanticType) {
errors.push(`${path}.fact.semanticType_must_equal_target_constant`);
}
const mappedFields = [...Object.keys(mapping.fact?.attributes || {}), ...(mapping.fact?.geometry ? ["geometry"] : [])];
if (!sameSet(mappedFields, product.fields)) errors.push(`${path}.fact_fields_must_match_data_product_fields`);
const contracts = isPlainObject(product.fieldContracts) ? product.fieldContracts : {};
for (const [field, contract] of Object.entries(contracts)) {
if (!isPlainObject(contract)) continue;
if (field === "geometry") {
if (contract.type === "point" && mapping.fact?.geometry?.type !== "Point") {
errors.push(`${path}.fact.geometry_contract_must_be_point`);
}
if (contract.type === "geometry" && mapping.fact?.geometry?.type !== "GeoJSON") {
errors.push(`${path}.fact.geometry_contract_must_be_geojson`);
}
if (!new Set(["point", "geometry"]).has(contract.type)) errors.push(`${path}.fact.geometry_contract_invalid`);
if (contract.required === true && mapping.fact?.geometry?.omitIfInvalid === true) {
errors.push(`${path}.fact.geometry_required_but_mapping_can_omit`);
}
continue;
}
const expression = mapping.fact?.attributes?.[field];
if (!isPlainObject(expression)) continue;
validateExpressionAgainstFieldContract(expression, contract, mapping.derivations, `${path}.fact.attributes.${field}`, errors);
}
}
function validateFieldContracts(value, fields, path, errors) {
if (value === undefined) return;
if (!isPlainObject(value)) return errors.push(`${path}_must_be_object`);
const names = Object.keys(value);
if (names.length && !sameSet(names, fields)) errors.push(`${path}_must_exactly_match_fields`);
for (const [field, contract] of Object.entries(value)) {
requiredIdentifier(field, `${path}.${field}`, errors);
if (SECRET_FIELD_NAME.test(field)) errors.push(`${path}.${field}_secret_field_not_allowed`);
validateFieldContract(contract, `${path}.${field}`, errors);
}
}
function validateFieldContract(value, path, errors) {
if (!isPlainObject(value)) return errors.push(`${path}_must_be_object`);
rejectUnknownKeys(value, FIELD_CONTRACT_KEYS, path, errors);
if (!FIELD_CONTRACT_TYPES.has(value.type)) errors.push(`${path}.type_invalid`);
if (typeof value.required !== "boolean") errors.push(`${path}.required_must_be_boolean`);
if (value.enum !== undefined) {
if (!Array.isArray(value.enum) || value.enum.length === 0 || new Set(value.enum.map(stableLiteral)).size !== value.enum.length) {
errors.push(`${path}.enum_must_be_nonempty_unique_array`);
} else if (new Set(["point", "geometry", "string_array", "telemetry_readings"]).has(value.type)
|| value.enum.some((item) => !fieldContractValueMatchesType(item, value.type))) {
errors.push(`${path}.enum_value_type_invalid`);
}
}
if (value.minimum !== undefined || value.maximum !== undefined) {
if (value.type !== "number") errors.push(`${path}.range_only_allowed_for_number`);
if (value.minimum !== undefined && !Number.isFinite(value.minimum)) errors.push(`${path}.minimum_invalid`);
if (value.maximum !== undefined && !Number.isFinite(value.maximum)) errors.push(`${path}.maximum_invalid`);
if (Number.isFinite(value.minimum) && Number.isFinite(value.maximum) && value.minimum > value.maximum) {
errors.push(`${path}.range_invalid`);
}
if (Array.isArray(value.enum) && value.enum.some((item) => (
typeof item === "number"
&& ((Number.isFinite(value.minimum) && item < value.minimum)
|| (Number.isFinite(value.maximum) && item > value.maximum))
))) errors.push(`${path}.enum_value_out_of_range`);
}
}
function validateExpressionAgainstFieldContract(expression, contract, derivations, path, errors) {
if (contract.required === true && expression.omitIfMissing === true) {
errors.push(`${path}_required_but_mapping_can_omit`);
}
if (contract.required === true && expression.omitIfInvalid === true) {
errors.push(`${path}_required_but_mapping_can_omit_invalid`);
}
if (expression.paths !== undefined) {
if (contract.required === false && expression.omitIfMissing !== true) {
errors.push(`${path}_optional_source_field_must_omit_missing`);
}
const expectedCoerce = new Map([["string", "string"], ["number", "number"], ["boolean", "boolean"]]).get(contract.type);
if (expectedCoerce && expression.coerce !== expectedCoerce) errors.push(`${path}.coerce_must_match_field_contract`);
if (Array.isArray(contract.enum)) errors.push(`${path}.enum_field_must_use_constant_or_derivation`);
if (contract.type === "number") {
if (!Object.is(expression.minimum, contract.minimum)) errors.push(`${path}.minimum_must_match_field_contract`);
if (!Object.is(expression.maximum, contract.maximum)) errors.push(`${path}.maximum_must_match_field_contract`);
if ((contract.minimum !== undefined || contract.maximum !== undefined) && expression.omitIfInvalid !== true) {
errors.push(`${path}.bounded_number_must_omit_invalid`);
}
}
}
if (expression.constant !== undefined) {
validateMappedValueAgainstFieldContract(expression.constant, contract, `${path}.constant`, errors);
}
if (typeof expression.derive === "string") {
const derivation = isPlainObject(derivations) ? derivations[expression.derive] : undefined;
if (!isPlainObject(derivation)) return;
validateMappedValueAgainstFieldContract(derivation.default, contract, `${path}.derivation.default`, errors);
if (Array.isArray(contract.enum) && Array.isArray(derivation.rules)) {
for (const [index, rule] of derivation.rules.entries()) {
const output = typeof rule === "string" ? rule.slice(rule.lastIndexOf(".") + 1) : undefined;
validateMappedValueAgainstFieldContract(output, contract, `${path}.derivation.rules[${index}]`, errors);
}
}
}
}
function validateMappedValueAgainstFieldContract(value, contract, path, errors) {
if (!fieldContractValueMatchesType(value, contract.type)) errors.push(`${path}_type_mismatch`);
if (Array.isArray(contract.enum) && !contract.enum.some((allowed) => Object.is(allowed, value))) {
errors.push(`${path}_not_in_field_contract_enum`);
}
if (typeof value === "number") {
if (Number.isFinite(contract.minimum) && value < contract.minimum) errors.push(`${path}_below_field_contract_minimum`);
if (Number.isFinite(contract.maximum) && value > contract.maximum) errors.push(`${path}_above_field_contract_maximum`);
}
}
function fieldContractValueMatchesType(value, type) {
if (type === "string_array") return Array.isArray(value) && value.every((item) => typeof item === "string");
if (type === "telemetry_readings") return isBoundedTelemetryReadings(value);
if (type === "point") {
return isPlainObject(value)
&& value.type === "Point"
&& Array.isArray(value.coordinates)
&& value.coordinates.length === 2
&& value.coordinates.every(Number.isFinite);
}
if (type === "geometry") {
return isPlainObject(value)
&& new Set(["Point", "LineString", "Polygon", "MultiPolygon"]).has(value.type)
&& Array.isArray(value.coordinates);
}
return typeof value === type && (type !== "number" || Number.isFinite(value));
}
function stableLiteral(value) {
return `${typeof value}:${JSON.stringify(value)}`;
}
function validateExpression(value, path, errors) {
if (!isPlainObject(value)) {
errors.push(`${path}_must_be_mapping_expression`);
return;
}
rejectUnknownKeys(value, EXPRESSION_KEYS, path, errors);
const selectors = [Array.isArray(value.paths) && value.paths.length > 0, value.constant !== undefined, typeof value.derive === "string"].filter(Boolean).length;
if (selectors !== 1) errors.push(`${path}_must_define_exactly_one_source`);
if (value.paths !== undefined) requiredSourcePathArray(value.paths, `${path}.paths`, errors);
if (value.strategy !== undefined && value.strategy !== "first_non_empty") errors.push(`${path}.strategy_invalid`);
if (value.coerce !== undefined && !new Set(["string", "number", "boolean", "unix_or_iso_timestamp"]).has(value.coerce)) {
errors.push(`${path}.coerce_invalid`);
}
if (value.prefix !== undefined && (typeof value.prefix !== "string" || !value.prefix || value.paths === undefined)) {
errors.push(`${path}.prefix_invalid`);
}
if (value.constant !== undefined && !isMappingLiteral(value.constant)) {
errors.push(`${path}.constant_must_be_scalar_or_scalar_array`);
}
if (value.fallback !== undefined && value.fallback !== "collection_received_at") errors.push(`${path}.fallback_invalid`);
if (value.derive !== undefined) requiredIdentifier(value.derive, `${path}.derive`, errors);
if (value.omitIfMissing !== undefined && typeof value.omitIfMissing !== "boolean") errors.push(`${path}.omitIfMissing_must_be_boolean`);
if (value.omitIfInvalid !== undefined && typeof value.omitIfInvalid !== "boolean") errors.push(`${path}.omitIfInvalid_must_be_boolean`);
const hasGuardPath = value.guardPath !== undefined;
const hasGuardEquals = value.guardEquals !== undefined;
if (hasGuardPath !== hasGuardEquals) errors.push(`${path}.guard_requires_path_and_equals`);
if (hasGuardPath) {
if (!isCanonicalSourcePath(value.guardPath)) {
errors.push(`${path}.guardPath_must_be_canonical_dot_path`);
}
if (!["string", "number", "boolean"].includes(typeof value.guardEquals)) {
errors.push(`${path}.guardEquals_must_be_scalar`);
}
if (value.omitIfMissing !== true) errors.push(`${path}.guard_requires_omitIfMissing`);
}
if (value.minimum !== undefined || value.maximum !== undefined) {
if (value.coerce !== "number") errors.push(`${path}.range_requires_number_coercion`);
if (value.minimum !== undefined && !Number.isFinite(value.minimum)) errors.push(`${path}.minimum_invalid`);
if (value.maximum !== undefined && !Number.isFinite(value.maximum)) errors.push(`${path}.maximum_invalid`);
if (Number.isFinite(value.minimum) && Number.isFinite(value.maximum) && value.minimum > value.maximum) {
errors.push(`${path}.range_invalid`);
}
if (value.omitIfInvalid !== true) errors.push(`${path}.range_requires_omitIfInvalid`);
} else if (value.omitIfInvalid !== undefined) {
errors.push(`${path}.omitIfInvalid_requires_range`);
}
}
function validateDerivation(value, path, errors) {
if (!isPlainObject(value)) return errors.push(`${path}_must_be_object`);
rejectUnknownKeys(value, DERIVATION_KEYS, path, errors);
if (!new Set([
"boolean_rule",
"ordered_rules",
"flag_set",
"bounded_readings",
"bounded_response_lookup",
"bounded_string_list",
"bounded_named_values",
]).has(value.kind)) errors.push(`${path}.kind_invalid`);
requiredUniqueIdentifierArray(value.rules, `${path}.rules`, errors);
if (value.default === undefined) {
errors.push(`${path}.default_required`);
} else if (!isMappingLiteral(value.default)) {
errors.push(`${path}.default_must_be_scalar_or_scalar_array`);
}
if (value.parameters !== undefined) {
if (!isPlainObject(value.parameters)) {
errors.push(`${path}.parameters_must_be_object`);
} else {
for (const [key, item] of Object.entries(value.parameters)) {
if (!/^[a-z][a-zA-Z0-9]{1,63}$/.test(key)) errors.push(`${path}.parameters.${key}_name_invalid`);
if (SECRET_FIELD_NAME.test(key)) errors.push(`${path}.parameters.${key}_secret_field_not_allowed`);
if (!["string", "number", "boolean"].includes(typeof item)) errors.push(`${path}.parameters.${key}_value_invalid`);
}
}
}
if (value.kind === "bounded_string_list") {
validateBoundedStringListDerivation(value, path, errors);
}
if (value.kind === "bounded_named_values") {
validateBoundedNamedValuesDerivation(value, path, errors);
}
if (value.kind === "bounded_response_lookup") {
validateBoundedResponseLookupDerivation(value, path, errors);
}
}
function validateBoundedResponseLookupDerivation(value, path, errors) {
const parameters = isPlainObject(value.parameters) ? value.parameters : {};
const allowed = new Set([
"responseCapabilityId",
"responseCollectionPath",
"responseKeyPath",
"sourceArrayPath",
"sourceKeyPath",
"valuePath",
"resultMode",
"maxResponseItems",
"maxSourceKeys",
"maxItems",
"maxItemLength",
]);
if (Object.keys(parameters).some((key) => !allowed.has(key))) {
errors.push(`${path}.parameters_shape_invalid`);
}
if (!IDENTIFIER.test(String(parameters.responseCapabilityId || ""))) {
errors.push(`${path}.parameters.responseCapabilityId_invalid`);
}
for (const key of [
"responseCollectionPath",
"responseKeyPath",
"sourceKeyPath",
"valuePath",
]) {
if (!isCanonicalSourcePath(parameters[key])) {
errors.push(`${path}.parameters.${key}_must_be_canonical_dot_path`);
}
}
if (SECRET_FIELD_NAME.test(String(parameters.valuePath || ""))) {
errors.push(`${path}.parameters.valuePath_secret_field_not_allowed`);
}
if (
parameters.sourceArrayPath !== undefined
&& !isCanonicalSourcePath(parameters.sourceArrayPath)
) {
errors.push(`${path}.parameters.sourceArrayPath_must_be_canonical_dot_path`);
}
if (!new Set(["first", "list"]).has(parameters.resultMode)) {
errors.push(`${path}.parameters.resultMode_invalid`);
}
if (
!Number.isInteger(parameters.maxResponseItems)
|| parameters.maxResponseItems < 1
|| parameters.maxResponseItems > 5000
) {
errors.push(`${path}.parameters.maxResponseItems_invalid`);
}
if (
!Number.isInteger(parameters.maxSourceKeys)
|| parameters.maxSourceKeys < 1
|| parameters.maxSourceKeys > 256
) {
errors.push(`${path}.parameters.maxSourceKeys_invalid`);
}
if (
!Number.isInteger(parameters.maxItems)
|| parameters.maxItems < 1
|| parameters.maxItems > 256
) {
errors.push(`${path}.parameters.maxItems_invalid`);
}
if (
!Number.isInteger(parameters.maxItemLength)
|| parameters.maxItemLength < 1
|| parameters.maxItemLength > 512
) {
errors.push(`${path}.parameters.maxItemLength_invalid`);
}
if (parameters.resultMode === "list") {
if (!Array.isArray(value.default) || value.default.length !== 0) {
errors.push(`${path}.default_must_be_empty_array_for_list`);
}
} else if (value.default !== "") {
errors.push(`${path}.default_must_be_empty_string_for_first`);
}
}
function validateBoundedStringListDerivation(value, path, errors) {
const parameters = isPlainObject(value.parameters) ? value.parameters : {};
const allowed = new Set(["arrayPath", "valuePath", "maxItems", "maxItemLength"]);
if (Object.keys(parameters).some((key) => !allowed.has(key))) {
errors.push(`${path}.parameters_shape_invalid`);
}
for (const key of ["arrayPath", "valuePath"]) {
if (!isCanonicalSourcePath(parameters[key])) {
errors.push(`${path}.parameters.${key}_must_be_canonical_dot_path`);
}
}
if (!Number.isInteger(parameters.maxItems) || parameters.maxItems < 1 || parameters.maxItems > 256) {
errors.push(`${path}.parameters.maxItems_invalid`);
}
if (
!Number.isInteger(parameters.maxItemLength)
|| parameters.maxItemLength < 1
|| parameters.maxItemLength > 512
) {
errors.push(`${path}.parameters.maxItemLength_invalid`);
}
if (!Array.isArray(value.default) || value.default.length !== 0) {
errors.push(`${path}.default_must_be_empty_array`);
}
}
function validateBoundedNamedValuesDerivation(value, path, errors) {
const parameters = isPlainObject(value.parameters) ? value.parameters : {};
const allowed = new Set([
"arrayPath",
"namePath",
"valuePath",
"maxItems",
"maxItemLength",
]);
if (Object.keys(parameters).some((key) => !allowed.has(key))) {
errors.push(`${path}.parameters_shape_invalid`);
}
for (const key of ["arrayPath", "namePath", "valuePath"]) {
if (!isCanonicalSourcePath(parameters[key])) {
errors.push(`${path}.parameters.${key}_must_be_canonical_dot_path`);
}
}
if (!Number.isInteger(parameters.maxItems) || parameters.maxItems < 1 || parameters.maxItems > 256) {
errors.push(`${path}.parameters.maxItems_invalid`);
}
if (
!Number.isInteger(parameters.maxItemLength)
|| parameters.maxItemLength < 1
|| parameters.maxItemLength > 512
) {
errors.push(`${path}.parameters.maxItemLength_invalid`);
}
if (!Array.isArray(value.default) || value.default.length !== 0) {
errors.push(`${path}.default_must_be_empty_array`);
}
}
function mappingDerivationReferences(value, found = new Set()) {
if (Array.isArray(value)) {
value.forEach((item) => mappingDerivationReferences(item, found));
return found;
}
if (!isPlainObject(value)) return found;
if (typeof value.derive === "string") found.add(value.derive);
Object.values(value).forEach((item) => mappingDerivationReferences(item, found));
return found;
}
function mappingSourcePaths(value, found = []) {
if (Array.isArray(value)) {
value.forEach((item) => mappingSourcePaths(item, found));
return found;
}
if (!isPlainObject(value)) return found;
if (Array.isArray(value.paths)) {
value.paths.forEach((path) => {
if (isCanonicalSourcePath(path)) found.push(path);
});
}
for (const key of ["kindPath", "polygonPointsPath", "corridorPointsPath", "radiusPath"]) {
if (isCanonicalSourcePath(value[key])) found.push(value[key]);
}
Object.entries(value).forEach(([key, item]) => {
if (key !== "paths" && key.endsWith("Path") && isCanonicalSourcePath(item)) {
found.push(item);
} else if (key !== "paths" && !key.endsWith("Path")) {
mappingSourcePaths(item, found);
}
});
return found;
}
function validateCredentialVisibleScope(value, path, errors) {
if (!isPlainObject(value)) {
errors.push(`${path}_must_be_object`);
return;
}
rejectUnknownKeys(value, ENTITY_SCOPE_KEYS, path, errors);
if (value.mode !== "all_visible_to_credential") errors.push(`${path}.mode_must_be_all_visible_to_credential`);
if (value.refresh !== "each_collection_run") errors.push(`${path}.refresh_must_be_each_collection_run`);
if (value.businessEntityFilter !== "forbidden") errors.push(`${path}.businessEntityFilter_must_be_forbidden`);
}
function validateArtifactArray(value, path, validator, errors) {
if (!Array.isArray(value) || value.length === 0) {
errors.push(`${path}_must_be_nonempty_array`);
return [];
}
const seen = new Set();
value.forEach((item, index) => {
if (!isPlainObject(item)) return errors.push(`${path}[${index}]_must_be_object`);
validator(item, `${path}[${index}]`, errors);
if (seen.has(item.id)) errors.push(`${path}_ids_must_be_unique`);
seen.add(item.id);
});
return value.filter(isPlainObject);
}
function assertExactReferences(references, artifacts, path, errors) {
if (!sameSet(references, artifacts.map((item) => item.id))) errors.push(`${path}_must_exactly_reference_package_artifacts`);
}
function assertReference(value, allowed, path, errors) {
if (!allowed.has(value)) errors.push(`${path}_unknown_reference`);
}
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}_must_be_semver`);
}
function requiredString(value, path, errors) {
if (typeof value !== "string" || !value.trim()) errors.push(`${path}_required`);
}
function requiredOpaqueReference(value, path, errors) {
if (!isNdcCredentialReferenceValue(value)) errors.push(`${path}_invalid`);
}
function validateProviderBaseUrl(value, path, errors) {
if (typeof value !== "string") return errors.push(`${path}_must_be_https_origin`);
try {
const parsed = new URL(value);
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.search || parsed.hash || parsed.pathname !== "/") {
errors.push(`${path}_must_be_https_origin`);
}
} catch {
errors.push(`${path}_must_be_https_origin`);
}
}
function validateProviderRequestPath(value, path, errors) {
if (typeof value !== "string"
|| value.length < 1
|| value.length > 2048
|| !value.startsWith("/")
|| /[\u0000-\u001f\u007f\\?#]/.test(value)
|| value.split("/").some((segment) => !SAFE_PATH_SEGMENT.test(segment))) {
errors.push(`${path}_invalid`);
}
}
function validateStaticQuery(value, path, errors) {
if (!isPlainObject(value)) return errors.push(`${path}_must_be_object`);
for (const [key, item] of Object.entries(value)) {
if (!/^[A-Za-z][A-Za-z0-9._-]{0,63}$/.test(key)) errors.push(`${path}.${key}_name_invalid`);
if (SECRET_QUERY_KEY.test(key)) errors.push(`${path}.${key}_credential_parameter_not_allowed`);
if (!["string", "number", "boolean"].includes(typeof item) || (typeof item === "string" && item.length > 2048)) {
errors.push(`${path}.${key}_value_invalid`);
}
if (containsSecretValue(item)) errors.push(`${path}.${key}_must_not_contain_secret_material`);
}
}
function requiredStringArray(value, path, errors) {
if (!Array.isArray(value) || value.length === 0) {
errors.push(`${path}_must_be_nonempty_array`);
return;
}
value.forEach((item, index) => requiredString(item, `${path}[${index}]`, errors));
if (new Set(value).size !== value.length) errors.push(`${path}_must_not_contain_duplicates`);
}
function requiredSourcePathArray(value, path, errors) {
requiredStringArray(value, path, errors);
if (!Array.isArray(value)) return;
value.forEach((item, index) => {
if (typeof item === "string" && !isCanonicalSourcePath(item)) {
errors.push(`${path}[${index}]_must_be_canonical_dot_path`);
}
});
}
function requiredCollectionPathArray(value, path, errors) {
requiredStringArray(value, path, errors);
if (!Array.isArray(value)) return;
value.forEach((item, index) => {
if (item !== "$" && !isCanonicalSourcePath(item)) {
errors.push(`${path}[${index}]_invalid`);
}
});
}
function isCanonicalSourcePath(value) {
return typeof value === "string"
&& SOURCE_PATH.test(value)
&& value.split(".").every((segment) => !MAGIC_PATH_SEGMENTS.has(segment));
}
function requiredUniqueIdentifierArray(value, path, errors) {
if (!Array.isArray(value) || value.length === 0) {
errors.push(`${path}_must_be_nonempty_array`);
return;
}
value.forEach((item, index) => requiredIdentifier(item, `${path}[${index}]`, errors));
if (new Set(value).size !== value.length) errors.push(`${path}_must_not_contain_duplicates`);
}
function rejectUnknownKeys(value, allowed, path, errors) {
if (!isPlainObject(value)) return;
for (const key of Object.keys(value)) {
if (!allowed.has(key)) errors.push(`${path}.${key}_not_allowed`);
}
}
function ids(values) {
return new Set(values.map((item) => item.id));
}
function sameSet(left, right) {
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
const leftSet = new Set(left);
return leftSet.size === left.length && right.every((item) => leftSet.has(item));
}
function sameArray(left, right) {
return Array.isArray(left)
&& Array.isArray(right)
&& left.length === right.length
&& left.every((item, index) => item === right[index]);
}
function containsSecretValue(value) {
if (typeof value === "string") return SECRET_VALUE.test(value);
if (Array.isArray(value)) return value.some(containsSecretValue);
if (!isPlainObject(value)) return false;
return Object.values(value).some(containsSecretValue);
}
function isMappingLiteral(value) {
if (value === null || ["string", "number", "boolean"].includes(typeof value)) return true;
return Array.isArray(value)
&& value.length <= 256
&& value.every((item) => item === null || ["string", "number", "boolean"].includes(typeof item));
}
function isPlainObject(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function deepFreeze(value) {
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
Object.freeze(value);
for (const child of Object.values(value)) deepFreeze(child);
return value;
}
function result(errors) {
const unique = [...new Set(errors)];
return Object.freeze({ ok: unique.length === 0, errors: Object.freeze(unique) });
}