| 2026-10-03 |

Frontend review fixes: copy buttons, error toasts, page-reset, notes row, localized gate errors
...
- Extension SHA card and token-modal copy buttons wired to GnCopyButton
text (was :value — dead button); :label localized
- Create/save/export/reveal/token flows show error toasts instead of
failing silently; failed create/save keeps the modal and user input
- Query debounce resets secretsPage when the search narrows
- History panel drops the misleading metadata block (show-details=false)
- Notes now visible in the detail panel (between category and tags) when
the secret carries one; notes inputs widened 140 -> 255 chars
- Biometric gate failure/unavailability messages are i18n slugs
(bio.*), rendered localized instead of raw English debug strings
- backups table formats size (KB) and CREATED timestamp
- vite dev proxy points at the real backend port 8018
- en/ru/uk dictionaries kept at full parity (223 keys)
Verified visually on 1280x900 and 390x844; console clean.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
13 hours ago
|
Backend review fixes (stages B3/B4): OAuth state cleanup, callback error mapping, MCP arg parsing
...
- /auth/login and /auth/callback purge expired OAuthState rows
opportunistically (the table previously only grew)
- /auth/callback maps token-exchange/user-fetch failures to a clean
502 AppError instead of an unhandled traceback
- legacy MCP adapter parses offset/limit through a bounded clamped
parser (garbage arguments are 400, not a 500)
- webhook body decodes invalid utf-8 with replacement bytes, which can
only fail the signature check (401) instead of a 500
- tests: 78 -> 80
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
23 hours ago
|

Backend review fixes (stages B1/B2): salted master KDF, AAD-bound ciphertexts, hardened destructive endpoints
...
- crypto: master-key derivation is now scrypt (n=2^15) with a per-install
salt (GNEXUS_CREDS_MASTER_KEY_SALT); plain sha256 kept as the legacy
path so pre-salt data still decrypts; salt is mandatory in production
- crypto: encrypted field envelopes are now bound by aad to
"<user_id>:<secret_id>:<version_id>" (pre-generated version ids) so a
ciphertext transplanted between rows of the same user fails auth
instead of silently decrypting; unbound legacy envelopes decrypt
through a single fallback and are re-bound by scripts/migrate-crypto.py
- secrets.notes widened 140 -> 255 (model, schemas, alembic 0002)
- list_secrets: sort_by restricted to a column whitelist (422 otherwise);
limit clamp aligned with the API layer (1..200)
- update_secret: tag normalization moved to schemas.normalize_tags; the
metadata audit now records the tags diff BEFORE the rows change (the
old value used to be read after the rows were cleared)
- DELETE /account-data now requires Scope.admin (ui channel unaffected)
plus the sensitive rate limiter, and audits the deleted count
- POST /import caps payload.secrets at 1000
- POST /admin/restore takes a typed payload and resolves the filename
against the backup listing; restore_backup itself refuses files
outside the backup dir
- production settings: master_key_salt required, wildcard cors_origins
rejected
- tests: 62 -> 78 (crypto KDF/aad, sort whitelist, notes cap, import cap,
account-data scopes, restore validation, config prod rules)
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
23 hours ago
|
| 2026-10-02 |
Review fixes: trailing-slash URLs, pack refetch on server change, ru autofill
...
Findings from the full extension review (0.2.4):
- api.js built "baseUrl/api/v1/..." without stripping a trailing slash —
"//api/v1/secrets" requests on a base URL saved with one; now stripped
at the single URL-building point (same trap as the webhook 405).
-SAVE_SETTINGS now triggers a design-pack refetch: the install-time fetch
runs against the DEFAULT baseUrl, so a user pointing the extension at
another server would not see that server's pack until browser restart.
- The autofill card failed to fill fields named in russian (логин/почта)
— username-name matching now covers them (password side already did).
- activeTab removed (never used); manifest description is now
user-facing. Version 0.2.4, fresh dist zips and release manifest.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
23 hours ago
|
Save feedback becomes a body-level toast; popup is GNEXUS CREDS again
...
- Settings-drawer save feedback used to live at the bottom of the drawer
body: clipped at real popup height and destroyed 800ms later when the
drawer node was removed. Success now rides a kit toast on document.body
(outlives the drawer, always in viewport); the drawer closes 1600ms in.
- All alert wraps moved to the top of the drawer body so errors never sit
under the fold.
- Kit Drawer quirk fixed at the call sites: close() leaves a-hide on the
element and the class survives the node removal, so reopening the
settings drawer slid the panel back out of view — every reopen clears
the stale class first.
- Titles: static HTML title + runtime localized popupTitle (new pack
string, en/uk/ru); manifest renamed GNEXUS CREDS; version 0.2.3 with
fresh dist zips and pack v25.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
Bearer auth fails closed: a dead token no longer rides the session cookie
...
Presenting an invalid or revoked token used to fall through to the
session cookie in the same request, silently authenticating as
channel=ui with the session's privileges — a revoked token kept
"working" inside a logged-in browser and masked revocation (and the
extension with it). Now a Bearer header is final: a token that does not
authenticate answers 401; cookie auth is only for requests without one.
Regression test added.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
Session death drops the SPA onto the sign-in screen without a reload
...
Any 401 from any API call fires gncreds:unauthenticated; the App listens
and switches to the internal sign-in card immediately, so a finished
session (global auth logout, TTL, revoked session) is visible at once
instead of letting the app look alive while every request fails. api.js
also stops retrying 4xx answers — only network errors and 5xx are
retried, so a dead session is not re-sent three times with backoff.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
Webhook: accept trailing-slash target, 401 on bad signature, global_logout ends sessions
...
POST /webhooks/gnexus-auth/ (the spelling the auth platform had stored)
never redirected: the GET SPA catch-all matched the path and answered
405 allow:GET, so the platform's auth.global_logout deliveries kept
retrying and creds sessions outlived a global logout. Register both
spellings, return 401/400 on signature/payload failures instead of an
unhandled 500, and on auth.global_logout delete every session row of the
targeted user (other users' sessions untouched; API tokens long-lived
by design and revoked explicitly).
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
Revoked tokens stop being invisible: popup plate + token ID, tokens table columns
...
Extension 0.2.2: a 401 from any API call flips a tokenInvalid flag that the
popup renders as a dedicated plate (same load, via status propagation from
the background, plus the pre-flagged check on popup open); the Settings
drawer shows the stored token's public ID so it can be matched against the
tokens table. Web: Tokens tab gains Public ID / Created / Last used columns.
Design pack v24 ships the new strings (en/uk/ru); served zips rehashed.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
Splash over the empty shell on sign-in; extension v0.2.1 catches submit() logins
...
- App.vue gates on auth state: 'checking' renders a logo + spinner card
until /me answers, so the shell never flashes empty before the
sign-in screen appears; 'denied' -> sign-in card, 'ready' -> app.
- content.js: DLE-style <button onclick="submit();"> calls
form.submit(), which fires no submit event - the login form's
interceptor stayed silent (animaunt.org regression). Capture happens
on the submit-button click and Enter too; duplicate captures within
1.5s with identical credentials are dropped. Verified end-to-end on
animaunt.org: modal login -> popup save card -> vault entry.
- Release 0.2.1: Makefile + extension manifest bumped, zips rebuilt
and hashed into the deploy manifest for the download page.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
Logout lands on the internal sign-in screen, no SSO auto-login
...
Nav-style flow: POST /auth/logout then reload the app — /me answers 401,
so the SPA shows its own sign-in gate (logo card with one button) instead
of bouncing to /auth/login, which silently re-authenticated through the
still-alive gnexus-auth session. OAuth now starts only from the explicit
"Sign in with GNEXUS" click; a non-401 failure on load keeps the old
redirect.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
Serve 0.2.0 extension builds from the download page
...
extensions/manifest.json advertised the 0.2.0 filenames while the served
extensions/dist still held the 0.1.1 zips — the page filtered unknown
files and shipped an empty build list, so download buttons never
appeared. Ship the current 0.2.0 builds and refresh their sha256/size,
drop the 0.1.1 zips.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
Settings page hosts the language switcher; profile-edit modal removed
...
The interface-language GnSelect now lives on the Settings page (Auto /
English / Українська / Русский) and applies on change, so the Edit-profile
modal — and the pencil button that opened it — are gone. The dead profile
slugs leave the dictionaries.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
Web app: en/uk/ru localization, language picked from the backend
...
Tiny reactive i18n module (locale ref, t() with {param} interpolation and
ru/uk plural forms, flat per-area dictionaries, en fallback) instead of a
vue-i18n dependency. App.vue and SecretDetailPanel.vue migrate every
hardcoded string to message slugs; /me's locale_effective (settings
override -> auth account -> en) seeds the language on load, the profile
modal gains a language GnSelect (Auto/English/Українська/Русский) that
PATCHes /me and re-applies the effective locale, and <html lang> follows.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
Extension: localized strings ship in the design pack, UI language from the backend
Eugene Sukhodolskiy
committed
1 day ago
|
Per-service locale override: effective locale chain, PATCH /me validation
...
users.locale is now purely the per-service override chosen in settings;
the auth-derived default lives in users.profile.locale and is never
written back over the override by login or the webhook. PATCH /me
normalizes language tags (en-US -> en), accepts auto to clear, and
rejects unsupported locales with 422. UserRead gains locale_effective
(override -> profile -> en) which the web UI and the extension use as
their UI language. The validation error handler strips non-serializable
pydantic ctx.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
Mobile audit: collapse table into event cards with wrapping metadata
...
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
Mobile secrets rows: single-line list without the status badge
...
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
Compact mobile secrets rows: title line plus status/chevron line in one grid card
...
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
UX fixes: pagination ellipsis, mobile layouts, tokens table, danger-zone radius
...
- Pagination re-collapses long page runs to "1 2 3 … 22" (AppPagination, same
look as the kit's) for secrets, audit and admin users.
- Settings metrics: grid columns; metric cards stretch to the full track and
stack one per row on mobile.
- Danger zone gets the standard 6px border radius.
- Revealed fields: one column on mobile, long values wrap instead of pushing
the page into horizontal scroll.
- Secrets table on mobile becomes a card list: category stacks under the
title, purpose column drops out, thead hidden.
- API tokens list is now a full-width table (Name / Scopes / actions).
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
Serve extension design resources from the server as a versioned pack
...
CSS (card + popup), field-detection keyword tables and UI config now ship as
extensions/pack/ with sha256 in pack.json and a monotonic version. The backend
serves pack.json unauthenticated (no-cache) and files versioned/immutable; a
background-worker sync validates schema, ext-version compatibility and hashes,
then stores the pack atomically in chrome.storage.local — content scripts and
the popup only read storage, never fetch, and fall back to the bundled
defaults. Publish tool auto-bumps on content change and writes pack.json last.
Firefox bundle inlines pack-shared.js; release 0.2.0.
E2E (15/15): adoption, live restyle without re-release, rollback and
min-extension guards, sha-mismatch abort, offline fallback. Tests: node 25+21,
pytest 57.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
| 2026-10-01 |

Add PWA shell and a client-side biometric gate on secret reveal
...
PWA: hand-rolled service worker (public/sw.js, no build plugin) with a
precached shell, network-first navigations with a 4s timeout and an
inline offline page, and stale-while-revalidate for /assets and public
files that byte-compares in the background and replaces an entry only
when the server copy differs. API/auth/MCP/webhook traffic is never
intercepted or cached — cookie and secret responses stay out of cache
storage. manifest.webmanifest + theme-color + apple-touch-icon; icons
rasterized from the shield logo by tools/rasterize_icons.py (192/512,
any + maskable). SW registers in main.js under the same
secure-context condition the gate uses, with silent background update
ticks on visibility/online/15min.
Biometric gate (frontend/src/biometric.js): a platform-authenticator
passkey (Android fingerprints through BiometricPrompt, Touch ID,
Windows Hello) is enrolled opt-in from Settings; every reveal and
version reveal runs a fresh WebAuthn assertion with
userVerification:"required", whose clientData/authenticatorData/
signature are verified offline in the same browser against the stored
public key. The server never learns about biometrics — this is an
interface lock, not encryption. Disable requires the same proof, and
the toggle disables itself with an explanatory note on devices
without a platform authenticator.
Also fixes a pre-existing bug the verification run caught: the detail
panel's Hide button wrote `revealed.value = null` in the template,
where refs auto-unwrap, so the panel never returned to masked state.
Verified end to end with a CDP virtual authenticator: 16 checks cover
manifest/SW/offline shell/freshness/no-API-cache, enrollment, reveal
through the gate (HTTP 200), reveal blocked with no sensor, disable
refused without the sensor and allowed with it, and the disabled
toggle + note on a device without biometrics.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
2 days ago
|

Detect login fields with a scoring classifier instead of heuristics
...
field-detect.js is a pure UMD module: every input becomes a descriptor
(name/id/placeholder/aria-label/label text, visibility state, type,
autocomplete token) scored against multilingual keyword tables with
anti-keywords for traps (search, newsletter, cc, otp and now repeat-
password fields). findLoginTargets pairs one username with one password
per form context, merges confirm-password pairs, and falls back to the
closest preceding input in anonymous SPA forms.
content.js reduces to DOM probing (descriptor building + scan) and the
card/interceptor flow; the manifest loads field-detect.js first.
The module is unit-tested without a browser via tools/test-field-detect.js
(16 plain-node cases over a fake tree) and verified end-to-end in Chromium
on a page mixing russian placeholders, newsletter/search/otp/cc traps and
a confirm-password signup form: three cards, correct pairs, Use fills the
right fields only.
Makefile: the per-file $(SRC) replaces the directory prerequisite (a
stale edit inside src/ never rebuilt the bundles), recipes copy with
cp --parents, and make test runs the node suite.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
2 days ago
|

Render the autofill card in a closed Shadow DOM
...
The card used to be injected straight into the page: host-page CSS could
restyle it (or hide/mimic it), page JS could read the card's DOM — leaking
secret titles — and reach its buttons via querySelector on our class names.
Now it lives in a closed shadow root on a bare sized-to-zero host element:
- CSS does not cross the shadow boundary in either direction, so page styles
can no longer repaint the card;
- the page cannot traverse into a closed root (host.shadowRoot is null), so
secret titles and buttons are unreachable from the main world;
- src/content.css is no longer injected into page stylesheets — it is now a
web-accessible resource, fetched once per page and injected as a <style>
inside the shadow root (with a graceful no-styles fallback).
- while restyling the card, bring it onto the gnexus-ui-kit 1.0 palette
(panel #16161e, left accent border, uppercase IBM Plex Mono titles) to
match the rebuilt popup.
Verified end-to-end in real Chromium: isolation probe reports a bare empty
host, null shadowRoot, no card nodes or CSS rules reachable from the page,
no title leak under deliberately hostile page CSS (color:red !important over
all divs); clicking Use fills the form and removes the card.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
2 days ago
|
Polish custom views on kit 1.0: timeline, audit chain, modals
...
- Audit: the GnEmptyState's v-else bound to the GnPagination's v-if, so the
phantom "No audit events" block rendered alongside a populated table; move
pagination outside the loading/rows/empty chain and gate it explicitly.
- Timeline: spell out the text fallback inside the per-item slot (a truthy
dynamic slot always overrides GnTimeline's item.text), humanize created_at
to local "YYYY-MM-DD HH:mm" in history and audit, pluralize the field
count, and draw a connector rail between markers.
- Modals/toasts: drop the app-side a-show/a-hide keyframes — kit 1.0 owns
both transitions itself; align create-modal spacing to the kit's 15px
rhythm and add a hairline separator between modal sections.
Verified with Playwright screenshots (history, audit, create modal
open/close/reopen) on the live server; no page errors.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
2 days ago
|
Release extension 0.1.1: kit 1.0 lib, shield logo icons
...
Rebuild both browsers' packages against gnexus-ui-kit 1.0 (make lib
from frontend/node_modules) and swap the generic dots mark for the
shield keyhole logo rasterized from frontend/public/logo.svg
(icon.svg + 16/32/48/128 PNG rsvg-convert). Popup verified in
chromium: settings drawer and secrets list render with no page errors.
Version bumped in extension manifest + Makefile; extensions/manifest.json
points at the 0.1.1 zips with fresh sha256/size (0.1.0 zips removed).
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
2 days ago
|
Import browser extension source under extensions/extension
...
Move the gnexus-creds-extension sources (Chrome/Firefox MV3) from the
separate sibling repository into extensions/extension/ so builds live
next to the packaged releases this service distributes
(extensions/dist/ + extensions/manifest.json, untouched).
- add a reproducible `make lib` target materializing lib/ from the
frontend's pinned gnexus-ui-kit (gitignored, like before)
- drop the playwright-only package.json/lock; system node+zip suffice
- scoped .gitignore so it cannot shadow tracked extensions/dist zips
- README (main + extension): document the in-repo build and publish flow
Rebuilt zips differ from released ones only in the three lib files
(older kit in the release); extension code itself is byte-identical.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
2 days ago
|
Adopt shield keyhole logo, serve dist public assets from SPA fallback
...
gnexus-creds now has its own mark: a shield with a keyhole, drawn in the
gnexus-ui-kit language (dark tile, line-art strokes, square keyhole
terminal in the role color). Ship it as frontend/public/logo.svg, use it
in the navigation shell instead of the external gnexus.space PNG, and
register it as the SVG favicon.
The SPA fallback in main.py returned index.html for every unknown path,
so /logo.svg rendered the HTML shell instead of the image; serve real
files that exist in frontend/dist first and fall back only afterwards.
Also drop two unused imports flagged by ruff.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
2 days ago
|
Update gnexus-ui-kit to 1.0.0, drop vendored GnModal, use kit tokens
...
Upgrade the git dependency to 1.0.0 (allow-git for npm 12, recorded in
frontend/.npmrc). The vendored GnModal.vue copy is no longer needed: the
kit's overlay transitions are fixed upstream via useOverlayTransition,
and the official component is API-identical. Replace the five hardcoded
palette hexes in styles.css with kit design tokens so theme swaps stay
one import away.
Also restate the form-group hint styles for .tag-input: GnTagInput
renders its hint outside .form-group, so the scoped .form-group
.input-info styles from kit 1.0 miss it and the hint was glued to the
field with no top margin.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
2 days ago
|
| 2026-08-25 |
Refresh extension builds (split Chrome/Firefox)
...
Repackage the Chrome and Firefox builds from the extension repo and
update extensions/manifest.json checksums, sizes, and release date.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 25 Aug
|