639 lines
19 KiB
JavaScript
639 lines
19 KiB
JavaScript
import { randomUUID } from "node:crypto";
|
||
|
||
export const HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION = "hwlab-agent-runtime-skill-cli-v1";
|
||
export const HWLAB_M3_IO_API_ROUTE = "/v1/m3/io";
|
||
export const HWLAB_M3_IO_SKILL_NAME = "hwlab-agent-runtime.m3-io";
|
||
export const HWLAB_M3_IO_CAPABILITY_LEVELS = Object.freeze({
|
||
blocked: "hwlab-api-control-blocked",
|
||
ready: "hwlab-api-control-ready"
|
||
});
|
||
|
||
const allowedActions = new Set(["do.write", "di.read"]);
|
||
const forbiddenDirectTarget = /(?:gateway(?:-simu)?|box(?:-simu)?|patch-panel|hwlab-patch-panel|\/invoke\b|\/sync\/tick\b|:7101\b|:7201\b|:7301\b)/iu;
|
||
const defaultTimeoutMs = 30000;
|
||
|
||
export async function runM3IoSkillCommand(argv = [], options = {}) {
|
||
const env = options.env ?? process.env;
|
||
const parsed = parseM3IoArgs(argv, env);
|
||
if (parsed.help) {
|
||
return helpPayload();
|
||
}
|
||
|
||
const traceId = parsed.traceId || `trc_${randomUUID()}`;
|
||
const requestId = parsed.requestId || `req_${randomUUID()}`;
|
||
const actorId = parsed.actorId || "usr_code_agent";
|
||
const apiTarget = resolveCloudApiTarget(parsed.apiBaseUrl, env);
|
||
const validation = validateCloudApiTarget(apiTarget);
|
||
if (validation) {
|
||
return blockedPayload({
|
||
traceId,
|
||
requestId,
|
||
apiTarget,
|
||
code: validation.code,
|
||
message: validation.message
|
||
});
|
||
}
|
||
|
||
const command = normalizeCommand(parsed);
|
||
if (!allowedActions.has(command.action)) {
|
||
return blockedPayload({
|
||
traceId,
|
||
requestId,
|
||
apiTarget,
|
||
code: "invalid_action",
|
||
message: "M3 Skill CLI only allows do.write or di.read."
|
||
});
|
||
}
|
||
if (command.action === "do.write" && typeof command.value !== "boolean") {
|
||
return blockedPayload({
|
||
traceId,
|
||
requestId,
|
||
apiTarget,
|
||
code: "invalid_boolean_value",
|
||
message: "M3 DO1 write requires --value true or --value false."
|
||
});
|
||
}
|
||
|
||
const payload = requestPayload(command, { traceId, requestId, actorId });
|
||
const startedAt = nowIso(options.now);
|
||
const response = await postJson(apiTarget.url, payload, {
|
||
requestJson: options.requestJson,
|
||
timeoutMs: parsed.timeoutMs,
|
||
headers: {
|
||
"x-trace-id": traceId,
|
||
"x-request-id": requestId,
|
||
"x-actor-id": actorId
|
||
}
|
||
});
|
||
const finishedAt = nowIso(options.now);
|
||
|
||
return normalizeSkillResponse({
|
||
response,
|
||
command,
|
||
payload,
|
||
apiTarget,
|
||
traceId,
|
||
requestId,
|
||
actorId,
|
||
startedAt,
|
||
finishedAt
|
||
});
|
||
}
|
||
|
||
export function parseM3IoArgs(argv = [], env = process.env) {
|
||
const args = [...argv];
|
||
const command = args.shift() ?? "help";
|
||
const subcommand = args.shift() ?? "";
|
||
const parsed = {
|
||
command,
|
||
subcommand,
|
||
action: null,
|
||
value: undefined,
|
||
apiBaseUrl: null,
|
||
traceId: null,
|
||
requestId: null,
|
||
actorId: null,
|
||
timeoutMs: defaultTimeoutMs,
|
||
pretty: false,
|
||
help: command === "help" || command === "--help" || command === "-h"
|
||
};
|
||
|
||
if (command !== "m3" || subcommand !== "io") {
|
||
parsed.help = true;
|
||
return parsed;
|
||
}
|
||
|
||
for (let index = 0; index < args.length; index += 1) {
|
||
const arg = args[index];
|
||
if (arg === "--help" || arg === "-h") {
|
||
parsed.help = true;
|
||
} else if (arg === "--pretty") {
|
||
parsed.pretty = true;
|
||
} else if (arg === "--action") {
|
||
parsed.action = readOption(args, ++index, arg);
|
||
} else if (arg === "--value") {
|
||
parsed.value = parseBoolean(readOption(args, ++index, arg));
|
||
} else if (arg === "--api-base-url") {
|
||
parsed.apiBaseUrl = readOption(args, ++index, arg);
|
||
} else if (arg === "--trace-id") {
|
||
parsed.traceId = readOption(args, ++index, arg);
|
||
} else if (arg === "--request-id") {
|
||
parsed.requestId = readOption(args, ++index, arg);
|
||
} else if (arg === "--actor-id") {
|
||
parsed.actorId = readOption(args, ++index, arg);
|
||
} else if (arg === "--timeout-ms") {
|
||
parsed.timeoutMs = parseTimeout(readOption(args, ++index, arg));
|
||
} else if (arg === "write") {
|
||
parsed.action = "do.write";
|
||
} else if (arg === "read") {
|
||
parsed.action = "di.read";
|
||
} else {
|
||
throw new Error(`unknown argument ${arg}`);
|
||
}
|
||
}
|
||
|
||
parsed.apiBaseUrl = parsed.apiBaseUrl || firstNonEmpty(
|
||
env.HWLAB_CODE_AGENT_HWLAB_API_BASE_URL,
|
||
env.HWLAB_API_BASE_URL,
|
||
env.HWLAB_CLOUD_API_BASE_URL,
|
||
loopbackCloudApiBaseUrl(env)
|
||
);
|
||
return parsed;
|
||
}
|
||
|
||
export function resolveCloudApiTarget(apiBaseUrl, env = process.env) {
|
||
const source = apiBaseUrl
|
||
? "explicit"
|
||
: firstNonEmpty(env.HWLAB_CODE_AGENT_HWLAB_API_BASE_URL, null)
|
||
? "env:HWLAB_CODE_AGENT_HWLAB_API_BASE_URL"
|
||
: firstNonEmpty(env.HWLAB_API_BASE_URL, null)
|
||
? "env:HWLAB_API_BASE_URL"
|
||
: firstNonEmpty(env.HWLAB_CLOUD_API_BASE_URL, null)
|
||
? "env:HWLAB_CLOUD_API_BASE_URL"
|
||
: "loopback-cloud-api-self";
|
||
const base = firstNonEmpty(apiBaseUrl, env.HWLAB_CODE_AGENT_HWLAB_API_BASE_URL, env.HWLAB_API_BASE_URL, env.HWLAB_CLOUD_API_BASE_URL, loopbackCloudApiBaseUrl(env));
|
||
const url = new URL(HWLAB_M3_IO_API_ROUTE, ensureTrailingSlash(base));
|
||
return {
|
||
route: HWLAB_M3_IO_API_ROUTE,
|
||
url: url.toString(),
|
||
redactedUrl: redactUrl(url.toString()),
|
||
source,
|
||
cloudApiOnly: true,
|
||
directGatewayCalls: false,
|
||
directBoxCalls: false,
|
||
directPatchPanelCalls: false
|
||
};
|
||
}
|
||
|
||
export function validateCloudApiTarget(apiTarget) {
|
||
let url;
|
||
try {
|
||
url = new URL(apiTarget.url);
|
||
} catch {
|
||
return {
|
||
code: "invalid_hwlab_api_url",
|
||
message: "HWLAB API base URL is not a valid URL."
|
||
};
|
||
}
|
||
|
||
if (url.pathname !== HWLAB_M3_IO_API_ROUTE) {
|
||
return {
|
||
code: "invalid_hwlab_api_route",
|
||
message: `Skill CLI must call exactly ${HWLAB_M3_IO_API_ROUTE}.`
|
||
};
|
||
}
|
||
|
||
const targetText = `${url.hostname}${url.pathname}${url.port ? `:${url.port}` : ""}`;
|
||
if (forbiddenDirectTarget.test(targetText)) {
|
||
return {
|
||
code: "direct_hardware_target_blocked",
|
||
message: "Skill CLI target must be HWLAB cloud-api, not gateway/box/patch-panel."
|
||
};
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function normalizeCommand(parsed) {
|
||
const action = String(parsed.action ?? "").trim().toLowerCase();
|
||
if (action === "do.write") {
|
||
return {
|
||
action,
|
||
gatewayId: "gwsimu_1",
|
||
resourceId: "res_boxsimu_1",
|
||
boxId: "boxsimu_1",
|
||
port: "DO1",
|
||
value: parsed.value
|
||
};
|
||
}
|
||
|
||
return {
|
||
action,
|
||
gatewayId: "gwsimu_2",
|
||
resourceId: "res_boxsimu_2",
|
||
boxId: "boxsimu_2",
|
||
port: "DI1"
|
||
};
|
||
}
|
||
|
||
function requestPayload(command, meta) {
|
||
return {
|
||
action: command.action,
|
||
gatewayId: command.gatewayId,
|
||
resourceId: command.resourceId,
|
||
boxId: command.boxId,
|
||
port: command.port,
|
||
...(Object.hasOwn(command, "value") ? { value: command.value } : {}),
|
||
traceId: meta.traceId,
|
||
requestId: meta.requestId,
|
||
actorId: meta.actorId,
|
||
source: HWLAB_M3_IO_SKILL_NAME
|
||
};
|
||
}
|
||
|
||
async function postJson(url, body, { requestJson, timeoutMs, headers }) {
|
||
if (typeof requestJson === "function") {
|
||
try {
|
||
const result = await requestJson(url, {
|
||
method: "POST",
|
||
headers: {
|
||
accept: "application/json",
|
||
"content-type": "application/json",
|
||
...headers
|
||
},
|
||
body,
|
||
timeoutMs
|
||
});
|
||
return normalizeJsonResponse(result);
|
||
} catch (error) {
|
||
return {
|
||
ok: false,
|
||
status: 0,
|
||
body: null,
|
||
error: error instanceof Error ? error.message : String(error)
|
||
};
|
||
}
|
||
}
|
||
|
||
const controller = new AbortController();
|
||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: "POST",
|
||
headers: {
|
||
accept: "application/json",
|
||
"content-type": "application/json",
|
||
...headers
|
||
},
|
||
body: JSON.stringify(body),
|
||
signal: controller.signal
|
||
});
|
||
const text = await response.text();
|
||
const parsed = parseJsonOrNull(text);
|
||
return {
|
||
ok: response.ok,
|
||
status: response.status,
|
||
body: parsed,
|
||
error: response.ok ? null : parsed?.error?.message ?? parsed?.message ?? response.statusText
|
||
};
|
||
} catch (error) {
|
||
return {
|
||
ok: false,
|
||
status: 0,
|
||
body: null,
|
||
error: error.name === "AbortError" ? `HWLAB API request timed out after ${timeoutMs}ms` : error.message
|
||
};
|
||
} finally {
|
||
clearTimeout(timer);
|
||
}
|
||
}
|
||
|
||
function normalizeJsonResponse(result = {}) {
|
||
if (Object.hasOwn(result, "ok") || Object.hasOwn(result, "body")) {
|
||
return {
|
||
ok: result.ok ?? (Number(result.status ?? 200) >= 200 && Number(result.status ?? 200) < 300),
|
||
status: result.status ?? 200,
|
||
body: result.body ?? result.json ?? null,
|
||
error: result.error ?? result.body?.error?.message ?? null
|
||
};
|
||
}
|
||
const status = result.status ?? 200;
|
||
return {
|
||
ok: status >= 200 && status < 300,
|
||
status,
|
||
body: result.json ?? result,
|
||
error: result.error ?? null
|
||
};
|
||
}
|
||
|
||
function normalizeSkillResponse({ response, command, payload, apiTarget, traceId, requestId, actorId, startedAt, finishedAt }) {
|
||
const body = response.body ?? {};
|
||
const blocker = normalizeBlocker(body, response);
|
||
const accepted = body.accepted === true;
|
||
const status = body.status ?? (response.ok && accepted ? "succeeded" : "blocked");
|
||
const durable = durableSummary(body);
|
||
const controlReady = response.ok && accepted && status !== "blocked" && body.controlPath?.cloudApi !== false && body.controlPath?.frontendBypass !== true;
|
||
const capabilityLevel = controlReady
|
||
? HWLAB_M3_IO_CAPABILITY_LEVELS.ready
|
||
: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked;
|
||
const capabilityBlocker = controlReady ? null : normalizeCapabilityBlocker(blocker, response);
|
||
const trustBlocker = normalizeTrustBlocker(body);
|
||
const blockers = dedupeBlockers([capabilityBlocker, trustBlocker]);
|
||
|
||
return {
|
||
ok: response.ok && accepted && status !== "blocked",
|
||
service: HWLAB_M3_IO_SKILL_NAME,
|
||
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
|
||
route: HWLAB_M3_IO_API_ROUTE,
|
||
hwlabApi: apiTarget,
|
||
capabilityLevel,
|
||
controlReady,
|
||
action: command.action,
|
||
accepted,
|
||
status,
|
||
traceId: body.traceId ?? traceId,
|
||
requestId,
|
||
actorId,
|
||
operationId: body.operationId ?? null,
|
||
audit: {
|
||
auditId: body.auditId ?? body.auditState?.auditId ?? null,
|
||
status: body.auditState?.status ?? null,
|
||
durableStatus: body.auditState?.durableStatus?.status ?? body.durableStatus?.status ?? null,
|
||
summary: auditSummary(body)
|
||
},
|
||
evidence: {
|
||
evidenceId: body.evidenceId ?? body.evidenceState?.evidenceId ?? null,
|
||
status: body.evidenceState?.status ?? null,
|
||
sourceKind: body.evidenceState?.sourceKind ?? null,
|
||
blocker: body.evidenceState?.blocker ?? null,
|
||
writeStatus: body.evidenceState?.writeStatus ?? null,
|
||
summary: evidenceSummary(body)
|
||
},
|
||
durable,
|
||
blocker,
|
||
capabilityBlocker,
|
||
trustBlocker,
|
||
blockers,
|
||
readiness: {
|
||
status: controlReady ? "ready" : "blocked",
|
||
controlReady,
|
||
capabilityLevel,
|
||
route: HWLAB_M3_IO_API_ROUTE,
|
||
blocker: capabilityBlocker,
|
||
trustBlocker
|
||
},
|
||
command,
|
||
result: {
|
||
value: body.result?.value ?? body.value ?? null,
|
||
targetReadback: body.result?.targetReadback ?? null
|
||
},
|
||
controlPath: body.controlPath ?? {
|
||
cloudApi: true,
|
||
gatewaySimu: false,
|
||
boxSimu: false,
|
||
patchPanel: false,
|
||
frontendBypass: false
|
||
},
|
||
safety: {
|
||
cloudApiRouteOnly: true,
|
||
allowedRoute: HWLAB_M3_IO_API_ROUTE,
|
||
directGatewayCalls: false,
|
||
directBoxCalls: false,
|
||
directPatchPanelCalls: false,
|
||
fallbackUsed: false,
|
||
openAiFallbackUsed: false
|
||
},
|
||
httpStatus: response.status,
|
||
error: response.ok ? body.error ?? null : {
|
||
code: "hwlab_api_unavailable",
|
||
message: response.error ?? "HWLAB API request failed"
|
||
},
|
||
rawStatus: body.status ?? null,
|
||
startedAt,
|
||
finishedAt,
|
||
response: body
|
||
};
|
||
}
|
||
|
||
function blockedPayload({ traceId, requestId, apiTarget, code, message }) {
|
||
const capabilityBlocker = {
|
||
code,
|
||
message,
|
||
zh: message,
|
||
category: blockerCategory(code)
|
||
};
|
||
return {
|
||
ok: false,
|
||
service: HWLAB_M3_IO_SKILL_NAME,
|
||
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
|
||
route: HWLAB_M3_IO_API_ROUTE,
|
||
hwlabApi: apiTarget ?? null,
|
||
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked,
|
||
controlReady: false,
|
||
accepted: false,
|
||
status: "blocked",
|
||
traceId,
|
||
requestId,
|
||
operationId: null,
|
||
audit: {
|
||
auditId: null,
|
||
status: "not_written",
|
||
durableStatus: null,
|
||
summary: "blocked before HWLAB API request"
|
||
},
|
||
evidence: {
|
||
evidenceId: null,
|
||
status: "blocked",
|
||
sourceKind: "BLOCKED",
|
||
blocker: code,
|
||
writeStatus: "not_written",
|
||
summary: "blocked before HWLAB API request"
|
||
},
|
||
durable: {
|
||
status: "blocked",
|
||
blocker: code,
|
||
summary: message
|
||
},
|
||
blocker: capabilityBlocker,
|
||
capabilityBlocker,
|
||
trustBlocker: null,
|
||
blockers: [capabilityBlocker],
|
||
readiness: {
|
||
status: "blocked",
|
||
controlReady: false,
|
||
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked,
|
||
route: HWLAB_M3_IO_API_ROUTE,
|
||
blocker: capabilityBlocker,
|
||
trustBlocker: null
|
||
},
|
||
safety: {
|
||
cloudApiRouteOnly: true,
|
||
allowedRoute: HWLAB_M3_IO_API_ROUTE,
|
||
directGatewayCalls: false,
|
||
directBoxCalls: false,
|
||
directPatchPanelCalls: false,
|
||
fallbackUsed: false,
|
||
openAiFallbackUsed: false
|
||
}
|
||
};
|
||
}
|
||
|
||
function normalizeBlocker(body, response) {
|
||
const raw = body.blocker ?? body.error ?? null;
|
||
if (raw) {
|
||
const code = raw.code ?? body.blockerClassification?.code ?? "m3_io_blocked";
|
||
return {
|
||
code,
|
||
message: raw.message ?? raw.reason ?? raw.zh ?? body.blockerClassification?.reason ?? "M3 IO request was blocked.",
|
||
zh: raw.zh ?? raw.message ?? raw.reason ?? "M3 IO request was blocked.",
|
||
category: body.blockerClassification?.category ?? blockerCategory(code)
|
||
};
|
||
}
|
||
if (!response.ok) {
|
||
return {
|
||
code: "hwlab_api_unavailable",
|
||
message: response.error ?? `HWLAB API HTTP ${response.status}`,
|
||
zh: response.error ?? `HWLAB API HTTP ${response.status}`,
|
||
category: "hwlab_api"
|
||
};
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function normalizeCapabilityBlocker(blocker, response) {
|
||
if (blocker) return blocker;
|
||
return {
|
||
code: response.ok ? "hwlab_api_control_blocked" : "hwlab_api_unavailable",
|
||
message: response.ok ? "HWLAB API did not accept the M3 IO control request." : response.error ?? "HWLAB API request failed",
|
||
zh: response.ok ? "HWLAB API 未接受 M3 IO 控制请求。" : response.error ?? "HWLAB API 请求失败",
|
||
category: response.ok ? "readiness_blocked" : "hwlab_api"
|
||
};
|
||
}
|
||
|
||
function normalizeTrustBlocker(body) {
|
||
const code = body.evidenceState?.blocker ?? body.durableStatus?.blocker ?? null;
|
||
if (!code) return null;
|
||
return {
|
||
code,
|
||
message: `Durable trust is blocked: ${code}`,
|
||
zh: `可信持久化仍 blocked:${code}`,
|
||
category: blockerCategory(code),
|
||
layer: "runtime-durable",
|
||
source: body.evidenceState?.blocker ? "evidenceState.blocker" : "durableStatus.blocker"
|
||
};
|
||
}
|
||
|
||
function dedupeBlockers(blockers) {
|
||
const seen = new Set();
|
||
const result = [];
|
||
for (const blocker of blockers) {
|
||
if (!blocker?.code) continue;
|
||
const key = `${blocker.code}:${blocker.layer ?? ""}:${blocker.category ?? ""}`;
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
result.push(blocker);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function blockerCategory(code) {
|
||
const value = String(code ?? "");
|
||
if (/readiness|control_disabled|not_ready/u.test(value)) return "readiness_blocked";
|
||
if (/gateway/u.test(value)) return "gateway_unavailable";
|
||
if (/box|resource/u.test(value)) return "box_unavailable";
|
||
if (/wiring|patch_panel|patch-panel/u.test(value)) return "patch_panel_wiring_missing";
|
||
if (/durable|runtime/u.test(value)) return "durable_blocked";
|
||
if (/direct_hardware/u.test(value)) return "direct_hardware_blocked";
|
||
return "m3_io_blocked";
|
||
}
|
||
|
||
function durableSummary(body) {
|
||
const status = body.durableStatus?.status ?? body.evidenceState?.status ?? null;
|
||
const blocker = body.durableStatus?.blocker ?? body.evidenceState?.blocker ?? null;
|
||
return {
|
||
status,
|
||
durable: body.durableStatus?.durable ?? false,
|
||
blocker,
|
||
category: blocker ? blockerCategory(blocker) : null,
|
||
summary: blocker
|
||
? `durable evidence blocked: ${blocker}`
|
||
: status
|
||
? `durable status: ${status}`
|
||
: "durable status not reported"
|
||
};
|
||
}
|
||
|
||
function auditSummary(body) {
|
||
if (body.auditId) {
|
||
return `${body.auditId}; auditState=${body.auditState?.status ?? "unknown"}`;
|
||
}
|
||
return body.auditState?.reason ?? "audit not written";
|
||
}
|
||
|
||
function evidenceSummary(body) {
|
||
if (body.evidenceId) {
|
||
return `${body.evidenceId}; evidenceState=${body.evidenceState?.status ?? "unknown"}`;
|
||
}
|
||
return body.evidenceState?.reason ?? "evidence not written";
|
||
}
|
||
|
||
function helpPayload() {
|
||
return {
|
||
ok: true,
|
||
service: HWLAB_M3_IO_SKILL_NAME,
|
||
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
|
||
usage: [
|
||
"node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs m3 io --action do.write --value true --api-base-url http://127.0.0.1:6667",
|
||
"node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs m3 io --action do.write --value false --api-base-url http://127.0.0.1:6667",
|
||
"node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs m3 io --action di.read --api-base-url http://127.0.0.1:6667"
|
||
],
|
||
route: HWLAB_M3_IO_API_ROUTE,
|
||
safety: {
|
||
cloudApiRouteOnly: true,
|
||
directGatewayCalls: false,
|
||
directBoxCalls: false,
|
||
directPatchPanelCalls: false,
|
||
fallbackUsed: false
|
||
}
|
||
};
|
||
}
|
||
|
||
function readOption(args, index, name) {
|
||
const value = args[index];
|
||
if (!value || value.startsWith("--")) {
|
||
throw new Error(`${name} requires a value`);
|
||
}
|
||
return value;
|
||
}
|
||
|
||
function parseBoolean(value) {
|
||
const normalized = String(value ?? "").trim().toLowerCase();
|
||
if (["true", "1", "on", "high"].includes(normalized)) return true;
|
||
if (["false", "0", "off", "low"].includes(normalized)) return false;
|
||
return value;
|
||
}
|
||
|
||
function parseTimeout(value) {
|
||
const parsed = Number.parseInt(value, 10);
|
||
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 60000) {
|
||
throw new Error("--timeout-ms must be an integer between 1 and 60000");
|
||
}
|
||
return parsed;
|
||
}
|
||
|
||
function loopbackCloudApiBaseUrl(env = process.env) {
|
||
const port = env.HWLAB_CLOUD_API_PORT || env.PORT || "6667";
|
||
return `http://127.0.0.1:${port}`;
|
||
}
|
||
|
||
function ensureTrailingSlash(value) {
|
||
return String(value ?? "").endsWith("/") ? String(value) : `${value}/`;
|
||
}
|
||
|
||
function firstNonEmpty(...values) {
|
||
return values.find((value) => typeof value === "string" && value.trim())?.trim() ?? null;
|
||
}
|
||
|
||
function redactUrl(value) {
|
||
try {
|
||
const url = new URL(value);
|
||
url.username = "";
|
||
url.password = "";
|
||
return url.toString();
|
||
} catch {
|
||
return String(value ?? "").replace(/\/\/[^/@]+@/u, "//***@");
|
||
}
|
||
}
|
||
|
||
function parseJsonOrNull(value) {
|
||
try {
|
||
return JSON.parse(value);
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function nowIso(now) {
|
||
return typeof now === "function" ? now() : new Date().toISOString();
|
||
}
|