Files
MachinePainting 09362f42c7 v1.1.0: refactor Setup Node, security hardening, auto-curl auth flow
Setup Node refactor (the secret-bearing fields no longer exist on the
node at all):
  • Removed app_key / app_secret / auth_code from INPUT_TYPES. The
    Setup Node now exposes only storage_method, encryption_key_method,
    and reconnect — none of which are credentials.
  • Added a "Set credentials..." button on the node (LiteGraph button
    widget installed via beforeRegisterNodeDef + onNodeCreated
    prototype hook so it survives reload, drag-and-drop workflow
    JSON, and PNG metadata restore).
  • Click opens a browser-only modal with three <input type=password>
    fields. Browser-native dot masking the whole time the user types
    or pastes — no canvas-rendered plaintext, no shoulder-surf.
  • Modal Save POSTs values to a new same-origin route
    /dropsend/setup/stash and closes. setup() consumes the stash
    entry on the next Queue (one-shot, keyed by client_id, 60-second
    TTL, 32-entry cap with oldest-eviction).
  • Architectural consequence: secrets never become widget values, so
    they cannot enter saved workflow JSON, ComfyUI's localStorage
    auto-save, PNG metadata, copy-pasted nodes, or PromptServer.history
    (and therefore the unauthenticated /history HTTP endpoint).

Auto-curl auth flow (one-click second paste):
  • After Run 1 prints the OAuth URL banner, setup() sends a
    sid-targeted dropsend_credentials_needed WebSocket event.
  • JS handler auto-opens the entry modal in the originating browser
    so the user can paste auth_code as soon as Dropbox shows it,
    without clicking the button between runs.
  • Defense in depth: client_id is echoed in the WS payload and the
    JS handler verifies it matches api.clientId before opening.

Security hardening on /dropsend/setup/stash:
  • Same-origin enforcement: Origin header must match request.host.
    Closes cross-origin CSRF from another browser tab even when an
    attacker has sniffed a victim's client_id.
  • Content-Type lockdown: rejects anything other than
    application/json with HTTP 415. Closes the confused-deputy class
    where form-encoded bodies could be parsed as JSON.
  • Live-session check: client_id must correspond to an active
    PromptServer.instance.sockets entry. Returns 403 otherwise.
    Closes the stash-poisoning vector where an attacker who guesses
    a victim's client_id could pre-populate credentials.
  • Per-IP rate limit: 30 POSTs / 10-second sliding window, returns
    429 over budget. Defense against stash-spam DoS attempting to
    evict a legitimate user's entry.
  • Body cap (32 KB), client_id regex validation, JSON parse errors
    return 400, empty payloads dropped.
  • Both rate-limit and live-session checks empirically verified via
    curl smoke tests.

Reconnect now revokes the Dropbox refresh token at the source:
  • reconnect=True populates DropboxAuthManager from os.environ and
    calls reset(revoke_token=True).
  • POST /2/auth/token/revoke with an 8-second timeout so a Dropbox
    outage cannot hang the operator mid-reconnect. Local cleanup
    proceeds regardless of revocation outcome.
  • Revocation outcome is now logged at logger.error level (not just
    print) and the failure message tells the operator to manually
    disconnect at https://www.dropbox.com/account/connected_apps so
    a silent revocation failure cannot leave a still-valid token at
    Dropbox.

Logging:
  • Single FileHandler attached to the root logger from __init__.py,
    pointed at a plugin-relative dropsend.log. Replaces the four
    duplicated logging.basicConfig() blocks in monitor_output.py /
    encrypt_file.py / dropbox_upload.py / dropsend_uploader_node.py
    that were dead code (basicConfig is a no-op once ComfyUI has
    already configured root). Log records from every DropSend module
    now actually persist to disk.
  • Race-free creation: pre-create with os.open(O_CREAT | O_WRONLY |
    O_APPEND, 0o600) before FileHandler attaches, follow-up chmod as
    belt-and-suspenders. Closes the brief default-umask window
    between FileHandler creating the file and post-hoc chmod.
  • Rotation via RotatingFileHandler (5 MB per file, 3 backups, ~20
    MB max). Bounds disk use on long-lived hosts.
  • dropbox_auth_manager.py now has its own logger so revocation
    successes/failures land in dropsend.log alongside stdout.

Tests (tests/ directory, unittest, no extra deps):
  • test_safe_paths.py — 23 tests covering symlink rejection, path-
    traversal rejection, allowlist behaviour, dropbox_dest validation
    (NUL bytes, missing leading slash, parent-traversal segments),
    and resolve_safe_watch_folder edge cases. safe_paths is the only
    barrier between remote workflow inputs and arbitrary-file-read
    on the host; this is regression coverage for that barrier.
  • test_setup_node_security.py — static AST analysis of
    dropsend_setup_node.py asserting that every send_sync call for a
    credential event passes a non-None sid AND sits inside an
    "if sid:" guard. Catches future regressions of the original
    WebSocket-broadcast vulnerability deterministically. Also asserts
    INPUT_TYPES never re-introduces app_key/app_secret/auth_code.

WebSocket-broadcast guard maintained on all three event sites
(dropsend_credentials_ready, dropbox_reconnect_complete,
dropsend_credentials_needed) — refuse to send when client_id is None
to prevent send_sync(..., None) from broadcasting credentials to
every connected browser tab.

README:
  • New Changelog section documenting the v1.1.0 security advisory
    (workflow-persistence + WS-broadcast leaks in earlier versions)
    and the fixes shipped in this release.
  • New "Bind ComfyUI to localhost or front it with auth" Best
    Practice item (item 4) explicitly calling out that ComfyUI's
    /prompt endpoint is unauthenticated.
  • New "Encryption key rotation" subsection covering symmetric-key
    implications, leaked-key recovery, and how to generate a new key.
  • New "Uninstalling / disconnecting" subsection with step-by-step
    instructions for revoking Dropbox app, resetting app secret,
    removing platform secrets, and handling existing .enc files.
  • Threat model reflects same-origin/Content-Type checks, log
    rotation, plugin-directory metadata-leak note for multi-tenant
    hosts, reconnect-revocation silent-failure note, and Dropbox
    SDK error-message log review.

.gitignore:
  • Expanded to cover *.bak, *.enc, *.key/.pem/.p12/.pfx, .idea/,
    .pytest_cache/, build artifacts, Thumbs.db/Desktop.ini.

Bundled in this minor release:
  • Single-Observer fix for the macOS _fsevents conflict that
    previously prevented .enc files from being picked up by the
    upload watcher.
  • folder_format dropdown for AutoUploader (project_name default
    plus five worldwide date formats), with two-base persistence so
    switching between project-name and dated modes preserves both
    patterns.
  • RUNNING banner with per-setting plain-English explanations.
  • Branded watching/encrypted/sent log emojis.
  • Per-shell copy-paste commands and echo verification for the
    COMFYUI_DROPSEND_ALLOW_SETUP gate.
2026-05-09 05:44:47 -07:00

583 lines
26 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// dropbox_oauth.js — ComfyUI extension for the DropSend Setup Node.
//
// Architecture summary
// --------------------
// The Setup Node deliberately has NO secret-bearing inputs in its
// Python INPUT_TYPES. app_key, app_secret, and auth_code are entered
// only via a browser-only modal launched from the "Set credentials…"
// button this extension installs on the node. The modal POSTs the
// values directly to /dropsend/setup/stash (a same-origin route
// registered by dropsend_setup_node.py) and closes. setup() consumes
// the stash entry on the next Queue.
//
// What this guarantees
// • Secrets never enter any LiteGraph widget value, so they cannot
// be serialized to workflow JSON, ComfyUI's localStorage auto-save,
// PNG metadata, copy-pasted nodes, or PromptServer.history /
// /history.
// • Browser-native masking (input type=password) covers shoulder-
// surfing during typing.
// • Modal closes after Save; no in-process JS cache. User pastes
// fresh values each time the modal opens — they don't persist
// across modal sessions.
//
// Auto-trigger UX
// ---------------
// After Run 1 prints the OAuth URL banner, dropsend_setup_node.py
// sends a "dropsend_credentials_needed" WebSocket event to the
// originating client_id only. Receiving that event auto-opens the
// entry modal so the user can paste the auth_code (plus re-paste
// app_key/app_secret) without clicking the button between runs.
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
// ---------------------------------------------------------------------------
// Constants and small helpers
// ---------------------------------------------------------------------------
// Node-type registered in NODE_CLASS_MAPPINGS in dropsend_setup_node.py.
const SETUP_NODE_TYPE = "DropSendSetup";
// Track OAuth popup window so a second click reuses/replaces the first.
let oauthPopup = null;
// Find every Setup Node currently in the graph (typically just one).
function findSetupNodes() {
try {
return app.graph.nodes.filter(n => n.type === SETUP_NODE_TYPE);
} catch (e) {
console.warn("[DropSendNode] Could not enumerate graph nodes:", e);
return [];
}
}
// ---------------------------------------------------------------------------
// "Set credentials…" button widget on the Setup Node
// ---------------------------------------------------------------------------
// Add the button widget to a Setup Node instance. Idempotent — safe
// to call repeatedly from any code path (prototype.onNodeCreated,
// loadedGraphNode, setup-time sweep) without producing duplicate
// buttons.
//
// Critical detail: after addWidget the node's bounding box must be
// recomputed via setSize(computeSize()), otherwise the new widget is
// drawn outside the existing draw area and looks "missing." This is
// the failure mode that hit when nodeCreated installed the button
// after ComfyUI had already laid out the node for its three other
// widgets.
function ensureCredentialButton(node) {
if (!node || node.type !== SETUP_NODE_TYPE) return;
if (node.__dropsendButtonAdded) return;
if (typeof node.addWidget !== "function") {
console.warn("[DropSendNode] node.addWidget unavailable; cannot install Set credentials button");
return;
}
node.addWidget("button", "Set credentials…", null, () => {
openCredentialEntryModal({ reason: "user-clicked" });
});
node.__dropsendButtonAdded = true;
try {
if (typeof node.computeSize === "function" && typeof node.setSize === "function") {
node.setSize(node.computeSize());
}
} catch (e) {
console.warn("[DropSendNode] could not recompute node size after adding button:", e);
}
try { app.graph.setDirtyCanvas(true, true); } catch (_) {}
console.log(`[DropSendNode] Installed Set-credentials button on ${SETUP_NODE_TYPE}`);
}
// ---------------------------------------------------------------------------
// Credential ENTRY modal (the new input surface)
// ---------------------------------------------------------------------------
//
// This is the modal the user types secrets INTO. Distinct from
// showCredentialModal below, which is the OUTPUT modal that displays
// the four delivered credentials at the end of a successful Setup run.
function openCredentialEntryModal({ reason }) {
// If an entry modal is already open (e.g. user clicked the button
// twice, or auto-trigger fired while one was already open), just
// bring it to focus instead of stacking.
const existing = document.getElementById("dropsend-credentials-entry-modal");
if (existing) {
const firstInput = existing.querySelector("input");
if (firstInput) firstInput.focus();
return;
}
const overlay = document.createElement("div");
overlay.id = "dropsend-credentials-entry-modal";
overlay.style.cssText = [
"position:fixed", "inset:0",
"background:rgba(0,0,0,0.7)",
"z-index:10001",
"display:flex", "align-items:center", "justify-content:center",
'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif'
].join(";");
const dialog = document.createElement("div");
dialog.style.cssText = [
"background:#1e1e1e", "color:#eaeaea",
"border-radius:12px", "padding:24px",
"max-width:520px", "width:90%",
"box-shadow:0 20px 60px rgba(0,0,0,0.5)",
"box-sizing:border-box"
].join(";");
const header = document.createElement("div");
header.style.cssText = "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px";
const title = document.createElement("h2");
title.textContent = "Set DropSend Credentials";
title.style.cssText = "margin:0;font-size:1.2rem;color:#fff";
const closeX = document.createElement("button");
closeX.textContent = "×";
closeX.setAttribute("aria-label", "Close");
closeX.style.cssText = "background:none;border:none;color:#999;font-size:1.5rem;cursor:pointer;padding:0 8px;line-height:1";
closeX.onclick = () => overlay.remove();
header.appendChild(title);
header.appendChild(closeX);
dialog.appendChild(header);
const intro = document.createElement("p");
intro.style.cssText = "color:#bbb;font-size:0.85rem;line-height:1.5;margin:0 0 16px 0";
intro.textContent =
reason === "auto-after-oauth-url"
? "Authorization URL printed in the ComfyUI terminal. Authorize at Dropbox, copy the auth code, then paste app_key + app_secret + auth_code below and click Save."
: "Paste your Dropbox app_key and app_secret. Leave auth_code empty for now; you'll paste it later after authorizing at Dropbox. Values are sent to the server (browser only, never written to disk in display_only mode) and are NOT saved by ComfyUI in any workflow JSON, PNG metadata, or browser cache.";
dialog.appendChild(intro);
// Three password inputs.
const fields = [
{ name: "app_key", label: "App Key" },
{ name: "app_secret", label: "App Secret" },
{ name: "auth_code", label: "Auth Code (paste after authorizing at Dropbox)" },
];
const inputs = {};
fields.forEach(({ name, label }) => {
const row = document.createElement("div");
row.style.cssText = "margin-bottom:12px";
const lbl = document.createElement("label");
lbl.textContent = label;
lbl.style.cssText = "display:block;font-size:0.8rem;color:#aaa;margin-bottom:4px;letter-spacing:0.02em";
row.appendChild(lbl);
const input = document.createElement("input");
input.type = "password";
input.autocomplete = "off";
input.spellcheck = false;
input.value = "";
input.style.cssText = "width:100%;box-sizing:border-box;padding:8px;font-family:Menlo,Monaco,monospace;font-size:0.9rem;background:#2a2a2a;color:#eaeaea;border:1px solid #444;border-radius:4px";
row.appendChild(input);
dialog.appendChild(row);
inputs[name] = input;
});
// Status line for inline error messages from the Save handler.
const status = document.createElement("p");
status.style.cssText = "color:#f87171;font-size:0.8rem;margin:6px 0 0 0;min-height:1em";
status.textContent = "";
dialog.appendChild(status);
const footer = document.createElement("div");
footer.style.cssText = "display:flex;gap:10px;margin-top:16px;padding-top:12px;border-top:1px solid #333";
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "Cancel";
cancelBtn.style.cssText = "flex:1;padding:9px;background:#444;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:0.9rem";
cancelBtn.onclick = () => overlay.remove();
const saveBtn = document.createElement("button");
saveBtn.textContent = "Save";
saveBtn.style.cssText = "flex:1;padding:9px;background:#3b82f6;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:0.9rem";
saveBtn.onclick = async () => {
const payload = {
app_key: inputs.app_key.value || "",
app_secret: inputs.app_secret.value || "",
auth_code: inputs.auth_code.value || "",
};
if (!payload.app_key && !payload.app_secret && !payload.auth_code) {
status.textContent = "Nothing to save. Paste at least app_key + app_secret.";
return;
}
const clientId = api && api.clientId ? api.clientId : null;
if (!clientId) {
status.textContent = "Browser not yet connected to ComfyUI. Wait a moment and try again.";
return;
}
saveBtn.disabled = true;
cancelBtn.disabled = true;
try {
const resp = await fetch("/dropsend/setup/stash", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ client_id: clientId, ...payload }),
});
if (!resp.ok) throw new Error("server returned " + resp.status);
} catch (e) {
console.error("[DropSendNode] Stash POST failed:", e);
status.textContent = "Could not save: " + e.message;
saveBtn.disabled = false;
cancelBtn.disabled = false;
return;
}
// Wipe the input values from DOM the moment Save succeeds —
// even though the modal is about to be removed, we don't want
// the values to sit in input.value while the GC catches up.
Object.values(inputs).forEach(i => { i.value = ""; });
overlay.remove();
console.log("[DropSendNode] Stashed credentials via entry modal; click Queue to run setup");
};
footer.appendChild(cancelBtn);
footer.appendChild(saveBtn);
dialog.appendChild(footer);
// Submit on Enter from any input.
Object.values(inputs).forEach(input => {
input.addEventListener("keydown", (ev) => {
if (ev.key === "Enter") { ev.preventDefault(); saveBtn.click(); }
if (ev.key === "Escape") { ev.preventDefault(); cancelBtn.click(); }
});
});
overlay.appendChild(dialog);
document.body.appendChild(overlay);
// Auto-focus the first empty input — usually app_key on initial
// open, auth_code if app_key is already filled (no longer applies
// here since we don't pre-fill, but the same logic is harmless).
setTimeout(() => {
const firstEmpty = fields.map(f => inputs[f.name]).find(i => !i.value);
if (firstEmpty) firstEmpty.focus();
}, 0);
}
// ---------------------------------------------------------------------------
// OAuth popup window (delegate to Dropbox)
// ---------------------------------------------------------------------------
window.openDropboxOAuth = function (url) {
console.log("[DropSendNode] Opening OAuth popup:", url);
if (oauthPopup && !oauthPopup.closed) oauthPopup.close();
const popup = window.open(
url,
"dropbox_oauth",
"width=500,height=700,scrollbars=yes,resizable=yes,status=no,location=no,toolbar=no,menubar=no"
);
if (popup) {
oauthPopup = popup;
popup.focus();
const checkClosed = setInterval(() => {
if (popup.closed) {
console.log("[DropSendNode] OAuth popup closed");
clearInterval(checkClosed);
oauthPopup = null;
}
}, 500);
return popup;
}
console.error("[DropSendNode] OAuth popup blocked by browser?");
return null;
};
// ---------------------------------------------------------------------------
// Credential OUTPUT modal (the existing display_only delivery surface)
// ---------------------------------------------------------------------------
//
// Renders the four credentials delivered by the server after a
// successful exchange. Browser-only — never persisted, discarded
// when closed.
function showCredentialModal(creds) {
const existing = document.getElementById("dropsend-credentials-modal");
if (existing) existing.remove();
const overlay = document.createElement("div");
overlay.id = "dropsend-credentials-modal";
overlay.style.cssText = [
"position:fixed", "inset:0",
"background:rgba(0,0,0,0.7)",
"z-index:10000",
"display:flex", "align-items:center", "justify-content:center",
'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif'
].join(";");
const dialog = document.createElement("div");
dialog.style.cssText = [
"background:#1e1e1e", "color:#eaeaea",
"border-radius:12px", "padding:24px",
"max-width:600px", "width:90%", "max-height:85vh",
"overflow-y:auto",
"box-shadow:0 20px 60px rgba(0,0,0,0.5)",
"box-sizing:border-box"
].join(";");
const header = document.createElement("div");
header.style.cssText = "display:flex;justify-content:space-between;align-items:center;margin-bottom:12px";
const title = document.createElement("h2");
title.textContent = "DropSend Credentials";
title.style.cssText = "margin:0;font-size:1.25rem;color:#fff";
const closeX = document.createElement("button");
closeX.textContent = "×";
closeX.setAttribute("aria-label", "Close");
closeX.style.cssText = "background:none;border:none;color:#999;font-size:1.5rem;cursor:pointer;padding:0 8px;line-height:1";
closeX.onclick = () => overlay.remove();
header.appendChild(title);
header.appendChild(closeX);
dialog.appendChild(header);
const intro = document.createElement("p");
intro.style.cssText = "color:#bbb;font-size:0.9rem;line-height:1.5;margin:0 0 18px 0";
intro.textContent =
"These values are shown only in this browser and will be discarded when you close " +
"this dialog. They are NOT written to disk in this mode. Save each value somewhere " +
"safe now (password manager, secure note). Then configure them as your platform's " +
"secrets (RunPod Secrets, Docker env, systemd EnvironmentFile). Most cloud platforms " +
"require a pod or container restart for new secrets to take effect. Running the " +
"AutoUploader will fail until that restart completes.";
dialog.appendChild(intro);
const flashCopied = (btn) => {
const original = btn.textContent;
btn.textContent = "✓";
setTimeout(() => { btn.textContent = original; }, 1200);
};
Object.entries(creds).forEach(([name, value]) => {
const row = document.createElement("div");
row.style.cssText = "margin-bottom:14px";
const labelRow = document.createElement("div");
labelRow.style.cssText = "display:flex;justify-content:space-between;align-items:center;margin-bottom:4px";
const label = document.createElement("label");
label.textContent = name;
label.style.cssText = "font-weight:600;font-size:0.85rem;color:#aaa;letter-spacing:0.02em";
const copyBtn = document.createElement("button");
copyBtn.textContent = "Copy";
copyBtn.style.cssText = "padding:4px 12px;font-size:0.8rem;background:#3b82f6;color:#fff;border:none;border-radius:4px;cursor:pointer";
copyBtn.onclick = async () => {
try {
await navigator.clipboard.writeText(value);
flashCopied(copyBtn);
} catch (e) {
input.select();
document.execCommand("copy");
flashCopied(copyBtn);
}
};
labelRow.appendChild(label);
labelRow.appendChild(copyBtn);
const input = document.createElement("input");
input.type = "text";
input.value = value;
input.readOnly = true;
input.style.cssText = "width:100%;box-sizing:border-box;padding:8px;font-family:Menlo,Monaco,monospace;font-size:0.85rem;background:#2a2a2a;color:#eaeaea;border:1px solid #444;border-radius:4px";
input.onclick = () => input.select();
input.onfocus = () => input.select();
row.appendChild(labelRow);
row.appendChild(input);
dialog.appendChild(row);
});
const footer = document.createElement("div");
footer.style.cssText = "display:flex;gap:10px;margin-top:18px;padding-top:14px;border-top:1px solid #333";
const copyAllBtn = document.createElement("button");
copyAllBtn.textContent = "Copy all as NAME=value";
copyAllBtn.style.cssText = "flex:1;padding:10px;background:#3b82f6;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:0.9rem";
copyAllBtn.onclick = async () => {
const text = Object.entries(creds).map(([k, v]) => `${k}=${v}`).join("\n");
try {
await navigator.clipboard.writeText(text);
flashCopied(copyAllBtn);
} catch (e) {
console.error("[DropSendNode] Clipboard write failed:", e);
alert("Clipboard write failed; select and copy values manually.");
}
};
const doneBtn = document.createElement("button");
doneBtn.textContent = "I have copied them";
doneBtn.style.cssText = "flex:1;padding:10px;background:#444;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:0.9rem";
doneBtn.onclick = () => overlay.remove();
footer.appendChild(copyAllBtn);
footer.appendChild(doneBtn);
dialog.appendChild(footer);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
}
// ---------------------------------------------------------------------------
// Extension registration
// ---------------------------------------------------------------------------
app.registerExtension({
name: "DropSendNode.OAuthHandler",
async setup() {
console.log("[DropSendNode] OAuth extension loaded");
// Defensive sweep: if ComfyUI restored a workflow before our
// hooks were attached, install the button on any Setup Node
// already in the graph at this point.
try {
findSetupNodes().forEach(n => ensureCredentialButton(n));
} catch (e) {
console.warn("[DropSendNode] setup-time sweep failed:", e);
}
// Reconnect-complete: server cleared credentials and wants the
// browser to refresh so the auth UI is back in its initial state.
api.addEventListener("dropbox_reconnect_complete", (event) => {
console.log("[DropSendNode] dropbox_reconnect_complete:", event.detail);
const data = event.detail;
if (data && data.success) {
this.handleReconnectSuccess();
}
});
// Credentials-needed: server printed the OAuth URL and is
// signaling the browser to open the entry modal so the user
// can paste the auth_code (and re-paste app_key/app_secret)
// as soon as Dropbox shows them the code.
api.addEventListener("dropsend_credentials_needed", (event) => {
console.log("[DropSendNode] dropsend_credentials_needed:", event.detail);
const data = event.detail || {};
const expectedClientId = data.client_id;
const ourClientId = api && typeof api.clientId !== "undefined" ? api.clientId : null;
if (expectedClientId && ourClientId && expectedClientId !== ourClientId) {
console.warn(
"[DropSendNode] Ignoring credentials-needed event: client_id mismatch (got " +
String(expectedClientId) + ", expected " + String(ourClientId) + ")"
);
return;
}
openCredentialEntryModal({ reason: "auto-after-oauth-url" });
});
// Credentials-ready: server delivered the four output values
// for display_only mode. Show the OUTPUT modal.
api.addEventListener("dropsend_credentials_ready", (event) => {
console.log("[DropSendNode] dropsend_credentials_ready");
const data = event.detail;
if (!data || !data.credentials || typeof data.credentials !== "object") {
console.error("[DropSendNode] credentials_ready payload missing/malformed", event);
return;
}
const expectedClientId = data.client_id;
const ourClientId = api && typeof api.clientId !== "undefined" ? api.clientId : null;
if (ourClientId == null) {
console.warn(
"[DropSendNode] api.clientId not available, cannot defense-in-depth verify; " +
"trusting the server's sid-targeted delivery."
);
} else if (expectedClientId && expectedClientId !== ourClientId) {
console.warn(
"[DropSendNode] Ignoring credentials event: client_id mismatch (got " +
String(expectedClientId) + ", expected " + String(ourClientId) + ")"
);
return;
}
// If an entry modal is still open from before, close it —
// we don't need it any more, the values were consumed and
// the OUTPUT modal is about to render.
const entry = document.getElementById("dropsend-credentials-entry-modal");
if (entry) entry.remove();
showCredentialModal(data.credentials);
});
},
resetReconnectFields() {
try {
findSetupNodes().forEach(node => {
if (!node.widgets) return;
const reconnectWidget = node.widgets.find(w => w.name === "reconnect");
if (reconnectWidget && reconnectWidget.value === true) {
reconnectWidget.value = false;
if (node.onWidgetChanged) node.onWidgetChanged("reconnect", false);
}
});
} catch (e) {
console.log("[DropSendNode] Could not reset reconnect fields:", e);
}
},
handleReconnectSuccess() {
console.log("[DropSendNode] Reconnect success - refreshing UI to show auth fields");
this.resetReconnectFields();
// Close any open entry modal — the prior session is gone.
const entry = document.getElementById("dropsend-credentials-entry-modal");
if (entry) entry.remove();
setTimeout(() => {
console.log("[DropSendNode] Reloading after reconnect");
window.location.reload();
}, 1000);
},
nodeCreated(node) {
ensureCredentialButton(node);
},
// Fires when a node is restored from saved state — auto-save in
// localStorage on page reload, dragged workflow JSON, dragged PNG
// with embedded workflow metadata. Without this, the button
// wouldn't be added to nodes that already existed when the
// workflow was loaded.
loadedGraphNode(node, _app) {
ensureCredentialButton(node);
},
// Install the Set-credentials button on every instance of the
// Setup Node. We override prototype.onNodeCreated here (rather
// than relying solely on the nodeCreated extension hook) because
// it runs in-context with the node's construction, so the
// button is added before ComfyUI computes the node's initial
// size — guaranteeing the button is inside the drawn bounds.
//
// Also detects the OAuth URL printed by the Python setup() and
// auto-opens it in a popup window.
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
if (nodeData.name !== SETUP_NODE_TYPE) return;
console.log(`[DropSendNode] Registered ${SETUP_NODE_TYPE} enhancement`);
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = origOnNodeCreated ? origOnNodeCreated.apply(this, arguments) : undefined;
try {
ensureCredentialButton(this);
} catch (e) {
console.warn("[DropSendNode] ensureCredentialButton via prototype hook failed:", e);
}
return r;
};
const originalExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
try {
if (message && message.text && message.text[0]) {
const text = message.text[0];
if (text.includes("Dropbox OAuth Ready!")) {
const m = text.match(/(https:\/\/www\.dropbox\.com\/oauth2\/authorize[^\s]+)/);
if (m) {
setTimeout(() => window.openDropboxOAuth(m[1]), 500);
}
}
}
} catch (e) {
console.warn("[DropSendNode] onExecuted hook error:", e);
}
if (originalExecuted) return originalExecuted.apply(this, arguments);
};
},
});