Files
pikasTech-HWLAB/internal/cloud/json-rpc.mjs
T
2026-05-21 18:33:01 +00:00

263 lines
7.0 KiB
JavaScript

import { randomUUID } from "node:crypto";
import {
ENVIRONMENT_DEV,
ERROR_CODES,
HwlabProtocolError,
JSON_RPC_VERSION,
validateRequest,
validateResponse
} from "../protocol/index.mjs";
import {
AUDIT_FIELD_NAMES,
CLOUD_API_SERVICE_ID,
createAuditRecord,
deriveActorFromMeta
} from "../audit/index.mjs";
import { buildDbHealthContract } from "./db-contract.mjs";
import { createCloudRuntimeStore } from "../db/runtime-store.mjs";
export const SUPPORTED_RPC_METHODS = Object.freeze([
"system.health",
"cloud.adapter.describe",
"gateway.session.register",
"box.resource.register",
"box.capability.report",
"hardware.operation.request",
"hardware.invoke.shell",
"audit.event.write",
"audit.event.query",
"evidence.record.write",
"evidence.record.query"
]);
const rpcHandlers = new Map([
["system.health", handleSystemHealth],
["cloud.adapter.describe", handleAdapterDescribe],
["gateway.session.register", (params, envelope, context) => getRuntimeStore(context).registerGatewaySession(params, envelope.meta)],
["box.resource.register", (params, envelope, context) => getRuntimeStore(context).registerBoxResource(params, envelope.meta)],
["box.capability.report", (params, envelope, context) => getRuntimeStore(context).reportBoxCapabilities(params, envelope.meta)],
["hardware.operation.request", (params, envelope, context) => getRuntimeStore(context).requestHardwareOperation(params, envelope.meta)],
["hardware.invoke.shell", (params, envelope, context) => getRuntimeStore(context).invokeHardwareShell(params, envelope.meta)],
["audit.event.write", (params, envelope, context) => getRuntimeStore(context).writeAuditEvent(params, envelope.meta)],
["audit.event.query", (params, envelope, context) => getRuntimeStore(context).queryAuditEvents(params)],
["evidence.record.write", (params, envelope, context) => getRuntimeStore(context).writeEvidenceRecord(params, envelope.meta)],
["evidence.record.query", (params, envelope, context) => getRuntimeStore(context).queryEvidenceRecords(params)]
]);
const defaultRuntimeStore = createCloudRuntimeStore();
export async function handleJsonRpcRequest(envelope, context = {}) {
const requestMeta = extractMeta(envelope);
const requestId = envelope?.id ?? "req_unassigned";
const method = typeof envelope?.method === "string" ? envelope.method : "method_unspecified";
try {
validateRequest(envelope);
} catch (error) {
return createErrorEnvelope({
id: requestId,
code: ERROR_CODES.invalidRequest,
message: "Invalid JSON-RPC request",
data: {
reason: error.message
},
requestMeta,
context: {
...context,
operation: "json_rpc.validate",
target: {
type: "rpc_method",
id: method
}
}
});
}
const handler = rpcHandlers.get(envelope.method);
if (!handler) {
return createErrorEnvelope({
id: envelope.id,
code: ERROR_CODES.methodNotFound,
message: `Method ${envelope.method} is not implemented by hwlab-cloud-api`,
data: {
method: envelope.method
},
requestMeta: envelope.meta,
context: {
...context,
operation: envelope.method,
target: {
type: "rpc_method",
id: envelope.method
},
result: "rejected"
}
});
}
try {
const result = await handler(envelope.params ?? {}, envelope, context);
return createResultEnvelope({
id: envelope.id,
result,
requestMeta: envelope.meta
});
} catch (error) {
if (error instanceof HwlabProtocolError) {
return createErrorEnvelope({
id: envelope.id,
code: error.code,
message: error.message,
data: error.data,
requestMeta: envelope.meta,
context: {
...context,
...error.context,
operation: envelope.method,
target: {
type: "rpc_method",
id: envelope.method
},
result: "rejected"
}
});
}
return createErrorEnvelope({
id: envelope.id,
code: ERROR_CODES.internalError,
message: "hwlab-cloud-api failed to process the request",
data: {
reason: error.message
},
requestMeta: envelope.meta,
context: {
...context,
operation: envelope.method,
target: {
type: "rpc_method",
id: envelope.method
},
result: "failed"
}
});
}
}
export function createResultEnvelope({ id, result, requestMeta = {} }) {
const envelope = {
jsonrpc: JSON_RPC_VERSION,
id,
result,
meta: createResponseMeta(requestMeta)
};
validateResponse(envelope);
return envelope;
}
export function createErrorEnvelope({
id = "req_unassigned",
code = ERROR_CODES.hwlabUnknown,
message = "HWLAB runtime error",
data = {},
requestMeta = {},
context = {}
} = {}) {
const responseId = id ?? "req_unassigned";
const meta = createResponseMeta(requestMeta);
const audit = createAuditRecord({
requestId: responseId,
actor: deriveActorFromMeta(requestMeta),
source: {
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
traceId: meta.traceId,
adapter: context.adapter || "json-rpc"
},
operation: context.operation || "json_rpc.error",
target: context.target || {
type: "cloud_api",
id: CLOUD_API_SERVICE_ID
},
result: context.result || "failed",
timestamp: context.timestamp
});
const envelope = {
jsonrpc: JSON_RPC_VERSION,
id: responseId,
error: {
code,
message,
data: {
...normalizeErrorData(data),
audit
}
},
meta
};
validateResponse(envelope);
return envelope;
}
export function createResponseMeta(requestMeta = {}) {
const meta = {
traceId: requestMeta.traceId || `trc_${randomUUID()}`,
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV
};
if (requestMeta.actorId) {
meta.actorId = requestMeta.actorId;
}
return meta;
}
function handleSystemHealth(params, envelope, context) {
return {
status: healthStatus(),
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
db: buildDbHealthContract(),
runtime: getRuntimeStore(context).summary()
};
}
function handleAdapterDescribe(params, envelope, context) {
return {
serviceId: CLOUD_API_SERVICE_ID,
rpcEndpoint: "POST /rpc",
restEndpoint: "POST /v1/rpc/{method}",
methods: SUPPORTED_RPC_METHODS,
auditFields: AUDIT_FIELD_NAMES,
db: buildDbHealthContract(),
runtime: getRuntimeStore(context).summary()
};
}
function getRuntimeStore(context = {}) {
return context.runtimeStore || defaultRuntimeStore;
}
function healthStatus() {
return "degraded";
}
function extractMeta(envelope) {
if (envelope && typeof envelope === "object" && !Array.isArray(envelope) && envelope.meta) {
return envelope.meta;
}
return {};
}
function normalizeErrorData(data) {
if (!data || typeof data !== "object" || Array.isArray(data)) {
return {};
}
return data;
}