Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/recomma/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type AppConfig struct {
PublicOrigin string
LogLevel string
LogFormatJSON bool
Debug bool
HyperliquidIOCInitialOffsetBps float64
OrderScalerMaxMultiplier float64
}
Expand All @@ -34,6 +35,7 @@ func DefaultConfig() AppConfig {
HTTPListen: ":8080",
LogLevel: "info",
LogFormatJSON: false,
Debug: false,
HyperliquidIOCInitialOffsetBps: 0,
OrderScalerMaxMultiplier: 5,
}
Expand All @@ -52,6 +54,7 @@ func NewConfigFlagSet(cfg *AppConfig) *pflag.FlagSet {
fs.StringVar(&cfg.PublicOrigin, "public-origin", cfg.PublicOrigin, "Public origin served to clients (env: RECOMMA_PUBLIC_ORIGIN)")
fs.StringVar(&cfg.LogLevel, "log-level", cfg.LogLevel, "Log level (env: RECOMMA_LOG_LEVEL)")
fs.BoolVar(&cfg.LogFormatJSON, "log-json", cfg.LogFormatJSON, "Emit logs as JSON (env: RECOMMA_LOG_JSON)")
registerDebugFlag(fs, cfg)
fs.Float64Var(&cfg.HyperliquidIOCInitialOffsetBps, "hyperliquid-ioc-offset-bps", cfg.HyperliquidIOCInitialOffsetBps, "Basis points to widen the first IOC price check (env: RECOMMA_HYPERLIQUID_IOC_OFFSET_BPS)")
fs.Float64Var(&cfg.OrderScalerMaxMultiplier, "order-scaler-max-multiplier", cfg.OrderScalerMaxMultiplier, "Maximum allowed order scaler multiplier (env: RECOMMA_ORDER_SCALER_MAX_MULTIPLIER)")

Expand Down Expand Up @@ -123,6 +126,10 @@ func ApplyEnvDefaults(fs *pflag.FlagSet, cfg *AppConfig) error {
setFloat("hyperliquid-ioc-offset-bps", "RECOMMA_HYPERLIQUID_IOC_OFFSET_BPS", &cfg.HyperliquidIOCInitialOffsetBps)
setFloat("order-scaler-max-multiplier", "RECOMMA_ORDER_SCALER_MAX_MULTIPLIER", &cfg.OrderScalerMaxMultiplier)

if err := applyDebugEnvDefaults(flagSet, cfg); err != nil {
return err
}

return nil
}

Expand Down
70 changes: 70 additions & 0 deletions cmd/recomma/internal/config/debug_disabled.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//go:build !debugmode

package config

import (
"fmt"
"os"
"strconv"

"github.com/spf13/pflag"
)

type disabledDebugValue struct {
underlying pflag.Value
target *bool
}

func (v *disabledDebugValue) Set(s string) error {
if s == "" {
s = "true"
}
if err := v.underlying.Set(s); err != nil {
return err
}
parsed, err := strconv.ParseBool(v.underlying.String())
if err != nil {
return err
}
if parsed {
*v.target = false
return fmt.Errorf("debug mode requires building with -tags debugmode")
}
*v.target = false
return nil
}

func (v *disabledDebugValue) String() string {
return v.underlying.String()
}

func (v *disabledDebugValue) Type() string {
return "bool"
}

func registerDebugFlag(fs *pflag.FlagSet, cfg *AppConfig) {
fs.BoolVar(&cfg.Debug, "debug", false, "Debug mode requires building with -tags debugmode")
flag := fs.Lookup("debug")
underlying := flag.Value
flag.Value = &disabledDebugValue{underlying: underlying, target: &cfg.Debug}
flag.Hidden = true
flag.NoOptDefVal = "true"
cfg.Debug = false
}

func applyDebugEnvDefaults(flagSet map[string]struct{}, cfg *AppConfig) error {
cfg.Debug = false
if _, ok := flagSet["debug"]; ok {
return nil
}
if v, ok := os.LookupEnv("RECOMMA_DEBUG"); ok && v != "" {
parsed, err := strconv.ParseBool(v)
if err != nil {
return err
}
if parsed {
return fmt.Errorf("RECOMMA_DEBUG=true requires building with -tags debugmode")
}
}
return nil
}
26 changes: 26 additions & 0 deletions cmd/recomma/internal/config/debug_enabled.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//go:build debugmode

package config

import (
"os"
"strconv"

"github.com/spf13/pflag"
)

func registerDebugFlag(fs *pflag.FlagSet, cfg *AppConfig) {
fs.BoolVar(&cfg.Debug, "debug", cfg.Debug, "Enable debug mode (env: RECOMMA_DEBUG)")
}

func applyDebugEnvDefaults(flagSet map[string]struct{}, cfg *AppConfig) error {
if _, ok := flagSet["debug"]; ok {
return nil
}
if v, ok := os.LookupEnv("RECOMMA_DEBUG"); ok {
if parsed, err := strconv.ParseBool(v); err == nil {
cfg.Debug = parsed
}
}
return nil
}
58 changes: 44 additions & 14 deletions cmd/recomma/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/recomma/recomma/hl"
"github.com/recomma/recomma/hl/ws"
"github.com/recomma/recomma/internal/api"
"github.com/recomma/recomma/internal/debugmode"
"github.com/recomma/recomma/internal/origin"
"github.com/recomma/recomma/internal/vault"
rlog "github.com/recomma/recomma/log"
Expand Down Expand Up @@ -55,6 +56,12 @@ func main() {
fatal("invalid configuration", err)
}

if cfg.Debug && !debugmode.Available() {
fatal("debug mode unavailable", debugmode.ErrUnavailable)
}

debugEnabled := cfg.Debug && debugmode.Available()

allowedOrigins := origin.BuildAllowedOrigins(cfg.HTTPListen, cfg.PublicOrigin)
rpID := origin.DeriveRPID(cfg.HTTPListen, cfg.PublicOrigin)

Expand All @@ -67,6 +74,8 @@ func main() {
slog.SetDefault(logger)
log.SetOutput(slog.NewLogLogger(logger.Handler(), slog.LevelDebug).Writer())

webui.SetDebug(debugEnabled)

appCtx = rlog.ContextWithLogger(appCtx, logger)

streamController := api.NewStreamController(api.WithStreamLogger(logger))
Expand All @@ -89,21 +98,38 @@ func main() {
initialVaultState := vault.StateSetupRequired
var controllerOpts []vault.ControllerOption

existingUser, err := store.GetVaultUser(appCtx)
if err != nil {
fatal("load vault user", err)
}
if existingUser != nil {
controllerOpts = append(controllerOpts, vault.WithInitialUser(existingUser))

payload, err := store.GetVaultPayloadForUser(appCtx, existingUser.ID)
if debugEnabled {
secrets, err := debugmode.LoadSecretsFromEnv()
if err != nil {
fatal("load vault payload", err)
fatal("load debug secrets", err)
}
if payload != nil {
initialVaultState = vault.StateSealed
sealedAt := payload.UpdatedAt
controllerOpts = append(controllerOpts, vault.WithInitialTimestamps(&sealedAt, nil, nil))
now := secrets.ReceivedAt
if now.IsZero() {
now = time.Now().UTC()
}
controllerOpts = append(controllerOpts,
vault.WithInitialSecrets(secrets),
vault.WithInitialUser(debugmode.DebugUser(now)),
vault.WithInitialTimestamps(nil, &now, nil),
)
initialVaultState = vault.StateUnsealed
} else {
existingUser, err := store.GetVaultUser(appCtx)
if err != nil {
fatal("load vault user", err)
}
if existingUser != nil {
controllerOpts = append(controllerOpts, vault.WithInitialUser(existingUser))

payload, err := store.GetVaultPayloadForUser(appCtx, existingUser.ID)
if err != nil {
fatal("load vault payload", err)
}
if payload != nil {
initialVaultState = vault.StateSealed
sealedAt := payload.UpdatedAt
controllerOpts = append(controllerOpts, vault.WithInitialTimestamps(&sealedAt, nil, nil))
}
}
}

Expand All @@ -122,6 +148,7 @@ func main() {
api.WithWebAuthnService(webAuthApi),
api.WithVaultController(vaultController),
api.WithOrderScalerMaxMultiplier(cfg.OrderScalerMaxMultiplier),
api.WithDebugMode(debugEnabled),
)

strictServer := api.NewStrictHandler(apiHandler, []api.StrictMiddlewareFunc{
Expand Down Expand Up @@ -199,9 +226,12 @@ func main() {
}
}

// we can now access the secrets
secrets := vaultController.Secrets()

if secrets == nil {
fatal("vault secrets unavailable", errors.New("vault secrets unavailable"))
}

client, err := tc.New3CommasClient(tc.ClientConfig{
APIKey: secrets.Secrets.THREECOMMASAPIKEY,
PrivatePEM: []byte(secrets.Secrets.THREECOMMASPRIVATEKEY),
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
* xref:index.adoc[Overview]
* xref:setup.adoc[Setup]
* xref:debug-mode.adoc[Debug mode (requires debug build tag)]
* xref:architecture.adoc[Architecture]
** xref:architecture/order-scaler.adoc[Order scaler]
* xref:operations.adoc[Operations]
Expand Down
81 changes: 81 additions & 0 deletions docs/modules/ROOT/pages/debug-mode.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
= Debug mode
:page-role: guide

This page explains how developers can exercise the Recomma binary without completing the normal WebAuthn vault ceremony. Debug mode is designed for local experiments, feature development, and automated tests. It should never be enabled in environments that handle real funds.

[NOTE]
====
The debug helpers are excluded from release builds. Compile the binary with the `debugmode` build tag whenever you need this feature:

```
go build -tags debugmode ./cmd/recomma
```

The `--debug` flag and `RECOMMA_DEBUG` environment toggle are hidden in builds that omit the tag and will raise an error if set.
====

== What debug mode does

Debug mode pretends that a vault user already completed enrollment and injected secrets into the controller. When `--debug` (or `RECOMMA_DEBUG=true`) is set and the expected environment variables are present, the process:

* seeds the vault controller with the supplied credential bundle and immediately transitions the state machine to *Unsealed*;
* skips the WebAuthn flow and session cookie checks so API endpoints stay usable during local runs;
* publishes `DEBUG_LOGS` to the Web UI runtime config, enabling additional console output; and
* advertises `debug_mode: true` through the `/api/v1/vault/status` response so tools can detect the relaxed authentication requirements.

Important: the controller still waits for the unseal signal. Because the secrets are preloaded, the wait completes as soon as the controller processes the injected state. No changes are required to the main startup flow.

== Required environment variables

Provide the same secrets you would normally store through the vault, but via environment variables that start with `RECOMMA_DEBUG_`. All of them are required; the process aborts if any value is missing or empty.

[cols="1,3",options="header"]
|===
| Variable | Purpose
| `RECOMMA_DEBUG_THREECOMMAS_API_KEY` | Public API key for 3Commas requests.
| `RECOMMA_DEBUG_THREECOMMAS_PRIVATE_KEY` | PEM-encoded private key that signs 3Commas requests.
| `RECOMMA_DEBUG_HYPERLIQUID_WALLET` | Hyperliquid wallet address used for submissions and subscriptions.
| `RECOMMA_DEBUG_HYPERLIQUID_PRIVATE_KEY` | Hyperliquid signing key corresponding to the debug wallet.
| `RECOMMA_DEBUG_HYPERLIQUID_URL` | Base URL for the Hyperliquid endpoint (for example `https://api.hyperliquid.xyz`). Must be a valid URL.
|===

Each variable is trimmed before use. URL validation rejects obvious typos to avoid confusing connection errors later in startup.

== Enabling debug mode

. Export the required environment variables. Store development-only credentials in a `.env.debug` file or your shell profile. Never reuse production keys.
. Build or run the binary with the `debugmode` tag so the flag is available:
+
[source,bash]
----
go run -tags debugmode ./cmd/recomma --help
----
. Launch the binary with the flag or environment toggle:
+
[source,bash]
----
RECOMMA_DEBUG=true \
RECOMMA_DEBUG_THREECOMMAS_API_KEY=... \
RECOMMA_DEBUG_THREECOMMAS_PRIVATE_KEY="$(cat private.pem)" \
RECOMMA_DEBUG_HYPERLIQUID_WALLET=... \
RECOMMA_DEBUG_HYPERLIQUID_PRIVATE_KEY=... \
RECOMMA_DEBUG_HYPERLIQUID_URL=https://example.hyperliquid.dev \
go run ./cmd/recomma --public-origin=http://localhost:8080
----
. Open the Web UI. It will behave as if you already authenticated: vault status shows `debug_mode: true` and the API skips session cookies.

TIP: When scripting tests, inject the environment variables directly into the process instead of writing them to disk.

== Interacting with the API in debug mode

* Session cookies are no longer required because `requireSession` short-circuits when debug mode is active. You can call authenticated endpoints directly with `curl` or Postman.
* The embedded Web UI exposes `DEBUG_LOGS` in `/config.js`, so you can toggle verbose browser logging if the SPA supports it.
* The vault status endpoint signals debug mode so that test harnesses can assert they are running against a relaxed environment.

== Limitations and cautions

* Secrets stay in memory for the entire process lifetime. There is no reseal flow or passkey protection.
* The injected user has the hard-coded username `debug` and an ID of `0`. Features that rely on real user metadata may need additional scaffolding when tested in debug mode.
* This mode bypasses the strongest security guarantees of Recomma. Keep it behind firewalls and avoid connecting wallets with transferable value.

Disable debug mode and restart the binary when you are ready to return to normal operations. Building without `-tags debugmode` also removes the flag and environment toggle entirely.
9 changes: 9 additions & 0 deletions internal/api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

tc "github.com/recomma/3commas-sdk-go/threecommas"
"github.com/recomma/recomma/hl"
"github.com/recomma/recomma/internal/debugmode"
"github.com/recomma/recomma/internal/vault"
"github.com/recomma/recomma/metadata"
"github.com/recomma/recomma/recomma"
Expand Down Expand Up @@ -57,6 +58,7 @@ type ApiHandler struct {
webauthn *WebAuthnService
vault *vault.Controller
session *vaultSessionManager
debug bool

orderScalerMaxMultiplier float64
orders recomma.Emitter
Expand Down Expand Up @@ -166,6 +168,13 @@ func WithOrderScalerMaxMultiplier(max float64) HandlerOption {
}
}

// WithDebugMode toggles debug behaviour for the handler.
func WithDebugMode(enabled bool) HandlerOption {
return func(h *ApiHandler) {
h.debug = enabled && debugmode.Available()
}
}

// WithOrderEmitter wires the queue-backed emitter used for manual order actions.
func WithOrderEmitter(emitter recomma.Emitter) HandlerOption {
return func(h *ApiHandler) {
Expand Down
3 changes: 3 additions & 0 deletions internal/api/ops.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading