Mirrors the security architecture of ComfyUI_DropSendNode v1.1.x to close
the same vulnerability classes. The actual Google OAuth grant path is
unchanged from v1.0.1 (same Flow.from_client_config, same OOB redirect,
same fetch_token call), so existing users' OAuth setup continues to work.
What changed is the wrapper around it. Note: this release has not been
runtime-tested end to end; the security logic is a direct port of the
tested DropSend implementation.
Setup Node refactor:
- Removed google_client_id, google_client_secret, auth_code, and the
service-account JSON from INPUT_TYPES. They no longer enter
PromptServer.history, workflow JSON, PNG metadata, localStorage, or
copy-pasted nodes.
- Added a browser-only modal (web/drivesend_oauth.js) and a same-origin
POST route /drivesend/setup/stash for entry. Hardening: origin check,
live-WebSocket gate, per-IP rate limit, 32 KB body cap, JSON content
type lockdown, 60-second TTL, 32-entry capacity, lock-protected access.
- Added COMFYUI_DRIVESEND_ALLOW_SETUP=1 opt-in gate. Without it the node
refuses to write or clear credentials, blocking remote workflow
submitters from hijacking or wiping the operator's Drive connection.
- Removed plaintext stdout prints of GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
/ GOOGLE_REFRESH_TOKEN / COMFYUI_ENCRYPTION_KEY. Replaced with a
redacted (last-4-chars) banner.
- WebSocket credential delivery is sid-targeted, never broadcast. Refuses
to send when client_id is None and surfaces a clear error. JS handler
verifies client_id matches api.clientId before rendering.
- Service-account JSON is entered via a textarea in the same modal,
base64-encoded into GOOGLE_SERVICE_ACCOUNT_JSON for delivery.
File-system hardening:
- .env and token.json now created via os.open(O_WRONLY|O_CREAT|O_TRUNC,
0o600), race-free, instead of open('w') with default umask (0o644).
- Added safe_paths.py: clamps watch_folder to ComfyUI's output tree
(with COMFYUI_DRIVESEND_ALLOWED_WATCH_PATHS opt-in for additional
roots), rejects symlinks in event paths, validates Drive folder IDs.
- AutoUploader now runs every event path through safe_paths.is_safe_event_path.
Logging:
- Centralized logging in __init__.py with race-free 0o600 creation of
drivesend.log on the root logger. Removed module-level
logging.basicConfig duplicates from monitor_output.py, encrypt_file.py,
drivesend_uploader_node.py.
- Verified: no logger or print call in the codebase emits an access
token, refresh token, client secret, service-account JSON body, or
encryption key value.
.gitignore additions:
- token.json, client_secret.json, service_account.json (Google credential
caches, previously not ignored).
- Editor backups: *.bak, *.backup, *~, *.swp, *.swo, .idea/.
- Encrypted artifacts: *.enc.
- Stray credential material: *.key, *.pem, *.p12, *.pfx.
- .claude/, .pytest_cache/, .coverage, htmlcov/, build artifacts,
Thumbs.db, Desktop.ini.
README rewrite:
- Linear three-step structure (Google Cloud setup, Setup Node, AutoUploader).
- Cloud / Local / Service-Account sections for the Setup Node.
- Threat model section documenting all enforced protections.
- "Setup is disabled" and "Browser delivery refused" troubleshooting.
- No em dashes per project style.
Tests:
- tests/test_security.py with five regression tests covering: send_sync
is never called with sid=None for credential events; payload echoes
client_id for JS-side verification; .env file mode is exactly 0o600;
client_id pattern rejects malformed input; stash entries expire after
TTL. Tests run without google-auth, googleapiclient, or watchdog
installed via ghost modules.
Version: 1.0.1 -> 1.1.0
572 lines
24 KiB
JavaScript
572 lines
24 KiB
JavaScript
// drivesend_oauth.js - ComfyUI extension for the DriveSend Setup Node.
|
|
//
|
|
// Architecture summary
|
|
// --------------------
|
|
// The Setup Node deliberately has NO secret-bearing inputs in its
|
|
// Python INPUT_TYPES. google_client_id, google_client_secret, auth_code,
|
|
// and the service-account JSON body 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
|
|
// /drivesend/setup/stash (a same-origin route registered by
|
|
// drivesend_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 with no in-process JS cache. User pastes
|
|
// fresh values each time the modal opens.
|
|
|
|
import { app } from "../../scripts/app.js";
|
|
import { api } from "../../scripts/api.js";
|
|
|
|
const SETUP_NODE_TYPE = "DriveSendSetup";
|
|
|
|
let oauthPopup = null;
|
|
|
|
function findSetupNodes() {
|
|
try {
|
|
return app.graph.nodes.filter(n => n.type === SETUP_NODE_TYPE);
|
|
} catch (e) {
|
|
console.warn("[DriveSendNode] Could not enumerate graph nodes:", e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// "Set credentials..." button widget on the Setup Node
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function ensureCredentialButton(node) {
|
|
if (!node || node.type !== SETUP_NODE_TYPE) return;
|
|
if (node.__drivesendButtonAdded) return;
|
|
if (typeof node.addWidget !== "function") {
|
|
console.warn("[DriveSendNode] node.addWidget unavailable");
|
|
return;
|
|
}
|
|
node.addWidget("button", "Set credentials...", null, () => {
|
|
openCredentialEntryModal({ reason: "user-clicked", node });
|
|
});
|
|
node.__drivesendButtonAdded = true;
|
|
try {
|
|
if (typeof node.computeSize === "function" && typeof node.setSize === "function") {
|
|
node.setSize(node.computeSize());
|
|
}
|
|
} catch (e) {
|
|
console.warn("[DriveSendNode] could not recompute node size:", e);
|
|
}
|
|
try { app.graph.setDirtyCanvas(true, true); } catch (_) {}
|
|
console.log(`[DriveSendNode] Installed Set-credentials button on ${SETUP_NODE_TYPE}`);
|
|
}
|
|
|
|
// Read the auth_method widget value from the given node so the modal
|
|
// can show only the relevant fields (OAuth vs service account).
|
|
function getAuthMethod(node) {
|
|
try {
|
|
if (!node || !node.widgets) return "oauth";
|
|
const w = node.widgets.find(x => x.name === "auth_method");
|
|
return (w && w.value) || "oauth";
|
|
} catch (e) {
|
|
return "oauth";
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Credential ENTRY modal
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// This is the modal the user types secrets INTO. Distinct from
|
|
// showCredentialModal below, which displays the credentials delivered
|
|
// at the end of a successful Setup run.
|
|
|
|
function openCredentialEntryModal({ reason, node }) {
|
|
const existing = document.getElementById("drivesend-credentials-entry-modal");
|
|
if (existing) {
|
|
const firstInput = existing.querySelector("input, textarea");
|
|
if (firstInput) firstInput.focus();
|
|
return;
|
|
}
|
|
|
|
// If no node was passed (auto-trigger from server event), pick the
|
|
// first Setup Node in the graph so we can read its auth_method.
|
|
if (!node) {
|
|
const nodes = findSetupNodes();
|
|
node = nodes && nodes.length ? nodes[0] : null;
|
|
}
|
|
const authMethod = getAuthMethod(node);
|
|
|
|
const overlay = document.createElement("div");
|
|
overlay.id = "drivesend-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:560px", "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:8px";
|
|
const title = document.createElement("h2");
|
|
title.textContent = "Set DriveSend Credentials";
|
|
title.style.cssText = "margin:0;font-size:1.2rem;color:#fff";
|
|
const closeX = document.createElement("button");
|
|
closeX.textContent = "x";
|
|
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";
|
|
if (authMethod === "service_account") {
|
|
intro.textContent =
|
|
"Paste the entire JSON body of your service account key file " +
|
|
"below. 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.";
|
|
} else {
|
|
intro.textContent =
|
|
reason === "auto-after-oauth-url"
|
|
? "Authorization URL printed in the ComfyUI terminal. Authorize at Google, copy the auth code, then paste Client ID + Client Secret + Auth Code below and click Save."
|
|
: "Paste your Google OAuth Client ID and Client Secret. Leave the Auth Code blank for now; you'll paste it later after authorizing at Google. 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);
|
|
|
|
const inputs = {};
|
|
|
|
if (authMethod === "service_account") {
|
|
// One large textarea for the JSON body.
|
|
const row = document.createElement("div");
|
|
row.style.cssText = "margin-bottom:12px";
|
|
const lbl = document.createElement("label");
|
|
lbl.textContent = "Service Account JSON";
|
|
lbl.style.cssText = "display:block;font-size:0.8rem;color:#aaa;margin-bottom:4px;letter-spacing:0.02em";
|
|
row.appendChild(lbl);
|
|
|
|
const ta = document.createElement("textarea");
|
|
ta.autocomplete = "off";
|
|
ta.spellcheck = false;
|
|
ta.value = "";
|
|
ta.rows = 12;
|
|
ta.style.cssText = "width:100%;box-sizing:border-box;padding:8px;font-family:Menlo,Monaco,monospace;font-size:0.8rem;background:#2a2a2a;color:#eaeaea;border:1px solid #444;border-radius:4px;resize:vertical";
|
|
ta.placeholder = '{ "type": "service_account", "project_id": "...", ... }';
|
|
row.appendChild(ta);
|
|
dialog.appendChild(row);
|
|
inputs.service_account_json = ta;
|
|
} else {
|
|
const fields = [
|
|
{ name: "google_client_id", label: "Google OAuth Client ID" },
|
|
{ name: "google_client_secret", label: "Google OAuth Client Secret" },
|
|
{ name: "auth_code", label: "Auth Code (paste after authorizing at Google)" },
|
|
];
|
|
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;
|
|
});
|
|
}
|
|
|
|
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 = {
|
|
google_client_id: (inputs.google_client_id && inputs.google_client_id.value) || "",
|
|
google_client_secret: (inputs.google_client_secret && inputs.google_client_secret.value) || "",
|
|
auth_code: (inputs.auth_code && inputs.auth_code.value) || "",
|
|
service_account_json: (inputs.service_account_json && inputs.service_account_json.value) || "",
|
|
};
|
|
const anyValue = Object.values(payload).some(v => v && v.trim());
|
|
if (!anyValue) {
|
|
status.textContent = "Nothing to save. Paste values into at least one field.";
|
|
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("/drivesend/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("[DriveSendNode] Stash POST failed:", e);
|
|
status.textContent = "Could not save: " + e.message;
|
|
saveBtn.disabled = false;
|
|
cancelBtn.disabled = false;
|
|
return;
|
|
}
|
|
Object.values(inputs).forEach(i => { if (i) i.value = ""; });
|
|
overlay.remove();
|
|
console.log("[DriveSendNode] Stashed credentials via entry modal; click Queue to run setup");
|
|
};
|
|
|
|
footer.appendChild(cancelBtn);
|
|
footer.appendChild(saveBtn);
|
|
dialog.appendChild(footer);
|
|
|
|
Object.values(inputs).forEach(input => {
|
|
if (!input) return;
|
|
input.addEventListener("keydown", (ev) => {
|
|
if (ev.key === "Enter" && input.tagName !== "TEXTAREA") {
|
|
ev.preventDefault();
|
|
saveBtn.click();
|
|
}
|
|
if (ev.key === "Escape") {
|
|
ev.preventDefault();
|
|
cancelBtn.click();
|
|
}
|
|
});
|
|
});
|
|
|
|
overlay.appendChild(dialog);
|
|
document.body.appendChild(overlay);
|
|
|
|
setTimeout(() => {
|
|
const list = Object.values(inputs).filter(Boolean);
|
|
const firstEmpty = list.find(i => !i.value);
|
|
if (firstEmpty) firstEmpty.focus();
|
|
}, 0);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// OAuth popup window (delegate to Google)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
window.openDriveSendOAuth = function (url) {
|
|
console.log("[DriveSendNode] Opening OAuth popup:", url);
|
|
if (oauthPopup && !oauthPopup.closed) oauthPopup.close();
|
|
const popup = window.open(
|
|
url,
|
|
"google_oauth",
|
|
"width=520,height=720,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("[DriveSendNode] OAuth popup closed");
|
|
clearInterval(checkClosed);
|
|
oauthPopup = null;
|
|
}
|
|
}, 500);
|
|
return popup;
|
|
}
|
|
console.error("[DriveSendNode] OAuth popup blocked by browser?");
|
|
return null;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Credential OUTPUT modal (display_only delivery surface)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function showCredentialModal(creds) {
|
|
const existing = document.getElementById("drivesend-credentials-modal");
|
|
if (existing) existing.remove();
|
|
|
|
const overlay = document.createElement("div");
|
|
overlay.id = "drivesend-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:640px", "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 = "DriveSend Credentials";
|
|
title.style.cssText = "margin:0;font-size:1.25rem;color:#fff";
|
|
const closeX = document.createElement("button");
|
|
closeX.textContent = "x";
|
|
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.";
|
|
dialog.appendChild(intro);
|
|
|
|
const flashCopied = (btn) => {
|
|
const original = btn.textContent;
|
|
btn.textContent = "Copied";
|
|
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);
|
|
|
|
// Service-account JSON is long; render it as a textarea.
|
|
const isLong = (value && value.length > 120) || /JSON|SERVICE_ACCOUNT/i.test(name);
|
|
const input = document.createElement(isLong ? "textarea" : "input");
|
|
if (!isLong) input.type = "text";
|
|
input.value = value;
|
|
input.readOnly = true;
|
|
if (isLong) input.rows = 5;
|
|
input.style.cssText = "width:100%;box-sizing:border-box;padding:8px;font-family:Menlo,Monaco,monospace;font-size:0.8rem;background:#2a2a2a;color:#eaeaea;border:1px solid #444;border-radius:4px;resize:vertical";
|
|
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("[DriveSendNode] 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: "DriveSendNode.OAuthHandler",
|
|
|
|
async setup() {
|
|
console.log("[DriveSendNode] OAuth extension loaded");
|
|
|
|
try {
|
|
findSetupNodes().forEach(n => ensureCredentialButton(n));
|
|
} catch (e) {
|
|
console.warn("[DriveSendNode] setup-time sweep failed:", e);
|
|
}
|
|
|
|
api.addEventListener("drivesend_reconnect_complete", (event) => {
|
|
console.log("[DriveSendNode] drivesend_reconnect_complete:", event.detail);
|
|
const data = event.detail;
|
|
if (data && data.success) {
|
|
this.handleReconnectSuccess();
|
|
}
|
|
});
|
|
|
|
api.addEventListener("drivesend_credentials_needed", (event) => {
|
|
console.log("[DriveSendNode] drivesend_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(
|
|
"[DriveSendNode] Ignoring credentials-needed event: client_id mismatch"
|
|
);
|
|
return;
|
|
}
|
|
openCredentialEntryModal({ reason: "auto-after-oauth-url" });
|
|
});
|
|
|
|
api.addEventListener("drivesend_credentials_ready", (event) => {
|
|
console.log("[DriveSendNode] drivesend_credentials_ready");
|
|
const data = event.detail;
|
|
if (!data || !data.credentials || typeof data.credentials !== "object") {
|
|
console.error("[DriveSendNode] 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(
|
|
"[DriveSendNode] api.clientId not available, cannot defense-in-depth verify; " +
|
|
"trusting the server's sid-targeted delivery."
|
|
);
|
|
} else if (expectedClientId && expectedClientId !== ourClientId) {
|
|
console.warn(
|
|
"[DriveSendNode] Ignoring credentials event: client_id mismatch"
|
|
);
|
|
return;
|
|
}
|
|
const entry = document.getElementById("drivesend-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("[DriveSendNode] Could not reset reconnect fields:", e);
|
|
}
|
|
},
|
|
|
|
handleReconnectSuccess() {
|
|
console.log("[DriveSendNode] Reconnect success - refreshing UI to show auth fields");
|
|
this.resetReconnectFields();
|
|
const entry = document.getElementById("drivesend-credentials-entry-modal");
|
|
if (entry) entry.remove();
|
|
setTimeout(() => {
|
|
console.log("[DriveSendNode] Reloading after reconnect");
|
|
window.location.reload();
|
|
}, 1000);
|
|
},
|
|
|
|
nodeCreated(node) {
|
|
ensureCredentialButton(node);
|
|
},
|
|
|
|
loadedGraphNode(node, _app) {
|
|
ensureCredentialButton(node);
|
|
},
|
|
|
|
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
|
|
if (nodeData.name !== SETUP_NODE_TYPE) return;
|
|
console.log(`[DriveSendNode] 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("[DriveSendNode] 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("Google OAuth Ready!")) {
|
|
const m = text.match(/(https:\/\/accounts\.google\.com\/o\/oauth2\/auth[^\s]+)/);
|
|
if (m) {
|
|
setTimeout(() => window.openDriveSendOAuth(m[1]), 500);
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn("[DriveSendNode] onExecuted hook error:", e);
|
|
}
|
|
if (originalExecuted) return originalExecuted.apply(this, arguments);
|
|
};
|
|
},
|
|
});
|