Added
-
Build tab and Image inspector for container repositories (#875, backend artifact-keeper#4034) -
docker,podman,buildxandorasrepositories 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'sbase-infoendpoint 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 legacyapt/conda/pipshorthand 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 insrc/lib/api/image-builds.tsuntil 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, thededup_scopelabel (per_repo/instance) with an inline caveat that instance-scope figures reflect blobs pooled across the whole instance, andcomputed_atfreshness (hover for the exact timestamp). Admins additionally see the instance-wideinstance_unique_bytestotal and an on-demand "reclaimable now" estimate (astorage-gcdry-run — nothing is deleted). Security (backend artifact-keeper#2560): oninstance-scope backends the backend omitsphysical_bytes/unique_bytes/shared_bytes/dedup_ratiofor 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), sosrc/lib/api/storage.tsuses the sharedapiFetchwrapper (same pattern as routing-rules / audit / downloads); if the endpoint is unavailable the panel falls back to the repository's logicalstorage_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 newscan_typevalueexternal. Every scan table printedscan_typeraw, so those rows showed up as a bare "external" with no indication of what they were. New sharedScanTypeBadge/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'sscanner_versionin 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 madetotallie; filtering now happens in the backend and the responsetotaldrives pagination. A new Source column surfaces each finding's reporting scanner, which is where an external submission's vendor identity lives (scan_typeis 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), sosrc/lib/api/security.tsdeclares 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-radiusadmin 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 flagsaccess_scope=publicas "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, sosrc/lib/api/blast-radius.tsuses the sharedapiFetchwrapper 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) rebuildsrepository_usage_ledgerfrom 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 (formatBytesreturns 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), sosrc/lib/api/maintenance.tsuses the sharedapiFetchwrapper 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-repoversioning_enabledflag). 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 writesversioning_enabledthrough the existing update-repository endpoint. The endpoints are not in the generated SDK yet, sosrc/lib/api/versions.tsuses the sharedapiFetchwrapper with zod validation at the trust boundary (same pattern as audit/downloads), normalizes the backend's 404-on-no-history to an empty list, andrepositoriesApireads/writesversioning_enabledthrough a narrowed cast until the SDK regenerates. NewArtifactVersionsPagee2e page object +artifact-version-history.spec.ts(API contract: newest-first list,?version=pins old bytes,versioning_enabledround-trip; best-effort admin UI checks), and aversioning_enabled: truegeneric repo seeded for the e2e stack. Backend gap: the/versionsresponse does not serializeuploaded_byyet (the column exists onartifact_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
slugthat 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.slugis not in the generated SDK yet, soadaptSamlConfigreads it through a cast defaulting tonull(the same pattern asallow_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
/downloadsadmin 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, sosrc/lib/api/downloads.tsuses the sharedapiFetchwrapper 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_ecrandaws_codeartifactbeside 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 orAWS_*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.setUpstreamAuthis still the hand-writtenapiFetchwrapper (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
/auditadmin 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, sosrc/lib/api/audit.tsuses the sharedapiFetchwrapper 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
latesttag" 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 theconfigit posted before. The preview panel now reports "would delete N artifacts and reclaim X" from the newbytes_matchedfield instead ofbytes_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 unrecognisedconfigkeys (a misspeltexcludesused 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 thesize_quota_bytesconfig hint was corrected frommax_bytes(never a valid key, now a hard rejection) toquota_bytes.bytes_matchedis not in the pinned SDK yet, sosrc/lib/api/lifecycle.tsreads it through a narrowed cast and degrades tonull— the size is omitted, not reported as 0 — against a backend that predates the field. -
SSO admin UI: surface the SAML
use_absolute_acs_urlopt-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.tsand theadaptSamlConfigadapter propagate the field with a defensivefalsedefault so the UI is safe to deploy against a backend that predates the column. (The sibling OIDCallow_legacy_rsa_keystoggle, #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 adefault_dayswhen 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 readsGET /api/v1/admin/settings/token-policyand saves throughPUT: a require-expiration switch, min/max/default day inputs (empty default = refuse defaulted mints), and an apply-to-service-accounts switch. When theAPI_TOKEN_EXPIRATION_*environment variables pin the policy the backend reportseditable: falseand answersPUTwith 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 carriesexpires_atandpolicy_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-rangeexpires_in_daysshows the backend's message verbatim so the permitted range reaches the user. The endpoints are not in the generated SDK yet, sosrc/lib/api/token-policy.tsuses the sharedapiFetchwrapper with zod validation at the trust boundary (an unmodelledsourcedegrades todatabaserather 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 recordsApiTokenPolicyChangedthere). -
release/1.1.xmaintenance branch +:1.1-devDocker tag rule (#331) - mirrorsartifact-keeper#890; pushes torelease/1.1.xnow publishghcr.io/artifact-keeper/artifact-keeper-web:1.1-devso 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 nevercasts insrc/lib/api/sso.tswith adapter functions andassertDataguards. 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 vianarrowEnumto the localoidc | ldap | samlunion. The SDK declares attribute_mapping values asunknownwhile the local types declare them as string; the adapter coerces non-strings defensively.ldapLoginruntime-narrows the SDK'sunknown200 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 nevercasts insrc/lib/api/security.tswith adapter functions andassertDataguards. 9 read adapters (Dashboard / Score / Scan / ScanList / Finding / FindingList / Policy / ScanConfig / RepoSecurity / TriggerScanResponse) and 4 write adapters (TriggerRequest / CreatePolicyRequest / UpdatePolicyRequest / UpsertScanConfigRequest). The Score adapter synthesizestotal_findingsfrom 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 nevercasts insrc/lib/api/promotion.tswith adapter functions,assertDataguards, andnarrowEnumforseverity(critical/high/medium/low/info) andPromotionHistoryStatus(promoted/rejected/pending_approval). Oneas unknown asretained inline forpolicy_result(the SDK exposes the field as an opaque key/value bag, the local type declares a typedPolicyEvaluationResultthat consumers only access lazily — bridge documented). Also exportsadaptArtifact/adaptArtifactListfromartifacts.tsandadaptRepository/adaptRepositoryListfromrepositories.tsso 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 nevercasts insrc/lib/api/dependency-track.tswith adapter functions andassertDataguards. The SDK declares every metric counter onDtProjectMetrics/DtPortfolioMetricsas 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 forDtFinding(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 nevercasts insrc/lib/api/sbom.tswith adapter functions,assertDataguards, and exportednarrowCveStatus/narrowPolicyActionhelpers for callers that want a typed status. Multiple SDK shape mismatches are now explicit and documented:LicenseCheckResultis synthesized (SDK returnsviolations: string[]with noaction; adapter coerces to{license, reason}rows and derivesaction: "block"|"allow"fromcompliant);getByArtifactno longer accepts aformatquery 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 nevercasts insrc/lib/api/replication.tswith adapter functions,assertDataguards, andnarrowEnumfor thePeerStatusunion. Dropped three dead fields fromPeerInstance(api_key/sync_filter/updated_at) and one fromPeerConnection(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 nevercasts insrc/lib/api/telemetry.tswith adapter functions,assertDataguards, 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 nevercasts insrc/lib/api/webhooks.tsand all 11 insrc/lib/api/analytics.tswith adapter functions,assertDataguards, andnarrowEnumfor theWebhookEventstring-to-union narrowing. Webhook events that the web doesn't model yet now fall back toartifact_uploadedwith 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 nevercasts insrc/lib/api/monitoring.tsandsrc/lib/api/lifecycle.tswith adapter functions andassertDataguards. 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. Twoas unknown ascasts remain inlifecycle.tsand are commented inline: the SDK incorrectly typescreateLifecyclePolicy/updateLifecyclePolicybodies 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/settingsthree times via separateuseQueryhooks (one each forpassword-policy,storage-settings,smtp-config). Replaced with a singleadmin-settingsquery backed by newsettingsApi.getAllSettings()and theuseAdminSettings()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 inlinePasswordPolicyHint). 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. toUserMessagetruncates 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.toUserMessageprefixes 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
mutationErrorToasthelper to deduplicate ~125 mutationonErrorcallsites (#354) - the patternonError: (err) => toast.error(toUserMessage(err, "Failed to <action>"))was repeated across most pages; collapsed toonError: 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 Tandas nevercasts in 15 ofsrc/lib/api/*.tsfiles. Each SDK call now goes through an adapter function or a Set-backed narrowing helper that warns on unknown enum values.assertData(new infetch.ts) rejects empty body responses with a contextual error.settings.tsuses zod.safeParse()at the trust boundary forgetPasswordPolicy/getSmtpConfig. PublicxxxApireturn types unchanged so consumer code is untouched.
Fixed
-
OIDC claim config keys now match the backend's
<field>_claimschema (#516) - the OIDC provider form wrote its claim overrides toattribute_mappingunder the bare keysusername/email/groups(plusdisplay_name), but the backend (sso.rs::resolve_oidc_claim_name) only readsusername_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.handleSubmitnow writes the_claim-suffixed keys (display_name_claimtoo, 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. Unrelatedattribute_mappingkeys 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-repoUsernamefor hyphenated repo keys; technically legal ingradle.propertiesbut looks broken to readers expecting identifier rules. Added arepoKeyToGradleIdhelper 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 fallbackhttps://artifacts.example.comwith__REPLACE_WITH_REGISTRY_URL__so prerendered HTML doesn't ship with a real-looking domain a user could accidentally copy. Remainingrepo_type(proxy/virtual hides publish steps) andis_public(anonymous mode) fixes deferred to follow-up. -
Single-request artifact uploads now send the
X-Requested-WithCSRF header (#821, backend artifact-keeper#3592/#3942) -artifactsApi.uploaduses a rawXMLHttpRequestfor the progress callbackfetchcannot provide, which puts it outside every layer that attaches the header (the SDK request interceptor,apiFetch, and the hand-patched raw fetches inuploads.ts/promotion.ts), and browsers never add it themselves. Files under 100 MiB were therefore refused with403 "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 afterxhr.open(...); the remote-instance base URL handling is unchanged. Backend 1.10.0 relaxed the contract to also accept the browser's ownSec-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 nativescan_findingstable. A user who triggered a scan viaPOST /api/v1/security/scanfor a specific artifact had no way to see the resulting findings on the artifact's own page — they had to navigate to/security/scansand find the right scan ID by name+timestamp. NewArtifactScansSectioncomponent lists recent scan_results rows for the artifact (status / type / counts / completed_at) with a "View findings" link to the per-scan page. Sourced fromsecurityApi.listArtifactScans(artifact.id)which already existed but had no consumer. -
getInstallCommandreturns Gradle/SBT-native snippets instead of Maven XML (#361) - the JVM case inpackage-utils.tsreturned the same<dependency>XML for all three ofmaven/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. Nowgradlereturnsimplementation 'GROUP:name:version'andsbtreturnslibraryDependencies += "GROUP" % "name" % "version". Maven output is unchanged. -
Surface load failures in
getPasswordPolicyandgetSmtpConfiginstead 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) -
toUserMessagegains 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 thatApiErrorrenders asAPI 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 repositorywrite/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, becausetotal_itemsis "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. -
formatBytesreturns "--" 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.gettakes an optionalrepository_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.queryisnever), so that one branch goes throughapiFetch; 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=localquery 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 publiclocal_auth_enabledflag. -
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}/repositoriesreturned a bare array of repository ids, so the page fell back toassignedSet.has(r.id) ? "pull" : "none"and displayedpullforpushandmirrorsubscriptions 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 readsreplication_modeoff 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 modeNonerather than showing a wrong direction. The generated SDK still types the response asstring[], sopeersApi.getRepositoriesreads the subscription shape through a passthrough cast until the SDK is regenerated, matching howadmin.tshandles the healthimage_tag.DataTableColumn.headerwidens fromstringtoReactNodeto 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
useMutationand replaced opaqueonError: () => toast.error("Failed to ...")callbacks withtoUserMessage(err, fallback)-driven toasts. 91 callsites across 27 files. Also addsonErrorto 8 previously-silent mutations (security/policies/scans + repo-selector preview), and disambiguates the SSO toggle toasts per provider (OIDC/LDAP/SAML).toUserMessagenow also reads FastAPI-style.detailfields 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: trueon the proxying types with a 400: proxied content is recorded inproxy_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 forremote/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/stagingkeep the editable form unchanged, and a rejected request still surfaces the backend's own message.
Accessibility
- Aria attribute coverage on admin pages (#208) - replaced
titlewitharia-labelon icon-only buttons (lifecycle, monitoring, quality-gates, sso, telemetry, groups, security/scans, file-viewer); paired form inputs with labels viahtmlFor/id; added accessible names toSwitchcomponents. 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-namedRefreshbuttons on approvals, security, and migration pages.
Security
- Pin third-party GitHub Actions to commit SHAs (#205) - every third-party
uses:line incodeql.yml,dependency-review.yml,docker-publish.yml, andstale.ymlis now pinned to a specific commit SHA (with a version comment) so an upstream tag rewrite cannot silently swap action code.ci.ymlwas already pinned and is the model. The same-org reusable workflowartifact-keeper/artifact-keeper-test/.github/workflows/release-gate.yml@main(docker-publish.yml line 191) is intentionally tracked onmain— 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 forgithub-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