/**
* Popup logic for gnexus-creds extension.
* Uses GNexusUIKit as a dependency.
*/
const { Helper, Drawer } = window.GNexusUIKit;
async function sendMessage(type, payload) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({ type, payload }, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
return;
}
if (!response || !response.ok) {
reject(new Error(response?.error || "Unknown error"));
return;
}
resolve(response.data);
});
});
}
const searchInput = document.getElementById("search");
const secretsList = document.getElementById("secrets-list");
const emptyState = document.getElementById("empty-state");
const loaderWrap = document.getElementById("loader-wrap");
const mainErrorWrap = document.getElementById("main-error-wrap");
const pendingSaveWrap = document.getElementById("pending-save-wrap");
const refreshBtn = document.getElementById("refresh");
const openSiteBtn = document.getElementById("open-site");
const openSettingsBtn = document.getElementById("open-settings");
const settingsDrawer = Drawer.create("settings-drawer", {
title: "Settings",
body: () => {
const div = document.createElement("div");
div.innerHTML = `
<div class="form-group">
<label class="label" for="base-url">Server URL
<input type="url" id="base-url" class="input w-100" value="https://creds.gnexus.space">
</label>
</div>
<div class="form-group">
<label class="label" for="api-token">API token
<input type="password" id="api-token" class="input w-100" placeholder="gcr_...">
</label>
<div class="hint text-muted text-sm">
Create a token with <code>read</code>, <code>reveal</code>, <code>write</code> scopes on the website.
</div>
</div>
<div id="settings-error-wrap"></div>
<div id="settings-success-wrap"></div>
`;
return div;
},
actions: () => {
const saveBtn = Helper.template.createElement("button", { class: "btn btn-primary" }, "Save");
saveBtn.id = "save-settings";
const openBtn = Helper.template.createElement("button", { class: "btn btn-secondary" }, "Open gnexus-creds");
openBtn.id = "open-site-settings";
return [saveBtn, openBtn];
},
});
const baseUrlInput = settingsDrawer.querySelector("#base-url");
const tokenInput = settingsDrawer.querySelector("#api-token");
const saveBtn = settingsDrawer.querySelector("#save-settings");
const openSiteSettingsBtn = settingsDrawer.querySelector("#open-site-settings");
const settingsErrorWrap = settingsDrawer.querySelector("#settings-error-wrap");
const settingsSuccessWrap = settingsDrawer.querySelector("#settings-success-wrap");
function escapeHtml(text) {
if (text == null) return "";
const div = document.createElement("div");
div.textContent = String(text);
return div.innerHTML;
}
function escapeAttr(text) {
if (text == null) return "";
return String(text)
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(/'/g, "'")
.replace(/</g, "<")
.replace(/>/g, ">");
}
function showWrapAlert(wrap, type, text) {
wrap.innerHTML = "";
const el = Helper.template.createAlert(type, text);
wrap.appendChild(el);
}
function clearWrap(wrap) {
wrap.innerHTML = "";
}
function renderPendingSave(pending) {
pendingSaveWrap.classList.remove("d-none");
pendingSaveWrap.innerHTML = "";
const card = document.createElement("div");
card.className = "card status-card card-info";
card.style.width = "100%";
card.style.maxWidth = "100%";
const usernameField = pending.fields?.find((f) => f.name === "username");
const username = usernameField?.value || "";
card.innerHTML = `
<div class="card-title">Save credentials for ${escapeHtml(pending.source)}?</div>
<div class="card-content">
<p>Account: <strong>${escapeHtml(username)}</strong></p>
</div>
<div class="card-footer">
<button id="pending-save-btn" class="btn btn-success with-icon"><i class="ph ph-check"></i> Save</button>
<button id="pending-dismiss-btn" class="btn btn-secondary">Dismiss</button>
</div>
`;
pendingSaveWrap.appendChild(card);
card.querySelector("#pending-save-btn").addEventListener("click", async () => {
clearWrap(mainErrorWrap);
try {
const payload = {
title: pending.title,
source: pending.source,
fields: pending.fields,
category: pending.category,
tags: pending.tags,
allow_ui: pending.allow_ui,
allow_rest_api: pending.allow_rest_api,
};
await sendMessage("CREATE_SECRET", payload);
pendingSaveWrap.classList.add("d-none");
pendingSaveWrap.innerHTML = "";
mainErrorWrap.classList.remove("d-none");
showWrapAlert(mainErrorWrap, "success", "Credentials saved");
loadSecrets();
} catch (err) {
mainErrorWrap.classList.remove("d-none");
showWrapAlert(mainErrorWrap, "error", err.message);
}
});
card.querySelector("#pending-dismiss-btn").addEventListener("click", async () => {
try {
await sendMessage("CLEAR_PENDING_SAVE");
} catch {
// ignore
}
pendingSaveWrap.classList.add("d-none");
pendingSaveWrap.innerHTML = "";
});
}
async function init() {
try {
const settings = await sendMessage("GET_SETTINGS");
baseUrlInput.value = settings.baseUrl || "https://creds.gnexus.space";
const pending = await sendMessage("GET_PENDING_SAVE");
if (pending) {
if (!settings.token) {
settingsDrawer.show();
return;
}
renderPendingSave(pending);
await loadSecrets();
return;
}
if (!settings.token) {
if (settings.baseUrl) baseUrlInput.value = settings.baseUrl;
settingsDrawer.show();
return;
}
await loadSecrets();
} catch (err) {
settingsDrawer.show();
mainErrorWrap.classList.remove("d-none");
showWrapAlert(mainErrorWrap, "error", err.message);
}
}
async function loadSecrets(query = "", force = false) {
try {
loaderWrap.classList.remove("d-none");
loaderWrap.innerHTML = `<div class="d-flex justify-center">${Helper.template.circleLoaderHTML()}</div>`;
secretsList.innerHTML = "";
emptyState.classList.add("d-none");
mainErrorWrap.classList.add("d-none");
clearWrap(mainErrorWrap);
const data = query
? await sendMessage("SEARCH_SECRETS", { q: query })
: await sendMessage("LIST_SECRETS", { force });
renderSecrets(data?.items || []);
} catch (err) {
secretsList.innerHTML = "";
emptyState.classList.remove("d-none");
emptyState.querySelector(".empty-state-text").textContent = `Error: ${err.message}`;
} finally {
loaderWrap.classList.add("d-none");
}
}
function renderSecrets(secrets) {
secretsList.innerHTML = "";
if (!secrets.length) {
emptyState.classList.remove("d-none");
return;
}
emptyState.classList.add("d-none");
const list = document.createElement("div");
list.className = "list list-actions w-100";
for (const secret of secrets) {
const tags = (secret.tags || [])
.map((t) => `<span class="badge badge-secondary">${escapeHtml(t)}</span>`)
.join("");
const category = secret.category
? `<span class="badge">${escapeHtml(secret.category)}</span>`
: "";
const meta = [category, tags].filter(Boolean).join("");
const item = document.createElement("div");
item.className = "list-item";
item.innerHTML = `
<div class="list-content">
<div class="list-title">${escapeHtml(secret.title)}</div>
${meta ? `<div class="list-subtitle">${meta}</div>` : ""}
</div>
<button class="btn-icon btn-open-secret" data-id="${escapeAttr(secret.id)}" title="Open on gnexus-creds">
<i class="ph ph-arrow-square-out"></i>
</button>
`;
list.appendChild(item);
}
secretsList.appendChild(list);
}
saveBtn.addEventListener("click", async () => {
clearWrap(settingsErrorWrap);
clearWrap(settingsSuccessWrap);
const baseUrl = baseUrlInput.value.trim() || "https://creds.gnexus.space";
const token = tokenInput.value.trim();
if (!token) {
showWrapAlert(settingsErrorWrap, "error", "Enter API token");
return;
}
try {
await sendMessage("VERIFY_TOKEN", { token, baseUrl });
await sendMessage("SAVE_SETTINGS", { token, baseUrl });
showWrapAlert(settingsSuccessWrap, "success", "Settings saved");
tokenInput.value = "";
setTimeout(() => {
settingsDrawer.close();
init();
}, 800);
} catch (err) {
showWrapAlert(settingsErrorWrap, "error", err.message);
}
});
openSettingsBtn.addEventListener("click", () => settingsDrawer.show());
openSiteBtn.addEventListener("click", () => {
const base = baseUrlInput.value.trim() || "https://creds.gnexus.space";
chrome.tabs.create({ url: base });
});
if (openSiteSettingsBtn) {
openSiteSettingsBtn.addEventListener("click", () => {
const base = baseUrlInput.value.trim() || "https://creds.gnexus.space";
chrome.tabs.create({ url: base });
});
}
secretsList.addEventListener("click", (e) => {
const btn = e.target.closest(".btn-open-secret");
if (!btn) return;
const id = btn.dataset.id;
const base = baseUrlInput.value.trim() || "https://creds.gnexus.space";
const url = base.replace(/\/$/, "") + "/secret/" + id;
chrome.tabs.create({ url });
});
refreshBtn.addEventListener("click", () => loadSecrets(searchInput.value.trim(), true));
let searchDebounce;
searchInput.addEventListener("input", (e) => {
clearTimeout(searchDebounce);
searchDebounce = setTimeout(() => loadSecrets(e.target.value.trim()), 300);
});
init();