v0.280.0: the setup gate (decision 46); R-710 'I changed it' + absent-record window; R-709 password fields off the page; password:N:special generator
gates / gates (push) Successful in 25s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-29 08:51:46 +02:00
parent 2548b4c924
commit 7fa8768cfd
37 changed files with 4015 additions and 107 deletions
+30 -5
View File
@@ -11,6 +11,7 @@ import (
"net/url"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"time"
@@ -463,7 +464,7 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
// v0.279.0 (decision 45): the default login, before the install when nothing will replace it, after it
// while it is still in effect.
data["KnownLoginLine"] = s.knownLoginLine(lang, meta, appCfg, alreadyDeployed)
data["DefaultLoginReplaced"] = meta.AfterInstall != nil && !defaultLoginInEffect(meta, appCfg, alreadyDeployed)
data["DefaultLoginReplaced"] = defaultLoginReplaced(meta, appCfg, alreadyDeployed)
data["LogoURL"] = s.cfg.AppLogoURL(meta.Slug)
data["LogoPNGURL"] = s.cfg.AppLogoPNGURL(meta.Slug)
data["AppPageURL"] = s.cfg.AppPageURL(meta.Slug)
@@ -500,6 +501,13 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
delete(decryptedEnv, n)
}
}
// R-709 (v0.280.0): a `type: password` value is never written into this page's HTML either — like a
// `type: secret` (R-254) it is fetched on demand (/stacks/<n>/auto-field/reveal).
for _, f := range meta.DeployFields {
if f.Type == "password" {
delete(decryptedEnv, f.EnvVar)
}
}
data["DeployedFieldValues"] = decryptedEnv
data["RestoredLogins"] = restored
}
@@ -762,7 +770,10 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
data["AppInfo"] = found.Meta.AppInfo
// v0.279.0 (decision 45): the default-login card only while that login is in effect.
data["KnownLoginLine"] = s.knownLoginLine(s.langFor(r), &found.Meta, found.AppConfig, found.Deployed)
data["DefaultLoginReplaced"] = found.Meta.AfterInstall != nil && !defaultLoginInEffect(&found.Meta, found.AppConfig, found.Deployed)
data["DefaultLoginReplaced"] = defaultLoginReplaced(&found.Meta, found.AppConfig, found.Deployed)
// v0.280.0 (decision 46): the setup gate's card, while the gate stands.
data["SetupGateClosed"] = found.Deployed && found.AppConfig != nil && found.AppConfig.SetupGate.Closed()
data["SetupGateHasProbe"] = found.Meta.SetupDoneProbe != nil && found.Meta.SetupDoneProbe.URL != ""
data["HasAppInfo"] = found.Meta.HasAppInfo()
data["EffectiveSubdomain"] = effectiveSubdomain
@@ -1129,8 +1140,8 @@ func (s *Server) backupsRemoteHandler(w http.ResponseWriter, r *http.Request) {
// 1./2./3. tier rows.
func (s *Server) backupsAppsHandler(w http.ResponseWriter, r *http.Request) {
data := s.backupsCommonData("backups-apps", "Biztonsági mentés — Alkalmazások", r)
data["TitleKey"] = "page.title.backups_apps" // i18n: the Hungarian title above is what hu renders
s.backupsOffboxData(data, s.langFor(r)) // the tier-3 rows render $.Offbox status
data["TitleKey"] = "page.title.backups_apps" // i18n: the Hungarian title above is what hu renders
s.backupsOffboxData(data, s.langFor(r)) // the tier-3 rows render $.Offbox status
data["HollowCopyLines"] = s.hollowCopyLines(s.langFor(r)) // Part D: running apps whose copy holds no data
if fullStatus, ok := data["Backup"].(*backup.FullBackupStatus); ok && fullStatus != nil {
@@ -2477,7 +2488,9 @@ func (s *Server) appAutoFieldRevealHandler(w http.ResponseWriter, r *http.Reques
escrowJSON(w, http.StatusBadRequest, nil, s.msg(r, "escrow.missing_field"))
return
}
// AUTHORISATION: the field must be an auto-generated SECRET of this stack's catalog metadata.
// AUTHORISATION: the field must be an auto-generated SECRET of this stack's catalog metadata — or, since
// v0.280.0 (R-709), a `type: password` field of an INSTALLED app, unless a restore generated it (R-694:
// that value is not the app's login and is never shown).
allowed := false
for _, f := range stack.Meta.AutoGeneratedFields() {
if f.EnvVar == envVar && f.Type == "secret" {
@@ -2485,6 +2498,18 @@ func (s *Server) appAutoFieldRevealHandler(w http.ResponseWriter, r *http.Reques
break
}
}
if !allowed && stack.Deployed {
restored := false
if stack.AppConfig != nil {
restored = slices.Contains(stack.AppConfig.RestoredLogins, envVar)
}
for _, f := range stack.Meta.UserFacingFields() {
if f.EnvVar == envVar && f.Type == "password" && !restored {
allowed = true
break
}
}
}
if !allowed {
s.logger.Printf("[WARN] [web] auto-field reveal refused for %s/%s: not an auto-generated secret field", stackName, envVar)
escrowJSON(w, http.StatusForbidden, nil, s.msg(r, "escrow.field_not_revealable"))