mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor: centralize isPlainObject, isRecord, isErrno, isLoopbackHost utilities (#12926)
This commit is contained in:
@@ -10,7 +10,7 @@ import type { CliBackendConfig } from "../../config/types.js";
|
||||
import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
|
||||
import { runExec } from "../../process/exec.js";
|
||||
import { buildTtsSystemPromptHint } from "../../tts/tts.js";
|
||||
import { escapeRegExp } from "../../utils.js";
|
||||
import { escapeRegExp, isRecord } from "../../utils.js";
|
||||
import { resolveDefaultModelForAgent } from "../model-selection.js";
|
||||
import { detectRuntimeShell } from "../shell-utils.js";
|
||||
import { buildSystemPromptParams } from "../system-prompt-params.js";
|
||||
@@ -280,10 +280,6 @@ function toUsage(raw: Record<string, unknown>): CliUsage | undefined {
|
||||
return { input, output, cacheRead, cacheWrite, total };
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function collectText(value: unknown): string {
|
||||
if (!value) {
|
||||
return "";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isRecord } from "../utils.js";
|
||||
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
||||
|
||||
type MinimaxBaseResp = {
|
||||
@@ -30,10 +31,6 @@ function coerceApiHost(params: {
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function pickString(rec: Record<string, unknown>, key: string): string {
|
||||
const v = rec[key];
|
||||
return typeof v === "string" ? v : "";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { type OpenClawConfig, loadConfig } from "../config/config.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { resolveOpenClawAgentDir } from "./agent-paths.js";
|
||||
import {
|
||||
normalizeProviders,
|
||||
@@ -14,10 +15,6 @@ type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
|
||||
|
||||
const DEFAULT_MODE: NonNullable<ModelsConfig["mode"]> = "merge";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function mergeProviderModels(implicit: ProviderConfig, explicit: ProviderConfig): ProviderConfig {
|
||||
const implicitModels = Array.isArray(implicit.models) ? implicit.models : [];
|
||||
const explicitModels = Array.isArray(explicit.models) ? explicit.models : [];
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
||||
import type { ClientToolDefinition } from "./pi-embedded-runner/run/params.js";
|
||||
import { logDebug, logError } from "../logger.js";
|
||||
import { isPlainObject } from "../utils.js";
|
||||
import { runBeforeToolCallHook } from "./pi-tools.before-tool-call.js";
|
||||
import { normalizeToolName } from "./tool-policy.js";
|
||||
import { jsonResult } from "./tools/common.js";
|
||||
@@ -32,10 +33,6 @@ type ToolExecuteArgs = ToolDefinition["execute"] extends (...args: infer P) => u
|
||||
: ToolExecuteArgsCurrent;
|
||||
type ToolExecuteArgsAny = ToolExecuteArgs | ToolExecuteArgsLegacy | ToolExecuteArgsCurrent;
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isAbortSignal(value: unknown): value is AbortSignal {
|
||||
return typeof value === "object" && value !== null && "aborted" in value;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AnyAgentTool } from "./tools/common.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
|
||||
import { isPlainObject } from "../utils.js";
|
||||
import { normalizeToolName } from "./tool-policy.js";
|
||||
|
||||
type HookContext = {
|
||||
@@ -12,10 +13,6 @@ type HookOutcome = { blocked: true; reason: string } | { blocked: false; params:
|
||||
|
||||
const log = createSubsystemLogger("agents/tools");
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export async function runBeforeToolCallHook(args: {
|
||||
toolName: string;
|
||||
params: unknown;
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { CronDelivery, CronMessageChannel } from "../../cron/types.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import { normalizeCronJobCreate, normalizeCronJobPatch } from "../../cron/normalize.js";
|
||||
import { parseAgentSessionKey } from "../../sessions/session-key-utils.js";
|
||||
import { truncateUtf16Safe } from "../../utils.js";
|
||||
import { isRecord, truncateUtf16Safe } from "../../utils.js";
|
||||
import { resolveSessionAgentId } from "../agent-scope.js";
|
||||
import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
|
||||
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
|
||||
@@ -157,10 +157,6 @@ async function buildReminderContextLines(params: {
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function stripThreadSuffixFromSessionKey(sessionKey: string): string {
|
||||
const normalized = sessionKey.toLowerCase();
|
||||
const idx = normalized.lastIndexOf(":thread:");
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import WebSocket from "ws";
|
||||
import { isLoopbackHost } from "../gateway/net.js";
|
||||
import { rawDataToString } from "../infra/ws.js";
|
||||
import { getChromeExtensionRelayAuthHeaders } from "./extension-relay.js";
|
||||
|
||||
export { isLoopbackHost };
|
||||
|
||||
type CdpResponse = {
|
||||
id: number;
|
||||
result?: unknown;
|
||||
@@ -15,19 +18,6 @@ type Pending = {
|
||||
|
||||
export type CdpSendFn = (method: string, params?: Record<string, unknown>) => Promise<unknown>;
|
||||
|
||||
export function isLoopbackHost(host: string) {
|
||||
const h = host.trim().toLowerCase();
|
||||
return (
|
||||
h === "localhost" ||
|
||||
h === "127.0.0.1" ||
|
||||
h === "0.0.0.0" ||
|
||||
h === "[::1]" ||
|
||||
h === "::1" ||
|
||||
h === "[::]" ||
|
||||
h === "::"
|
||||
);
|
||||
}
|
||||
|
||||
export function getHeadersWithAuth(url: string, headers: Record<string, string> = {}) {
|
||||
const relayHeaders = getChromeExtensionRelayAuthHeaders(url);
|
||||
const mergedHeaders = { ...relayHeaders, ...headers };
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
deriveDefaultBrowserControlPort,
|
||||
DEFAULT_BROWSER_CONTROL_PORT,
|
||||
} from "../config/port-defaults.js";
|
||||
import { isLoopbackHost } from "../gateway/net.js";
|
||||
import {
|
||||
DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
DEFAULT_OPENCLAW_BROWSER_ENABLED,
|
||||
@@ -42,19 +43,6 @@ export type ResolvedBrowserProfile = {
|
||||
driver: "openclaw" | "extension";
|
||||
};
|
||||
|
||||
function isLoopbackHost(host: string) {
|
||||
const h = host.trim().toLowerCase();
|
||||
return (
|
||||
h === "localhost" ||
|
||||
h === "127.0.0.1" ||
|
||||
h === "0.0.0.0" ||
|
||||
h === "[::1]" ||
|
||||
h === "::1" ||
|
||||
h === "[::]" ||
|
||||
h === "::"
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeHexColor(raw: string | undefined) {
|
||||
const value = (raw ?? "").trim();
|
||||
if (!value) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Duplex } from "node:stream";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { createServer } from "node:http";
|
||||
import WebSocket, { WebSocketServer } from "ws";
|
||||
import { isLoopbackHost } from "../gateway/net.js";
|
||||
import { rawDataToString } from "../infra/ws.js";
|
||||
|
||||
type CdpCommand = {
|
||||
@@ -101,19 +102,6 @@ export type ChromeExtensionRelayServer = {
|
||||
stop: () => Promise<void>;
|
||||
};
|
||||
|
||||
function isLoopbackHost(host: string) {
|
||||
const h = host.trim().toLowerCase();
|
||||
return (
|
||||
h === "localhost" ||
|
||||
h === "127.0.0.1" ||
|
||||
h === "0.0.0.0" ||
|
||||
h === "[::1]" ||
|
||||
h === "::1" ||
|
||||
h === "[::]" ||
|
||||
h === "::"
|
||||
);
|
||||
}
|
||||
|
||||
function isLoopbackAddress(ip: string | undefined): boolean {
|
||||
if (!ip) {
|
||||
return false;
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { PluginOrigin } from "../../plugins/types.js";
|
||||
import type { ChannelMeta } from "./types.js";
|
||||
import { MANIFEST_KEY } from "../../compat/legacy-names.js";
|
||||
import { discoverOpenClawPlugins } from "../../plugins/discovery.js";
|
||||
import { CONFIG_DIR, resolveUserPath } from "../../utils.js";
|
||||
import { CONFIG_DIR, isRecord, resolveUserPath } from "../../utils.js";
|
||||
|
||||
export type ChannelUiMetaEntry = {
|
||||
id: string;
|
||||
@@ -61,10 +61,6 @@ const ENV_CATALOG_PATHS = ["OPENCLAW_PLUGIN_CATALOG_PATHS", "OPENCLAW_MPM_CATALO
|
||||
|
||||
type ManifestKey = typeof MANIFEST_KEY;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function parseCatalogEntries(raw: unknown): ExternalCatalogEntry[] {
|
||||
if (Array.isArray(raw)) {
|
||||
return raw.filter((entry): entry is ExternalCatalogEntry => isRecord(entry));
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { isRecord } from "../../../utils.js";
|
||||
export { isRecord };
|
||||
|
||||
export function asString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function formatMatchMetadata(params: {
|
||||
matchKey?: unknown;
|
||||
matchSource?: unknown;
|
||||
|
||||
@@ -12,14 +12,10 @@ import {
|
||||
} from "../config/config.js";
|
||||
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { resolveHomeDir } from "../utils.js";
|
||||
import { isRecord, resolveHomeDir } from "../utils.js";
|
||||
import { normalizeLegacyConfigValues } from "./doctor-legacy-config.js";
|
||||
import { autoMigrateLegacyStateDir } from "./doctor-state-migrations.js";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
type UnrecognizedKeysIssue = ZodIssue & {
|
||||
code: "unrecognized_keys";
|
||||
keys: PropertyKey[];
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isPlainObject } from "../utils.js";
|
||||
|
||||
type PathNode = Record<string, unknown>;
|
||||
|
||||
const BLOCKED_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
||||
@@ -79,12 +81,3 @@ export function getConfigValueAtPath(root: PathNode, path: string[]): unknown {
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
Object.prototype.toString.call(value) === "[object Object]"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
// Pattern for valid uppercase env var names: starts with letter or underscore,
|
||||
// followed by letters, numbers, or underscores (all uppercase)
|
||||
import { isPlainObject } from "../utils.js";
|
||||
|
||||
const ENV_VAR_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
||||
|
||||
export class MissingEnvVarError extends Error {
|
||||
@@ -34,15 +36,6 @@ export class MissingEnvVarError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
Object.prototype.toString.call(value) === "[object Object]"
|
||||
);
|
||||
}
|
||||
|
||||
function substituteString(value: string, env: NodeJS.ProcessEnv, configPath: string): string {
|
||||
if (!value.includes("$")) {
|
||||
return value;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import JSON5 from "json5";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { isPlainObject } from "../utils.js";
|
||||
|
||||
export const INCLUDE_KEY = "$include";
|
||||
export const MAX_INCLUDE_DEPTH = 10;
|
||||
@@ -52,15 +53,6 @@ export class CircularIncludeError extends ConfigIncludeError {
|
||||
// Utilities
|
||||
// ============================================================================
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
Object.prototype.toString.call(value) === "[object Object]"
|
||||
);
|
||||
}
|
||||
|
||||
/** Deep merge: arrays concatenate, objects merge recursively, primitives: source wins */
|
||||
export function deepMerge(target: unknown, source: unknown): unknown {
|
||||
if (Array.isArray(target) && Array.isArray(source)) {
|
||||
|
||||
@@ -10,8 +10,8 @@ export type LegacyConfigMigration = {
|
||||
apply: (raw: Record<string, unknown>, changes: string[]) => void;
|
||||
};
|
||||
|
||||
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
import { isRecord } from "../utils.js";
|
||||
export { isRecord };
|
||||
|
||||
export const getRecord = (value: unknown): Record<string, unknown> | null =>
|
||||
isRecord(value) ? value : null;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
type PlainObject = Record<string, unknown>;
|
||||
import { isPlainObject } from "../utils.js";
|
||||
|
||||
function isPlainObject(value: unknown): value is PlainObject {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
type PlainObject = Record<string, unknown>;
|
||||
|
||||
export function applyMergePatch(base: unknown, patch: unknown): unknown {
|
||||
if (!isPlainObject(patch)) {
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import type { OpenClawConfig } from "./types.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { isPlainObject, resolveUserPath } from "../utils.js";
|
||||
|
||||
const PATH_VALUE_RE = /^~(?=$|[\\/])/;
|
||||
|
||||
const PATH_KEY_RE = /(dir|path|paths|file|root|workspace)$/i;
|
||||
const PATH_LIST_KEYS = new Set(["paths", "pathPrepend"]);
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function normalizeStringValue(key: string | undefined, value: string): string {
|
||||
if (!PATH_VALUE_RE.test(value.trim())) {
|
||||
return value;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
listChatChannels,
|
||||
normalizeChatChannelId,
|
||||
} from "../channels/registry.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { hasAnyWhatsAppAuth } from "../web/accounts.js";
|
||||
|
||||
type PluginEnableChange = {
|
||||
@@ -36,10 +37,6 @@ const PROVIDER_PLUGIN_IDS: Array<{ pluginId: string; providerId: string }> = [
|
||||
{ pluginId: "minimax-portal-auth", providerId: "minimax-portal" },
|
||||
];
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function hasNonEmptyString(value: unknown): boolean {
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { OpenClawConfig } from "./types.js";
|
||||
import { isPlainObject } from "../utils.js";
|
||||
import { parseConfigPath, setConfigValueAtPath, unsetConfigValueAtPath } from "./config-paths.js";
|
||||
|
||||
type OverrideTree = Record<string, unknown>;
|
||||
@@ -19,15 +20,6 @@ function mergeOverrides(base: unknown, override: unknown): unknown {
|
||||
return next;
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
Object.prototype.toString.call(value) === "[object Object]"
|
||||
);
|
||||
}
|
||||
|
||||
export function getConfigOverrides(): OverrideTree {
|
||||
return overrides;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../plugins/config-state.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { validateJsonSchemaValue } from "../plugins/schema-validator.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { findDuplicateAgentDirs, formatDuplicateAgentDirError } from "./agent-dirs.js";
|
||||
import { applyAgentDefaults, applyModelDefaults, applySessionDefaults } from "./defaults.js";
|
||||
import { findLegacyConfigIssues } from "./legacy.js";
|
||||
@@ -129,10 +130,6 @@ export function validateConfigObject(
|
||||
};
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
export function validateConfigObjectWithPlugins(raw: unknown):
|
||||
| {
|
||||
ok: true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CronJobCreate, CronJobPatch } from "./types.js";
|
||||
import { sanitizeAgentId } from "../routing/session-key.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { parseAbsoluteTimeMs } from "./parse.js";
|
||||
import { migrateLegacyCronPayload } from "./payload-migration.js";
|
||||
import { inferLegacyName } from "./service/normalize.js";
|
||||
@@ -14,10 +15,6 @@ const DEFAULT_OPTIONS: NormalizeOptions = {
|
||||
applyDefaults: false,
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is UnknownRecord {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function coerceSchedule(schedule: UnknownRecord) {
|
||||
const next: UnknownRecord = { ...schedule };
|
||||
const rawKind = typeof schedule.kind === "string" ? schedule.kind.trim().toLowerCase() : "";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { DiscordGuildChannelConfig, DiscordGuildEntry } from "../config/types.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { resolveDiscordAccount } from "./accounts.js";
|
||||
import { fetchChannelPermissionsDiscord } from "./send.js";
|
||||
|
||||
@@ -22,10 +23,6 @@ export type DiscordChannelPermissionsAudit = {
|
||||
|
||||
const REQUIRED_CHANNEL_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function shouldAuditChannelConfig(config: DiscordGuildChannelConfig | undefined) {
|
||||
if (!config) {
|
||||
return true;
|
||||
|
||||
@@ -2,6 +2,7 @@ import chokidar from "chokidar";
|
||||
import type { OpenClawConfig, ConfigFileSnapshot, GatewayReloadMode } from "../config/config.js";
|
||||
import { type ChannelId, listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { isPlainObject } from "../utils.js";
|
||||
|
||||
export type GatewayReloadSettings = {
|
||||
mode: GatewayReloadMode;
|
||||
@@ -126,15 +127,6 @@ function matchRule(path: string): ReloadRule | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
!Array.isArray(value) &&
|
||||
Object.prototype.toString.call(value) === "[object Object]",
|
||||
);
|
||||
}
|
||||
|
||||
export function diffConfigPaths(prev: unknown, next: unknown, prefix = ""): string[] {
|
||||
if (prev === next) {
|
||||
return [];
|
||||
|
||||
@@ -255,6 +255,20 @@ function isValidIPv4(host: string): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a hostname or IP refers to the local machine.
|
||||
* Handles: localhost, 127.x.x.x, ::1, [::1], ::ffff:127.x.x.x
|
||||
* Note: 0.0.0.0 and :: are NOT loopback - they bind to all interfaces.
|
||||
*/
|
||||
export function isLoopbackHost(host: string): boolean {
|
||||
return isLoopbackAddress(host);
|
||||
if (!host) {
|
||||
return false;
|
||||
}
|
||||
const h = host.trim().toLowerCase();
|
||||
if (h === "localhost") {
|
||||
return true;
|
||||
}
|
||||
// Handle bracketed IPv6 addresses like [::1]
|
||||
const unbracket = h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
|
||||
return isLoopbackAddress(unbracket);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isLoopbackHost } from "./net.js";
|
||||
|
||||
type OriginCheckResult = { ok: true } | { ok: false; reason: string };
|
||||
|
||||
function normalizeHostHeader(hostHeader?: string): string {
|
||||
@@ -38,22 +40,6 @@ function parseOrigin(
|
||||
}
|
||||
}
|
||||
|
||||
function isLoopbackHost(hostname: string): boolean {
|
||||
if (!hostname) {
|
||||
return false;
|
||||
}
|
||||
if (hostname === "localhost") {
|
||||
return true;
|
||||
}
|
||||
if (hostname === "::1") {
|
||||
return true;
|
||||
}
|
||||
if (hostname === "127.0.0.1" || hostname.startsWith("127.")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function checkBrowserOrigin(params: {
|
||||
requestHost?: string;
|
||||
origin?: string;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isLoopbackHost } from "../gateway/net.js";
|
||||
|
||||
type HostSource = string | null | undefined;
|
||||
|
||||
type CanvasHostUrlParams = {
|
||||
@@ -9,23 +11,6 @@ type CanvasHostUrlParams = {
|
||||
scheme?: "http" | "https";
|
||||
};
|
||||
|
||||
const isLoopbackHost = (value: string) => {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (normalized === "localhost") {
|
||||
return true;
|
||||
}
|
||||
if (normalized === "::1") {
|
||||
return true;
|
||||
}
|
||||
if (normalized === "0.0.0.0" || normalized === "::") {
|
||||
return true;
|
||||
}
|
||||
return normalized.startsWith("127.");
|
||||
};
|
||||
|
||||
const normalizeHost = (value: HostSource, rejectLoopback: boolean) => {
|
||||
if (!value) {
|
||||
return "";
|
||||
|
||||
@@ -12,6 +12,20 @@ export function extractErrorCode(err: unknown): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for NodeJS.ErrnoException (any error with a `code` property).
|
||||
*/
|
||||
export function isErrno(err: unknown): err is NodeJS.ErrnoException {
|
||||
return Boolean(err && typeof err === "object" && "code" in err);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error has a specific errno code.
|
||||
*/
|
||||
export function hasErrnoCode(err: unknown, code: string): boolean {
|
||||
return isErrno(err) && err.code === code;
|
||||
}
|
||||
|
||||
export function formatErrorMessage(err: unknown): string {
|
||||
if (err instanceof Error) {
|
||||
return err.message || err.name || "Error";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import net from "node:net";
|
||||
import type { PortListener, PortUsage, PortUsageStatus } from "./ports-types.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { isErrno } from "./errors.js";
|
||||
import { buildPortHints } from "./ports-format.js";
|
||||
import { resolveLsofCommand } from "./ports-lsof.js";
|
||||
|
||||
@@ -11,10 +12,6 @@ type CommandResult = {
|
||||
error?: string;
|
||||
};
|
||||
|
||||
function isErrno(err: unknown): err is NodeJS.ErrnoException {
|
||||
return Boolean(err && typeof err === "object" && "code" in err);
|
||||
}
|
||||
|
||||
async function runCommandSafe(argv: string[], timeoutMs = 5_000): Promise<CommandResult> {
|
||||
try {
|
||||
const res = await runCommandWithTimeout(argv, { timeoutMs });
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { PortListener, PortListenerKind, PortUsage, PortUsageStatus } from
|
||||
import { danger, info, shouldLogVerbose, warn } from "../globals.js";
|
||||
import { logDebug } from "../logger.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { isErrno } from "./errors.js";
|
||||
import { formatPortDiagnostics } from "./ports-format.js";
|
||||
import { inspectPortUsage } from "./ports-inspect.js";
|
||||
|
||||
@@ -19,10 +20,6 @@ class PortInUseError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function isErrno(err: unknown): err is NodeJS.ErrnoException {
|
||||
return Boolean(err && typeof err === "object" && "code" in err);
|
||||
}
|
||||
|
||||
export async function describePortOwner(port: number): Promise<string | undefined> {
|
||||
const diagnostics = await inspectPortUsage(port);
|
||||
if (diagnostics.listeners.length === 0) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { fetchJson } from "./provider-usage.fetch.shared.js";
|
||||
import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js";
|
||||
|
||||
@@ -148,10 +149,6 @@ const WINDOW_MINUTE_KEYS = [
|
||||
"minutes",
|
||||
] as const;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function pickNumber(record: Record<string, unknown>, keys: readonly string[]): number | undefined {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import net from "node:net";
|
||||
import { isErrno } from "./errors.js";
|
||||
import { ensurePortAvailable } from "./ports.js";
|
||||
|
||||
export type SshParsedTarget = {
|
||||
@@ -17,10 +18,6 @@ export type SshTunnel = {
|
||||
stop: () => Promise<void>;
|
||||
};
|
||||
|
||||
function isErrno(err: unknown): err is NodeJS.ErrnoException {
|
||||
return Boolean(err && typeof err === "object" && "code" in err);
|
||||
}
|
||||
|
||||
export function parseSshTarget(raw: string): SshParsedTarget | null {
|
||||
const trimmed = raw.trim().replace(/^ssh\s+/, "");
|
||||
if (!trimmed) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { PluginConfigUiHint, PluginKind } from "./types.js";
|
||||
import { MANIFEST_KEY } from "../compat/legacy-names.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
|
||||
export const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json";
|
||||
export const PLUGIN_MANIFEST_FILENAMES = [PLUGIN_MANIFEST_FILENAME] as const;
|
||||
@@ -30,10 +31,6 @@ function normalizeStringList(value: unknown): string[] {
|
||||
return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
export function resolvePluginManifestPath(rootDir: string): string {
|
||||
for (const filename of PLUGIN_MANIFEST_FILENAMES) {
|
||||
const candidate = path.join(rootDir, filename);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { hasErrnoCode } from "../infra/errors.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -52,16 +53,6 @@ export function isScannable(filePath: string): boolean {
|
||||
return SCANNABLE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
||||
}
|
||||
|
||||
function isErrno(err: unknown, code: string): boolean {
|
||||
if (!err || typeof err !== "object") {
|
||||
return false;
|
||||
}
|
||||
if (!("code" in err)) {
|
||||
return false;
|
||||
}
|
||||
return (err as { code?: unknown }).code === code;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rule definitions
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -327,7 +318,7 @@ async function resolveForcedFiles(params: {
|
||||
try {
|
||||
st = await fs.stat(includePath);
|
||||
} catch (err) {
|
||||
if (isErrno(err, "ENOENT")) {
|
||||
if (hasErrnoCode(err, "ENOENT")) {
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
@@ -374,7 +365,7 @@ async function readScannableSource(filePath: string, maxFileBytes: number): Prom
|
||||
try {
|
||||
st = await fs.stat(filePath);
|
||||
} catch (err) {
|
||||
if (isErrno(err, "ENOENT")) {
|
||||
if (hasErrnoCode(err, "ENOENT")) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
@@ -385,7 +376,7 @@ async function readScannableSource(filePath: string, maxFileBytes: number): Prom
|
||||
try {
|
||||
return await fs.readFile(filePath, "utf-8");
|
||||
} catch (err) {
|
||||
if (isErrno(err, "ENOENT")) {
|
||||
if (hasErrnoCode(err, "ENOENT")) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { WebClient } from "@slack/web-api";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { createSlackWebClient } from "./client.js";
|
||||
|
||||
export type SlackScopesResult = {
|
||||
@@ -10,10 +11,6 @@ export type SlackScopesResult = {
|
||||
|
||||
type SlackScopesSource = "auth.scopes" | "apps.permissions.info";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function collectScopes(value: unknown, into: string[]) {
|
||||
if (!value) {
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TelegramGroupConfig } from "../config/types.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { makeProxyFetch } from "./proxy.js";
|
||||
|
||||
const TELEGRAM_API_BASE = "https://api.telegram.org";
|
||||
@@ -38,10 +39,6 @@ async function fetchWithTimeout(
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function collectTelegramUnmentionedGroupIds(
|
||||
groups: Record<string, TelegramGroupConfig> | undefined,
|
||||
) {
|
||||
|
||||
21
src/utils.ts
21
src/utils.ts
@@ -42,6 +42,27 @@ export function safeParseJson<T>(raw: string): T | null {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for plain objects (not arrays, null, Date, RegExp, etc.).
|
||||
* Uses Object.prototype.toString for maximum safety.
|
||||
*/
|
||||
export function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
Object.prototype.toString.call(value) === "[object Object]"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for Record<string, unknown> (less strict than isPlainObject).
|
||||
* Accepts any non-null object that isn't an array.
|
||||
*/
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export type WebChannel = "web";
|
||||
|
||||
export function assertWebChannel(input: string): asserts input is WebChannel {
|
||||
|
||||
Reference in New Issue
Block a user