import { createHash, randomUUID } from "node:crypto"; import { createRpcErrorBody, ENVIRONMENT_DEV, ERROR_CODES, isProtocolEnvironment } from "../protocol/index.mjs"; import { getPortDefinition } from "./model.mjs"; export const AUDIT_SHAPE_FIELDS = Object.freeze([ "auditId", "traceId", "actorType", "actorId", "action", "targetType", "targetId", "serviceId", "environment", "occurredAt" ]); export const EVIDENCE_SHAPE_FIELDS = Object.freeze([ "evidenceId", "projectId", "operationId", "kind", "uri", "sha256", "serviceId", "environment", "createdAt" ]); export function isoNow() { return new Date().toISOString(); } export function makeId(prefix, value = randomUUID()) { const normalized = String(value) .trim() .replace(/[^A-Za-z0-9._:-]+/g, "_") .replace(/^_+|_+$/g, ""); return `${prefix}_${normalized || randomUUID()}`; } export function operationContext(input = {}, { serviceId, actorId = `svc_${serviceId}`, now = isoNow() } = {}) { const traceId = input.traceId ?? makeId("trc", randomUUID()); const operationId = input.operationId ?? makeId("op", randomUUID()); const requestId = input.requestId ?? makeId("req", randomUUID()); return { traceId, operationId, requestId, actorType: input.actorType ?? "service", actorId: normalizeActorId(input.actorId ?? actorId), requestedBy: normalizeActorId(input.requestedBy ?? input.actorId ?? actorId), observedAt: input.observedAt ?? now }; } function normalizeActorId(value) { const text = String(value ?? ""); return /^[a-z][a-z0-9]*_/.test(text) ? text : makeId("svc", text || "hwlab-sim"); } export function createAuditEvent({ serviceId, action, targetType, targetId, projectId, gatewaySessionId, operationId, traceId, actorType = "service", actorId = `svc_${serviceId}`, outcome, reason, metadata = {}, environment = ENVIRONMENT_DEV, occurredAt = isoNow() }) { return { auditId: makeId("aud", `${serviceId}_${operationId ?? traceId ?? randomUUID()}_${action}`), traceId, actorType, actorId, action, targetType, targetId, ...(projectId ? { projectId } : {}), ...(gatewaySessionId ? { gatewaySessionId } : {}), ...(operationId ? { operationId } : {}), serviceId, environment: normalizeEnvironment(environment), ...(outcome ? { outcome } : {}), ...(reason ? { reason } : {}), metadata, occurredAt }; } export function createEvidenceRecord({ serviceId, projectId, operationId, traceId, kind = "measurement", payload = {}, metadata = {}, environment = ENVIRONMENT_DEV, createdAt = isoNow() }) { const normalizedEnvironment = normalizeEnvironment(environment); const serialized = JSON.stringify({ serviceId, projectId, operationId, traceId, kind, environment: normalizedEnvironment, payload, metadata, createdAt }); const sha256 = createHash("sha256").update(serialized).digest("hex"); return { evidenceId: makeId("evd", `${serviceId}_${operationId}_${kind}`), projectId, operationId, kind, uri: `memory://${serviceId}/${operationId}/${kind}.json`, mimeType: "application/json", sha256, sizeBytes: Buffer.byteLength(serialized), serviceId, environment: normalizedEnvironment, metadata: { traceId, ...metadata }, createdAt }; } function normalizeEnvironment(environment) { return isProtocolEnvironment(environment) ? environment : ENVIRONMENT_DEV; } export function normalizePortValue(value, definition) { if (!definition) { return value; } if (definition.valueType === "boolean") { if (typeof value === "boolean") { return value; } if (value === "true") { return true; } if (value === "false") { return false; } throw createRuntimeError({ code: ERROR_CODES.invalidParams, message: `${definition.port} requires a boolean value`, data: { port: definition.port, valueType: definition.valueType } }); } if (definition.valueType === "number") { const number = typeof value === "number" ? value : Number(value); if (Number.isFinite(number)) { return number; } throw createRuntimeError({ code: ERROR_CODES.invalidParams, message: `${definition.port} requires a finite number value`, data: { port: definition.port, valueType: definition.valueType } }); } if (definition.valueType === "string" && value !== null && value !== undefined) { return String(value); } if (definition.valueType === "object" && value !== null && value !== undefined) { if (typeof value === "object" && !Array.isArray(value)) { return value; } throw createRuntimeError({ code: ERROR_CODES.invalidParams, message: `${definition.port} requires an object value`, data: { port: definition.port, valueType: definition.valueType } }); } return value ?? null; } export function findCapability(state, capabilityId) { if (!capabilityId) { return null; } return state.capabilities.find((capability) => capability.capabilityId === capabilityId) ?? null; } export function resolvePortForRead(state, { port, capabilityId }) { if (capabilityId) { const capability = findCapability(state, capabilityId); if (!capability?.constraints?.port || !state.ports[capability.constraints.port]) { throw createRuntimeError({ code: ERROR_CODES.capabilityUnavailable, message: "Requested hardware capability is unavailable", data: { capabilityId, port: port ?? null, resourceId: state.resource.resourceId, reason: "capability_unavailable" } }); } if (port && port !== capability.constraints.port) { throw createRuntimeError({ code: ERROR_CODES.invalidParams, message: "Port does not match requested capability", data: { capabilityId, capabilityPort: capability.constraints.port, port, resourceId: state.resource.resourceId, reason: "capability_port_mismatch" } }); } return capability.constraints.port; } if (port && state.ports[port]) { return port; } throw createRuntimeError({ code: ERROR_CODES.capabilityUnavailable, message: "Requested hardware capability is unavailable", data: { capabilityId: capabilityId ?? null, port: port ?? null, resourceId: state.resource.resourceId, reason: "capability_unavailable" } }); } export function resolvePortForWrite(state, { port, capabilityId }) { const resolvedPort = resolvePortForRead(state, { port, capabilityId }); const capability = capabilityId ? findCapability(state, capabilityId) : null; if (capabilityId && !capability?.mutatesState) { throw createRuntimeError({ code: ERROR_CODES.operationRejected, message: "Read-only capability cannot be used for a write operation", data: { capabilityId, port: resolvedPort, reason: "read_only_capability" } }); } return resolvedPort; } export function readBoxPort({ state, port, capabilityId, context = operationContext({}) }) { const resolvedPort = resolvePortForRead(state, { port, capabilityId }); const current = state.ports[resolvedPort]; const capability = capabilityId ? findCapability(state, capabilityId) : null; const audit = createAuditEvent({ serviceId: "hwlab-box-simu", action: "hardware.operation.succeeded", targetType: "box_resource", targetId: state.resource.resourceId, projectId: state.projectId, gatewaySessionId: state.gatewaySessionId, operationId: context.operationId, traceId: context.traceId, actorType: context.actorType, actorId: context.actorId, outcome: "succeeded", metadata: { boxId: state.boxId, port: resolvedPort, direction: current.direction, capabilityId: capability?.capabilityId ?? null, operation: "port.read" }, occurredAt: context.observedAt }); const evidence = createEvidenceRecord({ serviceId: "hwlab-box-simu", projectId: state.projectId, operationId: context.operationId, traceId: context.traceId, payload: current, metadata: { boxId: state.boxId, resourceId: state.resource.resourceId, capabilityId: capability?.capabilityId ?? null, port: resolvedPort }, createdAt: context.observedAt }); return { accepted: true, operationId: context.operationId, traceId: context.traceId, boxId: state.boxId, resourceId: state.resource.resourceId, capabilityId: capability?.capabilityId ?? null, port: resolvedPort, value: current.value, state: current, auditId: audit.auditId, evidenceId: evidence.evidenceId, audit, evidence }; } export function writeBoxPort({ state, port, capabilityId, value, source = "local", sourceResourceId, sourcePort, propagatedBy, internal = false, context = operationContext({}) }) { const resolvedPort = resolvePortForWrite(state, { port, capabilityId }); const definition = getPortDefinition(resolvedPort); const current = state.ports[resolvedPort]; const normalizedValue = normalizePortValue(value, definition); const now = context.observedAt; const capability = capabilityId ? findCapability(state, capabilityId) : null; state.ports[resolvedPort] = { ...current, value: normalizedValue, source, ...(sourceResourceId ? { sourceResourceId } : {}), ...(sourcePort ? { sourcePort } : {}), ...(propagatedBy ? { propagatedBy } : {}), updatedAt: now }; state.updatedAt = now; const audit = createAuditEvent({ serviceId: "hwlab-box-simu", action: internal ? "hardware.internal_port.written" : "hardware.operation.succeeded", targetType: "box_resource", targetId: state.resource.resourceId, projectId: state.projectId, gatewaySessionId: state.gatewaySessionId, operationId: context.operationId, traceId: context.traceId, actorType: context.actorType, actorId: context.actorId, outcome: "succeeded", metadata: { boxId: state.boxId, port: resolvedPort, direction: current.direction, capabilityId: capability?.capabilityId ?? null, internalPortApi: internal, patchPanelOnlyPropagation: true, localLoopbackEnabled: false }, occurredAt: now }); const evidence = createEvidenceRecord({ serviceId: "hwlab-box-simu", projectId: state.projectId, operationId: context.operationId, traceId: context.traceId, payload: state.ports[resolvedPort], metadata: { boxId: state.boxId, resourceId: state.resource.resourceId, capabilityId: capability?.capabilityId ?? null, port: resolvedPort, internalPortApi: internal, producerServiceId: "hwlab-box-simu" }, createdAt: now }); return { accepted: true, operationId: context.operationId, traceId: context.traceId, boxId: state.boxId, resourceId: state.resource.resourceId, capabilityId: capability?.capabilityId ?? null, port: resolvedPort, value: normalizedValue, state: state.ports[resolvedPort], crossDevicePropagation: "patch-panel-only", localLoopbackEnabled: false, auditId: audit.auditId, evidenceId: evidence.evidenceId, audit, evidence }; } export function handleBoxInvoke({ state, body, context = operationContext(body ?? {}) }) { const operation = body.operation ?? body.method; if (operation === "port.read" || operation === "hardware.port.read") { return readBoxPort({ state, port: body.port, capabilityId: body.capabilityId, context }); } if (operation === "port.write" || operation === "hardware.port.write") { return writeBoxPort({ state, port: body.port, capabilityId: body.capabilityId, value: body.value, source: body.source ?? "gateway", context }); } throw createRuntimeError({ code: ERROR_CODES.methodNotFound, message: "Box simulator invoke method is not implemented", data: { method: operation ?? null, resourceId: state.resource.resourceId } }); } export function createRuntimeError({ code, message, data = {}, statusCode }) { const error = new Error(message); error.statusCode = statusCode ?? httpStatusForErrorCode(code); error.body = createRpcErrorBody({ code, message, data }); return error; } export function httpStatusForErrorCode(code) { if (code === ERROR_CODES.invalidRequest || code === ERROR_CODES.invalidParams) { return 400; } if (code === ERROR_CODES.methodNotFound || code === ERROR_CODES.capabilityUnavailable) { return 404; } if (code === ERROR_CODES.resourceLocked || code === ERROR_CODES.operationRejected) { return 409; } return 500; }