Files
2026-06-01 08:26:21 +08:00

481 lines
13 KiB
JavaScript

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;
}