A malformed address says so; Memoria capitalised (review of the door)
The door's form leaves validation to the server, so "paul@" or an empty field reached the owner check and was told a link had been sent. A shape check now answers ?err=address first — the same for everyone, looking at the text only, so it reveals nothing about owners. The header and page titles say Memoria, as the door does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 files changed,
+45
−9
internal/memoria/auth.go+20 −0internal/memoria/auth_test.go+16 −0internal/memoria/templates/add.html+1 −1internal/memoria/templates/connectors.html+1 −1internal/memoria/templates/credits.html+1 −1internal/memoria/templates/document.html+1 −1internal/memoria/templates/index.html+1 −1internal/memoria/templates/layout.html+2 −2internal/memoria/templates/notion.html+1 −1internal/memoria/templates/signin.html+1 −1
CI passed — run details
CI log
=== vet === go vet ./... === fmt === === test === go test ./... ? amadan.net/oficina/memoria/cmd/memoria [no test files] ok amadan.net/oficina/memoria/internal/memoria 3.624s ok amadan.net/oficina/memoria/internal/memoria/chunk (cached) ok amadan.net/oficina/memoria/internal/memoria/fact (cached) ? amadan.net/oficina/memoria/internal/memoria/icons [no test files] ok amadan.net/oficina/memoria/internal/memoria/index (cached) ok amadan.net/oficina/memoria/internal/memoria/notion (cached) ok amadan.net/oficina/memoria/internal/memoria/provider (cached) ok amadan.net/oficina/memoria/internal/memoria/query (cached) ok amadan.net/oficina/memoria/internal/memoria/seal (cached) ok amadan.net/oficina/memoria/internal/memoria/sibling (cached) ok amadan.net/oficina/memoria/internal/memoriatest 1.865s ? amadan.net/oficina/memoria/scripts/writeestilo [no test files] ? amadan.net/oficina/memoria/tools/bench [no test files] ? amadan.net/oficina/memoria/tools/drive [no test files] ? amadan.net/oficina/memoria/tools/seed [no test files] ? amadan.net/oficina/memoria/tools/vendorassets [no test files] === migration-check === go run github.com/carlosframework/rastrillo/cmd/rastrillo migration check rastrillo migration check: models and migrations agree
diff --git a/internal/memoria/auth.go b/internal/memoria/auth.go| index 6b4b162..57c1154 100644 |
| --- a/internal/memoria/auth.go |
| +++ b/internal/memoria/auth.go |
| @@ -117,6 +117,14 @@ func (a *app) beginGuard(origin string, next http.HandlerFunc) http.HandlerFunc |
| // differently from the thing it guards is not guarding it. There |
| // is no error branch now, because FormValue has no error to |
| // branch on — the two calls cannot disagree. |
| + // A shape check first, answered the same for everyone: the door's |
| + // form leaves validation to the server, and "paul@" or an empty |
| + // field should say so rather than claim a link was sent. It looks |
| + // only at the text, never at who owns what, so it tells nothing. |
| + if !plausibleAddress(r.FormValue("address")) { |
| + http.Redirect(w, r, signinPath+"?err=address", http.StatusSeeOther) |
| + return |
| + } |
| if !a.isOwner(normaliseAddress(r.FormValue("address"))) { |
| // No address in the log line, ever: §3.2, gate 4. |
| a.logger.Info("sign-in refused before classifier dial") |
| @@ -268,3 +276,15 @@ type oauthPage struct { |
| Data connect.OAuthData |
| Door chrome.SignInNotice |
| } |
| + |
| +// plausibleAddress is the shape of an email address and nothing more: |
| +// something, one @, a domain with a dot, no spaces. |
| +func plausibleAddress(v string) bool { |
| + v = strings.TrimSpace(v) |
| + at := strings.LastIndexByte(v, '@') |
| + if at <= 0 || at == len(v)-1 || strings.ContainsAny(v, " \t\r\n") || strings.Count(v, "@") != 1 { |
| + return false |
| + } |
| + domain := v[at+1:] |
| + return strings.Contains(domain, ".") && !strings.HasPrefix(domain, ".") && !strings.HasSuffix(domain, ".") |
| +} |
diff --git a/internal/memoria/auth_test.go b/internal/memoria/auth_test.go| new file mode 100644 |
| index 0000000..101568f |
| --- /dev/null |
| +++ b/internal/memoria/auth_test.go |
| @@ -0,0 +1,16 @@ |
| +package memoria |
| + |
| +import "testing" |
| + |
| +func TestPlausibleAddressIsShapeOnly(t *testing.T) { |
| + for v, want := range map[string]bool{ |
| + "paul@example.com": true, " paul@example.com ": true, "a@b.co": true, |
| + "": false, "paul@": false, "@example.com": false, "paul": false, |
| + "paul@example": false, "paul@.com": false, "paul@example.": false, |
| + "pa ul@example.com": false, "a@b@c.com": false, |
| + } { |
| + if got := plausibleAddress(v); got != want { |
| + t.Errorf("plausibleAddress(%q) = %v, want %v", v, got, want) |
| + } |
| + } |
| +} |
diff --git a/internal/memoria/templates/add.html b/internal/memoria/templates/add.html| index 7af1689..2c5439e 100644 |
| --- a/internal/memoria/templates/add.html |
| +++ b/internal/memoria/templates/add.html |
| @@ -1,4 +1,4 @@ |
| -{{define "title"}}Add content — memoria{{end}} |
| +{{define "title"}}Add content — Memoria{{end}} |
| {{define "content"}} |
| {{/* Adding content, on its own page (2026-09-29, Paul: "remove the Add |
| forms from the index … that should be a separate Add content form"). |
diff --git a/internal/memoria/templates/connectors.html b/internal/memoria/templates/connectors.html| index 83e1060..0872f10 100644 |
| --- a/internal/memoria/templates/connectors.html |
| +++ b/internal/memoria/templates/connectors.html |
| @@ -1,4 +1,4 @@ |
| -{{define "title"}}Sources · memoria{{end}} |
| +{{define "title"}}Sources · Memoria{{end}} |
| {{define "content"}} |
| <h1>Sources</h1> |
diff --git a/internal/memoria/templates/credits.html b/internal/memoria/templates/credits.html| index 9f026c3..c9d1e6d 100644 |
| --- a/internal/memoria/templates/credits.html |
| +++ b/internal/memoria/templates/credits.html |
| @@ -1,4 +1,4 @@ |
| -{{define "title"}}Open source · memoria{{end}} |
| +{{define "title"}}Open source · Memoria{{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}} |
diff --git a/internal/memoria/templates/document.html b/internal/memoria/templates/document.html| index 50538e9..e80a57e 100644 |
| --- a/internal/memoria/templates/document.html |
| +++ b/internal/memoria/templates/document.html |
| @@ -1,4 +1,4 @@ |
| -{{define "title"}}{{.Title}} — memoria{{end}} |
| +{{define "title"}}{{.Title}} — Memoria{{end}} |
| {{define "content"}} |
| <nav rst-breadcrumb><a href="/">← Back to search</a></nav> |
| <h1>{{.Title}}</h1> |
diff --git a/internal/memoria/templates/index.html b/internal/memoria/templates/index.html| index efd4d7f..02d824a 100644 |
| --- a/internal/memoria/templates/index.html |
| +++ b/internal/memoria/templates/index.html |
| @@ -1,4 +1,4 @@ |
| -{{define "title"}}memoria{{end}} |
| +{{define "title"}}Memoria{{end}} |
| {{define "content"}} |
| <form method="get" action="/" rst-search> |
diff --git a/internal/memoria/templates/layout.html b/internal/memoria/templates/layout.html| index 9dafaa4..6d3b6d3 100644 |
| --- a/internal/memoria/templates/layout.html |
| +++ b/internal/memoria/templates/layout.html |
| @@ -7,7 +7,7 @@ |
| <link rel="stylesheet" href="{{asset "static/brand/brand.css"}}"> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1"> |
| -<title>{{block "title" .}}memoria{{end}}</title> |
| +<title>{{block "title" .}}Memoria{{end}}</title> |
| <link rel="stylesheet" href="/static/estilo/fonts/v1/fonts.css"> |
| <link rel="stylesheet" href="{{asset "static/tokens.css"}}"> |
| <link rel="stylesheet" href="{{asset "static/busy.css"}}"> |
| @@ -34,7 +34,7 @@ |
| </head> |
| <body> |
| <header rst-page-header> |
| - <a href="/" rst-brand estilo-brand><img estilo-brand-icon src="{{asset "static/brand/icon.svg"}}" alt="" width="28" height="28">memoria</a> |
| + <a href="/" rst-brand estilo-brand><img estilo-brand-icon src="{{asset "static/brand/icon.svg"}}" alt="" width="28" height="28">Memoria</a> |
| {{if .Signed}}<nav rst-inline> |
| <details rst-dropdown name="rst-menus"><summary>Help</summary><div rst-dropdown-menu><a href="/help/open-source" referrerpolicy="no-referrer" rel="noreferrer">Open source</a></div></details> |
| {{if .HomeEnabled}}<a href="/account/home" rst-btn="ghost">Connect Home account</a>{{end}} |
diff --git a/internal/memoria/templates/notion.html b/internal/memoria/templates/notion.html| index 83e8ae7..46c5278 100644 |
| --- a/internal/memoria/templates/notion.html |
| +++ b/internal/memoria/templates/notion.html |
| @@ -1,4 +1,4 @@ |
| -{{define "title"}}Notion · memoria{{end}} |
| +{{define "title"}}Notion · Memoria{{end}} |
| {{/* The status page refreshes itself ONLY while a run is active, and it |
| is a meta refresh because every affordance here works scriptless. |
diff --git a/internal/memoria/templates/signin.html b/internal/memoria/templates/signin.html| index 242ac1c..c4d1151 100644 |
| --- a/internal/memoria/templates/signin.html |
| +++ b/internal/memoria/templates/signin.html |
| @@ -1,4 +1,4 @@ |
| -{{define "title"}}Sign in — memoria{{end}} |
| +{{define "title"}}Sign in — Memoria{{end}} |
| {{/* The suite's door (estilo chrome): one card on the team's sky, the |
| app's mark and name at its head. chrome.css draws the card, sky.css |
| the sky; both are vendored from estilo (go run ./scripts/writeestilo). |