singleserve

package module
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 22 Imported by: 0

README

Singleserve

Singleserve is a lightweight Go toolkit for single-binary, browser-based local applications. It provides the local process boundary—loopback HTTP hosting, per-launch authentication, browser startup, browser-presence tracking, lifetime policy, shutdown guards, and graceful drain—while applications keep ownership of their router, business logic, frontend framework, and embedded assets.

[!IMPORTANT] v0.2.x is the current supported release line. v0.2.0 introduced the hardened browser-authentication contract; use v0.2.1 or later for Windows-compatible programmatic clients. See the v0.2 API specification, v0.1-to-v0.2 migration guide, release governance, and roadmap.

Intended use

Singleserve targets local applications that:

  • ship as one Go binary;
  • serve a browser UI from a loopback-only HTTP listener;
  • need a fresh authentication secret for every process launch;
  • may live until explicitly stopped or until their browser tabs disconnect;
  • need application-specific vetoes before a user- or browser-driven shutdown; and
  • do not want a desktop webview, hosted account system, or prescribed frontend stack.

It is not a router, asset pipeline, desktop shell, remote-access server, application framework, or business-logic layer.

Install

Add the released module to an application:

go get github.com/rztaylor/singleserve@v0.2.1

Runnable example

Run the executable specification from the repository root:

go run ./examples/minimal

It starts a browser-bound loopback server and opens a live lifecycle dashboard. The page shows each heartbeat and its timestamp, the next-heartbeat countdown, consecutive failures, a manual health probe, backend-loss detection timing, and the normal/fallback tab-close shutdown windows. Shutting down or losing the backend moves the page into a terminal state and attempts to close the tab; if the browser blocks script closure, the page clearly asks the user to close it. Normal output identifies only the non-secret listener address. If browser opening fails, the command prints the authenticated URL once for manual opening. Ctrl+C remains an owner-forced shutdown path.

Usage

The public Go API lives in the root package:

import "github.com/rztaylor/singleserve"

The startup flow is deliberately explicit:

srv, err := singleserve.New(singleserve.Options{
    Handler:  appRouter,
    Lifetime: singleserve.BrowserBoundLifetime(),
    Guard: singleserve.ShutdownGuardFunc(func(ctx context.Context, request singleserve.ShutdownRequest) error {
        if workIsRunning() {
            return singleserve.DenyShutdown("work_running", "Wait for the current operation to finish")
        }
        return nil
    }),
})
if err != nil {
    return err
}

launch, err := srv.Start(ctx)
if err != nil {
    return err
}
if err := launch.OpenBrowser(ctx); err != nil {
    manualURL, renewErr := launch.NewBootstrapURL()
    if renewErr != nil {
        return renewErr
    }
    fmt.Fprintln(os.Stderr, "Open this URL within two minutes:", manualURL)
}
result, err := launch.Wait()

Start binds before returning. OpenBrowser is optional. If it fails, NewBootstrapURL invalidates the earlier unconsumed capability and returns a fresh two-minute, one-time manual URL. Renewal does not extend or change the server's lifetime policy, and an already established browser session remains valid. Wait returns only after the server has drained or reports a serving/drain error.

The application can import the dependency-free browser client directly from the running server:

<script type="module">
  import { connect } from "/_singleserve/client.js";

  const session = await connect({
    onServerUnavailable: () => showRecoveryUI(),
  });

  const response = await session.fetch("/api/items");
</script>

In v0.2, launch.URL() opens a fixed Singleserve bootstrap page on a high-entropy per-launch .localhost origin. Its two-minute, one-time capability is carried only in the URL fragment, scrubbed before the network exchange, and replaced with a clean application URL before consumer content runs. Owner code may call launch.NewBootstrapURL() when it needs to replace an unconsumed or expired manual URL; consumers remain responsible for deciding whether their own interface exposes that recovery action. Reloads and new tabs use only a host-isolated, HttpOnly, Secure, SameSite=Strict, __Host- session cookie. The lifecycle client never reads or writes Web Storage and exposes no authentication capability.

Security notice for v0.1.0: its browser client stores the launch capability in script-readable sessionStorage and exposes it as session.token. That release is superseded by v0.2, which removes the weaker behavior rather than retaining a compatibility mode; see the migration guide.

Programmatic callers in v0.2 use a launch-bound client instead of parsing a browser URL:

response, err := launch.Client().Get(launch.BaseURL() + "api/items")

The client carries an independent private credential, bypasses environment proxies, refuses requests or redirects outside the exact launch origin, and dials the bound loopback listener directly so programmatic use does not depend on operating-system .localhost DNS behavior.

Design boundaries

  • Applications pass an http.Handler; Singleserve wraps it but never constructs or mutates the application router.
  • The reserved /_singleserve/ endpoints are the only HTTP routes owned by the toolkit.
  • Authentication, browser state, and lifecycle state are process-local and never persisted.
  • The framework-neutral browser client is a plain ES module with no npm or UI-framework dependency.
  • The runtime implementation is standard-library-only.
  • Non-loopback listeners, TLS, remote access, multi-user identity, WebSockets, and application persistence remain outside v0.2.

Repository map

Licence

Singleserve uses the MIT License. The choice is intended to keep adoption simple for open-source and proprietary local applications while retaining the standard warranty disclaimer.

Documentation

Overview

Package singleserve provides the local HTTP, browser authentication, browser-presence, and graceful-shutdown boundary for single-binary browser applications while leaving routing, assets, and business logic to callers.

A Server wraps an application-owned net/http.Handler, binds only to loopback, and produces a short-lived browser bootstrap URL on an isolated per-launch origin. Trusted owner code can rotate that URL with Launch.NewBootstrapURL. Browser authentication becomes an HttpOnly session before application content runs; programmatic callers use Launch.Client. Applications choose an explicit or browser-bound lifetime and may supply a shutdown guard.

Example
package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/rztaylor/singleserve"
)

func main() {
	server, err := singleserve.New(singleserve.Options{
		Handler:  http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprint(w, "hello") }),
		Lifetime: singleserve.ExplicitLifetime(),
		Opener:   singleserve.BrowserOpenerFunc(func(context.Context, string) error { return nil }),
	})
	if err != nil {
		panic(err)
	}

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	_, err = server.Start(ctx)
	fmt.Println(err)
}
Output:
context canceled

Index

Examples

Constants

View Source
const (
	// TokenHeader carries the private programmatic credential applied by
	// Launch.Client. Browser clients do not send it.
	TokenHeader = "X-Singleserve-Token"
	// TabHeader identifies one browser tab instance.
	TabHeader = "X-Singleserve-Tab"
	// ControlPath is reserved for Singleserve lifecycle endpoints.
	ControlPath = "/_singleserve/"
)

Variables

View Source
var ErrAlreadyStarted = errors.New("singleserve: server already started")

ErrAlreadyStarted reports an attempt to start a single-use Server twice.

View Source
var ErrBrowserAlreadyOpened = errors.New("singleserve: browser already opened")

ErrBrowserAlreadyOpened reports an attempt to open one Launch twice.

View Source
var (

	// ErrTooManyTabs reports that the bounded tab registry has no reclaimable entries.
	ErrTooManyTabs = errors.New("singleserve: too many connected tabs")
)

Functions

func DenyShutdown

func DenyShutdown(code, message string) error

DenyShutdown creates a typed shutdown veto. Invalid codes or empty messages are treated as internal guard failures when evaluated.

Types

type BrowserOpener

type BrowserOpener interface {
	Open(context.Context, string) error
}

BrowserOpener opens a URL without waiting for the browser to exit.

type BrowserOpenerFunc

type BrowserOpenerFunc func(context.Context, string) error

BrowserOpenerFunc adapts a function to BrowserOpener.

func (BrowserOpenerFunc) Open

func (f BrowserOpenerFunc) Open(ctx context.Context, rawURL string) error

Open calls f.

type Launch

type Launch struct {
	// contains filtered or unexported fields
}

Launch is the active view of a started Server.

func (*Launch) Address

func (l *Launch) Address() string

Address returns the assigned listener address.

func (*Launch) BaseURL

func (l *Launch) BaseURL() string

BaseURL returns the listener URL without authentication material.

func (*Launch) Client added in v0.2.0

func (l *Launch) Client() *http.Client

Client returns an HTTP client authenticated only for this launch origin. The client bypasses environment proxies, dials the bound listener directly, and never exposes its credential.

func (*Launch) NewBootstrapURL added in v0.2.0

func (l *Launch) NewBootstrapURL() (string, error)

NewBootstrapURL invalidates any earlier unconsumed bootstrap capability and returns a fresh one-time browser URL that expires after two minutes. It does not change the launch lifetime or invalidate an established browser session. Treat the complete URL as a secret.

func (*Launch) OpenBrowser

func (l *Launch) OpenBrowser(ctx context.Context) error

OpenBrowser opens URL with the configured platform opener.

func (*Launch) Shutdown

func (l *Launch) Shutdown(ctx context.Context) error

Shutdown forces owner-controlled graceful shutdown and waits for completion or for ctx to be canceled. It bypasses the application guard.

func (*Launch) URL

func (l *Launch) URL() string

URL returns the authenticated browser bootstrap URL. Treat it as a secret.

func (*Launch) Wait

func (l *Launch) Wait() (Result, error)

Wait blocks until the HTTP server has fully stopped.

type LifetimeMode

type LifetimeMode uint8

LifetimeMode controls whether browser absence can stop the server.

const (
	// LifetimeExplicit stops only on parent cancellation, owner shutdown, or an
	// authenticated browser shutdown request.
	LifetimeExplicit LifetimeMode = iota
	// LifetimeBrowserBound also stops when first contact never arrives or every
	// contacted browser tab disconnects.
	LifetimeBrowserBound
)

type LifetimePolicy

type LifetimePolicy struct {
	Mode                LifetimeMode
	FirstContactTimeout time.Duration
	HeartbeatTimeout    time.Duration
	DisconnectGrace     time.Duration
	CheckInterval       time.Duration
}

LifetimePolicy configures browser-presence shutdown behavior.

func BrowserBoundLifetime

func BrowserBoundLifetime() LifetimePolicy

BrowserBoundLifetime returns the recommended browser-owned policy.

func ExplicitLifetime

func ExplicitLifetime() LifetimePolicy

ExplicitLifetime returns the safe zero-value lifetime policy.

type Options

type Options struct {
	Handler  http.Handler
	Address  string
	Lifetime LifetimePolicy
	Guard    ShutdownGuard
	Opener   BrowserOpener
}

Options configures one single-use local server.

type Result

type Result struct {
	Reason    ShutdownReason
	StartedAt time.Time
	StoppedAt time.Time
}

Result describes the completed server lifetime.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server owns one authenticated loopback listener and its lifecycle state. A Server is single-use.

func New

func New(options Options) (*Server, error)

New validates options and prepares a single-use Server.

func (*Server) Start

func (s *Server) Start(ctx context.Context) (*Launch, error)

Start binds the configured listener and starts serving before returning.

func (*Server) Tabs

func (s *Server) Tabs() []TabSnapshot

Tabs returns a time-ordered copy of the current tab registry.

type ShutdownDeniedError

type ShutdownDeniedError struct {
	Code    string
	Message string
}

ShutdownDeniedError is a safe, expected application veto.

func (*ShutdownDeniedError) Error

func (e *ShutdownDeniedError) Error() string

type ShutdownGuard

type ShutdownGuard interface {
	CheckShutdown(context.Context, ShutdownRequest) error
}

ShutdownGuard can veto browser-initiated or browser-bound shutdown.

type ShutdownGuardFunc

type ShutdownGuardFunc func(context.Context, ShutdownRequest) error

ShutdownGuardFunc adapts a function to ShutdownGuard.

func (ShutdownGuardFunc) CheckShutdown

func (f ShutdownGuardFunc) CheckShutdown(ctx context.Context, request ShutdownRequest) error

CheckShutdown calls f.

type ShutdownReason

type ShutdownReason string

ShutdownReason records why graceful shutdown began.

const (
	ShutdownBrowserRequest      ShutdownReason = "browser_request"
	ShutdownBrowserDisconnected ShutdownReason = "browser_disconnected"
	ShutdownFirstContactTimeout ShutdownReason = "first_contact_timeout"
	ShutdownProgrammatic        ShutdownReason = "programmatic"
	ShutdownContextCanceled     ShutdownReason = "context_canceled"
	ShutdownServerError         ShutdownReason = "server_error"
)

type ShutdownRequest

type ShutdownRequest struct {
	Reason ShutdownReason
	Tabs   []TabSnapshot
}

ShutdownRequest is the server-built input to an application shutdown guard.

type TabSnapshot

type TabSnapshot struct {
	ID             string
	State          TabState
	ConnectedAt    time.Time
	LastSeenAt     time.Time
	DisconnectedAt time.Time
}

TabSnapshot is an immutable copy of tracked browser presence.

type TabState

type TabState string

TabState is the current server-side state of one browser tab instance.

const (
	TabConnected    TabState = "connected"
	TabDisconnected TabState = "disconnected"
	TabExpired      TabState = "expired"
)

Directories

Path Synopsis
examples
minimal command
Command minimal is an executable specification of Singleserve's public API.
Command minimal is an executable specification of Singleserve's public API.
internal
testbrowserjar
Package testbrowserjar provides browser-like host-only cookie behavior for repository HTTP tests.
Package testbrowserjar provides browser-like host-only cookie behavior for repository HTTP tests.
testtransport
Package testtransport provides browser-like loopback name resolution for repository HTTP tests.
Package testtransport provides browser-like loopback name resolution for repository HTTP tests.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL