mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
71a33f07a9
* fix(imessage): harden remote Mac transport Route SSH-backed iMessage actions through JSON-RPC, preserve remote database paths, and stage outbound files on the Messages Mac with bounded cleanup. Keep local action semantics intact while failing closed on ambiguous wrappers and surfacing the remaining imsg v0.13.4 limits. * fix(imessage): remove test-only exports
152 lines
4.5 KiB
TypeScript
152 lines
4.5 KiB
TypeScript
// Imessage plugin module classifies CLI and Messages database locality.
|
|
import { constants, accessSync, readFileSync, realpathSync } from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
const localCliPathCache = new Map<string, boolean>();
|
|
const MACH_O_MAGICS = new Set([
|
|
"feedface",
|
|
"feedfacf",
|
|
"cefaedfe",
|
|
"cffaedfe",
|
|
"cafebabe",
|
|
"cafebabf",
|
|
"bebafeca",
|
|
"bfbafeca",
|
|
]);
|
|
|
|
export function resolveIMessageHomeDir(): string | undefined {
|
|
const configuredHome = process.env.HOME;
|
|
const home = configuredHome?.trim();
|
|
if (home) {
|
|
return home;
|
|
}
|
|
try {
|
|
// On POSIX, os.homedir() echoes a defined blank HOME instead of querying the account.
|
|
const systemHome = configuredHome === undefined ? os.homedir() : os.userInfo().homedir;
|
|
return systemHome.trim() || undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export function expandIMessageUserPath(value: string): string {
|
|
if (!value.startsWith("~")) {
|
|
return value;
|
|
}
|
|
const home = resolveIMessageHomeDir();
|
|
return home ? value.replace(/^~(?=$|[\\/])/, home) : value;
|
|
}
|
|
|
|
function resolveIMessageExecutable(cliPath: string): string | undefined {
|
|
const expanded = expandIMessageUserPath(cliPath);
|
|
if (expanded.includes(path.sep)) {
|
|
return expanded;
|
|
}
|
|
for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
if (!directory) {
|
|
continue;
|
|
}
|
|
const candidate = path.join(directory, expanded);
|
|
try {
|
|
accessSync(candidate, constants.X_OK);
|
|
return candidate;
|
|
} catch {
|
|
// Continue through PATH until an executable candidate is found.
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function isMachOExecutable(filePath: string): boolean {
|
|
try {
|
|
return MACH_O_MAGICS.has(readFileSync(realpathSync(filePath)).subarray(0, 4).toString("hex"));
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isProvenLocalIMessageCliPath(params: { cliPath: string; remoteHost?: string }): boolean {
|
|
if (params.remoteHost?.trim()) {
|
|
return false;
|
|
}
|
|
const cliPath = params.cliPath.trim();
|
|
const cacheKey = `${cliPath}\0${process.env.PATH ?? ""}`;
|
|
const cached = localCliPathCache.get(cacheKey);
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
const executable = resolveIMessageExecutable(cliPath);
|
|
let local = executable ? isMachOExecutable(executable) : false;
|
|
if (executable && !local) {
|
|
try {
|
|
const wrapper = readFileSync(realpathSync(executable), "utf8");
|
|
const match = wrapper.match(
|
|
/^#![^\r\n]+\r?\n\s*exec\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s+"\$@"\s*$/u,
|
|
);
|
|
const target = match?.[1] ?? match?.[2] ?? match?.[3];
|
|
local = Boolean(target && path.isAbsolute(target) && isMachOExecutable(target));
|
|
} catch {
|
|
local = false;
|
|
}
|
|
}
|
|
// CLI installation is process-stable channel metadata; avoid repeated file reads.
|
|
localCliPathCache.set(cacheKey, local);
|
|
return local;
|
|
}
|
|
|
|
function isLikelyLocalIMessageCliPath(params: { cliPath: string; remoteHost?: string }): boolean {
|
|
if (params.remoteHost?.trim()) {
|
|
return false;
|
|
}
|
|
const cliPath = params.cliPath.trim();
|
|
if (cliPath === "imsg") {
|
|
return true;
|
|
}
|
|
if (path.basename(cliPath) !== "imsg") {
|
|
return false;
|
|
}
|
|
try {
|
|
return !/\bssh\b[\s\S]*\bimsg\b/u.test(readFileSync(expandIMessageUserPath(cliPath), "utf8"));
|
|
} catch {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function defaultMessagesDbPath(): string | undefined {
|
|
const home = resolveIMessageHomeDir();
|
|
return home ? path.join(home, "Library", "Messages", "chat.db") : undefined;
|
|
}
|
|
|
|
export function resolveIMessageChatDbLookupPath(params: {
|
|
cliPath: string;
|
|
dbPath?: string;
|
|
remoteHost?: string;
|
|
}): string | undefined {
|
|
if (params.remoteHost?.trim()) {
|
|
return undefined;
|
|
}
|
|
const configured = params.dbPath?.trim();
|
|
if (configured) {
|
|
return expandIMessageUserPath(configured);
|
|
}
|
|
// Receipt recovery is best effort and preserves the shipped wrapper heuristic.
|
|
if (!isLikelyLocalIMessageCliPath({ cliPath: params.cliPath, remoteHost: params.remoteHost })) {
|
|
return undefined;
|
|
}
|
|
return defaultMessagesDbPath();
|
|
}
|
|
|
|
export function resolveLocalIMessageChatDbPath(params: {
|
|
cliPath: string;
|
|
dbPath?: string;
|
|
remoteHost?: string;
|
|
}): string | undefined {
|
|
// Authorization may use chat.db only after positively attesting a local imsg executable.
|
|
if (!isProvenLocalIMessageCliPath({ cliPath: params.cliPath, remoteHost: params.remoteHost })) {
|
|
return undefined;
|
|
}
|
|
const configured = params.dbPath?.trim();
|
|
return configured ? expandIMessageUserPath(configured) : defaultMessagesDbPath();
|
|
}
|