export const ENVIRONMENT_DEV = "dev"; export const DEV_ENDPOINT = "http://74.48.78.17:16667"; export const DEV_FRONTEND_ENDPOINT = "http://74.48.78.17:16666"; export const JSON_RPC_VERSION = "2.0"; export const SERVICE_IDS = Object.freeze([ "hwlab-cloud-api", "hwlab-cloud-web", "hwlab-agent-mgr", "hwlab-agent-worker", "hwlab-gateway", "hwlab-gateway-simu", "hwlab-box-simu", "hwlab-patch-panel", "hwlab-router", "hwlab-tunnel-client", "hwlab-edge-proxy", "hwlab-cli", "hwlab-agent-skills" ]); export const TABLES = Object.freeze([ "projects", "gateway_sessions", "box_resources", "box_capabilities", "wiring_configs", "patch_panel_status", "hardware_operations", "audit_events", "agent_sessions", "worker_sessions", "agent_trace_events", "evidence_records" ]); export const ERROR_CODES = Object.freeze({ parseError: -32700, invalidRequest: -32600, methodNotFound: -32601, invalidParams: -32602, internalError: -32603, hwlabUnknown: -32000, projectNotFound: -32010, sessionNotFound: -32011, capabilityUnavailable: -32020, resourceLocked: -32021, wiringInvalid: -32022, operationRejected: -32023, evidenceMissing: -32030, auditRequired: -32040 }); function freezeRecordShape({ required, allowed }) { return Object.freeze({ required: Object.freeze([...required]), allowed: Object.freeze([...allowed]) }); } export const PROTOCOL_RECORD_SHAPES = Object.freeze({ gatewaySession: freezeRecordShape({ required: [ "gatewaySessionId", "projectId", "serviceId", "gatewayId", "status", "environment", "startedAt" ], allowed: [ "gatewaySessionId", "projectId", "serviceId", "gatewayId", "endpoint", "status", "environment", "startedAt", "lastSeenAt", "stoppedAt", "labels" ] }), boxResource: freezeRecordShape({ required: [ "resourceId", "projectId", "gatewaySessionId", "boxId", "resourceType", "state", "environment", "createdAt", "updatedAt" ], allowed: [ "resourceId", "projectId", "gatewaySessionId", "boxId", "resourceType", "name", "state", "environment", "metadata", "createdAt", "updatedAt" ] }), boxCapability: freezeRecordShape({ required: [ "capabilityId", "resourceId", "projectId", "name", "direction", "valueType", "mutatesState", "createdAt", "updatedAt" ], allowed: [ "capabilityId", "resourceId", "projectId", "name", "description", "direction", "valueType", "unit", "constraints", "mutatesState", "createdAt", "updatedAt" ] }), hardwareOperation: freezeRecordShape({ required: [ "operationId", "projectId", "gatewaySessionId", "resourceId", "capabilityId", "requestedBy", "status", "environment", "requestedAt", "updatedAt" ], allowed: [ "operationId", "projectId", "gatewaySessionId", "agentSessionId", "workerSessionId", "resourceId", "capabilityId", "requestedBy", "input", "output", "status", "environment", "requestedAt", "startedAt", "completedAt", "updatedAt", "error" ] }), auditEvent: freezeRecordShape({ required: [ "auditId", "traceId", "actorType", "actorId", "action", "targetType", "targetId", "serviceId", "environment", "occurredAt" ], allowed: [ "auditId", "traceId", "actorType", "actorId", "action", "targetType", "targetId", "projectId", "gatewaySessionId", "workerSessionId", "operationId", "serviceId", "environment", "outcome", "reason", "metadata", "occurredAt" ] }), evidenceRecord: freezeRecordShape({ required: [ "evidenceId", "projectId", "operationId", "kind", "uri", "sha256", "serviceId", "environment", "createdAt" ], allowed: [ "evidenceId", "projectId", "operationId", "agentSessionId", "workerSessionId", "kind", "uri", "mimeType", "sha256", "sizeBytes", "serviceId", "environment", "metadata", "createdAt" ] }) }); export class HwlabProtocolError extends Error { constructor(message, { code = ERROR_CODES.hwlabUnknown, data = {}, context = {} } = {}) { super(message); this.name = "HwlabProtocolError"; this.code = code; this.data = data; this.context = context; } } const methodPattern = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9_]*)+$/; const protocolIdPattern = /^[a-z][a-z0-9]*_[A-Za-z0-9._:-]+$/; const actionPattern = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/; const timestampFieldNames = new Set([ "startedAt", "lastSeenAt", "stoppedAt", "createdAt", "updatedAt", "requestedAt", "completedAt", "occurredAt" ]); const idFieldNames = new Set([ "gatewaySessionId", "projectId", "gatewayId", "resourceId", "boxId", "capabilityId", "operationId", "agentSessionId", "workerSessionId", "requestedBy", "auditId", "traceId", "actorId", "targetId", "evidenceId" ]); const actorTypes = new Set(["user", "service", "agent", "worker", "system"]); const auditOutcomes = new Set(["accepted", "succeeded", "failed", "rejected", "canceled"]); const evidenceKinds = new Set(["log", "screenshot", "measurement", "artifact", "report", "trace"]); const gatewayStatuses = new Set(["starting", "connected", "degraded", "disconnected", "stopped"]); const resourceStates = new Set(["available", "reserved", "running", "maintenance", "offline"]); const operationStatuses = new Set([ "requested", "accepted", "queued", "running", "succeeded", "failed", "rejected", "canceled", "timed_out" ]); const resourceTypes = new Set([ "board", "power_supply", "serial_port", "network_port", "relay", "sensor", "simulator_endpoint" ]); const capabilityDirections = new Set(["input", "output", "bidirectional"]); const capabilityValueTypes = new Set(["boolean", "integer", "number", "string", "object", "binary"]); export function isFrozenServiceId(serviceId) { return SERVICE_IDS.includes(serviceId); } export function isProtocolId(value) { return typeof value === "string" && protocolIdPattern.test(value); } export function validateMeta(meta) { if (!meta || typeof meta !== "object" || Array.isArray(meta)) { throw new Error("meta must be an object"); } if (!meta.traceId) { throw new Error("meta.traceId is required"); } if (!isFrozenServiceId(meta.serviceId)) { throw new Error(`unknown serviceId ${JSON.stringify(meta.serviceId)}`); } if (meta.environment !== ENVIRONMENT_DEV) { throw new Error(`unsupported environment ${JSON.stringify(meta.environment)}`); } } export function validateRequest(envelope) { if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) { throw new Error("request must be an object"); } if (envelope.jsonrpc !== JSON_RPC_VERSION) { throw new Error(`jsonrpc must be ${JSON_RPC_VERSION}`); } if (envelope.id === undefined || envelope.id === null) { throw new Error("id is required"); } if (!methodPattern.test(envelope.method)) { throw new Error(`invalid method ${JSON.stringify(envelope.method)}`); } if ( envelope.params !== undefined && (!envelope.params || typeof envelope.params !== "object" || Array.isArray(envelope.params)) ) { throw new Error("params must be an object when present"); } validateMeta(envelope.meta); } export function validateResponse(envelope) { if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) { throw new Error("response must be an object"); } if (envelope.jsonrpc !== JSON_RPC_VERSION) { throw new Error(`jsonrpc must be ${JSON_RPC_VERSION}`); } if (envelope.id === undefined || envelope.id === null) { throw new Error("id is required"); } const hasResult = Object.hasOwn(envelope, "result"); const hasError = Object.hasOwn(envelope, "error"); if (hasResult === hasError) { throw new Error("response must contain exactly one of result or error"); } if (hasError) { validateRpcError(envelope.error); } validateMeta(envelope.meta); } export function validateRpcError(error) { if (!error || typeof error !== "object" || Array.isArray(error)) { throw new Error("error must be an object"); } if (!Number.isInteger(error.code)) { throw new Error("error.code must be an integer"); } if (!error.message || typeof error.message !== "string") { throw new Error("error.message is required"); } } export function errorNameForCode(code) { for (const [name, value] of Object.entries(ERROR_CODES)) { if (value === code) { return name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); } } return "hwlab_unknown"; } export function createRpcErrorBody({ code = ERROR_CODES.hwlabUnknown, message = "HWLAB runtime error", data = {} }) { const error = { code, name: errorNameForCode(code), message, data }; validateRpcError(error); return { error }; } export function assertProtocolRecord(kind, record) { const shape = PROTOCOL_RECORD_SHAPES[kind]; if (!shape) { throw new Error(`unknown protocol record kind ${JSON.stringify(kind)}`); } if (!record || typeof record !== "object" || Array.isArray(record)) { throw new Error(`${kind} must be an object`); } for (const field of shape.required) { if (!Object.hasOwn(record, field)) { throw new Error(`${kind}.${field} is required`); } } const allowed = new Set(shape.allowed); for (const [field, value] of Object.entries(record)) { if (!allowed.has(field)) { throw new Error(`${kind}.${field} is not part of the frozen protocol schema`); } assertProtocolField(kind, field, value); } } export function assertProtocolRecords(kind, records) { if (!Array.isArray(records)) { throw new Error(`${kind} records must be an array`); } for (const record of records) { assertProtocolRecord(kind, record); } } function assertProtocolField(kind, field, value) { if (idFieldNames.has(field) && !isProtocolId(value)) { throw new Error(`${kind}.${field} must be a protocol id`); } if (timestampFieldNames.has(field) && Number.isNaN(Date.parse(value))) { throw new Error(`${kind}.${field} must be an RFC 3339 timestamp`); } if (field === "environment" && value !== ENVIRONMENT_DEV) { throw new Error(`${kind}.environment must be ${ENVIRONMENT_DEV}`); } if (field === "serviceId" && !isFrozenServiceId(value)) { throw new Error(`${kind}.serviceId is not a frozen service id`); } if ((field === "action" || field === "name") && !actionPattern.test(value)) { throw new Error(`${kind}.${field} must be a dotted lower-case action name`); } if (field === "actorType" && !actorTypes.has(value)) { throw new Error(`${kind}.actorType is invalid`); } if (field === "outcome" && !auditOutcomes.has(value)) { throw new Error(`${kind}.outcome is invalid`); } if (field === "kind" && !evidenceKinds.has(value)) { throw new Error(`${kind}.kind is invalid`); } if (field === "status") { assertStatusField(kind, value); } if (field === "state" && !resourceStates.has(value)) { throw new Error(`${kind}.state is invalid`); } if (field === "resourceType" && !resourceTypes.has(value)) { throw new Error(`${kind}.resourceType is invalid`); } if (field === "direction" && !capabilityDirections.has(value)) { throw new Error(`${kind}.direction is invalid`); } if (field === "valueType" && !capabilityValueTypes.has(value)) { throw new Error(`${kind}.valueType is invalid`); } if (field === "mutatesState" && typeof value !== "boolean") { throw new Error(`${kind}.mutatesState must be boolean`); } if (field === "sha256" && !/^[a-f0-9]{64}$/.test(value)) { throw new Error(`${kind}.sha256 must be lowercase sha256 hex`); } } function assertStatusField(kind, value) { if (kind === "gatewaySession" && !gatewayStatuses.has(value)) { throw new Error(`${kind}.status is invalid`); } if (kind === "boxResource" && !resourceStates.has(value)) { throw new Error(`${kind}.status is invalid`); } if (kind === "hardwareOperation" && !operationStatuses.has(value)) { throw new Error(`${kind}.status is invalid`); } }