- Status: done
- Date: 2026-06-05
- Specs touched:
APP_MANIFEST.md,SERVICE_PROVISIONING.md,DECISIONS.md,NEXT.md
Closes the blocks-start gap captured in docs/dev/catalog-import-gaps.md # secret-injection — kan: an app that needs an app-specific random signing secret (BETTER_AUTH_SECRET for Better Auth, and the whole class of JWT/HMAC/SECRET_KEY_BASE secrets) had no way to obtain one — the author can't ship a value in a public catalog and the non-technical user can't be asked to generate one, so the app simply wouldn't boot. The brain now generates and injects it. Design rationale in DECISIONS.md 2026-06-05.
- New top-level
secrets: [{name, bytes?}]field (Secretstruct).nameis lowercase snake_case (validated bysecretName);bytesdefaults toDefaultSecretBytes(32) and is floored atMinSecretBytes(16).validateSecretsrejects bad names and duplicates and normalizesbytesin place, wired intoManifest.validate.
- New
instance_secretstable (instance_id, name, value, PK(instance_id, name),FOREIGN KEY … ON DELETE CASCADE— secrets are reclaimed with the instance via the already-enabledforeign_keys=ON). InstanceSecrettype +SetInstanceSecrets/GetInstanceSecrets, mirroring theinstance_imagesCRUD shape.
- Install step
5b(generating_secrets):generateSecretsdraws each declared secret fromcrypto/rand, base64url-encodes it, and the result is persisted viaSetInstanceSecretsbefore.envis written. On any failure the existing install rollback removes the row (and its cascaded secrets). writeEnvre-emits the persisted secrets asMALMO_SECRET_<NAME>by reading them back from the store, never regenerating — so the value is stable across every.envrewrite. Stability is the load-bearing property: a token-signing secret that changed on restart would invalidate every live session.
- Manifest declares
secrets: [{name: auth}]; compose mapsBETTER_AUTH_SECRET: ${MALMO_SECRET_AUTH}(was a hand-set-before-start placeholder). - Same pass closed kan's
app-url-injectionledger entry:NEXT_PUBLIC_BASE_URL: ${MALMO_APP_URL}. The ledger's claim that no URL-injection mechanism existed was wrong —writeEnvhas injectedMALMO_APP_URL(http://<slug>.local) all along; both kanapp-urlentries are corrected in the ledger.
manifest: byte normalization (default/floor/explicit) and rejection of bad/duplicate names.store: secrets roundtrip + cascade-on-instance-delete.lifecycle: install injects a non-emptyMALMO_SECRET_AUTHthat matches the persisted value; two installs get distinct secrets;generateSecretsentropy → encoded length (32→43, 16→22 chars).malmo manifest check catalog/kan/manifest.ymlpasses (schema + admission).
- Security hardening is deliberately deferred, parked as
NEXT.md# App-secret injection hardening:.envis still0o644(should be0o600/root-owned now that it holds a secret); env-var delivery's leak surface (docker inspect,/proc/environ, child inheritance) vs. the_FILEconvention; at-rest encryption (plaintext in SQLite +.env, relationship to LUKS); backup-archive encryption (the secret must travel in the app's backup for a restored app to keep validating old tokens); rotation and log/audit hygiene. These were reviewed and parked, not missed — the mechanism ships correct-but-unhardened under the household trust model. docusealapp-url-injectionstaysopenin the ledger: no platform gap (it's the sameMALMO_APP_URLmapping), but its compose wasn't rewritten here — revisit on the next docuseal touch.- App update doesn't yet exist; when it lands it must preserve the persisted secret (read from store, never regenerate), which is exactly why persistence — not just the on-disk
.env— is the source of truth.