windows-debug: elevated GUI installer with Verae key pages (sign in / create account + email code / paste / skip), hidden scripts, per-step finish page, user-scoped key save + helper scheduled task; installer registration API spec

This commit is contained in:
Grok Bot (for George) 2026-10-03 19:14:49 -04:00
commit 6e76bc7b0e
3 changed files with 1857 additions and 61 deletions

143
internal/auth/portal.go Normal file
View file

@ -0,0 +1,143 @@
package auth
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
// Portal account API — the same JSON endpoints the customer.verae.ai/portal web
// client (portal/app.js) uses: POST /portal/api/login|signup → {token: session},
// POST /portal/api/tokens {name} (Bearer session) → {token: "xVerae-…"},
// POST /portal/api/logout. There is no device-code / email-code flow, so the
// installer uses email+password (+ optional authenticator code) and mints a key.
// PortalBase is overridable for testing via VERAE_PORTAL_BASE.
func PortalBase() string {
if v := strings.TrimRight(os.Getenv("VERAE_PORTAL_BASE"), "/"); v != "" {
return v
}
return "https://customer.verae.ai"
}
// ErrTOTPRequired means the account has 2FA and a 6-digit code is needed.
var ErrTOTPRequired = fmt.Errorf("totp_required")
var portalHTTP = &http.Client{Timeout: 20 * time.Second}
func decodeJSON(res *http.Response, v any) error {
return json.NewDecoder(io.LimitReader(res.Body, 1<<20)).Decode(v)
}
func portalPost(path, bearer string, body any) (map[string]any, int, error) {
if !strings.HasPrefix(PortalBase(), "https://") && os.Getenv("VERAE_ALLOW_INSECURE_PORTAL") != "1" {
return nil, 0, fmt.Errorf("refusing non-HTTPS portal base %q", PortalBase())
}
b, _ := json.Marshal(body)
req, err := http.NewRequest(http.MethodPost, PortalBase()+"/portal/api"+path, bytes.NewReader(b))
if err != nil {
return nil, 0, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "VeraePrintStamp-Installer")
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
res, err := portalHTTP.Do(req)
if err != nil {
return nil, 0, err
}
defer res.Body.Close()
raw, _ := io.ReadAll(io.LimitReader(res.Body, 1<<20))
out := map[string]any{}
_ = json.Unmarshal(raw, &out)
if res.StatusCode >= 300 {
msg, _ := out["error"].(string)
if msg == "totp_required" {
return out, res.StatusCode, ErrTOTPRequired
}
if msg == "" {
msg = strings.TrimSpace(string(raw))
if len(msg) > 200 {
msg = msg[:200]
}
}
return out, res.StatusCode, fmt.Errorf("HTTP %d: %s", res.StatusCode, msg)
}
return out, res.StatusCode, nil
}
// PortalMintKey signs in (or signs up), mints a long-lived xVerae- token named
// tokenName, and signs out. Credentials are never stored.
func PortalMintKey(signup bool, name, email, password, totp, tokenName string) (string, error) {
var body map[string]any
path := "/login"
if signup {
path = "/signup"
body = map[string]any{"name": name, "email": email, "password": password}
} else {
body = map[string]any{"email": email, "password": password}
if totp != "" {
body["totpCode"] = totp
}
}
out, _, err := portalPost(path, "", body)
if err != nil {
return "", err
}
session, _ := out["token"].(string)
if session == "" {
return "", fmt.Errorf("portal %s returned no session", path)
}
defer func() { _, _, _ = portalPost("/logout", session, map[string]any{}) }()
tk, _, err := portalPost("/tokens", session, map[string]any{"name": tokenName})
if err != nil {
return "", fmt.Errorf("mint key: %w", err)
}
key, _ := tk["token"].(string)
if !strings.HasPrefix(key, "xVerae-") && !strings.HasPrefix(key, "key-") {
return "", fmt.Errorf("mint key: unexpected response (no xVerae- token)")
}
return key, nil
}
// ValidateXVeraeKey checks a key against zap.verae.ai GET /v1/status (documented
// as free). Returns "valid", "invalid", "badformat", "unreachable" or "unknown".
func ValidateXVeraeKey(key string) (string, string) {
key = strings.TrimSpace(key)
if !(strings.HasPrefix(key, "xVerae-") || strings.HasPrefix(key, "key-")) || len(key) < 16 || strings.ContainsAny(key, " \t\r\n") {
return "badformat", "must start with xVerae- (or legacy key-)"
}
req, _ := http.NewRequest(http.MethodGet, "https://zap.verae.ai/v1/status", nil)
req.Header.Set("x-api-key", key)
res, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)
if err != nil {
return "unreachable", err.Error()
}
res.Body.Close()
switch {
case res.StatusCode < 300:
return "valid", fmt.Sprintf("HTTP %d", res.StatusCode)
case res.StatusCode == 401 || res.StatusCode == 403:
return "invalid", fmt.Sprintf("HTTP %d from Verae", res.StatusCode)
default:
return "unknown", fmt.Sprintf("HTTP %d from Verae", res.StatusCode)
}
}
// MaskKey renders xVerae-...abcd.
func MaskKey(k string) string {
k = strings.TrimSpace(k)
if len(k) <= 12 {
if len(k) > 4 {
return k[:4] + "..."
}
return "..."
}
return k[:7] + "..." + k[len(k)-4:]
}

View file

@ -0,0 +1,192 @@
package auth
import (
"errors"
"fmt"
"net/http"
"regexp"
"strings"
"unicode"
)
// Installer-friendly registration API (proposed; spec in
// docs/installer-registration-api.md). When the server does not implement it
// (GET /portal/api/install/capabilities → 404) the client falls back to the
// endpoints the portal web client uses today (/login, /signup, /tokens), which
// have no email-verification step.
// PasswordPolicy mirrors GET /portal/api/install/capabilities .passwordPolicy.
type PasswordPolicy struct {
MinLength int `json:"minLength"`
RequireLetter bool `json:"requireLetter"`
RequireDigit bool `json:"requireDigit"`
RequireSymbol bool `json:"requireSymbol"`
DisallowEmail bool `json:"disallowEmail"`
}
// DefaultPolicy is used when the server does not publish one (conservative).
var DefaultPolicy = PasswordPolicy{MinLength: 10, RequireLetter: true, RequireDigit: true, DisallowEmail: true}
// Capabilities describes what the server supports for installers.
type Capabilities struct {
InstallAPI bool
EmailVerification bool
Policy PasswordPolicy
}
// Describe renders the policy for UI text.
func (p PasswordPolicy) Describe() string {
parts := []string{fmt.Sprintf("at least %d characters", p.MinLength)}
if p.RequireLetter {
parts = append(parts, "a letter")
}
if p.RequireDigit {
parts = append(parts, "a number")
}
if p.RequireSymbol {
parts = append(parts, "a symbol")
}
if p.DisallowEmail {
parts = append(parts, "not containing your email")
}
return "Password: " + strings.Join(parts, ", ")
}
// Check validates a password client-side.
func (p PasswordPolicy) Check(pw, email string) error {
if len([]rune(pw)) < p.MinLength {
return fmt.Errorf("password must be at least %d characters", p.MinLength)
}
var l, d, s bool
for _, r := range pw {
switch {
case unicode.IsLetter(r):
l = true
case unicode.IsDigit(r):
d = true
case !unicode.IsSpace(r):
s = true
}
}
if p.RequireLetter && !l {
return errors.New("password must contain a letter")
}
if p.RequireDigit && !d {
return errors.New("password must contain a number")
}
if p.RequireSymbol && !s {
return errors.New("password must contain a symbol")
}
if p.DisallowEmail && email != "" {
local := strings.ToLower(strings.SplitN(email, "@", 2)[0])
if len(local) >= 3 && strings.Contains(strings.ToLower(pw), local) {
return errors.New("password must not contain your email name")
}
}
return nil
}
var emailRe = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`)
// ValidEmail is a light syntax check.
func ValidEmail(e string) bool { return emailRe.MatchString(strings.TrimSpace(e)) }
// MaskEmail renders g***@example.com for logs.
func MaskEmail(e string) string {
at := strings.LastIndex(e, "@")
if at < 1 {
return "***"
}
return e[:1] + "***" + e[at:]
}
// GetCapabilities probes GET /portal/api/install/capabilities.
func GetCapabilities() Capabilities {
c := Capabilities{Policy: DefaultPolicy}
req, _ := http.NewRequest(http.MethodGet, PortalBase()+"/portal/api/install/capabilities", nil)
req.Header.Set("User-Agent", "VeraePrintStamp-Installer")
res, err := portalHTTP.Do(req)
if err != nil {
return c
}
defer res.Body.Close()
if res.StatusCode != 200 || !strings.Contains(res.Header.Get("Content-Type"), "json") {
return c
}
var body struct {
InstallAPI bool `json:"installApi"`
EmailVerification bool `json:"emailVerification"`
PasswordPolicy *PasswordPolicy `json:"passwordPolicy"`
}
if err := decodeJSON(res, &body); err != nil {
return c
}
c.InstallAPI, c.EmailVerification = body.InstallAPI, body.EmailVerification
if body.PasswordPolicy != nil && body.PasswordPolicy.MinLength > 0 {
c.Policy = *body.PasswordPolicy
}
return c
}
// RegisterResult is returned by InstallRegister.
type RegisterResult struct {
PendingID string // non-empty → email code sent; call InstallVerify
Key string // non-empty → account created and key minted (fallback path)
}
// InstallRegister starts a new-account registration.
func InstallRegister(caps Capabilities, name, email, password, tokenName string) (RegisterResult, error) {
if !caps.InstallAPI {
// Fallback: current portal API creates the account immediately (no email code).
k, err := PortalMintKey(true, name, email, password, "", tokenName)
return RegisterResult{Key: k}, err
}
out, _, err := portalPost("/install/register", "", map[string]any{"name": name, "email": email, "password": password, "client": "VeraePrintStamp-Windows"})
if err != nil {
return RegisterResult{}, err
}
id, _ := out["pendingId"].(string)
if id == "" {
return RegisterResult{}, errors.New("register: server returned no pendingId")
}
return RegisterResult{PendingID: id}, nil
}
// InstallVerify submits the emailed code and returns the minted key.
func InstallVerify(pendingID, code, tokenName string) (string, error) {
out, _, err := portalPost("/install/verify", "", map[string]any{"pendingId": pendingID, "code": strings.TrimSpace(code), "tokenName": tokenName})
if err != nil {
return "", err
}
return keyFrom(out)
}
// InstallResend asks for a new code.
func InstallResend(pendingID string) error {
_, _, err := portalPost("/install/resend", "", map[string]any{"pendingId": pendingID})
return err
}
// InstallLogin signs in and mints a key (install API or fallback).
func InstallLogin(caps Capabilities, email, password, totp, tokenName string) (string, error) {
if !caps.InstallAPI {
return PortalMintKey(false, "", email, password, totp, tokenName)
}
body := map[string]any{"email": email, "password": password, "tokenName": tokenName}
if totp != "" {
body["totpCode"] = totp
}
out, _, err := portalPost("/install/login", "", body)
if err != nil {
return "", err
}
return keyFrom(out)
}
func keyFrom(out map[string]any) (string, error) {
k, _ := out["token"].(string)
if !strings.HasPrefix(k, "xVerae-") && !strings.HasPrefix(k, "key-") {
return "", errors.New("server returned no xVerae- token")
}
return k, nil
}

View file

@ -0,0 +1,116 @@
package auth
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
)
func fakePortal(t *testing.T, installAPI bool) *httptest.Server {
mux := http.NewServeMux()
js := func(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(v)
}
body := func(r *http.Request) map[string]any {
m := map[string]any{}
_ = json.NewDecoder(r.Body).Decode(&m)
return m
}
if installAPI {
mux.HandleFunc("/portal/api/install/capabilities", func(w http.ResponseWriter, r *http.Request) {
js(w, 200, map[string]any{"installApi": true, "emailVerification": true, "passwordPolicy": map[string]any{"minLength": 12, "requireLetter": true, "requireDigit": true}})
})
mux.HandleFunc("/portal/api/install/register", func(w http.ResponseWriter, r *http.Request) { js(w, 202, map[string]any{"pendingId": "p1"}) })
mux.HandleFunc("/portal/api/install/verify", func(w http.ResponseWriter, r *http.Request) {
if body(r)["code"] != "123456" {
js(w, 400, map[string]any{"error": "invalid_code"})
return
}
js(w, 201, map[string]any{"token": "xVerae-verified-abcdefgh"})
})
mux.HandleFunc("/portal/api/install/login", func(w http.ResponseWriter, r *http.Request) {
b := body(r)
if b["totpCode"] == nil {
js(w, 401, map[string]any{"error": "totp_required"})
return
}
js(w, 200, map[string]any{"token": "xVerae-login-abcdefgh"})
})
}
mux.HandleFunc("/portal/api/login", func(w http.ResponseWriter, r *http.Request) { js(w, 200, map[string]any{"token": "sess"}) })
mux.HandleFunc("/portal/api/signup", func(w http.ResponseWriter, r *http.Request) { js(w, 200, map[string]any{"token": "sess"}) })
mux.HandleFunc("/portal/api/tokens", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer sess" {
js(w, 401, map[string]any{"error": "no"})
return
}
js(w, 200, map[string]any{"token": "xVerae-minted-abcdefgh"})
})
mux.HandleFunc("/portal/api/logout", func(w http.ResponseWriter, r *http.Request) { js(w, 200, map[string]any{}) })
s := httptest.NewServer(mux)
t.Cleanup(s.Close)
t.Setenv("VERAE_PORTAL_BASE", s.URL)
t.Setenv("VERAE_ALLOW_INSECURE_PORTAL", "1")
return s
}
func TestPortalFallback(t *testing.T) {
fakePortal(t, false)
caps := GetCapabilities()
if caps.InstallAPI || caps.Policy.MinLength != DefaultPolicy.MinLength {
t.Fatalf("caps %+v", caps)
}
k, err := InstallLogin(caps, "a@b.co", "pw", "", "n")
if err != nil || k != "xVerae-minted-abcdefgh" {
t.Fatal(k, err)
}
rr, err := InstallRegister(caps, "N", "a@b.co", "pw", "n")
if err != nil || rr.Key == "" || rr.PendingID != "" {
t.Fatal(rr, err)
}
}
func TestPortalInstallAPI(t *testing.T) {
fakePortal(t, true)
caps := GetCapabilities()
if !caps.InstallAPI || caps.Policy.MinLength != 12 {
t.Fatalf("caps %+v", caps)
}
if _, err := InstallLogin(caps, "a@b.co", "pw", "", "n"); !errors.Is(err, ErrTOTPRequired) {
t.Fatal("want totp_required", err)
}
if k, err := InstallLogin(caps, "a@b.co", "pw", "123456", "n"); err != nil || k != "xVerae-login-abcdefgh" {
t.Fatal(k, err)
}
rr, err := InstallRegister(caps, "N", "a@b.co", "pw", "n")
if err != nil || rr.PendingID != "p1" {
t.Fatal(rr, err)
}
if _, err := InstallVerify("p1", "000000", "n"); err == nil {
t.Fatal("bad code accepted")
}
if k, err := InstallVerify("p1", "123456", "n"); err != nil || k != "xVerae-verified-abcdefgh" {
t.Fatal(k, err)
}
}
func TestPolicyAndHTTPS(t *testing.T) {
p := DefaultPolicy
for pw, ok := range map[string]bool{"short1": false, "longenoughnodigit": false, "longenough123": true, "george12345x": false} {
if (p.Check(pw, "george@x.com") == nil) != ok {
t.Errorf("policy %q want ok=%v", pw, ok)
}
}
t.Setenv("VERAE_PORTAL_BASE", "http://example.com")
t.Setenv("VERAE_ALLOW_INSECURE_PORTAL", "")
if _, _, err := portalPost("/login", "", map[string]any{}); err == nil {
t.Fatal("non-https portal allowed")
}
if MaskKey("xVerae-abcdefghijklmnop") != "xVerae-...mnop" || MaskEmail("george@x.com") != "g***@x.com" {
t.Fatal("masking")
}
}