Files
pikasTech-HWLAB/internal/protocol/index.mjs
T
2026-05-21 14:27:53 +00:00

128 lines
3.6 KiB
JavaScript

export const ENVIRONMENT_DEV = "dev";
export const DEV_ENDPOINT = "http://74.48.78.17:6667";
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
});
const methodPattern = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9_]*)+$/;
export function isFrozenServiceId(serviceId) {
return SERVICE_IDS.includes(serviceId);
}
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");
}
}