refactor(settings): replace boolean toggles with custom renderer UI
- Use ComfyUI SettingCustomRenderer API for action buttons instead of ToggleSwitch
- Add key status display (Client/Server key configuration state)
- Replace raw DOM toast with app.extensionManager.toast API
- Replace window.confirm with app.extensionManager.dialog API
- Inject themed CSS matching ComfyUI design tokens
- Add pi-icon buttons with primary/default/danger variants
- Change category to [CryptIO🔒, Key Management] for proper nav tree structure
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* CryptIO Settings - ComfyUI Settings Integration
|
||||
* Rich key management UI using custom renderer in the settings panel
|
||||
*/
|
||||
//@ts-ignore
|
||||
import { app as rawApp } from "../../scripts/app.js";
|
||||
import { CLIENT_KEYPAIR_STORAGE_KEY, SERVER_KEYS_STORAGE_KEY, } from "./utils/cryptoKeys.js";
|
||||
const app = rawApp;
|
||||
const SETTINGS_STYLES = `
|
||||
.cryptio-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
width: 100%;
|
||||
}
|
||||
.cryptio-section-title {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-muted, #8a8a8a);
|
||||
margin-bottom: 2px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid var(--border-subtle, #3c3d42);
|
||||
}
|
||||
.cryptio-section-title-danger {
|
||||
color: var(--destructive-background, #f75951);
|
||||
border-bottom-color: var(--destructive-background, #f75951);
|
||||
}
|
||||
.cryptio-status-card {
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-subtle, #3c3d42);
|
||||
background: var(--secondary-background, #262729);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.cryptio-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.cryptio-status-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cryptio-status-label {
|
||||
font-weight: 500;
|
||||
color: var(--base-foreground, #fff);
|
||||
}
|
||||
.cryptio-status-detail {
|
||||
color: var(--text-muted, #8a8a8a);
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
.cryptio-btn-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.cryptio-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--border-default, #494a50);
|
||||
border-radius: 6px;
|
||||
background: var(--base-background, #171718);
|
||||
color: var(--base-foreground, #fff);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s, border-color 0.15s, opacity 0.15s;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.cryptio-btn:hover {
|
||||
background: var(--secondary-background-hover, #313235);
|
||||
border-color: var(--p-primary-color, #0b8ce9);
|
||||
}
|
||||
.cryptio-btn:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.cryptio-btn i {
|
||||
font-size: 14px;
|
||||
}
|
||||
.cryptio-btn-primary {
|
||||
background: var(--p-primary-color, #0b8ce9);
|
||||
color: #fff;
|
||||
border-color: var(--p-primary-color, #0b8ce9);
|
||||
}
|
||||
.cryptio-btn-primary:hover {
|
||||
background: var(--p-primary-hover-color, #185a8b);
|
||||
border-color: var(--p-primary-hover-color, #185a8b);
|
||||
}
|
||||
.cryptio-btn-danger {
|
||||
background: var(--destructive-background, #b33a3a);
|
||||
color: #fff;
|
||||
border-color: var(--destructive-background, #b33a3a);
|
||||
}
|
||||
.cryptio-btn-danger:hover {
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
`;
|
||||
function injectStyles() {
|
||||
if (document.getElementById("cryptio-settings-styles"))
|
||||
return;
|
||||
const style = document.createElement("style");
|
||||
style.id = "cryptio-settings-styles";
|
||||
style.textContent = SETTINGS_STYLES;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
// ──────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ──────────────────────────────────────────────
|
||||
function getExtMgr() {
|
||||
return app.extensionManager;
|
||||
}
|
||||
function showToast(message, severity = "info") {
|
||||
const em = getExtMgr();
|
||||
if (em?.toast?.add) {
|
||||
em.toast.add({ severity, summary: "CryptIO", detail: message, life: 3000 });
|
||||
}
|
||||
}
|
||||
async function showConfirm(title, message) {
|
||||
const em = getExtMgr();
|
||||
if (em?.dialog?.confirm) {
|
||||
try {
|
||||
const result = await em.dialog.confirm({ title, message });
|
||||
return result === true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return window.confirm(message);
|
||||
}
|
||||
function downloadJSON(data, filename) {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
function readJSONFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.target?.result);
|
||||
resolve(data);
|
||||
}
|
||||
catch {
|
||||
reject(new Error("Invalid JSON file"));
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(new Error("Failed to read file"));
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
function createFileInput(id, onChange) {
|
||||
const existing = document.getElementById(id);
|
||||
if (existing)
|
||||
existing.remove();
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.id = id;
|
||||
input.accept = ".json";
|
||||
input.style.display = "none";
|
||||
input.addEventListener("change", (event) => {
|
||||
onChange(event);
|
||||
input.remove();
|
||||
});
|
||||
document.body.appendChild(input);
|
||||
return input;
|
||||
}
|
||||
function triggerFileUpload(id, handler) {
|
||||
const input = createFileInput(id, handler);
|
||||
input.click();
|
||||
}
|
||||
function getKeyInfo(storageKey) {
|
||||
const raw = localStorage.getItem(storageKey);
|
||||
if (!raw)
|
||||
return { present: false, keyType: "" };
|
||||
try {
|
||||
const data = JSON.parse(raw);
|
||||
const pubKey = data.publicKey || "";
|
||||
if (pubKey.includes("RSA"))
|
||||
return { present: true, keyType: "RSA" };
|
||||
if (pubKey.includes("PUBLIC"))
|
||||
return { present: true, keyType: "Public Key" };
|
||||
return { present: true, keyType: "Configured" };
|
||||
}
|
||||
catch {
|
||||
return { present: false, keyType: "" };
|
||||
}
|
||||
}
|
||||
// ──────────────────────────────────────────────
|
||||
// Action handlers
|
||||
// ──────────────────────────────────────────────
|
||||
async function handleGenerate() {
|
||||
const confirmed = await showConfirm("Generate New Client Keys", "This will overwrite your existing client keypair and cannot be undone. Continue?");
|
||||
if (!confirmed)
|
||||
return;
|
||||
try {
|
||||
const { generateClientKeyPair } = await import("./utils/cryptoKeys.js");
|
||||
const keyPair = await generateClientKeyPair();
|
||||
localStorage.setItem(CLIENT_KEYPAIR_STORAGE_KEY, JSON.stringify(keyPair));
|
||||
showToast("New client keys generated", "success");
|
||||
}
|
||||
catch (error) {
|
||||
showToast(`Failed to generate keys: ${error}`, "error");
|
||||
}
|
||||
}
|
||||
async function handleClear() {
|
||||
const confirmed = await showConfirm("Clear All Keys", "This will remove all CryptIO keys from localStorage. This action cannot be undone.");
|
||||
if (!confirmed)
|
||||
return;
|
||||
localStorage.removeItem(CLIENT_KEYPAIR_STORAGE_KEY);
|
||||
localStorage.removeItem(SERVER_KEYS_STORAGE_KEY);
|
||||
showToast("All keys cleared", "success");
|
||||
}
|
||||
function handleDownloadClient() {
|
||||
const raw = localStorage.getItem(CLIENT_KEYPAIR_STORAGE_KEY);
|
||||
if (raw) {
|
||||
downloadJSON(JSON.parse(raw), "cryptio-client-keypair.json");
|
||||
showToast("Client keys downloaded", "success");
|
||||
}
|
||||
else {
|
||||
showToast("No client keys found", "error");
|
||||
}
|
||||
}
|
||||
function handleDownloadServer() {
|
||||
const raw = localStorage.getItem(SERVER_KEYS_STORAGE_KEY);
|
||||
if (raw) {
|
||||
downloadJSON(JSON.parse(raw), "cryptio-server-keys.json");
|
||||
showToast("Server keys downloaded", "success");
|
||||
}
|
||||
else {
|
||||
showToast("No server keys found", "error");
|
||||
}
|
||||
}
|
||||
async function handleUploadClient(event) {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file)
|
||||
return;
|
||||
try {
|
||||
const data = await readJSONFile(file);
|
||||
if (data.publicKey && data.privateKey) {
|
||||
localStorage.setItem(CLIENT_KEYPAIR_STORAGE_KEY, JSON.stringify(data));
|
||||
showToast("Client keys uploaded", "success");
|
||||
}
|
||||
else {
|
||||
showToast("Invalid key format — must contain publicKey and privateKey", "error");
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
showToast(`Failed to upload: ${error}`, "error");
|
||||
}
|
||||
}
|
||||
async function handleUploadServer(event) {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file)
|
||||
return;
|
||||
try {
|
||||
const data = await readJSONFile(file);
|
||||
if (data.publicKey && data.privateKey) {
|
||||
localStorage.setItem(SERVER_KEYS_STORAGE_KEY, JSON.stringify(data));
|
||||
showToast("Server keys uploaded", "success");
|
||||
}
|
||||
else {
|
||||
showToast("Invalid key format — must contain publicKey and privateKey", "error");
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
showToast(`Failed to upload: ${error}`, "error");
|
||||
}
|
||||
}
|
||||
// ──────────────────────────────────────────────
|
||||
// DOM builders
|
||||
// ──────────────────────────────────────────────
|
||||
function el(tag, className, text) {
|
||||
const e = document.createElement(tag);
|
||||
e.className = className;
|
||||
if (text !== undefined)
|
||||
e.textContent = text;
|
||||
return e;
|
||||
}
|
||||
function makeSectionTitle(text, variant) {
|
||||
if (variant === "danger")
|
||||
return el("div", "cryptio-section-title cryptio-section-title-danger", text);
|
||||
return el("div", "cryptio-section-title", text);
|
||||
}
|
||||
function makeButton(text, icon, variant, onClick) {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "cryptio-btn";
|
||||
if (variant === "primary")
|
||||
btn.classList.add("cryptio-btn-primary");
|
||||
if (variant === "danger")
|
||||
btn.classList.add("cryptio-btn-danger");
|
||||
const i = document.createElement("i");
|
||||
i.className = icon;
|
||||
btn.appendChild(i);
|
||||
btn.appendChild(document.createTextNode(" " + text));
|
||||
btn.addEventListener("click", onClick);
|
||||
return btn;
|
||||
}
|
||||
function makeButtonRow(buttons) {
|
||||
const row = el("div", "cryptio-btn-row");
|
||||
buttons.forEach((b) => row.appendChild(b));
|
||||
return row;
|
||||
}
|
||||
function makeStatusCard(items) {
|
||||
const card = el("div", "cryptio-status-card");
|
||||
items.forEach((item) => {
|
||||
const row = el("div", "cryptio-status-row");
|
||||
const dot = el("span", "cryptio-status-dot");
|
||||
dot.style.backgroundColor = item.present
|
||||
? "var(--success-background, #00cd72)"
|
||||
: "var(--text-muted, #8a8a8a)";
|
||||
row.appendChild(dot);
|
||||
row.appendChild(el("span", "cryptio-status-label", item.label));
|
||||
row.appendChild(el("span", "cryptio-status-detail", item.detail));
|
||||
card.appendChild(row);
|
||||
});
|
||||
return card;
|
||||
}
|
||||
// ──────────────────────────────────────────────
|
||||
// Custom renderer (SettingCustomRenderer)
|
||||
// ──────────────────────────────────────────────
|
||||
function renderCryptIOSettings(_name, _setter, _value, _attrs) {
|
||||
const container = el("div", "cryptio-settings");
|
||||
const clientInfo = getKeyInfo(CLIENT_KEYPAIR_STORAGE_KEY);
|
||||
const serverInfo = getKeyInfo(SERVER_KEYS_STORAGE_KEY);
|
||||
// ── Key Status ──
|
||||
container.appendChild(makeSectionTitle("Key Status"));
|
||||
container.appendChild(makeStatusCard([
|
||||
{
|
||||
present: clientInfo.present,
|
||||
label: "Client Key",
|
||||
detail: clientInfo.present
|
||||
? `${clientInfo.keyType} keypair configured`
|
||||
: "Not configured",
|
||||
},
|
||||
{
|
||||
present: serverInfo.present,
|
||||
label: "Server Key",
|
||||
detail: serverInfo.present
|
||||
? `${serverInfo.keyType} keypair configured`
|
||||
: "Not configured",
|
||||
},
|
||||
]));
|
||||
// ── Client Keys ──
|
||||
container.appendChild(makeSectionTitle("Client Keys"));
|
||||
container.appendChild(makeButtonRow([
|
||||
makeButton("Generate New", "pi pi-refresh", "primary", () => handleGenerate()),
|
||||
makeButton("Download", "pi pi-download", "default", () => handleDownloadClient()),
|
||||
makeButton("Upload", "pi pi-upload", "default", () => triggerFileUpload("cryptio-client-upload", handleUploadClient)),
|
||||
]));
|
||||
// ── Server Keys ──
|
||||
container.appendChild(makeSectionTitle("Server Keys"));
|
||||
container.appendChild(makeButtonRow([
|
||||
makeButton("Download", "pi pi-download", "default", () => handleDownloadServer()),
|
||||
makeButton("Upload", "pi pi-upload", "default", () => triggerFileUpload("cryptio-server-upload", handleUploadServer)),
|
||||
]));
|
||||
// ── Danger Zone ──
|
||||
container.appendChild(makeSectionTitle("Danger Zone", "danger"));
|
||||
container.appendChild(makeButtonRow([
|
||||
makeButton("Clear All Keys", "pi pi-trash", "danger", () => handleClear()),
|
||||
]));
|
||||
return container;
|
||||
}
|
||||
// ──────────────────────────────────────────────
|
||||
// Extension registration
|
||||
// ──────────────────────────────────────────────
|
||||
app.registerExtension({
|
||||
name: "cryptio.Settings",
|
||||
settings: [
|
||||
{
|
||||
id: "cryptio.category",
|
||||
name: "CryptIO Settings",
|
||||
type: "hidden",
|
||||
defaultValue: "",
|
||||
category: ["CryptIO🔒", "Key Management"],
|
||||
},
|
||||
{
|
||||
id: "cryptio.key_management",
|
||||
name: "Key Management",
|
||||
type: renderCryptIOSettings,
|
||||
defaultValue: null,
|
||||
category: ["CryptIO🔒", "Key Management"],
|
||||
},
|
||||
],
|
||||
async setup() {
|
||||
injectStyles();
|
||||
console.log("[CryptIO] Settings extension loaded");
|
||||
},
|
||||
});
|
||||
+390
-255
@@ -1,26 +1,156 @@
|
||||
/**
|
||||
* CryptIO Settings - ComfyUI Settings Integration
|
||||
* Integrates CryptIO key management into ComfyUI's native settings panel
|
||||
* Rich key management UI using custom renderer in the settings panel
|
||||
*/
|
||||
|
||||
//@ts-ignore
|
||||
import { app as rawApp } from "../../scripts/app.js";
|
||||
import type {
|
||||
ComfyApp,
|
||||
} from "@comfyorg/comfyui-frontend-types";
|
||||
|
||||
import type { ComfyApp } from "@comfyorg/comfyui-frontend-types";
|
||||
import {
|
||||
CLIENT_KEYPAIR_STORAGE_KEY,
|
||||
SERVER_KEYS_STORAGE_KEY,
|
||||
KeyPair,
|
||||
ServerKeys
|
||||
} from "./utils/cryptoKeys.js";
|
||||
|
||||
const app: ComfyApp = rawApp;
|
||||
|
||||
/**
|
||||
* Download JSON file
|
||||
*/
|
||||
const SETTINGS_STYLES = `
|
||||
.cryptio-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
width: 100%;
|
||||
}
|
||||
.cryptio-section-title {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-muted, #8a8a8a);
|
||||
margin-bottom: 2px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid var(--border-subtle, #3c3d42);
|
||||
}
|
||||
.cryptio-section-title-danger {
|
||||
color: var(--destructive-background, #f75951);
|
||||
border-bottom-color: var(--destructive-background, #f75951);
|
||||
}
|
||||
.cryptio-status-card {
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-subtle, #3c3d42);
|
||||
background: var(--secondary-background, #262729);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.cryptio-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.cryptio-status-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cryptio-status-label {
|
||||
font-weight: 500;
|
||||
color: var(--base-foreground, #fff);
|
||||
}
|
||||
.cryptio-status-detail {
|
||||
color: var(--text-muted, #8a8a8a);
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
.cryptio-btn-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.cryptio-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--border-default, #494a50);
|
||||
border-radius: 6px;
|
||||
background: var(--base-background, #171718);
|
||||
color: var(--base-foreground, #fff);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s, border-color 0.15s, opacity 0.15s;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.cryptio-btn:hover {
|
||||
background: var(--secondary-background-hover, #313235);
|
||||
border-color: var(--p-primary-color, #0b8ce9);
|
||||
}
|
||||
.cryptio-btn:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.cryptio-btn i {
|
||||
font-size: 14px;
|
||||
}
|
||||
.cryptio-btn-primary {
|
||||
background: var(--p-primary-color, #0b8ce9);
|
||||
color: #fff;
|
||||
border-color: var(--p-primary-color, #0b8ce9);
|
||||
}
|
||||
.cryptio-btn-primary:hover {
|
||||
background: var(--p-primary-hover-color, #185a8b);
|
||||
border-color: var(--p-primary-hover-color, #185a8b);
|
||||
}
|
||||
.cryptio-btn-danger {
|
||||
background: var(--destructive-background, #b33a3a);
|
||||
color: #fff;
|
||||
border-color: var(--destructive-background, #b33a3a);
|
||||
}
|
||||
.cryptio-btn-danger:hover {
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
`;
|
||||
|
||||
function injectStyles(): void {
|
||||
if (document.getElementById("cryptio-settings-styles")) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = "cryptio-settings-styles";
|
||||
style.textContent = SETTINGS_STYLES;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
function getExtMgr(): any {
|
||||
return (app as any).extensionManager;
|
||||
}
|
||||
|
||||
function showToast(message: string, severity: "success" | "error" | "info" = "info"): void {
|
||||
const em = getExtMgr();
|
||||
if (em?.toast?.add) {
|
||||
em.toast.add({ severity, summary: "CryptIO", detail: message, life: 3000 });
|
||||
}
|
||||
}
|
||||
|
||||
async function showConfirm(title: string, message: string): Promise<boolean> {
|
||||
const em = getExtMgr();
|
||||
if (em?.dialog?.confirm) {
|
||||
try {
|
||||
const result = await em.dialog.confirm({ title, message });
|
||||
return result === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return window.confirm(message);
|
||||
}
|
||||
|
||||
function downloadJSON(data: any, filename: string): void {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -33,17 +163,14 @@ function downloadJSON(data: any, filename: string): void {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read file content as JSON
|
||||
*/
|
||||
function readJSONFile(file: File): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.target?.result as string);
|
||||
const data = JSON.parse((e.target as any)?.result);
|
||||
resolve(data);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
reject(new Error("Invalid JSON file"));
|
||||
}
|
||||
};
|
||||
@@ -52,139 +179,9 @@ function readJSONFile(file: File): Promise<any> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show temporary message
|
||||
*/
|
||||
function showMessage(message: string, type: "success" | "error" | "info" = "info"): void {
|
||||
const colors = {
|
||||
success: "#4CAF50",
|
||||
error: "#f44336",
|
||||
info: "#2196F3"
|
||||
};
|
||||
|
||||
const messageEl = document.createElement("div");
|
||||
messageEl.textContent = message;
|
||||
messageEl.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 20px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
background-color: ${colors[type]};
|
||||
z-index: 10001;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||||
`;
|
||||
document.body.appendChild(messageEl);
|
||||
setTimeout(() => {
|
||||
messageEl.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate new client key pair
|
||||
*/
|
||||
async function generateNewClientKeys(): Promise<void> {
|
||||
if (confirm("Are you sure you want to generate new client keys? This will overwrite your existing client keypair and cannot be undone.")) {
|
||||
try {
|
||||
const { generateClientKeyPair } = await import("./utils/cryptoKeys.js");
|
||||
const keyPair = await generateClientKeyPair();
|
||||
localStorage.setItem(CLIENT_KEYPAIR_STORAGE_KEY, JSON.stringify(keyPair));
|
||||
showMessage("New client keys generated successfully!", "success");
|
||||
} catch (error) {
|
||||
showMessage(`Failed to generate keys: ${error}`, "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all CryptIO keys
|
||||
*/
|
||||
function clearAllKeys(): void {
|
||||
if (confirm("Are you sure you want to clear all CryptIO keys? This action cannot be undone.")) {
|
||||
localStorage.removeItem(CLIENT_KEYPAIR_STORAGE_KEY);
|
||||
localStorage.removeItem(SERVER_KEYS_STORAGE_KEY);
|
||||
showMessage("All keys cleared successfully!", "success");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download client keys
|
||||
*/
|
||||
function downloadClientKeys(): void {
|
||||
const clientKeys = localStorage.getItem(CLIENT_KEYPAIR_STORAGE_KEY);
|
||||
if (clientKeys) {
|
||||
downloadJSON(JSON.parse(clientKeys), "cryptio-client-keypair.json");
|
||||
showMessage("Client keys downloaded successfully!", "success");
|
||||
} else {
|
||||
showMessage("No client keys found", "error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download server keys
|
||||
*/
|
||||
function downloadServerKeys(): void {
|
||||
const serverKeys = localStorage.getItem(SERVER_KEYS_STORAGE_KEY);
|
||||
if (serverKeys) {
|
||||
downloadJSON(JSON.parse(serverKeys), "cryptio-server-keys.json");
|
||||
showMessage("Server keys downloaded successfully!", "success");
|
||||
} else {
|
||||
showMessage("No server keys found", "error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle client key file upload
|
||||
*/
|
||||
async function handleClientKeyUpload(event: Event): Promise<void> {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const data = await readJSONFile(file);
|
||||
if (data.publicKey && data.privateKey) {
|
||||
localStorage.setItem(CLIENT_KEYPAIR_STORAGE_KEY, JSON.stringify(data));
|
||||
showMessage("Client keys uploaded successfully!", "success");
|
||||
} else {
|
||||
showMessage("Invalid key format. Must contain publicKey and privateKey", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage(`Failed to upload client keys: ${error}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle server key file upload
|
||||
*/
|
||||
async function handleServerKeyUpload(event: Event): Promise<void> {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const data = await readJSONFile(file);
|
||||
if (data.publicKey && data.privateKey) {
|
||||
localStorage.setItem(SERVER_KEYS_STORAGE_KEY, JSON.stringify(data));
|
||||
showMessage("Server keys uploaded successfully!", "success");
|
||||
} else {
|
||||
showMessage("Invalid key format. Must contain publicKey and privateKey", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage(`Failed to upload server keys: ${error}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create hidden file input for uploads
|
||||
*/
|
||||
function createFileInput(id: string, onChange: (event: Event) => void): HTMLInputElement {
|
||||
// Remove any existing input with the same id to avoid duplicates
|
||||
const existing = document.getElementById(id);
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
}
|
||||
if (existing) existing.remove();
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
@@ -199,7 +196,244 @@ function createFileInput(id: string, onChange: (event: Event) => void): HTMLInpu
|
||||
return input;
|
||||
}
|
||||
|
||||
// Register extension with ComfyUI settings
|
||||
function triggerFileUpload(id: string, handler: (e: Event) => void): void {
|
||||
const input = createFileInput(id, handler);
|
||||
input.click();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Key info helpers
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
interface KeyInfo {
|
||||
present: boolean;
|
||||
keyType: string;
|
||||
}
|
||||
|
||||
function getKeyInfo(storageKey: string): KeyInfo {
|
||||
const raw = localStorage.getItem(storageKey);
|
||||
if (!raw) return { present: false, keyType: "" };
|
||||
try {
|
||||
const data = JSON.parse(raw);
|
||||
const pubKey: string = data.publicKey || "";
|
||||
if (pubKey.includes("RSA")) return { present: true, keyType: "RSA" };
|
||||
if (pubKey.includes("PUBLIC")) return { present: true, keyType: "Public Key" };
|
||||
return { present: true, keyType: "Configured" };
|
||||
} catch {
|
||||
return { present: false, keyType: "" };
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Action handlers
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
async function handleGenerate(): Promise<void> {
|
||||
const confirmed = await showConfirm(
|
||||
"Generate New Client Keys",
|
||||
"This will overwrite your existing client keypair and cannot be undone. Continue?"
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const { generateClientKeyPair } = await import("./utils/cryptoKeys.js");
|
||||
const keyPair = await generateClientKeyPair();
|
||||
localStorage.setItem(CLIENT_KEYPAIR_STORAGE_KEY, JSON.stringify(keyPair));
|
||||
showToast("New client keys generated", "success");
|
||||
} catch (error) {
|
||||
showToast(`Failed to generate keys: ${error}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClear(): Promise<void> {
|
||||
const confirmed = await showConfirm(
|
||||
"Clear All Keys",
|
||||
"This will remove all CryptIO keys from localStorage. This action cannot be undone."
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
localStorage.removeItem(CLIENT_KEYPAIR_STORAGE_KEY);
|
||||
localStorage.removeItem(SERVER_KEYS_STORAGE_KEY);
|
||||
showToast("All keys cleared", "success");
|
||||
}
|
||||
|
||||
function handleDownloadClient(): void {
|
||||
const raw = localStorage.getItem(CLIENT_KEYPAIR_STORAGE_KEY);
|
||||
if (raw) {
|
||||
downloadJSON(JSON.parse(raw), "cryptio-client-keypair.json");
|
||||
showToast("Client keys downloaded", "success");
|
||||
} else {
|
||||
showToast("No client keys found", "error");
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownloadServer(): void {
|
||||
const raw = localStorage.getItem(SERVER_KEYS_STORAGE_KEY);
|
||||
if (raw) {
|
||||
downloadJSON(JSON.parse(raw), "cryptio-server-keys.json");
|
||||
showToast("Server keys downloaded", "success");
|
||||
} else {
|
||||
showToast("No server keys found", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUploadClient(event: Event): Promise<void> {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const data = await readJSONFile(file);
|
||||
if (data.publicKey && data.privateKey) {
|
||||
localStorage.setItem(CLIENT_KEYPAIR_STORAGE_KEY, JSON.stringify(data));
|
||||
showToast("Client keys uploaded", "success");
|
||||
} else {
|
||||
showToast("Invalid key format — must contain publicKey and privateKey", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(`Failed to upload: ${error}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUploadServer(event: Event): Promise<void> {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const data = await readJSONFile(file);
|
||||
if (data.publicKey && data.privateKey) {
|
||||
localStorage.setItem(SERVER_KEYS_STORAGE_KEY, JSON.stringify(data));
|
||||
showToast("Server keys uploaded", "success");
|
||||
} else {
|
||||
showToast("Invalid key format — must contain publicKey and privateKey", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(`Failed to upload: ${error}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// DOM builders
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
function el(tag: string, className: string, text?: string): HTMLElement {
|
||||
const e = document.createElement(tag);
|
||||
e.className = className;
|
||||
if (text !== undefined) e.textContent = text;
|
||||
return e;
|
||||
}
|
||||
|
||||
function makeSectionTitle(text: string, variant?: "danger"): HTMLElement {
|
||||
if (variant === "danger") return el("div", "cryptio-section-title cryptio-section-title-danger", text);
|
||||
return el("div", "cryptio-section-title", text);
|
||||
}
|
||||
|
||||
function makeButton(text: string, icon: string, variant: "primary" | "default" | "danger", onClick: () => void): HTMLButtonElement {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "cryptio-btn";
|
||||
if (variant === "primary") btn.classList.add("cryptio-btn-primary");
|
||||
if (variant === "danger") btn.classList.add("cryptio-btn-danger");
|
||||
const i = document.createElement("i");
|
||||
i.className = icon;
|
||||
btn.appendChild(i);
|
||||
btn.appendChild(document.createTextNode(" " + text));
|
||||
btn.addEventListener("click", onClick);
|
||||
return btn;
|
||||
}
|
||||
|
||||
function makeButtonRow(buttons: HTMLButtonElement[]): HTMLElement {
|
||||
const row = el("div", "cryptio-btn-row");
|
||||
buttons.forEach((b) => row.appendChild(b));
|
||||
return row;
|
||||
}
|
||||
|
||||
function makeStatusCard(items: Array<{ present: boolean; label: string; detail: string }>): HTMLElement {
|
||||
const card = el("div", "cryptio-status-card");
|
||||
items.forEach((item) => {
|
||||
const row = el("div", "cryptio-status-row");
|
||||
const dot = el("span", "cryptio-status-dot");
|
||||
dot.style.backgroundColor = item.present
|
||||
? "var(--success-background, #00cd72)"
|
||||
: "var(--text-muted, #8a8a8a)";
|
||||
row.appendChild(dot);
|
||||
row.appendChild(el("span", "cryptio-status-label", item.label));
|
||||
row.appendChild(el("span", "cryptio-status-detail", item.detail));
|
||||
card.appendChild(row);
|
||||
});
|
||||
return card;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Custom renderer (SettingCustomRenderer)
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
function renderCryptIOSettings(
|
||||
_name: string,
|
||||
_setter: (v: unknown) => void,
|
||||
_value: unknown,
|
||||
_attrs?: Record<string, unknown>
|
||||
): HTMLElement {
|
||||
const container = el("div", "cryptio-settings");
|
||||
|
||||
const clientInfo = getKeyInfo(CLIENT_KEYPAIR_STORAGE_KEY);
|
||||
const serverInfo = getKeyInfo(SERVER_KEYS_STORAGE_KEY);
|
||||
|
||||
// ── Key Status ──
|
||||
container.appendChild(makeSectionTitle("Key Status"));
|
||||
container.appendChild(
|
||||
makeStatusCard([
|
||||
{
|
||||
present: clientInfo.present,
|
||||
label: "Client Key",
|
||||
detail: clientInfo.present
|
||||
? `${clientInfo.keyType} keypair configured`
|
||||
: "Not configured",
|
||||
},
|
||||
{
|
||||
present: serverInfo.present,
|
||||
label: "Server Key",
|
||||
detail: serverInfo.present
|
||||
? `${serverInfo.keyType} keypair configured`
|
||||
: "Not configured",
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
// ── Client Keys ──
|
||||
container.appendChild(makeSectionTitle("Client Keys"));
|
||||
container.appendChild(
|
||||
makeButtonRow([
|
||||
makeButton("Generate New", "pi pi-refresh", "primary", () => handleGenerate()),
|
||||
makeButton("Download", "pi pi-download", "default", () => handleDownloadClient()),
|
||||
makeButton("Upload", "pi pi-upload", "default", () =>
|
||||
triggerFileUpload("cryptio-client-upload", handleUploadClient)
|
||||
),
|
||||
])
|
||||
);
|
||||
|
||||
// ── Server Keys ──
|
||||
container.appendChild(makeSectionTitle("Server Keys"));
|
||||
container.appendChild(
|
||||
makeButtonRow([
|
||||
makeButton("Download", "pi pi-download", "default", () => handleDownloadServer()),
|
||||
makeButton("Upload", "pi pi-upload", "default", () =>
|
||||
triggerFileUpload("cryptio-server-upload", handleUploadServer)
|
||||
),
|
||||
])
|
||||
);
|
||||
|
||||
// ── Danger Zone ──
|
||||
container.appendChild(makeSectionTitle("Danger Zone", "danger"));
|
||||
container.appendChild(
|
||||
makeButtonRow([
|
||||
makeButton("Clear All Keys", "pi pi-trash", "danger", () => handleClear()),
|
||||
])
|
||||
);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Extension registration
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
app.registerExtension({
|
||||
name: "cryptio.Settings",
|
||||
settings: [
|
||||
@@ -208,117 +442,18 @@ app.registerExtension({
|
||||
name: "CryptIO Settings",
|
||||
type: "hidden",
|
||||
defaultValue: "",
|
||||
category: ["CryptIO🔒"]
|
||||
category: ["CryptIO🔒", "Key Management"],
|
||||
},
|
||||
{
|
||||
id: "cryptio.download_client_keys" as any,
|
||||
name: "Download Client Keys",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
category: ["CryptIO🔒", "Client Keys", "Download"],
|
||||
tooltip: "Click to download your client keypair as a JSON file",
|
||||
onChange: (newVal: boolean, oldVal: boolean) => {
|
||||
if (newVal === true) {
|
||||
downloadClientKeys();
|
||||
// Reset the toggle
|
||||
setTimeout(() => {
|
||||
app.extensionManager.setting.set("cryptio.download_client_keys", false);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
id: "cryptio.key_management" as any,
|
||||
name: "Key Management",
|
||||
type: renderCryptIOSettings as any,
|
||||
defaultValue: null,
|
||||
category: ["CryptIO🔒", "Key Management"],
|
||||
},
|
||||
{
|
||||
id: "cryptio.upload_client_keys" as any,
|
||||
name: "Upload Client Keys",
|
||||
type: "boolean",
|
||||
category: ["CryptIO🔒", "Client Keys", "Upload"],
|
||||
defaultValue: false,
|
||||
|
||||
tooltip: "Click to upload a client keypair JSON file",
|
||||
onChange: (newVal: boolean, oldVal: boolean) => {
|
||||
if (newVal === true) {
|
||||
// Create file input and trigger it
|
||||
const input = createFileInput("cryptio-client-upload", handleClientKeyUpload);
|
||||
input.click();
|
||||
// Reset the toggle
|
||||
setTimeout(() => {
|
||||
app.extensionManager.setting.set("cryptio.upload_client_keys", false);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "cryptio.download_server_keys" as any,
|
||||
name: "Download Server Keys",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
category: ["CryptIO🔒", "Server Keys", "Download"],
|
||||
tooltip: "Click to download server keys as a JSON file",
|
||||
onChange: (newVal: boolean, oldVal: boolean) => {
|
||||
if (newVal === true) {
|
||||
downloadServerKeys();
|
||||
// Reset the toggle
|
||||
setTimeout(() => {
|
||||
app.extensionManager.setting.set("cryptio.download_server_keys", false);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "cryptio.upload_server_keys" as any,
|
||||
name: "Upload Server Keys",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
category: ["CryptIO🔒", "Server Keys", "Upload"],
|
||||
tooltip: "Click to upload a server keys JSON file",
|
||||
onChange: (newVal: boolean, oldVal: boolean) => {
|
||||
if (newVal === true) {
|
||||
// Create file input and trigger it
|
||||
const input = createFileInput("cryptio-server-upload", handleServerKeyUpload);
|
||||
input.click();
|
||||
// Reset the toggle
|
||||
setTimeout(() => {
|
||||
app.extensionManager.setting.set("cryptio.upload_server_keys", false);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "cryptio.generate_new_keys" as any,
|
||||
name: "Generate New Client Keys",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
category: ["CryptIO🔒", "Key Management", "Generate"],
|
||||
tooltip: "Generate a new client keypair (this will overwrite existing keys)",
|
||||
onChange: (newVal: boolean, oldVal: boolean) => {
|
||||
if (newVal === true) {
|
||||
generateNewClientKeys();
|
||||
// Reset the toggle
|
||||
setTimeout(() => {
|
||||
app.extensionManager.setting.set("cryptio.generate_new_keys", false);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "cryptio.clear_all_keys" as any,
|
||||
name: "Clear All Keys",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
category: ["CryptIO🔒", "Key Management", "Actions"],
|
||||
tooltip: "Remove all CryptIO keys from localStorage (cannot be undone)",
|
||||
onChange: (newVal: boolean, oldVal: boolean) => {
|
||||
if (newVal === true) {
|
||||
clearAllKeys();
|
||||
// Reset the toggle
|
||||
setTimeout(() => {
|
||||
app.extensionManager.setting.set("cryptio.clear_all_keys", false);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
async setup() {
|
||||
console.log("CryptIO Settings extension loaded - all settings registered");
|
||||
injectStyles();
|
||||
console.log("[CryptIO] Settings extension loaded");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user