github artifact-keeper/artifact-keeper-web v1.10.0
Artifact Keeper Web 1.10.0

latest releases: v1.11.0, v1.10.1
27 days ago

Added

  • Build tab and Image inspector for container repositories (#875, backend artifact-keeper#4034) - docker, podman, buildx and oras repositories gain a Build tab: what the builder can do here (enabled, base-image allowlist, policy, concurrency, push address), the builds so far polled while any is active, and a New image wizard whose Containerfile preview is the server's dry run (POST …/image-builds/render), so the validation shown is the one the build applies. Packages are ordered groups, each with a manager dropdown limited to what the instance reports as supported (apt, dnf, microdnf, yum, apk, pip, conda); the dropdown is narrowed to what the base image is — read from the image's own build history and labels through the backend's base-info endpoint when the base is stored in this registry, guessed from the name otherwise (UBI → dnf/microdnf, Alpine → apk, Debian/Ubuntu/python/Ray → apt), with show all managers lifting the filter — and Add package group pre-selects that manager. Conda groups take channels, system groups warn that they install as root and need a final user, a two-stage checkbox keeps pip's caches out of the pushed image, and when the instance allows it a Dockerfile mode replaces the form. A build's detail shows its log as it streams, the exact Containerfile and, once pushed, the image; Rebuild with changes reopens the wizard from its spec, with legacy apt/conda/pip shorthand folded into groups. Every manifest's detail dialog gains an Image tab: digest, platforms, size, user, workdir, entrypoint/cmd, ports, provenance, a Dockerfile view (the real one from the provenance attestation when present, otherwise the build history reconstructed as instructions with per-step layer sizes), layers with their share of the image, env and labels. Types and calls are hand-written in src/lib/api/image-builds.ts until the SDK is regenerated.

  • Repository deduplicated storage usage panel (#593, backend epic artifact-keeper#2056) - the repository detail view gains a Storage panel that reports the real, dedup-aware footprint instead of only the coarse logical storage_used_bytes: logical vs physical bytes, dedup ratio and savings (bytes + %), a unique-vs-shared stacked bar, the dedup_scope label (per_repo / instance) with an inline caveat that instance-scope figures reflect blobs pooled across the whole instance, and computed_at freshness (hover for the exact timestamp). Admins additionally see the instance-wide instance_unique_bytes total and an on-demand "reclaimable now" estimate (a storage-gc dry-run — nothing is deleted). Security (backend artifact-keeper#2560): on instance-scope backends the backend omits physical_bytes/unique_bytes/shared_bytes/dedup_ratio for non-admin viewers, so the panel degrades gracefully — a non-admin on instance scope sees only logical size + blob count plus a note that the detailed breakdown is admin-only, and the descriptive shared-bytes copy + instance total + reclaimable estimate are gated on admin both server-side and in the UI. The per-repository endpoints are not in the generated SDK yet (v1.5.0 only ships the instance-wide /admin/analytics/storage/breakdown + /admin/storage-gc), so src/lib/api/storage.ts uses the shared apiFetch wrapper (same pattern as routing-rules / audit / downloads); if the endpoint is unavailable the panel falls back to the repository's logical storage_used_bytes. The folder / path-tree storage rollup (epic sub-task 4) is intentionally out of scope here.

  • External scan type + scan and finding filters (#858, backend artifact-keeper#3411 / #3410) - backend 1.10.0 lets out-of-tree scanners post verdicts (POST /api/v1/security/findings/external), which land as scans with the new scan_type value external. Every scan table printed scan_type raw, so those rows showed up as a bare "external" with no indication of what they were. New shared ScanTypeBadge / scanTypeLabel() renders an icon + readable label for every known scan type (unknown values still render verbatim rather than being hidden) on the scan list, the scan detail header and the per-artifact Scan Results table, with the scan's scanner_version in the tooltip. The scan list gains a scan type filter wired to ?scan_type=, and the per-scan findings table gains severity / source / CVE filters wired to ?severity=&source=&cve_id= — the severity filter used to run client-side over the current page only, so it both under-reported matches and made total lie; filtering now happens in the backend and the response total drives pagination. A new Source column surfaces each finding's reporting scanner, which is where an external submission's vendor identity lives (scan_type is deliberately generic). A rejected filter value comes back as a 400 naming the accepted set; that message is shown verbatim in an alert and the table is emptied, so a typo'd engine name never reads as "no scans from that engine". The new query parameters are not in the generated SDK yet (1.7.0), so src/lib/api/security.ts declares them on its own params types and hands them to the SDK's query serializer, which forwards every key it is given.

  • CVE Blast-Radius view (#570) - new /security/blast-radius admin page surfacing the blast-radius endpoints backend #2364 added (GET /api/v1/admin/security/cve/{cve_id}/blast-radius, /security/artifact/{artifact_id}/blast-radius): given a CVE/GHSA id or an artifact id, shows who is exposed. Summary tiles (affected artifacts, affected repos, distinct downloaders, distinct IPs, total downloads) plus an "Anonymous downloads present" badge; an affected repositories table that classifies each repo's reachability and loudly flags access_scope=public as "Public — everyone exposed" (vs. restricted-ACL / restricted-roles); and a paginated downloaders table (username or 'anonymous', download count, distinct-IP spread with a sample preview, first/last download) with server-side page/per_page pagination (default 20, max 100 per the backend cap). Target ids are validated client-side (CVE/GHSA format, artifact UUID) and CVE ids are normalized to canonical casing. Deep-linkable via ?cve= / ?artifact=, and scan-finding advisory cells on the scan detail page now carry a crosshair drill-in link to the report. New "Blast Radius" entry in the Security nav group. The endpoints are not in the generated SDK yet, so src/lib/api/blast-radius.ts uses the shared apiFetch wrapper with zod validation at the trust boundary (same pattern as audit and downloads); on a backend without the endpoints the page degrades to an "unavailable" alert.

  • Admin maintenance card: storage-ledger and package-catalog backfills (#859, backend artifact-keeper#3650, artifact-keeper#3659/#3660) - the admin Settings page gains a Maintenance card exposing the two idempotent repair actions backend 1.10.0 added, both of which operators are expected to run once after an upgrade or a restore-from-backup and neither of which had any UI. Recompute storage ledger (POST /api/v1/admin/storage/ledger/backfill) rebuilds repository_usage_ledger from the artifact / proxy-cache / OCI blob tables — a database restored from backup never fires the triggers that maintain it, so the storage headline can understate the real figure by orders of magnitude — and reports the run as totals (repositories checked / repaired / skipped, total drift, headline before vs after) plus a per-repository drift table showing only the rows the backfill actually changed, with "No ledger row" called out for the restore case and signed byte deltas (formatBytes returns the -- sentinel for negatives, #348, so the sign is carried separately). Backfill package catalog (POST /api/v1/admin/packages/backfill?repository_key=…) replays existing artifacts through the catalog upsert so packages published before catalog registration landed become visible on the Packages page, with an optional repository picker (defaulting to every catalog-eligible hosted repository) and a scanned / registered / skipped / failed count panel. Both actions sit behind a confirm dialog, show an in-flight state, surface the backend's own message inline and in a toast on failure, and invalidate the affected queries on success; the card renders nothing for a non-admin, matching the server-side admin gate. The endpoints are not in the generated SDK yet (backend 1.10.0), so src/lib/api/maintenance.ts uses the shared apiFetch wrapper with zod validation at the trust boundary (same pattern as rate-limits and downloads).

  • Generic artifact version history UI (#571) - surfaces the first-class versioning backend #2367 added for Generic/Mlmodel repositories (GET /api/v1/repositories/{key}/versions/{path}, ?version=<rev|label|latest> on the download/metadata routes, and a per-repo versioning_enabled flag). In the repository artifact browser, the artifact detail dialog gains a Versions tab — a table of stored revisions (revision number with a "latest" badge, optional version label, size, short SHA-256 with copy, uploader when the backend provides it, and stored date) with a per-revision download that pins the exact stored bytes via ?version=<revision> (composing a download ticket onto the version-selected URL). The tab only appears for repositories that opted into versioning AND whose format participates (Generic/Mlmodel); an artifact with no recorded history renders a quiet empty state and the normal single-artifact download is unchanged, so existing (non-versioned) repos are unaffected. The repository Settings tab gains an Artifact Versioning section with an "Enable versioning" toggle (Generic/Mlmodel only) that writes versioning_enabled through the existing update-repository endpoint. The endpoints are not in the generated SDK yet, so src/lib/api/versions.ts uses the shared apiFetch wrapper with zod validation at the trust boundary (same pattern as audit/downloads), normalizes the backend's 404-on-no-history to an empty list, and repositoriesApi reads/writes versioning_enabled through a narrowed cast until the SDK regenerates. New ArtifactVersionsPage e2e page object + artifact-version-history.spec.ts (API contract: newest-first list, ?version= pins old bytes, versioning_enabled round-trip; best-effort admin UI checks), and a versioning_enabled: true generic repo seeded for the e2e stack. Backend gap: the /versions response does not serialize uploaded_by yet (the column exists on artifact_versions), so the uploader column stays hidden until the backend adds it — the UI plumbs the field through defensively.

  • SAML provider slug and slug-addressed login/ACS URLs (#856, backend artifact-keeper#2583) - SAML providers gain an optional operator-chosen slug that the public SAML routes accept in place of the provider UUID (GET /api/v1/auth/sso/saml/{id|slug}/login, POST .../acs), so the ACS URL registered at the IdP survives this deployment being rebuilt from scratch. The SAML provider form gains a Slug (optional) input validated client-side against the backend's rule (^[a-z0-9][a-z0-9_-]*$, at most 64 characters, never UUID-shaped) with the offending spelling explained inline and the save blocked until it is fixed, plus a read-only Login URL / ACS URL pair with copy buttons that is derived from the slug as it is typed and falls back to the provider id when there is none. Leaving the field blank on an edit preserves the stored slug, matching the backend's update semantics — a slug can be replaced but not removed. A duplicate name or slug is a 409 since #2583 (it used to be a generic 500) and now lands as an error on the field that actually collided rather than in a toast; the SAML list gains a Slug column. slug is not in the generated SDK yet, so adaptSamlConfig reads it through a cast defaulting to null (the same pattern as allow_legacy_rsa_keys) and the create/update bodies forward it untouched; a backend that predates migration 218 simply reports no slug and every provider stays addressable by its id.

  • Download Attribution & Network-Topology dashboard (#569) - new /downloads admin page surfacing the download-attribution endpoints backend #2365 added (GET /api/v1/admin/downloads, /downloads/by-ip/{ip}, /downloads/by-user/{user_id}). Three views over attributed download events: Events (paginated table of time, user — 'anonymous' when unauthenticated, client IP, artifact, user agent), By IP / Subnet (network-topology grouping: downloads, unique users, unique artifacts, and last activity per client IP with its /24 or /64 subnet), and By User (per-user activity with unique-IP spread). Grouped views aggregate client-side over the most recent matching events (the backend returns rows, not aggregates) and note when the sample is truncated; each group row drills back into the filtered Events view. Filters: artifact id and user id (UUID-validated client-side), exact client IP, and an inclusive date range, plus server-side page/per_page pagination (default 20, max 100 per the backend cap). An exclusive IP or user filter is routed through the dedicated by-ip/by-user endpoint. New "Downloads" entry in the Operations nav group. The endpoints are not in the generated SDK yet, so src/lib/api/downloads.ts uses the shared apiFetch wrapper with zod validation at the trust boundary (same pattern as rate-limits and audit); on a backend without the endpoints the page degrades to an "unavailable" alert.

  • AWS ECR and CodeArtifact upstream auth types (#857, backend 1.10.0 artifact-keeper#1559) - the remote repository's upstream-authentication form now offers aws_ecr and aws_codeartifact beside Basic and Bearer, with a conditional AWS section (region required for both, CodeArtifact domain required for CodeArtifact, plus the optional ECR registry id / domain owner / token lifetime). Neither type takes a password: the backend mints a short-lived token from the server's own AWS identity (IRSA, EKS Pod Identity, an instance profile or AWS_* environment keys), so a proxied ECR/CodeArtifact remote no longer breaks when a hand-pasted 12-hour token expires. The AWS types are offered only on the edit/settings surface - the backend does not accept them on the repository-create body - and because the stored provider settings are never returned on a read the fields start empty and must be re-entered to change them. setUpstreamAuth is still the hand-written apiFetch wrapper (the generated SDK has no 1.10.0 endpoints); the existing types and the "test upstream" action are unchanged.

  • Admin Audit Log viewer (#568) - new /audit admin page surfacing the backend audit-log query endpoint (GET /api/v1/admin/audit, backend #2366). Table of recorded events (time, actor, action, resource, IP, details) with filters for action, resource type, acting user id (UUID-validated client-side), and an inclusive date range, plus server-side page/per_page pagination (default 50, max 200 per the backend cap). Actor user ids are resolved to usernames client-side via the admin user list since the audit response only carries the id. New "Audit Log" entry in the Administration nav group. The endpoint is not in the generated SDK yet, so src/lib/api/audit.ts uses the shared apiFetch wrapper with zod validation at the trust boundary (same pattern as rate-limits); on a backend without the endpoint the page degrades to an "unavailable" alert.

  • Lifecycle policy exclusions + a real preview size (#855, backend artifact-keeper#2024) - the create-policy form gains an Exclusions editor for every policy type: a list of exact versions and a list of regex patterns that the sweep must never delete (for Docker/OCI repositories the version is the tag, so one editor covers "keep the latest tag" and "keep release 1.4.2"). Patterns are compiled client-side before they can be added, duplicates and blank entries are refused, and an empty list is omitted from the request entirely, so a policy without exclusions posts exactly the config it posted before. The preview panel now reports "would delete N artifacts and reclaim X" from the new bytes_matched field instead of bytes_freed — a dry run frees nothing, so every preview used to read "free 0 B", i.e. "there is nothing to clean up". Backend 1.10.0 also stopped ignoring unrecognised config keys (a misspelt excludes used to validate cleanly and then delete the releases it was written to protect), so a rejected key is now surfaced under the control that produced it rather than only in a toast, and the size_quota_bytes config hint was corrected from max_bytes (never a valid key, now a hard rejection) to quota_bytes. bytes_matched is not in the pinned SDK yet, so src/lib/api/lifecycle.ts reads it through a narrowed cast and degrades to null — the size is omitted, not reported as 0 — against a backend that predates the field.

  • SSO admin UI: surface the SAML use_absolute_acs_url opt-in (#521) - backfills the per-provider toggle the backend added (migration 139) but the admin UI never surfaced (the "lockstep debt"). The SAML provider form gains a Use absolute ACS URL switch in the Sign Requests / Require Signed Assertions group, for stricter SAML 2.0 IdPs that reject relative AssertionConsumerServiceURLs (e.g. Lark AnyCross). Off by default (pre-138 wire format), sourced from the loaded config, and echoed on create + update. src/types/sso.ts and the adaptSamlConfig adapter propagate the field with a defensive false default so the UI is safe to deploy against a backend that predates the column. (The sibling OIDC allow_legacy_rsa_keys toggle, #522, landed later and shipped in 1.9.0.)

  • API token expiry policy: admin panel + minted-expiry on the token reveal (#810, #854, backend artifact-keeper#3460 and artifact-keeper#3625) - backend 1.10.0 adds an instance-wide policy that can require newly minted API tokens to carry an expiration, bound it to a [min_days, max_days] range, apply a default_days when the caller omits one, and optionally extend all of that to service accounts. Admin Settings gains a Security tab with an API token expiry policy card that reads GET /api/v1/admin/settings/token-policy and saves through PUT: a require-expiration switch, min/max/default day inputs (empty default = refuse defaulted mints), and an apply-to-service-accounts switch. When the API_TOKEN_EXPIRATION_* environment variables pin the policy the backend reports editable: false and answers PUT with a 409, so the card renders a Pinned by environment badge with every control disabled instead of offering a save that cannot succeed; a 409 that arrives anyway is shown verbatim and re-reads the policy, and the 400 for an inconsistent policy is attached to the field the backend named (min_days / max_days / default_days) rather than a generic toast. The two never-expiring-token counters the endpoint returns are surfaced as a rotation to-do, because the policy applies at mint time only and never invalidates a credential a pipeline is already running on. Separately, every token-create response now carries expires_at and policy_applied, so the one-time reveal dialogs (profile API keys, personal access tokens, service-account tokens) report the expiry the server actually stamped — “Expires 17/12/2026 · set by instance policy” when the policy set or clamped it, “Never expires” otherwise — and a refused out-of-range expires_in_days shows the backend's message verbatim so the permitted range reaches the user. The endpoints are not in the generated SDK yet, so src/lib/api/token-policy.ts uses the shared apiFetch wrapper with zod validation at the trust boundary (an unmodelled source degrades to database rather than throwing) and a backend that predates 1.10.0 degrades to “never expires, no policy” on the reveal and an “unavailable” alert on the card. Not covered: admin-minted user tokens and repository-scoped tokens have no create UI in the web app today, and #810's optional policy-enforcement audit trail is left to the existing Audit Log page (the backend records ApiTokenPolicyChanged there).

  • release/1.1.x maintenance branch + :1.1-dev Docker tag rule (#331) - mirrors artifact-keeper#890; pushes to release/1.1.x now publish ghcr.io/artifact-keeper/artifact-keeper-web:1.1-dev so the v1.1.x release-gate can test a true v1.1.x web/backend pair.

Changed

  • Type-safe API layer — extend #206 hardening to sso (final batch) (#359 batch 9) - replaced all 30 as never casts in src/lib/api/sso.ts with adapter functions and assertData guards. 7 read adapters (SsoProvider / OidcConfig / LdapConfig / SamlConfig / LdapTestResult / TokenPair) and 6 write adapters covering the OIDC/LDAP/SAML create+update request shapes. Provider type narrowed via narrowEnum to the local oidc | ldap | saml union. The SDK declares attribute_mapping values as unknown while the local types declare them as string; the adapter coerces non-strings defensively. ldapLogin runtime-narrows the SDK's unknown 200 response to extract the access/refresh token pair. Closes #359 in full.
  • Type-safe API layer — extend #206 hardening to security (#359 batch 8) - replaced all 25 as never casts in src/lib/api/security.ts with adapter functions and assertData guards. 9 read adapters (Dashboard / Score / Scan / ScanList / Finding / FindingList / Policy / ScanConfig / RepoSecurity / TriggerScanResponse) and 4 write adapters (TriggerRequest / CreatePolicyRequest / UpdatePolicyRequest / UpsertScanConfigRequest). The Score adapter synthesizes total_findings from severity counts since the SDK ScoreResponse doesn't expose it directly. SDK PolicyResponse has additional fields the local ScanPolicy doesn't model (max_artifact_age_days, min_staging_hours, require_signature) which the adapter intentionally drops — those are consumed by the lifecycle module, not security.
  • Type-safe API layer — extend #206 hardening to promotion (#359 batch 7) - replaced 9 of 10 as never casts in src/lib/api/promotion.ts with adapter functions, assertData guards, and narrowEnum for severity (critical/high/medium/low/info) and PromotionHistoryStatus (promoted/rejected/pending_approval). One as unknown as retained inline for policy_result (the SDK exposes the field as an opaque key/value bag, the local type declares a typed PolicyEvaluationResult that consumers only access lazily — bridge documented). Also exports adaptArtifact / adaptArtifactList from artifacts.ts and adaptRepository / adaptRepositoryList from repositories.ts so promotion can reuse them rather than re-implementing.
  • Type-safe API layer — extend #206 hardening to dependency-track (#359 batch 6) - replaced all 12 as never casts in src/lib/api/dependency-track.ts with adapter functions and assertData guards. The SDK declares every metric counter on DtProjectMetrics / DtPortfolioMetrics as optional; the local types declare them as required : number. Adapters coerce undefined → 0 so an empty backend response renders numeric zeros in the metrics card instead of "undefined". Nested adapters for DtFinding (component / vulnerability / analysis / attribution / cwe / license) preserve existing render behavior.
  • Type-safe API layer — extend #206 hardening to sbom (#359 batch 5) - replaced all 21 as never casts in src/lib/api/sbom.ts with adapter functions, assertData guards, and exported narrowCveStatus / narrowPolicyAction helpers for callers that want a typed status. Multiple SDK shape mismatches are now explicit and documented: LicenseCheckResult is synthesized (SDK returns violations: string[] with no action; adapter coerces to {license, reason} rows and derives action: "block"|"allow" from compliant); getByArtifact no longer accepts a format query param (the SDK has no query and the backend ignored it pre-#359). No app consumer surfaces these endpoints today, so the synthesis is best-effort and documented inline. Other endpoints (generate/list/get/getComponents/convert/getCveHistory/updateCveStatus/getCveTrends/list-get-upsert-deletePolicy) round-trip pages unchanged.
  • Type-safe API layer — extend #206 hardening to replication (#359 batch 4) - replaced all 11 as never casts in src/lib/api/replication.ts with adapter functions, assertData guards, and narrowEnum for the PeerStatus union. Dropped three dead fields from PeerInstance (api_key/sync_filter/updated_at) and one from PeerConnection (source_peer_id) — all four were declared on the local types but never populated by the SDK and never read by any consumer (verified via grep). The peers list and connections table render unchanged.
  • Type-safe API layer — extend #206 hardening to telemetry (#359 batch 3) - replaced all 9 as never casts in src/lib/api/telemetry.ts with adapter functions, assertData guards, and explicit body forwarding. CrashReport's optional+nullable fields (stack_trace, os_info, uptime_seconds, submitted_at, submission_error) now normalize undefined → null. Pages that consume this API are unchanged.
  • Type-safe API layer — extend #206 hardening to webhooks + analytics (#359 batch 2) - replaced all 9 as never casts in src/lib/api/webhooks.ts and all 11 in src/lib/api/analytics.ts with adapter functions, assertData guards, and narrowEnum for the WebhookEvent string-to-union narrowing. Webhook events that the web doesn't model yet now fall back to artifact_uploaded with a console warning instead of crashing render code expecting a known event. Pages that consume these APIs are unchanged.
  • Type-safe API layer — extend #206 hardening to monitoring + lifecycle (#359 batch 1) - replaced all as never casts in src/lib/api/monitoring.ts and src/lib/api/lifecycle.ts with adapter functions and assertData guards. Adapters normalize the SDK's ?: string | null (optional + nullable) shape to the local types' : string | null (required + nullable) shape so callers see a stable contract. Two as unknown as casts remain in lifecycle.ts and are commented inline: the SDK incorrectly types createLifecyclePolicy / updateLifecyclePolicy bodies as the security-policy request shape rather than the lifecycle request shape — to be removed when the generator is rebuilt against the corrected OpenAPI spec. Pages that consume these APIs are unchanged.
  • Admin Settings page now issues one HTTP call instead of three (#349) - the page used to call /api/v1/admin/settings three times via separate useQuery hooks (one each for password-policy, storage-settings, smtp-config). Replaced with a single admin-settings query backed by new settingsApi.getAllSettings() and the useAdminSettings() hook. The SMTP tab consumes the same hook so react-query dedups it. Public per-getter API (getPasswordPolicy / getStorageSettings / getSmtpConfig) preserved for non-page consumers (e.g. the inline PasswordPolicyHint). Cuts settings page network round trips by 67%. Behavioral note: with one shared query, a malformed slice of the response (e.g. bad SMTP fields) now fails the whole bundle — Storage and Password Policy rows show "Unavailable" alongside the SMTP error, even though their fields parsed fine. Pre-PR these would have parsed independently. The trade-off is acceptable because all three slices come from the same endpoint and a malformed bundle is almost always a backend-wide problem; per-slice fault isolation is filed as a follow-up.
  • toUserMessage truncates user-untrusted error text at 240 chars (#356) - prevents a 50KB stack trace or HTML 500 page from rendering as a wall of text in a toast. Truncated output is suffixed with … [truncated, <n> more chars] so it's clear the message was clipped. Author-controlled fallback strings are not truncated.
  • toUserMessage prefixes fallback with HTTP status code (#355) - when an error carries an HTTP status (.status / .statusCode / .body.status) but the body has no useful message, the fallback now reads "(HTTP 409) Failed to create permission" instead of just "Failed to create permission" so a 409 Conflict differentiates from a 500 Internal Server Error in toast text. Backend-provided messages stay unchanged (no double-decoration). Closes the deferred half of #207.
  • Extract mutationErrorToast helper to deduplicate ~125 mutation onError callsites (#354) - the pattern onError: (err) => toast.error(toUserMessage(err, "Failed to <action>")) was repeated across most pages; collapsed to onError: mutationErrorToast("Failed to <action>") in 36 files (-145 LOC). Centralizes future tweaks (HTTP status prefix, truncation, telemetry) to one place. User-visible toast strings are unchanged.
  • Type-safe API layer — replace double-casts with adapters and zod (#206) - removed all as unknown as T and as never casts in 15 of src/lib/api/*.ts files. Each SDK call now goes through an adapter function or a Set-backed narrowing helper that warns on unknown enum values. assertData (new in fetch.ts) rejects empty body responses with a contextual error. settings.ts uses zod .safeParse() at the trust boundary for getPasswordPolicy/getSmtpConfig. Public xxxApi return types unchanged so consumer code is untouched.

Fixed

  • OIDC claim config keys now match the backend's <field>_claim schema (#516) - the OIDC provider form wrote its claim overrides to attribute_mapping under the bare keys username / email / groups (plus display_name), but the backend (sso.rs::resolve_oidc_claim_name) only reads username_claim / email_claim / groups_claim, so every configured OIDC claim override was silently ignored — logins fell back to the built-in defaults regardless of what the operator typed. handleSubmit now writes the _claim-suffixed keys (display_name_claim too, for parity, though the backend does not consume it yet) and drops the legacy bare keys from the JSONB blob on save. The edit dialog reads the new keys with a fallback to the legacy keys so a provider saved by the pre-fix UI still displays its configured claim names. Unrelated attribute_mapping keys still round-trip (regression #406 preserved).

  • Extend SSE EVENT_TYPE_MAP to webhook/artifact/scan/backup/plugin events (#213) - the per-domain map only covered 7 domains (users, groups, repositories, service accounts, permissions, quality gates, dashboard). When backend events fired for the missing domains over SSE, the UI didn't refetch stale data — operators had to hard-refresh. Adds 5 QUERY_KEYS (WEBHOOKS, WEBHOOK_DELIVERIES, BACKUPS, SECURITY, PLUGINS), 4 INVALIDATION_GROUPS (webhooks, backups, security, plugins), and 19 new event-type entries (webhook.{created,updated,deleted,delivery}, artifact.{uploaded,deleted}, scan.{started,completed,failed}, finding.{acknowledged,acknowledgment_revoked}, backup.{created,completed,failed,restored}, plugin.{installed,uninstalled,enabled,disabled}). Map size grew 20 → 39.

  • Setup Guide: sanitize repo keys for Gradle/SBT property names + clearer SSR placeholder (#362, partial) - the Gradle credentials snippet emitted property names like my-jvm-repoUsername for hyphenated repo keys; technically legal in gradle.properties but looks broken to readers expecting identifier rules. Added a repoKeyToGradleId helper that camelCases kebab/dot/underscore separators and strips remaining non-alphanumerics. URLs and <id> slots keep the raw key — only property names sanitize. Also replaced the SSR fallback https://artifacts.example.com with __REPLACE_WITH_REGISTRY_URL__ so prerendered HTML doesn't ship with a real-looking domain a user could accidentally copy. Remaining repo_type (proxy/virtual hides publish steps) and is_public (anonymous mode) fixes deferred to follow-up.

  • Single-request artifact uploads now send the X-Requested-With CSRF header (#821, backend artifact-keeper#3592/#3942) - artifactsApi.upload uses a raw XMLHttpRequest for the progress callback fetch cannot provide, which puts it outside every layer that attaches the header (the SDK request interceptor, apiFetch, and the hand-patched raw fetches in uploads.ts / promotion.ts), and browsers never add it themselves. Files under 100 MiB were therefore refused with 403 "This request requires the X-Requested-With header" while anything at or above that size succeeded on the chunked path, which already set it. The header is now set immediately after xhr.open(...); the remote-instance base URL handling is unchanged. Backend 1.10.0 relaxed the contract to also accept the browser's own Sec-Fetch-Site: same-origin/none, so a same-origin reverse-proxy deployment recovers without this fix, but a split-origin deployment, a same-site sibling origin, and any browser that sends no Fetch Metadata still require the header — and every web build from 1.6.x on hits this against any backend from 1.8.0 on.

  • Per-artifact Security tab now surfaces native scan_findings (#368) - the Security tab on the repository view (security-tab-content.tsx) used to show only SBOM CVE history and Dependency-Track findings, never the native scan_findings table. A user who triggered a scan via POST /api/v1/security/scan for a specific artifact had no way to see the resulting findings on the artifact's own page — they had to navigate to /security/scans and find the right scan ID by name+timestamp. New ArtifactScansSection component lists recent scan_results rows for the artifact (status / type / counts / completed_at) with a "View findings" link to the per-scan page. Sourced from securityApi.listArtifactScans(artifact.id) which already existed but had no consumer.

  • getInstallCommand returns Gradle/SBT-native snippets instead of Maven XML (#361) - the JVM case in package-utils.ts returned the same <dependency> XML for all three of maven / gradle / sbt. Users browsing a Gradle-named repo saw Maven XML in the package detail / copy-snippet UI — same bug class #333 fixed on the Setup Guide page. Now gradle returns implementation 'GROUP:name:version' and sbt returns libraryDependencies += "GROUP" % "name" % "version". Maven output is unchanged.

  • Surface load failures in getPasswordPolicy and getSmtpConfig instead of silently falling back to defaults (#347) - both getters previously caught any SDK error or schema mismatch and returned baked-in defaults, so a backend outage rendered as plausible-looking placeholder values on the admin Settings page (same failure mode as #334). Now the getters throw on SDK error / unparseable response, and the page renders explicit "Unavailable" states (Password Policy row + SMTP tab error alert) so an operator can tell something's actually wrong.

  • 1.10.0 follow-ups: friendly copy for the new status codes, provisional migration progress (#861, backend artifact-keeper#3489/#3590/#3824) - toUserMessage gains author-written copy for 423 (a VSIX withheld while its scan is inconclusive on a fail-closed instance) and 451 (a Cargo publish-age hold), whose bodies are structured JSON that ApiError renders as API error <status>: {…}, plus the three new 409s: republishing a resolved unique Maven snapshot (the backend says only "Artifact already exists"), a duplicate SAML name or slug, and a token policy pinned by environment variables. Every other status and conflict keeps the backend's own message, so an unrelated "already exists" is not mislabelled - including the 403s the SBOM generate/convert/delete routes now return for a viewer without repository write / delete, which already reach the toast with the backend's reason. Migration progress marks the percentage provisional (~, with an explanatory tooltip) while a job is still enumerating, because total_items is "found so far" and the figure can otherwise walk backwards; terminal jobs read as before. No "API docs" link to the now opt-in /swagger-ui/ exists in the chrome, so nothing needed removing there.

  • formatBytes returns "--" for NaN/Infinity/negative input (#348) - previously these inputs produced nonsense strings like "NaN undefined" or "Infinity undefined" visible on the admin Settings → Storage tab. Now returns the same -- sentinel already used elsewhere in the package/search rendering paths. Also clamps the unit index for >TB values so multi-PB byte counts render as " TB" rather than indexing past the units table.

  • Package detail keeps the virtual repository's key when reached from one (#860, backend artifact-keeper#3532) - virtual repositories now aggregate their members in GET /packages?repository_key=<virtual>, so the catalog and the repository Packages tab list member packages under the virtual key — but the detail lookup still reported the owning member's key, so the same package changed repository mid-navigation (and the Files tree switched to the member repo). packagesApi.get takes an optional repository_key, the catalog's links and master/detail pane carry the listed row's key through ?repository_key=, and the repository Packages tab passes the repository being browsed. The param is not in the pinned SDK (GetPackageData.query is never), so that one branch goes through apiFetch; without a key the generated operation is still used, and the backend ignores a key that is not a virtual repo containing the package.

  • SSO login button reads "Sign in with SSO" instead of generic provider names like "default" (#351) - when an admin's SSO provider is named default / primary / main / sso (or empty/whitespace), the button now falls back to a protocol-aware label (Sign in with SSO (OIDC) / (SAML)) so users see what they're actually clicking. Real provider names like "Corp SSO" are preserved unchanged.

  • Login page hides username/password fields when only redirect SSO is configured (#350) - previously the form rendered even when the only available auth method was OIDC/SAML, leaving the fields with no consumer. The form now hides when SSO providers exist and no LDAP provider is configured. Setup mode and the ?fallback=local query param keep the form available for first-time setup and operator recovery. A loading skeleton during the SSO providers fetch prevents the form from briefly flashing visible. Heuristic stopgap until the backend exposes a public local_auth_enabled flag.

  • Migration Add Connection now lets users pick the source repository manager type (#319) - the form previously had no Source Type field, so the backend silently defaulted every connection to Artifactory. Adds a Source Type Select with Artifactory + Nexus options (the two values the SDK currently accepts), threaded through types, the API adapter, the form state, and the create-connection mutation body. Default remains Artifactory to preserve prior behavior.

  • Every assigned repository on the Replication Subscriptions tab read "Pull", whatever mode was stored (#817, backend artifact-keeper#3575) - the per-repository Replication Mode dropdown had nothing to render from: GET /api/v1/peers/{id}/repositories returned a bare array of repository ids, so the page fell back to assignedSet.has(r.id) ? "pull" : "none" and displayed pull for push and mirror subscriptions alike. Since that dropdown is also the editing control, a confident wrong reading was one save away from becoming the stored value. The tab now reads replication_mode off the subscription list the endpoint returns after backend artifact-keeper#3575, and the column header gains a tooltip stating that direction is relative to this instance (push = this instance sends to the peer), which the UI stated nowhere before. Requires a backend carrying artifact-keeper#3575: against an older backend the endpoint still returns bare ids, and the tab reads every repository as unassigned with mode None rather than showing a wrong direction. The generated SDK still types the response as string[], so peersApi.getRepositories reads the subscription shape through a passthrough cast until the SDK is regenerated, matching how admin.ts handles the health image_tag. DataTableColumn.header widens from string to ReactNode to carry the tooltip.

  • Setup Guide now shows correct client snippets for Gradle and SBT repos (#333) - JVM-format repos (maven / gradle / sbt) previously rendered only Maven pom.xml / settings.xml. The dialog now offers Maven, Gradle (Groovy), Gradle (Kotlin), and SBT tabs with the correct credential and dependency snippets per client. Default tab tracks the repo's declared format so a Gradle repo opens on Gradle (Groovy).

  • Mutation errors now surface backend details instead of generic placeholders (#207) - audited every TanStack Query useMutation and replaced opaque onError: () => toast.error("Failed to ...") callbacks with toUserMessage(err, fallback)-driven toasts. 91 callsites across 27 files. Also adds onError to 8 previously-silent mutations (security/policies/scans + repo-selector preview), and disambiguates the SSO toggle toasts per provider (OIDC/LDAP/SAML). toUserMessage now also reads FastAPI-style .detail fields so plugin-install errors (and any other FastAPI-shaped backend error) surface their server-side message.

  • Package Age Policy is no longer offered on remote and virtual repositories (#853, backend artifact-keeper#3647) - backend 1.10.0 rejects quarantine_enabled: true on the proxying types with a 400: proxied content is recorded in proxy_cache_artifacts, which carries no quarantine identity, so the hold has no release path and degrades into a total block on everything not already cached. The Repository → Settings panel now renders read-only for remote / virtual, explaining the restriction and pointing at the local or staging repository the content lands in; a repository left enabled by an earlier release shows its stored cooldown plus a working Disable Age Policy action, since disabling is still accepted on every type. local / staging keep the editable form unchanged, and a rejected request still surfaces the backend's own message.

Accessibility

  • Aria attribute coverage on admin pages (#208) - replaced title with aria-label on icon-only buttons (lifecycle, monitoring, quality-gates, sso, telemetry, groups, security/scans, file-viewer); paired form inputs with labels via htmlFor/id; added accessible names to Switch components. Per-row table action buttons (SSO providers, quality gates, lifecycle policies, telemetry crash reports, users, monitoring suppress) now interpolate the row's identifying name into the aria-label so screen readers can disambiguate. Newly accessible-named Refresh buttons on approvals, security, and migration pages.

Security

  • Pin third-party GitHub Actions to commit SHAs (#205) - every third-party uses: line in codeql.yml, dependency-review.yml, docker-publish.yml, and stale.yml is now pinned to a specific commit SHA (with a version comment) so an upstream tag rewrite cannot silently swap action code. ci.yml was already pinned and is the model. The same-org reusable workflow artifact-keeper/artifact-keeper-test/.github/workflows/release-gate.yml@main (docker-publish.yml line 191) is intentionally tracked on main — same-org workflows inherit the org's branch-protection trust boundary, and pinning a reusable workflow to a SHA is operationally heavier. Dependabot is configured for github-actions, so bumps continue to flow through review.

Notes

  • v1.1.8 web image is permanently unavailable (#320) - the web release process stopped at v1.1.3 while the backend continued through v1.1.8. There is no v1.1.8 source ref to rebuild from; backfilling would falsify provenance. See docs/release-history/v1.1.8-web-postmortem.md. Recurrence is prevented by artifact-keeper#882 (image-publish gate).

What's Changed

  • fix(repositories): stop offering the age policy on remote/virtual repositories (backend 1.10.0) by @brandonrc in #863
  • fix(ui): friendly copy for the 1.10.0 status codes and provisional migration progress by @brandonrc in #871
  • fix(artifacts): send the CSRF header on the single-request upload XHR by @brandonrc in #864
  • feat(security): external scan type, scan-type filter and findings filters (backend 1.10.0) by @brandonrc in #870
  • feat(analytics): local+remote totals with a split toggle by @nicola-preda in #820
  • feat(repositories): AWS ECR and CodeArtifact upstream auth types (backend 1.10.0) by @brandonrc in #865
  • fix(packages): carry the virtual repository key into the package detail view (backend 1.10.0) by @brandonrc in #868
  • feat(settings): admin Maintenance card for the 1.10.0 backfills by @brandonrc in #866
  • feat(sso): SAML provider slug and slug-addressed login/ACS URLs (backend 1.10.0) by @brandonrc in #867
  • feat(lifecycle): exclusions editor and a real preview size (backend 1.10.0) by @brandonrc in #869
  • feat(settings): admin API token expiry policy and minted expiry on token reveal (backend 1.10.0) by @brandonrc in #872
  • fix(replication): show the real replication mode instead of defaulting to pull by @nicola-preda in #818
  • docs(changelog): drop the 1.9.0 bullets that #818's rebase re-added under Unreleased by @brandonrc in #876
  • feat(repositories): Build tab and Image inspector for container repositories by @brandonrc in #851
  • sidebar: show the commit hash for builds that are not a release tag by @brandonrc in #878
  • chore(release): web 1.10.0 prep — CHANGELOG + version bump by @brandonrc in #879

Full Changelog: v1.9.0...v1.10.0

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

NewReleases is sending notifications on new releases.