import { createReadStream, createWriteStream } from "node:fs"; import { mkdir, readFile, readdir, rename, stat, writeFile } from "node:fs/promises"; import { createServer } from "node:http"; import { randomUUID } from "node:crypto"; import { basename, extname, join, normalize } from "node:path"; import { pipeline } from "node:stream/promises"; import { fileURLToPath } from "node:url"; const root = fileURLToPath(new URL("..", import.meta.url)); const distDir = join(root, "apps", "catalog", "dist"); const runtimeDir = 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 layoutPath = join(runtimeDir, "layout.json"); const pageRegistryPath = join(root, "registry", "pages.json"); const port = Number(process.env.PORT || 3333); await mkdir(uploadDir, { recursive: true }); await mkdir(applicationsDir, { recursive: true }); await mkdir(designProfilesDir, { recursive: true }); await mkdir(designProfileReleasesDir, { recursive: true }); const pageRegistry = JSON.parse(await readFile(pageRegistryPath, "utf8")); 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) { 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`); } 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 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"), }, }; } 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) }; } async function ensureDefaultDesignProfile() { try { await readFile(designProfilePath("default"), "utf8"); } catch (error) { if (error?.code !== "ENOENT") throw error; let layout = {}; 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(); 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"); 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"); } } return { ...value, 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 || "Первый Application Draft NDC Module Studio."), }, 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); const tempPath = `${targetPath}.${randomUUID()}.tmp`; await writeFile(tempPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8"); await rename(tempPath, targetPath); } 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; } 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 server = createServer(async (request, response) => { try { const url = new URL(request.url || "/", `http://${request.headers.host || "127.0.0.1"}`); 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 Studio 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; } 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.listen(port, "127.0.0.1", () => { console.log(`NODE.DC Design Guideline: http://127.0.0.1:${port}`); console.log(`Layout store: ${layoutPath}`); console.log(`Application drafts: ${applicationsDir}`); });