diff --git a/extensions/dist/gnexus-creds-extension-chrome-0.2.1.zip b/extensions/dist/gnexus-creds-extension-chrome-0.2.1.zip deleted file mode 100644 index 5977341..0000000 --- a/extensions/dist/gnexus-creds-extension-chrome-0.2.1.zip +++ /dev/null Binary files differ diff --git a/extensions/dist/gnexus-creds-extension-chrome-0.2.2.zip b/extensions/dist/gnexus-creds-extension-chrome-0.2.2.zip new file mode 100644 index 0000000..3cf04ee --- /dev/null +++ b/extensions/dist/gnexus-creds-extension-chrome-0.2.2.zip Binary files differ diff --git a/extensions/dist/gnexus-creds-extension-firefox-0.2.1.zip b/extensions/dist/gnexus-creds-extension-firefox-0.2.1.zip deleted file mode 100644 index 014e607..0000000 --- a/extensions/dist/gnexus-creds-extension-firefox-0.2.1.zip +++ /dev/null Binary files differ diff --git a/extensions/dist/gnexus-creds-extension-firefox-0.2.2.zip b/extensions/dist/gnexus-creds-extension-firefox-0.2.2.zip new file mode 100644 index 0000000..0a49e47 --- /dev/null +++ b/extensions/dist/gnexus-creds-extension-firefox-0.2.2.zip Binary files differ diff --git a/extensions/extension/Makefile b/extensions/extension/Makefile index 1494343..4525beb 100644 --- a/extensions/extension/Makefile +++ b/extensions/extension/Makefile @@ -1,5 +1,5 @@ NAME := gnexus-creds-extension -VERSION := 0.2.1 +VERSION := 0.2.2 # Per-file prerequisites: a directory prerequisite (plain "src") never goes # stale when only a file inside it is edited. SRC := manifest.json $(shell find src icons lib -type f) diff --git a/extensions/extension/manifest.json b/extensions/extension/manifest.json index cac27a8..df5f1e8 100644 --- a/extensions/extension/manifest.json +++ b/extensions/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "gnexus-creds", - "version": "0.2.1", + "version": "0.2.2", "description": "Browser extension for gnexus-creds secret manager.", "permissions": ["storage", "activeTab", "alarms"], "host_permissions": ["https://*/*", "http://*/*"], diff --git a/extensions/extension/pack-src/config.json b/extensions/extension/pack-src/config.json index 0a8d3e0..11a751b 100644 --- a/extensions/extension/pack-src/config.json +++ b/extensions/extension/pack-src/config.json @@ -30,7 +30,9 @@ "enterToken": "Enter API token", "settingsSaved": "Settings saved", "error": "Error", - "openOnSite": "Open on gnexus-creds" + "openOnSite": "Open on gnexus-creds", + "tokenInvalid": "The saved API token no longer works — open Settings and paste a new one.", + "tokenId": "Token ID" }, "uk": { "use": "Використати", @@ -53,7 +55,9 @@ "enterToken": "Вкажіть API-токен", "settingsSaved": "Налаштування збережено", "error": "Помилка", - "openOnSite": "Відкрити на gnexus-creds" + "openOnSite": "Відкрити на gnexus-creds", + "tokenInvalid": "Збережений API-токен більше не працює — відкрийте налаштування та вставте новий.", + "tokenId": "ID токена" }, "ru": { "use": "Использовать", @@ -76,7 +80,9 @@ "enterToken": "Введите API-токен", "settingsSaved": "Настройки сохранены", "error": "Ошибка", - "openOnSite": "Открыть на gnexus-creds" + "openOnSite": "Открыть на gnexus-creds", + "tokenInvalid": "Сохранённый API-токен больше не работает — откройте настройки и вставьте новый.", + "tokenId": "ID токена" } } } \ No newline at end of file diff --git a/extensions/extension/src/api.js b/extensions/extension/src/api.js index 48a8779..ae6cf5f 100644 --- a/extensions/extension/src/api.js +++ b/extensions/extension/src/api.js @@ -15,7 +15,9 @@ const payload = await response.json().catch(() => ({})); if (!response.ok) { - throw new Error(payload?.error?.message || `HTTP ${response.status}`); + const err = new Error(payload?.error?.message || `HTTP ${response.status}`); + err.status = response.status; + throw err; } return payload; } diff --git a/extensions/extension/src/background.js b/extensions/extension/src/background.js index 99d4472..81c61b1 100644 --- a/extensions/extension/src/background.js +++ b/extensions/extension/src/background.js @@ -185,6 +185,17 @@ await syncUiLocale("alarm"); }); +// Revoked token visibility: a 401 from any server call must not stay +// silent — the popup reads the flag and shows "token invalid" instead of +// looking like a working (but stale) extension +async function setTokenInvalid(value) { + if (value) { + await chrome.storage.local.set({ tokenInvalid: true }); + } else { + await chrome.storage.local.remove("tokenInvalid"); + } +} + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { (async () => { try { @@ -205,6 +216,7 @@ case "VERIFY_TOKEN": { const me = await getMe(message.payload.token, message.payload.baseUrl); + await setTokenInvalid(false); // cache the per-user UI language while we hold a fresh /me try { const locale = String(me?.locale_effective || me?.locale || "en").trim().toLowerCase(); @@ -218,6 +230,7 @@ case "LIST_SECRETS": { const data = await getSecretsCached(message.payload?.force); + await setTokenInvalid(false); sendResponse({ ok: true, data }); } break; @@ -236,6 +249,7 @@ case "CREATE_SECRET": { const data = await createSecret(token, baseUrl, message.payload); + await setTokenInvalid(false); invalidateCache(); try { await chrome.storage.session.remove("pendingSave"); @@ -317,7 +331,12 @@ sendResponse({ ok: false, error: "Unknown message type" }); } } catch (err) { - sendResponse({ ok: false, error: err.message }); + // 401 from any API call means the stored token no longer works — + // remember it for the popup; a successful VERIFY_TOKEN clears it + if (err?.status === 401) { + await setTokenInvalid(true).catch(() => {}); + } + sendResponse({ ok: false, error: err.message, status: err.status }); } })(); return true; diff --git a/extensions/extension/src/pack-shared.js b/extensions/extension/src/pack-shared.js index cac9410..2e67a13 100644 --- a/extensions/extension/src/pack-shared.js +++ b/extensions/extension/src/pack-shared.js @@ -171,6 +171,7 @@ "openAppTitle", "refresh", "settingsTitle", "searchPlaceholder", "noSecrets", "saveCredentials", "account", "credentialsSaved", "enterToken", "settingsSaved", "error", "openOnSite", + "tokenInvalid", "tokenId", ]; const LOCALIZED_LOCALES = ["en", "uk", "ru"]; @@ -198,6 +199,8 @@ settingsSaved: "Settings saved", error: "Error", openOnSite: "Open on gnexus-creds", + tokenInvalid: "The saved API token no longer works — open Settings and paste a new one.", + tokenId: "Token ID", }; const DEFAULT_STRINGS_BY_LOCALE = { diff --git a/extensions/extension/src/popup/popup.js b/extensions/extension/src/popup/popup.js index f665a37..3abd770 100644 --- a/extensions/extension/src/popup/popup.js +++ b/extensions/extension/src/popup/popup.js @@ -13,7 +13,9 @@ return; } if (!response || !response.ok) { - reject(new Error(response?.error || "Unknown error")); + const err = new Error(response?.error || "Unknown error"); + err.status = response?.status; + reject(err); return; } resolve(response.data); @@ -83,6 +85,7 @@
${loc.tokenHint}
+
@@ -106,6 +109,17 @@ const settingsErrorWrap = settingsDrawer.querySelector("#settings-error-wrap"); const settingsSuccessWrap = settingsDrawer.querySelector("#settings-success-wrap"); +// The stored token carries its public id: gcr_{public_id16hex}_{secret}. +// Showing the id lets the user match the popup against the tokens table on +// the site (which token is this extension actually using). +async function updateTokenId() { + const el = document.getElementById("token-id"); + if (!el) return; + const token = String((await chrome.storage.local.get("token")).token || ""); + const publicId = token.match(/^gcr_([0-9a-f]{16})_/)?.[1] || ""; + el.textContent = publicId ? `${loc.tokenId}: ${publicId}` : ""; +} + function escapeHtml(text) { if (text == null) return ""; const div = document.createElement("div"); @@ -228,6 +242,15 @@ try { const settings = await sendMessage("GET_SETTINGS"); baseUrlInput.value = settings.baseUrl || "https://creds.gnexus.space"; + updateTokenId(); + + // The background sets a 401-side flag; show the invalid-token plate + // instead of a silently stale secrets list. + const flagged = (await chrome.storage.local.get("tokenInvalid")).tokenInvalid; + if (flagged && settings.token) { + mainErrorWrap.classList.remove("d-none"); + showWrapAlert(mainErrorWrap, "error", loc.tokenInvalid); + } const pending = await sendMessage("GET_PENDING_SAVE"); if (pending) { @@ -270,6 +293,12 @@ secretsList.innerHTML = ""; emptyState.classList.remove("d-none"); emptyState.querySelector(".empty-state-text").textContent = `${loc.error}: ${err.message}`; + // the 401 that flips the background flag is discovered right here — + // surface the dedicated plate on this same load, not only after a reload + if (err?.status === 401) { + mainErrorWrap.classList.remove("d-none"); + showWrapAlert(mainErrorWrap, "error", loc.tokenInvalid); + } } finally { loaderWrap.classList.add("d-none"); } @@ -342,6 +371,7 @@ settingsDrawer.show(); // body() rebuilds on show() — set the status line after that updatePackStatus(await applyPackStyles()); + updateTokenId(); }); openSiteBtn.addEventListener("click", () => { diff --git a/extensions/manifest.json b/extensions/manifest.json index 556a228..5f1fdf8 100644 --- a/extensions/manifest.json +++ b/extensions/manifest.json @@ -1,19 +1,19 @@ { - "version": "0.2.1", + "version": "0.2.2", "released_at": "2026-10-02T00:00:00Z", - "notes": "Design-resource pack: CSS, detection tables and UI config refresh from the server without re-releasing the extension.", + "notes": "Revoked-token visibility: the popup flags a dead token and shows its public ID; the tokens table adds Public ID, Created and Last used columns.", "builds": [ { "browser": "chrome", - "filename": "gnexus-creds-extension-chrome-0.2.1.zip", - "sha256": "a516b258b7f6ee4221355ddf976fffeefb76506b61b318a49fbf48a273ef7d2a", - "size": 5061252 + "filename": "gnexus-creds-extension-chrome-0.2.2.zip", + "sha256": "9db133fcb0b7dcccfa19a0243dc56b6e30b684d4f7e64ed0c757dcae11a71251", + "size": 5062020 }, { "browser": "firefox", - "filename": "gnexus-creds-extension-firefox-0.2.1.zip", - "sha256": "30ee6dfff83693324f770196d990af76c781f1d9da8cfcbb8ee88ae82aa9384c", - "size": 5060362 + "filename": "gnexus-creds-extension-firefox-0.2.2.zip", + "sha256": "898c7b4b71358d18a7b080aeb0652d0164a876789726752c405de56dc92e9f84", + "size": 5061105 } ] } diff --git a/extensions/pack/pack.json b/extensions/pack/pack.json index 1235afb..c503652 100644 --- a/extensions/pack/pack.json +++ b/extensions/pack/pack.json @@ -1,8 +1,8 @@ { - "packVersion": 18, - "minExtensionVersion": "0.2.0", + "packVersion": 24, + "minExtensionVersion": "0.2.2", "maxExtensionVersion": null, - "publishedAt": "2026-10-02T08:43:57.810Z", + "publishedAt": "2026-10-02T20:20:01.090Z", "files": { "content.css": { "sha256": "e17800f82cbde350b6fe3c60746136260844ede20a5f1d4fbfd2d4930fcd6005", @@ -17,8 +17,8 @@ "size": 2211 }, "config.json": { - "sha256": "19c7b3cfc7b42da9f87dcb6d5d9a55af8be976b8a1b1ab3ad4cd75c62a3b1ba9", - "size": 2973 + "sha256": "5b11b004a51f2be2f05b2c6e4652b10a1795d14ff7691b047182ca304cad81cf", + "size": 3498 } } } diff --git a/extensions/pack/v24/config.json b/extensions/pack/v24/config.json new file mode 100644 index 0000000..11a751b --- /dev/null +++ b/extensions/pack/v24/config.json @@ -0,0 +1,88 @@ +{ + "timings": { + "scanDebounceMs": 300, + "cardExitMs": 280 + }, + "strings": { + "use": "Use", + "dismiss": "Dismiss", + "selectAccount": "Select account" + }, + "stringsByLocale": { + "en": { + "use": "Use", + "dismiss": "Dismiss", + "selectAccount": "Select account", + "drawerTitle": "Settings", + "serverUrl": "Server URL", + "apiToken": "API token", + "tokenHint": "Create a token with read, reveal, write scopes on the website.", + "save": "Save", + "openApp": "Open gnexus-creds", + "openAppTitle": "Open gnexus-creds", + "refresh": "Refresh", + "settingsTitle": "Settings", + "searchPlaceholder": "Search secrets...", + "noSecrets": "No secrets", + "saveCredentials": "Save credentials for {source}?", + "account": "Account:", + "credentialsSaved": "Credentials saved", + "enterToken": "Enter API token", + "settingsSaved": "Settings saved", + "error": "Error", + "openOnSite": "Open on gnexus-creds", + "tokenInvalid": "The saved API token no longer works — open Settings and paste a new one.", + "tokenId": "Token ID" + }, + "uk": { + "use": "Використати", + "dismiss": "Приховати", + "selectAccount": "Виберіть акаунт", + "drawerTitle": "Налаштування", + "serverUrl": "Адреса сервера", + "apiToken": "API-токен", + "tokenHint": "Створіть токен зі scopes read, reveal, write на сайті.", + "save": "Зберегти", + "openApp": "Відкрити gnexus-creds", + "openAppTitle": "Відкрити gnexus-creds", + "refresh": "Оновити", + "settingsTitle": "Налаштування", + "searchPlaceholder": "Пошук секретів...", + "noSecrets": "Немає секретів", + "saveCredentials": "Зберегти дані для {source}?", + "account": "Акаунт:", + "credentialsSaved": "Дані збережено", + "enterToken": "Вкажіть API-токен", + "settingsSaved": "Налаштування збережено", + "error": "Помилка", + "openOnSite": "Відкрити на gnexus-creds", + "tokenInvalid": "Збережений API-токен більше не працює — відкрийте налаштування та вставте новий.", + "tokenId": "ID токена" + }, + "ru": { + "use": "Использовать", + "dismiss": "Скрыть", + "selectAccount": "Выберите аккаунт", + "drawerTitle": "Настройки", + "serverUrl": "URL сервера", + "apiToken": "API-токен", + "tokenHint": "Создайте токен со scopes read, reveal, write на сайте.", + "save": "Сохранить", + "openApp": "Открыть gnexus-creds", + "openAppTitle": "Открыть gnexus-creds", + "refresh": "Обновить", + "settingsTitle": "Настройки", + "searchPlaceholder": "Поиск секретов...", + "noSecrets": "Нет секретов", + "saveCredentials": "Сохранить данные для {source}?", + "account": "Аккаунт:", + "credentialsSaved": "Данные сохранены", + "enterToken": "Введите API-токен", + "settingsSaved": "Настройки сохранены", + "error": "Ошибка", + "openOnSite": "Открыть на gnexus-creds", + "tokenInvalid": "Сохранённый API-токен больше не работает — откройте настройки и вставьте новый.", + "tokenId": "ID токена" + } + } +} \ No newline at end of file diff --git a/extensions/pack/v24/content.css b/extensions/pack/v24/content.css new file mode 100644 index 0000000..e508290 --- /dev/null +++ b/extensions/pack/v24/content.css @@ -0,0 +1,116 @@ +/* Card styles for the closed Shadow DOM root (content.js injects this file's + text into the shadow root; it is never added to the page stylesheet, so the + host page can neither restyle the card nor see our class names). + Palette and geometry mirror gnexus-ui-kit 1.0 — keep in sync with lib/. */ + +.gnexus-creds-autofill-container { + position: fixed; + top: 16px; + left: 50%; + transform: translateX(-50%); + z-index: 2147483647; + display: flex; + flex-direction: column; + gap: 8px; + width: max-content; + max-width: calc(100% - 32px); + pointer-events: none; +} + +.gnexus-creds-autofill-card { + pointer-events: auto; + background: #16161e; + border: 2px solid rgba(192, 202, 245, 0.24); + border-left: 6px solid #7aa2f7; + color: #c0caf5; + font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-size: 14px; + border-radius: 6px; + box-shadow: 0 14px 36px rgba(22, 22, 30, 0.36); + padding: 15px 18px; + min-width: 300px; + max-width: 460px; + width: 100%; + opacity: 0; + transform: translateY(-12px); + transition: opacity 0.28s ease, transform 0.28s ease; +} + +.gnexus-creds-autofill-card-visible { + opacity: 1; + transform: translateY(0); +} + +.gnexus-creds-autofill-card-exit { + opacity: 0; + transform: translateY(-12px); + transition: opacity 0.25s ease-in, transform 0.25s ease-in; +} + +.gnexus-creds-autofill-title { + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: #7aa2f7; + line-height: 1.25; + word-break: break-word; + margin: 0 0 12px; + font-size: 14px; +} + +.gnexus-creds-autofill-list { + max-height: 200px; + overflow-y: auto; + margin-bottom: 12px; +} + +.gnexus-creds-autofill-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 10px 0; + border-bottom: 2px solid rgba(192, 202, 245, 0.08); +} + +.gnexus-creds-autofill-row:last-child { + border-bottom: none; +} + +.gnexus-creds-autofill-row-title { + font-size: 13px; + color: #c0caf5; + word-break: break-word; +} + +.gnexus-creds-autofill-actions { + display: flex; + gap: 10px; + justify-content: flex-end; +} + +.gnexus-creds-autofill-btn { + background: #16161e; + border: 2px solid rgba(192, 202, 245, 0.24); + border-left: 6px solid #7aa2f7; + color: #7aa2f7; + padding: 8px 16px; + border-radius: 6px; + font-family: inherit; + font-size: 13px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + cursor: pointer; + line-height: 1; + flex-shrink: 0; +} + +.gnexus-creds-autofill-btn-secondary { + border-left-width: 2px; + color: #a9b1d6; +} + +.gnexus-creds-autofill-btn:hover { + opacity: 0.85; +} \ No newline at end of file diff --git a/extensions/pack/v24/popup.css b/extensions/pack/v24/popup.css new file mode 100644 index 0000000..ecb886d --- /dev/null +++ b/extensions/pack/v24/popup.css @@ -0,0 +1,22 @@ +body { + width: 420px; + min-height: 500px; +} + +.page-header .page-header-content { + display: flex; + flex-direction: row; + align-items: center; +} + +.page-header-logo { + width: 28px; + height: 28px; + margin-right: 8px; +} + +.list-subtitle { + display: flex; + flex-wrap: wrap; + gap: 4px; +} diff --git a/extensions/pack/v24/tables.json b/extensions/pack/v24/tables.json new file mode 100644 index 0000000..c22d391 --- /dev/null +++ b/extensions/pack/v24/tables.json @@ -0,0 +1,255 @@ +{ + "usernameKeywords": [ + [ + "username", + 6 + ], + [ + "user_name", + 6 + ], + [ + "user_id", + 4 + ], + [ + "userid", + 4 + ], + [ + "login", + 5 + ], + [ + "log_in", + 3 + ], + [ + "logon", + 3 + ], + [ + "signin", + 2 + ], + [ + "email", + 5 + ], + [ + "e_mail", + 5 + ], + [ + "mail", + 3 + ], + [ + "correo", + 4 + ], + [ + "courriel", + 4 + ], + [ + "phone", + 4 + ], + [ + "tel", + 2 + ], + [ + "telefon", + 4 + ], + [ + "mobile", + 2 + ], + [ + "account", + 2 + ], + [ + "nick", + 3 + ], + [ + "handle", + 2 + ], + [ + "member", + 2 + ], + [ + "benutzer", + 5 + ], + [ + "benutzername", + 6 + ], + [ + "kennung", + 3 + ], + [ + "логин", + 6 + ], + [ + "пользователь", + 5 + ], + [ + "юзер", + 4 + ], + [ + "почта", + 5 + ], + [ + "телефон", + 4 + ], + [ + "usuario", + 5 + ], + [ + "utilisateur", + 4 + ] + ], + "passwordKeywords": [ + [ + "password", + 8 + ], + [ + "passwd", + 8 + ], + [ + "pwd", + 8 + ], + [ + "pass", + 7 + ], + [ + "pw", + 5 + ], + [ + "passwort", + 8 + ], + [ + "motdepasse", + 8 + ], + [ + "mot_de_passe", + 8 + ], + [ + "contrasena", + 8 + ], + [ + "contrasenia", + 8 + ], + [ + "senha", + 8 + ], + [ + "haslo", + 6 + ], + [ + "geslo", + 6 + ], + [ + "пароль", + 8 + ], + [ + "пассворд", + 6 + ] + ], + "antiKeywords": [ + "cc", + "card", + "cvc", + "cvv", + "security_code", + "search", + "query", + "filter", + "coupon", + "promo", + "newsletter", + "subscribe", + "subject", + "message", + "comment", + "first_name", + "lastname", + "last_name", + "fname", + "lname", + "birth", + "dob", + "zip", + "postal", + "amount", + "price", + "total", + "qty", + "country", + "city", + "street", + "address", + "otp", + "one_time", + "onetime", + "one_time_code", + "verification", + "confirm", + "repeat", + "retype", + "again" + ], + "autocompleteTokens": { + "username": { + "username": 9, + "password": 0 + }, + "current_password": { + "username": 0, + "password": 9 + }, + "new_password": { + "username": 0, + "password": 7 + }, + "email": { + "username": 7, + "password": 0 + }, + "tel": { + "username": 3, + "password": 0 + } + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 544daa1..d60ff0f 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -379,13 +379,19 @@ .map((token) => ({ key: token.public_id, name: token.name, + public_id: token.public_id, scopes: token.scopes, + created_at: formatTimestamp(token.created_at), + last_used_at: token.last_used_at ? formatTimestamp(token.last_used_at) : "—", raw: token })) ); const tokenColumns = computed(() => [ { key: "name", label: t("col.name") }, + { key: "public_id", label: t("col.publicId") }, { key: "scopes", label: t("col.scopes") }, + { key: "created_at", label: t("col.created") }, + { key: "last_used_at", label: t("col.lastUsed") }, { key: "actions", label: "" } ]); const diagnosticsItems = computed(() => [ @@ -1471,6 +1477,9 @@ + diff --git a/frontend/src/i18n/messages/en.js b/frontend/src/i18n/messages/en.js index 47d7019..194ff19 100644 --- a/frontend/src/i18n/messages/en.js +++ b/frontend/src/i18n/messages/en.js @@ -70,6 +70,8 @@ "col.file": "File", "col.size": "Size", "col.created": "Created", + "col.publicId": "Public ID", + "col.lastUsed": "Last used", // detail panel (SecretDetailPanel.vue) "detail.purpose": "Purpose", diff --git a/frontend/src/i18n/messages/ru.js b/frontend/src/i18n/messages/ru.js index 1c284b3..60de70a 100644 --- a/frontend/src/i18n/messages/ru.js +++ b/frontend/src/i18n/messages/ru.js @@ -63,6 +63,8 @@ "col.file": "Файл", "col.size": "Размер", "col.created": "Создан", + "col.publicId": "Публичный ID", + "col.lastUsed": "Последнее использование", "detail.purpose": "Назначение", "detail.category": "Категория", diff --git a/frontend/src/i18n/messages/uk.js b/frontend/src/i18n/messages/uk.js index 5200464..dc3e365 100644 --- a/frontend/src/i18n/messages/uk.js +++ b/frontend/src/i18n/messages/uk.js @@ -63,6 +63,8 @@ "col.file": "Файл", "col.size": "Розмір", "col.created": "Створено", + "col.publicId": "Публічний ID", + "col.lastUsed": "Останнє використання", "detail.purpose": "Призначення", "detail.category": "Категорія",