Skip to content

Commit 5737ba8

Browse files
committed
chore: release v0.10.0-beta.3
1 parent d061be0 commit 5737ba8

14 files changed

Lines changed: 140 additions & 121 deletions

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ on:
66
workflow_dispatch:
77
inputs:
88
version:
9-
description: 'Version to build (e.g., v0.10.0-beta.2)'
9+
description: 'Version to build (e.g., v0.10.0-beta.3)'
1010
required: true
11-
default: 'v0.10.0-beta.2'
11+
default: 'v0.10.0-beta.3'
1212

1313
permissions:
1414
contents: read

README.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@
77
[![Go Version](https://img.shields.io/github/go-mod/go-version/Cod-e-Codes/marchat?logo=go)](https://go.dev/dl/)
88
[![GitHub all releases](https://img.shields.io/github/downloads/Cod-e-Codes/marchat/total?logo=github)](https://github.com/Cod-e-Codes/marchat/releases)
99
[![Docker Pulls](https://img.shields.io/docker/pulls/codecodesxyz/marchat?logo=docker)](https://hub.docker.com/r/codecodesxyz/marchat)
10-
[![Version](https://img.shields.io/badge/version-v0.10.0--beta.2-blue)](https://github.com/Cod-e-Codes/marchat/releases/tag/v0.10.0-beta.2)
10+
[![Version](https://img.shields.io/badge/version-v0.10.0--beta.3-blue)](https://github.com/Cod-e-Codes/marchat/releases/tag/v0.10.0-beta.3)
1111

1212
A lightweight terminal chat with real-time messaging over WebSockets, optional E2E encryption, and a flexible plugin ecosystem. Built for developers who prefer the command line.
1313

1414
**Quick start:** [QUICKSTART.md](QUICKSTART.md) for a single-page walkthrough (install → server → client → next docs).
1515

1616
## Latest Updates
1717

18-
### v0.10.0-beta.2 (Current)
18+
### v0.10.0-beta.3 (Current)
19+
- **Caddy / WSS**: `docker-compose.proxy.yml`, `deploy/caddy/` (`Caddyfile`, `proxy.env.example`), cross-platform `scripts/build-linux.sh` and `scripts/connect-local-wss.sh`, full walkthrough in `deploy/CADDY-REVERSE-PROXY.md`
20+
- **Client**: WSS/TLS tweaks; sanitize pasted `--server` URLs; connect from flags without profile picker when server+username are set (keystore passphrase prompted for `--e2e` unless `--non-interactive`); `MARCHAT_GLOBAL_E2E_KEY` unchanged
21+
- **Server config**: Document `godotenv.Overload` for `config/.env` vs process env (see README / env.example)
22+
23+
### v0.10.0-beta.2
1924
- **CLI diagnostics**: `marchat-client` and `marchat-server` support `-doctor` and `-doctor-json` for environment, paths, and config health
2025
- **Build**: `build-release.ps1` sets `CGO_ENABLED=0` for consistent cross-compilation
2126
- **Dependencies**: `modernc.org/sqlite` 1.47.0 → 1.48.0 (via Dependabot)
@@ -36,6 +41,7 @@ A lightweight terminal chat with real-time messaging over WebSockets, optional E
3641
- **Plugins**: Full plugin system wiring (message forwarding, user list updates, command responses, init handshake, store UI, license enforcement)
3742

3843
### Recent Releases
44+
- **v0.10.0-beta.3**: Caddy TLS proxy example, Unix helper scripts, client WSS/direct-connect UX, config `.env` precedence docs
3945
- **v0.10.0-beta.2**: Doctor CLI, build-release cross-compile fix, sqlite bump, doc metrics refresh, Docker image entrypoint/volume permission fixes
4046
- **v0.9.0-beta.6**: Rebuilt with Go 1.25.8 to address CVE-2026-25679, CVE-2026-27142, CVE-2026-27139
4147
- **v0.9.0-beta.5**: Automated release workflow, PBKDF2 keystore key derivation, JWT secret auto-generation, race condition fixes, Docker optimizations
@@ -134,24 +140,24 @@ Key tables for message tracking and moderation:
134140
**Binary Installation:**
135141
```bash
136142
# Linux (amd64)
137-
wget https://github.com/Cod-e-Codes/marchat/releases/download/v0.10.0-beta.2/marchat-v0.10.0-beta.2-linux-amd64.zip
138-
unzip marchat-v0.10.0-beta.2-linux-amd64.zip && chmod +x marchat-*
143+
wget https://github.com/Cod-e-Codes/marchat/releases/download/v0.10.0-beta.3/marchat-v0.10.0-beta.3-linux-amd64.zip
144+
unzip marchat-v0.10.0-beta.3-linux-amd64.zip && chmod +x marchat-*
139145

140146
# macOS (amd64)
141-
wget https://github.com/Cod-e-Codes/marchat/releases/download/v0.10.0-beta.2/marchat-v0.10.0-beta.2-darwin-amd64.zip
142-
unzip marchat-v0.10.0-beta.2-darwin-amd64.zip && chmod +x marchat-*
147+
wget https://github.com/Cod-e-Codes/marchat/releases/download/v0.10.0-beta.3/marchat-v0.10.0-beta.3-darwin-amd64.zip
148+
unzip marchat-v0.10.0-beta.3-darwin-amd64.zip && chmod +x marchat-*
143149

144150
# Windows - PowerShell
145151
iwr -useb https://raw.githubusercontent.com/Cod-e-Codes/marchat/main/install.ps1 | iex
146152
```
147153

148154
**Docker:**
149155
```bash
150-
docker pull codecodesxyz/marchat:v0.10.0-beta.2
156+
docker pull codecodesxyz/marchat:v0.10.0-beta.3
151157
docker run -d -p 8080:8080 \
152158
-e MARCHAT_ADMIN_KEY=$(openssl rand -hex 32) \
153159
-e MARCHAT_USERS=admin1,admin2 \
154-
codecodesxyz/marchat:v0.10.0-beta.2
160+
codecodesxyz/marchat:v0.10.0-beta.3
155161
```
156162

157163
**Docker Compose (local development):**

SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Supported Versions
44

5-
`marchat` is currently at **v0.10.0-beta.2**.
5+
`marchat` is currently at **v0.10.0-beta.3**.
66
All security updates and fixes are applied to the `main` branch.
77

88
| Version | Supported |

build-release.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
# Build script for marchat v0.10.0-beta.2
1+
# Build script for marchat v0.10.0-beta.3
22
# This script builds all platform targets and creates release zips
33

44
$ErrorActionPreference = "Stop"
55

6-
$VERSION = "v0.10.0-beta.2"
6+
$VERSION = "v0.10.0-beta.3"
77
$BUILD_DIR = "build"
88
$RELEASE_DIR = "release"
99

client/main.go

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
tea "github.com/charmbracelet/bubbletea"
3535
"github.com/charmbracelet/lipgloss"
3636
"github.com/gorilla/websocket"
37+
"golang.org/x/term"
3738
)
3839

3940
const maxMessages = 100
@@ -87,7 +88,7 @@ var (
8788
skipTLSVerify = flag.Bool("skip-tls-verify", false, "Skip TLS certificate verification")
8889
quickStart = flag.Bool("quick-start", false, "Use last connection or select from saved profiles")
8990
autoConnect = flag.Bool("auto", false, "Automatically connect using most recent profile")
90-
nonInteractive = flag.Bool("non-interactive", false, "Skip interactive prompts (require all flags)")
91+
nonInteractive = flag.Bool("non-interactive", false, "Skip interactive prompts (with --e2e, require --keystore-passphrase on the command line)")
9192
runDoctor = flag.Bool("doctor", false, "Print environment and configuration diagnostics, then exit")
9293
runDoctorJSON = flag.Bool("doctor-json", false, "Same as -doctor with JSON output (if both are set, JSON is used)")
9394
)
@@ -2228,24 +2229,37 @@ func main() {
22282229
var cfg *config.Config
22292230
var err error
22302231

2231-
// Check if all required flags are provided for non-interactive mode
2232-
if *nonInteractive || (allFlagsProvided(*serverURL, *username, *isAdmin, *adminKey, *useE2E, *keystorePassphrase)) {
2232+
// Skip profile picker when CLI gives enough to connect: server, username, and admin key if --admin.
2233+
// If --e2e without --keystore-passphrase, prompt once on the terminal (unless --non-interactive).
2234+
if *nonInteractive || directConnectFromFlags(*serverURL, *username, *isAdmin, *adminKey) {
22332235
// Use traditional flag-based configuration
22342236
cfg, err = loadConfigFromFlags(*configPath, *serverURL, *username, *theme, *isAdmin, *useE2E, *skipTLSVerify)
22352237
if err != nil {
22362238
fmt.Printf("Error loading config: %v\n", err)
22372239
os.Exit(1)
22382240
}
22392241

2240-
// Validate required flags for non-interactive mode
2241-
if err := validateFlags(*isAdmin, *adminKey, *useE2E, *keystorePassphrase); err != nil {
2242+
keystorePass := *keystorePassphrase
2243+
if cfg.UseE2E && keystorePass == "" {
2244+
if *nonInteractive {
2245+
fmt.Fprintln(os.Stderr, "Error: --e2e requires --keystore-passphrase when using --non-interactive")
2246+
os.Exit(1)
2247+
}
2248+
var readErr error
2249+
keystorePass, readErr = readKeystorePassphraseFromTerminal()
2250+
if readErr != nil {
2251+
fmt.Fprintf(os.Stderr, "Error reading keystore passphrase: %v\n", readErr)
2252+
os.Exit(1)
2253+
}
2254+
}
2255+
2256+
if err := validateFlags(*isAdmin, *adminKey, cfg.UseE2E, keystorePass); err != nil {
22422257
fmt.Printf("Error: %v\n", err)
22432258
flag.Usage()
22442259
os.Exit(1)
22452260
}
22462261

2247-
// Continue with existing client initialization using flag values
2248-
initializeClient(cfg, *adminKey, *keystorePassphrase)
2262+
initializeClient(cfg, *adminKey, keystorePass)
22492263

22502264
} else {
22512265
// Check if this is a first-time user (no profiles exist)
@@ -2395,20 +2409,26 @@ func main() {
23952409
}
23962410
}
23972411

2398-
func allFlagsProvided(serverURL, username string, isAdmin bool, adminKey string, useE2E bool, keystorePassphrase string) bool {
2412+
// directConnectFromFlags is true when the user supplied enough CLI args to skip the profile menu.
2413+
// Keystore passphrase may still be prompted when --e2e is set (see main).
2414+
func directConnectFromFlags(serverURL, username string, isAdmin bool, adminKey string) bool {
23992415
if serverURL == "" || username == "" {
24002416
return false
24012417
}
2402-
24032418
if isAdmin && adminKey == "" {
24042419
return false
24052420
}
2421+
return true
2422+
}
24062423

2407-
if useE2E && keystorePassphrase == "" {
2408-
return false
2424+
func readKeystorePassphraseFromTerminal() (string, error) {
2425+
fmt.Fprint(os.Stderr, "Keystore passphrase: ")
2426+
b, err := term.ReadPassword(int(syscall.Stdin))
2427+
fmt.Fprintln(os.Stderr)
2428+
if err != nil {
2429+
return "", err
24092430
}
2410-
2411-
return true
2431+
return string(b), nil
24122432
}
24132433

24142434
func loadConfigFromFlags(configPath, serverURL, username, theme string, isAdmin, useE2E, skipTLSVerify bool) (*config.Config, error) {

client/main_test.go

Lines changed: 42 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -179,26 +179,20 @@ func TestMainPackageStructure(t *testing.T) {
179179

180180
func TestErrorHandling(t *testing.T) {
181181
// Test error handling functions
182-
if allFlagsProvided("", "", false, "", false, "") {
183-
t.Error("allFlagsProvided should return false for empty required flags")
182+
if directConnectFromFlags("", "", false, "") {
183+
t.Error("directConnectFromFlags should return false for empty required flags")
184184
}
185-
if allFlagsProvided("ws://localhost", "", false, "", false, "") {
186-
t.Error("allFlagsProvided should return false for missing username")
185+
if directConnectFromFlags("ws://localhost", "", false, "") {
186+
t.Error("directConnectFromFlags should return false for missing username")
187187
}
188-
if allFlagsProvided("ws://localhost", "user", true, "", false, "") {
189-
t.Error("allFlagsProvided should return false for admin without admin key")
188+
if directConnectFromFlags("ws://localhost", "user", true, "") {
189+
t.Error("directConnectFromFlags should return false for admin without admin key")
190190
}
191-
if allFlagsProvided("ws://localhost", "user", false, "", true, "") {
192-
t.Error("allFlagsProvided should return false for e2e without passphrase")
191+
if !directConnectFromFlags("ws://localhost", "user", false, "") {
192+
t.Error("directConnectFromFlags should return true for valid basic flags (e2e passphrase is prompted separately)")
193193
}
194-
if !allFlagsProvided("ws://localhost", "user", false, "", false, "") {
195-
t.Error("allFlagsProvided should return true for valid basic flags")
196-
}
197-
if !allFlagsProvided("ws://localhost", "user", true, "key", false, "") {
198-
t.Error("allFlagsProvided should return true for valid admin flags")
199-
}
200-
if !allFlagsProvided("ws://localhost", "user", false, "", true, "pass") {
201-
t.Error("allFlagsProvided should return true for valid e2e flags")
194+
if !directConnectFromFlags("ws://localhost", "user", true, "key") {
195+
t.Error("directConnectFromFlags should return true for valid admin flags")
202196
}
203197
}
204198

@@ -589,102 +583,59 @@ func TestLogOutputHandling(t *testing.T) {
589583
// We can't easily test this without file system manipulation
590584
}
591585

592-
func TestAllFlagsProvided(t *testing.T) {
593-
// Test allFlagsProvided function thoroughly
586+
func TestDirectConnectFromFlags(t *testing.T) {
594587
testCases := []struct {
595588
name string
596589
serverURL, username string
597590
isAdmin bool
598591
adminKey string
599-
useE2E bool
600-
keystorePassphrase string
601592
expected bool
602593
}{
603594
{
604-
name: "all provided",
605-
serverURL: "ws://localhost:8080",
606-
username: "user",
607-
isAdmin: false,
608-
adminKey: "",
609-
useE2E: false,
610-
keystorePassphrase: "",
611-
expected: true,
612-
},
613-
{
614-
name: "missing serverURL",
615-
serverURL: "",
616-
username: "user",
617-
isAdmin: false,
618-
adminKey: "",
619-
useE2E: false,
620-
keystorePassphrase: "",
621-
expected: false,
622-
},
623-
{
624-
name: "missing username",
625-
serverURL: "ws://localhost:8080",
626-
username: "",
627-
isAdmin: false,
628-
adminKey: "",
629-
useE2E: false,
630-
keystorePassphrase: "",
631-
expected: false,
632-
},
633-
{
634-
name: "admin without key",
635-
serverURL: "ws://localhost:8080",
636-
username: "user",
637-
isAdmin: true,
638-
adminKey: "",
639-
useE2E: false,
640-
keystorePassphrase: "",
641-
expected: false,
595+
name: "basic",
596+
serverURL: "ws://localhost:8080",
597+
username: "user",
598+
isAdmin: false,
599+
adminKey: "",
600+
expected: true,
642601
},
643602
{
644-
name: "admin with key",
645-
serverURL: "ws://localhost:8080",
646-
username: "user",
647-
isAdmin: true,
648-
adminKey: "secret",
649-
useE2E: false,
650-
keystorePassphrase: "",
651-
expected: true,
603+
name: "missing serverURL",
604+
serverURL: "",
605+
username: "user",
606+
isAdmin: false,
607+
adminKey: "",
608+
expected: false,
652609
},
653610
{
654-
name: "e2e without passphrase",
655-
serverURL: "ws://localhost:8080",
656-
username: "user",
657-
isAdmin: false,
658-
adminKey: "",
659-
useE2E: true,
660-
keystorePassphrase: "",
661-
expected: false,
611+
name: "missing username",
612+
serverURL: "ws://localhost:8080",
613+
username: "",
614+
isAdmin: false,
615+
adminKey: "",
616+
expected: false,
662617
},
663618
{
664-
name: "e2e with passphrase",
665-
serverURL: "ws://localhost:8080",
666-
username: "user",
667-
isAdmin: false,
668-
adminKey: "",
669-
useE2E: true,
670-
keystorePassphrase: "pass",
671-
expected: true,
619+
name: "admin without key",
620+
serverURL: "ws://localhost:8080",
621+
username: "user",
622+
isAdmin: true,
623+
adminKey: "",
624+
expected: false,
672625
},
673626
{
674-
name: "both admin and e2e",
675-
serverURL: "ws://localhost:8080",
676-
username: "user",
677-
isAdmin: true,
678-
adminKey: "secret",
679-
useE2E: true,
680-
keystorePassphrase: "pass",
681-
expected: true,
627+
name: "admin with key",
628+
serverURL: "ws://localhost:8080",
629+
username: "user",
630+
isAdmin: true,
631+
adminKey: "secret",
632+
expected: true,
682633
},
683634
}
684635

685636
for _, tc := range testCases {
686637
t.Run(tc.name, func(t *testing.T) {
687-
result := allFlagsProvided(tc.serverURL, tc.username, tc.isAdmin, tc.adminKey, tc.useE2E, tc.keystorePassphrase)
638+
result := directConnectFromFlags(tc.serverURL, tc.username, tc.isAdmin, tc.adminKey)
688639
if result != tc.expected {
689640
t.Errorf("Expected %v for %s, got %v", tc.expected, tc.name, result)
690641
}

client/websocket.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,26 @@ func (m *model) abortPartialWebSocketConnect() {
204204
m.connected = false
205205
}
206206

207+
// sanitizeServerURL trims whitespace and matching quotes often pasted from docs or PowerShell.
208+
func sanitizeServerURL(raw string) string {
209+
s := strings.TrimSpace(raw)
210+
s = strings.Trim(s, `"'`+"`")
211+
// Unicode curly quotes (copy-paste from word processors)
212+
for {
213+
trimmed := strings.TrimPrefix(s, "\u2018") // ‘
214+
trimmed = strings.TrimPrefix(trimmed, "\u201c") // “
215+
trimmed = strings.TrimSuffix(trimmed, "\u2019") // ’
216+
trimmed = strings.TrimSuffix(trimmed, "\u201d") // ”
217+
if trimmed == s {
218+
break
219+
}
220+
s = strings.TrimSpace(trimmed)
221+
}
222+
return strings.TrimSpace(s)
223+
}
224+
207225
func (m *model) connectWebSocket(serverURL string) error {
226+
serverURL = sanitizeServerURL(serverURL)
208227
escapedUsername := url.QueryEscape(m.cfg.Username)
209228
fullURL := serverURL + "?username=" + escapedUsername
210229

0 commit comments

Comments
 (0)