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_keysopt-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; ajupyterrepository is treated exactly likepoetry— a PyPI alias — so it gets the Packages default tab,pip installsnippets and the PyPI routes. The Setup tab / set-me-up dialog gains a JupyterLab client tab (the default forjupyterrepos, a secondary tab on plainpypirepos) with the pip index config for the install side,pip install --index-url <base>/pypi/<repo>/simple/ <extension>, thejupyter_lab_config.pylinec.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-RPCbrowse, so the tab shows a note to use a hosted or virtual repository instead. Trade-offs:PyPIExtensionManagerhas 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 localRepositoryFormatunion is hand-maintained (the published SDK typesformatas a plainstring), so a backend older than artifact-keeper#3786 still rejects creating ajupyterrepository 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=noneround 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 answerslogin_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 theprompt=nonelogin 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 frequentlySameSite=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: backendOIDC_SILENT_SSO=false(surfaced asauth.silent_sso_enabledinGET /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 onPOST /api/v1/admin/sso/samlandPUT /api/v1/admin/sso/saml/{id}, and round-trips throughSamlConfigResponseso the edit dialog shows the provider's stored setting rather than resetting to the default. Backends predating the field are handled by a defensivefalsefallback in the SAML response adapter, matching the existinguse_absolute_acs_urlhandling. - 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}/statswhen 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_keysopt-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 fromconfig.allow_legacy_rsa_keysand 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
RepoSetupGuidecomponent 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_mappingsand 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 to0, so a backend predating artifact-keeper#3087 shows the previous local-only totals rather than an error.
Fixed
- Downloads read
0for every artifact in a proxy repository, whether or not anything was measured (#808, backend artifact-keeper#3388 / artifact-keeper#3446) - the artifact browser rendereddownload_countverbatim, which was the honest thing to do while the backend hardcoded0for 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 at0no matter how much traffic the proxy serves. Both cases rendered as the same0, 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 of0; a format that does record still shows its real0, 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-reportedrecords_proxy_downloadscapability offGET /api/v1/formatsfirst, 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 confident0for 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.jsonwas 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, andGET /api/version. Confirmed against the published image rather than inferred — the client bundle inghcr.io/artifact-keeper/artifact-keeper-web:1.8.0carries the literal"1.7.0"at the sidebar render site, and contains no occurrence of1.8.0at 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 theAPP_VERSIONbuild arg did not mask this and cannot: CI passesAPP_VERSION=v1.8.0from the git tag intoENV NEXT_PUBLIC_APP_VERSION, butnext.config.tssets the same variable topkg.versionliterally and that explicit config value wins, sopackage.jsonis the sole source of the displayed version regardless of how the image is built.package.json(and the two matchingpackage-lock.jsonentries) now read 1.8.0, andscripts/assert-version-matches-tag.shis wired into bothdocker-publish.ymlandrelease.ymlso a tag whosepackage.jsondisagrees 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 reachingmain— catching it earlier would mean deciding onmainwhat 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 Regressionjob 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.pngwhile CI runs onubuntu-latestand 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-snapshotsand genuinely compares; the 31 baselines are regenerated as Linux images (captured on the sameubuntu-latestrunner CI uses, so font rendering matches) and committed, and the unverified darwin baselines are removed with a.gitignorerule 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_LOGINfor break-glass access were left with nothing to type into, and/login?fallback=localwas 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 readsauth.local_login_enabledfromGET /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 setALLOW_LOCAL_ADMIN_LOGINwithout settingSSO_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=localstays 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, soparseSystemConfigdefaults it totruerather 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=localbypasses 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) -
parseSystemConfigrequiredscanners,search_engine,storage_backendandpermissions, 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 ofGET /api/v1/system/configtherefore threw, the query failed, and consumers silently fell back toDEFAULT_SYSTEM_CONFIG. The login page is by definition an anonymous caller, so it never saw a real config at all and could not have honoredauth.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:apiFetchsets 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 asmy-libinstead ofmy-lib-1.2.3.jarand had to be renamed by hand before any tool would accept them. The filename now comes from the last segment ofartifact.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,-javadocand 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-awareparseMavenFilenameand a fullMavenGavcfromparseMavenGavnow prefer the backend-parsedartifact.metadata(groupId / artifactId / version / extension / classifier) and fall back to the path parse,buildPomDependencySnippetemits<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) -
buildMavenSearchQuerysent the dotted groupId as a single search term, but the backend's full-text vector tokenizes the artifact path on/, so a query likecom.examplecould 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_tagrequest 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 sharedDOCKER_FAMILY_FORMATSset —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:artifactIdinside<artifactId>(#770, backend artifact-keeper#3136) - Maven packages are stored under the colon-joined namegroupId: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 literalGROUP:prefix on an already colon-joined name. The split was intact at parse and storage time and lost only at render time. A newparseMavenPackageNamesplits 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; themavencase now routes through the samebuildPomDependencySnippetrenderer 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-upGET /api/v1/auth/mefired 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 forcewhitespace-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'sScrollAreaviewport sizes its content withdisplay: table(shrink-to-fit, floored at 100%) and Radix derives the viewport's overflow from the scrollbars that mount, so with only a verticalScrollBarrendered it is permanentlyoverflow-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 newMiddleEllipsiscomponent: artifact names are distinguished by their tail as often as their head (…-tlsconsulvs…-tlsconsul-docker, version and variant suffixes, file extensions), so end-truncation would have rendered such rows visually identical. CSStext-overflowcan only elide the end, so the value is split into a head that shrinks and truncates and a tail pinned withshrink-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 underlyingoverflow-x: hiddentrap remains at everyScrollAreacall 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 21on page 1,21-40 of 41on page 2 and41-60 of 61on page 3, whatever the repository actually held, with an equally incoherentPage 1 of 2/Page 2 of 3beside it. The arithmetic inDataTablePaginationwas sound; the number handed to it was never a total. The keyset-paged listings (backend artifact-keeper#2520 / #2519 / #2518) deliberately avoid a whole-catalogCOUNTon every page request, sopagination.totaldefaults to a cheap lower bound —offset + rows + has_more— withhas_moreas 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 andtotalPagesfromtotal, that single lower-bound value produced both symptoms.ListArtifactsParamsgainscount?: 'exact', forwarded inbuildArtifactsListPath(listGroupedbuilds its query string by hand, since the SDK models nogroup_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=exactalready exists and is honoured on every branch of the handler (flat hosted, virtual, remote-cached,group_by=maven_componentfor 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 extraCOUNTquery 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 keepcountunset and readhas_more. - Pub (Dart/Flutter) repositories showed a generic
curl -X PUTinstead of Dart setup steps (#747) -getSetupSnippetshad nopubcase, so a Pub repository fell through to the default branch and told users to upload withcurlagainst the generic artifact route. Dart clients do not work that way —dartandflutterresolve throughPUB_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: exportPUB_HOSTED_URL,dart pub token add, ahosted:dependency block forpubspec.yaml,dart pub get/flutter pub get, anddart pub publish. No other format is touched and thedefault: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
virtualrepositories and always attachednpm_allowed_scopes/npm_allowed_name_patterns/npm_allow_unscopedto the request, even when the operator never touched it. The backend gate is presence-based ([]deserializes toSome([]),falsetoSome(false)), so every npm virtual create failed withValidation error: npm scope policy is only configurable on remote (proxy) repositoriesand 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) — sohasNpmScopePolicynow returns true only for npm remote, and the misleading comment claiming both types qualify is corrected. Second, worse half: a storedallow_unscoped: falsemakes the policyis_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 newhasNpmScopePolicyInputpredicate, so an untouched section sends nothing and the backend's allow-all default stands.buildNpmScopePolicyFieldsstill 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. Becausetotal_pagesnow 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=highadvisories that turned theDependency AuditCI 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.next16.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;nextis pinned exactly, so this is a deliberatepackage.jsonbump rather thannpm audit fix --force),sharp0.35.3 → 0.35.4 via the existingoverrides.next.sharpfloor (GHSA-rgj7-g3m4-5g8c, libheif),js-yaml4.3.1 → 4.3.2 via the existingoverrides.js-yamlfloor (GHSA-2883-xcg3-v3hh, transitive through@hey-api/openapi-ts),vitest/@vitest/coverage-v8/@vitest/mocker4.1.10 → 4.1.11 (GHSA-82fw-gwwq-j7x9), and@humanfs/node0.16.7 → 0.16.8 (GHSA-p498-v437-472g, transitive througheslint). Audit is now clean at every level. Trade-off / trap noted: Next 16.3.0 turned onexperimental.useTypeScriptCliby default, sonext buildnow type-checks the wholetsconfig.jsonincludeset (test files included) instead of only app code; 22 pre-existingtsc --noEmiterrors inaudit/__tests__/page.test.tsxandsecurity/blast-radius/__tests__/page.test.tsx(mock helpers defaultingdata: undefined, which TypeScript narrowed to the literalundefinedtype) would have failed the build, so both helpers now declaredata: undefined as unknown.tsc --noEmitis at 0 errors as a result. Also,npm audit fixandnpm install vitest@4.1.11crash 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 cleannpm cion npm 10.9.2 / Node 22, which is what CI runs. - Build provenance is no longer allowed to fail silently in
docker-publish(#815) - theGenerate artifact attestationstep carried a barecontinue-on-error: true, so if attaching provenance to the published multi-arch image had ever failed, the step would have turned yellow while theMulti-Arch Manifestjob still reportedsuccess—needs.merge.resultgreen, 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 (successin all 99 of the last 100 runs that reached the job), the job already grantsid-token: writeandattestations: write, and the digest currently behind:devdoes 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
- @sblaisot made their first contribution in #746
- @StringKe made their first contribution in #524
- @andrlange made their first contribution in #748
Full Changelog: v1.8.0...v1.9.0