Odin/src/hermodr-daemon.cjs

1391 lines
48 KiB
JavaScript

#!/usr/bin/env node
"use strict";
const fs = require("fs");
const http = require("http");
const { createRequire } = require("module");
const path = require("path");
const { defineOdinDocuments } = require("./odin/documents.cjs");
const { parseArgs } = require("./odin/utils.cjs");
const repoRoot = path.resolve(__dirname, "..");
const projectsRoot = path.resolve(repoRoot, "..");
const defaultEveRepoRoot = path.resolve(projectsRoot, "Eve");
const defaultEveWebRoot = path.resolve(defaultEveRepoRoot, "web");
const defaultAetheriaResourcesRoot = path.resolve(projectsRoot, "Aetheria", "Assets", "Resources");
const defaultOdinCultMeshUri = "cultmesh://odin/rendezvous/provider-catalog";
const defaultOdinRudpEndpoint = "rudp://127.0.0.1:17871";
const odinRudpConnectionId = 0x0d1d0002;
const providerRudpConnectionId = 0x43554c54;
const providerRudpPeers = new Map();
const defaultProviderCatalogKeys = [
"aetheria.main_menu.root",
"aetheria.inventory.panel",
"aetheria.inventory.panel.dropdown",
"aetheria.map.zone_details",
"aetheria.trade.menu",
];
const cultCacheRequire = createRequire(resolveCultCachePackagePath());
const cultMeshRequire = createRequire(resolveCultMeshPackagePath());
const { defineDocumentType } = cultCacheRequire(resolveCultCacheRuntimePath());
const { CultMesh } = cultMeshRequire(resolveCultMeshRuntimePath());
const { decode: decodeMessagePack } = cultMeshRequire("@msgpack/msgpack");
const documents = defineOdinDocuments(defineDocumentType);
const odinDocumentDefinitions = Object.values(documents).filter(Boolean);
const HermodrCommandDefinition = defineDocumentType({
type: "gamecult.eve.command",
schemaName: "gamecult.eve.command",
schemaId: "gamecult.eve.command.v1",
schemaVersion: "gamecult.eve.command.v1",
global: false,
name: (value) => value?.commandId || value?.command_id || "hermodr-command",
schema: requireObject("Hermodr command document"),
});
async function main() {
const options = parseOptions(process.argv.slice(2));
rejectRemovedOptions(options);
if (options.help) {
printUsage();
return;
}
const bridge = createHermodrBridge(options);
const server = http.createServer((request, response) => {
bridge.handle(request, response).catch((error) => {
writeJson(response, error?.statusCode || 500, {
ok: false,
error: error instanceof Error ? error.message : String(error),
});
});
});
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(options.port, options.host, () => {
server.off("error", reject);
resolve();
});
});
const address = server.address();
const port = typeof address === "object" && address ? address.port : options.port;
console.log(`Hermodr browser lowering listening on http://${options.host}:${port}`);
console.log(`Hermodr Odin CultMesh URI: ${options.odinCultMeshUri}`);
const shutdown = () => {
server.close(() => {});
closeProviderRudpPeers();
};
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
}
function parseOptions(argv) {
const parsed = parseArgs(argv);
return {
help: Boolean(parsed.help),
host: stringOption(parsed.host, process.env.HERMODR_BIND_HOST || "127.0.0.1"),
port: numberOption(parsed.port, process.env.HERMODR_PORT || 8798),
odinCultMeshUri: stringOption(
parsed.odinCultMeshUri || parsed.odinUri || parsed["odin-cultmesh-uri"],
process.env.HERMODR_ODIN_CULTMESH_URI || process.env.ODIN_CULTMESH_URI || defaultOdinCultMeshUri,
),
odinRudpEndpoint: stringOption(
parsed.odinRudpEndpoint || parsed["odin-rudp-endpoint"],
process.env.HERMODR_ODIN_RUDP || process.env.CULTMESH_URI_ODIN_RUDP || defaultOdinRudpEndpoint,
),
odinSurfaceKey: stringOption(parsed.odinSurfaceKey, process.env.HERMODR_ODIN_SURFACE_KEY || "gamecult.network.status"),
eveWebRoot: path.resolve(stringOption(parsed.eveWebRoot, process.env.HERMODR_EVE_WEB_ROOT || defaultEveWebRoot)),
eveRepoRoot: path.resolve(stringOption(parsed.eveRepoRoot, process.env.HERMODR_EVE_REPO_ROOT || defaultEveRepoRoot)),
aetheriaResourcesRoot: path.resolve(stringOption(
parsed.aetheriaResourcesRoot,
process.env.HERMODR_AETHERIA_RESOURCES_ROOT || defaultAetheriaResourcesRoot,
)),
providerCatalogKeys: stringListOption(
parsed.providerCatalogKeys || parsed["provider-catalog-keys"],
process.env.HERMODR_PROVIDER_CATALOG_KEYS,
defaultProviderCatalogKeys,
),
enableLocalAetheriaAssetFallback: boolOption(parsed.enableLocalAetheriaAssetFallback, process.env.HERMODR_ENABLE_LOCAL_AETHERIA_ASSET_FALLBACK),
sleipnirProviderId: stringOption(parsed.sleipnirProviderId, process.env.HERMODR_SLEIPNIR_PROVIDER_ID || "sleipnir.input-mirror.starfire"),
raw: parsed,
};
}
function rejectRemovedOptions(options) {
const removed = [
"odinHttpBase",
"odinWsBase",
"odinStatePath",
"sleipnirMappingPath",
"sleipnirStorePath",
"sleipnirCommandEndpoint",
"sleipnirCommandRudp",
"muninnRudp",
"aetheriaCommandRudp",
"hermodrCommandStorePath",
"odinCachePath",
];
for (const option of removed) {
if (Object.hasOwn(options.raw, option)) {
throw new Error(`--${option} was removed. Hermodr is a browser lowering over Odin/CultMesh state, not a fallback discovery or raw command transport.`);
}
}
const removedEnv = [
"HERMODR_ODIN_HTTP_BASE",
"HERMODR_ODIN_WS_BASE",
"HERMODR_ODIN_STATE_PATH",
"HERMODR_SLEIPNIR_MAPPING_PATH",
"HERMODR_SLEIPNIR_STORE_PATH",
"HERMODR_SLEIPNIR_COMMAND_ENDPOINT",
"HERMODR_SLEIPNIR_COMMAND_RUDP",
"HERMODR_MUNINN_RUDP",
"HERMODR_AETHERIA_COMMAND_RUDP",
"HERMODR_ODIN_CULTMESH_STORE",
"ODIN_CULTMESH_STORE",
];
for (const name of removedEnv) {
if (process.env[name]) {
throw new Error(`${name} was removed. Hermodr must lower Odin/CultMesh state and publish typed CultMesh command documents.`);
}
}
}
function createHermodrBridge(options) {
return {
async handle(request, response) {
const url = new URL(request.url || "/", `http://${request.headers.host || "localhost"}`);
if (request.method === "OPTIONS") {
writeCors(response, 204);
response.end();
return;
}
if (request.method === "GET" && url.pathname === "/health") {
writeJson(response, 200, {
ok: true,
service: "hermodr-browser-lowering",
authority: "lowering-only",
visibleAuthority: "Odin sees provider-owned CultMesh documents; Odin does not own provider daemons.",
odinCultMeshUri: options.odinCultMeshUri,
eveWebRoot: options.eveWebRoot,
assetBytes: "provider-cultmesh-cdn",
localAetheriaAssetFallback: options.enableLocalAetheriaAssetFallback,
});
return;
}
if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/index.html")) {
await serveStaticFile(response, path.join(options.eveWebRoot, "index.html"), "text/html; charset=utf-8");
return;
}
if (request.method === "GET" && url.pathname === "/cultmesh-cdn/catalog") {
writeJson(response, 200, await readAssetCatalog(options));
return;
}
if ((request.method === "GET" || request.method === "HEAD") && url.pathname.startsWith("/cultmesh-cdn/")) {
await serveCultMeshAsset(
options,
response,
decodeURIComponent(url.pathname.slice("/cultmesh-cdn/".length)),
request.method === "HEAD",
);
return;
}
if (request.method === "GET" && url.pathname === "/hermodr/verse/catalog") {
writeJson(response, 200, await readCatalog(options));
return;
}
if (request.method === "GET" && url.pathname.startsWith("/hermodr/surface/")) {
const providerId = decodeURIComponent(url.pathname.slice("/hermodr/surface/".length));
const catalog = await readCatalog(options);
const surface = await readProviderSurface(catalog, providerId);
if (!surface) {
writeJson(response, 404, { ok: false, error: `No surface found for provider '${providerId}'.` });
return;
}
writeJson(response, 200, surface);
return;
}
if (request.method === "GET" && url.pathname.startsWith("/hermodr/document/")) {
const providerId = decodeURIComponent(url.pathname.slice("/hermodr/document/".length));
const documentId = stringOption(url.searchParams.get("documentId"), "");
const schemaId = stringOption(url.searchParams.get("schemaId"), "");
const resolved = await readVisibleDocument(options, { providerId, documentId, schemaId });
if (!resolved) {
writeJson(response, 404, { ok: false, error: `No visible document '${documentId}' (${schemaId || "any schema"}) for provider '${providerId}'.` });
return;
}
writeJson(response, 200, resolved);
return;
}
if (request.method === "POST" && url.pathname === "/hermodr/commands/sleipnir/input-mapping") {
const body = await readJsonBody(request);
writeJson(response, 202, await publishSleipnirMapping(options, body));
return;
}
if (request.method === "POST" && url.pathname === "/hermodr/commands/eve") {
const body = await readJsonBody(request);
writeJson(response, 202, await publishEveCommand(options, body));
return;
}
if (request.method === "GET" && url.pathname.startsWith("/packages/")) {
await serveStaticFile(response, safeJoin(path.join(options.eveRepoRoot, "packages"), url.pathname.slice("/packages/".length)));
return;
}
if (request.method === "GET") {
await serveStaticFile(response, safeJoin(options.eveWebRoot, url.pathname.slice(1)));
return;
}
writeJson(response, 404, { ok: false, error: "not found" });
},
};
}
async function readCatalog(options) {
const providerAdvertisements = await readOdinProviderAdvertisements(options);
const surfaces = providerAdvertisements.map((provider) => ({
providerId: provider.id,
title: provider.title || provider.id,
version: 0,
updatedAt: provider.updatedAt || null,
surface: { advertised: true },
}));
return {
schema: "gamecult.hermodr.browser_lowering_catalog.v1",
service: "hermodr-browser-lowering",
generatedAt: new Date().toISOString(),
authority: {
owner: "Odin/CultMesh",
ownerNote: "Odin sees provider-owned daemon publications; it does not own provider daemon state.",
hermodrRole: "browser lowering adapter",
forbidden: ["provider catalog synthesis", "provider state ownership", "renderer-owned discovery"],
},
source: {
odinCultMeshUri: options.odinCultMeshUri,
odinSurfaceKey: options.odinSurfaceKey,
},
surface: null,
surfaces,
providers: providerAdvertisements,
};
}
async function readOdinProviderAdvertisements(options) {
const peer = await CultMesh.createRudpPeer(
"hermodr-browser-lowering-odin",
odinRudpConnectionId,
options.odinCultMeshUri,
{
connectTimeoutMs: 2_000,
maxFragmentBytes: 1200,
maxPendingReliablePackets: 512,
resolveCultMeshRudpEndpoint: (uri) => resolveHermodrRudpEndpoint(uri, options),
},
);
try {
const response = await requestCultNetRawSnapshot(
peer,
options.providerCatalogKeys,
{
schemaIds: ["gamecult.eve.provider_advertisement.v1"],
timeoutMs: 4_000,
messageIdPrefix: "hermodr-odin-providers",
},
);
return response.documents
.filter((document) => document.schemaId === "gamecult.eve.provider_advertisement.v1")
.map((document) => normalizeProviderAdvertisement(
decodeMessagePack(bufferFromPayload(document.payload)),
document.recordKey,
))
.filter(Boolean);
} finally {
if (typeof peer.close === "function") {
peer.close();
}
}
}
function resolveHermodrRudpEndpoint(uri, options) {
const text = String(uri || "").trim();
if (text === defaultOdinCultMeshUri || text.startsWith("cultmesh://odin/")) {
return options.odinRudpEndpoint;
}
return undefined;
}
async function readProviderSurface(catalog, providerId) {
const route = findProviderSnapshotRoute(catalog, providerId);
if (!route) return null;
const peer = await getProviderRudpPeer(route.endpoint);
const document = await requestCultNetRawSnapshotFirstDocument(
peer,
"gamecult.eve.surface.v1",
[providerId, `eve:surface:${providerId}`],
{ timeoutMs: 4_000, messageIdPrefix: "hermodr-surface" },
);
return normalizeEveSurfaceDocument(decodeMessagePack(bufferFromPayload(document.payload)), providerId);
}
function normalizeEveSurfaceDocument(value, fallbackProviderId) {
if (Array.isArray(value)) {
if (Array.isArray(value[5])) {
return {
providerId: String(fallbackProviderId || value[0] || ""),
documentProviderId: String(value[0] || ""),
providerKind: String(value[1] || ""),
title: String(value[2] || value[0] || fallbackProviderId || ""),
version: value[3] ?? 0,
updatedAt: value[4] || null,
surface: normalizeEveSurfaceTree(value[5]),
commands: Array.isArray(value[6]) ? normalizeEveCommands(value[6]) : [],
};
}
return {
providerId: String(value[2] || fallbackProviderId || ""),
providerKind: String(value[3] || ""),
title: String(value[4] || value[2] || fallbackProviderId || ""),
version: value[5] ?? 0,
updatedAt: value[6] || null,
surface: normalizeEveSurfaceTree(value[7]),
commands: Array.isArray(value[8]) ? normalizeEveCommands(value[8]) : [],
};
}
return {
providerId: String(value?.providerId || fallbackProviderId || ""),
providerKind: String(value?.providerKind || ""),
title: String(value?.title || value?.surface?.title || value?.providerId || fallbackProviderId || ""),
version: value?.version ?? 0,
updatedAt: value?.updatedAtUtc || value?.updatedAt || null,
surface: normalizeEveSurfaceTree(value?.surface),
commands: Array.isArray(value?.commands) ? normalizeEveCommands(value.commands) : [],
};
}
function normalizeEveSurfaceTree(value) {
if (Array.isArray(value)) {
return {
id: String(value[0] || ""),
root: normalizeEveSurfaceComponent(value[1]),
styles: Array.isArray(value[2]) ? value[2] : [],
};
}
return {
id: String(value?.id || ""),
title: value?.title,
root: normalizeEveSurfaceComponent(value?.root),
styles: Array.isArray(value?.styles) ? value.styles : [],
};
}
function normalizeEveSurfaceComponent(value) {
if (Array.isArray(value)) {
const component = {
id: String(value[0] || ""),
kind: String(value[1] || ""),
props: value[2] && typeof value[2] === "object" && !Array.isArray(value[2]) ? value[2] : {},
children: Array.isArray(value[3]) ? value[3].map(normalizeEveSurfaceComponent) : [],
};
if (Array.isArray(value[5])) component.embeddedDocuments = normalizeEmbeddedDocuments(value[5]);
if (value[6] && typeof value[6] === "object" && !Array.isArray(value[6])) component.layout = value[6];
if (value[7] && typeof value[7] === "object" && !Array.isArray(value[7])) component.style = value[7];
return component;
}
if (!value || typeof value !== "object") {
return { id: "", kind: "surface", props: {}, children: [] };
}
return {
...value,
props: value.props && typeof value.props === "object" && !Array.isArray(value.props) ? value.props : {},
children: Array.isArray(value.children) ? value.children.map(normalizeEveSurfaceComponent) : [],
};
}
function normalizeEveCommands(commands) {
return commands.map((command) => {
if (!Array.isArray(command)) return command;
return {
commandId: String(command[0] || ""),
label: String(command[1] || ""),
description: String(command[2] || ""),
authority: String(command[3] || ""),
transport: String(command[4] || ""),
};
});
}
function normalizeEmbeddedDocuments(documents) {
return documents.map((document) => {
if (!Array.isArray(document)) return document;
return {
slotId: String(document[0] || ""),
documentId: String(document[1] || ""),
schemaId: String(document[2] || ""),
presentationKind: String(document[3] || ""),
bindingMode: String(document[4] || ""),
query: String(document[5] || ""),
};
});
}
async function publishSleipnirMapping(options, body) {
if (!body || typeof body !== "object" || Array.isArray(body)) {
throw new Error("Sleipnir input mapping command body must be an object.");
}
const catalog = await readCatalog(options);
const route = findSleipnirCommandRoute(catalog, options.sleipnirProviderId);
if (!route) {
const error = new Error(`Odin does not advertise a cultmesh:// Sleipnir input mapping command route for ${options.sleipnirProviderId}.`);
error.statusCode = 409;
throw error;
}
const commandId = body.commandId || body.command_id || `hermodr:sleipnir:${Date.now()}:${Math.random().toString(16).slice(2)}`;
const mapping = {
...body,
schema: "sleipnir.input_mapping.v1",
providerId: options.sleipnirProviderId,
commandId,
commandRoute: route.uri,
commandRouteResolver: "odin-cultmesh",
publishedBy: "hermodr-browser-lowering",
publishedAt: new Date().toISOString(),
};
delete mapping.muninnRudp;
delete mapping.muninn_rudp;
const publication = await publishCommandDocument(options, documents.SleipnirInputMappingDefinition, options.sleipnirProviderId, mapping, route.uri);
if (publication.routeError) {
const error = new Error(`CultMesh route publish failed for ${route.uri}: ${publication.routeError}`);
error.statusCode = 502;
throw error;
}
return {
ok: true,
schema: "sleipnir.input_mapping.v1",
commandId,
providerId: options.sleipnirProviderId,
commandRoute: route.uri,
route: route.uri,
routeError: publication.routeError || undefined,
};
}
async function publishEveCommand(options, body) {
if (!body || typeof body !== "object" || Array.isArray(body)) {
throw new Error("Eve command body must be an object.");
}
const target = typeof body.target === "string" ? body.target.trim() : "";
const catalog = await readCatalog(options);
const route = target.startsWith("cultmesh://")
? target
: findProviderCommandRoute(catalog, body.providerId || body.provider_id || "");
if (!route) {
const error = new Error("No provider-advertised cultmesh:// command route was visible.");
error.statusCode = 409;
throw error;
}
const commandId = body.commandId || body.command_id || `hermodr:eve:${Date.now()}:${Math.random().toString(16).slice(2)}`;
const command = {
...body,
schema: "gamecult.eve.command.v1",
commandId,
target: route || target || null,
publishedBy: "hermodr-browser-lowering",
publishedAt: new Date().toISOString(),
};
const publication = await publishCommandDocument(options, HermodrCommandDefinition, commandId, command, route);
if (publication.routeError) {
const error = new Error(`CultMesh route publish failed for ${route}: ${publication.routeError}`);
error.statusCode = 502;
throw error;
}
return {
ok: true,
schema: "gamecult.eve.command.v1",
commandId,
target: route,
};
}
async function publishCommandDocument(options, definition, key, value, route) {
let routeError = null;
if (route && route.startsWith("cultmesh://")) {
try {
await CultMesh.publishRudpDocumentOnce(
"hermodr-browser-lowering",
0xe1e00001,
route,
{ definition },
key,
value,
{
sourceRuntimeId: "hermodr-browser-lowering",
sourceRole: "browser-lowering",
tags: ["hermodr", "browser", "command"],
},
);
} catch (error) {
routeError = error instanceof Error ? error.message : String(error);
}
}
return { routeError };
}
async function readVisibleDocument(options, request) {
const catalog = await readCatalog(options);
const resolved = await fetchProviderDocument(catalog, request);
if (!resolved) return null;
return {
ok: true,
schema: "gamecult.hermodr.resolved_document.v1",
providerId: request.providerId,
documentId: request.documentId,
schemaId: resolved.schemaId,
recordKey: resolved.recordKey,
document: resolved.document,
source: {
visibility: "provider-cultmesh-rudp-snapshot",
endpoint: resolved.endpoint,
odinCultMeshUri: options.odinCultMeshUri,
},
};
}
function findSleipnirCommandRoute(catalog, providerId) {
const providers = catalog.providers.filter((provider) => provider.id === providerId || String(provider.id).includes("sleipnir"));
for (const provider of providers) {
const endpoints = [...(provider.endpoints || []), ...(provider.routes || [])];
const route = endpoints.find((endpoint) => {
const uri = endpoint.uri || endpoint.endpoint || endpoint.address || "";
const role = [endpoint.id, endpoint.role, ...(endpoint.tags || [])].join(" ").toLowerCase();
return uri.startsWith("cultmesh://") && role.includes("command") && role.includes("sleipnir");
}) || endpoints.find((endpoint) => {
const uri = endpoint.uri || endpoint.endpoint || endpoint.address || "";
return uri.startsWith("cultmesh://") && uri.includes("sleipnir") && uri.includes("input_mapping");
});
if (route) {
return {
...route,
uri: route.uri || route.endpoint || route.address,
};
}
}
return null;
}
function findProviderCommandRoute(catalog, providerId) {
const normalized = String(providerId || "").trim();
const providers = catalog.providers.filter((provider) => {
if (!normalized) return true;
return provider.id === normalized || provider.id.includes(normalized) || normalized.includes(provider.id);
});
for (const provider of providers) {
const endpoints = [...(provider.endpoints || []), ...(provider.routes || [])];
const route = endpoints.find((endpoint) => {
const uri = endpoint.uri || endpoint.endpoint || endpoint.address || "";
const role = [endpoint.id, endpoint.role, ...(endpoint.tags || [])].join(" ").toLowerCase();
return uri.startsWith("cultmesh://") && role.includes("command");
});
if (route) return route.uri || route.endpoint || route.address;
}
return "";
}
function normalizeProviderAdvertisement(value, recordKey) {
const providerId = value?.providerId || value?.provider?.id || value?.id || recordKey;
if (!providerId) {
return null;
}
const endpoints = arrayOfObjects(value?.endpoints);
const routes = arrayOfObjects(value?.routes);
return {
id: String(providerId),
title: value.title || value.provider?.title || String(providerId),
description: value.description || "",
cultMeshAddress: value.cultMeshAddress || value.provider?.endpoint || null,
endpoints,
routes,
commandSurface: value.commandSurface || null,
status: value.status || "unknown",
updatedAt: value.updatedAt || value.generatedAt || null,
};
}
function normalizeSurfaceState(value, recordKey) {
if (Array.isArray(value)) {
value = {
providerId: value[0],
title: value[1],
version: value[2],
updatedAt: value[3],
surface: value[4],
};
}
const providerId = value?.providerId || value?.provider_id || recordKey?.replace(/^surface:/, "");
if (!providerId) {
return null;
}
return {
providerId: String(providerId),
title: value.title || String(providerId),
version: value.version || 0,
updatedAt: value.updatedAt || value.updated_at || null,
surface: value.surface || null,
};
}
function decodeCultMeshDocumentId(documentId) {
const text = String(documentId || "");
if (!text.startsWith("cultmesh://")) return text;
const parsed = new URL(text);
return decodeURIComponent(parsed.pathname.replace(/^\/+/, ""));
}
function arrayOfObjects(value) {
return Array.isArray(value) ? value.filter((item) => item && typeof item === "object" && !Array.isArray(item)) : [];
}
function requireObject(label) {
return {
parse(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`${label} must be an object.`);
}
return value;
},
};
}
function readJsonBody(request) {
return new Promise((resolve, reject) => {
const chunks = [];
request.on("data", (chunk) => chunks.push(chunk));
request.on("error", reject);
request.on("end", () => {
try {
const text = Buffer.concat(chunks).toString("utf8").trim();
resolve(text ? JSON.parse(text) : {});
} catch (error) {
reject(new Error(`Invalid JSON body: ${error.message}`));
}
});
});
}
function writeJson(response, statusCode, body) {
writeCors(response, statusCode);
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.end(`${JSON.stringify(body, null, 2)}\n`);
}
function writeCors(response, statusCode) {
response.statusCode = statusCode;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "content-type");
}
async function serveStaticFile(response, filePath, contentType, headOnly = false) {
if (!filePath || !fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
writeJson(response, 404, { ok: false, error: "not found" });
return;
}
writeCors(response, 200);
response.setHeader("Content-Type", contentType || contentTypeForPath(filePath));
response.setHeader("Content-Length", fs.statSync(filePath).size);
if (headOnly) {
response.end();
return;
}
response.end(await fs.promises.readFile(filePath));
}
function safeJoin(root, relativePath) {
const resolved = path.resolve(root, relativePath || "");
const normalizedRoot = path.resolve(root);
if (resolved !== normalizedRoot && !resolved.startsWith(`${normalizedRoot}${path.sep}`)) {
return "";
}
return resolved;
}
async function readAssetCatalog(options) {
const catalog = await readCatalog(options);
return {
ok: true,
schema: "gamecult.hermodr.asset_catalog.v1",
source: "odin-cultmesh-provider-routes",
providers: catalog.providers,
manifests: [],
};
}
async function serveCultMeshAsset(options, response, uri, headOnly = false) {
const assetUri = normalizeCultMeshAssetUri(uri);
try {
const asset = await fetchCultMeshCdnAsset(options, assetUri);
writeCors(response, 200);
response.setHeader("Content-Type", asset.contentType || contentTypeForPath(assetUri));
response.setHeader("Content-Length", asset.bytes.length);
response.setHeader("X-CultMesh-CDN-Provider", asset.providerId);
response.setHeader("X-CultMesh-CDN-Endpoint", asset.endpoint);
if (headOnly) {
response.end();
return;
}
response.end(asset.bytes);
return;
} catch (error) {
if (!options.enableLocalAetheriaAssetFallback) {
writeJson(response, 502, {
ok: false,
error: `Provider-owned CultMesh CDN fetch failed for ${assetUri}: ${error instanceof Error ? error.message : String(error)}`,
note: "Odin only exposes asset route metadata. Hermodr does not read asset bytes from Odin's cache or from Aetheria's filesystem.",
});
return;
}
}
const filePath = resolveAetheriaResourceAsset(options, assetUri);
if (options.enableLocalAetheriaAssetFallback && filePath) {
await serveStaticFile(response, filePath, undefined, headOnly);
return;
}
writeJson(response, 404, {
ok: false,
error: `No provider-owned CultMesh CDN route could be lowered for ${uri}.`,
note: "Odin only exposes asset route metadata. Hermodr does not read asset bytes from Odin's cache.",
});
}
async function fetchCultMeshCdnAsset(options, assetUri) {
const catalog = await readCatalog(options);
const route = findProviderCdnRoute(catalog);
if (!route) {
throw new Error("No Aetheria provider advertised a cultmesh:// CDN route.");
}
const peer = await getProviderRudpPeer(route.endpoint);
const document = await requestCultNetRawSnapshotDocument(
peer,
"gamecult.cultmesh.cdn.asset_blob.v1",
assetUri,
{ timeoutMs: 4_000, messageIdPrefix: "hermodr-cdn" },
);
return {
providerId: route.providerId,
endpoint: route.endpoint,
contentType: contentTypeFromTags(document.tags) || contentTypeForPath(assetUri),
bytes: bufferFromMessagePackedBytes(document.payload),
};
}
async function fetchProviderDocument(catalog, request) {
const route = findProviderSnapshotRoute(catalog, request.providerId);
if (!route) return null;
const recordKey = decodeCultMeshDocumentId(request.documentId);
const document = await requestProviderSnapshotDocumentWithReconnect(
route.endpoint,
request.schemaId,
recordKey,
{ timeoutMs: 4_000, messageIdPrefix: "hermodr-document" },
);
return {
endpoint: route.endpoint,
schemaId: document.schemaId,
recordKey: document.recordKey,
document: normalizeProviderDocumentPayload(document.schemaId, decodeMessagePack(bufferFromPayload(document.payload))),
};
}
async function requestProviderSnapshotDocumentWithReconnect(endpoint, schemaId, recordKey, options = {}) {
try {
const peer = await getProviderRudpPeer(endpoint);
return await requestCultNetRawSnapshotDocument(peer, schemaId, recordKey, options);
} catch (firstError) {
await dropProviderRudpPeer(endpoint);
const peer = await getProviderRudpPeer(endpoint);
try {
return await requestCultNetRawSnapshotDocument(peer, schemaId, recordKey, options);
} catch (secondError) {
secondError.message = `${secondError.message} Provider route ${endpoint} was retried after dropping a stale Hermodr RUDP peer. First error: ${
firstError instanceof Error ? firstError.message : String(firstError)
}`;
throw secondError;
}
}
}
async function getProviderRudpPeer(endpoint) {
const key = String(endpoint || "").trim();
if (!key) throw new Error("Provider RUDP endpoint is empty.");
const existing = providerRudpPeers.get(key);
if (existing) return existing;
const pending = CultMesh.createRudpPeer(
"hermodr-browser-lowering-provider",
providerRudpConnectionId,
key,
{
connectTimeoutMs: 2_000,
maxFragmentBytes: 1200,
maxPendingReliablePackets: 512,
},
).catch((error) => {
providerRudpPeers.delete(key);
throw error;
});
providerRudpPeers.set(key, pending);
return pending;
}
async function dropProviderRudpPeer(endpoint) {
const key = String(endpoint || "").trim();
const peerOrPromise = providerRudpPeers.get(key);
providerRudpPeers.delete(key);
if (!peerOrPromise) return;
try {
const peer = await peerOrPromise;
if (typeof peer?.close === "function") peer.close();
} catch {
// The peer is already unusable; removing it is the repair.
}
}
function closeProviderRudpPeers() {
for (const peerOrPromise of providerRudpPeers.values()) {
Promise.resolve(peerOrPromise)
.then((peer) => {
if (typeof peer?.close === "function") peer.close();
})
.catch(() => {});
}
providerRudpPeers.clear();
}
function normalizeProviderDocumentPayload(schemaId, value) {
const documentSchema = Array.isArray(value) ? value[0] : value?.schema;
if (
schemaMatches(schemaId, "gamecult.aetheria.render_splats_viewport.v1") ||
schemaMatches(documentSchema, "gamecult.aetheria.render_splats_viewport.v1")
) {
return normalizeRenderSplatsViewport(value);
}
if (
schemaMatches(schemaId, "gamecult.aetheria.gravity_viewport.v1") ||
schemaMatches(documentSchema, "gamecult.aetheria.gravity_viewport.v1")
) {
return normalizeGravityViewport(value);
}
if (
schemaMatches(schemaId, "gamecult.aetheria.objects_viewport.v1") ||
schemaMatches(documentSchema, "gamecult.aetheria.objects_viewport.v1")
) {
return normalizeObjectsViewport(value);
}
return value;
}
function schemaMatches(schemaId, expected) {
return String(schemaId || "") === expected;
}
function normalizeRenderSplatsViewport(value) {
if (!Array.isArray(value)) return value;
const splats = Array.isArray(value[9]) ? value[9] : [];
return {
schema: value[0],
frameId: value[1],
publishedAtUtc: value[2],
simulationTimeSeconds: value[3],
runId: value[4],
zoneIndex: value[5],
zoneName: value[6],
viewport: normalizeViewport(value[7]),
layers: Array.isArray(value[8]) ? value[8].map(normalizeRenderSplatLayer) : [],
splats: {
count: splats[0] ?? 0,
centerX: splats[1] || [],
centerY: splats[2] || [],
halfExtentX: splats[3] || [],
halfExtentY: splats[4] || [],
rotationCos: splats[5] || [],
rotationSin: splats[6] || [],
channel: splats[7] || [],
falloff: splats[8] || [],
valueR: splats[9] || [],
valueG: splats[10] || [],
valueB: splats[11] || [],
valueA: splats[12] || [],
sourceKey: splats[13] || [],
layerIndex: splats[14] || [],
sourceKind: splats[15] || [],
frequencyX: splats[16] || [],
frequencyY: splats[17] || [],
phaseX: splats[18] || [],
phaseY: splats[19] || [],
animationSpeed: splats[20] || [],
sourceFlags: splats[21] || [],
},
};
}
function normalizeViewport(value) {
return Array.isArray(value)
? { minX: value[0], minY: value[1], maxX: value[2], maxY: value[3] }
: value;
}
function normalizeRenderSplatLayer(value) {
if (!Array.isArray(value)) return value;
return {
layerKey: value[0],
displayName: value[1],
channel: value[2],
blendMode: value[3],
graphicsFormat: value[4],
clearBeforeDraw: value[5],
clearR: value[6],
clearG: value[7],
clearB: value[8],
clearA: value[9],
};
}
function normalizeGravityViewport(value) {
if (!Array.isArray(value)) return value;
return {
schema: value[0],
frameId: value[1],
publishedAtUtc: value[2],
simulationTimeSeconds: value[3],
runId: value[4],
zoneIndex: value[5],
zoneName: value[6],
viewport: normalizeViewport(value[7]),
gravityInfluences: Array.isArray(value[8]) ? value[8].map(normalizeGravityInfluence) : [],
bodies: Array.isArray(value[9]) ? value[9].map(normalizeBodyView) : [],
terrainRadius: value[10],
terrainDepth: value[11],
terrainDepthExponent: value[12],
terrainWaveFrequency: value[13],
};
}
function normalizeObjectsViewport(value) {
if (!Array.isArray(value)) return value;
return {
schema: value[0],
frameId: value[1],
publishedAtUtc: value[2],
simulationTimeSeconds: value[3],
runId: value[4],
zoneIndex: value[5],
zoneName: value[6],
currentEntityKey: value[7],
viewport: normalizeViewport(value[8]),
controlledEntityIndices: Array.isArray(value[9]) ? value[9] : [],
objects: Array.isArray(value[10]) ? value[10].map(normalizeViewportObject) : [],
};
}
function normalizeGravityInfluence(value) {
if (!Array.isArray(value)) return value;
return {
bodyKey: value[0],
orbitKey: value[1],
kind: value[2],
x: value[3],
y: value[4],
radius: value[5],
gravityDepth: value[6],
gravityDepthExponent: value[7],
waveRadius: value[8],
waveDepth: value[9],
waveSpeed: value[10],
};
}
function normalizeBodyView(value) {
if (!Array.isArray(value)) return value;
return {
bodyKey: value[0],
orbitKey: value[1],
name: value[2],
kind: value[3],
x: value[4],
y: value[5],
radius: value[6],
isAsteroidBelt: value[7],
body: value[8],
iconAsset: normalizeAssetRef(value[9]),
};
}
function normalizeViewportObject(value) {
if (!Array.isArray(value)) return value;
return {
entityIndex: value[0],
entityKey: value[1],
displayName: value[2],
kind: value[3],
factionKey: value[4],
x: value[5],
y: value[6],
z: value[7],
directionX: value[8],
directionY: value[9],
velocityX: value[10],
velocityY: value[11],
controlled: value[12],
targetEntityIndex: value[13],
isActive: value[14],
visibility: value[15],
status: normalizeEntityStatus(value[16]),
inventory: Array.isArray(value[17]) ? value[17].map(normalizeInventoryItem) : [],
iconAsset: normalizeAssetRef(value[18]),
};
}
function normalizeEntityStatus(value) {
if (!Array.isArray(value)) return value || {};
return {
hull: value[0],
shield: value[1],
heat: value[2],
};
}
function normalizeInventoryItem(value) {
if (!Array.isArray(value)) return value;
return {
source: value[0],
itemKey: value[1],
quantity: value[2],
quality: value[3],
durability: value[4],
enabled: value[5],
sourceIndex: value[6],
x: value[7],
y: value[8],
iconAsset: normalizeAssetRef(value[9]),
};
}
function normalizeAssetRef(value) {
if (!Array.isArray(value)) return value || {};
return {
assetKey: value[0],
kind: value[1],
uri: value[2],
transport: value[3],
contentHash: value[4],
mimeType: value[5],
metadata: value[6] || {},
};
}
function findProviderSnapshotRoute(catalog, providerId) {
const normalized = String(providerId || "").trim();
const providers = (catalog.providers || []).filter((provider) => {
if (!normalized) return true;
return provider.id === normalized ||
provider.id.includes(normalized) ||
normalized.includes(provider.id) ||
provider.id.includes("aetheria");
});
for (const provider of providers.length ? providers : (catalog.providers || [])) {
const endpoints = [...(provider.endpoints || []), ...(provider.routes || [])];
const route = endpoints.find((endpoint) => {
const address = String(endpoint.uri || endpoint.endpoint || endpoint.address || "");
const role = [endpoint.id, endpoint.role, endpoint.resolver, ...(endpoint.tags || [])].join(" ").toLowerCase();
return address.startsWith("rudp://") && (role.includes("snapshot") || role.includes("provider-cultmesh-rudp"));
}) || endpoints.find((endpoint) => {
const address = String(endpoint.uri || endpoint.endpoint || endpoint.address || "");
return address.startsWith("rudp://");
});
if (route) {
return {
providerId: provider.id,
endpoint: route.uri || route.endpoint || route.address,
};
}
}
return null;
}
function findProviderCdnRoute(catalog) {
const providers = (catalog.providers || []).filter((provider) => {
const haystack = `${provider.id || ""} ${provider.title || ""} ${provider.canonicalService || ""}`.toLowerCase();
return haystack.includes("aetheria");
});
for (const provider of providers.length ? providers : (catalog.providers || [])) {
const endpoints = [...(provider.endpoints || []), ...(provider.routes || [])];
const route = endpoints.find((endpoint) => {
const address = String(endpoint.uri || endpoint.endpoint || endpoint.address || "");
const role = [endpoint.id, endpoint.role, endpoint.resolver, endpoint.schemaId, ...(endpoint.tags || [])].join(" ").toLowerCase();
return (address.startsWith("cultmesh://") || address.startsWith("rudp://")) &&
role.includes("cultmesh-cdn") &&
(role.includes("asset_blob") || role.includes("cdn"));
}) || endpoints.find((endpoint) => {
const address = String(endpoint.uri || endpoint.endpoint || endpoint.address || "");
const role = [endpoint.id, endpoint.role, endpoint.resolver, ...(endpoint.tags || [])].join(" ").toLowerCase();
return (address.startsWith("cultmesh://") || address.startsWith("rudp://")) && role.includes("cdn");
});
if (route) {
return {
providerId: provider.id,
endpoint: route.uri || route.endpoint || route.address,
};
}
}
return null;
}
async function requestCultNetRawSnapshotDocument(peer, schemaId, recordKey, options = {}) {
const messageId = `${options.messageIdPrefix || "hermodr-snapshot"}:${Date.now()}:${Math.random().toString(16).slice(2)}`;
const response = await requestCultNetRawSnapshot(peer, [recordKey], {
schemaIds: [schemaId],
messageId,
timeoutMs: options.timeoutMs,
});
const candidates = response.documents.filter((candidate) => candidate.recordKey === recordKey);
const document = candidates.find((candidate) => candidate.schemaId === schemaId) || candidates[0];
if (!document) {
throw new Error(`CultMesh peer snapshot did not return ${schemaId} at ${recordKey}.`);
}
return document;
}
async function requestCultNetRawSnapshotFirstDocument(peer, schemaId, recordKeys, options = {}) {
const keys = [...new Set(recordKeys.map((key) => String(key || "").trim()).filter(Boolean))];
const messageId = `${options.messageIdPrefix || "hermodr-snapshot"}:${Date.now()}:${Math.random().toString(16).slice(2)}`;
const response = await requestCultNetRawSnapshot(peer, keys, {
schemaIds: [schemaId],
messageId,
timeoutMs: options.timeoutMs,
});
const document = response.documents.find((candidate) => candidate.schemaId === schemaId) || response.documents[0];
if (!document) {
throw new Error(`CultMesh peer snapshot did not return ${schemaId} at ${keys.join(", ")}.`);
}
return document;
}
async function requestCultNetRawSnapshot(peer, recordKeys, options) {
const messageId = options.messageId || `${options.messageIdPrefix || "hermodr-snapshot"}:${Date.now()}:${Math.random().toString(16).slice(2)}`;
return new Promise((resolve, reject) => {
const cleanup = () => {
clearTimeout(timer);
peer.off("message", onMessage);
peer.off("invalidMessage", onInvalidMessage);
peer.off("error", onError);
peer.off("close", onClose);
};
const rejectWith = (error) => {
cleanup();
reject(error);
};
const onMessage = (message) => {
if (message?.schemaVersion === "cultnet.error.v0") {
rejectWith(new Error(message.error || "CultNet peer returned an error."));
return;
}
if (message?.schemaVersion !== "cultnet.snapshot_response_raw.v0" ||
message.messageId !== messageId) {
return;
}
cleanup();
resolve(message);
};
const onInvalidMessage = (error) => rejectWith(error);
const onError = (error) => rejectWith(error);
const onClose = () => rejectWith(new Error("CultNet peer closed before snapshot response."));
const timer = setTimeout(
() => rejectWith(new Error(`Timed out waiting for CultNet snapshot ${messageId}.`)),
options.timeoutMs || 4_000,
);
peer.on("message", onMessage);
peer.on("invalidMessage", onInvalidMessage);
peer.on("error", onError);
peer.on("close", onClose);
peer.sendSnapshotRequest({
schemaVersion: "cultnet.snapshot_request.v0",
messageId,
recordKeys: [...recordKeys],
schemaIds: Array.isArray(options.schemaIds) ? [...options.schemaIds] : undefined,
});
});
}
function bufferFromMessagePackedBytes(payload) {
const decoded = decodeMessagePack(bufferFromPayload(payload));
return bufferFromPayload(decoded);
}
function bufferFromPayload(payload) {
if (Buffer.isBuffer(payload)) return payload;
if (payload instanceof Uint8Array) return Buffer.from(payload);
if (Array.isArray(payload)) return Buffer.from(payload);
if (payload && typeof payload === "object") {
const values = Object.keys(payload)
.sort((left, right) => Number(left) - Number(right))
.map((key) => payload[key]);
if (values.every((value) => Number.isInteger(value) && value >= 0 && value <= 255)) {
return Buffer.from(values);
}
}
throw new Error("CultMesh CDN asset payload was not binary.");
}
function contentTypeFromTags(tags) {
const tag = (Array.isArray(tags) ? tags : []).find((candidate) => String(candidate).startsWith("mime:"));
return tag ? String(tag).slice("mime:".length) : "";
}
function normalizeCultMeshAssetUri(uri) {
const text = String(uri || "").trim();
if (!text) return "";
if (text.startsWith("cultmesh://") || text.startsWith("resources://")) return text;
return `cultmesh://aetheria/assets/${text.replace(/^\/+/, "")}`;
}
function resolveAetheriaResourceAsset(options, uri) {
const parsed = parseAssetUri(uri);
if (!parsed) return "";
const aliases = {
"icons/ui/sun": "Sprites/Icons/Stroked/Sun.png",
"icons/ui/star": "Sprites/Icons/Stroked/Sun.png",
"icons/ui/planet": "Sprites/Icons/Stroked/Planet.png",
"icons/ui/gasgiant": "Sprites/Icons/Stroked/gasgiant.png",
"icons/ui/asteroid": "Sprites/Icons/Stroked/Planet.png",
"icons/ui/ship": "Sprites/Icons/Stroked/Ship.png",
"icons/ui/player": "Sprites/Icons/Stroked/Ship.png",
"icons/ui/station": "Sprites/Icons/station1.png",
"icons/ui/orbital": "Sprites/Icons/Stroked/orbital.png",
"textures/tint_splat": "Sprites/Flat UI/areaFade2.png",
"textures/perlines-nebula": "Sprites/MinimapBG.psd",
};
const relative = aliases[parsed.path] || parsed.resourcePath;
if (!relative) return "";
const candidates = [
safeJoin(options.aetheriaResourcesRoot, relative),
safeJoin(options.aetheriaResourcesRoot, `${relative}.png`),
safeJoin(options.aetheriaResourcesRoot, `${relative}.PNG`),
];
return candidates.find((candidate) => candidate && fs.existsSync(candidate) && !fs.statSync(candidate).isDirectory()) || "";
}
function parseAssetUri(uri) {
const text = String(uri || "").trim();
if (!text) return null;
if (text.startsWith("resources://")) {
return { resourcePath: text.slice("resources://".length).replace(/^\/+/, "") };
}
if (!text.startsWith("cultmesh://")) {
return { path: text.replace(/^\/+/, ""), resourcePath: "" };
}
const parsed = new URL(text);
const parts = parsed.pathname.replace(/^\/+/, "").split("/");
const assetIndex = parts.indexOf("assets");
const pathParts = assetIndex >= 0 ? parts.slice(assetIndex + 1) : parts;
return {
path: pathParts.join("/"),
resourcePath: "",
};
}
function contentTypeForPath(filePath) {
const ext = path.extname(filePath).toLowerCase();
return {
".css": "text/css; charset=utf-8",
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".svg": "image/svg+xml",
}[ext] || "application/octet-stream";
}
function stringOption(value, fallback) {
return typeof value === "string" && value.trim() ? value.trim() : fallback;
}
function numberOption(value, fallback) {
const parsed = Number.parseInt(String(value ?? ""), 10);
return Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 ? parsed : fallback;
}
function boolOption(value, fallback) {
const text = String(value ?? fallback ?? "").trim().toLowerCase();
return text === "1" || text === "true" || text === "yes" || text === "on";
}
function stringListOption(value, fallback, defaultValue = []) {
const text = String(value ?? fallback ?? "").trim();
if (!text) return [...defaultValue];
return text.split(",").map((entry) => entry.trim()).filter(Boolean);
}
function resolveCultCachePackagePath() {
const candidate = path.resolve(projectsRoot, "CultLib", "packages", "cultcache-ts", "package.json");
if (!fs.existsSync(candidate)) {
throw new Error(`CultCache TypeScript runtime is unavailable at ${candidate}`);
}
return candidate;
}
function resolveCultMeshPackagePath() {
const candidate = path.resolve(projectsRoot, "CultLib", "packages", "cultmesh-ts", "package.json");
if (!fs.existsSync(candidate)) {
throw new Error(`CultMesh TypeScript runtime is unavailable at ${candidate}`);
}
return candidate;
}
function resolveCultCacheRuntimePath() {
return path.resolve(projectsRoot, "CultLib", "packages", "cultcache-ts", "dist", "index.js");
}
function resolveCultMeshRuntimePath() {
return path.resolve(projectsRoot, "CultLib", "packages", "cultmesh-ts", "dist", "index.js");
}
function printUsage() {
console.log(`Usage:
node src/hermodr-daemon.cjs [--host 127.0.0.1] [--port 8798] [--odin-cultmesh-uri cultmesh://odin/rendezvous/provider-catalog]
Hermodr is a browser lowering adapter over Odin/CultMesh state. It does not own
provider discovery, daemon health, or raw command transport.
`);
}
main().catch((error) => {
const statusCode = error?.statusCode || 1;
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = statusCode === 409 ? 1 : statusCode;
});