oficina / memoria Public

Keep credits within Memoria admission and address review findings

Paul Campbell pushed by paul@keymail.dev 480be17a4943cae964910d65ee360389a013a8a8 parent 2ceb79c
10 files changed, +80 −24
  • Makefile +1 −0
  • docs/superpowers/specs/2026-09-03-memoria-design.md +11 −8
  • go.mod +3 −1
  • go.sum +2 −2
  • implementation/open-source-credits/proposal.json +5 −1
  • internal/memoria/app.go +1 −1
  • internal/memoria/credits.go +8 −4
  • internal/memoria/credits_test.go +44 −4
  • internal/memoria/templates/credits.html +3 −1
  • internal/memoria/templates/layout.html +2 −2

CI passed — run details

diff --git a/Makefile b/Makefile
index 12d8a51..f77e342 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,4 @@
+export GOTOOLCHAIN := go1.26.5
export GOPRIVATE := $(GOPRIVATE),amadan.net/oficina/estilo.git
APP := memoria
diff --git a/docs/superpowers/specs/2026-09-03-memoria-design.md b/docs/superpowers/specs/2026-09-03-memoria-design.md
index 2b876a8..edc74a6 100644
--- a/docs/superpowers/specs/2026-09-03-memoria-design.md
+++ b/docs/superpowers/specs/2026-09-03-memoria-design.md
@@ -1494,6 +1494,12 @@ No guests, no anonymous link viewers — and after this section, no guest
of a *sibling* either. Admission is not authorization: an admitted person
still sees only what §4.1 and §4.2 allow.
+The suite attribution page (`GET /help/open-source`, 2026-09-11) uses
+this same session gate. It serves embedded software notices and retains
+the signed-in navigation, without reading content or passage counts. Its
+stylesheet is a public static asset, like the other stylesheets. The
+credits page omits the passage-count footer; it creates no session.
+
---
## 5. Ingestion
@@ -2914,7 +2920,8 @@ more than one read from memory:
`GET /`, `GET /search`, `GET /documents/{id}`, `POST /documents`,
`POST /documents/{id}/delete`, the connector routes under
-`/connectors/…`, and the auth routes.
+`/connectors/…`, authenticated `GET /help/open-source`, public static
+`GET /static/credits.css`, and the auth routes.
Two of those need saying out loud. **`GET /query?q=` above is not served
and never has been** — the search page is `GET /?q=`, and the paragraph
@@ -2969,12 +2976,6 @@ renders on its own origin. The home never sees `q`.
## 10. Testing gates
-The suite attribution request adds public `GET /help/open-source` and
-`GET /static/credits.css` on 2026-09-11. They serve embedded software
-notices and shared Estilo styles. Neither route reads team content,
-checks passage counts, nor grants a session. Help links from the signed-in
-header to the notices page. The route inventory declares both explicitly.
-
**Two rules govern this list, and §1.4 is why.** Three of the first five
gates built could not fail for the thing they named.
@@ -3171,7 +3172,9 @@ The gates:
fail any inbound route not on the person-facing or connector list —
the regression gate for the push surface draft 1 shipped by accident.
**The list is literal, not descriptive:** `GET /`, `GET /documents/{id}`,
- `POST /documents`, `POST /documents/{id}/delete`, plus the auth routes.
+ `POST /documents`, `POST /documents/{id}/delete`, authenticated
+ `GET /help/open-source`, public static `GET /static/credits.css`,
+ plus the auth routes.
Written that way so that adding a thread or chat route goes red on the
way in (decision 14) rather than being argued about afterwards.
**Built 2026-09-04** as `TestNoUndeclaredRoutes`; **rewritten
diff --git a/go.mod b/go.mod
index 4f00b50..5c4d3d0 100644
--- a/go.mod
+++ b/go.mod
@@ -2,8 +2,10 @@ module amadan.net/oficina/memoria
go 1.26
+toolchain go1.26.5
+
require (
- amadan.net/oficina/estilo.git v0.0.0-20260911162556-f5fa1128b794
+ amadan.net/oficina/estilo.git v0.0.0-20260911182719-0bcb9287988d
amadan.net/oficina/home/connect v0.0.0-20260910000424-5b8d8410775f
amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489
amadan.net/oficina/suite v0.0.0-20260905215529-60b01a8a0d6d
diff --git a/go.sum b/go.sum
index 5ddca98..70db724 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-amadan.net/oficina/estilo.git v0.0.0-20260911162556-f5fa1128b794 h1:2oBeIneg/f+m3/n8qLsQGlpEhdnAfW3kOI80yRv60pw=
-amadan.net/oficina/estilo.git v0.0.0-20260911162556-f5fa1128b794/go.mod h1:0R45/vCUAT+kpS7S6K/ZxBdE8f7YxFN4HT3HyDJ3nPY=
+amadan.net/oficina/estilo.git v0.0.0-20260911182719-0bcb9287988d h1:3HsB96tGs+P0tGf2VzqpqA8yqYBhJ+apBh2txAoz9qI=
+amadan.net/oficina/estilo.git v0.0.0-20260911182719-0bcb9287988d/go.mod h1:0R45/vCUAT+kpS7S6K/ZxBdE8f7YxFN4HT3HyDJ3nPY=
amadan.net/oficina/home/connect v0.0.0-20260910000424-5b8d8410775f h1:lkw3tjzHfZxe74pNfo+tGDz4mkZLg+D6R+FTkmsym6M=
amadan.net/oficina/home/connect v0.0.0-20260910000424-5b8d8410775f/go.mod h1:YroavXe43Wietec+po8mtSk4a9Bir2h31Q3iAf4MWxI=
amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489 h1:RPKtyry/71i06o8L+gjAaUyexbmG56kO1YPs601URAM=
diff --git a/implementation/open-source-credits/proposal.json b/implementation/open-source-credits/proposal.json
index 89e6bb5..7234ae5 100644
--- a/implementation/open-source-credits/proposal.json
+++ b/implementation/open-source-credits/proposal.json
@@ -17,5 +17,9 @@
"320/1200px Chromium reflow and keyboard with scripts disabled",
"Five mutation checks caught removed routes, notice drift, invalid attribute, undeclared route; reverted"
],
- "review": "Pending Claude Fable; no deploy"
+ "review": "Fable5.1 requested public-readiness changes at2ceb79c; this revision addresses them and requires a new review.",
+ "access": "Credits page now uses existing requireSession; CSS remains a public static asset. Both literal spec route lists and admission section updated.",
+ "behavior": "Signed-in header retained; passage-count footer omitted, with no count query. Embedded inventory parsed at boot; missing notice explicitly labelled. CSS revalidates; release/generator/test GOTOOLCHAIN fixed togo1.26.5.",
+ "shared": "Estilo0bcb928: corrected divider token, original provenance gate, heading/newline fix and missing-notice label.",
+ "verification": "Full make ci passed; session-boundary and served-CSS-token mutations failed correctly; native320/1200 keyboard/reflow/separator checks passed. Final brand-size adjustment rechecked."
}
diff --git a/internal/memoria/app.go b/internal/memoria/app.go
index 4b1d685..3f9c121 100644
--- a/internal/memoria/app.go
+++ b/internal/memoria/app.go
@@ -75,7 +75,6 @@ func App(d *db.DB, origin string, sealer *seal.Sealer, logger *slog.Logger) (*ht
r.Use(a.homeMiddleware(origin))
// Public: the sign-in page and auth's own callbacks.
- r.Get("/help/open-source", a.credits)
r.Get("/signin", a.signin)
r.Get("/signin/local", func(w http.ResponseWriter, r *http.Request) {
if a.home == nil {
@@ -126,6 +125,7 @@ func App(d *db.DB, origin string, sealer *seal.Sealer, logger *slog.Logger) (*ht
// door.
r.Group(func(r chi.Router) {
r.Use(a.requireSession)
+ r.Get("/help/open-source", a.credits)
r.Get("/", a.index)
r.Get("/search", a.searchResults)
r.Post("/documents", a.createDocument)
diff --git a/internal/memoria/credits.go b/internal/memoria/credits.go
index ca6c6b7..9a7ba3e 100644
--- a/internal/memoria/credits.go
+++ b/internal/memoria/credits.go
@@ -16,19 +16,23 @@ type creditInventory struct {
MissingNotices []string `json:"missing_notices"`
}
-func (a *app) credits(w http.ResponseWriter, r *http.Request) {
+var embeddedCredits = func() creditInventory {
var inventory creditInventory
if err := json.Unmarshal(creditNotices, &inventory); err != nil {
- http.Error(w, "something went wrong", http.StatusInternalServerError)
- return
+ panic(err)
}
+ return inventory
+}()
+
+func (a *app) credits(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Referrer-Policy", "no-referrer")
- render(w, "credits", map[string]any{"Data": credits.PageData{Title: "Open source", Components: inventory.Components}})
+ render(w, "credits", map[string]any{"Signed": true, "HomeEnabled": a.showHomeConnection(r), "Data": credits.PageData{Title: "Open source", MissingNoticeLabel: "No notice file is included in this release.", Components: embeddedCredits.Components}})
}
func serveCreditsCSS(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css; charset=utf-8")
+ w.Header().Set("Cache-Control", "no-cache")
w.Write(credits.CSS())
}
diff --git a/internal/memoria/credits_test.go b/internal/memoria/credits_test.go
index f9d9aa9..825d1f6 100644
--- a/internal/memoria/credits_test.go
+++ b/internal/memoria/credits_test.go
@@ -12,13 +12,25 @@ import (
)
// Mutation-checked 2026-09-11: removing either route fails this test.
-func TestCreditsArePublicAndContainDependencyNotices(t *testing.T) {
+func TestCreditsRequireSessionAndContainDependencyNotices(t *testing.T) {
fixture := newHomeRouterFixture(t)
app := fixture.handler
- response := httptest.NewRecorder()
- app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, homeTestOrigin+"/help/open-source", nil))
+ anonymous := fixture.do(http.MethodGet, "/help/open-source", "")
+ if anonymous.Code != http.StatusSeeOther || strings.Contains(anonymous.Body.String(), "estilo-credits") {
+ t.Fatal("anonymous credits exposed", anonymous.Code)
+ }
+ fixture.local(t)
+ response := fixture.do(http.MethodGet, "/help/open-source", "")
if response.Code != http.StatusOK || response.Header().Get("Location") != "" || len(response.Result().Cookies()) != 0 {
- t.Fatal("credits required authentication", response.Code)
+ t.Fatal("signed-in credits unavailable", response.Code)
+ }
+ if strings.Contains(response.Body.String(), `rst-refresh="passages"`) {
+ t.Fatal("credits rendered a passage count")
+ }
+ for _, wanted := range []string{"Sign out", "Sources", "No notice file is included in this release."} {
+ if !strings.Contains(response.Body.String(), wanted) {
+ t.Fatal("credits lost signed-in shell or missing notice label", wanted)
+ }
}
if response.Header().Get("Cache-Control") != "no-store" || response.Header().Get("Referrer-Policy") != "no-referrer" {
t.Fatal("credits shell may retain account context")
@@ -59,3 +71,31 @@ func TestCreditsArePublicAndContainDependencyNotices(t *testing.T) {
t.Fatal("new missing notice requires review")
}
}
+
+// Mutation-checked 2026-09-11: a served undefined token fails this gate.
+func TestCreditStylesUseServedTokens(t *testing.T) {
+ definitions := map[string]bool{}
+ for _, path := range []string{"static/tokens.css", "static/theme.css"} {
+ raw, err := appFS.ReadFile(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, m := range regexp.MustCompile(`(--rst-[a-z0-9-]+)\s*:`).FindAllSubmatch(raw, -1) {
+ definitions[string(m[1])] = true
+ }
+ }
+ response := httptest.NewRecorder()
+ serveCreditsCSS(response, httptest.NewRequest(http.MethodGet, "/static/credits.css", nil))
+ if response.Header().Get("Cache-Control") != "no-cache" {
+ t.Fatal("shared CSS can be stale")
+ }
+ references := regexp.MustCompile(`var\(\s*(--rst-[a-z0-9-]+)`).FindAllSubmatch(response.Body.Bytes(), -1)
+ if len(references) == 0 {
+ t.Fatal("credit token references disappeared")
+ }
+ for _, m := range references {
+ if !definitions[string(m[1])] {
+ t.Fatal("undefined served credit token", string(m[1]))
+ }
+ }
+}
diff --git a/internal/memoria/templates/credits.html b/internal/memoria/templates/credits.html
index 8ba92f4..9f026c3 100644
--- a/internal/memoria/templates/credits.html
+++ b/internal/memoria/templates/credits.html
@@ -1,3 +1,5 @@
{{define "title"}}Open source · memoria{{end}}
-{{define "head"}}<link rel="stylesheet" href="/static/credits.css">{{end}}
+{{define "head"}}<link rel="stylesheet" href="/static/credits.css"><style>[rst-brand] { min-block-size: 24px; display: inline-flex; align-items: center; }</style>{{end}}
{{define "content"}}{{template "estilo/credits" .Data}}{{end}}
+
+{{define "footer"}}<footer rst-page-footer></footer>{{end}}
diff --git a/internal/memoria/templates/layout.html b/internal/memoria/templates/layout.html
index 1e59e76..5fbd414 100644
--- a/internal/memoria/templates/layout.html
+++ b/internal/memoria/templates/layout.html
@@ -38,7 +38,7 @@
<main rst-page>
{{template "content" .}}
</main>
-<footer rst-page-footer>
+{{block "footer" .}}<footer rst-page-footer>
{{/* The passage count is stale for the same reason the document list
is — an upload that never navigates cannot change either — so it
carries rst-refresh and drop.js swaps it out of the same answer.
@@ -55,7 +55,7 @@
Search is lexical only — no assistant, and <strong>no document ever
leaves this instance</strong>.
</p>
-</footer>
+</footer>{{end}}
</body>
</html>
{{end}}