openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
226 lines (225 loc) • 11.6 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { w as resolveStateDir } from "./paths-D2sRr1a_.js";
import { h as normalizeSecretInputString, s as coerceSecretRef } from "./types.secrets-kC0nOetj.js";
import { i as registerSecretValueForRedaction } from "./secret-redaction-registry-BOLC6DkF.js";
import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js";
import { s as runOpenClawStateWriteTransaction } from "./openclaw-state-db-BRTnL-D8.js";
import { i as readConfigMachineStateWithMetadata } from "./config-machine-state-BCereLZr.js";
import { t as materializeSecretInput } from "./resolve-secret-input-string-DRcwYgb7.js";
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
//#region src/node-host/gateway-cloudflare-access.ts
const CF_ACCESS_CLIENT_ID_ENV = "CF_ACCESS_CLIENT_ID";
const CF_ACCESS_CLIENT_SECRET_ENV = "CF_ACCESS_CLIENT_SECRET";
function normalizeCloudflareAccessSecretInput(value, path) {
const ref = coerceSecretRef(value);
if (ref) return ref;
const literal = normalizeSecretInputString(value);
if (literal) return literal;
throw new Error(`invalid node-host ${path}: expected a non-empty SecretInput`);
}
function normalizeNodeHostCloudflareAccessConfig(value) {
if (value === void 0 || value === null) return;
if (!isRecord(value) || Object.keys(value).length !== 2 || !("clientId" in value) || !("clientSecret" in value)) throw new Error("invalid node-host gateway.cloudflareAccess: expected clientId and clientSecret");
return {
clientId: normalizeCloudflareAccessSecretInput(value.clientId, "gateway.cloudflareAccess.clientId"),
clientSecret: normalizeCloudflareAccessSecretInput(value.clientSecret, "gateway.cloudflareAccess.clientSecret")
};
}
/** Persist conventional environment fallback as refs, never as copied plaintext. */
function nodeHostCloudflareAccessConfigFromEnv(env) {
const clientId = normalizeSecretInputString(env[CF_ACCESS_CLIENT_ID_ENV]);
const clientSecret = normalizeSecretInputString(env[CF_ACCESS_CLIENT_SECRET_ENV]);
if (!clientId && !clientSecret) return;
if (!clientId || !clientSecret) throw new Error(`${CF_ACCESS_CLIENT_ID_ENV} and ${CF_ACCESS_CLIENT_SECRET_ENV} must be configured together`);
return {
clientId: {
source: "env",
provider: "default",
id: CF_ACCESS_CLIENT_ID_ENV
},
clientSecret: {
source: "env",
provider: "default",
id: CF_ACCESS_CLIENT_SECRET_ENV
}
};
}
async function resolveNodeHostCloudflareAccess(params) {
if (!params.value) return;
const [clientId, clientSecret] = await Promise.all([materializeSecretInput({
config: params.config,
value: params.value.clientId,
env: params.env
}), materializeSecretInput({
config: params.config,
value: params.value.clientSecret,
env: params.env
})]);
if (!clientId || !clientSecret) throw new Error("node-host Cloudflare Access credentials resolved empty");
registerSecretValueForRedaction(clientId);
registerSecretValueForRedaction(clientSecret);
return {
clientId,
clientSecret
};
}
function nodeHostGatewayMatchesUrl(gateway, target) {
const host = gateway.host ?? "127.0.0.1";
const urlHost = host.includes(":") && !(host.startsWith("[") && host.endsWith("]")) ? `[${host}]` : host;
const protocol = gateway.tls ? "https:" : "http:";
const port = gateway.port ?? (gateway.tls ? 443 : 80);
const configured = new URL(`${protocol}//${urlHost}:${port}`);
return configured.protocol === target.protocol && configured.host === target.host;
}
function nodeHostGatewaysShareOrigin(left, right) {
const host = right.host ?? "127.0.0.1";
const urlHost = host.includes(":") && !(host.startsWith("[") && host.endsWith("]")) ? `[${host}]` : host;
const protocol = right.tls ? "https:" : "http:";
const port = right.port ?? (right.tls ? 443 : 80);
return nodeHostGatewayMatchesUrl(left, new URL(`${protocol}//${urlHost}:${port}`));
}
//#endregion
//#region src/node-host/config.ts
/** Canonical shared-SQLite configuration for the node-host runner. */
const NODE_HOST_CONFIG_KEY = "nodeHost.config";
const LEGACY_NODE_HOST_CONFIG_FILE = "node.json";
const LEGACY_NODE_HOST_CONFIG_CLAIM_SUFFIX = ".doctor-importing";
function databaseOptions(env) {
return { env };
}
function resolveLegacyNodeHostConfigPath(env = process.env) {
return path.join(resolveStateDir(env), LEGACY_NODE_HOST_CONFIG_FILE);
}
function resolveLegacyNodeHostConfigClaimPath(env = process.env) {
return `${resolveLegacyNodeHostConfigPath(env)}${LEGACY_NODE_HOST_CONFIG_CLAIM_SUFFIX}`;
}
function legacyPathMayExist(filePath) {
try {
fs.lstatSync(filePath);
return true;
} catch (error) {
if (error.code === "ENOENT") return false;
throw new Error(`unable to verify retired node-host state path ${filePath}`, { cause: error });
}
}
/** Runtime must not choose between canonical SQLite state and a retired file store. */
function assertNodeHostLegacyStateMigrated(env = process.env) {
const sourcePath = resolveLegacyNodeHostConfigPath(env);
const claimPath = resolveLegacyNodeHostConfigClaimPath(env);
if (!legacyPathMayExist(sourcePath) && !legacyPathMayExist(claimPath)) return;
throw new Error(`retired node-host state remains at ${sourcePath}; stop the node host and run \`openclaw doctor --fix\``);
}
function optionalNonEmptyString(value, label) {
if (value === null || value === void 0) return;
if (typeof value !== "string") throw new Error(`invalid node-host SQLite row: ${label} must be a string`);
const normalized = value.trim();
if (!normalized) throw new Error(`invalid node-host SQLite row: ${label} must not be empty`);
return normalized;
}
function optionalInputString(value) {
return value?.trim() || void 0;
}
function validatePort(value, label) {
if (value === null || value === void 0) return;
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0 || value > 65535) throw new Error(`invalid node-host ${label}: expected an integer between 1 and 65535`);
return value;
}
function normalizeStoredNodeHostConfig(value) {
if (!isRecord(value)) throw new Error("invalid node-host SQLite row: expected a configuration object");
if (value.version !== 1) throw new Error(`invalid node-host SQLite row: unsupported version ${String(value.version)}`);
const nodeId = typeof value.nodeId === "string" ? value.nodeId.trim() : "";
if (!nodeId) throw new Error("invalid node-host SQLite row: node_id must not be empty");
const storedGateway = value.gateway;
if (storedGateway !== void 0 && !isRecord(storedGateway)) throw new Error("invalid node-host SQLite row: gateway must be an object");
const gatewayTls = storedGateway?.tls;
if (gatewayTls !== void 0 && typeof gatewayTls !== "boolean") throw new Error("invalid node-host SQLite row: gateway_tls must be a boolean");
if (value.installedAppsSharing !== void 0 && typeof value.installedAppsSharing !== "boolean") throw new Error("invalid node-host SQLite row: installed_apps_sharing must be a boolean");
const gateway = storedGateway ? normalizeGatewayConfig({
host: optionalNonEmptyString(storedGateway.host, "gateway_host"),
port: validatePort(storedGateway.port, "SQLite gateway_port"),
tls: typeof gatewayTls === "boolean" ? gatewayTls : void 0,
tlsFingerprint: optionalNonEmptyString(storedGateway.tlsFingerprint, "gateway_tls_fingerprint"),
contextPath: optionalNonEmptyString(storedGateway.contextPath, "gateway_context_path"),
...cloudflareAccessEntry(normalizeNodeHostCloudflareAccessConfig(storedGateway.cloudflareAccess))
}) : void 0;
return {
version: 1,
nodeId,
displayName: optionalNonEmptyString(value.displayName, "display_name"),
gateway,
installedAppsSharing: value.installedAppsSharing === true
};
}
function cloudflareAccessEntry(cloudflareAccess) {
return cloudflareAccess ? { cloudflareAccess } : {};
}
function normalizeGatewayConfig(gateway) {
const normalized = {
host: optionalInputString(gateway.host),
port: validatePort(gateway.port, "gateway port"),
tls: gateway.tls,
tlsFingerprint: optionalInputString(gateway.tlsFingerprint),
contextPath: optionalInputString(gateway.contextPath),
...cloudflareAccessEntry(normalizeNodeHostCloudflareAccessConfig(gateway.cloudflareAccess))
};
return Object.values(normalized).some((value) => value !== void 0) ? normalized : void 0;
}
function readNodeHostConfig(env) {
const stored = readConfigMachineStateWithMetadata(NODE_HOST_CONFIG_KEY, databaseOptions(env));
if (!stored) return null;
if (!Number.isSafeInteger(stored.updatedAtMs) || stored.updatedAtMs < 0) throw new Error("invalid node-host SQLite row: updated_at_ms must be a non-negative integer");
return normalizeStoredNodeHostConfig(stored.value);
}
/** Load canonical node-host state. Legacy files block the read until Doctor migrates them. */
async function loadNodeHostConfig(env = process.env) {
assertNodeHostLegacyStateMigrated(env);
return readNodeHostConfig(env);
}
/** Load existing node-host state without creating or joining the writable shared-state lifecycle. */
async function loadNodeHostConfigReadOnly(env = process.env) {
assertNodeHostLegacyStateMigrated(env);
return readNodeHostConfig(env);
}
/**
* Atomically create or replace the complete node-host snapshot.
* Candidate facts are prepared before BEGIN; the transaction rereads the authoritative row.
*/
async function configureNodeHost(params) {
const env = params.env ?? process.env;
assertNodeHostLegacyStateMigrated(env);
const explicitNodeId = optionalInputString(params.nodeId);
const explicitDisplayName = optionalInputString(params.displayName);
const fallbackDisplayName = optionalInputString(params.fallbackDisplayName);
const candidateNodeId = params.candidateNodeId?.trim() || crypto.randomUUID();
const gateway = normalizeGatewayConfig(params.gateway);
const updatedAtMs = params.nowMs ?? Date.now();
if (!Number.isSafeInteger(updatedAtMs) || updatedAtMs < 0) throw new Error("invalid node-host updatedAtMs: expected a non-negative integer");
const config = runOpenClawStateWriteTransaction(({ db }) => {
const stateDb = getNodeSqliteKysely(db);
const stored = executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("config_machine_state").select("value_json").where("state_key", "=", NODE_HOST_CONFIG_KEY));
const existing = stored ? normalizeStoredNodeHostConfig(JSON.parse(stored.value_json)) : void 0;
const next = {
version: 1,
nodeId: explicitNodeId ?? existing?.nodeId ?? candidateNodeId,
displayName: explicitDisplayName ?? existing?.displayName ?? fallbackDisplayName,
gateway,
installedAppsSharing: params.installedAppsSharing ?? existing?.installedAppsSharing ?? false
};
const valueJson = JSON.stringify(next);
executeSqliteQuerySync(db, stateDb.insertInto("config_machine_state").values({
state_key: NODE_HOST_CONFIG_KEY,
value_json: valueJson,
updated_at_ms: updatedAtMs
}).onConflict((conflict) => conflict.column("state_key").doUpdateSet({
value_json: valueJson,
updated_at_ms: updatedAtMs
})));
return next;
}, databaseOptions(env));
assertNodeHostLegacyStateMigrated(env);
return config;
}
//#endregion
export { loadNodeHostConfig as a, nodeHostGatewayMatchesUrl as c, resolveNodeHostCloudflareAccess as d, configureNodeHost as i, nodeHostGatewaysShareOrigin as l, LEGACY_NODE_HOST_CONFIG_FILE as n, loadNodeHostConfigReadOnly as o, NODE_HOST_CONFIG_KEY as r, nodeHostCloudflareAccessConfigFromEnv as s, LEGACY_NODE_HOST_CONFIG_CLAIM_SUFFIX as t, normalizeNodeHostCloudflareAccessConfig as u };