From f24a4d858ad061002a071df65b2a86b86f891ec4 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 19:58:43 +0200 Subject: [PATCH] issue 1859: move the mail page into internal/web, so the local run can serve it A program cannot import a main package; the page's code moves to modules/mesh-mail-web/internal/web, and cmd/mesh-mail-web is a two-line program calling it, so cmd/mail-page-dev (1859.5) serves the same handlers. Nothing the module does changes. --- .../runtime_player_integration_test.go | 2 +- .../mesh-mail-web/cmd/mesh-mail-web/main.go | 125 +---------------- .../web}/acceptance_integration_test.go | 2 +- .../web}/accounts.go | 2 +- .../web}/accounts_test.go | 2 +- .../mesh-mail-web => internal/web}/auth.go | 2 +- .../web}/auth_test.go | 2 +- .../mesh-mail-web => internal/web}/config.go | 2 +- .../web}/config_test.go | 2 +- .../mesh-mail-web => internal/web}/csrf.go | 2 +- .../web}/csrf_test.go | 2 +- .../mesh-mail-web => internal/web}/mail.go | 2 +- .../web}/mail_test.go | 2 +- .../web}/mailreading_integration_test.go | 2 +- .../web}/mailreading_test.go | 2 +- .../web}/mailwrite.go | 2 +- .../web}/mailwrite_integration_test.go | 2 +- .../web}/mailwrite_test.go | 2 +- .../web}/manifest_test.go | 2 +- modules/mesh-mail-web/internal/web/run.go | 127 ++++++++++++++++++ .../web}/runtime_integration_test.go | 4 +- .../mesh-mail-web => internal/web}/search.go | 2 +- .../web}/search_test.go | 2 +- .../mesh-mail-web => internal/web}/server.go | 2 +- .../web}/server_test.go | 2 +- .../web}/sessions.go | 2 +- .../mesh-mail-web => internal/web}/tools.go | 2 +- .../web}/tools_test.go | 2 +- .../mesh-mail-web => internal/web}/write.go | 2 +- .../web}/write_test.go | 2 +- 30 files changed, 160 insertions(+), 150 deletions(-) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/acceptance_integration_test.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/accounts.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/accounts_test.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/auth.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/auth_test.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/config.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/config_test.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/csrf.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/csrf_test.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/mail.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/mail_test.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/mailreading_integration_test.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/mailreading_test.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/mailwrite.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/mailwrite_integration_test.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/mailwrite_test.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/manifest_test.go (99%) create mode 100644 modules/mesh-mail-web/internal/web/run.go rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/runtime_integration_test.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/search.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/search_test.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/server.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/server_test.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/sessions.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/tools.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/tools_test.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/write.go (99%) rename modules/mesh-mail-web/{cmd/mesh-mail-web => internal/web}/write_test.go (99%) diff --git a/modules/mesh-mail-reader/cmd/mesh-mail-reader/runtime_player_integration_test.go b/modules/mesh-mail-reader/cmd/mesh-mail-reader/runtime_player_integration_test.go index fd580219..0b6fc9a6 100644 --- a/modules/mesh-mail-reader/cmd/mesh-mail-reader/runtime_player_integration_test.go +++ b/modules/mesh-mail-reader/cmd/mesh-mail-reader/runtime_player_integration_test.go @@ -3,7 +3,7 @@ package main // The runtime player for mesh-mail-reader (novox/hq ADR 0192, ADR 0198; issue 1431), mirroring -// mesh-mail-web's own harness (modules/mesh-mail-web/cmd/mesh-mail-web/runtime_integration_test.go, issue +// mesh-mail-web's own harness (modules/mesh-mail-web/internal/web/runtime_integration_test.go, issue // 1285) closely: a built binary, played over its real stdin/stdout (initialize, tools/list, tools/call; // mesh/ask answered by stand-ins for mail-store, claude-code's judge and judged, messenger and // operator-channel). mesh-mail-reader also HEARS mesh-mail's added events (stdio.Subscribe), which diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/main.go b/modules/mesh-mail-web/cmd/mesh-mail-web/main.go index b87d5e9d..fc94cf97 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/main.go +++ b/modules/mesh-mail-web/cmd/mesh-mail-web/main.go @@ -1,124 +1,7 @@ -// mesh-mail-web: the mail web client (novox/hq ADR 0440), built from mesh-mail's source as mesh-mail-notifier is -// (novox/hq ADR 0198 §1, ADR 0175 §1). A Go bundle the node's runtime launches, its own account (runs-as): it -// claims mail-webclient (open, status), reaches mail-store's seat verbs (accounts, search, read, send, -// send-status), and — once issue 1283 lands — answers its own HTTP endpoint `web`. No database, no state, no -// writable directory beyond the one the runtime account needs for its own settings. What this module says, it -// says on stderr; stdout is the MCP channel. +// mesh-mail-web: the mail web client (novox/hq ADR 0440). Its code is the package internal/web, shared with the +// local run (cmd/mail-page-dev, issue 1859); this is the program the module's bundle builds. package main -import ( - "context" - "errors" - "fmt" - "io" - "net/http" - "os" - "sync" - "time" +import "mesh-mail-web/internal/web" - stdio "git.novox.be/novox/mesh-sdk/go" -) - -func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) } - -// The HTTP endpoint's own state (novox/hq ADR 0440 §8; review round 1 of issue 1283, finding 4): before this -// fix, a failure setting up sign-in (the issuer never answering) or starting to listen (its port already -// taken) was only logged — mail-webclient.status went on answering serves: true, and mail_web_health healthy, -// regardless, since neither asked serveHTTP anything. Kept on the Bundle instead, so both tools say the one -// truth. -const ( - HTTPSettingUpSignIn = "setting up sign-in" - HTTPListening = "listening" - HTTPEnded = "ended" -) - -// HTTPStatus is serveHTTP's own phase and (once it has one) its error, read by statusTool and healthTool. -type HTTPStatus struct { - mu sync.Mutex - phase string - err error -} - -// NewHTTPStatus starts HTTPSettingUpSignIn: serveHTTP's own first phase, before issuer discovery has even been -// tried once. -func NewHTTPStatus() *HTTPStatus { return &HTTPStatus{phase: HTTPSettingUpSignIn} } - -func (h *HTTPStatus) set(phase string, err error) { - h.mu.Lock() - defer h.mu.Unlock() - h.phase = phase - h.err = err -} - -// Get answers the endpoint's own phase, and its error once it has one. -func (h *HTTPStatus) Get() (phase string, err error) { - h.mu.Lock() - defer h.mu.Unlock() - return h.phase, h.err -} - -func main() { - b := &Bundle{Env: envFromOS(), HTTP: NewHTTPStatus()} - go serveHTTP(b) - if err := run(b, os.Stdin, os.Stdout); err != nil { - logf("%v", err) - os.Exit(1) - } -} - -// serveHTTP starts the HTTP endpoint `web` (issue 1283), on the port the mesh gave it; a listen it cannot -// start is logged, never fatal to the seat verbs stdio.Serve answers on the MCP channel — but it is also kept -// on b.HTTP (finding 4), so mail-webclient.status and mail_web_health say it rather than going on claiming to -// serve. Sign-in (issue 1263) is set up first, retried on a failure (the issuer may not answer yet): never -// served with no session guard at all, which a nil Auth would be (server.go's own fallback, used by its unit -// tests alone). -func serveHTTP(b *Bundle) { - if b.Env.Listen == "" { - logf("[mesh-mail-web] started without MESH_MAIL_WEB_LISTEN; its HTTP endpoint is not served") - b.HTTP.set(HTTPEnded, errors.New("started without its own listen setting (MESH_MAIL_WEB_LISTEN)")) - return - } - host := InternalHost(b.Env.Node) - var auth *Auth - for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) { - a, err := buildAuth(host, b.Env) - if err == nil { - auth = a - break - } - logf("[mesh-mail-web] sign-in cannot be set up yet (%v); trying again in %s", err, wait) - b.HTTP.set(HTTPSettingUpSignIn, err) - time.Sleep(wait) - } - s := NewServer(host, auth, b) - b.HTTP.set(HTTPListening, nil) - if err := http.ListenAndServe(":"+b.Env.Listen, s); err != nil { - logf("[mesh-mail-web] its HTTP endpoint ended: %v", err) - b.HTTP.set(HTTPEnded, err) - } -} - -// buildAuth reads the oidc-client binding (issue 1262's oidc.json) and discovers the issuer (issue 1263). -func buildAuth(host string, env Env) (*Auth, error) { - cfg, err := ReadOIDCConfig(env.OIDC) - if err != nil { - return nil, err - } - authenticator, err := NewAuthenticator(context.Background(), cfg, "https://"+host+SignedInPath) - if err != nil { - return nil, err - } - return &Auth{ - Auth: authenticator, - Sessions: NewSessions(nil), - Pending: NewPendingAuths(nil), - Operators: Operators{Path: env.Operators}, - Logf: logf, - }, nil -} - -// run is main's startup path, factored so a test can run it over a pipe instead of the real stdin/stdout -// (mirroring mesh-mail-notifier's own run). -func run(b *Bundle, in io.Reader, out io.Writer) error { - return stdio.ServeOn("", b.Tools(), in, out) -} +func main() { web.Main() } diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/acceptance_integration_test.go b/modules/mesh-mail-web/internal/web/acceptance_integration_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/acceptance_integration_test.go rename to modules/mesh-mail-web/internal/web/acceptance_integration_test.go index 4d48c17e..d3923b15 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/acceptance_integration_test.go +++ b/modules/mesh-mail-web/internal/web/acceptance_integration_test.go @@ -1,6 +1,6 @@ //go:build integration -package main +package web // Design 59 milestone 1's acceptance tests, 1 to 4 (novox/hq ADR 0440; issue 1268), on the harness issue // 1285 built: mesh-mail-web started whole, a stand-in identity provider (the same httptest issuer as diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/accounts.go b/modules/mesh-mail-web/internal/web/accounts.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/accounts.go rename to modules/mesh-mail-web/internal/web/accounts.go index 89e4fcc6..a92baf78 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/accounts.go +++ b/modules/mesh-mail-web/internal/web/accounts.go @@ -1,4 +1,4 @@ -package main +package web // The accounts and folder pages (novox/hq ADR 0440 §4, issue 1264): the accounts page lists every account // mail-store.accounts answers, each with its state and its folders' mail and unread counts; the folder page diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/accounts_test.go b/modules/mesh-mail-web/internal/web/accounts_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/accounts_test.go rename to modules/mesh-mail-web/internal/web/accounts_test.go index 5814333a..3e59b511 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/accounts_test.go +++ b/modules/mesh-mail-web/internal/web/accounts_test.go @@ -1,4 +1,4 @@ -package main +package web // The accounts and folder pages (novox/hq ADR 0440 §4, issue 1264). diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/auth.go b/modules/mesh-mail-web/internal/web/auth.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/auth.go rename to modules/mesh-mail-web/internal/web/auth.go index 4a43544a..c1396334 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/auth.go +++ b/modules/mesh-mail-web/internal/web/auth.go @@ -1,4 +1,4 @@ -package main +package web // Keycloak sign-in (novox/hq ADR 0440 §3, issue 1263): the authorisation-code flow with state and PKCE against // the bound issuer, the callback at /signed-in, the ID token verified (issuer, audience, expiry, signature from diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/auth_test.go b/modules/mesh-mail-web/internal/web/auth_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/auth_test.go rename to modules/mesh-mail-web/internal/web/auth_test.go index 2112e601..2e87b7ff 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/auth_test.go +++ b/modules/mesh-mail-web/internal/web/auth_test.go @@ -1,4 +1,4 @@ -package main +package web // Keycloak sign-in (novox/hq ADR 0440 §3, issue 1263): an httptest stand-in issuer, as the lead's prepare // findings said to check this against, minting its own ID tokens (RS256) so a test controls exactly what diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/config.go b/modules/mesh-mail-web/internal/web/config.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/config.go rename to modules/mesh-mail-web/internal/web/config.go index 8d419e81..13c90094 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/config.go +++ b/modules/mesh-mail-web/internal/web/config.go @@ -1,4 +1,4 @@ -package main +package web // The trusted setting `operators` (novox/hq ADR 0440 §3, settlement 5 of design 59 milestone 1's prepare step): // the Keycloak identities (the ID token's preferred_username) let in. Nobody by default; a setting that cannot diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/config_test.go b/modules/mesh-mail-web/internal/web/config_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/config_test.go rename to modules/mesh-mail-web/internal/web/config_test.go index 415a7df2..032625dd 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/config_test.go +++ b/modules/mesh-mail-web/internal/web/config_test.go @@ -1,4 +1,4 @@ -package main +package web import ( "os" diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/csrf.go b/modules/mesh-mail-web/internal/web/csrf.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/csrf.go rename to modules/mesh-mail-web/internal/web/csrf.go index 661541f2..f14e66d4 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/csrf.go +++ b/modules/mesh-mail-web/internal/web/csrf.go @@ -1,4 +1,4 @@ -package main +package web // The form token bound to the session (novox/hq ADR 0440 §3's last clause, issue 1284): every form that // changes something — today mail_send, once issue 1267 builds the write page — carries a hidden field with diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/csrf_test.go b/modules/mesh-mail-web/internal/web/csrf_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/csrf_test.go rename to modules/mesh-mail-web/internal/web/csrf_test.go index b2f62166..f365fcf4 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/csrf_test.go +++ b/modules/mesh-mail-web/internal/web/csrf_test.go @@ -1,4 +1,4 @@ -package main +package web // The form token bound to the session (novox/hq ADR 0440 §3, issue 1284). There is no write page to test // against yet (issue 1267 builds it); these tests stand in for it with a minimal changing handler ("send"), diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/mail.go b/modules/mesh-mail-web/internal/web/mail.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/mail.go rename to modules/mesh-mail-web/internal/web/mail.go index 62844e59..461662eb 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/mail.go +++ b/modules/mesh-mail-web/internal/web/mail.go @@ -1,4 +1,4 @@ -package main +package web // The mail page and attachment download (novox/hq ADR 0440 §5, issue 1266): mail_read already turns a mail's // HTML part into text (mesh-mail's own mime handling); this page renders that text through html/template diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/mail_test.go b/modules/mesh-mail-web/internal/web/mail_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/mail_test.go rename to modules/mesh-mail-web/internal/web/mail_test.go index b6a5f6c9..fdd43350 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/mail_test.go +++ b/modules/mesh-mail-web/internal/web/mail_test.go @@ -1,4 +1,4 @@ -package main +package web // The mail page and attachment download (novox/hq ADR 0440 §5, issue 1266). diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/mailreading_integration_test.go b/modules/mesh-mail-web/internal/web/mailreading_integration_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/mailreading_integration_test.go rename to modules/mesh-mail-web/internal/web/mailreading_integration_test.go index d09fea28..ef759a79 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/mailreading_integration_test.go +++ b/modules/mesh-mail-web/internal/web/mailreading_integration_test.go @@ -1,6 +1,6 @@ //go:build integration -package main +package web // Design 59 test 16's page half (novox/hq ADR 0459 §12, §13; issue 1524, split from 1431): mesh-mail-web // started whole, its stand-in mail-reading.status answering unhealthy with a cause, shows that cause above diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/mailreading_test.go b/modules/mesh-mail-web/internal/web/mailreading_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/mailreading_test.go rename to modules/mesh-mail-web/internal/web/mailreading_test.go index d16655f4..4ddd0a68 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/mailreading_test.go +++ b/modules/mesh-mail-web/internal/web/mailreading_test.go @@ -1,4 +1,4 @@ -package main +package web // The mail-reading section of the accounts page (novox/hq ADR 0459 §7, §12, §13; issue 1430). diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/mailwrite.go b/modules/mesh-mail-web/internal/web/mailwrite.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/mailwrite.go rename to modules/mesh-mail-web/internal/web/mailwrite.go index ae176bce..5eddb78d 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/mailwrite.go +++ b/modules/mesh-mail-web/internal/web/mailwrite.go @@ -1,4 +1,4 @@ -package main +package web // Corrections and undo on the mail page (novox/hq ADR 0459 §8, §13; issue 1523): each POST is called only // once RequireFormToken (issue 1284, csrf.go) has already let it through — a POST with no token, or diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/mailwrite_integration_test.go b/modules/mesh-mail-web/internal/web/mailwrite_integration_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/mailwrite_integration_test.go rename to modules/mesh-mail-web/internal/web/mailwrite_integration_test.go index 7ac2eec1..4176955a 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/mailwrite_integration_test.go +++ b/modules/mesh-mail-web/internal/web/mailwrite_integration_test.go @@ -1,6 +1,6 @@ //go:build integration -package main +package web // 1523/AC4: mesh-mail-web started whole (the harness issue 1285 built, its stand-in seat extended to // mail-reading by runtime_integration_test.go's standInMailReading): a correction posted on the page diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/mailwrite_test.go b/modules/mesh-mail-web/internal/web/mailwrite_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/mailwrite_test.go rename to modules/mesh-mail-web/internal/web/mailwrite_test.go index 9d4bba3b..16918ed7 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/mailwrite_test.go +++ b/modules/mesh-mail-web/internal/web/mailwrite_test.go @@ -1,4 +1,4 @@ -package main +package web // Corrections and undo on the mail page (novox/hq ADR 0459 §8, §13; issue 1523). diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/manifest_test.go b/modules/mesh-mail-web/internal/web/manifest_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/manifest_test.go rename to modules/mesh-mail-web/internal/web/manifest_test.go index ca7a006e..df61b446 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/manifest_test.go +++ b/modules/mesh-mail-web/internal/web/manifest_test.go @@ -1,4 +1,4 @@ -package main +package web // The manifest says what the code does (novox/hq ADR 0440 §1, §2, §3, §8; issue 1262). // diff --git a/modules/mesh-mail-web/internal/web/run.go b/modules/mesh-mail-web/internal/web/run.go new file mode 100644 index 00000000..445ed110 --- /dev/null +++ b/modules/mesh-mail-web/internal/web/run.go @@ -0,0 +1,127 @@ +// Package web is mesh-mail-web, the mail web client (novox/hq ADR 0440), built from mesh-mail's source as mesh-mail-notifier is +// (novox/hq ADR 0198 §1, ADR 0175 §1). A Go bundle the node's runtime launches, its own account (runs-as): it +// claims mail-webclient (open, status), reaches mail-store's seat verbs (accounts, search, read, send, +// send-status), and — once issue 1283 lands — answers its own HTTP endpoint `web`. No database, no state, no +// writable directory beyond the one the runtime account needs for its own settings. What this module says, it +// says on stderr; stdout is the MCP channel. It lives in a package of its own (issue 1859) so the local run, +// cmd/mail-page-dev, serves these same page handlers; cmd/mesh-mail-web is the module's own program. +package web + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "os" + "sync" + "time" + + stdio "git.novox.be/novox/mesh-sdk/go" +) + +func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) } + +// The HTTP endpoint's own state (novox/hq ADR 0440 §8; review round 1 of issue 1283, finding 4): before this +// fix, a failure setting up sign-in (the issuer never answering) or starting to listen (its port already +// taken) was only logged — mail-webclient.status went on answering serves: true, and mail_web_health healthy, +// regardless, since neither asked serveHTTP anything. Kept on the Bundle instead, so both tools say the one +// truth. +const ( + HTTPSettingUpSignIn = "setting up sign-in" + HTTPListening = "listening" + HTTPEnded = "ended" +) + +// HTTPStatus is serveHTTP's own phase and (once it has one) its error, read by statusTool and healthTool. +type HTTPStatus struct { + mu sync.Mutex + phase string + err error +} + +// NewHTTPStatus starts HTTPSettingUpSignIn: serveHTTP's own first phase, before issuer discovery has even been +// tried once. +func NewHTTPStatus() *HTTPStatus { return &HTTPStatus{phase: HTTPSettingUpSignIn} } + +func (h *HTTPStatus) set(phase string, err error) { + h.mu.Lock() + defer h.mu.Unlock() + h.phase = phase + h.err = err +} + +// Get answers the endpoint's own phase, and its error once it has one. +func (h *HTTPStatus) Get() (phase string, err error) { + h.mu.Lock() + defer h.mu.Unlock() + return h.phase, h.err +} + +// Main is the module's own program (cmd/mesh-mail-web): the seat verbs on the MCP channel and the HTTP +// endpoint beside them. +func Main() { + b := &Bundle{Env: envFromOS(), HTTP: NewHTTPStatus()} + go serveHTTP(b) + if err := run(b, os.Stdin, os.Stdout); err != nil { + logf("%v", err) + os.Exit(1) + } +} + +// serveHTTP starts the HTTP endpoint `web` (issue 1283), on the port the mesh gave it; a listen it cannot +// start is logged, never fatal to the seat verbs stdio.Serve answers on the MCP channel — but it is also kept +// on b.HTTP (finding 4), so mail-webclient.status and mail_web_health say it rather than going on claiming to +// serve. Sign-in (issue 1263) is set up first, retried on a failure (the issuer may not answer yet): never +// served with no session guard at all, which a nil Auth would be (server.go's own fallback, used by its unit +// tests alone). +func serveHTTP(b *Bundle) { + if b.Env.Listen == "" { + logf("[mesh-mail-web] started without MESH_MAIL_WEB_LISTEN; its HTTP endpoint is not served") + b.HTTP.set(HTTPEnded, errors.New("started without its own listen setting (MESH_MAIL_WEB_LISTEN)")) + return + } + host := InternalHost(b.Env.Node) + var auth *Auth + for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) { + a, err := buildAuth(host, b.Env) + if err == nil { + auth = a + break + } + logf("[mesh-mail-web] sign-in cannot be set up yet (%v); trying again in %s", err, wait) + b.HTTP.set(HTTPSettingUpSignIn, err) + time.Sleep(wait) + } + s := NewServer(host, auth, b) + b.HTTP.set(HTTPListening, nil) + if err := http.ListenAndServe(":"+b.Env.Listen, s); err != nil { + logf("[mesh-mail-web] its HTTP endpoint ended: %v", err) + b.HTTP.set(HTTPEnded, err) + } +} + +// buildAuth reads the oidc-client binding (issue 1262's oidc.json) and discovers the issuer (issue 1263). +func buildAuth(host string, env Env) (*Auth, error) { + cfg, err := ReadOIDCConfig(env.OIDC) + if err != nil { + return nil, err + } + authenticator, err := NewAuthenticator(context.Background(), cfg, "https://"+host+SignedInPath) + if err != nil { + return nil, err + } + return &Auth{ + Auth: authenticator, + Sessions: NewSessions(nil), + Pending: NewPendingAuths(nil), + Operators: Operators{Path: env.Operators}, + Logf: logf, + }, nil +} + +// run is main's startup path, factored so a test can run it over a pipe instead of the real stdin/stdout +// (mirroring mesh-mail-notifier's own run). +func run(b *Bundle, in io.Reader, out io.Writer) error { + return stdio.ServeOn("", b.Tools(), in, out) +} diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/runtime_integration_test.go b/modules/mesh-mail-web/internal/web/runtime_integration_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/runtime_integration_test.go rename to modules/mesh-mail-web/internal/web/runtime_integration_test.go index a6e45b05..fd77ebb9 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/runtime_integration_test.go +++ b/modules/mesh-mail-web/internal/web/runtime_integration_test.go @@ -1,6 +1,6 @@ //go:build integration -package main +package web // The runtime player (novox/hq ADR 0192, ADR 0198; issue 1285): mesh-mail-web is a bundle under runs-as, and // reaches the bus only through the runtime that launches it, over stdin and stdout (initialize, tools/list, @@ -282,7 +282,7 @@ func buildMailWebBinary(t *testing.T) string { } t.Cleanup(func() { _ = os.RemoveAll(dir) }) bin := filepath.Join(dir, "mesh-mail-web") - cmd := exec.Command("go", "build", "-o", bin, ".") + cmd := exec.Command("go", "build", "-o", bin, "../../cmd/mesh-mail-web") cmd.Dir, err = os.Getwd() if err != nil { t.Fatal(err) diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/search.go b/modules/mesh-mail-web/internal/web/search.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/search.go rename to modules/mesh-mail-web/internal/web/search.go index f800faf8..e717422d 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/search.go +++ b/modules/mesh-mail-web/internal/web/search.go @@ -1,4 +1,4 @@ -package main +package web // The search page (novox/hq ADR 0440 §4, issue 1265): the form's own field names are exactly mail-store.search's // own argument names (mesh-mail tools.go searchArgs, issue 1232), so a filled field maps straight onto the diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/search_test.go b/modules/mesh-mail-web/internal/web/search_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/search_test.go rename to modules/mesh-mail-web/internal/web/search_test.go index 33aa1a36..aa511733 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/search_test.go +++ b/modules/mesh-mail-web/internal/web/search_test.go @@ -1,4 +1,4 @@ -package main +package web // The search page (novox/hq ADR 0440 §4, issue 1265). diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/server.go b/modules/mesh-mail-web/internal/web/server.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/server.go rename to modules/mesh-mail-web/internal/web/server.go index 54e16968..7219b848 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/server.go +++ b/modules/mesh-mail-web/internal/web/server.go @@ -1,4 +1,4 @@ -package main +package web // The web server (novox/hq ADR 0440 §2, §5; issue 1283): answers only its internal name mail..internal // (404 for any other Host — a public name, an IP, or none), sets a content security policy allowing only its diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/server_test.go b/modules/mesh-mail-web/internal/web/server_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/server_test.go rename to modules/mesh-mail-web/internal/web/server_test.go index c7804eaf..b39c28d7 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/server_test.go +++ b/modules/mesh-mail-web/internal/web/server_test.go @@ -1,4 +1,4 @@ -package main +package web import ( "net/http" diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/sessions.go b/modules/mesh-mail-web/internal/web/sessions.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/sessions.go rename to modules/mesh-mail-web/internal/web/sessions.go index 26579612..85549273 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/sessions.go +++ b/modules/mesh-mail-web/internal/web/sessions.go @@ -1,4 +1,4 @@ -package main +package web // Sessions and the pending authorisations in flight (novox/hq ADR 0440 §3, issue 1263): both kept in memory // only, never on disk — a restart signs everyone out, accepted (design 59 milestone 1's prepare step). diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/tools.go b/modules/mesh-mail-web/internal/web/tools.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/tools.go rename to modules/mesh-mail-web/internal/web/tools.go index 91bf6d40..c7c50d4f 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/tools.go +++ b/modules/mesh-mail-web/internal/web/tools.go @@ -1,4 +1,4 @@ -package main +package web // The module's seat verbs (novox/hq ADR 0440 §8): mail-webclient.status (whether it serves, and which accounts // it is granted) and mail-webclient.open (issue 1283: the page's address). mail_web_health is its own health diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/tools_test.go b/modules/mesh-mail-web/internal/web/tools_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/tools_test.go rename to modules/mesh-mail-web/internal/web/tools_test.go index 3b16a841..a676ae82 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/tools_test.go +++ b/modules/mesh-mail-web/internal/web/tools_test.go @@ -1,4 +1,4 @@ -package main +package web import ( "context" diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/write.go b/modules/mesh-mail-web/internal/web/write.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/write.go rename to modules/mesh-mail-web/internal/web/write.go index cfae978f..d91b8570 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/write.go +++ b/modules/mesh-mail-web/internal/web/write.go @@ -1,4 +1,4 @@ -package main +package web // The write page and a send's state (novox/hq ADR 0440 §6, issue 1267): the form offers only the accounts // mail-store.accounts answers; its POST (behind issue 1284's own per-session token, wired in here as this diff --git a/modules/mesh-mail-web/cmd/mesh-mail-web/write_test.go b/modules/mesh-mail-web/internal/web/write_test.go similarity index 99% rename from modules/mesh-mail-web/cmd/mesh-mail-web/write_test.go rename to modules/mesh-mail-web/internal/web/write_test.go index b3dcc7ba..6ca6cb7d 100644 --- a/modules/mesh-mail-web/cmd/mesh-mail-web/write_test.go +++ b/modules/mesh-mail-web/internal/web/write_test.go @@ -1,4 +1,4 @@ -package main +package web // The write page and a send's state (novox/hq ADR 0440 §6, issue 1267), and (beside 1267's own) issue 1284's // form token wired into this real write form and its real POST.