oficina / home Public

Merge branch 'codex/home-placeholder-icons' into main

paul@keymail.dev 8535299e4c22862562bd9f136128fb86811cbf24 parent f1caf3aparent 0493320
4 files changed, +152 −0
  • cmd/home-placeholder/README.md +26 −0
  • cmd/home-placeholder/main.go +32 −0
  • cmd/home-placeholder/main_test.go +75 −0
  • internal/home/placeholder.go +19 −0

CI passed — run details

diff --git a/cmd/home-placeholder/README.md b/cmd/home-placeholder/README.md
new file mode 100644
index 0000000..7a41754
--- /dev/null
+++ b/cmd/home-placeholder/README.md
@@ -0,0 +1,26 @@
+# Public placeholder release
+
+Build `./cmd/home-placeholder` for the existing public skeleton at
+`home.bds.oncarlos.com` (including its aliases). It serves the same placeholder
+page and Oficina icons without opening a database or loading secrets. Activation
+may supply `--db`; this command deliberately ignores it.
+
+The paired credential/directory deployments continue to use `./cmd/home`.
+Do not deploy that binary onto the old public skeleton until its database and
+identity/domain migration have been planned and accepted. The old skeleton's
+Litestream-only database cannot be adopted by the current identity migrations.
+
+Stamp the release with the source commit in
+`github.com/carlosframework/rastrillo.BuildVersion`, as for the main Home binary.
+
+```sh
+CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath \
+ -ldflags "-s -w -X github.com/carlosframework/rastrillo.BuildVersion=$(git rev-parse --short HEAD)" \
+ -o /tmp/home-placeholder ./cmd/home-placeholder
+```
+
+Run Home's full `make test` gate on Linux with the checkout on ext4 or XFS.
+The provisioner is Linux-only and deliberately refuses overlayfs, Btrfs,
+and host-shared filesystems, including ordinary Docker bind mounts on macOS.
+A fresh runner also needs access to the pinned private Estilo Go module, or
+its verified Go download cache, before starting the gate.
diff --git a/cmd/home-placeholder/main.go b/cmd/home-placeholder/main.go
new file mode 100644
index 0000000..570421e
--- /dev/null
+++ b/cmd/home-placeholder/main.go
@@ -0,0 +1,32 @@
+// Command home-placeholder preserves the public, stateless Home skeleton while
+// the identity/directory database rollout is prepared separately. Never use it
+// for a credential or directory instance; those run cmd/home.
+package main
+
+import (
+ "log/slog"
+ "net/http"
+ "os"
+
+ "amadan.net/oficina/home/internal/home"
+ "github.com/carlosframework/rastrillo"
+)
+
+func main() {
+ logger := slog.Default()
+ opts, err := rastrillo.Resolve(rastrillo.Options{Logger: logger})
+ if err != nil {
+ logger.Error("resolve", "err", err)
+ os.Exit(1)
+ }
+ // Activation may supply --db even though this binary has no storage.
+ // Clear it after Resolve so Serve cannot open or mutate that database.
+ opts.DBPath = ""
+ mux := http.NewServeMux()
+ mux.Handle("/", home.PlaceholderHandler())
+ opts.Mux = mux
+ if err := rastrillo.Serve(opts); err != nil {
+ logger.Error("serve", "err", err)
+ os.Exit(1)
+ }
+}
diff --git a/cmd/home-placeholder/main_test.go b/cmd/home-placeholder/main_test.go
new file mode 100644
index 0000000..97e6412
--- /dev/null
+++ b/cmd/home-placeholder/main_test.go
@@ -0,0 +1,75 @@
+package main
+
+import (
+ "io"
+ "net"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestPlaceholderNeverOpensActivationDatabase(t *testing.T) {
+ dir := t.TempDir()
+ binary := filepath.Join(dir, "home-placeholder")
+ build := exec.Command("go", "build", "-o", binary, ".")
+ if out, err := build.CombinedOutput(); err != nil {
+ t.Fatalf("build: %v: %s", err, out)
+ }
+ l, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ addr := l.Addr().String()
+ l.Close()
+ // The parent is deliberately missing: any database open must fail boot.
+ database := filepath.Join(dir, "absent", "must-not-exist.db")
+ cmd := exec.Command(binary, "-addr", addr, "-db", database)
+ cmd.Env = []string{"PATH=" + os.Getenv("PATH")}
+ if err := cmd.Start(); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { _ = cmd.Process.Kill(); _ = cmd.Wait() })
+ client := &http.Client{Timeout: time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
+ origin := "http://" + addr
+ deadline := time.Now().Add(10 * time.Second)
+ for {
+ response, err := client.Get(origin + "/healthz")
+ if err == nil {
+ response.Body.Close()
+ if response.StatusCode == 200 {
+ break
+ }
+ }
+ if time.Now().After(deadline) {
+ t.Fatal("placeholder did not start without secrets and with an unusable database path")
+ }
+ time.Sleep(25 * time.Millisecond)
+ }
+ for path, status := range map[string]int{"/": 200, "/static/brand/icon.svg": 200, "/signin": 404, "/authorize": 404, "/static/brand/": 404} {
+ response, err := client.Get(origin + path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ body, err := io.ReadAll(response.Body)
+ response.Body.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if response.StatusCode != status {
+ t.Errorf("%s: status %d, want %d", path, response.StatusCode, status)
+ }
+ if len(response.Cookies()) != 0 {
+ t.Errorf("%s set cookies", path)
+ }
+ if path == "/" && !strings.Contains(string(body), `href="/static/brand/icon.svg"`) {
+ t.Error("missing favicon")
+ }
+ }
+ if _, err := os.Stat(filepath.Dir(database)); !os.IsNotExist(err) {
+ t.Fatalf("database parent created or unreadable: %v", err)
+ }
+}
diff --git a/internal/home/placeholder.go b/internal/home/placeholder.go
new file mode 100644
index 0000000..e388bd4
--- /dev/null
+++ b/internal/home/placeholder.go
@@ -0,0 +1,19 @@
+package home
+
+import (
+ "net/http"
+
+ "github.com/go-chi/chi/v5"
+)
+
+// PlaceholderHandler serves the original public landing page and approved
+// artwork without constructing any identity, directory, or database services.
+func PlaceholderHandler() http.Handler {
+ r := chi.NewRouter()
+ mountBrand(r)
+ r.Get("/", func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _, _ = w.Write([]byte(indexPage))
+ })
+ return r
+}