Merge pull request #342 from naturallaw777/copilot/fix-lnd-alby-hub-port-collision

Resolve deterministic LND/Alby Hub port collision and enforce loopback-only binding
This commit is contained in:
Sovran Systems
2026-07-27 13:34:13 +00:00
committed by GitHub
9 changed files with 345 additions and 7 deletions
@@ -0,0 +1,76 @@
name: Wallet Connections Nix Validation
on:
workflow_dispatch:
pull_request:
branches: [ main ]
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-latest
timeout-minutes: 180
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v16
- name: Targeted Wallet Connections tests
run: python3 -m unittest app/tests/test_wallet_connections.py
- name: Full Python tests
run: python3 -m unittest discover -s app/tests -p 'test_*.py'
- name: JavaScript syntax checks
run: |
set -euo pipefail
for f in app/sovran_systemsos_web/static/js/*.js; do
node --check "$f"
done
- name: Verify Alby Hub patches apply to v1.23.0
run: |
set -euo pipefail
tmpdir="$(mktemp -d)"
curl -fsSL https://github.com/getAlby/hub/archive/refs/tags/v1.23.0.tar.gz | tar -xz -C "$tmpdir"
srcdir="$(find "$tmpdir" -maxdepth 1 -type d -name 'hub-*' | head -n1)"
test -n "$srcdir"
cd "$srcdir"
git init -q
git add -A
git commit -q -m "snapshot"
git apply --check "$GITHUB_WORKSPACE/packages/albyhub/0001-private-route-hints.patch"
git apply --check "$GITHUB_WORKSPACE/packages/albyhub/0002-isolated-invoice-app-id.patch"
git apply --check "$GITHUB_WORKSPACE/packages/albyhub/0003-loopback-bind-host.patch"
- name: Build patched Alby Hub
run: |
nix --extra-experimental-features 'nix-command flakes' build \
--no-link --print-build-logs --impure --expr \
'let
flake = builtins.getFlake (toString ./.);
pkgs = import flake.inputs.nixpkgs { system = "x86_64-linux"; };
patchedAlbyHub = pkgs.albyhub.overrideAttrs (old: {
patches = (old.patches or []) ++ [
./packages/albyhub/0001-private-route-hints.patch
./packages/albyhub/0002-isolated-invoice-app-id.patch
./packages/albyhub/0003-loopback-bind-host.patch
];
});
in patchedAlbyHub'
- name: Run real NixOS listener test
run: |
nix --extra-experimental-features 'nix-command flakes' build \
'.#checks.x86_64-linux.nwc-wallets-port-collision' \
--no-link --print-build-logs
- name: Build full system closure
run: |
nix --extra-experimental-features 'nix-command flakes' build \
'.#nixosConfigurations.nixos.config.system.build.toplevel' \
--no-link --print-build-logs
+6 -2
View File
@@ -1,7 +1,8 @@
""" """
Alby Hub manager — shared backend for Wallet Connections API and recovery CLI. Alby Hub manager — shared backend for Wallet Connections API and recovery CLI.
Interfaces with the local Alby Hub instance at http://127.0.0.1:8080. Interfaces with the local Alby Hub instance at
http://127.0.0.1:18080 by default (override with NWC_ALBY_HUB_API_BASE).
All sensitive values (passwords, bearer tokens, pairing URIs, macaroon All sensitive values (passwords, bearer tokens, pairing URIs, macaroon
contents, Nostr private keys) are redacted from any exception messages contents, Nostr private keys) are redacted from any exception messages
or log output. or log output.
@@ -24,7 +25,10 @@ logger = logging.getLogger(__name__)
# ── Constants ────────────────────────────────────────────────────── # ── Constants ──────────────────────────────────────────────────────
DEFAULT_API_BASE = "http://127.0.0.1:8080" DEFAULT_API_BASE = os.environ.get(
"NWC_ALBY_HUB_API_BASE",
"http://127.0.0.1:18080",
)
DEFAULT_UNLOCK_PASSWORD_FILE = "/var/lib/albyhub/unlock-password" DEFAULT_UNLOCK_PASSWORD_FILE = "/var/lib/albyhub/unlock-password"
DEFAULT_MACAROON_FILE = os.environ.get( DEFAULT_MACAROON_FILE = os.environ.get(
"NWC_LND_MACAROON_FILE", "/run/lnd/albyhub.macaroon" "NWC_LND_MACAROON_FILE", "/run/lnd/albyhub.macaroon"
+65
View File
@@ -25,6 +25,8 @@ Tests cover:
""" """
import json import json
import importlib
import os
import re import re
import shutil import shutil
import subprocess import subprocess
@@ -169,6 +171,28 @@ def _fresh_manager() -> mgr.AlbyHubManager:
) )
class ManagerApiBaseConfigurationTests(unittest.TestCase):
def setUp(self):
self._original_api_base = os.environ.get("NWC_ALBY_HUB_API_BASE")
def tearDown(self):
if self._original_api_base is None:
os.environ.pop("NWC_ALBY_HUB_API_BASE", None)
else:
os.environ["NWC_ALBY_HUB_API_BASE"] = self._original_api_base
importlib.reload(mgr)
def test_default_api_base_falls_back_to_18080(self):
os.environ.pop("NWC_ALBY_HUB_API_BASE", None)
reloaded_mgr = importlib.reload(mgr)
self.assertEqual(reloaded_mgr.DEFAULT_API_BASE, "http://127.0.0.1:18080")
def test_env_api_base_overrides_default(self):
os.environ["NWC_ALBY_HUB_API_BASE"] = "http://127.0.0.1:19999"
reloaded_mgr = importlib.reload(mgr)
self.assertEqual(reloaded_mgr.DEFAULT_API_BASE, "http://127.0.0.1:19999")
# ── Feature registry tests ──────────────────────────────────────── # ── Feature registry tests ────────────────────────────────────────
@@ -1113,6 +1137,7 @@ class NixPatchContractTests(unittest.TestCase):
"patchedAlbyHub = pkgs.albyhub.overrideAttrs (old: { patches = (old.patches or []) ++ [ " "patchedAlbyHub = pkgs.albyhub.overrideAttrs (old: { patches = (old.patches or []) ++ [ "
"./packages/albyhub/0001-private-route-hints.patch " "./packages/albyhub/0001-private-route-hints.patch "
"./packages/albyhub/0002-isolated-invoice-app-id.patch " "./packages/albyhub/0002-isolated-invoice-app-id.patch "
"./packages/albyhub/0003-loopback-bind-host.patch "
"]; }); " "]; }); "
f"in {result_expr}" f"in {result_expr}"
) )
@@ -1124,10 +1149,13 @@ class NixPatchContractTests(unittest.TestCase):
self.assertIn("pkgs.albyhub.overrideAttrs", text) self.assertIn("pkgs.albyhub.overrideAttrs", text)
self.assertIn("../packages/albyhub/0001-private-route-hints.patch", text) self.assertIn("../packages/albyhub/0001-private-route-hints.patch", text)
self.assertIn("../packages/albyhub/0002-isolated-invoice-app-id.patch", text) self.assertIn("../packages/albyhub/0002-isolated-invoice-app-id.patch", text)
self.assertIn("../packages/albyhub/0003-loopback-bind-host.patch", text)
self.assertNotIn("sha256-AAAA", text) self.assertNotIn("sha256-AAAA", text)
self.assertNotIn("lib.fakeHash", text) self.assertNotIn("lib.fakeHash", text)
self.assertIn("AUTO_UNLOCK_PASSWORD", text) self.assertIn("AUTO_UNLOCK_PASSWORD", text)
self.assertNotIn("AUTO_UNLOCK_PASSWORD_FILE", text) self.assertNotIn("AUTO_UNLOCK_PASSWORD_FILE", text)
self.assertIn("albyHubPort = 18080;", text)
self.assertIn('albyHubApiBase = "http://127.0.0.1:${toString albyHubPort}";', text)
def test_nwc_module_uses_lib_getexe_for_albyhub_binary(self): def test_nwc_module_uses_lib_getexe_for_albyhub_binary(self):
repo_root = Path(__file__).resolve().parents[2] repo_root = Path(__file__).resolve().parents[2]
@@ -1215,6 +1243,43 @@ class NixPatchContractTests(unittest.TestCase):
self.assertIn("MakeInvoice(ctx, amountMsat, description,", text) self.assertIn("MakeInvoice(ctx, amountMsat, description,", text)
self.assertIn(", appId, nil, nil)", text) self.assertIn(", appId, nil, nil)", text)
def test_loopback_bind_host_patch_contains_required_changes(self):
repo_root = Path(__file__).resolve().parents[2]
patch_path = repo_root / "packages" / "albyhub" / "0003-loopback-bind-host.patch"
text = patch_path.read_text()
self.assertIn("diff --git a/cmd/http/main.go b/cmd/http/main.go", text)
self.assertIn("diff --git a/config/models.go b/config/models.go", text)
self.assertIn('Host string `envconfig:"HOST" default:"127.0.0.1"`', text)
self.assertIn("bindAddress := net.JoinHostPort", text)
self.assertIn("if err := e.Start(bindAddress);", text)
def test_nwc_services_share_authoritative_alby_hub_api_base(self):
repo_root = Path(__file__).resolve().parents[2]
module_path = repo_root / "modules" / "nwc-wallets.nix"
text = module_path.read_text()
self.assertIn("environment.NWC_ALBY_HUB_API_BASE = albyHubApiBase;", text)
self.assertIn("NWC_ALBY_HUB_API_BASE = albyHubApiBase;", text)
def test_albyhub_port_contract_and_assertions(self):
repo_root = Path(__file__).resolve().parents[2]
module_path = repo_root / "modules" / "nwc-wallets.nix"
text = module_path.read_text()
self.assertIn("PORT = toString albyHubPort;", text)
self.assertNotIn('PORT = "8080";', text)
self.assertIn("assertion = albyHubPort != config.services.lnd.restPort;", text)
self.assertIn("assertion = albyHubPort != 8181;", text)
self.assertIn("assertion = !(lib.elem albyHubPort config.networking.firewall.allowedTCPPorts);", text)
def test_recovery_cli_uses_manager_default_endpoint(self):
repo_root = Path(__file__).resolve().parents[2]
cli_path = repo_root / "app" / "sovran_systemsos_web" / "nwc_wallet_cli.py"
manager_path = repo_root / "app" / "sovran_systemsos_web" / "nwc_hub_manager.py"
cli_text = cli_path.read_text()
manager_text = manager_path.read_text()
self.assertIn("get_manager()", cli_text)
self.assertNotIn("127.0.0.1:8080", cli_text)
self.assertIn('"http://127.0.0.1:18080"', manager_text)
def test_nwc_lnurl_service_runs_as_albyhub(self): def test_nwc_lnurl_service_runs_as_albyhub(self):
"""nwc-lnurl.service must run as albyhub to read /var/lib/albyhub/unlock-password.""" """nwc-lnurl.service must run as albyhub to read /var/lib/albyhub/unlock-password."""
repo_root = Path(__file__).resolve().parents[2] repo_root = Path(__file__).resolve().parents[2]
+4 -3
View File
@@ -36,7 +36,7 @@ Guardrails:
``` ```
Authenticated Hub management API Authenticated Hub management API
-> local Alby Hub (port 8080, loopback only) -> local Alby Hub (port 18080, loopback only)
-> local LND -> local LND
Public Lightning Address Public Lightning Address
@@ -48,7 +48,7 @@ Public Lightning Address
Security invariants: Security invariants:
- Alby Hub management port (8080) is never opened to the public firewall. - Alby Hub management port (18080) is never opened to the public firewall.
- Dedicated LNURL service port (8181) is never opened to the public firewall. - Dedicated LNURL service port (8181) is never opened to the public firewall.
- LNURL callback and discovery are exposed only through Caddy on 80/443. - LNURL callback and discovery are exposed only through Caddy on 80/443.
- Management APIs are authenticated and remain under `/api/nwc/`. - Management APIs are authenticated and remain under `/api/nwc/`.
@@ -68,10 +68,11 @@ Security invariants:
## Alby Hub package and patches ## Alby Hub package and patches
Wallet Connections uses `pkgs.albyhub` from the repository's pinned `nixpkgs` input and applies two conventional patches via `overrideAttrs`: Wallet Connections uses `pkgs.albyhub` from the repository's pinned `nixpkgs` input and applies three conventional patches via `overrideAttrs`:
1. **Private route hints** (`packages/albyhub/0001-private-route-hints.patch`): changes regular LND invoice creation from `Private: !hasPublicChannels` to `Private: true` and leaves hold-invoice logic unchanged. 1. **Private route hints** (`packages/albyhub/0001-private-route-hints.patch`): changes regular LND invoice creation from `Private: !hasPublicChannels` to `Private: true` and leaves hold-invoice logic unchanged.
2. **Invoice app attribution** (`packages/albyhub/0002-isolated-invoice-app-id.patch`): updates `api/models.go`, `api/transactions.go`, `http/http_service.go`, and `wails/wails_handlers.go` so invoice creation accepts and forwards optional `appId`. 2. **Invoice app attribution** (`packages/albyhub/0002-isolated-invoice-app-id.patch`): updates `api/models.go`, `api/transactions.go`, `http/http_service.go`, and `wails/wails_handlers.go` so invoice creation accepts and forwards optional `appId`.
3. **Loopback bind host** (`packages/albyhub/0003-loopback-bind-host.patch`): adds `HOST` to config and binds Echo to `HOST:PORT` instead of `:PORT`.
No placeholder source/vendor hashes are used in the Wallet Connections module. No placeholder source/vendor hashes are used in the Wallet Connections module.
+9
View File
@@ -57,5 +57,14 @@
]; ];
}; };
}; };
nixosTests.nwc-wallets-port-collision =
import ./nix/tests/nwc-wallets-port-collision.nix {
inherit nixpkgs;
system = "x86_64-linux";
};
checks.x86_64-linux.nwc-wallets-port-collision =
self.nixosTests.nwc-wallets-port-collision;
}; };
} }
+1 -1
View File
@@ -195,7 +195,7 @@ EOF
$LIGHTNING { $LIGHTNING {
# LNURL discovery and callback are served by the dedicated # LNURL discovery and callback are served by the dedicated
# nwc-lnurl service on loopback port 8181. Only these paths # nwc-lnurl service on loopback port 8181. Only these paths
# are proxied; the Alby Hub management port (8080) is never exposed. # are proxied; the Alby Hub management port (18080) is never exposed.
reverse_proxy /.well-known/lnurlp/* http://127.0.0.1:8181 reverse_proxy /.well-known/lnurlp/* http://127.0.0.1:8181
reverse_proxy /lnurlp/* http://127.0.0.1:8181 reverse_proxy /lnurlp/* http://127.0.0.1:8181
} }
+18 -1
View File
@@ -1,10 +1,13 @@
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
let let
albyHubPort = 18080;
albyHubApiBase = "http://127.0.0.1:${toString albyHubPort}";
patchedAlbyHub = pkgs.albyhub.overrideAttrs (old: { patchedAlbyHub = pkgs.albyhub.overrideAttrs (old: {
patches = (old.patches or []) ++ [ patches = (old.patches or []) ++ [
../packages/albyhub/0001-private-route-hints.patch ../packages/albyhub/0001-private-route-hints.patch
../packages/albyhub/0002-isolated-invoice-app-id.patch ../packages/albyhub/0002-isolated-invoice-app-id.patch
../packages/albyhub/0003-loopback-bind-host.patch
]; ];
}); });
@@ -33,6 +36,18 @@ lib.mkIf config.sovran_systemsOS.features."nwc-wallets" {
assertion = !(lib.attrByPath [ "nix-bitcoin" "netns-isolation" "enable" ] false config); assertion = !(lib.attrByPath [ "nix-bitcoin" "netns-isolation" "enable" ] false config);
message = "Wallet Connections requires nix-bitcoin.netns-isolation.enable = false."; message = "Wallet Connections requires nix-bitcoin.netns-isolation.enable = false.";
} }
{
assertion = albyHubPort != config.services.lnd.restPort;
message = "Alby Hub and LND REST must use different ports.";
}
{
assertion = albyHubPort != 8181;
message = "Alby Hub and the public LNURL service must use different ports.";
}
{
assertion = !(lib.elem albyHubPort config.networking.firewall.allowedTCPPorts);
message = "Alby Hub management port must not be opened on the public TCP firewall.";
}
]; ];
users.groups.albyhub = { }; users.groups.albyhub = { };
@@ -79,7 +94,7 @@ lib.mkIf config.sovran_systemsOS.features."nwc-wallets" {
LND_MACAROON_FILE = "/run/lnd/albyhub.macaroon"; LND_MACAROON_FILE = "/run/lnd/albyhub.macaroon";
WORK_DIR = "/var/lib/albyhub"; WORK_DIR = "/var/lib/albyhub";
DATABASE_URI = "/var/lib/albyhub/nwc.db"; DATABASE_URI = "/var/lib/albyhub/nwc.db";
PORT = "8080"; PORT = toString albyHubPort;
RELAY = "wss://relay.getalby.com,wss://relay2.getalby.com"; RELAY = "wss://relay.getalby.com,wss://relay2.getalby.com";
AUTO_LINK_ALBY_ACCOUNT = "false"; AUTO_LINK_ALBY_ACCOUNT = "false";
SEND_EVENTS_TO_ALBY = "false"; SEND_EVENTS_TO_ALBY = "false";
@@ -110,6 +125,7 @@ lib.mkIf config.sovran_systemsOS.features."nwc-wallets" {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "albyhub.service" "sovran-hub-web.service" ]; after = [ "albyhub.service" "sovran-hub-web.service" ];
wants = [ "albyhub.service" ]; wants = [ "albyhub.service" ];
environment.NWC_ALBY_HUB_API_BASE = albyHubApiBase;
serviceConfig = { serviceConfig = {
Type = "simple"; Type = "simple";
@@ -131,6 +147,7 @@ lib.mkIf config.sovran_systemsOS.features."nwc-wallets" {
}; };
systemd.services.sovran-hub-web.environment = { systemd.services.sovran-hub-web.environment = {
NWC_ALBY_HUB_API_BASE = albyHubApiBase;
NWC_LND_ADDRESS = "${lndRpcAddress}:${lndRpcPort}"; NWC_LND_ADDRESS = "${lndRpcAddress}:${lndRpcPort}";
NWC_LND_CERT_FILE = lndCertPath; NWC_LND_CERT_FILE = lndCertPath;
NWC_LND_MACAROON_FILE = "/run/lnd/albyhub.macaroon"; NWC_LND_MACAROON_FILE = "/run/lnd/albyhub.macaroon";
+129
View File
@@ -0,0 +1,129 @@
{ nixpkgs, system ? "x86_64-linux" }:
let
pkgs = import nixpkgs { inherit system; };
in
pkgs.testers.runNixOSTest {
name = "nwc-wallets-port-collision";
nodes.machine = { lib, pkgs, ... }: {
imports = [ ../../modules/nwc-wallets.nix ];
options = {
sovran_systemsOS.features = lib.mkOption {
type = lib.types.attrsOf lib.types.bool;
default = { };
};
sovran_systemsOS.domainRequirements = lib.mkOption {
type = lib.types.listOf lib.types.attrs;
default = [ ];
};
services.sovranHub.webPackage = lib.mkOption {
type = lib.types.package;
};
services.lnd = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
};
rpcAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
};
rpcPort = lib.mkOption {
type = lib.types.int;
default = 10009;
};
restPort = lib.mkOption {
type = lib.types.int;
default = 8080;
};
certPath = lib.mkOption {
type = lib.types.str;
default = "/var/lib/lnd/tls.cert";
};
macaroons = lib.mkOption {
type = lib.types.attrs;
default = { };
};
};
};
config = {
system.stateVersion = "24.11";
networking.firewall.enable = true;
sovran_systemsOS.features."nwc-wallets" = true;
services.lnd.enable = true;
services.lnd.restPort = 8080;
services.sovranHub.webPackage = pkgs.writeShellApplication {
name = "stub-sovran-hub-web";
runtimeInputs = [ pkgs.python3 ];
text = ''
if [ "$(basename "$0")" = "nwc-lnurl" ]; then
exec ${pkgs.python3}/bin/python -m http.server "''${NWC_LNURL_PORT:-8181}" --bind 127.0.0.1
fi
exec ${pkgs.coreutils}/bin/sleep infinity
'';
};
systemd.services.sovran-hub-web = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.coreutils}/bin/sleep infinity";
};
};
systemd.services.lnd-prepare = {
description = "Prepare mock LND cert and macaroon files";
before = [ "lnd.service" "albyhub.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "oneshot";
script = ''
mkdir -p /var/lib/lnd /run/lnd /var/lib/domains
printf 'stub-cert\n' > /var/lib/lnd/tls.cert
printf 'stub-macaroon\n' > /run/lnd/albyhub.macaroon
printf 'lightning.example.com\n' > /var/lib/domains/lightning
'';
};
systemd.services.lnd = {
description = "Mock LND REST listener";
after = [ "network.target" "lnd-prepare.service" ];
requires = [ "lnd-prepare.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.python3}/bin/python -m http.server 8080 --bind 127.0.0.1";
Restart = "always";
};
};
};
};
testScript = ''
start_all()
machine.wait_for_unit("lnd.service")
machine.wait_for_unit("sovran-hub-web.service")
machine.wait_for_unit("albyhub.service")
machine.wait_for_unit("nwc-lnurl.service")
machine.wait_for_open_port(8080)
machine.wait_for_open_port(18080)
machine.wait_for_open_port(8181)
machine.succeed("ss -ltn '( sport = :8080 )' | grep -F '127.0.0.1:8080'")
machine.succeed("ss -ltn '( sport = :18080 )' | grep -F '127.0.0.1:18080'")
machine.succeed("ss -ltn '( sport = :8181 )' | grep -F '127.0.0.1:8181'")
machine.fail("ss -ltn '( sport = :18080 )' | grep -E '0\\.0\\.0\\.0:18080|\\[::\\]:18080'")
machine.fail("ss -ltn '( sport = :8181 )' | grep -E '0\\.0\\.0\\.0:8181|\\[::\\]:8181'")
machine.succeed("${pkgs.curl}/bin/curl --fail --silent http://127.0.0.1:18080/api/info | ${pkgs.gnugrep}/bin/grep -q 'setupCompleted'")
machine.succeed("systemctl show -p Environment nwc-lnurl.service | grep -q 'NWC_ALBY_HUB_API_BASE=http://127.0.0.1:18080'")
machine.succeed("systemctl show -p Environment sovran-hub-web.service | grep -q 'NWC_ALBY_HUB_API_BASE=http://127.0.0.1:18080'")
'';
}
@@ -0,0 +1,37 @@
diff --git a/cmd/http/main.go b/cmd/http/main.go
index ed31f96..4d31212 100644
--- a/cmd/http/main.go
+++ b/cmd/http/main.go
@@ -2,8 +2,8 @@ package main
import (
"context"
- "fmt"
nethttp "net/http"
+ "net"
"os"
"os/signal"
"syscall"
@@ -51,8 +51,9 @@ func main() {
httpSvc := http.NewHttpService(svc, svc.GetEventPublisher())
httpSvc.RegisterSharedRoutes(e)
//start Echo server
+ bindAddress := net.JoinHostPort(svc.GetConfig().GetEnv().Host, svc.GetConfig().GetEnv().Port)
go func() {
- if err := e.Start(fmt.Sprintf(":%v", svc.GetConfig().GetEnv().Port)); err != nil && err != nethttp.ErrServerClosed {
+ if err := e.Start(bindAddress); err != nil && err != nethttp.ErrServerClosed {
logger.Logger.WithError(err).Error("echo server failed to start")
cancel()
}
diff --git a/config/models.go b/config/models.go
index 0fb8870..dc11afd 100644
--- a/config/models.go
+++ b/config/models.go
@@ -24,6 +24,7 @@ type AppConfig struct {
LNDAddress string `envconfig:"LND_ADDRESS"`
LNDCertFile string `envconfig:"LND_CERT_FILE"`
LNDMacaroonFile string `envconfig:"LND_MACAROON_FILE"`
+ Host string `envconfig:"HOST" default:"127.0.0.1"`
Workdir string `envconfig:"WORK_DIR"`
Port string `envconfig:"PORT" default:"8080"`
DatabaseUri string `envconfig:"DATABASE_URI" default:"nwc.db"`