diff --git a/docs/deployment.md b/docs/deployment.md index 4fdb4a0..c866acf 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -127,11 +127,17 @@ package uploads and downloads stay open longer than portal requests. ## 8. Publish a protected package -The normal workflow is **Protected packages → Upload a new version** in the -portal. The broker streams the file directly to Gitea, calculates SHA-256, -updates the catalog, and writes an audit event without buffering the installer +The normal workflow is **Protected packages → Update a package** in the portal. +The package ID is the stable name used by the broker, ProxMenu, and Gitea; a +separate registry package name is not required. The broker streams the +replacement directly to Gitea, calculates SHA-256, updates the catalog, removes +the prior Gitea version, and writes audit events without buffering the installer on local disk. +An authorization records the exact version and checksum selected when its code +is created. Revoke and reissue any active code that included a package after +replacing that package, because the superseded registry version is removed. + The load balancer or Nginx must allow request bodies up to `TAPM_MAX_UPLOAD_BYTES` and use a sufficiently long request timeout. The default is 1 GiB and 30 minutes. diff --git a/internal/app/authorization.go b/internal/app/authorization.go index ca91a43..fc94917 100644 --- a/internal/app/authorization.go +++ b/internal/app/authorization.go @@ -336,7 +336,7 @@ func (s *Server) handleRevokeAuthorization(w http.ResponseWriter, r *http.Reques func (s *Server) handleUpsertPackage(w http.ResponseWriter, r *http.Request) { tech, _ := s.currentTechnician(r) fields := []string{ - "slug", "display_name", "package_name", "package_version", "file_name", "sha256", + "slug", "display_name", "package_version", "file_name", "sha256", } values := make(map[string]string) for _, field := range fields { @@ -348,7 +348,6 @@ func (s *Server) handleUpsertPackage(w http.ResponseWriter, r *http.Request) { } if !validSlug(values["slug"]) || len(values["display_name"]) > 255 || - !validRegistrySegment(values["package_name"], 255) || !validRegistrySegment(values["package_version"], 100) || !validRegistrySegment(values["file_name"], 255) || !validSHA256(values["sha256"]) { @@ -360,7 +359,7 @@ func (s *Server) handleUpsertPackage(w http.ResponseWriter, r *http.Request) { r, values["slug"], values["display_name"], - values["package_name"], + values["slug"], values["package_version"], values["file_name"], strings.ToLower(values["sha256"]), diff --git a/internal/app/static/app.css b/internal/app/static/app.css index 293b2c4..4e54d84 100644 --- a/internal/app/static/app.css +++ b/internal/app/static/app.css @@ -24,7 +24,7 @@ body { #091211; } -button, input { font: inherit; } +button, input, select { font: inherit; } button:disabled { cursor: wait; opacity: .58; } .topbar { @@ -110,7 +110,7 @@ h2 { margin-bottom: 0; font-size: 1.4rem; letter-spacing: -.02em; } .stack.compact { padding: 0; } label, legend { color: var(--muted); font-size: .78rem; font-weight: 750; letter-spacing: .045em; text-transform: uppercase; } -input { +input, select { width: 100%; margin-top: 8px; padding: 12px 13px; @@ -120,7 +120,7 @@ input { outline: none; background: #0a1514; } -input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(88, 224, 173, .12); } +input:focus, select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(88, 224, 173, .12); } .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } fieldset { display: grid; gap: 10px; padding: 0; border: 0; } @@ -211,6 +211,7 @@ dd { margin: 4px 0 0; overflow-wrap: anywhere; } .empty, .fine-print { color: var(--muted); } .package-forms { display: grid; gap: 26px; } .form-title { margin: 0; color: var(--ink); font-weight: 850; } +.form-help { margin: 0; color: var(--muted); font-size: .82rem; line-height: 1.55; } .upload-status { min-height: 1.2em; margin: 0; color: var(--amber); overflow-wrap: anywhere; } .manual-metadata { padding-top: 22px; border-top: 1px solid var(--line); } .manual-metadata summary { color: var(--accent); cursor: pointer; font-weight: 750; } diff --git a/internal/app/static/app.js b/internal/app/static/app.js index ed2c7ae..b149e36 100644 --- a/internal/app/static/app.js +++ b/internal/app/static/app.js @@ -37,10 +37,15 @@ document.addEventListener("submit", async (event) => { if (!response.ok) { throw new Error(result.error || `Upload failed with HTTP ${response.status}`); } - status.textContent = `Uploaded ${result.filename}; SHA-256 ${result.sha256}`; - window.setTimeout(() => { - window.location.assign("/portal?notice=Package+uploaded+and+registered"); - }, 900); + if (result.warning) { + status.textContent = `${result.warning} The replacement is active; see the audit trail for details.`; + button.disabled = false; + } else { + status.textContent = `Uploaded ${result.filename}; SHA-256 ${result.sha256}`; + window.setTimeout(() => { + window.location.assign("/portal?notice=Package+uploaded+and+registered"); + }, 900); + } } catch (error) { status.textContent = error.message; button.disabled = false; diff --git a/internal/app/templates/portal.html b/internal/app/templates/portal.html index 05ba258..4c04572 100644 --- a/internal/app/templates/portal.html +++ b/internal/app/templates/portal.html @@ -181,18 +181,37 @@