- Status: done
- Date: 2026-06-13
- Specs touched:
APP_MANIFEST.md(# D2 —showflag + example, locked decisions),SERVICE_PROVISIONING.md(# Env-var injection — owner-visible paragraph, locked decisions),DASHBOARD.md(# Installed apps — Setup secrets surface),DECISIONS.md2026-06-13
Closes #152. The brain already generated and injected per-app secrets (MALMO_SECRET_<NAME>), but had no way to show one to the owner — so a self-authenticating app (login gated by a token rather than malmo's session) had to ship a published constant as its bootstrap token (Jupyter's molma-setup, #124/#136), whose self-grepping disable gate fails open into a permanent LAN backdoor on a future image bump. This adds the missing molma-side capability: a manifest can mark a secret owner-visible, and the owner reads its value from the app detail page — so the bootstrap token can be per-instance random instead of public. The Jupyter manifest migration off molma-setup is a deliberate follow-up PR (see Known gaps).
Secretgains an optionalShow bool(yaml:"show,omitempty").show: trueopts the secret into owner-visibility; omitted (default) keeps it internal. Purely additive — nomanifest_versionbump, back-compatible, andvalidateSecretsis unchanged (the flag needs no normalization). Test:TestParseSecretShowFlag.
- New
RevealSecrets(id)joins the instance-dir manifest'sshowflags against the stored secret values and returns only the owner-visible{name, value}pairs (nil when none declared, short-circuiting before the store read). The manifest read is the installer's persisted instance-dir copy, so a later catalog withdrawal never changes what's revealable. The internal-only filter means a single reveal can't dump every injected credential (a managed-service password stays internal). Tests:TestRevealSecretsReturnsOnlyOwnerVisible,…EmptyWhenNoneVisible,…UnknownInstance.
GET /api/v1/apps/{id}/secrets(get-app-secrets) behindauthorizeAppMutation— the same owner-or-admin control-authorization gate as stop/start: a member reads their own personal app's secret, a household app is admin-only, another user's personal app 404s (existence leak guard). DTOsAppSecretDTO{name,value}/AppSecretsDTO{secrets[]}; a host/manifest read failure maps to 500, not a misleading empty 200. Revealing is a pure read, so it does not audit (only elevation-class mutations audit, per the house rule). Registered viaregisterAppSecretsinregisterAll. Tests (appsecrets_test.go): 401 (unauth via the full router + direct handler), owner-sees-only-visible, admin-sees-any, other-member 404, member-household 403, reveal-error 500 — handler + registration at 100%,RevealSecretsat 94% (the lone gap is the defensive store-error wrap).
- A Setup secrets section on the app detail page, shown only when
canControland the app declares an owner-visible secret (the list comes back empty otherwise). Each secret is masked by default (••••••••••••) with a per-secret Reveal/Hide toggle and a Copy button. Copy is best-effort:navigator.clipboardis unavailable on the HTTP-only.localorigin, so the value staysselect-allon screen as the fallback (same constraintRecoverView.vuealready handles for recovery codes). NewAppSecrets/AppSecrettype aliases. OpenAPI spec + generated TS regenerated.
- Jupyter migration (the motivating follow-up). With the reveal in place,
catalog/jupyter/compose.ymlshould move off the publishedmolma-setupconstant to a per-instance randomsecrets: [{name: setup_token, show: true}]value — a separate PR per #152's "Done when". Until it lands, theBUMPING THE IMAGE TAGnote in that compose stays the floor. - No first-class "this app authenticates itself" seam. This issue addressed only the surfacing gap (#152's root-cause half 1); the larger gap — auth hand-rolled in a compose
command:rather than a manifest declaration — is unaddressed and tracked inNEXT.md. - At-rest + reveal hardening. Secret values are plaintext in SQLite and in the
.envon disk (unchanged by this PR); reveal adds an owner-gated read surface but no new at-rest exposure. Encryption/rotation/delivery hardening stays open inNEXT.md# App-secret injection hardening. - No install-success surface. #152 allowed "install success and/or the detail page"; this ships the durable detail-page home only. An install-completion reveal is a possible later enhancement.
- Drive-by, not part of #152's scope: re-applied the pre-existing
mainLiveResources.vuevue-tsc break fix (hoist the load line into aloadLinecomputed — an inline.mapoversample.loaddefeats thev-elsenull-narrowing). It is required for this PR'sci-webgate (path-filtered toweb-ui/**) to pass, and is the same fix carried by the in-flight #149/PR #158; whichever of the two merges second resolves the trivial overlap.