feat(webgw): per-user wallet sessions + invite register
Add the gateway backend for the wallet onboarding flow so each browser session carries its OWN bus identity instead of sharing the single operator client. - POST /api/session (session.go): the browser hands its full wallet keypair (unlocked from the local encrypted key, over TLS) and the gateway spins up a dedicated bus client that acts AS that user. The private key lives only in process memory for the life of the session and is dropped on logout/shutdown. identityFromHex enforces the exact key sizes (sign_pub 32, sign_priv 64, kex_pub 32, kex_priv 32) that match cs.Identity on the Go side. - POST /api/register (register.go): unauthenticated onboarding gated by a one-shot invite token. Validates the two PUBLIC key halves, then either consumes a configured --mock-tokens invite (local testing) or proxies to the bus POST /register (--register-url, bus >= 0.12.0). The handle/role come from the invite, never from the client. - server.go: sessions move from a token->time map to a sessionStore of per-user *session records; auth() now resolves the session and passes its gateway to each handler. The legacy operator passphrase login (POST /api/login) is kept, bound to the shared operator gateway. - main.go: build a busTemplate config that wallet sessions clone with their own Identity; wire --register-url / --mock-tokens. - webgw_test.go: identity-size validation, hex-key validation, mock token parsing, and single-use register (201 then 409) using a fixed browser-derived wallet vector. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+24
-5
@@ -50,8 +50,10 @@ func main() {
|
||||
caPath = flag.String("ca", "", "bus CA cert path; set to talk TLS+nkey to a secured bus (empty = plaintext dev)")
|
||||
identityFile = flag.String("identity-file", "", "path to the operator identity JSON file (0600). Mutually exclusive with --identity-pass")
|
||||
identityPass = flag.String("identity-pass", "", "pass(1) entry holding the operator identity JSON, e.g. unibus/operator-identity")
|
||||
unlockPass = flag.String("unlock-pass", "", "literal passphrase the browser must send to unlock a session (dev). Prefer --unlock-pass-entry")
|
||||
unlockEntry = flag.String("unlock-pass-entry", "unibus/admin-panel-password", "pass(1) entry holding the unlock passphrase (used when --unlock-pass is empty)")
|
||||
unlockPass = flag.String("unlock-pass", "", "literal passphrase the browser must send to unlock a LEGACY operator session (dev). Prefer --unlock-pass-entry")
|
||||
unlockEntry = flag.String("unlock-pass-entry", "unibus/admin-panel-password", "pass(1) entry holding the operator unlock passphrase (used when --unlock-pass is empty)")
|
||||
registerURL = flag.String("register-url", "", "bus POST /register URL for wallet onboarding. Empty = derive from --ctrl-url (<ctrl-url>/register)")
|
||||
mockTokens = flag.String("mock-tokens", "", "DEV ONLY: comma-separated one-shot invite tokens for local testing, 'token=handle:role'. Empty in production (real invites come from the bus). Example: demo=demo:member")
|
||||
webDir = flag.String("web-dir", "", "OPTIONAL path to the built SPA (web/dist) to serve. Empty = API only (use vite dev server)")
|
||||
)
|
||||
flag.Parse()
|
||||
@@ -77,19 +79,34 @@ func main() {
|
||||
|
||||
resolvedWebDir := resolveWebDir(*webDir)
|
||||
|
||||
gw, err := newGateway(gatewayConfig{
|
||||
// busTemplate is the connection config every bus client uses. The operator
|
||||
// gateway uses it as-is; each wallet session clones it and overrides Identity
|
||||
// with the logged-in user's keypair.
|
||||
busTemplate := gatewayConfig{
|
||||
Identity: id,
|
||||
NatsURL: *natsURL,
|
||||
CtrlURL: *ctrlURL,
|
||||
CtrlURLs: splitCSV(*ctrlURLs),
|
||||
NatsURLs: splitCSV(*natsURLs),
|
||||
CAPath: *caPath,
|
||||
})
|
||||
}
|
||||
|
||||
gw, err := newGateway(busTemplate)
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
defer gw.Close()
|
||||
|
||||
// Wallet onboarding backend: POST /api/register targets the bus's /register
|
||||
// (added by the user-accounts work). When --register-url is empty we derive it
|
||||
// from --ctrl-url; --mock-tokens supplies one-shot invites for local testing
|
||||
// before that endpoint is deployed.
|
||||
regURL := *registerURL
|
||||
if regURL == "" {
|
||||
regURL = strings.TrimRight(*ctrlURL, "/") + "/register"
|
||||
}
|
||||
registrar := newRegistrar(regURL, *mockTokens)
|
||||
|
||||
log.Printf("operator endpoint: %s", gw.endpoint)
|
||||
log.Printf("control plane: %s (+%d failover)", *ctrlURL, len(splitCSV(*ctrlURLs)))
|
||||
tls := "OFF (plaintext dev)"
|
||||
@@ -103,7 +120,9 @@ func main() {
|
||||
log.Printf("API only (no --web-dir): use the vite dev server with a /api+stream proxy")
|
||||
}
|
||||
|
||||
srv := newServer(gw, unlock, resolvedWebDir)
|
||||
log.Printf("wallet register: %s (mock tokens: %d)", regURL, mockTokenCount(*mockTokens))
|
||||
|
||||
srv := newServer(gw, busTemplate, registrar, unlock, resolvedWebDir)
|
||||
addr := *bind + ":" + *port
|
||||
httpSrv := &http.Server{
|
||||
Addr: addr,
|
||||
|
||||
Reference in New Issue
Block a user