import { constants as fsConstants, createReadStream, createWriteStream } from "node:fs"; import { lstat, mkdir, open, readFile, readdir, rename, stat, writeFile } from "node:fs/promises"; import { createServer, request as httpRequest } from "node:http"; import { request as httpsRequest } from "node:https"; import { createHash, createHmac, randomUUID } from "node:crypto"; import { basename, extname, join, normalize, resolve } from "node:path"; import { Readable, Transform } from "node:stream"; import { pipeline } from "node:stream/promises"; import { fileURLToPath } from "node:url"; import { FOUNDRY_BINDING_CATALOG_PATH, FOUNDRY_BINDING_UPSERT_PATH, handleFoundryBindingApiRequest, } from "./foundry-binding-api.mjs"; import { createFoundryAgentGateway } from "./foundry-agent-gateway.mjs"; import { createFoundryAgentStore } from "./foundry-agent-store.mjs"; import { createFoundryDataProductConsumerManager } from "./foundry-data-product-consumer.mjs"; import { createFoundryReaderGrantProvisioner } from "./foundry-reader-grant-provisioner.mjs"; import { handleFoundryEntitlementRequest, handleFoundryMcpRequest } from "./foundry-mcp.mjs"; import { isMapLiveDataSlot } from "./map-live-data-slot.mjs"; import { normalizeMapPresentationProfile, normalizeMapPresentationProfiles } from "./map-presentation-profile.mjs"; import { normalizeMapSubjectDetailProfile, normalizeMapSubjectDetailProfiles } from "./map-subject-detail-profile.mjs"; import { createFoundryAuth } from "./nodedc-auth.mjs"; const root = fileURLToPath(new URL("..", import.meta.url)); const distDir = join(root, "apps", "catalog", "dist"); const runtimeDir = process.env.FOUNDRY_RUNTIME_DIR ? resolve(root, process.env.FOUNDRY_RUNTIME_DIR) : join(root, "runtime-data"); const uploadDir = join(runtimeDir, "uploads"); const applicationsDir = join(runtimeDir, "applications"); const designProfilesDir = join(runtimeDir, "design-profiles"); const designProfileReleasesDir = join(runtimeDir, "design-profile-releases"); const pageLayoutsDir = join(runtimeDir, "page-layouts"); const foundryMcpOperationsDir = join(runtimeDir, "foundry-mcp-operations"); const foundryDataProductConsumersDir = join(runtimeDir, "data-product-consumers"); const foundryManagedReaderGrantsDir = join(runtimeDir, "runtime-secrets", "external-data-plane-reader-grants"); const foundryAgentDataDir = join(runtimeDir, "foundry-agent-gateway"); const layoutPath = join(runtimeDir, "layout.json"); const pageRegistryPath = join(root, "registry", "pages.json"); const port = Number(process.env.PORT || 3333); const host = process.env.HOST || "127.0.0.1"; await mkdir(uploadDir, { recursive: true }); await mkdir(applicationsDir, { recursive: true }); await mkdir(designProfilesDir, { recursive: true }); await mkdir(designProfileReleasesDir, { recursive: true }); await mkdir(pageLayoutsDir, { recursive: true }); await mkdir(foundryMcpOperationsDir, { recursive: true }); await mkdir(foundryDataProductConsumersDir, { recursive: true }); await mkdir(foundryAgentDataDir, { recursive: true }); const pageRegistry = JSON.parse(await readFile(pageRegistryPath, "utf8")); const dataProductConsumerPolicyRegistry = JSON.parse(await readFile(join(root, "registry", "data-product-consumer-policies.json"), "utf8")); const mapPresentationProfileRegistry = JSON.parse(await readFile(join(root, "registry", "map-presentation-profiles.json"), "utf8")); if (mapPresentationProfileRegistry?.schemaVersion !== "nodedc.map-presentation-profiles/v1") { throw new Error("map_presentation_profile_registry_invalid"); } const canonicalMapPresentationProfiles = normalizeMapPresentationProfiles(mapPresentationProfileRegistry.profiles); const mapSubjectDetailProfileRegistry = JSON.parse(await readFile(join(root, "registry", "map-subject-detail-profiles.json"), "utf8")); if (mapSubjectDetailProfileRegistry?.schemaVersion !== "nodedc.map-subject-detail-profiles/v1") { throw new Error("map_subject_detail_profile_registry_invalid"); } const canonicalMapSubjectDetailProfiles = normalizeMapSubjectDetailProfiles(mapSubjectDetailProfileRegistry.profiles); const foundryAuth = createFoundryAuth(); const mapGatewayHeadersTimeoutMs = boundedMapGatewayTimeout( process.env.NODEDC_MAP_GATEWAY_HEADERS_TIMEOUT_MS, 15_000, ); const mapGatewayBodyIdleTimeoutMs = boundedMapGatewayTimeout( process.env.NODEDC_MAP_GATEWAY_BODY_IDLE_TIMEOUT_MS, 30_000, ); const mapGatewayInternalUrl = String( process.env.NODEDC_MAP_GATEWAY_INTERNAL_URL || process.env.NODEDC_MAP_GATEWAY_URL || (process.env.NODE_ENV === "production" ? "" : "http://127.0.0.1:18103"), ).trim().replace(/\/$/, ""); // External Data Plane remains private to Platform. A browser can only address // a persisted application/page/binding target through the same-origin BFF. const externalDataPlaneInternalUrl = String(process.env.NODEDC_EXTERNAL_DATA_PLANE_INTERNAL_URL || "").trim().replace(/\/$/, ""); const externalDataPlaneReaderGrantsDir = String(process.env.NODEDC_EXTERNAL_DATA_PLANE_READER_GRANTS_DIR || "").trim(); const foundryReaderGrantProvisioner = createFoundryReaderGrantProvisioner({ dataPlaneUrl: externalDataPlaneInternalUrl, privateKeyFile: process.env.NODEDC_EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_PRIVATE_KEY_FILE, grantsDir: foundryManagedReaderGrantsDir, serviceId: process.env.NODEDC_EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_SERVICE_ID || "nodedc-module-foundry", keyId: process.env.NODEDC_EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_KEY_ID || "foundry-edp-managed-provisioner-v1", audience: process.env.NODEDC_EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_AUDIENCE || "nodedc-external-data-plane.managed-provisioning.v1", }); const foundryBindingGrantsDir = String(process.env.NODEDC_FOUNDRY_BINDING_GRANTS_DIR || "").trim(); const activeRuntimeStreams = new Set(); // A dedicated server-only signing value shared with Map Gateway. In production // the runner supplies it as a read-only file, never an env value or browser API. const mapGatewayAdminSecret = await readMapGatewayAdminSecret(); const foundryPublicUrl = normalizeFoundryPublicUrl(process.env.FOUNDRY_PUBLIC_URL); const foundryAgentStore = createFoundryAgentStore({ dataRoot: foundryAgentDataDir }); const foundryAgentGateway = createFoundryAgentGateway({ store: foundryAgentStore, configuredPublicOrigin: foundryPublicUrl?.origin || "", installerPackagePath: join(root, "server", "assets", "nodedc-foundry-codex-agent-0.1.0.tgz"), ontologyCoreUrl: process.env.NODEDC_ONTOLOGY_CORE_URL, ontologyCoreAccessToken: process.env.NODEDC_INTERNAL_ACCESS_TOKEN, }); function boundedMapGatewayTimeout(value, fallback) { const parsed = Number.parseInt(String(value || ""), 10); if (!Number.isFinite(parsed)) return fallback; return Math.min(120_000, Math.max(50, parsed)); } async function readMapGatewayAdminSecret() { const secretFile = String(process.env.NODEDC_MAP_GATEWAY_ADMIN_SECRET_FILE || "").trim(); if (secretFile) { let secret; try { secret = (await readFile(secretFile, "utf8")).trim(); } catch { throw new Error("map_gateway_admin_secret_file_unreadable"); } if (!/^[A-Za-z0-9_-]{48,256}$/.test(secret)) throw new Error("map_gateway_admin_secret_file_invalid"); return secret; } if (process.env.NODE_ENV === "production") throw new Error("map_gateway_admin_secret_file_required"); return String(process.env.NODEDC_MAP_GATEWAY_ADMIN_SECRET || "").trim(); } function normalizeFoundryPublicUrl(value) { const raw = String(value || "").trim(); if (!raw) return null; let url; try { url = new URL(raw); } catch { throw new Error("invalid_foundry_public_url"); } if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) { throw new Error("invalid_foundry_public_url"); } url.pathname = url.pathname.replace(/\/+$/, "") || "/"; return url; } function normalizeFoundryReferer(value) { const raw = String(value || "").trim(); if (!raw || raw.length > 2048) return null; let url; try { url = new URL(raw); } catch { return null; } if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return null; url.search = ""; url.hash = ""; return url; } function foundryRequestIonReferer(request) { const browserReferer = normalizeFoundryReferer(request.headers.referer); if (foundryPublicUrl) { // Do not turn an arbitrary browser-provided Referer into Cesium provider // authorization. Only the configured public Foundry origin is delegated. if (browserReferer && browserReferer.origin === foundryPublicUrl.origin) return browserReferer.toString(); return foundryPublicUrl.toString(); } return browserReferer?.toString() || ""; } function findPageTemplate(id, version) { return pageRegistry.templates.find((template) => template.id === id && (!version || template.version === version)); } const mimeTypes = { ".css": "text/css; charset=utf-8", ".gif": "image/gif", ".html": "text/html; charset=utf-8", ".ico": "image/x-icon", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".js": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".map": "application/json; charset=utf-8", ".mp4": "video/mp4", ".png": "image/png", ".svg": "image/svg+xml", ".webm": "video/webm", ".webp": "image/webp", ".mov": "video/quicktime", }; function json(response, statusCode, value) { // A downstream stream can fail after its status and headers have already // reached the browser. Never attempt a second response in that case: Node // would throw ERR_HTTP_HEADERS_SENT and a normal Cesium request cancellation // could terminate the complete Foundry process. if (response.headersSent || response.writableEnded || response.destroyed) { if (!response.writableEnded && !response.destroyed) response.destroy(); return; } response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" }); response.end(JSON.stringify(value)); } function applicationError(code, statusCode = 400) { const error = new Error(code); error.statusCode = statusCode; return error; } function normalizeSlug(value) { return String(value || "") .trim() .toLowerCase() .replace(/[^a-z0-9-]+/g, "-") .replace(/-{2,}/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 64); } function applicationPath(id) { const safeId = String(id || ""); if (!/^[0-9a-f-]{36}$/i.test(safeId)) throw applicationError("invalid_application_id"); return join(applicationsDir, `${safeId}.json`); } function designProfilePath(id) { const safeId = String(id || ""); if (safeId !== "default" && !/^[0-9a-f-]{36}$/i.test(safeId)) throw applicationError("invalid_design_profile_id"); return join(designProfilesDir, `${safeId}.json`); } function designProfileReleaseDir(id) { const safeId = String(id || ""); if (safeId !== "default" && !/^[0-9a-f-]{36}$/i.test(safeId)) throw applicationError("invalid_design_profile_id"); return join(designProfileReleasesDir, safeId); } function designProfileReleasePath(id, version) { const safeVersion = String(version || ""); if (!/^\d+\.\d+\.\d+$/.test(safeVersion)) throw applicationError("invalid_design_profile_version"); return join(designProfileReleaseDir(id), `${safeVersion}.json`); } function pageLayoutPath(pageId) { const safePageId = String(pageId || ""); if (!/^[a-z0-9-]+$/i.test(safePageId) || !findPageTemplate(safePageId)) { throw applicationError("unknown_page_template"); } return join(pageLayoutsDir, `${safePageId}.json`); } const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value); const requireHex = (value, code) => { const normalized = String(value || ""); if (!/^#[0-9a-f]{6}$/i.test(normalized)) throw applicationError(code); return normalized.toLowerCase(); }; const requireNumber = (value, min, max, code) => { if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) throw applicationError(code); return value; }; const requireInteger = (value, min, max, code) => { const normalized = requireNumber(value, min, max, code); if (!Number.isInteger(normalized)) throw applicationError(code); return normalized; }; const requireString = (value, code, max = 4096) => { if (typeof value !== "string" || value.length > max) throw applicationError(code); return value; }; const requireNonEmptyString = (value, code, max = 4096) => { const normalized = requireString(value, code, max); if (!normalized) throw applicationError(code); return normalized; }; const requireBoolean = (value, code) => { if (typeof value !== "boolean") throw applicationError(code); return value; }; function validateMapPinBinding(value) { if (!isObject(value)) throw applicationError("invalid_map_pin_binding"); const id = requireNonEmptyString(value.id, "invalid_map_pin_id", 128); const subjectId = requireNonEmptyString(value.subjectId, "invalid_map_pin_subject_id", 256); if (!/^[A-Za-z0-9._:-]+$/.test(id)) throw applicationError("invalid_map_pin_id"); if (!isObject(value.coordinates)) throw applicationError("invalid_map_pin_coordinates"); if (!isObject(value.source)) throw applicationError("invalid_map_pin_source"); const attributes = value.attributes === undefined ? {} : value.attributes; if (!isObject(attributes) || Object.keys(attributes).length > 30) throw applicationError("invalid_map_pin_attributes"); const normalizedAttributes = {}; for (const [key, raw] of Object.entries(attributes)) { if (!/^[A-Za-z][A-Za-z0-9_.-]{0,63}$/.test(key)) throw applicationError("invalid_map_pin_attribute_key"); if (!["string", "number", "boolean"].includes(typeof raw) || (typeof raw === "string" && raw.length > 500)) { throw applicationError("invalid_map_pin_attribute_value"); } normalizedAttributes[key] = raw; } const displayFields = value.source.displayFields === undefined ? [] : value.source.displayFields; if (!Array.isArray(displayFields) || displayFields.length > 24 || displayFields.some((field) => typeof field !== "string" || field.length > 128)) { throw applicationError("invalid_map_pin_display_fields"); } return { id, subjectId, kind: "elevated-spike", label: String(value.label || "").slice(0, 240), status: String(value.status || "").slice(0, 80), coordinates: { longitude: requireNumber(value.coordinates.longitude, -180, 180, "invalid_map_pin_longitude"), latitude: requireNumber(value.coordinates.latitude, -90, 90, "invalid_map_pin_latitude"), heightMeters: value.coordinates.heightMeters === undefined ? 0 : requireNumber(value.coordinates.heightMeters, -10000, 10000000, "invalid_map_pin_height"), }, source: { entityId: requireNonEmptyString(value.source.entityId, "invalid_map_pin_source_entity", 240), streamId: String(value.source.streamId || "").slice(0, 240), displayFields: [...displayFields], }, attributes: normalizedAttributes, }; } function validateMapPinBindings(value) { if (!Array.isArray(value) || value.length > 10000) throw applicationError("invalid_map_pin_bindings"); const ids = new Set(); return value.map((item) => { const binding = validateMapPinBinding(item); if (ids.has(binding.id)) throw applicationError("duplicate_map_pin_id"); ids.add(binding.id); return binding; }); } function validateMapDataProductBinding(value) { if (!isObject(value)) throw applicationError("invalid_map_data_product_binding"); const id = requireNonEmptyString(value.id, "invalid_map_data_product_binding_id", 128); const dataProductId = requireNonEmptyString(value.dataProductId, "invalid_map_data_product_id", 160); const slotId = requireNonEmptyString(value.slotId, "invalid_map_data_product_slot", 80); const displayName = value.displayName === undefined ? null : requireNonEmptyString(value.displayName, "invalid_map_data_product_display_name", 120).trim(); const order = value.order === undefined ? null : requireInteger(value.order, 0, 10_000, "invalid_map_data_product_order"); const semanticTypes = value.semanticTypes === undefined ? [] : value.semanticTypes; const fieldProjection = value.fieldProjection === undefined ? [] : value.fieldProjection; const presentationProfileId = value.presentationProfileId === undefined ? null : requireNonEmptyString(value.presentationProfileId, "invalid_map_data_product_presentation_profile_id", 160); const subjectDetailProfileId = value.subjectDetailProfileId === undefined ? null : requireNonEmptyString(value.subjectDetailProfileId, "invalid_map_data_product_subject_detail_profile_id", 160); const aspectId = value.aspectId === undefined ? "primary" : requireNonEmptyString(value.aspectId, "invalid_map_data_product_aspect_id", 160); const joinToBindingId = value.joinToBindingId === undefined ? null : requireNonEmptyString(value.joinToBindingId, "invalid_map_data_product_join_binding_id", 128); const dataClass = value.dataClass === undefined ? "operational" : requireNonEmptyString(value.dataClass, "invalid_map_data_product_data_class", 32); if (!/^[A-Za-z0-9._:-]+$/.test(id)) throw applicationError("invalid_map_data_product_binding_id"); if (!/^[A-Za-z0-9._:-]+$/.test(dataProductId)) throw applicationError("invalid_map_data_product_id"); if (!/^[A-Za-z0-9-]+$/.test(slotId)) throw applicationError("invalid_map_data_product_slot"); if (!Array.isArray(semanticTypes) || semanticTypes.length === 0 || semanticTypes.length > 8 || semanticTypes.some((item) => typeof item !== "string" || !/^[A-Za-z0-9._:-]+$/.test(item))) { throw applicationError("invalid_map_data_product_semantic_types"); } if (!Array.isArray(fieldProjection) || fieldProjection.length > 32 || fieldProjection.some((item) => typeof item !== "string" || !/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(item))) { throw applicationError("invalid_map_data_product_field_projection"); } if (presentationProfileId && !/^[a-z][a-z0-9._:-]{1,159}$/.test(presentationProfileId)) { throw applicationError("invalid_map_data_product_presentation_profile_id"); } if (subjectDetailProfileId && !/^[a-z][a-z0-9._:-]{1,159}$/.test(subjectDetailProfileId)) { throw applicationError("invalid_map_data_product_subject_detail_profile_id"); } if (!/^[a-z][a-z0-9._:-]{1,159}$/.test(aspectId)) throw applicationError("invalid_map_data_product_aspect_id"); if (joinToBindingId && !/^[A-Za-z0-9._:-]+$/.test(joinToBindingId)) { throw applicationError("invalid_map_data_product_join_binding_id"); } if (!new Set(["operational", "restricted"]).has(dataClass)) { throw applicationError("invalid_map_data_product_data_class"); } if (Object.keys(value).some((key) => /(provider|tenant|connection|endpoint|url|credential|token|secret|payload)/i.test(key))) { throw applicationError("map_data_product_binding_contains_transport"); } return { id, dataProductId, slotId, delivery: "snapshot+patch", semanticTypes: [...semanticTypes], fieldProjection: [...fieldProjection], ...(displayName ? { displayName } : {}), ...(order !== null ? { order } : {}), ...(presentationProfileId ? { presentationProfileId } : {}), ...(subjectDetailProfileId ? { subjectDetailProfileId } : {}), aspectId, ...(joinToBindingId ? { joinToBindingId } : {}), dataClass, }; } function validateMapDataProductBindings(value) { if (!Array.isArray(value) || value.length > 64) throw applicationError("invalid_map_data_product_bindings"); const ids = new Set(); return value.map((item) => { const binding = validateMapDataProductBinding(item); if (ids.has(binding.id)) throw applicationError("duplicate_map_data_product_binding_id"); ids.add(binding.id); return binding; }); } function defaultMapSubjectState(bindingId, index) { return { bindingId, visible: true, filters: {}, window: { open: false, rect: { x: 24 + (index % 5) * 28, y: 56 + (index % 5) * 28, width: 280, height: 260 }, maximized: false, zIndex: 20 + index, }, }; } function validateMapSubjectStates(value, dataProductBindings) { if (!Array.isArray(value) || value.length > 64) throw applicationError("invalid_map_subject_states"); const bindingIds = new Set(dataProductBindings.map((binding) => binding.id)); const states = new Map(); for (const raw of value) { if (!isObject(raw)) throw applicationError("invalid_map_subject_state"); const bindingId = requireNonEmptyString(raw.bindingId, "invalid_map_subject_state_binding_id", 128); if (!bindingIds.has(bindingId)) continue; if (states.has(bindingId)) throw applicationError("duplicate_map_subject_state_binding_id"); const filters = raw.filters === undefined ? {} : raw.filters; if (!isObject(filters) || Object.keys(filters).length > 32) throw applicationError("invalid_map_subject_filters"); const normalizedFilters = {}; for (const [field, selected] of Object.entries(filters)) { if (!/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(field)) throw applicationError("invalid_map_subject_filter_field"); if (!Array.isArray(selected) || selected.length > 64 || selected.some((item) => typeof item !== "string" || item.length > 160)) { throw applicationError("invalid_map_subject_filter_values"); } normalizedFilters[field] = [...new Set(selected)]; } if (!isObject(raw.window) || !isObject(raw.window.rect)) throw applicationError("invalid_map_subject_window"); states.set(bindingId, { bindingId, visible: requireBoolean(raw.visible, "invalid_map_subject_visibility"), filters: normalizedFilters, window: { open: requireBoolean(raw.window.open, "invalid_map_subject_window_open"), rect: { x: requireNumber(raw.window.rect.x, -100_000, 100_000, "invalid_map_subject_window_x"), y: requireNumber(raw.window.rect.y, -100_000, 100_000, "invalid_map_subject_window_y"), width: requireNumber(raw.window.rect.width, 200, 10_000, "invalid_map_subject_window_width"), height: requireNumber(raw.window.rect.height, 160, 10_000, "invalid_map_subject_window_height"), }, maximized: requireBoolean(raw.window.maximized, "invalid_map_subject_window_maximized"), zIndex: requireInteger(raw.window.zIndex, 1, 100_000, "invalid_map_subject_window_z_index"), }, }); } return dataProductBindings.map((binding, index) => states.get(binding.id) ?? defaultMapSubjectState(binding.id, index)); } const MAP_PAGE_SETTING_KEYS = new Set([ "imagerySource", "imageryVisible", "cacheEnabled", "cacheNoOverwrite", "terrainEnabled", "terrainExaggeration", "monochrome", "monochromeColor", "imageryGamma", "imageryHue", "imageryAlpha", "globeColor", "backgroundColor", "atmosphereEnabled", "atmosphereHue", "atmosphereSaturation", "atmosphereBrightness", "fogEnabled", "fogDensity", "sunEnabled", "sunHour", "sunIntensity", "shadowsEnabled", "buildingsVisible", "buildingsColor", "buildingsOpacity", "buildingsDetail", "imageryBrightness", "imageryContrast", "imagerySaturation", "gridVisible", "gridLodEnabled", "gridHeightMeters", "gridLod1MaxHeightKm", "gridLod1StepKm", "gridLod2MaxHeightKm", "gridLod2StepKm", "gridLod3StepKm", "gridRadiusKm", "gridLineWidth", "gridColor", "gridOpacity", "gridDotsEnabled", "gridDotsSize", "gridDotsColor", "gridDotsOpacity", ]); function validateMapPageSettingsPatch(value) { if (!isObject(value) || Object.keys(value).length === 0) throw applicationError("invalid_map_page_settings_patch"); if (Object.keys(value).some((key) => !MAP_PAGE_SETTING_KEYS.has(key))) throw applicationError("invalid_map_page_settings_patch_field"); return value; } function validateMapPageLayout(value) { if (!isObject(value)) throw applicationError("invalid_map_page_layout"); if (value.schemaVersion !== 1 || value.pageId !== "map") throw applicationError("unsupported_map_page_layout"); if (!isObject(value.settings)) throw applicationError("invalid_map_page_settings"); if (!isObject(value.camera)) throw applicationError("invalid_map_page_camera"); const settings = value.settings; if (Object.keys(settings).some((key) => !MAP_PAGE_SETTING_KEYS.has(key))) throw applicationError("invalid_map_page_setting_field"); for (const key of ["imageryVisible", "cacheEnabled", "terrainEnabled", "monochrome", "atmosphereEnabled", "fogEnabled", "sunEnabled", "shadowsEnabled", "buildingsVisible", "gridVisible", "gridLodEnabled", "gridDotsEnabled"]) { requireBoolean(settings[key], `invalid_map_page_setting_${key}`); } // Layouts written before this field existed are safely upgraded to the // no-overwrite default during read/save; they are not rejected as damaged. if (settings.cacheNoOverwrite !== undefined) requireBoolean(settings.cacheNoOverwrite, "invalid_map_page_setting_cacheNoOverwrite"); requireString(settings.imagerySource, "invalid_map_page_imagery_source", 64); for (const key of ["terrainExaggeration", "imageryGamma", "imageryHue", "imageryAlpha", "atmosphereHue", "atmosphereSaturation", "atmosphereBrightness", "fogDensity", "sunHour", "sunIntensity", "buildingsOpacity", "buildingsDetail", "imageryBrightness", "imageryContrast", "imagerySaturation", "gridHeightMeters", "gridLod1MaxHeightKm", "gridLod1StepKm", "gridLod2MaxHeightKm", "gridLod2StepKm", "gridLod3StepKm", "gridRadiusKm", "gridLineWidth", "gridOpacity", "gridDotsSize", "gridDotsOpacity"]) { requireNumber(settings[key], -100000, 100000, `invalid_map_page_setting_${key}`); } for (const key of ["monochromeColor", "globeColor", "backgroundColor", "buildingsColor", "gridColor", "gridDotsColor"]) { requireHex(settings[key], `invalid_map_page_setting_${key}`); } const camera = value.camera; for (const key of ["longitude", "latitude", "height", "heading", "pitch", "roll"]) { requireNumber(camera[key], -1_000_000_000, 1_000_000_000, `invalid_map_page_camera_${key}`); } const presentationProfiles = normalizeMapPresentationProfiles(value.presentationProfiles === undefined ? [] : value.presentationProfiles); const subjectDetailProfiles = normalizeMapSubjectDetailProfiles( value.subjectDetailProfiles === undefined ? structuredClone(canonicalMapSubjectDetailProfiles) : value.subjectDetailProfiles, ); const dataProductBindings = validateMapDataProductBindings(value.dataProductBindings === undefined ? [] : value.dataProductBindings); const subjectStates = validateMapSubjectStates(value.subjectStates === undefined ? [] : value.subjectStates, dataProductBindings); const presentationProfileIds = new Set(presentationProfiles.map((profile) => profile.id)); if (dataProductBindings.some((binding) => binding.presentationProfileId && !presentationProfileIds.has(binding.presentationProfileId))) { throw applicationError("map_data_product_presentation_profile_not_found"); } const subjectDetailProfilesById = new Map(subjectDetailProfiles.map((profile) => [profile.id, profile])); const bindingsById = new Map(dataProductBindings.map((binding) => [binding.id, binding])); for (const binding of dataProductBindings) { const detailProfile = binding.subjectDetailProfileId ? subjectDetailProfilesById.get(binding.subjectDetailProfileId) : null; if (binding.subjectDetailProfileId && !detailProfile) throw applicationError("map_data_product_subject_detail_profile_not_found"); if (detailProfile && !binding.semanticTypes.some((semanticType) => detailProfile.semanticTypes.includes(semanticType))) { throw applicationError("map_data_product_subject_detail_profile_semantic_type_mismatch"); } if (binding.joinToBindingId) { if (binding.joinToBindingId === binding.id || !bindingsById.has(binding.joinToBindingId)) { throw applicationError("map_data_product_join_binding_not_found"); } if (binding.subjectDetailProfileId) throw applicationError("map_data_product_joined_binding_owns_detail_profile"); } } for (const primary of dataProductBindings.filter((binding) => !binding.joinToBindingId)) { const composition = [ primary, ...dataProductBindings.filter((binding) => binding.joinToBindingId === primary.id), ]; const aspectIds = composition.map((binding) => binding.aspectId); if (new Set(aspectIds).size !== aspectIds.length) throw applicationError("duplicate_map_data_product_aspect_id"); const detailProfile = primary.subjectDetailProfileId ? subjectDetailProfilesById.get(primary.subjectDetailProfileId) : null; if (!detailProfile) continue; const aspectsById = new Map(composition.map((binding) => [binding.aspectId, binding])); const detailFields = detailProfile.tabs.flatMap( (tab) => tab.sections.flatMap((section) => section.fields), ); for (const field of detailFields) { const aspect = aspectsById.get(field.aspectId); if (!aspect) throw applicationError("map_subject_detail_profile_aspect_not_found"); if (field.dataClass !== aspect.dataClass) { throw applicationError("map_subject_detail_profile_data_class_mismatch"); } } } return { schemaVersion: 1, pageId: "map", settings: { ...settings, cacheNoOverwrite: settings.cacheNoOverwrite ?? true }, mapHeight: requireInteger(value.mapHeight, 360, 5000, "invalid_map_page_height"), camera: { longitude: camera.longitude, latitude: camera.latitude, height: camera.height, heading: camera.heading, pitch: camera.pitch, roll: camera.roll, }, pinBindings: validateMapPinBindings(value.pinBindings === undefined ? [] : value.pinBindings), presentationProfiles, subjectDetailProfiles, dataProductBindings, subjectStates, }; } function validateMapDesignOverrides(value) { if (!isObject(value)) throw applicationError("invalid_map_design_overrides"); if (Object.keys(value).some((key) => !["settings", "presentationProfiles"].includes(key))) { throw applicationError("invalid_map_design_override_field"); } const result = {}; if (value.settings !== undefined) { if (!isObject(value.settings) || Object.keys(value.settings).some((key) => !MAP_PAGE_SETTING_KEYS.has(key))) { throw applicationError("invalid_map_design_override_settings"); } const defaults = defaultMapPageLayout(); const normalized = validateMapPageLayout({ ...defaults, settings: { ...defaults.settings, ...value.settings }, }).settings; result.settings = Object.fromEntries(Object.keys(value.settings).map((key) => [key, normalized[key]])); } if (value.presentationProfiles !== undefined) { result.presentationProfiles = normalizeMapPresentationProfiles(value.presentationProfiles); } return result; } function defaultMapPageLayout() { return { schemaVersion: 1, pageId: "map", settings: { imagerySource: "cesium-live", imageryVisible: true, cacheEnabled: true, cacheNoOverwrite: true, terrainEnabled: true, terrainExaggeration: 1, monochrome: false, monochromeColor: "#15151b", imageryGamma: 57, imageryHue: 13, imageryAlpha: 27, globeColor: "#15151b", backgroundColor: "#08090d", atmosphereEnabled: false, atmosphereHue: 0, atmosphereSaturation: 0, atmosphereBrightness: 0, fogEnabled: true, fogDensity: 2, sunEnabled: true, sunHour: 12, sunIntensity: 200, shadowsEnabled: true, buildingsVisible: true, buildingsColor: "#a27aff", buildingsOpacity: 1, buildingsDetail: 4, imageryBrightness: 118, imageryContrast: 102, imagerySaturation: 0, gridVisible: true, gridLodEnabled: true, gridHeightMeters: 500, gridLod1MaxHeightKm: 10, gridLod1StepKm: 1, gridLod2MaxHeightKm: 50, gridLod2StepKm: 5, gridLod3StepKm: 25, gridRadiusKm: 40, gridLineWidth: 4, gridColor: "#f5f5f5", gridOpacity: 12, gridDotsEnabled: true, gridDotsSize: 7, gridDotsColor: "#ffffff", gridDotsOpacity: 58, }, mapHeight: 470, camera: { longitude: 37.618423, latitude: 55.751244, height: 40000, heading: 0, pitch: -0.9, roll: 0, }, pinBindings: [], presentationProfiles: structuredClone(canonicalMapPresentationProfiles), subjectDetailProfiles: structuredClone(canonicalMapSubjectDetailProfiles), dataProductBindings: [], subjectStates: [], }; } function validateMaterial(value, theme) { if (!isObject(value)) throw applicationError(`invalid_${theme}_material`); return { panelHex: requireHex(value.panelHex, `invalid_${theme}_panel_color`), panelOpacity: requireNumber(value.panelOpacity, 0, 100, `invalid_${theme}_panel_opacity`), fieldHex: requireHex(value.fieldHex, `invalid_${theme}_field_color`), fieldOpacity: requireNumber(value.fieldOpacity, 0, 100, `invalid_${theme}_field_opacity`), nestedHex: requireHex(value.nestedHex, `invalid_${theme}_nested_color`), }; } function validateDesignProfileLayout(value) { if (!isObject(value)) throw applicationError("invalid_design_profile_layout"); if (value.theme !== "dark" && value.theme !== "light") throw applicationError("invalid_design_profile_theme"); if (!isObject(value.materialByTheme) || !isObject(value.environment) || !isObject(value.media) || !isObject(value.glass) || !isObject(value.toolbar)) { throw applicationError("incomplete_design_profile_layout"); } const environment = value.environment; const media = value.media; const glass = value.glass; const toolbar = value.toolbar; if (media.source !== "file" && media.source !== "url") throw applicationError("invalid_design_profile_media_source"); if (media.logoSource !== "file" && media.logoSource !== "url") throw applicationError("invalid_design_profile_logo_source"); if (!isObject(media.faviconAssets)) throw applicationError("invalid_design_profile_favicon_assets"); if (glass.version !== 4) throw applicationError("unsupported_glass_material_version"); if (!["left", "right", "bottom"].includes(toolbar.placement)) throw applicationError("invalid_toolbar_placement"); return { theme: value.theme, accentHex: requireHex(value.accentHex, "invalid_design_profile_accent"), materialByTheme: { dark: validateMaterial(value.materialByTheme.dark, "dark"), light: validateMaterial(value.materialByTheme.light, "light"), }, environment: { lightColor: requireHex(environment.lightColor, "invalid_environment_light_color"), brightness: requireNumber(environment.brightness, 0, 100, "invalid_environment_brightness"), glowDistance: requireNumber(environment.glowDistance, 0, 500, "invalid_environment_glow_distance"), connectionType: requireString(environment.connectionType, "invalid_environment_connection_type", 64), connectionColor: requireHex(environment.connectionColor, "invalid_environment_connection_color"), usePortColors: requireBoolean(environment.usePortColors, "invalid_environment_use_port_colors"), fillColor: requireHex(environment.fillColor, "invalid_environment_fill_color"), fillOpacity: requireNumber(environment.fillOpacity, 0, 100, "invalid_environment_fill_opacity"), strokeColor: requireHex(environment.strokeColor, "invalid_environment_stroke_color"), strokeOpacity: requireNumber(environment.strokeOpacity, 0, 100, "invalid_environment_stroke_opacity"), }, media: { source: media.source, url: requireString(media.url, "invalid_design_profile_media_url"), fileName: requireString(media.fileName, "invalid_design_profile_media_file_name", 255), fileSrc: requireString(media.fileSrc, "invalid_design_profile_media_file_src"), visible: requireBoolean(media.visible, "invalid_design_profile_media_visibility"), logoSource: media.logoSource, logoUrl: requireString(media.logoUrl, "invalid_design_profile_logo_url"), logoFileName: requireString(media.logoFileName, "invalid_design_profile_logo_file_name", 255), logoFileSrc: requireString(media.logoFileSrc, "invalid_design_profile_logo_file_src"), faviconFileName: requireString(media.faviconFileName, "invalid_design_profile_favicon_file_name", 255), faviconAssets: { ico: requireNonEmptyString(media.faviconAssets.ico, "invalid_design_profile_favicon_ico"), apple: requireNonEmptyString(media.faviconAssets.apple, "invalid_design_profile_favicon_apple"), icon192: requireNonEmptyString(media.faviconAssets.icon192, "invalid_design_profile_favicon_192"), icon512: requireNonEmptyString(media.faviconAssets.icon512, "invalid_design_profile_favicon_512"), }, }, glass: { version: 4, tintHex: requireHex(glass.tintHex, "invalid_glass_tint"), tintOpacity: requireNumber(glass.tintOpacity, 0, 100, "invalid_glass_tint_opacity"), blur: requireNumber(glass.blur, 0, 120, "invalid_glass_blur"), saturation: requireNumber(glass.saturation, 0, 300, "invalid_glass_saturation"), brightness: requireNumber(glass.brightness, 0, 200, "invalid_glass_brightness"), outlineOpacity: requireNumber(glass.outlineOpacity, 0, 100, "invalid_glass_outline_opacity"), shadowOpacity: requireNumber(glass.shadowOpacity, 0, 100, "invalid_glass_shadow_opacity"), }, toolbar: { placement: toolbar.placement, background: requireHex(toolbar.background, "invalid_toolbar_background"), border: requireHex(toolbar.border, "invalid_toolbar_border"), outline: requireHex(toolbar.outline, "invalid_toolbar_outline"), minSize: requireNumber(toolbar.minSize, 16, 64, "invalid_toolbar_min_size"), maxSize: requireNumber(toolbar.maxSize, 32, 160, "invalid_toolbar_max_size"), lensCount: requireInteger(toolbar.lensCount, 1, 12, "invalid_toolbar_lens_count"), autoHide: requireBoolean(toolbar.autoHide, "invalid_toolbar_auto_hide"), }, pageTypes: validateDesignProfilePageTypes(value.pageTypes), }; } function validateDesignProfilePageTypes(value) { // Profiles and immutable releases created before page-type fragments were // introduced inherit the registered provider-neutral Map design default. if (value === undefined) return defaultDesignProfilePageTypes(); if (!isObject(value) || Object.keys(value).length > 64) throw applicationError("invalid_design_profile_page_types"); const pageTypes = {}; for (const [key, fragment] of Object.entries(value)) { if (!isObject(fragment) || fragment.schemaVersion !== 1) throw applicationError("invalid_design_profile_page_fragment"); if (Object.keys(fragment).some((field) => !["schemaVersion", "templateId", "templateVersion", "settings", "presentationProfiles"].includes(field))) { throw applicationError("design_profile_page_fragment_contains_runtime"); } const templateId = requireNonEmptyString(fragment.templateId, "invalid_design_profile_page_template_id", 80); const templateVersion = requireNonEmptyString(fragment.templateVersion, "invalid_design_profile_page_template_version", 32); if (key !== `${templateId}@${templateVersion}`) throw applicationError("invalid_design_profile_page_fragment_key"); const template = findPageTemplate(templateId, templateVersion); if (!template) throw applicationError("unknown_design_profile_page_template"); if (templateId !== "map") throw applicationError("unsupported_design_profile_page_template"); const defaults = defaultMapPageLayout(); const normalized = validateMapPageLayout({ ...defaults, settings: fragment.settings, presentationProfiles: fragment.presentationProfiles, pinBindings: [], dataProductBindings: [], }); pageTypes[key] = { schemaVersion: 1, templateId: "map", templateVersion, settings: normalized.settings, presentationProfiles: normalized.presentationProfiles, }; } return pageTypes; } function defaultDesignProfilePageTypes() { const mapLayout = defaultMapPageLayout(); return { "map@0.1.0": { schemaVersion: 1, templateId: "map", templateVersion: "0.1.0", settings: mapLayout.settings, presentationProfiles: mapLayout.presentationProfiles, }, }; } function validateDesignProfile(value) { if (!value || typeof value !== "object" || Array.isArray(value)) throw applicationError("invalid_design_profile"); if (value.schemaVersion !== "0.1.0") throw applicationError("unsupported_design_profile_schema"); if (value.id !== "default" && !/^[0-9a-f-]{36}$/i.test(String(value.id || ""))) throw applicationError("invalid_design_profile_id"); const name = String(value.name || "").trim(); if (!name || name.length > 120) throw applicationError("invalid_design_profile_name"); if (!/^\d+\.\d+\.\d+$/.test(String(value.version || ""))) throw applicationError("invalid_design_profile_version"); if (value.status !== "draft" && value.status !== "published") throw applicationError("invalid_design_profile_status"); if (!isObject(value.timestamps) || !value.timestamps.createdAt || !value.timestamps.updatedAt) throw applicationError("invalid_design_profile_timestamps"); if (value.status === "published" && !value.timestamps.publishedAt) throw applicationError("missing_design_profile_published_at"); return { ...value, name, layout: validateDesignProfileLayout(value.layout) }; } function defaultDesignProfileLayout() { return { theme: "dark", accentHex: "#ff2f92", materialByTheme: { dark: { panelHex: "#151517", panelOpacity: 100, fieldHex: "#2a2a2c", fieldOpacity: 100, nestedHex: "#0b0b0d" }, light: { panelHex: "#ffffff", panelOpacity: 100, fieldHex: "#ffffff", fieldOpacity: 100, nestedHex: "#f4f4f4" }, }, environment: { lightColor: "#ff2f92", brightness: 49, glowDistance: 105, connectionType: "spline", connectionColor: "#404040", usePortColors: false, fillColor: "#ff6caf", fillOpacity: 100, strokeColor: "#2b2b36", strokeOpacity: 100, }, media: { source: "file", url: "", fileName: "possible shapes.mp4", fileSrc: "/uploads/1783715822132-possible-shapes.mp4", visible: true, logoSource: "file", logoUrl: "", logoFileName: "nodedc-mark.svg", logoFileSrc: "/nodedc-mark.svg", faviconFileName: "icon-adaptive.svg", faviconAssets: { ico: "/favicon/favicon.ico", apple: "/favicon/apple-touch-icon.png", icon192: "/favicon/icon-192.png", icon512: "/favicon/icon-512.png", }, }, glass: { version: 4, tintHex: "#0b0b0e", tintOpacity: 94, blur: 62, saturation: 170, brightness: 129, outlineOpacity: 11, shadowOpacity: 88, }, toolbar: { placement: "bottom", background: "#111115", border: "#111117", outline: "#1c1c1c", minSize: 25, maxSize: 78, lensCount: 3, autoHide: true, }, pageTypes: defaultDesignProfilePageTypes(), }; } async function ensureDefaultDesignProfile() { try { await readFile(designProfilePath("default"), "utf8"); } catch (error) { if (error?.code !== "ENOENT") throw error; let layout = defaultDesignProfileLayout(); try { layout = JSON.parse(await readFile(layoutPath, "utf8")); } catch (layoutError) { if (layoutError?.code !== "ENOENT") throw layoutError; } const now = new Date().toISOString(); await writeFile(designProfilePath("default"), `${JSON.stringify({ schemaVersion: "0.1.0", id: "default", name: "NODE.DC Default", version: "0.6.0", status: "draft", layout, timestamps: { createdAt: now, updatedAt: now }, }, null, 2)}\n`, "utf8"); } } await ensureDefaultDesignProfile(); async function writeDesignProfile(profile) { const targetPath = designProfilePath(profile.id); const tempPath = `${targetPath}.${randomUUID()}.tmp`; await writeFile(tempPath, `${JSON.stringify(profile, null, 2)}\n`, "utf8"); await rename(tempPath, targetPath); } async function writeDesignProfileRelease(profile) { const release = validateDesignProfile({ ...profile, status: "published", timestamps: { ...profile.timestamps, publishedAt: new Date().toISOString() }, }); await mkdir(designProfileReleaseDir(release.id), { recursive: true }); try { await writeFile(designProfileReleasePath(release.id, release.version), `${JSON.stringify(release, null, 2)}\n`, { encoding: "utf8", flag: "wx" }); } catch (error) { if (error?.code === "EEXIST") throw applicationError("design_profile_version_already_published", 409); throw error; } return release; } async function readDesignProfileReleases(id) { const releases = []; try { for (const entry of await readdir(designProfileReleaseDir(id), { withFileTypes: true })) { if (!entry.isFile() || !entry.name.endsWith(".json")) continue; try { releases.push(validateDesignProfile(JSON.parse(await readFile(join(designProfileReleaseDir(id), entry.name), "utf8")))); } catch { /* skip damaged immutable snapshot */ } } } catch (error) { if (error?.code !== "ENOENT") throw error; } return releases.sort((left, right) => String(right.version).localeCompare(String(left.version), undefined, { numeric: true })); } async function ensureDefaultDesignProfileRelease() { const profile = validateDesignProfile(JSON.parse(await readFile(designProfilePath("default"), "utf8"))); try { await readFile(designProfileReleasePath(profile.id, profile.version), "utf8"); } catch (error) { if (error?.code === "ENOENT") await writeDesignProfileRelease(profile); else throw error; } } await ensureDefaultDesignProfileRelease(); function designProfileVersionSummary(profile) { return { version: profile.version, status: profile.status, theme: profile.layout.theme, publishedAt: profile.timestamps.publishedAt }; } function designProfileSummary(profile, releases = []) { return { id: profile.id, name: profile.name, version: profile.version, status: profile.status, theme: profile.layout.theme, updatedAt: profile.timestamps.updatedAt, versions: releases.map(designProfileVersionSummary), latestPublishedVersion: releases[0]?.version ?? null, }; } function nextPatchVersion(version) { const match = String(version || "0.1.0").match(/^(\d+)\.(\d+)\.(\d+)$/); return match ? `${match[1]}.${match[2]}.${Number(match[3]) + 1}` : "0.1.0"; } function validateApplicationManifest(value) { if (!value || typeof value !== "object" || Array.isArray(value)) throw applicationError("invalid_application_manifest"); if (value.schemaVersion !== "0.1.0") throw applicationError("unsupported_application_schema"); if (!/^[0-9a-f-]{36}$/i.test(String(value.id || ""))) throw applicationError("invalid_application_id"); if (value.status !== "draft") throw applicationError("invalid_application_status"); if (!/^0\.\d+\.\d+$/.test(String(value.version || ""))) throw applicationError("invalid_application_version"); if (!value.metadata || typeof value.metadata !== "object") throw applicationError("invalid_application_metadata"); const name = String(value.metadata.name || "").trim(); const slug = normalizeSlug(value.metadata.slug); if (!name || name.length > 120) throw applicationError("invalid_application_name"); if (!slug) throw applicationError("invalid_application_slug"); if (!value.designProfile || typeof value.designProfile !== "object") throw applicationError("invalid_design_profile"); // Pre-versioning manifests already represented the stable default 0.6.0 snapshot. const designProfileStatus = value.designProfile.status === "draft" ? "draft" : "published"; if (value.designProfile.theme !== "dark" && value.designProfile.theme !== "light") throw applicationError("invalid_application_theme"); if (!Array.isArray(value.pages)) throw applicationError("invalid_application_pages"); const pageIds = new Set(); const normalizedPages = []; for (const page of value.pages) { if (!page || typeof page !== "object") throw applicationError("invalid_application_page"); const pageId = String(page.id || "").trim(); if (!/^[a-z0-9-]+$/.test(pageId) || pageIds.has(pageId)) throw applicationError("invalid_application_page_id"); pageIds.add(pageId); if (!page.template || typeof page.template !== "object" || !String(page.template.id || "").trim()) { throw applicationError("invalid_application_page_template"); } const template = findPageTemplate(String(page.template.id), String(page.template.version || "")); if (!template) throw applicationError("unknown_application_page_template"); if (!page.navigation || typeof page.navigation.visible !== "boolean") throw applicationError("invalid_application_navigation"); if (!page.features || typeof page.features !== "object" || Array.isArray(page.features)) throw applicationError("invalid_application_features"); let normalizedPage = page; if (page.layout !== undefined) { if (!isObject(page.layout)) throw applicationError("invalid_application_page_layout"); if (page.template.id === "map" && page.layout.map !== undefined) { normalizedPage = { ...page, layout: { ...page.layout, map: validateMapPageLayout(page.layout.map) }, }; } } if (page.designOverrides !== undefined) { if (!isObject(page.designOverrides) || Object.keys(page.designOverrides).some((key) => key !== "map")) { throw applicationError("invalid_application_page_design_overrides"); } if (page.designOverrides.map !== undefined) { if (page.template.id !== "map") throw applicationError("unsupported_application_page_design_overrides"); normalizedPage = { ...normalizedPage, designOverrides: { map: validateMapDesignOverrides(page.designOverrides.map) }, }; } } const allowedFeatures = new Set(template.features.map((feature) => feature.id)); if (Object.keys(page.features).some((feature) => !allowedFeatures.has(feature))) throw applicationError("unsupported_application_feature"); for (const feature of template.features) { if (feature.required && page.features[feature.id] !== true) throw applicationError("required_application_feature_disabled"); } normalizedPages.push(normalizedPage); } return { ...value, pages: normalizedPages, designProfile: { ...value.designProfile, status: designProfileStatus }, metadata: { ...value.metadata, name, slug, description: String(value.metadata.description || "").slice(0, 500), }, }; } function createApplicationManifest(input = {}) { const templateId = String(input?.templateId || "").trim(); const templateVersion = String(input?.templateVersion || "").trim() || undefined; const template = templateId ? findPageTemplate(templateId, templateVersion) : null; if (templateId && !template) throw applicationError("unknown_page_template"); const now = new Date().toISOString(); const name = String(input?.name || (template ? `NODE.DC ${template.title} Demo` : "Новый модуль")).trim() || "Новый модуль"; const slug = normalizeSlug(input?.slug || name) || "nodedc-map-demo"; return { schemaVersion: "0.1.0", id: randomUUID(), status: "draft", version: "0.1.0", metadata: { name, slug, description: String(input?.description || "Черновик NDC Module Foundry."), }, designProfile: { id: "default", version: "0.6.0", status: "published", theme: input?.theme === "light" ? "light" : "dark", }, pages: template ? [{ id: template.page.id, title: template.page.title, path: template.page.path, template: { id: template.id, version: template.version }, navigation: { visible: true, label: template.page.navigationLabel, order: 0 }, features: Object.fromEntries(template.features.map((feature) => [feature.id, feature.required ? true : feature.defaultVisible])), }] : [], favicon: { source: "design-profile", }, timestamps: { createdAt: now, updatedAt: now, }, }; } async function writeApplicationManifest(manifest) { const targetPath = applicationPath(manifest.id); await writeJsonAtomically(targetPath, manifest); } async function writeJsonAtomically(targetPath, value) { const tempPath = `${targetPath}.${randomUUID()}.tmp`; await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); await rename(tempPath, targetPath); } async function readApplicationManifest(id) { try { return validateApplicationManifest(JSON.parse(await readFile(applicationPath(id), "utf8"))); } catch (error) { if (error?.code === "ENOENT") throw applicationError("application_not_found", 404); throw error; } } function runtimeTargetGrantKey(applicationId, pageId, bindingId) { return createHash("sha256").update(`${applicationId}/${pageId}/${bindingId}`, "utf8").digest("hex"); } function isRuntimeIdentifier(value) { return typeof value === "string" && /^[A-Za-z0-9._:-]{1,160}$/.test(value); } function isRuntimeCursor(value) { return typeof value === "string" && /^(?:0|[1-9]\d*)$/.test(value); } function isRuntimeTimestamp(value) { return typeof value === "string" && !Number.isNaN(Date.parse(value)); } function isRuntimeVersion(value) { return typeof value === "string" && /^\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?$/i.test(value); } async function resolveRuntimeMapDataProductBinding(applicationId, pageId, bindingId) { const application = await readApplicationManifest(applicationId); const page = application.pages.find((candidate) => candidate.id === pageId); if (!page) throw applicationError("application_page_not_found", 404); if (page.template?.id !== "map") throw applicationError("map_page_required", 404); const template = findPageTemplate(page.template.id, page.template.version); const layout = validateMapPageLayout(page.layout?.map || defaultMapPageLayout()); const binding = layout.dataProductBindings.find((candidate) => candidate.id === bindingId); if (!binding) throw applicationError("data_product_binding_not_found", 404); const slot = template?.slots?.find((candidate) => candidate.id === binding.slotId); if (!isMapLiveDataSlot(slot)) throw applicationError("map_live_data_slot_required", 409); return { application, page, binding }; } async function resolveRuntimeDataProductReaderToken(applicationId, pageId, bindingId, generation = 1) { if (!externalDataPlaneInternalUrl || (!externalDataPlaneReaderGrantsDir && !foundryReaderGrantProvisioner.configured)) { throw applicationError("data_product_runtime_not_configured", 503); } const managedToken = await foundryReaderGrantProvisioner.readToken( { applicationId, pageId, bindingId }, { generation }, ); if (managedToken) return managedToken; if (generation !== 1) throw applicationError("data_product_reader_grant_not_found", 403); if (!externalDataPlaneReaderGrantsDir) throw applicationError("data_product_reader_grant_not_found", 403); // Compatibility fallback for grants issued before Foundry-owned managed // provisioning. There is no fallback to one shared Platform token. const grantPath = join(resolve(externalDataPlaneReaderGrantsDir), runtimeTargetGrantKey(applicationId, pageId, bindingId)); let handle; try { handle = await open(grantPath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0)); } catch (error) { if (error?.code === "ENOENT") throw applicationError("data_product_reader_grant_not_found", 403); if (error?.code === "ELOOP") throw applicationError("data_product_reader_grant_invalid", 503); throw applicationError("data_product_reader_grant_unavailable", 503); } try { const metadata = await handle.stat(); if (!metadata.isFile() || metadata.size < 2 || metadata.size > 1024 || (metadata.mode & 0o222) !== 0) { throw applicationError("data_product_reader_grant_invalid", 503); } // The production artifact mounts a root-owned, read-only secret // directory. Open the inode with O_NOFOLLOW and validate that same handle // before reading so an atomic rotation cannot become a symlink race. if (process.env.NODE_ENV === "production" && metadata.uid !== 0) { throw applicationError("data_product_reader_grant_invalid", 503); } let token; try { token = (await handle.readFile("utf8")).trim(); } catch { throw applicationError("data_product_reader_grant_unavailable", 503); } if (!/^ndc_edprb_[A-Za-z0-9_-]{32,512}$/.test(token)) { throw applicationError("data_product_reader_grant_invalid", 503); } return token; } finally { await handle.close(); } } async function preflightFoundryBindingReaderGrant({ applicationId, pageId, bindingId, dataProductId }) { let readerToken; try { let generation = 1; try { const statePath = join(foundryDataProductConsumersDir, `${runtimeTargetGrantKey(applicationId, pageId, bindingId)}.json`); const state = JSON.parse(await readFile(statePath, "utf8")); if (Number.isSafeInteger(state?.readerGrantGeneration) && state.readerGrantGeneration > 0) { generation = state.readerGrantGeneration; } } catch (error) { if (error?.code !== "ENOENT") throw error; } readerToken = await resolveRuntimeDataProductReaderToken(applicationId, pageId, bindingId, generation); } catch (error) { if (["data_product_reader_grant_not_found", "data_product_reader_grant_invalid"].includes(error?.message)) return false; throw error; } try { return Boolean(await readRuntimeReaderProduct(readerToken, dataProductId)); } catch (error) { if (error?.message === "data_product_access_denied") return false; throw error; } } async function readRuntimeReaderProduct(readerToken, dataProductId) { let upstream; try { upstream = await fetch(new URL("/internal/data-plane/v1/reader/data-products", `${externalDataPlaneInternalUrl}/`), { headers: { authorization: `Bearer ${readerToken}`, accept: "application/json" }, signal: AbortSignal.timeout(10_000), }); } catch { throw applicationError("data_product_reader_grant_preflight_unavailable", 503); } if (upstream.status === 401 || upstream.status === 403) throw applicationError("data_product_access_denied", 403); if (!upstream.ok) throw applicationError("data_product_reader_grant_preflight_unavailable", 503); const payload = await upstream.json().catch(() => null); const product = Array.isArray(payload?.dataProducts) ? payload.dataProducts.find((candidate) => candidate?.id === dataProductId) : null; return product && product.active !== false && product.deliveryMode === "snapshot+patch" ? product : null; } async function inspectFoundryConsumerReaderGrant(target, { generation = 1 } = {}) { try { const readerToken = await resolveRuntimeDataProductReaderToken( target.application.id, target.page.id, target.binding.id, generation, ); const product = await readRuntimeReaderProduct(readerToken, target.binding.dataProductId); if (product) return { product, readerGrantAction: "reuse", readerGrantGeneration: generation }; } catch (error) { if (!new Set(["data_product_reader_grant_not_found", "data_product_access_denied"]).has(error?.message)) throw error; } if (!foundryReaderGrantProvisioner.configured) { throw applicationError("data_product_reader_grant_not_found", 403); } const planned = await foundryReaderGrantProvisioner.plan(target); return { product: planned.product, readerGrantAction: "ensure", readerGrantGeneration: generation }; } function runtimePosition(value) { if (!Array.isArray(value) || value.length !== 2) return null; const [longitude, latitude] = value; if ( typeof longitude !== "number" || !Number.isFinite(longitude) || longitude < -180 || longitude > 180 || typeof latitude !== "number" || !Number.isFinite(latitude) || latitude < -90 || latitude > 90 ) return null; return [longitude, latitude]; } function runtimeLinearRing(value) { if (!Array.isArray(value) || value.length < 4 || value.length > 10_000) return null; const positions = value.map(runtimePosition); if (positions.some((position) => !position)) return null; const first = positions[0]; const last = positions.at(-1); if (!last || first[0] !== last[0] || first[1] !== last[1]) return null; return positions; } function runtimePolygonCoordinates(value) { if (!Array.isArray(value) || value.length < 1 || value.length > 256) return null; const rings = value.map(runtimeLinearRing); return rings.some((ring) => !ring) ? null : rings; } function runtimeGeometry(value) { if (!isObject(value) || JSON.stringify(value).length > 196_608) return null; if (value.type === "Point") { const coordinates = runtimePosition(value.coordinates); return coordinates ? { type: "Point", coordinates } : null; } if (value.type === "Polygon") { const coordinates = runtimePolygonCoordinates(value.coordinates); return coordinates ? { type: "Polygon", coordinates } : null; } if (value.type === "MultiPolygon" && Array.isArray(value.coordinates) && value.coordinates.length >= 1 && value.coordinates.length <= 256) { const coordinates = value.coordinates.map(runtimePolygonCoordinates); return coordinates.some((polygon) => !polygon) ? null : { type: "MultiPolygon", coordinates }; } return null; } const RUNTIME_SECRET_LIKE_KEY = /(token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key)/i; function safeRuntimeAttributeValue(value, depth = 0) { if (value === null || typeof value === "string" || typeof value === "boolean") return value; if (typeof value === "number" && Number.isFinite(value)) return value; if (depth >= 4) return null; if (Array.isArray(value)) return value.map((item) => safeRuntimeAttributeValue(item, depth + 1)); if (!isObject(value)) return null; return Object.fromEntries(Object.entries(value) .filter(([key]) => !RUNTIME_SECRET_LIKE_KEY.test(key)) .map(([key, item]) => [key, safeRuntimeAttributeValue(item, depth + 1)])); } function sanitizeRuntimeFact(value, binding) { if (!isObject(value)) return null; const sourceId = value.sourceId; const semanticType = value.semanticType; if (!isRuntimeIdentifier(sourceId) || !isRuntimeIdentifier(semanticType) || !binding.semanticTypes.includes(semanticType)) return null; if (!isRuntimeTimestamp(value.observedAt) || !isRuntimeTimestamp(value.receivedAt)) return null; const sourceAttributes = isObject(value.attributes) ? value.attributes : {}; const allowedFields = new Set(binding.fieldProjection); const attributes = Object.fromEntries(Object.entries(sourceAttributes) .filter(([key]) => allowedFields.has(key) && !RUNTIME_SECRET_LIKE_KEY.test(key)) .map(([key, item]) => [key, safeRuntimeAttributeValue(item)])); return { sourceId, semanticType, observedAt: value.observedAt, receivedAt: value.receivedAt, attributes, geometry: runtimeGeometry(value.geometry), }; } function sanitizeRuntimeSnapshot(value, binding) { if (!isObject(value) || value.schemaVersion !== "nodedc.data-product.snapshot/v1" || !isObject(value.dataProduct)) { throw applicationError("data_product_snapshot_contract_invalid", 502); } if (value.dataProduct.id !== binding.dataProductId || !isRuntimeVersion(value.dataProduct.version) || !isRuntimeCursor(value.cursor) || !isRuntimeTimestamp(value.generatedAt) || !Array.isArray(value.facts)) { throw applicationError("data_product_snapshot_contract_invalid", 502); } return { schemaVersion: "nodedc.data-product.snapshot/v1", dataProduct: { id: binding.dataProductId, version: String(value.dataProduct.version || "") }, generatedAt: value.generatedAt, cursor: value.cursor, facts: value.facts.map((fact) => sanitizeRuntimeFact(fact, binding)).filter(Boolean), }; } function sanitizeRuntimeHistory(value, binding) { if (!isObject(value) || value.schemaVersion !== "nodedc.data-product.history/v1" || !isObject(value.dataProduct) || !isObject(value.query)) { throw applicationError("data_product_history_contract_invalid", 502); } const query = value.query; if ( value.dataProduct.id !== binding.dataProductId || !isRuntimeVersion(value.dataProduct.version) || !isRuntimeTimestamp(value.generatedAt) || !isRuntimeTimestamp(query.from) || !isRuntimeTimestamp(query.to) || Date.parse(query.from) >= Date.parse(query.to) || !Number.isInteger(query.resolutionMs) || query.resolutionMs < 1000 || query.resolutionMs > 86_400_000 || query.order !== "asc" || !Array.isArray(query.sourceIds) || query.sourceIds.some((sourceId) => !isRuntimeIdentifier(sourceId)) || JSON.stringify(query.sourceIds) !== JSON.stringify([...new Set(query.sourceIds)].sort()) || !Array.isArray(value.facts) || (value.nextCursor !== undefined && (typeof value.nextCursor !== "string" || !/^[A-Za-z0-9_-]{1,1024}$/.test(value.nextCursor))) ) throw applicationError("data_product_history_contract_invalid", 502); let previousOrderKey = null; const facts = value.facts.flatMap((valueFact) => { if ( !isRuntimeTimestamp(valueFact?.bucketStart) || Date.parse(valueFact.bucketStart) < Date.parse(query.from) || Date.parse(valueFact.bucketStart) >= Date.parse(query.to) ) throw applicationError("data_product_history_contract_invalid", 502); const orderKey = `${valueFact.bucketStart}\u0000${valueFact.sourceId}\u0000${valueFact.semanticType}`; if (previousOrderKey !== null && orderKey <= previousOrderKey) { throw applicationError("data_product_history_contract_invalid", 502); } previousOrderKey = orderKey; const fact = sanitizeRuntimeFact(valueFact, binding); return fact ? [{ ...fact, bucketStart: valueFact.bucketStart }] : []; }); return { schemaVersion: "nodedc.data-product.history/v1", dataProduct: { id: binding.dataProductId, version: value.dataProduct.version }, generatedAt: value.generatedAt, query: { from: query.from, to: query.to, resolutionMs: query.resolutionMs, sourceIds: [...query.sourceIds], order: "asc", }, facts, ...(value.nextCursor ? { nextCursor: value.nextCursor } : {}), }; } function sanitizeRuntimePatch(value, binding) { if (!isObject(value) || value.schemaVersion !== "nodedc.data-product.patch/v1" || !isObject(value.dataProduct)) return null; if (value.dataProduct.id !== binding.dataProductId || !isRuntimeVersion(value.dataProduct.version) || !isRuntimeCursor(value.cursor) || !isRuntimeCursor(value.previousCursor) || !isRuntimeTimestamp(value.emittedAt) || !Array.isArray(value.operations)) return null; const operations = value.operations.flatMap((operation) => { if (!isObject(operation)) return []; if (operation.op === "upsert") { const fact = sanitizeRuntimeFact(operation.fact, binding); return fact ? [{ op: "upsert", fact }] : []; } if ( operation.op === "remove" && isRuntimeIdentifier(operation.sourceId) && isRuntimeIdentifier(operation.semanticType) && binding.semanticTypes.includes(operation.semanticType) ) { return [{ op: "remove", sourceId: operation.sourceId, semanticType: operation.semanticType, }]; } return []; }); return { schemaVersion: "nodedc.data-product.patch/v1", dataProduct: { id: binding.dataProductId, version: String(value.dataProduct.version || "") }, cursor: value.cursor, previousCursor: value.previousCursor, emittedAt: value.emittedAt, operations, }; } function resolveDataProductConsumerPolicy(product) { if (dataProductConsumerPolicyRegistry?.schemaVersion !== "nodedc.foundry.data-product-consumer-policies/v1") { throw applicationError("data_product_consumer_policy_registry_invalid", 500); } return dataProductConsumerPolicyRegistry.policies?.find((policy) => ( policy?.dataProductId === product.id && policy?.productVersion === product.version )) || null; } function runtimeUpstreamUrl(dataProductId, resource, after) { const target = new URL(`/internal/data-plane/v1/data-products/${encodeURIComponent(dataProductId)}/${resource}`, `${externalDataPlaneInternalUrl}/`); if (after) target.searchParams.set("after", after); return target; } function runtimeUpstreamFailure(status) { if (status === 401 || status === 403) return applicationError("data_product_access_denied", 403); if (status === 404) return applicationError("data_product_not_found", 404); if (status === 409) return applicationError("resync_required", 409); return applicationError("data_product_runtime_unavailable", 503); } function waitForResponseDrain(response) { return new Promise((resolve, reject) => { let settled = false; const settle = (result, error) => { if (settled) return; settled = true; response.off("drain", onDrain); response.off("close", onClose); response.off("error", onError); if (error) reject(error); else resolve(result); }; const onDrain = () => settle(true); const onClose = () => settle(false); const onError = (error) => settle(false, error); response.once("drain", onDrain); response.once("close", onClose); response.once("error", onError); if (response.destroyed || response.writableEnded) settle(false); }); } async function writeRuntimeStreamChunk(response, chunk) { if (response.destroyed || response.writableEnded) return false; try { if (response.write(chunk)) return true; return await waitForResponseDrain(response); } catch (error) { if (response.destroyed || response.writableEnded) return false; throw error; } } async function writeSse(response, { event, data, id }) { const frame = `${id ? `id: ${id}\n` : ""}event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; return writeRuntimeStreamChunk(response, frame); } function openRuntimeDataProductConsumerStream({ url, token, signal }) { return new Promise((resolveStream, rejectStream) => { const transport = url.protocol === "https:" ? httpsRequest : url.protocol === "http:" ? httpRequest : null; if (!transport) return rejectStream(applicationError("data_product_runtime_url_invalid", 500)); let incoming = null; let settled = false; const request = transport(url, { method: "GET", headers: { authorization: `Bearer ${token}`, accept: "text/event-stream", connection: "close", }, }); const abort = () => { incoming?.destroy(); request.destroy(); }; signal.addEventListener("abort", abort, { once: true }); request.once("response", (response) => { incoming = response; settled = true; response.once("close", () => signal.removeEventListener("abort", abort)); resolveStream({ status: response.statusCode || 500, ok: (response.statusCode || 500) >= 200 && (response.statusCode || 500) < 300, headers: new Headers(response.headers), body: Readable.toWeb(response), }); }); request.once("error", (error) => { signal.removeEventListener("abort", abort); if (!settled) rejectStream(signal.aborted ? applicationError("data_product_stream_closed", 503) : error); }); if (signal.aborted) abort(); else request.end(); }); } const dataProductConsumerManager = createFoundryDataProductConsumerManager({ stateDir: foundryDataProductConsumersDir, dataPlaneUrl: externalDataPlaneInternalUrl, resolveTarget: resolveRuntimeMapDataProductBinding, readReaderToken: resolveRuntimeDataProductReaderToken, inspectReaderGrant: inspectFoundryConsumerReaderGrant, ensureReaderGrant: (target, options) => foundryReaderGrantProvisioner.ensure(target, options), revokeReaderGrant: (target, options) => foundryReaderGrantProvisioner.revoke(target, options), resolvePolicy: resolveDataProductConsumerPolicy, sanitizeSnapshot: sanitizeRuntimeSnapshot, sanitizePatch: sanitizeRuntimePatch, openStream: openRuntimeDataProductConsumerStream, }); await dataProductConsumerManager.resumePersisted(); async function proxyRuntimeDataProductSnapshot(response, target) { json(response, 200, await dataProductConsumerManager.snapshot(target)); } async function readRuntimeJsonResponse(upstream, maxBytes = 64 * 1024 * 1024) { const contentLength = Number(upstream.headers.get("content-length") || 0); if (Number.isFinite(contentLength) && contentLength > maxBytes) { throw applicationError("data_product_runtime_response_too_large", 502); } if (!upstream.body) throw applicationError("data_product_runtime_invalid_response", 502); const reader = upstream.body.getReader(); const chunks = []; let bytes = 0; try { while (true) { const { done, value } = await reader.read(); if (done) break; bytes += value.byteLength; if (bytes > maxBytes) { await reader.cancel(); throw applicationError("data_product_runtime_response_too_large", 502); } chunks.push(Buffer.from(value)); } return JSON.parse(Buffer.concat(chunks, bytes).toString("utf8")); } catch (error) { if (error?.statusCode) throw error; throw applicationError("data_product_runtime_invalid_response", 502); } } function runtimeHistoryQuery(url) { const from = String(url.searchParams.get("from") || ""); const to = String(url.searchParams.get("to") || ""); const resolutionMs = Number(url.searchParams.get("resolutionMs") || 60_000); const limit = Number(url.searchParams.get("limit") || 1000); const sourceIds = [...new Set(String(url.searchParams.get("sourceIds") || "").split(",").map((value) => value.trim()).filter(Boolean))]; const cursor = String(url.searchParams.get("cursor") || ""); if ( !isRuntimeTimestamp(from) || !isRuntimeTimestamp(to) || Date.parse(from) >= Date.parse(to) || !Number.isInteger(resolutionMs) || resolutionMs < 1000 || resolutionMs > 86_400_000 || !Number.isInteger(limit) || limit < 1 || limit > 5000 || sourceIds.length > 1000 || sourceIds.some((sourceId) => !isRuntimeIdentifier(sourceId)) || (cursor && !/^[A-Za-z0-9_-]{1,1024}$/.test(cursor)) ) throw applicationError("data_product_history_query_invalid", 400); return { from, to, resolutionMs, limit, sourceIds, cursor }; } async function proxyRuntimeDataProductHistory(response, url, target) { const readerToken = await resolveRuntimeDataProductReaderToken(target.application.id, target.page.id, target.binding.id); const query = runtimeHistoryQuery(url); const upstreamUrl = runtimeUpstreamUrl(target.binding.dataProductId, "history"); upstreamUrl.searchParams.set("from", query.from); upstreamUrl.searchParams.set("to", query.to); upstreamUrl.searchParams.set("resolutionMs", String(query.resolutionMs)); upstreamUrl.searchParams.set("limit", String(query.limit)); if (query.sourceIds.length) upstreamUrl.searchParams.set("sourceIds", query.sourceIds.join(",")); if (query.cursor) upstreamUrl.searchParams.set("cursor", query.cursor); let upstream; try { upstream = await fetch(upstreamUrl, { headers: { authorization: `Bearer ${readerToken}`, accept: "application/json" }, signal: AbortSignal.timeout(10_000), }); } catch { throw applicationError("data_product_runtime_unavailable", 503); } if (!upstream.ok) throw runtimeUpstreamFailure(upstream.status); const payload = await readRuntimeJsonResponse(upstream); json(response, 200, sanitizeRuntimeHistory(payload, target.binding)); } async function proxyRuntimeDataProductStream(request, response, url, target) { const requestedAfter = String(url.searchParams.get("after") || ""); const lastEventId = String(request.headers["last-event-id"] || ""); if (requestedAfter && !isRuntimeCursor(requestedAfter)) throw applicationError("stream_cursor_invalid"); if (lastEventId && !isRuntimeCursor(lastEventId)) throw applicationError("stream_cursor_invalid"); // EventSource preserves the original URL on an automatic reconnect while // adding Last-Event-ID. The latter is the authoritative acknowledged // cursor; using it avoids replaying a stale query-string cursor forever. const after = lastEventId || requestedAfter; const controller = new AbortController(); let lease = null; let heartbeat = null; let headersReady = false; const bufferedEvents = []; let pendingEventCount = 0; let writeChain = Promise.resolve(true); const enqueue = (event) => { if (!headersReady) { bufferedEvents.push(event); return; } pendingEventCount += 1; if (pendingEventCount > 256) { abort(); response.destroy(); return; } writeChain = writeChain .then((open) => open && writeSse(response, event)) .catch(() => false) .finally(() => { pendingEventCount = Math.max(0, pendingEventCount - 1); }); }; const abort = () => { if (!controller.signal.aborted) controller.abort(); lease?.release(); }; const activeStream = { controller, response }; activeRuntimeStreams.add(activeStream); request.once("aborted", abort); response.once("close", abort); request.socket?.once("close", abort); try { lease = await dataProductConsumerManager.subscribe({ ...target, after }, enqueue); response.writeHead(200, { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-store, no-transform", connection: "keep-alive", "x-accel-buffering": "no", vary: "Cookie", }); headersReady = true; if (lease.resyncRequired) { await writeSse(response, { event: "nodedc.data-product.resync-required.v1", data: { schemaVersion: "nodedc.data-product.resync-required/v1", dataProductId: target.binding.dataProductId }, }); response.end(); return; } for (const event of bufferedEvents.splice(0)) enqueue(event); heartbeat = setInterval(() => { writeChain = writeChain.then((open) => open && writeRuntimeStreamChunk(response, ": keepalive\n\n")).catch(() => false); }, 15_000); heartbeat.unref?.(); await new Promise((resolve) => { if (controller.signal.aborted || response.destroyed) return resolve(); controller.signal.addEventListener("abort", resolve, { once: true }); }); } finally { if (heartbeat) clearInterval(heartbeat); abort(); activeRuntimeStreams.delete(activeStream); request.off("aborted", abort); response.off("close", abort); request.socket?.off("close", abort); if (!response.writableEnded && !response.destroyed) response.end(); } } async function listApplicationManifests() { const manifests = []; for (const entry of await readdir(applicationsDir, { withFileTypes: true })) { if (!entry.isFile() || !entry.name.endsWith(".json")) continue; try { manifests.push(validateApplicationManifest(JSON.parse(await readFile(join(applicationsDir, entry.name), "utf8")))); } catch { // A damaged draft must not make the complete Foundry project list unavailable. } } return manifests.sort((left, right) => String(right.timestamps.updatedAt).localeCompare(String(left.timestamps.updatedAt))); } function applicationSummary(manifest) { return { id: manifest.id, name: manifest.metadata.name, slug: manifest.metadata.slug, status: manifest.status, version: manifest.version, theme: manifest.designProfile.theme, pageCount: manifest.pages.length, updatedAt: manifest.timestamps.updatedAt, }; } async function assertDesignProfileReference(reference) { if (!reference || typeof reference !== "object") throw applicationError("invalid_design_profile_reference"); const profileId = String(reference.id || ""); const version = String(reference.version || ""); let profile; try { const sourcePath = reference.status === "published" ? designProfileReleasePath(profileId, version) : designProfilePath(profileId); profile = validateDesignProfile(JSON.parse(await readFile(sourcePath, "utf8"))); } catch (error) { if (error?.code === "ENOENT") throw applicationError("design_profile_reference_not_found", 409); throw error; } if (profile.version !== version || profile.layout.theme !== reference.theme) { throw applicationError("design_profile_reference_mismatch", 409); } if (reference.status === "published" && profile.status !== "published") { throw applicationError("design_profile_reference_not_published", 409); } return profile; } function stringListEnv(name) { return String(process.env[name] || "") .split(",") .map((value) => value.trim()) .filter(Boolean); } function foundryMcpConfig() { const publicUrl = String(process.env.FOUNDRY_PUBLIC_URL || "").trim().replace(/\/$/, ""); const mcpUrl = String(process.env.FOUNDRY_MCP_URL || (publicUrl ? `${publicUrl}/api/mcp` : "")).trim(); const internalAccessToken = String(process.env.NODEDC_INTERNAL_ACCESS_TOKEN || process.env.NODEDC_PLATFORM_SERVICE_TOKEN || "").trim(); const requestedCapabilityTtlMs = Number(process.env.FOUNDRY_MCP_CAPABILITY_TTL_MS || 600_000); return { // The shared platform service credential never leaves a server process. It is used // only to authenticate the AI Workspace entitlement request and to sign a short-lived // Foundry MCP capability bound to the resolved actor and owner key. internalAccessToken, capabilitySecret: internalAccessToken, mcpCapabilityTtlMs: Number.isFinite(requestedCapabilityTtlMs) ? Math.min(Math.max(Math.trunc(requestedCapabilityTtlMs), 60_000), 3_600_000) : 600_000, mcpUrl, mcpAllowedOrigins: stringListEnv("FOUNDRY_MCP_ALLOWED_ORIGINS"), allowAllAuthenticated: String(process.env.FOUNDRY_ALLOW_ALL_AUTHENTICATED || "").trim().toLowerCase() === "true", // dcctouch@gmail.com is the platform superadmin (user_root). The group is also // provisioned through Authentik/Launcher as the canonical service access rule. allowedOwnerIds: stringListEnv("FOUNDRY_ALLOWED_OWNER_IDS").concat("user_root"), allowedOwnerGroups: stringListEnv("FOUNDRY_ALLOWED_OWNER_GROUPS") .concat("nodedc:superadmin", "nodedc:module-foundry:admin", "nodedc:module-foundry:user", "nodedc:module-foundry:access") .map((value) => value.toLowerCase()), }; } function requiredMcpIdempotencyKey(value) { const key = requireNonEmptyString(value, "idempotency_key_required", 160); if (!/^[A-Za-z0-9._:-]+$/.test(key)) throw applicationError("invalid_idempotency_key"); return key; } const foundryMcpInFlightOperations = new Map(); async function executeFoundryMcpWrite({ tool, actor, input, action }) { const idempotencyKey = requiredMcpIdempotencyKey(input.idempotencyKey); const actorId = requireNonEmptyString(actor?.actorId, "mcp_actor_context_required", 256); const ownerKey = requireNonEmptyString(actor?.ownerKey, "mcp_actor_context_required", 256); const inputForHash = { ...input }; delete inputForHash.idempotencyKey; const inputHash = createHash("sha256").update(JSON.stringify(inputForHash)).digest("hex"); const operationHash = createHash("sha256").update(`${tool}\n${ownerKey}\n${idempotencyKey}`).digest("hex"); const path = join(foundryMcpOperationsDir, `${operationHash}.json`); const inFlight = foundryMcpInFlightOperations.get(operationHash); if (inFlight) return inFlight; const pending = (async () => { try { const recorded = JSON.parse(await readFile(path, "utf8")); if (recorded.tool !== tool || recorded.ownerKey !== ownerKey || recorded.inputHash !== inputHash) { throw applicationError("idempotency_key_reused", 409); } return { ...recorded.result, idempotency: { replayed: true } }; } catch (error) { if (error?.code !== "ENOENT") throw error; } const result = await action(); await writeJsonAtomically(path, { schemaVersion: "nodedc.module-foundry.mcp-operation.v1", tool, ownerKey, actorId, inputHash, completedAt: new Date().toISOString(), result, }); return { ...result, idempotency: { replayed: false } }; })(); foundryMcpInFlightOperations.set(operationHash, pending); try { return await pending; } finally { foundryMcpInFlightOperations.delete(operationHash); } } function normalizeMcpPageTitle(value, fallback, code) { if (value === undefined) return fallback; const title = requireNonEmptyString(value, code, 120).trim(); return title || fallback; } function nextPageInstance(manifest, input) { const templateId = requireNonEmptyString(input.templateId, "invalid_application_page_template", 64); const templateVersion = input.templateVersion === undefined ? undefined : requireNonEmptyString(input.templateVersion, "invalid_application_page_template_version", 64); const template = findPageTemplate(templateId, templateVersion); if (!template) throw applicationError("unknown_application_page_template"); const suffix = randomUUID().slice(0, 8); const id = `${template.page.id}-${suffix}`; const maxOrder = manifest.pages.reduce((max, page) => Math.max(max, Number(page.navigation?.order ?? -1)), -1); return { id, title: normalizeMcpPageTitle(input.title, template.page.title, "invalid_application_page_title"), path: `${template.page.path.replace(/\/$/, "") || "/"}-${suffix}`.replace(/^\/-/, "/"), template: { id: template.id, version: template.version }, navigation: { visible: true, label: normalizeMcpPageTitle(input.navigationLabel, template.page.navigationLabel, "invalid_application_page_navigation_label"), order: maxOrder + 1, }, features: Object.fromEntries(template.features.map((feature) => [feature.id, feature.required ? true : feature.defaultVisible])), }; } async function writeFoundryApplicationUpdate(applicationId, update) { const current = await readApplicationManifest(applicationId); const nextCandidate = await update(current); const next = validateApplicationManifest({ ...nextCandidate, id: current.id, schemaVersion: current.schemaVersion, status: current.status, timestamps: { createdAt: current.timestamps.createdAt, updatedAt: new Date().toISOString(), }, }); await assertDesignProfileReference(next.designProfile); await writeApplicationManifest(next); return next; } const foundryMcpOperations = { async status() { const config = foundryMcpConfig(); return { module: "NDC Module Foundry", schemaVersion: "nodedc.module-foundry.mcp.v0.6", status: config.capabilitySecret && config.mcpUrl ? "ready" : "configuration_required", canonicalPageLibrary: "read-only", applicationInstances: "read-write", destructiveApplicationDelete: false, supportedPageTemplates: pageRegistry.templates.map((template) => ({ id: template.id, version: template.version, title: template.title })), supportedActions: [ "application.create", "application.metadata.update", "page-instance.create", "map-pin.upsert", "map-pin.remove", "map-presentation-profile.upsert", "map-subject-detail-profile.upsert", "map-page-settings.update", "map-page-view-state.save", "map-data-product.upsert", "map-data-product-consumer.plan", "map-data-product-consumer.apply", "map-data-product-consumer.status", "map-data-product-consumer.accept", "map-data-product-consumer.rollback", ], persistence: { runtimeDir: "persistent runtime volume required", idempotentWrites: true, dataProductConsumerState: true }, }; }, async listApplications() { const manifests = await listApplicationManifests(); return { applications: manifests.map(applicationSummary) }; }, async getApplication(applicationId) { return { application: await readApplicationManifest(applicationId) }; }, async createApplication(input, actor) { return executeFoundryMcpWrite({ tool: "foundry_create_application", actor, input, action: async () => { const name = normalizeMcpPageTitle(input.name, "Новый модуль", "invalid_application_name"); const manifest = validateApplicationManifest(createApplicationManifest({ ...input, name, templateId: input.templateId || "map", description: input.description === undefined ? "Черновик NDC Module Foundry." : input.description, })); const collision = (await listApplicationManifests()).some((item) => item.metadata.slug === manifest.metadata.slug); if (collision) throw applicationError("application_slug_conflict", 409); await assertDesignProfileReference(manifest.designProfile); await writeApplicationManifest(manifest); return { application: manifest }; }, }); }, async updateApplicationMetadata(input, actor) { return executeFoundryMcpWrite({ tool: "foundry_update_application_metadata", actor, input, action: async () => { const application = await writeFoundryApplicationUpdate(input.applicationId, async (current) => ({ ...current, metadata: { ...current.metadata, ...(Object.hasOwn(input, "name") ? { name: normalizeMcpPageTitle(input.name, current.metadata.name, "invalid_application_name") } : {}), ...(Object.hasOwn(input, "slug") ? (() => { const slug = normalizeSlug(input.slug); if (!slug) throw applicationError("invalid_application_slug"); return { slug }; })() : {}), ...(Object.hasOwn(input, "description") ? { description: String(input.description || "").slice(0, 500) } : {}), }, })); return { application }; }, }); }, async addPageInstance(input, actor) { return executeFoundryMcpWrite({ tool: "foundry_add_page_instance", actor, input, action: async () => { const application = await writeFoundryApplicationUpdate(input.applicationId, async (current) => ({ ...current, pages: [...current.pages, nextPageInstance(current, input)], })); return { application, page: application.pages.at(-1) }; }, }); }, async upsertMapPinBinding(input, actor) { return executeFoundryMcpWrite({ tool: "foundry_upsert_map_pin_binding", actor, input, action: async () => { const binding = validateMapPinBinding(input.binding); let updatedPage = null; const application = await writeFoundryApplicationUpdate(input.applicationId, async (current) => ({ ...current, pages: current.pages.map((page) => { if (page.id !== input.pageId) return page; if (page.template?.id !== "map") throw applicationError("map_page_required"); const layout = validateMapPageLayout(page.layout?.map || defaultMapPageLayout()); const pinBindings = layout.pinBindings.filter((item) => item.id !== binding.id); pinBindings.push(binding); updatedPage = { ...page, layout: { ...page.layout, map: { ...layout, pinBindings, savedAt: new Date().toISOString() } }, }; return updatedPage; }), })); if (!updatedPage) throw applicationError("application_page_not_found", 404); return { application, page: updatedPage, binding }; }, }); }, async removeMapPinBinding(input, actor) { return executeFoundryMcpWrite({ tool: "foundry_remove_map_pin_binding", actor, input, action: async () => { const bindingId = requireNonEmptyString(input.bindingId, "invalid_map_pin_id", 128); if (!/^[A-Za-z0-9._:-]+$/.test(bindingId)) throw applicationError("invalid_map_pin_id"); let updatedPage = null; let removed = false; const application = await writeFoundryApplicationUpdate(input.applicationId, async (current) => ({ ...current, pages: current.pages.map((page) => { if (page.id !== input.pageId) return page; if (page.template?.id !== "map") throw applicationError("map_page_required"); const layout = validateMapPageLayout(page.layout?.map || defaultMapPageLayout()); const pinBindings = layout.pinBindings.filter((item) => item.id !== bindingId); removed = pinBindings.length !== layout.pinBindings.length; updatedPage = { ...page, layout: { ...page.layout, map: { ...layout, pinBindings, savedAt: new Date().toISOString() } }, }; return updatedPage; }), })); if (!updatedPage) throw applicationError("application_page_not_found", 404); return { application, page: updatedPage, bindingId, removed }; }, }); }, async upsertMapPresentationProfile(input, actor) { return executeFoundryMcpWrite({ tool: "foundry_upsert_map_presentation_profile", actor, input, action: async () => { const profile = normalizeMapPresentationProfile(input.profile); let updatedPage = null; const application = await writeFoundryApplicationUpdate(input.applicationId, async (current) => ({ ...current, pages: current.pages.map((page) => { if (page.id !== input.pageId) return page; if (page.template?.id !== "map") throw applicationError("map_page_required"); const layout = validateMapPageLayout(page.layout?.map || defaultMapPageLayout()); const presentationProfiles = layout.presentationProfiles.filter((item) => item.id !== profile.id); presentationProfiles.push(profile); updatedPage = { ...page, layout: { ...page.layout, map: validateMapPageLayout({ ...layout, presentationProfiles, savedAt: new Date().toISOString(), }) }, designOverrides: { ...page.designOverrides, map: { ...page.designOverrides?.map, presentationProfiles, }, }, }; return updatedPage; }), })); if (!updatedPage) throw applicationError("application_page_not_found", 404); return { application, page: updatedPage, profile }; }, }); }, async upsertMapSubjectDetailProfile(input, actor) { return executeFoundryMcpWrite({ tool: "foundry_upsert_map_subject_detail_profile", actor, input, action: async () => { const profile = normalizeMapSubjectDetailProfile(input.profile); let updatedPage = null; const application = await writeFoundryApplicationUpdate(input.applicationId, async (current) => ({ ...current, pages: current.pages.map((page) => { if (page.id !== input.pageId) return page; if (page.template?.id !== "map") throw applicationError("map_page_required"); const layout = validateMapPageLayout(page.layout?.map || defaultMapPageLayout()); const subjectDetailProfiles = layout.subjectDetailProfiles.filter((item) => item.id !== profile.id); subjectDetailProfiles.push(profile); updatedPage = { ...page, layout: { ...page.layout, map: validateMapPageLayout({ ...layout, subjectDetailProfiles, savedAt: new Date().toISOString(), }) }, }; return updatedPage; }), })); if (!updatedPage) throw applicationError("application_page_not_found", 404); return { application, page: updatedPage, profile }; }, }); }, async updateMapPageSettings(input, actor) { return executeFoundryMcpWrite({ tool: "foundry_update_map_page_settings", actor, input, action: async () => { const settingsPatch = validateMapPageSettingsPatch(input.settings); let updatedPage = null; const application = await writeFoundryApplicationUpdate(input.applicationId, async (current) => ({ ...current, pages: current.pages.map((page) => { if (page.id !== input.pageId) return page; if (page.template?.id !== "map") throw applicationError("map_page_required"); const layout = validateMapPageLayout(page.layout?.map || defaultMapPageLayout()); updatedPage = { ...page, layout: { ...page.layout, map: validateMapPageLayout({ ...layout, settings: { ...layout.settings, ...settingsPatch }, }) }, }; return updatedPage; }), })); if (!updatedPage) throw applicationError("application_page_not_found", 404); return { application, page: updatedPage, settings: updatedPage.layout.map.settings }; }, }); }, async saveMapPageViewState(input, actor) { return executeFoundryMcpWrite({ tool: "foundry_save_map_page_view_state", actor, input, action: async () => { if (!isObject(input.viewState)) throw applicationError("invalid_map_page_view_state"); let updatedPage = null; const application = await writeFoundryApplicationUpdate(input.applicationId, async (current) => ({ ...current, pages: current.pages.map((page) => { if (page.id !== input.pageId) return page; if (page.template?.id !== "map") throw applicationError("map_page_required"); const layout = validateMapPageLayout(page.layout?.map || defaultMapPageLayout()); const settings = input.viewState.settings === undefined ? layout.settings : { ...layout.settings, ...validateMapPageSettingsPatch(input.viewState.settings) }; updatedPage = { ...page, layout: { ...page.layout, map: validateMapPageLayout({ ...layout, settings, mapHeight: input.viewState.mapHeight, camera: input.viewState.camera, subjectStates: input.viewState.subjectStates, }) }, }; return updatedPage; }), })); if (!updatedPage) throw applicationError("application_page_not_found", 404); return { application, page: updatedPage, viewState: { settings: updatedPage.layout.map.settings, mapHeight: updatedPage.layout.map.mapHeight, camera: updatedPage.layout.map.camera, subjectStates: updatedPage.layout.map.subjectStates, } }; }, }); }, async upsertMapDataProductBinding(input, actor) { return executeFoundryMcpWrite({ tool: "foundry_upsert_map_data_product_binding", actor, input, action: async () => { const binding = validateMapDataProductBinding(input.binding); let updatedPage = null; const application = await writeFoundryApplicationUpdate(input.applicationId, async (current) => ({ ...current, pages: current.pages.map((page) => { if (page.id !== input.pageId) return page; if (page.template?.id !== "map") throw applicationError("map_page_required"); const template = findPageTemplate(page.template.id, page.template.version); const slot = template?.slots?.find((candidate) => candidate.id === binding.slotId); if (!isMapLiveDataSlot(slot)) throw applicationError("map_live_data_slot_required"); const layout = validateMapPageLayout(page.layout?.map || defaultMapPageLayout()); const existingBinding = layout.dataProductBindings.find((item) => item.id === binding.id); const resolvedBinding = { ...existingBinding, ...binding, ...(!binding.presentationProfileId && existingBinding?.presentationProfileId ? { presentationProfileId: existingBinding.presentationProfileId } : {}), }; if (resolvedBinding.presentationProfileId && !layout.presentationProfiles.some((profile) => profile.id === resolvedBinding.presentationProfileId)) { throw applicationError("map_data_product_presentation_profile_not_found"); } const dataProductBindings = layout.dataProductBindings.filter((item) => item.id !== binding.id); dataProductBindings.push(resolvedBinding); updatedPage = { ...page, layout: { ...page.layout, map: validateMapPageLayout({ ...layout, dataProductBindings, savedAt: new Date().toISOString(), }) }, }; return updatedPage; }), })); if (!updatedPage) throw applicationError("application_page_not_found", 404); return { application, page: updatedPage, binding: updatedPage.layout.map.dataProductBindings.find((item) => item.id === binding.id), }; }, }); }, async planMapDataProductConsumer(input) { return dataProductConsumerManager.plan(input); }, async applyMapDataProductConsumer(input, actor) { return executeFoundryMcpWrite({ tool: "foundry_apply_map_data_product_consumer", actor, input, action: () => dataProductConsumerManager.apply(input), }); }, async getMapDataProductConsumerStatus(input) { return dataProductConsumerManager.status(input); }, async acceptMapDataProductConsumer(input) { return dataProductConsumerManager.accept(input); }, async rollbackMapDataProductConsumer(input, actor) { return executeFoundryMcpWrite({ tool: "foundry_rollback_map_data_product_consumer", actor, input, action: () => dataProductConsumerManager.rollback(input), }); }, }; async function readJsonBody(request, maxBytes = 2 * 1024 * 1024) { const chunks = []; let size = 0; for await (const chunk of request) { size += chunk.length; if (size > maxBytes) throw new Error("payload_too_large"); chunks.push(chunk); } return JSON.parse(Buffer.concat(chunks).toString("utf8")); } function safeUploadName(name, contentType = "") { const allowed = new Set([".gif", ".ico", ".jpeg", ".jpg", ".m4v", ".mov", ".mp4", ".png", ".webm", ".webp"]); const contentTypeExtensions = { "image/gif": ".gif", "image/x-icon": ".ico", "image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp", "video/mp4": ".mp4", "video/quicktime": ".mov", "video/webm": ".webm", }; const requestedExtension = extname(name).toLowerCase(); const extension = allowed.has(requestedExtension) ? requestedExtension : contentTypeExtensions[contentType] || ".bin"; const stem = name.replace(/\.[^.]+$/, "").replace(/[^a-z0-9_-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "stage-video"; return `${Date.now()}-${stem}${extension}`; } async function serveFile(request, response, filePath) { const info = await stat(filePath); const contentType = mimeTypes[extname(filePath).toLowerCase()] || "application/octet-stream"; const range = request.headers.range?.match(/^bytes=(\d*)-(\d*)$/); if (range) { const start = range[1] ? Number(range[1]) : 0; const end = range[2] ? Math.min(Number(range[2]), info.size - 1) : info.size - 1; if (!Number.isFinite(start) || !Number.isFinite(end) || start > end || start >= info.size) { response.writeHead(416, { "content-range": `bytes */${info.size}` }); response.end(); return; } response.writeHead(206, { "content-type": contentType, "content-length": end - start + 1, "content-range": `bytes ${start}-${end}/${info.size}`, "accept-ranges": "bytes", }); if (request.method === "HEAD") return response.end(); createReadStream(filePath, { start, end }).pipe(response); return; } response.writeHead(200, { "content-type": contentType, "content-length": info.size, "accept-ranges": "bytes", }); if (request.method === "HEAD") return response.end(); createReadStream(filePath).pipe(response); } const browserCacheableMapStates = new Set([ "live-cache-hit", "live-cache-stale", "legacy-cache-hit", "legacy-cache-stale", "live-offline-hit", "live-offline-stale", "offline-offline-hit", "offline-offline-stale", "live-stale-upstream-error", ]); function mapGatewayRefreshRequested(incoming, pathname) { if (pathname !== "/api/map/cache") return false; const rawTarget = incoming.searchParams.get("url"); if (!rawTarget) return false; try { return new URL(rawTarget).searchParams.get("nodedc_cache_refresh") === "1"; } catch { return false; } } function weakEtagValue(value) { return String(value || "").trim().replace(/^W\//i, ""); } function requestValidatorMatches(request, etag, lastModified) { const ifNoneMatch = String(request.headers["if-none-match"] || "").trim(); if (ifNoneMatch && etag) { if (ifNoneMatch === "*") return true; const expected = weakEtagValue(etag); return ifNoneMatch.split(",").some((candidate) => weakEtagValue(candidate) === expected); } const ifModifiedSince = Date.parse(String(request.headers["if-modified-since"] || "")); const modifiedAt = Date.parse(String(lastModified || "")); return !ifNoneMatch && Number.isFinite(ifModifiedSince) && Number.isFinite(modifiedAt) && modifiedAt <= ifModifiedSince; } async function proxyMapGateway(request, response, pathname) { if (!mapGatewayInternalUrl) return json(response, 503, { error: "map_gateway_not_configured" }); if (!pathname.startsWith("/healthz") && !pathname.startsWith("/api/map/")) { return json(response, 404, { error: "map_gateway_route_not_found" }); } // Gateway administration is intentionally not a transparent browser proxy. // It is reachable only through the admin-only signed path below. if (pathname.startsWith("/api/map/admin/")) return json(response, 404, { error: "map_gateway_route_not_found" }); const sessionUser = foundryAuth.currentUserProfile(request); // Local development deliberately runs without Hub handoff; production always // requires a validated session before this proxy is reachable. const subjectId = sessionUser?.id || (!foundryAuth.authRequired ? "local-dev" : ""); if (!subjectId) return json(response, 401, { error: "module_foundry_auth_required" }); const incoming = new URL(request.url || "/", "http://foundry.local"); const target = new URL(`${pathname}${incoming.search}`, `${mapGatewayInternalUrl}/`); const ionReferer = foundryRequestIonReferer(request); const controller = new AbortController(); let clientDisconnected = false; let headersTimedOut = false; let bodyIdleTimedOut = false; let bodyIdleTimeout = null; const abortClientRequest = () => { clientDisconnected = true; if (!controller.signal.aborted) controller.abort(); }; const abortClosedResponse = () => { if (!response.writableEnded) abortClientRequest(); }; const headersTimeout = setTimeout(() => { headersTimedOut = true; if (!controller.signal.aborted) controller.abort(); }, mapGatewayHeadersTimeoutMs); const clearBodyIdleTimeout = () => { if (bodyIdleTimeout !== null) clearTimeout(bodyIdleTimeout); bodyIdleTimeout = null; }; const armBodyIdleTimeout = () => { clearBodyIdleTimeout(); bodyIdleTimeout = setTimeout(() => { bodyIdleTimedOut = true; if (!controller.signal.aborted) controller.abort(); }, mapGatewayBodyIdleTimeoutMs); }; request.once("aborted", abortClientRequest); response.once("close", abortClosedResponse); let upstream; try { const upstreamHeaders = { accept: String(request.headers.accept || "*/*"), "x-nodedc-user-id": subjectId, ...(ionReferer ? { "x-nodedc-ion-referer": ionReferer } : {}), }; for (const name of ["range", "if-none-match", "if-modified-since"]) { const value = String(request.headers[name] || "").trim(); if (value) upstreamHeaders[name] = value; } upstream = await fetch(target, { method: request.method, headers: upstreamHeaders, signal: controller.signal, }); // The connection/header deadline ends as soon as Gateway has produced a // response. The body has a separate progress-based idle timeout below, so // a healthy large Cesium tile is never killed by an absolute wall clock. clearTimeout(headersTimeout); if (clientDisconnected || response.destroyed) { await upstream.body?.cancel().catch(() => undefined); return; } const cacheState = String(upstream.headers.get("x-nodedc-map-cache") || ""); const cacheable = pathname === "/api/map/cache" && [200, 206, 304].includes(upstream.status) && browserCacheableMapStates.has(cacheState) && !mapGatewayRefreshRequested(incoming, pathname); const etag = upstream.headers.get("etag"); const lastModified = upstream.headers.get("last-modified"); const notModified = cacheable && upstream.status === 200 && request.method === "GET" && requestValidatorMatches(request, etag, lastModified); const headers = ["content-type", "content-length", "content-range", "accept-ranges", "etag", "last-modified", "x-nodedc-map-cache", "x-nodedc-map-cache-age"]; for (const header of headers) { const value = upstream.headers.get(header); if (value) response.setHeader(header, value); } if (cacheable) { // The object was confirmed by the private Gateway as a persistent-cache // hit. Keep it in this browser only; shared reverse proxies must still // revalidate the authenticated request. response.setHeader("cache-control", "private, max-age=300, stale-while-revalidate=60"); response.setHeader("vary", "Cookie"); } else { // Provider responses, health/configuration calls, errors and explicit // refreshes must never create an uncontrolled browser-side copy. response.setHeader("cache-control", "no-store"); } response.statusCode = notModified ? 304 : upstream.status; if (notModified) { response.removeHeader("content-length"); response.removeHeader("content-range"); await upstream.body?.cancel().catch(() => undefined); return response.end(); } if (request.method === "HEAD" || upstream.status === 304 || !upstream.body) { await upstream.body?.cancel().catch(() => undefined); return response.end(); } const bodyProgress = new Transform({ transform(chunk, _encoding, callback) { armBodyIdleTimeout(); callback(null, chunk); }, flush(callback) { // Once Gateway reached EOF there is no upstream body left to time out. // Let Node finish draining buffered bytes to a slow browser; its own // disconnect is still handled by the response close listener. clearBodyIdleTimeout(); callback(); }, }); armBodyIdleTimeout(); await pipeline(Readable.fromWeb(upstream.body), bodyProgress, response, { signal: controller.signal }); } catch (error) { if (clientDisconnected || response.destroyed) return; if (response.headersSent) { if (!response.writableEnded && !response.destroyed) response.destroy(error); return; } if (headersTimedOut) throw applicationError("map_gateway_headers_timeout", 504); if (bodyIdleTimedOut) throw applicationError("map_gateway_body_idle_timeout", 504); throw error; } finally { clearTimeout(headersTimeout); clearBodyIdleTimeout(); request.off("aborted", abortClientRequest); response.off("close", abortClosedResponse); } } function requireFoundryAdmin(request) { const access = foundryAuth.currentUserAccess(request); if (!access.admin) throw applicationError("module_foundry_admin_required", 403); const user = foundryAuth.currentUserProfile(request); const actorId = String(user?.id || (!foundryAuth.authRequired ? "local-dev" : "")).trim(); if (!actorId) throw applicationError("module_foundry_admin_required", 403); return { actorId }; } function signedGatewayAdminHeaders(method, pathname, actorId, body, ionReferer) { if (!mapGatewayAdminSecret) throw applicationError("map_gateway_admin_not_configured", 503); const timestamp = String(Date.now()); const bodyHash = createHash("sha256").update(body).digest("hex"); const message = `nodedc.map-gateway.admin.v2\n${method}\n${pathname}\n${timestamp}\n${actorId}\n${bodyHash}\n${ionReferer}`; const signature = createHmac("sha256", mapGatewayAdminSecret).update(message).digest("hex"); return { "x-nodedc-admin-timestamp": timestamp, "x-nodedc-admin-actor": actorId, "x-nodedc-admin-signature": signature, ...(ionReferer ? { "x-nodedc-ion-referer": ionReferer } : {}), }; } async function requestMapGatewayAdmin({ method, pathname, actorId, payload, ionReferer = "" }) { if (!mapGatewayInternalUrl) throw applicationError("map_gateway_not_configured", 503); const body = payload === undefined ? "" : JSON.stringify(payload); const upstream = await fetch(new URL(pathname, `${mapGatewayInternalUrl}/`), { method, headers: { accept: "application/json", ...(body ? { "content-type": "application/json" } : {}), ...signedGatewayAdminHeaders(method, pathname, actorId, body, ionReferer), }, body: body || undefined, signal: AbortSignal.timeout(10_000), }); const result = await upstream.json().catch(() => null); if (!upstream.ok || !result?.ok) { const status = upstream.status >= 400 && upstream.status < 600 ? upstream.status : 502; const safeError = ["map_gateway_admin_not_configured", "map_gateway_admin_unauthorized", "cesium_ion_token_verification_failed"].includes(result?.error) ? result.error : "map_gateway_settings_unavailable"; throw applicationError(safeError, status); } return result; } const serverSockets = new Set(); const server = createServer(async (request, response) => { try { const url = new URL(request.url || "/", `http://${request.headers.host || "127.0.0.1"}`); if (await foundryAgentGateway.handlePublicRequest(request, response, url)) return; if (await foundryAgentGateway.handleOntologyMcp(request, response, url)) return; if (url.pathname === "/auth/nodedc/handoff" && request.method === "GET") { await foundryAuth.handleLauncherHandoff(request, response, url); return; } if (url.pathname === "/auth/logout" && request.method === "GET") { foundryAuth.handleLogout(request, response); return; } if (url.pathname === "/healthz" && request.method === "GET") { const config = foundryMcpConfig(); json(response, 200, { status: "ok", service: "nodedc-module-foundry", mcp: { configured: Boolean(config.capabilitySecret && config.mcpUrl), entitlementConfigured: Boolean(config.internalAccessToken), }, codexAgent: foundryAgentGateway.health(), dataProductBindingApi: { configured: Boolean(foundryBindingGrantsDir && externalDataPlaneInternalUrl && externalDataPlaneReaderGrantsDir), auth: "scoped-workload-grant", }, dataProductConsumerProvisioner: { configured: foundryReaderGrantProvisioner.configured, auth: "dedicated-ed25519-service-identity", sourceScope: "external-data-plane-resolved", }, }); return; } if (url.pathname === "/api/mcp") { await handleFoundryMcpRequest(request, response, { getConfig: foundryMcpConfig, operations: foundryMcpOperations, authenticateAgentToken: foundryAgentGateway.authenticateFoundryToken, }); return; } if (url.pathname === "/api/ai-workspace/entitlements") { await handleFoundryEntitlementRequest(request, response, { getConfig: foundryMcpConfig }); return; } if (url.pathname === FOUNDRY_BINDING_CATALOG_PATH || url.pathname === FOUNDRY_BINDING_UPSERT_PATH) { await handleFoundryBindingApiRequest(request, response, { grantsDir: foundryBindingGrantsDir, production: process.env.NODE_ENV === "production", preflightReaderGrant: preflightFoundryBindingReaderGrant, upsertBinding: (input, actor) => foundryMcpOperations.upsertMapDataProductBinding(input, actor), }); return; } if (await foundryAuth.ensureRequestSession(request, response, url)) { return; } const foundryUserProfile = foundryAuth.currentUserProfile(request) || (!foundryAuth.authRequired ? { id: "local_foundry_user", email: "local-foundry@nodedc.local", displayName: "Local Foundry User", avatarUrl: null, initials: "LF", } : null); if (await foundryAgentGateway.handleManagementRequest(request, response, url, foundryUserProfile)) return; if (url.pathname === "/api/session/profile" && request.method === "GET") { const access = foundryAuth.currentUserAccess(request); json(response, 200, { user: foundryAuth.currentUserProfile(request), profileUrl: foundryAuth.profileUrl, access: { role: access.role } }); return; } if (url.pathname === "/api/platform-settings/cesium-ion" && request.method === "GET") { const { actorId } = requireFoundryAdmin(request); const settings = await requestMapGatewayAdmin({ method: "GET", pathname: "/api/map/admin/cesium-ion", actorId, ionReferer: foundryRequestIonReferer(request), }); json(response, 200, { configured: settings.configured === true, updatedAt: settings.updatedAt || null, updatedBy: settings.updatedBy || null, verification: settings.verification || "not-configured" }); return; } if (url.pathname === "/api/platform-settings/cesium-ion" && request.method === "PUT") { const { actorId } = requireFoundryAdmin(request); const input = await readJsonBody(request, 8 * 1024); const token = typeof input?.token === "string" ? input.token.trim() : ""; if (token.length < 16 || token.length > 4096 || /[\u0000-\u001f\u007f\s]/.test(token)) { throw applicationError("invalid_cesium_ion_token"); } const settings = await requestMapGatewayAdmin({ method: "PUT", pathname: "/api/map/admin/cesium-ion", actorId, payload: { token }, ionReferer: foundryRequestIonReferer(request), }); json(response, 200, { configured: settings.configured === true, updatedAt: settings.updatedAt || null, updatedBy: settings.updatedBy || null, verification: settings.verification || "not-configured" }); return; } const mapGatewayMatch = url.pathname.match(/^\/api\/map-gateway(\/.*)$/); if (mapGatewayMatch && ["GET", "HEAD"].includes(request.method || "")) { await proxyMapGateway(request, response, mapGatewayMatch[1]); return; } const runtimeDataProductMatch = url.pathname.match(/^\/api\/applications\/([0-9a-f-]{36})\/pages\/([a-z0-9-]+)\/data-bindings\/([A-Za-z0-9._:-]{1,160})\/(snapshot|history|stream)$/i); if (runtimeDataProductMatch && request.method === "GET") { const [, applicationId, pageId, bindingId, resource] = runtimeDataProductMatch; const target = await resolveRuntimeMapDataProductBinding(applicationId, pageId, bindingId); if (resource === "snapshot") await proxyRuntimeDataProductSnapshot(response, target); else if (resource === "history") await proxyRuntimeDataProductHistory(response, url, target); else await proxyRuntimeDataProductStream(request, response, url, target); return; } if (url.pathname === "/api/layout" && request.method === "GET") { try { json(response, 200, JSON.parse(await readFile(layoutPath, "utf8"))); } catch (error) { if (error?.code === "ENOENT") return json(response, 200, null); throw error; } return; } if (url.pathname === "/api/applications" && request.method === "GET") { const manifests = []; for (const entry of await readdir(applicationsDir, { withFileTypes: true })) { if (!entry.isFile() || !entry.name.endsWith(".json")) continue; try { const manifest = validateApplicationManifest(JSON.parse(await readFile(join(applicationsDir, entry.name), "utf8"))); manifests.push(applicationSummary(manifest)); } catch { // A damaged draft must not make the complete Foundry project list unavailable. } } manifests.sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt))); json(response, 200, manifests); return; } if (url.pathname === "/api/page-templates" && request.method === "GET") { json(response, 200, pageRegistry); return; } const pageLayoutMatch = url.pathname.match(/^\/api\/page-layouts\/([a-z0-9-]+)$/i); if (pageLayoutMatch && request.method === "GET") { try { json(response, 200, JSON.parse(await readFile(pageLayoutPath(pageLayoutMatch[1]), "utf8"))); } catch (error) { if (error?.code === "ENOENT") return json(response, 200, null); throw error; } return; } if (pageLayoutMatch && request.method === "PUT") { throw applicationError("page_library_read_only", 405); } if (url.pathname === "/api/map/runtime-config" && request.method === "GET") { // Foundry has no provider credentials. The private Platform Gateway is // mandatory for live Cesium resources in every environment. const gatewayReady = Boolean(mapGatewayInternalUrl); const gaussianAssetId = String(process.env.CESIUM_GAUSSIAN_SPLAT_ASSET_ID || "").trim(); json(response, 200, { cesiumVersion: "1.143.0", provider: gatewayReady ? "nodedc-map-gateway" : "gateway-required", ionReady: gatewayReady, gatewayReady, osmBuildingsReady: gatewayReady, gaussianSplatsReady: gatewayReady && Boolean(gaussianAssetId), gaussianAssetId: gaussianAssetId || null, assetEndpointBase: "/api/map-gateway/api/map/ion/assets", resourceProxyBase: gatewayReady ? "/api/map-gateway/api/map/cache?url=" : null, gatewayHealthUrl: gatewayReady ? "/api/map-gateway/healthz" : null, cache: { mode: gatewayReady ? "gateway" : "external-required", persistent: gatewayReady }, }); return; } if (url.pathname === "/api/design-profiles" && request.method === "GET") { const profiles = []; for (const entry of await readdir(designProfilesDir, { withFileTypes: true })) { if (!entry.isFile() || !entry.name.endsWith(".json")) continue; try { const profile = validateDesignProfile(JSON.parse(await readFile(join(designProfilesDir, entry.name), "utf8"))); profiles.push(designProfileSummary(profile, await readDesignProfileReleases(profile.id))); } catch { /* skip damaged draft */ } } profiles.sort((left, right) => left.name.localeCompare(right.name)); json(response, 200, profiles); return; } if (url.pathname === "/api/design-profiles" && request.method === "POST") { const input = await readJsonBody(request); const now = new Date().toISOString(); const profile = validateDesignProfile({ schemaVersion: "0.1.0", id: randomUUID(), name: input?.name, version: "0.1.0", status: "draft", layout: input?.layout, timestamps: { createdAt: now, updatedAt: now }, }); await writeDesignProfile(profile); json(response, 201, profile); return; } const designProfileMatch = url.pathname.match(/^\/api\/design-profiles\/(default|[0-9a-f-]{36})$/i); if (designProfileMatch && request.method === "GET") { try { json(response, 200, validateDesignProfile(JSON.parse(await readFile(designProfilePath(designProfileMatch[1]), "utf8")))); } catch (error) { if (error?.code === "ENOENT") return json(response, 404, { error: "design_profile_not_found" }); throw error; } return; } if (designProfileMatch && request.method === "PUT") { let current; try { current = validateDesignProfile(JSON.parse(await readFile(designProfilePath(designProfileMatch[1]), "utf8"))); } catch (error) { if (error?.code === "ENOENT") return json(response, 404, { error: "design_profile_not_found" }); throw error; } const input = await readJsonBody(request); const next = validateDesignProfile({ ...current, name: input?.name || current.name, version: nextPatchVersion(current.version), layout: input?.layout, timestamps: { createdAt: current.timestamps.createdAt, updatedAt: new Date().toISOString() }, }); await writeDesignProfile(next); json(response, 200, next); return; } const designProfileVersionsMatch = url.pathname.match(/^\/api\/design-profiles\/(default|[0-9a-f-]{36})\/versions$/i); if (designProfileVersionsMatch && request.method === "GET") { let current; try { current = validateDesignProfile(JSON.parse(await readFile(designProfilePath(designProfileVersionsMatch[1]), "utf8"))); } catch (error) { if (error?.code === "ENOENT") return json(response, 404, { error: "design_profile_not_found" }); throw error; } const releases = await readDesignProfileReleases(current.id); json(response, 200, { id: current.id, draft: designProfileVersionSummary(current), published: releases.map(designProfileVersionSummary) }); return; } const designProfileVersionMatch = url.pathname.match(/^\/api\/design-profiles\/(default|[0-9a-f-]{36})\/versions\/(\d+\.\d+\.\d+)$/i); if (designProfileVersionMatch && request.method === "GET") { try { json(response, 200, validateDesignProfile(JSON.parse(await readFile(designProfileReleasePath(designProfileVersionMatch[1], designProfileVersionMatch[2]), "utf8")))); } catch (error) { if (error?.code === "ENOENT") return json(response, 404, { error: "design_profile_version_not_found" }); throw error; } return; } const designProfilePublishMatch = url.pathname.match(/^\/api\/design-profiles\/(default|[0-9a-f-]{36})\/publish$/i); if (designProfilePublishMatch && request.method === "POST") { let current; try { current = validateDesignProfile(JSON.parse(await readFile(designProfilePath(designProfilePublishMatch[1]), "utf8"))); } catch (error) { if (error?.code === "ENOENT") return json(response, 404, { error: "design_profile_not_found" }); throw error; } const release = await writeDesignProfileRelease(current); json(response, 201, release); return; } if (url.pathname === "/api/applications" && request.method === "POST") { const input = await readJsonBody(request); const manifest = validateApplicationManifest(createApplicationManifest(input)); await assertDesignProfileReference(manifest.designProfile); await writeApplicationManifest(manifest); json(response, 201, manifest); return; } const applicationMatch = url.pathname.match(/^\/api\/applications\/([0-9a-f-]{36})$/i); if (applicationMatch && request.method === "GET") { try { const manifest = validateApplicationManifest(JSON.parse(await readFile(applicationPath(applicationMatch[1]), "utf8"))); json(response, 200, manifest); } catch (error) { if (error?.code === "ENOENT") return json(response, 404, { error: "application_not_found" }); throw error; } return; } if (applicationMatch && request.method === "PUT") { const currentPath = applicationPath(applicationMatch[1]); let current; try { current = validateApplicationManifest(JSON.parse(await readFile(currentPath, "utf8"))); } catch (error) { if (error?.code === "ENOENT") return json(response, 404, { error: "application_not_found" }); throw error; } const input = await readJsonBody(request); if (String(input?.id || "") !== applicationMatch[1]) throw applicationError("application_id_mismatch"); const next = validateApplicationManifest({ ...input, timestamps: { createdAt: current.timestamps.createdAt, updatedAt: new Date().toISOString(), }, }); await assertDesignProfileReference(next.designProfile); await writeApplicationManifest(next); json(response, 200, next); return; } if (applicationMatch && request.method === "DELETE") { const currentPath = applicationPath(applicationMatch[1]); try { await stat(currentPath); } catch (error) { if (error?.code === "ENOENT") return json(response, 404, { error: "application_not_found" }); throw error; } const deletedPath = join(runtimeDir, "deleted-applications"); await mkdir(deletedPath, { recursive: true }); await rename(currentPath, join(deletedPath, `${applicationMatch[1]}-${Date.now()}.json`)); json(response, 200, { deleted: true, id: applicationMatch[1] }); return; } if (url.pathname === "/api/layout" && request.method === "PUT") { const layout = await readJsonBody(request); const next = { ...layout, savedAt: new Date().toISOString(), schemaVersion: 1 }; const tempPath = `${layoutPath}.tmp`; await writeFile(tempPath, `${JSON.stringify(next, null, 2)}\n`, "utf8"); await rename(tempPath, layoutPath); json(response, 200, next); return; } if (url.pathname === "/api/layout/media" && request.method === "PUT") { const originalName = decodeURIComponent(String(request.headers["x-file-name"] || "stage-video.mp4")); const fileName = safeUploadName(originalName, String(request.headers["content-type"] || "")); const targetPath = join(uploadDir, fileName); await pipeline(request, createWriteStream(targetPath, { flags: "wx" })); json(response, 201, { fileName: originalName, url: `/uploads/${fileName}` }); return; } if (url.pathname.startsWith("/uploads/")) { const fileName = basename(normalize(url.pathname.slice("/uploads/".length))); await serveFile(request, response, join(uploadDir, fileName)); return; } const relativePath = url.pathname === "/" ? "index.html" : url.pathname.replace(/^\//, ""); const candidate = join(distDir, normalize(relativePath)); try { await serveFile(request, response, candidate); } catch (error) { if (error?.code !== "ENOENT") throw error; await serveFile(request, response, join(distDir, "index.html")); } } catch (error) { const statusCode = error?.statusCode || (error?.message === "payload_too_large" ? 413 : 500); json(response, statusCode, { error: error?.message || "server_error" }); } }); server.on("connection", (socket) => { serverSockets.add(socket); socket.once("close", () => serverSockets.delete(socket)); }); let shutdownPromise = null; function shutdownServer(signal) { if (shutdownPromise) return shutdownPromise; shutdownPromise = (async () => { for (const activeStream of activeRuntimeStreams) { activeStream.controller.abort(); activeStream.response.destroy(); } await dataProductConsumerManager.shutdown(); await new Promise((resolveShutdown) => { const timeout = setTimeout(() => { for (const socket of serverSockets) socket.destroy(); }, 5_000); server.close((error) => { clearTimeout(timeout); if (error) { process.exitCode = 1; console.error(`NDC Module Foundry shutdown error (${signal}): ${error.message}`); } resolveShutdown(); }); }); })(); return shutdownPromise; } async function shutdownAndExit(signal) { await shutdownServer(signal); // A canceled upstream fetch may leave an idle keep-alive socket owned by // Node's global HTTP dispatcher. All Foundry state and inbound sockets are // already closed at this point, so finish the container stop deterministically. process.exit(process.exitCode || 0); } process.once("SIGTERM", () => { void shutdownAndExit("SIGTERM"); }); process.once("SIGINT", () => { void shutdownAndExit("SIGINT"); }); server.listen(port, host, () => { console.log(`NDC Module Foundry: http://${host}:${port}`); console.log(`Layout store: ${layoutPath}`); console.log(`Application drafts: ${applicationsDir}`); console.log(`Foundry MCP: /api/mcp`); console.log(`AI Workspace entitlement adapter: /api/ai-workspace/entitlements`); });