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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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() }
|
||||
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package web
|
||||
|
||||
// The accounts and folder pages (novox/hq ADR 0440 §4, issue 1264).
|
||||
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package web
|
||||
|
||||
import (
|
||||
"os"
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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"),
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package web
|
||||
|
||||
// The mail page and attachment download (novox/hq ADR 0440 §5, issue 1266).
|
||||
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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).
|
||||
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package web
|
||||
|
||||
// Corrections and undo on the mail page (novox/hq ADR 0459 §8, §13; issue 1523).
|
||||
|
||||
+1
-1
@@ -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).
|
||||
//
|
||||
@@ -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)
|
||||
}
|
||||
+2
-2
@@ -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)
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package web
|
||||
|
||||
// The search page (novox/hq ADR 0440 §4, issue 1265).
|
||||
|
||||
+1
-1
@@ -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.<node>.internal
|
||||
// (404 for any other Host — a public name, an IP, or none), sets a content security policy allowing only its
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
+1
-1
@@ -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).
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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.
|
||||
Reference in New Issue
Block a user