github artifact-keeper/artifact-keeper-web v1.9.0
Artifact Keeper Web 1.9.0

latest releases: v1.11.2, v1.11.1, v1.11.0...
one month ago

First web release since 1.8.0 (2026-08-03), cut so that work merged over the last five weeks reaches users — most urgently the proxy Downloads-column fix (#808), merged 2026-08-19, and a rebuild of the published image on current UBI9 packages (#827). Highlights: silent SSO auto-login for users who already hold an IdP session, JupyterLab Extensions as a first-class repository format, SAML IdP group mapping, a per-repository Setup tab, and a batch of Maven browsing and install-snippet fixes.

Thank You

  • @nicola-preda — per-repository Setup tab (#560), migration repo remapping and completion totals (#742), server-side repository search (#739), and proxy cache in the dashboard storage/artifact stats (#737)
  • @sblaisot — artifact-name column capping (#768), exact artifact-list pagination totals (#767), and npm virtual repository creation (#745)
  • @StringKe — the OIDC allow_legacy_rsa_keys opt-in on the provider form (#522)
  • @andrlange — Dart-specific setup snippets for Pub repositories (#747)

Added

  • JupyterLab Extensions in the repository format dropdown + Extension Manager set-me-up (#833, backend artifact-keeper#3784 implemented in artifact-keeper#3786 and artifact-keeper#3783 implemented in artifact-keeper#3788) - users hosting JupyterLab extensions had to know that prebuilt extensions are pip wheels and create a plain PyPI repository, and nothing told them how to point JupyterLab 4's Extension Manager sidebar at it. The create dialog now offers JupyterLab Extensions (jupyter), filed in the Extensions group for the repository-list filter; a jupyter repository is treated exactly like poetry — a PyPI alias — so it gets the Packages default tab, pip install snippets and the PyPI routes. The Setup tab / set-me-up dialog gains a JupyterLab client tab (the default for jupyter repos, a secondary tab on plain pypi repos) with the pip index config for the install side, pip install --index-url <base>/pypi/<repo>/simple/ <extension>, the jupyter_lab_config.py line c.PyPIExtensionManager.base_url = "<base>/pypi/<repo>/pypi", and a note that source extensions come from an npm repository and nbextensions via pip or conda. The Extension Manager line is shown only for hosted and virtual repositories (virtual adds that members must be hosted); on a remote repository the backend faults XML-RPC browse, so the tab shows a note to use a hosted or virtual repository instead. Trade-offs: PyPIExtensionManager has no credential setting, so the sidebar catalogue only works on a repository that allows anonymous read — the tab says so rather than hiding it. The local RepositoryFormat union is hand-maintained (the published SDK types format as a plain string), so a backend older than artifact-keeper#3786 still rejects creating a jupyter repository even though the dropdown offers it.
  • Silent SSO auto-login (check-sso) (#812, backend artifact-keeper#3546) - users who already hold a live IdP session (e.g. Keycloak) previously had to click Sign in with … on every fresh visit; now the app signs them in invisibly. On load, when nobody is signed in, an enabled OIDC provider exists, and no prior attempt is recorded for this browser session, the app makes one silent prompt=none round trip through the IdP: a live IdP session completes the normal callback invisibly and the user lands back on the page they were on, signed in; no session means the IdP answers login_required, the backend relays it as /callback?silent_denied=1, and the visitor lands back where they were, still anonymous, with no error UI and never a visible IdP login page — anonymous registry browsing remains first-class, which is what separates this from a naive sole-provider auto-redirect. Fail open: before navigating anywhere, a short-timeout (4s) same-origin preflight fetches the prompt=none login URL with redirects disabled; the backend only answers with a redirect after its own server-side fetch of the IdP discovery document succeeded, so a down, unreachable, or hung IdP fails the preflight and nothing happens — a visitor can never be stranded on a browser error page. Trade-off accepted: the probe is a top-level navigation, not a hidden iframe, deliberately: IdP session cookies are frequently SameSite=Lax (Keycloak issues Lax unless certain the deployment is HTTPS-proxied), and Lax cookies are never sent on a cross-site iframe navigation — an iframe probe silently reports "no session" for users who have one; the top-level form works on both Lax and None IdPs at the cost of one invisible page round trip per session. Exactly one attempt per browser session (sessionStorage guard, written before the redirect so an interrupted round trip cannot loop), cleared on explicit sign-in and sign-out — so signing out of Artifact Keeper while the IdP session lives on means the next visit signs back in, the standard check-sso semantic. The explicit "Sign in with …" button, ?fallback=local, LDAP, and local login are unchanged; no probe starts on /login, /callback, or /change-password, nor during first-boot setup or demo mode auto-login. Operator kill switch: backend OIDC_SILENT_SSO=false (surfaced as auth.silent_sso_enabled in GET /api/v1/system/config; defaults on, including against older backends that omit the field — there the probe's preflight fails invisibly once and stops).
  • SSO admin UI: map SAML IdP groups to Artifact Keeper groups (#588, backend artifact-keeper#2333 implemented in artifact-keeper#2448) - the SAML provider form gains a Map IdP groups to Artifact Keeper groups switch on both create and edit, default off, mirroring the OIDC toggle from #534. With it on, the values of the assertion attribute named by attribute_mapping.groups (the Groups Attribute field on the same form) become Artifact Keeper group memberships at login: matching groups are auto-created for the provider, a group the provider did not create (including an operator-managed group of the same name) is never reused and that membership is refused instead per the backend's ownership guard, and membership is reconciled on every login, so removing a user from a group at the IdP removes the membership here. Off keeps the legacy role-mapping behavior. The flag is sent on POST /api/v1/admin/sso/saml and PUT /api/v1/admin/sso/saml/{id}, and round-trips through SamlConfigResponse so the edit dialog shows the provider's stored setting rather than resetting to the default. Backends predating the field are handled by a defensive false fallback in the SAML response adapter, matching the existing use_absolute_acs_url handling.
  • Artifact detail dialog shows when an artifact was last downloaded (#472, web half) - the dialog reported size, checksums and dates but never said whether anything had actually pulled the artifact, so deciding whether a file was still in use meant leaving the UI. It now fetches GET /api/v1/artifacts/{id}/stats when it opens and shows a Last downloaded row when the backend has one; the row is hidden on a fetch failure or when nothing has ever downloaded it, so an older backend degrades to the previous dialog rather than showing an error. Backend gap: the uploader half of the same issue waits on artifact-keeper#3271, so no uploader is shown yet.
  • SSO admin UI: surface the OIDC allow_legacy_rsa_keys opt-in (#522, backend artifact-keeper#2039) - completes the pair the #521 entry left open. The OIDC provider form gains an Allow legacy RSA keys (< 2048-bit) switch below Auto Create Users, off by default, so every existing provider keeps its strict pre-migration-139 verification behaviour. Turned on, it opts that one provider into a restricted manual PKCS#1 v1.5 fallback that accepts ID tokens signed with RSA keys shorter than 2048 bits, scoped to RS256 / RS384 / RS512 only — the escape hatch for legacy IdPs such as Lark AnyCross. The trade-off is stated at the point of decision, not in release notes: the help text spells out exactly what gets accepted and carries an amber "Below the OWASP ASVS 4.0 baseline" line, because an operator flipping this is lowering a security floor. Initial state is sourced from config.allow_legacy_rsa_keys and the value is echoed on both create and update.
  • Per-repository Setup tab (#560) - the client-setup guide lived only on the central Setup Guide page, so someone looking at a repository had to navigate away, find it again, and re-select the repository just to learn how to point their tooling at it. The repository detail page gains a Setup tab rendering the same format-aware guide (client-variant tabs for JVM / npm / PyPI, a flat numbered step list for the other formats). The guide is extracted into a shared RepoSetupGuide component now used by both places, so the central page's behaviour is unchanged. Frontend only, no backend dependency.
  • Migration import: rename repositories on import, readable job rows, and completion totals that reach 100% (#742, backend artifact-keeper#3038) - three defects on the admin Migration page. A repository could not be renamed while importing, so a name collision with something already on the instance had to be resolved outside the tool; the import form now sends repo_mappings and each job shows its repo scope. Job rows displayed the raw job UUID, which made a running migration illegible — they now show the repository name/scope. And a completed job's progress bar settled below 100% with the wrong item count; completed jobs now settle at 100% with correct effective counts. Trade-off: the remapping fields ride the SDK passthrough until the SDK regenerates, and a backend predating artifact-keeper#3038 ignores the mappings rather than failing the import.
  • Dashboard Storage Used and Artifacts now count the proxy cache (#737, backend artifact-keeper#3087) - both cards counted only locally hosted artifacts, so an instance whose bytes mostly live in remote-repository caches read as near-empty and its real footprint was invisible from the landing page. The two cards now report local + remote combined, with a hover breakdown splitting Local and Remote. The proxy_* fields are read off the stats API by passthrough until the SDK types land and default to 0, so a backend predating artifact-keeper#3087 shows the previous local-only totals rather than an error.

Fixed

  • Downloads read 0 for every artifact in a proxy repository, whether or not anything was measured (#808, backend artifact-keeper#3388 / artifact-keeper#3446) - the artifact browser rendered download_count verbatim, which was the honest thing to do while the backend hardcoded 0 for every proxy-cached row, but stops being honest from 1.8.0: backend artifact-keeper#3388 now returns a real count for cached rows, and only some formats produce one. Downloads served from the proxy cache are recorded for PyPI, npm, Maven, Ansible, Conda, CRAN, RPM and RubyGems; the other ~19 remote-capable formats — Docker/OCI, cargo, NuGet, Debian, Helm, Go, Composer, Conan and the rest — record nothing at all (artifact-keeper#3446), so their counter sits at 0 no matter how much traffic the proxy serves. Both cases rendered as the same 0, which reads as "nothing is using this proxy" and is exactly the reading that gets a busy repository deleted. On a Remote repository whose format records nothing, the Downloads cell now shows a dash with a hover explanation ("This format does not record downloads served from the proxy cache yet … It is not a measured zero") plus screen-reader text, and the artifact detail dialog says Not tracked instead of 0; a format that does record still shows its real 0, so a measured zero and an unmeasured one are no longer the same glyph. A non-zero count always renders as the number, whichever list says what — a count that exists is proof the format is instrumented, so this can never hide real data behind a dash and a format the backend starts counting mid-release surfaces its traffic without waiting for a web release. Trade-offs: the backend publishes no capability field for this today, so the boundary is a format list in the frontend (src/lib/proxy-downloads.ts) that has to be trimmed as artifact-keeper#3446 lands formats — it is consulted only for a zero, and it reads an API-reported records_proxy_downloads capability off GET /api/v1/formats first, so the day the backend states this per handler the hardcoded list stops being consulted with no further change here. The list is deliberately conservative: format ids that only resemble an instrumented handler (gradle, yarn, pnpm, poetry, conda_native) are left out, since claiming "not tracked" on an idle repository is a smaller error than showing a confident 0 for a busy one. The Packages tab's own Downloads column and the RAW folder-tree badge are untouched (the tree already renders nothing at zero rather than asserting a count).
  • The UI reported the wrong version: web 1.8.0 displayed itself as "Web 1.7.0" (#784) - package.json was not bumped for the 1.8.0 release, so the published 1.8.0 image advertised the previous version everywhere the app names itself: the sidebar's Web/Server pair, the admin Settings page, and GET /api/version. Confirmed against the published image rather than inferred — the client bundle in ghcr.io/artifact-keeper/artifact-keeper-web:1.8.0 carries the literal "1.7.0" at the sidebar render site, and contains no occurrence of 1.8.0 at all. The displayed version is what a user quotes in a bug report and what an operator checks to decide whether a fix landed, so everything shipped in 1.8.0 could be diagnosed as missing and send the investigation to the wrong changelog. Note the APP_VERSION build arg did not mask this and cannot: CI passes APP_VERSION=v1.8.0 from the git tag into ENV NEXT_PUBLIC_APP_VERSION, but next.config.ts sets the same variable to pkg.version literally and that explicit config value wins, so package.json is the sole source of the displayed version regardless of how the image is built. package.json (and the two matching package-lock.json entries) now read 1.8.0, and scripts/assert-version-matches-tag.sh is wired into both docker-publish.yml and release.yml so a tag whose package.json disagrees fails before an image is published or a Release is created. The trade-off: the gate runs at tag time, so it stops a mismatched release rather than preventing the mismatch from reaching main — catching it earlier would mean deciding on main what the next version is, which this repo deliberately does not do.
  • Visual regression CI now compares against baselines instead of silently rewriting them (#781) - the E2E Visual Regression job ran the suite with --update-snapshots, which overwrites each baseline with whatever the page currently renders rather than comparing, so the job reported success no matter what changed and caught zero regressions. Compounding it, all 31 committed baselines were *-darwin.png while CI runs on ubuntu-latest and looks for *-linux.png, so even without the flag the job would have generated every baseline from scratch on each run. The CI invocation now drops --update-snapshots and genuinely compares; the 31 baselines are regenerated as Linux images (captured on the same ubuntu-latest runner CI uses, so font rendering matches) and committed, and the unverified darwin baselines are removed with a .gitignore rule preventing wrong-platform images from re-entering. Baseline refresh moves to a manually dispatched Update Visual Baselines workflow that regenerates and commits the Linux baselines back to the branch under review — so an intentional UI change updates them deliberately, with the image diff visible in the commit, instead of every build erasing the evidence. Since these baselines were never actually compared before, the first comparing run may surface genuine pre-existing visual issues.
  • Local admin credentials form now appears under SSO when the operator allowed it (#615, backend artifact-keeper#2621/#2729) - with an OIDC or SAML provider configured and no LDAP, the login page hid the username/password form unconditionally. Operators who had enabled ALLOW_LOCAL_ADMIN_LOGIN for break-glass access were left with nothing to type into, and /login?fallback=local was the only way to get the form back. That check was a stopgap heuristic (no LDAP + a redirect provider exists means the fields have no consumer) written before the backend published the real policy. The page now reads auth.local_login_enabled from GET /api/v1/system/config, which is the backend's own answer to whether the form should be offered: true when no SSO provider is enabled, and under SSO only when the operator set ALLOW_LOCAL_ADMIN_LOGIN without setting SSO_DISABLE_ADMIN_BREAK_GLASS. The flag is deliberately narrower than the login endpoint's gate, since a verified admin keeps a break-glass password path by default (backend #443) that is not advertised, so ?fallback=local stays on as the supported admin recovery route rather than as a leftover. LDAP is unaffected: an enabled LDAP provider still shows the form regardless of the flag, as does first-time setup. Backends predating the flag omit the field, so parseSystemConfig defaults it to true rather than hiding the only form those deployments have. The form decision now also waits on the system config query in addition to the SSO provider list, so no form flashes and disappears on load, and ?fallback=local bypasses that wait so a hung request cannot strand the page on a spinner.
  • Public system config no longer fails to parse for unauthenticated callers (#615, backend artifact-keeper#1960) - parseSystemConfig required scanners, search_engine, storage_backend and permissions, but the backend made all four admin-only: they are omitted from the response entirely for anonymous and non-admin callers so the instance's security posture cannot be fingerprinted. Every anonymous fetch of GET /api/v1/system/config therefore threw, the query failed, and consumers silently fell back to DEFAULT_SYSTEM_CONFIG. The login page is by definition an anonymous caller, so it never saw a real config at all and could not have honored auth.local_login_enabled. The four fields now fall back to their documented defaults when absent, so the public-safe half of the payload (auth providers, upload limit, guest access, demo mode) parses and reaches the UI. The request also carries a 10 second timeout: apiFetch sets none, and the login page blocks its form decision on this query, so a request that hung rather than failed showed a permanent spinner.
  • Maven downloads arrived without their file extension (#477) - a download took its filename from the artifact's bare name, which for Maven is the artifactId, so files landed as my-lib instead of my-lib-1.2.3.jar and had to be renamed by hand before any tool would accept them. The filename now comes from the last segment of artifact.path, which also fixes the per-revision download in the versions section.
  • Maven classifiers were invisible in the browser and missing from the POM snippet (#482, #474) - the GAV section parsed coordinates out of the path with no classifier support, so -sources, -javadoc and platform-classified files of the same GAV were indistinguishable rows in the flat artifact table and the copyable <dependency> block silently omitted the classifier — pasting it fetched the wrong file. A classifier-aware parseMavenFilename and a full MavenGavc from parseMavenGav now prefer the backend-parsed artifact.metadata (groupId / artifactId / version / extension / classifier) and fall back to the path parse, buildPomDependencySnippet emits <classifier> when there is one, and maven/gradle repositories gain a Classifier column with compound-extension awareness.
  • Searching a Maven repository by GAV matched nothing (#475) - buildMavenSearchQuery sent the dotted groupId as a single search term, but the backend's full-text vector tokenizes the artifact path on /, so a query like com.example could never match a stored path. The dotted groupId is now split into separate AND-ed terms and GAV search returns results.
  • The Group toggle and the server-side grouping disagreed about which formats are groupable (#418) - the toggle's format check and the group_by=docker_tag request gate were written independently, so a format could offer grouping in the UI that the request never asked for (or the reverse). Both now read one shared DOCKER_FAMILY_FORMATS set — docker, podman, buildx, oras, helm_oci, wasm_oci — which is also what the e2e suite expects.
  • A virtual repository with no members could not be created (#754) - the create dialog sent member_repos: [], which the backend rejects outright, so creating an empty virtual repository to fill in later failed with a validation error. The field is now omitted entirely when no members are selected.
  • Maven install snippets put the whole groupId:artifactId inside <artifactId> (#770, backend artifact-keeper#3136) - Maven packages are stored under the colon-joined name groupId:artifactId, and the package views' install-command generator fed that straight into the snippet, so the copyable <dependency> block was not valid Maven and the Gradle and SBT variants carried a literal GROUP: prefix on an already colon-joined name. The split was intact at parse and storage time and lost only at render time. A new parseMavenPackageName splits on the colon — never on dots, which are legal inside both a groupId and an artifactId, so any dot-based heuristic mis-splits real coordinates — and recognises a trailing :version; the maven case now routes through the same buildPomDependencySnippet renderer the repository browser uses, so both views emit identical XML. A colon-free name keeps its previous placeholder behaviour.
  • Returning to a long-backgrounded tab could sign you out (#558) - Chromium can fire the first request after a frozen tab reactivates before the cookie store reattaches: the request goes out cookieless, gets a 401, the refresh is cookieless too, and the app hard-redirects to /login. Users came back to a tab they had left open and found themselves signed out with no explanation. Two layers now cover that window: a disposable warm-up GET /api/v1/auth/me fired 150 ms after the tab becomes visible, so the cookieless first request is spent on a throwaway call rather than a real data query; and a single 500 ms retry of a failed 401 refresh when the tab only recently became visible (a 5 s window) before falling through to the existing redirect. Deliberately narrow: outside that window the immediate redirect is unchanged, as are the refresh mutex/queue semantics, remote instances, and the auth endpoints.
  • A long artifact filename pushed the artifact table off the detail panel, out of reach (#768) - a single artifact whose filename was long enough widened the flat artifact table past the repository detail panel, and the overflow could not be scrolled to: Size, Downloads, Created and the per-row Details/Download actions were clipped with no horizontal scrollbar, and wheel/trackpad/keyboard reached nothing. Name was the only wide column with no width constraint — Path beside it was already capped with max-w-[200px] truncate — and since both table primitives force whitespace-nowrap, nothing wrapped and the column took the full rendered width of the longest filename on the page. That width then escaped the table: the panel's ScrollArea viewport sizes its content with display: table (shrink-to-fit, floored at 100%) and Radix derives the viewport's overflow from the scrollbars that mount, so with only a vertical ScrollBar rendered it is permanently overflow-x: hidden. The excess therefore applied to the whole detail column — also displacing right-aligned controls in the cards above the table, such as the Storage card's "Estimate" button — and was then clipped. The name cell is now capped and elided in the middle rather than the end, via a new MiddleEllipsis component: artifact names are distinguished by their tail as often as their head (…-tlsconsul vs …-tlsconsul-docker, version and variant suffixes, file extensions), so end-truncation would have rendered such rows visually identical. CSS text-overflow can only elide the end, so the value is split into a head that shrinks and truncates and a tail pinned with shrink-0, keeping the result width-responsive rather than guessing character counts against a container whose width is unknown until layout. The full name stays available via the native tooltip and the detail dialog, and the DOM text content stays complete so the name remains selectable, copyable, and correctly announced by screen readers. Affects every format that renders the flat table (generic, npm, and the rest); maven/gradle/docker default to grouped card views that already truncate. Note that the underlying overflow-x: hidden trap remains at every ScrollArea call site — this removes the input that was tripping it, not the trap.
  • Artifact list pagination reported a total that grew with the page number (#767) - the pagination bar under a repository's artifact list read 1-20 of 21 on page 1, 21-40 of 41 on page 2 and 41-60 of 61 on page 3, whatever the repository actually held, with an equally incoherent Page 1 of 2 / Page 2 of 3 beside it. The arithmetic in DataTablePagination was sound; the number handed to it was never a total. The keyset-paged listings (backend artifact-keeper#2520 / #2519 / #2518) deliberately avoid a whole-catalog COUNT on every page request, so pagination.total defaults to a cheap lower bound — offset + rows + has_more — with has_more as the authoritative next-page signal, and exact counts opt-in through ?count=exact, which the web client never sent. Because the bar derives both the range label and totalPages from total, that single lower-bound value produced both symptoms. ListArtifactsParams gains count?: 'exact', forwarded in buildArtifactsListPath (listGrouped builds its query string by hand, since the SDK models no group_by, and silently dropped the new param until it was threaded through), and the repository-content query now opts in. No backend change was required — ?count=exact already exists and is honoured on every branch of the handler (flat hosted, virtual, remote-cached, group_by=maven_component for hosted and remote, group_by=docker_tag). One query feeds the flat table, the Maven component list and the Docker tag list, so all three views are fixed together. This costs one extra COUNT query per page load, which is precisely what artifact-keeper#2520 set out to avoid; accepted here because this surface renders a total and a page count, so it genuinely needs a real one. Listings that only need "is there a next page" should keep count unset and read has_more.
  • Pub (Dart/Flutter) repositories showed a generic curl -X PUT instead of Dart setup steps (#747) - getSetupSnippets had no pub case, so a Pub repository fell through to the default branch and told users to upload with curl against the generic artifact route. Dart clients do not work that way — dart and flutter resolve through PUB_HOSTED_URL, and the backend serves the Pub protocol at /pub/<repo_key> — and neither appeared anywhere in this repository before. The Setup guide now gives Pub five real steps: export PUB_HOSTED_URL, dart pub token add, a hosted: dependency block for pubspec.yaml, dart pub get / flutter pub get, and dart pub publish. No other format is touched and the default: branch is unchanged for the formats that legitimately use it.
  • npm virtual repositories can be created again; npm remotes no longer silently block unscoped packages (#745) - the create dialog offered the npm scope-policy section for virtual repositories and always attached npm_allowed_scopes / npm_allowed_name_patterns / npm_allow_unscoped to the request, even when the operator never touched it. The backend gate is presence-based ([] deserializes to Some([]), false to Some(false)), so every npm virtual create failed with Validation error: npm scope policy is only configurable on remote (proxy) repositories and no repository was created. The policy is stored on and read from each Remote member — the npm virtual resolver consults the member's policy during candidate selection (backend artifact-keeper#2327/#2424) — so hasNpmScopePolicy now returns true only for npm remote, and the misleading comment claiming both types qualify is corrected. Second, worse half: a stored allow_unscoped: false makes the policy is_active(), and an active policy denies every unscoped name, so creating an npm proxy through the UI with the section untouched silently blocked bare package names (react, lodash, express) despite the form's own "Empty leaves the repository unrestricted by scope" hint. Create-time submission is now gated on a new hasNpmScopePolicyInput predicate, so an untouched section sends nothing and the backend's allow-all default stands. buildNpmScopePolicyFields still emits every key unconditionally — that is what lets the settings tab clear a stored allow-list, and the settings tab keeps its own "changed vs stored" gate.
  • Repository search only matched the repositories on the page you happened to be looking at (#739) - the search box filtered the rows already loaded for the current page, so a repository whose name or key matched but which lived on another page simply never appeared, and there was no way to tell it apart from one that does not exist. The query is now passed to the list endpoint (q) and runs server-side across every repository. Because total_pages now describes the filtered set, pagination hides itself for a handful of matches and stays available when there are many.

Security

  • Clear the npm audit --audit-level=high advisories that turned the Dependency Audit CI job red on every fresh PR run (#836) - main's lockfile had aged into 10 advisories (1 critical, 5 high, 4 moderate), so any PR whose CI ran after the advisories were published failed the gate for reasons unrelated to its diff. next 16.2.12 → 16.3.3 (GHSA-p293-qw3h-jr36 and GHSA-2xp9-vwfh-vxw4, both critical unauthenticated RCE — 16.3.3 is the smallest release carrying the fixes; next is pinned exactly, so this is a deliberate package.json bump rather than npm audit fix --force), sharp 0.35.3 → 0.35.4 via the existing overrides.next.sharp floor (GHSA-rgj7-g3m4-5g8c, libheif), js-yaml 4.3.1 → 4.3.2 via the existing overrides.js-yaml floor (GHSA-2883-xcg3-v3hh, transitive through @hey-api/openapi-ts), vitest / @vitest/coverage-v8 / @vitest/mocker 4.1.10 → 4.1.11 (GHSA-82fw-gwwq-j7x9), and @humanfs/node 0.16.7 → 0.16.8 (GHSA-p498-v437-472g, transitive through eslint). Audit is now clean at every level. Trade-off / trap noted: Next 16.3.0 turned on experimental.useTypeScriptCli by default, so next build now type-checks the whole tsconfig.json include set (test files included) instead of only app code; 22 pre-existing tsc --noEmit errors in audit/__tests__/page.test.tsx and security/blast-radius/__tests__/page.test.tsx (mock helpers defaulting data: undefined, which TypeScript narrowed to the literal undefined type) would have failed the build, so both helpers now declare data: undefined as unknown. tsc --noEmit is at 0 errors as a result. Also, npm audit fix and npm install vitest@4.1.11 crash npm 10.9.2's arborist (Cannot read properties of null (reading 'edgesOut') while walking vitest's optional-peer chain) — the lockfile was regenerated with npm 11.19.1 and then verified with a clean npm ci on npm 10.9.2 / Node 22, which is what CI runs.
  • Build provenance is no longer allowed to fail silently in docker-publish (#815) - the Generate artifact attestation step carried a bare continue-on-error: true, so if attaching provenance to the published multi-arch image had ever failed, the step would have turned yellow while the Multi-Arch Manifest job still reported success — needs.merge.result green, nothing annotated, and the image live on ghcr.io and Docker Hub with no provenance. That is the shape of the regression that shipped backend 1.6.1 unsigned (artifact-keeper#2824). Verified before removing it: the step has never actually failed (success in all 99 of the last 100 runs that reached the job), the job already grants id-token: write and attestations: write, and the digest currently behind :dev does carry an attestation — so this was a disarmed alarm rather than a live gap, and no web image has shipped without provenance. The soft-fail is now gone and an attestation failure fails the publish job.

What's Changed

  • docs(changelog): scope [1.8.0] to the actual change; restore backlog to [Unreleased] by @brandonrc in #736
  • chore: bump undici from 7.28.0 to 7.29.0 by @dependabot[bot] in #741
  • feat(dashboard): include proxy cache in storage/artifact stats with hover breakdown by @nicola-preda in #738
  • chore(deps): fix npm audit failures (nanoid 3.3.18, postcss 8.5.26) by @brandonrc in #750
  • chore: bump @aws-sdk/client-polly from 3.1094.0 to 3.1105.0 by @dependabot[bot] in #724
  • chore: bump actions/checkout from 6.0.2 to 7.0.1 by @dependabot[bot] in #718
  • chore: bump docker/build-push-action from 7.2.0 to 7.3.0 by @dependabot[bot] in #719
  • chore: bump docker/setup-buildx-action from 4.1.0 to 4.2.0 by @dependabot[bot] in #721
  • chore: bump @tanstack/react-query from 5.100.9 to 5.101.4 by @dependabot[bot] in #722
  • chore: bump @playwright/test from 1.58.2 to 1.62.1 by @dependabot[bot] in #725
  • chore: bump lucide-react from 1.27.0 to 1.29.0 by @dependabot[bot] in #727
  • chore: bump react-qr-code from 2.0.21 to 2.2.0 by @dependabot[bot] in #728
  • chore: bump react-hook-form from 7.75.0 to 7.84.0 by @dependabot[bot] in #729
  • chore: bump github/codeql-action/analyze from 4.36.0 to 4.37.6 by @dependabot[bot] in #730
  • chore: bump @vitejs/plugin-react from 6.0.4 to 6.0.5 by @dependabot[bot] in #731
  • chore: bump docker/login-action from 4.5.2 to 4.6.0 by @dependabot[bot] in #732
  • fix(repositories): show search matches from every page, not just the current one by @nicola-preda in #740
  • fix(repositories): stop sending npm scope policy for non-remote repos (#745) by @sblaisot in #746
  • feat(migration): repo remapping, useful job labels, and correct completion totals by @nicola-preda in #743
  • feat(repositories): add a Setup tab with the per-repo client-setup guide (#560) by @nicola-preda in #744
  • feat(sso): surface OIDC allow_legacy_rsa_keys opt-in on the provider edit form by @StringKe in #524
  • ci(codeql): pin init to v4.37.6 to match analyze (#730 follow-up) by @brandonrc in #753
  • fix(setup): give Pub repositories Dart-specific setup snippets by @andrlange in #748
  • test(e2e): Playwright coverage for the 1.8.x feature batch (#738/#740/#743/#744/#746/#748) by @brandonrc in #765
  • chore: bump @testing-library/user-event from 14.6.1 to 14.6.3 by @dependabot[bot] in #755
  • chore: bump @types/react from 19.2.17 to 19.2.18 by @dependabot[bot] in #756
  • chore: bump eslint-config-next from 16.2.6 to 16.3.0 by @dependabot[bot] in #757
  • chore: bump @types/react-dom from 19.2.3 to 19.2.4 by @dependabot[bot] in #758
  • chore: bump shiki from 4.3.1 to 4.4.2 by @dependabot[bot] in #759
  • chore: bump @hookform/resolvers from 5.4.0 to 5.7.1 by @dependabot[bot] in #760
  • chore: bump aws-actions/configure-aws-credentials from 6.1.1 to 6.2.3 by @dependabot[bot] in #762
  • chore: bump docker/metadata-action from 6.0.0 to 6.2.0 by @dependabot[bot] in #763
  • chore: bump actions/attest-build-provenance from 4.1.1 to 4.2.2 by @dependabot[bot] in #764
  • fix: request an exact total for the repository artifact list pagination by @sblaisot in #766
  • fix: cap and middle-elide the artifact name column so a long filename cannot push the table off-panel by @sblaisot in #769
  • Fix Maven install snippet: split colon-joined package name into groupId/artifactId by @brandonrc in #771
  • fix(auth): retry token refresh after tab unfreeze before redirecting to login (#558) by @brandonrc in #772
  • fix(repositories): Maven GAVC parsing, download filenames, groupable formats, member-less virtual create by @brandonrc in #773
  • ci: fix Mergify Dependabot auto-merge check names and skip linked-issue check for bots by @brandonrc in #777
  • fix(login): gate the local credentials form on auth.local_login_enabled by @brandonrc in #778
  • feat(sso): add SAML "Map IdP groups to Artifact Keeper groups" toggle by @brandonrc in #779
  • test: pin grouped pagination.total as a component/tag count (#417) by @brandonrc in #780
  • docs: changelog entries for #767 and #768, and require one per PR by @sblaisot in #775
  • ci(visual): compare against baselines instead of rewriting them (#781) by @brandonrc in #783
  • chore: bump @types/node from 25.6.2 to 26.2.0 by @dependabot[bot] in #723
  • chore: bump jsdom from 29.1.1 to 30.0.1 by @dependabot[bot] in #726
  • chore: bump framer-motion from 12.38.0 to 13.0.0 by @dependabot[bot] in #761
  • fix: bump package.json to 1.8.0 and gate version against the release tag by @brandonrc in #785
  • fix(repositories): distinguish untracked proxy downloads from a measured zero by @brandonrc in #809
  • ci(docker-publish): fail the publish when build provenance cannot be attached by @brandonrc in #816
  • chore(deps): clear high/critical npm audit advisories by @brandonrc in #837
  • test(e2e): refresh the admin monitoring desktop visual baseline by @brandonrc in #838
  • feat(auth): silent SSO auto-login (check-sso) with fail-open iframe probe by @brandonrc in #813
  • feat(repositories): JupyterLab Extensions format in the dropdown with Extension Manager set-me-up by @brandonrc in #834
  • chore(release): prepare 1.9.0 by @brandonrc in #843

New Contributors

Full Changelog: v1.8.0...v1.9.0

Don't miss a new artifact-keeper-web release

NewReleases is sending notifications on new releases.