const IDENTIFIER = /^[a-z][a-z0-9._:-]{1,159}$/; const FIELD = /^[a-z][a-z0-9_.-]{0,127}$/; const SEMVER = /^\d+\.\d+\.\d+$/; const SECRET_MATERIAL = /(?:token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key|decrypt|raw[_-]?params?)/i; const RESTRICTED_IDENTIFIER = /(?:imei|phone|telephone|address)/i; const PROFILE_KEYS = new Set(["id", "version", "title", "semanticTypes", "defaultTabId", "tabs"]); const TAB_KEYS = new Set(["id", "label", "emptyMessage", "sections"]); const SECTION_KEYS = new Set(["id", "label", "fields"]); const FIELD_KEYS = new Set([ "id", "aspectId", "source", "field", "label", "format", "unit", "allowedReadingIds", "dataClass", ]); const SOURCES = new Set(["fact", "attribute", "geometry", "context"]); const FORMATS = new Set([ "text", "number", "timestamp", "boolean", "coordinate", "signal_state", "movement_state", "string_list", "telemetry_readings", ]); const DATA_CLASSES = new Set(["operational", "restricted"]); const FACT_FIELDS = new Set(["sourceId", "semanticType", "observedAt", "receivedAt", "presentationStatus"]); const GEOMETRY_FIELDS = new Set(["latitude", "longitude"]); const CONTEXT_FIELDS = new Set(["dataProductId", "bindingId"]); export function normalizeMapSubjectDetailProfile(value) { object(value, "invalid_map_subject_detail_profile"); onlyKeys(value, PROFILE_KEYS, "invalid_map_subject_detail_profile_fields"); const id = identifier(value.id, "invalid_map_subject_detail_profile_id"); const version = text(value.version, 32, "invalid_map_subject_detail_profile_version"); if (!SEMVER.test(version)) fail("invalid_map_subject_detail_profile_version"); const title = safeLabel(value.title, 120, "invalid_map_subject_detail_profile_title"); const semanticTypes = identifiers(value.semanticTypes, 1, 8, "invalid_map_subject_detail_profile_semantic_types"); if (!Array.isArray(value.tabs) || value.tabs.length < 1 || value.tabs.length > 12) { fail("invalid_map_subject_detail_profile_tabs"); } const tabs = value.tabs.map(normalizeTab); unique(tabs.map((tab) => tab.id), "duplicate_map_subject_detail_profile_tab_id"); const defaultTabId = identifier(value.defaultTabId, "invalid_map_subject_detail_profile_default_tab"); if (!tabs.some((tab) => tab.id === defaultTabId)) fail("map_subject_detail_profile_default_tab_not_found"); const fieldIds = tabs.flatMap((tab) => tab.sections.flatMap((section) => section.fields.map((field) => field.id))); unique(fieldIds, "duplicate_map_subject_detail_profile_field_id"); return { id, version, title, semanticTypes, defaultTabId, tabs }; } export function normalizeMapSubjectDetailProfiles(value) { if (!Array.isArray(value) || value.length > 32) fail("invalid_map_subject_detail_profiles"); const profiles = value.map(normalizeMapSubjectDetailProfile); unique(profiles.map((profile) => profile.id), "duplicate_map_subject_detail_profile_id"); return profiles; } function normalizeTab(value) { object(value, "invalid_map_subject_detail_profile_tab"); onlyKeys(value, TAB_KEYS, "invalid_map_subject_detail_profile_tab_fields"); if (!Array.isArray(value.sections) || value.sections.length > 12) fail("invalid_map_subject_detail_profile_sections"); const sections = value.sections.map(normalizeSection); unique(sections.map((section) => section.id), "duplicate_map_subject_detail_profile_section_id"); return { id: identifier(value.id, "invalid_map_subject_detail_profile_tab_id"), label: safeLabel(value.label, 80, "invalid_map_subject_detail_profile_tab_label"), emptyMessage: safeLabel(value.emptyMessage, 240, "invalid_map_subject_detail_profile_empty_message"), sections, }; } function normalizeSection(value) { object(value, "invalid_map_subject_detail_profile_section"); onlyKeys(value, SECTION_KEYS, "invalid_map_subject_detail_profile_section_fields"); if (!Array.isArray(value.fields) || value.fields.length > 32) fail("invalid_map_subject_detail_profile_section_fields"); return { id: identifier(value.id, "invalid_map_subject_detail_profile_section_id"), label: safeLabel(value.label, 80, "invalid_map_subject_detail_profile_section_label"), fields: value.fields.map(normalizeField), }; } function normalizeField(value) { object(value, "invalid_map_subject_detail_profile_field"); onlyKeys(value, FIELD_KEYS, "invalid_map_subject_detail_profile_field_fields"); const source = text(value.source, 32, "invalid_map_subject_detail_profile_field_source"); const format = text(value.format, 32, "invalid_map_subject_detail_profile_field_format"); if (!SOURCES.has(source)) fail("invalid_map_subject_detail_profile_field_source"); if (!FORMATS.has(format)) fail("invalid_map_subject_detail_profile_field_format"); const field = sourceField(source, value.field); const dataClass = value.dataClass === undefined ? "operational" : text(value.dataClass, 32, "invalid_map_subject_detail_profile_field_data_class"); if (!DATA_CLASSES.has(dataClass)) fail("invalid_map_subject_detail_profile_field_data_class"); if (source === "attribute" && SECRET_MATERIAL.test(field)) { fail("map_subject_detail_profile_secret_field"); } if (source === "attribute" && RESTRICTED_IDENTIFIER.test(field) && dataClass !== "restricted") { fail("map_subject_detail_profile_restricted_field"); } const allowedReadingIds = value.allowedReadingIds === undefined ? [] : fields(value.allowedReadingIds, 0, 256, "invalid_map_subject_detail_profile_reading_ids"); if (allowedReadingIds.some((item) => SECRET_MATERIAL.test(item) || RESTRICTED_IDENTIFIER.test(item))) { fail("map_subject_detail_profile_restricted_reading_id"); } if (format === "telemetry_readings" && (source !== "attribute" || field !== "sensor_readings")) { fail("map_subject_detail_profile_readings_source_invalid"); } if (format === "string_list" && source !== "attribute") { fail("map_subject_detail_profile_string_list_source_invalid"); } if (format !== "telemetry_readings" && allowedReadingIds.length) { fail("map_subject_detail_profile_reading_ids_not_allowed"); } if (source === "geometry" && format !== "coordinate") fail("map_subject_detail_profile_geometry_format_invalid"); const unit = value.unit === undefined ? undefined : safeLabel(value.unit, 24, "invalid_map_subject_detail_profile_field_unit"); return { id: identifier(value.id, "invalid_map_subject_detail_profile_field_id"), aspectId: identifier(value.aspectId ?? "primary", "invalid_map_subject_detail_profile_aspect_id"), source, field, label: fieldLabel(value.label, dataClass), format, dataClass, ...(unit ? { unit } : {}), ...(format === "telemetry_readings" ? { allowedReadingIds } : {}), }; } function sourceField(source, value) { const normalized = text(value, 128, "invalid_map_subject_detail_profile_field_name"); if (source === "fact" && !FACT_FIELDS.has(normalized)) fail("invalid_map_subject_detail_profile_fact_field"); if (source === "geometry" && !GEOMETRY_FIELDS.has(normalized)) fail("invalid_map_subject_detail_profile_geometry_field"); if (source === "context" && !CONTEXT_FIELDS.has(normalized)) fail("invalid_map_subject_detail_profile_context_field"); if (source === "attribute" && !FIELD.test(normalized)) fail("invalid_map_subject_detail_profile_attribute_field"); return normalized; } function onlyKeys(value, allowed, code) { if (Object.keys(value).some((key) => !allowed.has(key))) fail(code); } function object(value, code) { if (!value || typeof value !== "object" || Array.isArray(value)) fail(code); } function identifier(value, code) { const normalized = text(value, 160, code); if (!IDENTIFIER.test(normalized)) fail(code); return normalized; } function identifiers(value, min, max, code) { if (!Array.isArray(value) || value.length < min || value.length > max) fail(code); const normalized = value.map((item) => identifier(item, code)); unique(normalized, code); return normalized; } function fields(value, min, max, code) { if (!Array.isArray(value) || value.length < min || value.length > max) fail(code); const normalized = value.map((item) => { const result = text(item, 128, code); if (!FIELD.test(result)) fail(code); return result; }); unique(normalized, code); return normalized; } function safeLabel(value, max, code) { const normalized = text(value, max, code); if (SECRET_MATERIAL.test(normalized)) fail(code); return normalized; } function fieldLabel(value, dataClass) { const normalized = safeLabel(value, 100, "invalid_map_subject_detail_profile_field_label"); if (RESTRICTED_IDENTIFIER.test(normalized) && dataClass !== "restricted") { fail("map_subject_detail_profile_restricted_field"); } return normalized; } function text(value, max, code) { if (typeof value !== "string") fail(code); const normalized = value.trim(); if (!normalized || normalized.length > max) fail(code); return normalized; } function unique(values, code) { if (new Set(values).size !== values.length) fail(code); } function fail(code) { throw Object.assign(new Error(code), { statusCode: 400 }); }