263 lines
7.0 KiB
JavaScript
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;
|
|
}
|