Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
8ce6dee
initial commit
atavism Apr 22, 2026
89bbaa1
code review updates
atavism Apr 22, 2026
2e1f0d4
code review updates
atavism Apr 22, 2026
d35977b
code review updates
atavism Apr 22, 2026
e999850
Merge remote-tracking branch 'origin/main' into atavism/report-issue-…
atavism Apr 24, 2026
ea73e98
Fix report issue screen issues
atavism Apr 28, 2026
68da104
Merge remote-tracking branch 'origin/main' into atavism/report-issue-…
atavism Apr 30, 2026
de1579b
Merge remote-tracking branch 'origin/main' into atavism/report-issue-…
atavism May 4, 2026
d8d535d
fix: disconnect VPN on backend close (#461)
garmr-ulfr May 5, 2026
28ed8f7
Merge remote-tracking branch 'origin/main' into atavism/report-issue-…
atavism May 5, 2026
9fac6db
bump lantern-box to v0.0.78 for QUIC err_class instrumentation (#459)
myleshorton May 6, 2026
b8f04e3
fix: preserve caller-supplied data dir; restore Pro on upgrade (#463)
myleshorton May 6, 2026
4445a20
settings: also migrate from pre-9.x flashlight/lantern-client YAML (#…
myleshorton May 6, 2026
6309530
account: forward payment redirect idempotency keys
atavism May 6, 2026
bfc8e50
Merge branch 'main' into atavism/report-issue-screen
atavism May 7, 2026
602b754
deps: bump kindling for method-aware retry across transports (#468)
myleshorton May 7, 2026
7d59316
bump lantern-box - urltest reselect on failure (#469)
garmr-ulfr May 7, 2026
dad58a1
feat(cli): add monitor TUI, throughput, and session history (#462)
garmr-ulfr May 7, 2026
5a0a9f5
code review updates
atavism May 7, 2026
23e81a3
code review updates
atavism May 7, 2026
5e6db1b
Merge remote-tracking branch 'origin/main' into atavism/report-issue-…
atavism May 7, 2026
a63194a
Merge pull request #434 from getlantern/atavism/report-issue-screen
atavism May 7, 2026
d4fc0cb
peer: emit ConnectionEvent on samizdat accept/close
May 7, 2026
48e0f6f
peer: serve live connection snapshot on 127.0.0.1:17099/peer/connections
May 7, 2026
a1c10cf
peer: drop localhost HTTP stats endpoint, keep ConnectionEvent emit
May 7, 2026
361a645
peer: support manual port-forward override via RADIANCE_PEER_EXTERNAL…
May 7, 2026
869dc9d
peer: read PeerManualPortKey setting alongside RADIANCE_PEER_EXTERNAL…
May 7, 2026
afc173e
ci: open a PR for fronted refresh instead of pushing direct to main (…
myleshorton May 7, 2026
15c896a
Merge remote-tracking branch 'origin/main' into atavism/payment-redirect
atavism May 7, 2026
71c8a8b
vpn: tune base box options and route RU locale to Yandex DNS (#470)
garmr-ulfr May 7, 2026
48d66aa
Merge branch 'main' into atavism/payment-redirect
atavism May 7, 2026
fa40e47
code review updates
atavism May 7, 2026
d079df5
Merge remote-tracking branch 'origin/atavism/payment-redirect' into a…
atavism May 7, 2026
e715af9
unbounded: integrate broflake widget-proxy lifecycle manager
May 8, 2026
f81a61f
Merge pull request #465 from getlantern/atavism/payment-redirect
atavism May 8, 2026
a292331
portforward: ManualForwarder.ExternalIP returns "" again
May 8, 2026
1644393
peer: call /peer/verify after starting sing-box; fix doubled /v1
May 6, 2026
8ce106b
peer: forward common headers (notably X-Lantern-Config-Client-IP) on …
May 7, 2026
b25b01b
peer: register lantern-box protocols in box ctx + regression test
May 6, 2026
e9491c6
set max compressed size for issue report to 19.5 (#475)
garmr-ulfr May 11, 2026
f6774c6
peer: silence connection-event cascade during box.Close
May 11, 2026
39b6b45
peer: emit phase-granular StatusEvents through Start/Stop lifecycle
May 11, 2026
51be53e
bump lantern-box to v0.0.82 (#476)
garmr-ulfr May 11, 2026
bf26ce2
peer: instrument peerconn listener registration + per-event forwarding
May 11, 2026
810ef9b
events: log Emit subscriber count to debug "events vanish" path
May 11, 2026
29a4b7e
ipc: stream peer-status + peer-connection events over IPC SSE
May 11, 2026
0b72cd4
bump lantern-box: real peer addr in ConnectionEvent.Source
May 11, 2026
5aa4acc
Merge remote-tracking branch 'origin/main' into fisk/peer-connection-…
May 12, 2026
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
33 changes: 17 additions & 16 deletions .github/workflows/refresh-fronted-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ on:

permissions:
contents: write
pull-requests: write

# Serialize concurrent runs (cron + manual dispatch can race). cancel-in-progress
# is false so a manual dispatch during a cron run still completes rather than
Expand Down Expand Up @@ -46,20 +47,20 @@ jobs:
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Commit and push
# Open a PR rather than pushing directly: main is protected by a
# "Changes must be made through a pull request" repo rule. The PR
# branch is reused day-to-day so a stale unmerged refresh gets
# superseded by the next one rather than piling up as separate PRs.
- name: Open / update refresh PR
if: steps.diff.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add kindling/fronted/fronted.yaml.gz
git commit -m "fronted: refresh embedded fronted.yaml.gz"
# Rebase-and-retry to survive concurrent commits to the target branch.
for attempt in 1 2 3; do
if git push; then
exit 0
fi
echo "Push attempt $attempt failed, rebasing on latest origin and retrying..."
git pull --rebase origin "$(git rev-parse --abbrev-ref HEAD)"
done
echo "Push failed after 3 attempts" >&2
exit 1
uses: peter-evans/create-pull-request@8a45c9a0f9071f4b1e4a0f3b660a1e9e3d9f0d7f # v7
with:
commit-message: "fronted: refresh embedded fronted.yaml.gz"
title: "fronted: refresh embedded fronted.yaml.gz"
body: |
Automated daily refresh of `kindling/fronted/fronted.yaml.gz`
from `getlantern/fronted@main`. Safe to merge once CI passes.
branch: chore/refresh-fronted-config
delete-branch: true
author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
committer: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
92 changes: 76 additions & 16 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,37 @@

## Code Comments

**Default: no comment.** Only add one if a specific *why* is load-bearing — invariant, concurrency guarantee, error condition, zero-value behavior, non-obvious caller contract, or a constraint that would surprise the reader. Aesthetic "this section is well-documented" comments are noise.
**Language doc conventions take precedence.** When writing a doc comment that the language's tooling formats or renders (Go's `// Foo ...`, Python docstrings, JSDoc, rustdoc, etc.), follow that convention even if it conflicts with the "lead with the *why*" guidance — for Go that means start with the identifier's name. The *why* still belongs in the comment, just in the body after the conventional opening.

- **Default: no comment.** Only comment when necessary to explain a non-obvious contract, invariant, rationale, or surprising behavior.
- Comments must answer *why* something is done a particular way, not *what* is being done (which should be clear from the code and naming).
- Before adding a comment, ask:
- Is this information not obvious from the code or naming?
- Does it document a constraint, invariant, concurrency guarantee, or error condition that would surprise a reader?
- Is it essential for future maintainers to understand the reasoning or risk behind this code?
- **Do not add comments that:**
- Restate the identifier name (in-line only).
- Narrate the next line of code.
- Reference tickets, coworkers, or code locations (these belong in commit messages).
- Describe the mechanism instead of the contract.
- Are aesthetic or redundant ("well-documented").
- Prefer documenting contracts at the declaration site. Use inline comments only for truly non-obvious lines.
- Remove or update obsolete comments promptly.
- **TODOs:** Must state both what needs to be done and why it isn’t done now. Remove or resolve unclear TODOs.

**Examples:**

Before writing any comment, run this checklist on the proposed text. If any answer is yes, delete or rewrite:

1. Does it restate the identifier name or signature? (`// Foo does foo`, `// updateX manages X across Y`)
2. Does it narrate what the visible next line does? (`// Cancel any existing listener` immediately above `cancel()`)
3. Does it open with a generic lifecycle/management preamble before getting to the point? (`// manages the lifecycle of...`, `// handles the X for Y`)
4. Does it reference tickets, coworkers, sibling files, commit SHAs, or other code locations? Those belong in the commit message / PR description — they rot in source.
5. Does it describe the mechanism instead of the contract? (`authenticates via peer credentials over a Unix socket` vs. `authenticates each connection`)
```go
// BAD: Restates what the code does
// Cancel any in-flight requests.
cancelRequests()

Lead with the *why*, not a summary of the function. If the only thing you can write is a summary, the comment isn't needed.
// GOOD: Explains why this is necessary
// Must cancel in-flight requests to avoid leaking goroutines on shutdown.
cancelRequests()
```

Examples:
---

```go
// BAD — restates name, generic preamble, narrates the code
Expand Down Expand Up @@ -50,18 +68,60 @@ c.offlineTestCancel()
// access is released before disk I/O so a slow write can't starve readers.
```

```go
// BAD — doc block enumerates every branch; only one branch has hidden why,
// the rest restate cases the code already shows
// mapStatusEvent maps a radiance VPN status event to the wire value sent
// to Dart. Three cases deviate from a direct pass-through:
// - vpn.Restarting collapses into vpn.Connecting so the UI shows a
// transitional state during a tunnel restart.
// - A non-empty evt.Error always maps to vpn.ErrorStatus.
// - An unrecognized status falls back to Disconnected.
func mapStatusEvent(evt vpn.StatusUpdateEvent) (vpn.VPNStatus, string) { ... }

// GOOD — no doc block; inline comment on the only branch with hidden context
func mapStatusEvent(evt vpn.StatusUpdateEvent) (vpn.VPNStatus, string) {
if evt.Error != "" {
return vpn.ErrorStatus, evt.Error
}
switch evt.Status {
case vpn.Connected, vpn.Connecting, vpn.Disconnecting, vpn.Disconnected, vpn.ErrorStatus:
return evt.Status, ""
case vpn.Restarting:
// Map to Connecting; Dart's parser falls back to Disconnected otherwise.
return vpn.Connecting, ""
default:
return vpn.Disconnected, ""
}
}
```

Before writing an inline comment, consider whether a doc comment on the enclosing function or type would make it unnecessary. Prefer documenting contracts at the declaration over explaining implementation details inline.

Conversely, before writing a multi-bullet doc block that enumerates branches or cases, check each bullet against the line that implements it. If only one bullet carries hidden *why* and the rest restate visible branches, drop the doc block and put a single inline comment on the surprising branch. Doc blocks belong on contracts that surprise as a whole, not on functions where one corner of the implementation is non-obvious. The bar is higher for unexported helpers: the Go doc convention targets exported API, and unexported functions should default to no comment unless the contract genuinely surprises.

TODO comments must state *what* needs to happen and *why* it isn't done now. `TODO: ???` is not actionable — either resolve it or remove it.

## Go Doc Comments

- Use Go doc comments (`// Foo ...`) for exported identifiers and any unexported ones with non-obvious contracts.
- Start with the identifier’s name and a concise summary: `// Foo does X.` The first sentence is shown by `go doc` and pkg.go.dev.
- Follow with additional context or rationale as needed, especially if the *why* is not obvious.
- Place the comment immediately above the declaration, with no blank line.
- For package comments, place one above the `package` clause (typically in `doc.go`), starting with `// Package foo ...`.
- Formatting:
- Use blank lines for paragraphs.
- Indent code blocks.
- Use lists and headings as supported by Go doc formatting.
- Avoid HTML and manual line wrapping; let gofmt handle formatting.
- Use `// Deprecated: ...` on its own paragraph for deprecated identifiers.
- Prefer `ExampleFoo` functions in `_test.go` for usage examples; these are rendered and tested by Go tooling.
- Review doc comments regularly to keep them accurate and relevant.

**Reference:** [Go doc comment guidelines](https://go.dev/doc/comment)

## Comment Verification

After any edit that adds or modifies a comment, you MUST spawn a code-reviewer subagent with the diff before declaring the task done. The subagent applies the Code Comments checklist above and reports violations. Fix the violations and re-spawn until the subagent reports none.

You MUST NOT skip this by self-reviewing the diff. The point of the subagent is to review without the generation bias of the Claude that wrote the comment — a self-review by the writer is a known failure mode and does not satisfy this step.

## Go Doc Comments

- When a doc comment is warranted on an exported identifier, start it with the identifier's name and use complete sentences: `// Foo does X.` The first sentence is the summary shown by `go doc` and pkg.go.dev.
- Package comments: one per package, above the `package` clause (conventionally in `doc.go` for larger packages), starting with `// Package foo ...`.
- Formatting (gofmt-aware since Go 1.19): blank lines separate paragraphs; indented lines render as code blocks; lines starting with `-`, `*`, or `1.` render as lists; `[Name]` links to other symbols; `# Heading` renders as a heading. Avoid HTML and manual wrapping.
26 changes: 19 additions & 7 deletions account/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"log/slog"
"net/url"
"strconv"
"strings"
"time"

"go.opentelemetry.io/otel"
Expand All @@ -33,11 +34,12 @@ const (

// PaymentRedirectData contains the data required to generate a payment redirect URL.
type PaymentRedirectData struct {
Plan string `json:"plan" validate:"required"`
Provider string `json:"provider" validate:"required"`
Email string `json:"email"`
DeviceName string `json:"deviceName" validate:"required" errorId:"device-name"`
BillingType SubscriptionType `json:"billingType"`
Plan string `json:"plan" validate:"required"`
Provider string `json:"provider" validate:"required"`
Email string `json:"email"`
DeviceName string `json:"deviceName" validate:"required" errorId:"device-name"`
BillingType SubscriptionType `json:"billingType"`
IdempotencyKey string `json:"idempotencyKey"`
}

type SubscriptionPlans struct {
Expand Down Expand Up @@ -150,7 +152,7 @@ func (a *Client) StripeBillingPortalURL(ctx context.Context, baseURL, userID, pr
}

type redirect struct {
Redirect string
Redirect string `json:"redirect"`
}

func (a *Client) paymentRedirect(ctx context.Context, path string, params map[string]string) (string, error) {
Expand All @@ -166,7 +168,11 @@ func (a *Client) paymentRedirect(ctx context.Context, path string, params map[st
if err := json.Unmarshal(resp, &r); err != nil {
return "", traces.RecordError(ctx, fmt.Errorf("unmarshaling payment redirect response: %w", err))
}
return r.Redirect, nil
redirectURL := strings.TrimSpace(r.Redirect)
if redirectURL == "" {
return "", traces.RecordError(ctx, fmt.Errorf("payment redirect response missing redirect URL"))
}
return redirectURL, nil
}

// SubscriptionPaymentRedirectURL generates a redirect URL for subscription payment.
Expand All @@ -180,6 +186,9 @@ func (a *Client) SubscriptionPaymentRedirectURL(ctx context.Context, data Paymen
"email": data.Email,
"billingType": string(data.BillingType),
}
if data.IdempotencyKey != "" {
params["idempotencyKey"] = data.IdempotencyKey
}
return a.paymentRedirect(ctx, "/subscription-payment-redirect", params)
}

Expand All @@ -194,6 +203,9 @@ func (a *Client) PaymentRedirect(ctx context.Context, data PaymentRedirectData)
"deviceName": data.DeviceName,
"email": data.Email,
}
if data.IdempotencyKey != "" {
params["idempotencyKey"] = data.IdempotencyKey
}
return a.paymentRedirect(ctx, "/payment-redirect", params)
}

Expand Down
51 changes: 44 additions & 7 deletions account/subscription_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,67 @@ import (
)

func TestSubscriptionPaymentRedirect(t *testing.T) {
ac, _ := newTestClient(t)
ac, ts := newTestClient(t)
data := PaymentRedirectData{
Provider: "stripe",
Plan: "pro",
DeviceName: "test-device",
Email: "",
BillingType: SubscriptionTypeOneTime,
Provider: "stripe",
Plan: "pro",
DeviceName: "test-device",
Email: "",
BillingType: SubscriptionTypeOneTime,
IdempotencyKey: "subscription-redirect-key",
}
url, err := ac.SubscriptionPaymentRedirectURL(context.Background(), data)
require.NoError(t, err)
assert.NotEmpty(t, url)
assert.Equal(t, data.IdempotencyKey, ts.subscriptionPaymentRedirectIdempotencyKey)
}

func TestPaymentRedirect(t *testing.T) {
ac, _ := newTestClient(t)
ac, ts := newTestClient(t)
data := PaymentRedirectData{
Provider: "stripe",
Plan: "pro",
DeviceName: "test-device",
Email: "",
IdempotencyKey: "payment-redirect-key",
}
url, err := ac.PaymentRedirect(context.Background(), data)
require.NoError(t, err)
assert.NotEmpty(t, url)
assert.Equal(t, data.IdempotencyKey, ts.paymentRedirectIdempotencyKey)
}

func TestPaymentRedirectOmitsEmptyIdempotencyKey(t *testing.T) {
ac, ts := newTestClient(t)
data := PaymentRedirectData{
Provider: "stripe",
Plan: "pro",
DeviceName: "test-device",
Email: "",
}

url, err := ac.PaymentRedirect(context.Background(), data)
require.NoError(t, err)
assert.NotEmpty(t, url)
assert.False(t, ts.paymentRedirectHasIdempotencyKey)

url, err = ac.SubscriptionPaymentRedirectURL(context.Background(), data)
require.NoError(t, err)
assert.NotEmpty(t, url)
assert.False(t, ts.subscriptionPaymentRedirectHasIdempotencyKey)
}

func TestPaymentRedirectRequiresRedirectURL(t *testing.T) {
ac, ts := newTestClient(t)
ts.paymentRedirectResponse = map[string]string{"status": "error", "error": "try again later"}

url, err := ac.PaymentRedirect(context.Background(), PaymentRedirectData{
Provider: "stripe",
Plan: "pro",
DeviceName: "test-device",
})
require.Error(t, err)
assert.Empty(t, url)
}

func TestNewUser(t *testing.T) {
Expand Down
25 changes: 20 additions & 5 deletions account/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@ import (

// testServer holds server-side SRP state for the mock auth server.
type testServer struct {
salt map[string][]byte
verifier []byte
cache map[string]string
salt map[string][]byte
verifier []byte
cache map[string]string
paymentRedirectIdempotencyKey string
paymentRedirectHasIdempotencyKey bool
subscriptionPaymentRedirectIdempotencyKey string
subscriptionPaymentRedirectHasIdempotencyKey bool
paymentRedirectResponse any
}

func writeProtoResponse(w http.ResponseWriter, msg proto.Message) {
Expand Down Expand Up @@ -180,11 +185,21 @@ func newTestServer(t *testing.T) (*httptest.Server, *testServer) {
})

mux.HandleFunc("/subscription-payment-redirect", func(w http.ResponseWriter, r *http.Request) {
writeJSONResponse(w, map[string]string{"Redirect": "https://example.com/redirect"})
values := r.URL.Query()
state.subscriptionPaymentRedirectIdempotencyKey = values.Get("idempotencyKey")
_, state.subscriptionPaymentRedirectHasIdempotencyKey = values["idempotencyKey"]
writeJSONResponse(w, map[string]string{"redirect": "https://example.com/redirect"})
})

mux.HandleFunc("/payment-redirect", func(w http.ResponseWriter, r *http.Request) {
writeJSONResponse(w, map[string]string{"Redirect": "https://example.com/redirect"})
values := r.URL.Query()
state.paymentRedirectIdempotencyKey = values.Get("idempotencyKey")
_, state.paymentRedirectHasIdempotencyKey = values["idempotencyKey"]
resp := state.paymentRedirectResponse
if resp == nil {
resp = map[string]string{"redirect": "https://example.com/redirect"}
}
writeJSONResponse(w, resp)
})

mux.HandleFunc("/stripe-subscription", func(w http.ResponseWriter, r *http.Request) {
Expand Down
Loading
Loading