UNPKG

comfyui-mcp

Version:

Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),

113 lines • 6.14 kB
import { createHash, randomBytes } from "node:crypto"; import { ValidationError } from "../../utils/errors.js"; import { isAzureBlobUrl, downloadAzureBlobToFile, uploadAzureBlobFile } from "./azure-blob.js"; import { uploadHfFile } from "./hf.js"; import { isHttpUrl, uploadHttpFile } from "./http.js"; import { downloadS3ToFile, isS3Url, uploadS3File } from "./s3.js"; export function supportsCloudDownload(url) { return isS3Url(url) || isAzureBlobUrl(url); } /** * A short, stable discriminator for the EFFECTIVE cloud principal/endpoint that * WOULD serve `url` right now — the explicit `auth` MERGED with the same * environment fallbacks the real downloaders use (s3.ts makeS3Client, * azure-blob.ts blobServiceClientFromEnv). Folded into the download cache identity * (#467 P1-B) so a cache entry populated under one principal/endpoint can NEVER be * served later under different or absent credentials (cross-principal leak): if the * effective principal changes, the cache identity changes → cache miss → re-fetch. * * Secrets are hashed here and never leave this module in the clear. Empty string * for a non-cloud URL (the caller omits it from the identity). */ /** Per-process nonce folded into the S3 identity when the principal is AMBIENT * (resolved by the AWS SDK's default chain, not observable here) — so such a cache * entry is never REUSED ACROSS PROCESS RESTARTS under a possibly-different * principal (#467 P1-B). Regenerated each process start. */ const AMBIENT_CLOUD_NONCE = randomBytes(12).toString("hex"); export function cloudPrincipalKey(url, auth = {}) { let raw; if (isS3Url(url)) { const s3 = auth.s3; // makeS3Client sets config.endpoint = auth.endpoint ?? AWS_S3_ENDPOINT; when // that's unset the AWS SDK still resolves an endpoint from AWS_ENDPOINT_URL_S3 / // AWS_ENDPOINT_URL — fold ALL of them so changing the effective S3 service behind // one s3:// URL never reuses the prior endpoint's cache entry (#467 P1-B). const endpoint = [ s3?.endpoint ?? process.env.AWS_S3_ENDPOINT ?? "", process.env.AWS_ENDPOINT_URL_S3 ?? "", process.env.AWS_ENDPOINT_URL ?? "", ].join("|"); const region = s3?.region ?? process.env.AWS_REGION ?? ""; const accessKeyId = s3?.access_key_id ?? process.env.AWS_ACCESS_KEY_ID ?? ""; const secretAccessKey = s3?.secret_access_key ?? process.env.AWS_SECRET_ACCESS_KEY ?? ""; const sessionToken = s3?.session_token ?? process.env.AWS_SESSION_TOKEN ?? ""; // When an explicit/env access-key PAIR is present the principal is fully // observable → hash it and safely reuse the cache across restarts. Otherwise the // SDK resolves an AMBIENT identity (shared profiles, AWS_PROFILE, web-identity/ // role, ECS/EC2 metadata) that ISN'T observable here — fold the profile-selecting // env vars AND a per-process nonce so the entry is never reused across processes // under a possibly-different principal. (Within a process the same ambient // identity → same nonce → cache hits remain safe. Instance-role rotation // mid-process isn't distinguished — accepted, as the SDK caches creds similarly.) const explicitPair = Boolean((s3?.access_key_id ?? process.env.AWS_ACCESS_KEY_ID) && (s3?.secret_access_key ?? process.env.AWS_SECRET_ACCESS_KEY)); const ambient = explicitPair ? "" : [ "ambient", process.env.AWS_PROFILE ?? "", process.env.AWS_ROLE_ARN ?? "", process.env.AWS_WEB_IDENTITY_TOKEN_FILE ?? "", process.env.AWS_SHARED_CREDENTIALS_FILE ?? "", process.env.AWS_CONFIG_FILE ?? "", process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI ?? "", process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI ?? "", AMBIENT_CLOUD_NONCE, ].join("\n"); raw = `s3\n${endpoint}\n${region}\n${accessKeyId}\n${secretAccessKey}\n${sessionToken}\n${ambient}`; } else if (isAzureBlobUrl(url)) { // Azure download uses env creds (connection string, else account+key), else // anonymous — no hidden ambient chain like AWS, so no nonce needed. The blob host // already encodes the account (part of `url`); this is the env credential material. const conn = process.env.AZURE_STORAGE_CONNECTION_STRING ?? ""; const account = process.env.AZURE_STORAGE_ACCOUNT ?? ""; const key = process.env.AZURE_STORAGE_KEY ?? ""; raw = `azure\n${conn}\n${account}\n${key}`; } return raw ? createHash("sha256").update(raw).digest("hex").slice(0, 16) : ""; } export async function downloadCloudUrlToFile(url, targetPath, auth = {}, signal) { if (isS3Url(url)) { await downloadS3ToFile(url, targetPath, auth.s3, signal); return; } if (isAzureBlobUrl(url)) { await downloadAzureBlobToFile(url, targetPath, signal); return; } throw new ValidationError("Unsupported cloud storage download URL. Expected s3://bucket/key or an Azure Blob URL."); } export async function uploadToStorage(source, destination, auth = {}) { const keys = Object.keys(destination); if (keys.length !== 1) { throw new ValidationError("Provide exactly one upload destination: s3, azure, http, or hf."); } if ("s3" in destination) { return uploadS3File(source, destination.s3, auth.s3); } if ("azure" in destination) { return uploadAzureBlobFile(source, destination.azure); } if ("http" in destination) { if (!isHttpUrl(destination.http.url)) { throw new ValidationError("http.url must start with http:// or https://."); } return uploadHttpFile(source, destination.http); } if ("hf" in destination) { return uploadHfFile(source, destination.hf); } throw new ValidationError("Unsupported upload destination. Expected s3, azure, http, or hf."); } //# sourceMappingURL=index.js.map