NODEDC_DESIGN_GUIDELINE/server/catalog-server.mjs

2937 lines
130 KiB
JavaScript

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);
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 (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 } : {}),
};
}
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 aspectIds = [primary, ...dataProductBindings.filter((binding) => binding.joinToBindingId === primary.id)]
.map((binding) => binding.aspectId);
if (new Set(aspectIds).size !== aspectIds.length) throw applicationError("duplicate_map_data_product_aspect_id");
}
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`);
});