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) }); }