feat(maven): metadata proxy miss and stale revalidation

Summary

A cache miss or stale maven-metadata.xml on a remote Maven repository used to return 501. It now fetches the upstream document, streams it unchanged, and commits the blob plus all four digest columns — the same miss/stale sequence Step 14 already shipped for artifacts.

Fresh hits stay on Step 11. This MR only owns the fill arms.

Why this shape

The metadata fill is a deliberate twin of fillRemoteArtifact. Freshness stays in Step 8's verdict; this handler does not do window math. Package-level rows keep a NULL version id. Version-level snapshot documents sit under their version row. snapshot_metadata_always_revalidate is consumed as that verdict, not as a branch in the handler.

Criterion 3's second half — a client that reads the new snapshot timestamp and then pulls the dated artifact — is a two-path sequence. It stays on Step 18's harness.

Shared-file touches on Step 14's remote_artifact.go are logging parity and //nolint:dupl for the twin, plus a redundant CodeInternalServerError case drop. remote_serve.go only updates comments that still pointed at the stub.

Size

Past the 500-LOC reviewable ceiling (development-model.md). Split would not help: one fill, one suite, one stub deletion. A tests-first stack does not compile until fillRemoteMetadata exists.

Group +
Production Go (remote_metadata.go, stub delete, small artifact/serve twins) ~629 ~62
Tests (integration + dispatch + serve-handoff) ~1,771 ~12
Plan record ~50 ~6
Total ~2,450 ~80

Most of the overage is remote_metadata_integration_test.go (~1,617). Same pattern as Step 14's artifact suite.

Spec coverage

Spec: docs/specs/S14-maven-remote.md

Acceptance criteria

# Criterion Tests
MP-1 Verbatim relay TestRemoteMetadata_Miss200_FillsStreamsAndCommits
MP-2 post-window After the window: 304 bumps, 200 replaces TestRemoteMetadata_Stale304_BumpsAndServes, TestRemoteMetadata_Stale200_ReplacesRow
MP-2 fresh half Second GET inside the window serves from cache Step 11
MP-3 metadata-fill half Snapshot document anchors under its version row and refreshes under the metadata window TestRemoteMetadata_SnapshotDefaultWindow_AnchorsUnderVersionAndRevalidates
MP-3 artifact follow-through Client resolves the new timestamp/build and pulls the dated artifact Step 18 harness
MP-4 Always-revalidate opt-in; package-level still windowed TestRemoteMetadata_SnapshotAlwaysRevalidate_RevalidatesEveryGet_304Bumps, _200Replaces, _PackageLevelUnaffected
MP-5 Metadata sidecar cache-generation consistency Step 12 / 16
MP-6 Group-level: too-short 400; two+ segments proxied as package-level TestRemoteMetadata_GroupLevelTooShort_400BeforeLookup, TestRemoteMetadata_GroupLevelTwoPlusSegments_ProxiedAsPackageLevel
MP-7 Single-flight; leader failure reaches followers TestRemoteMetadata_ConcurrentMiss_SingleUpstreamGet, TestRemoteMetadata_ConcurrentTruncation_LeaderInterruptedFollowers503
MP-8 HEAD on a hit Step 11
MP-9 HEAD on a miss fills TestRemoteMetadata_HeadMiss_FillsAndReturnsHeaders
MP-10 Stale hit, no stored ETag TestRemoteMetadata_StaleNoEtag_Refetches
MP-11 stale half Client conditional: upstream validator first TestRemoteMetadata_StaleClientConditional_UpstreamValidatorFirst
MP-11 fresh half If-None-Match on a fresh row Step 11
Fill-commit Blob + four digest columns; FIPS leaves md5 NULL TestRemoteMetadata_Miss200_FillsStreamsAndCommits, TestRemoteMetadata_Fill_MD5ColumnMatchesFIPSPosture

Error cases

# Condition Tests
E-404 Upstream 404 on a miss TestRemoteMetadata_Miss_Upstream404
E-4xx Upstream non-404 4xx → upstream_rejected TestRemoteMetadata_Miss_Upstream4xx_UpstreamRejected
E-500 Upstream 500 → internal_server_error TestRemoteMetadata_Miss_Upstream5xx_Mapped
E-5xx Other 5xx → upstream_unavailable; cache not served TestRemoteMetadata_Miss_Upstream5xx_Mapped, TestRemoteMetadata_Miss_Upstream503_RelaysUpstreamRetryAfter
E-transport + cache Serve cached copy TestRemoteMetadata_Stale_TransportFailure_ServesCached
E-transport, no cache 503 + Retry-After TestRemoteMetadata_Miss_TransportFailure_503RetryAfter
E-cap Body over metadata cap treated as transport failure TestRemoteMetadata_Miss_BodyCapBreach_Interrupted200
E-group-short Fewer than two directory segments → 400 TestRemoteMetadata_GroupLevelTooShort_400BeforeLookup
Path traversal / outbound segment safety S10 parse + Step 5 URL builder Earlier steps
PUT 405 Step 10 kind gate Earlier steps

Security considerations

# Concern Tests
S-credential hygiene Username/password never in logs or responses S13 client + Step 7; this fill does not log credentials
S-SSRF / redirect strip S13 upstream client S13-owned. Not tested in this MR
S-outbound segments S10 grammar before URL build Step 5
S-error-payload hygiene No upstream host/URL/headers on the client envelope TestRemoteMetadata_Miss_Upstream4xx_UpstreamRejected (no upstream headers)
S-FIPS Skip MD5 hash; leave column NULL TestRemoteMetadata_Fill_MD5ColumnMatchesFIPSPosture
S-checksum trust Sidecar attests cached bytes Sidecar steps 12 / 16

E2E catalog

No Maven catalog exists under docs/testing/ yet (only README.md, docker.md, oci.md). This MR does not add one. Step 18 is the plan's hermetic Resolver sequence for this path.

Related to #286 (closed)

Edited by Moaz Khalifa

Merge request reports

Loading
Loading