tinyclient

package
v0.0.0-...-9268282 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 38 Imported by: 0

README

tinyclient

tinyclient is the relay website. It includes the HTML templates, styles, JavaScript modules and embedded browser bundles used by the integrated tiny backend. Pages render on the server and remain readable without JavaScript. Signing, encrypted messages, uploads and live updates use browser enhancements.

Build and run

Run these commands from the repository root:

go build -o tinyclient-server ./cmd/tinyclient
./tinyclient-server \
  -listen 127.0.0.1:8081 \
  -backend http://127.0.0.1:7447 \
  -public-url https://relay.example

The backend must be the combined tiny host (tiny serve) from this repository. Set its --public-url to the same https://relay.example address. Route that public origin to tinyclient through your HTTPS reverse proxy, preserving the Host header. Keep the backend listener on the private network. The frontend renders pages and assets locally and forwards relay APIs, WebSocket connections, streams, signed requests and mutations to the backend.

For a path-based tenant, include the same path in both URLs:

./tinyclient-server \
  -backend http://127.0.0.1:7447/r/team \
  -public-url https://relay.example/r/team

Each process serves one tenant. The root path and /r/<name> are supported; arbitrary mount paths and different frontend and backend tenant paths are not. A root-mounted frontend rejects /r/<name> requests before rendering or forwarding them. A prefixed frontend accepts only its configured tenant path. The default listener is 127.0.0.1:8081. Run tinyclient-server -help for flags.

The binary embeds its assets and needs no working directory, SQLite database, Git installation or relay signing key at runtime. It requires a running backend for relay data and session validation. Embedded scripts and icons remain available when the backend is unavailable. Failure to load the relay snapshot returns HTTP 502 rather than serving cached private data. It is not an offline client or a frontend for an arbitrary Nostr relay.

Authentication

Keep the relay's public origin and tenant path unchanged when splitting the processes. NIP-98 signatures bind the exact URL, method and body. Tinyclient does not re-sign requests, accept a public key as authentication or translate proofs between origins. Incoming requests for a different public Host are rejected. Configure the relay's HTTPS public URL even when the private hop uses HTTP so it verifies the original scheme and issues secure cookies.

Browser sessions remain owned, checked and revoked by the relay. The frontend loads the presentation snapshot through the read-only contract at /api/tinyclient using the browser's session cookie. Anonymous method and typed reads omit that cookie, including public article feeds requested by signed-in users. Authenticated reads require the requested actor to match the snapshot's authenticated actor. The relay derives the actor from the session, checks access on data reads and redacts private relay metadata. Owner-only settings are not included in anonymous or nonowner snapshots. Signed operations and relay protocol traffic retain their authorization headers, paths, query strings and bodies as they pass upstream. Signed page requests are handled by the integrated backend renderer rather than replaying a signature across several reads.

Private pages and authenticated no-JavaScript navigation are supported with a valid browser session. Creating that session still requires a browser signer. Pointing the frontend at a different public origin is not a supported migration path for existing sessions or signed URLs.

Embed

Import github.com/FelineStateMachine/tinyrelay/tinyclient and call New with a Backend and Options. Public Policy, Features, CustomView and room types let another package implement the interfaces without importing relay-internal types. DefaultPolicy supplies the normal feature settings.

Backend.Query owns authorization for method calls. Options.Actor resolves authenticated requests; it must verify a session or proof, never a caller's claimed public key. Optional RoomsReader, PrivateReader, AccountPreferencesReader, CustomViewSource and ChatActivityReader interfaces provide typed room reads, membership checks and richer page data. The remote adapter preserves typed room summaries, folded replies, reaction counts, activity cards and account preferences.

App.BackendHandler() exposes the read contract for an embedded data service. App.ServeHTTP mounts it automatically. NewRemote returns the standalone HTTP handler for use in another executable.

The /api/tinyclient contract is a project-specific, read-only HTTP extension, not a Nostr NIP. See the read-adapter specification for request forms, authorization, errors and compatibility. GET without a method returns the authorized presentation snapshot. method and JSON-array params select an allowlisted backend read; op=rooms|room|thread|activity selects a typed read with its room, root, cursor and limit parameters. Every call resolves authentication at the tinyrelay backend. This adapter introduces no event kinds, signing formats or storage identifiers. Frontend validation improves feedback; the backend remains the authority for protocol validation, membership and mutation permissions.

Test and rebuild browser bundles

go test ./tinyclient ./cmd/tinyclient
go test ./internal/daemon -run TestStandaloneTinyclient
npm ci
npm run test:tinyclient
npm run build:tinyclient

Go builds use the checked-in browser bundles, so Node.js is not required to build or run the binary. Rebuild the bundles when their JavaScript sources or pinned dependencies change. Bundle verification compares regenerated bytes with the checked-in assets.

Documentation

Overview

Package tinyclient renders relay HTML pages and browser support modules.

App owns the HTTP surface, templates, page data and browser assets. It asks Backend for relay identity, policy and method results, and uses ActorResolver to associate a request with the authenticated public key. The daemon owns relay policy and relay writes.

New embeds the client with a local Backend. NewRemote renders against a relay's authenticated HTTP backend contract and proxies data protocols.

Pages use a shared shell in page.html. The shell supplies the navigation rail, content column, contextual panel and command footer. Each page template supplies one content section and remains readable when scripts are disabled. JavaScript modules progressively add signing, navigation, forms, components, room streams, file transfers and WebMCP controls.

Index

Constants

View Source
const BackendPath = "/api/tinyclient"

BackendPath is the read-only adapter endpoint mounted by App. It uses the relay's normal actor resolver; a client-supplied public key is never trusted.

Variables

This section is empty.

Functions

func NewRemote

func NewRemote(options RemoteOptions) (http.Handler, error)

NewRemote serves embedded templates and assets locally and forwards relay protocols, signed requests and mutations upstream. It does not open a database, own relay keys or start relay workers.

Types

type AccountPreferencesReader

type AccountPreferencesReader interface {
	SharePresence(context.Context, string) bool
}

AccountPreferencesReader provides private, account-scoped preferences to server-rendered account pages. It is optional for small embedders.

type ActorResolver

type ActorResolver func(request *http.Request) (string, error)

ActorResolver returns the authenticated public key associated with request. An empty key represents an anonymous browser request.

type App

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

func New

func New(backend Backend, options Options) (*App, error)

func (*App) BackendHandler

func (a *App) BackendHandler() http.Handler

BackendHandler exposes the read contract without rendering HTML. Mount it at BackendPath when embedding the client separately from its data service. Query authorization remains the responsibility of Backend.Query.

func (*App) ServeHTTP

func (a *App) ServeHTTP(writer http.ResponseWriter, request *http.Request)

type Backend

type Backend interface {
	Query(ctx context.Context, method string, params []json.RawMessage, actor string) (any, error)
	Policy() Policy
	URL() string
	Slug() string
	Identity() string
}

Backend supplies relay identity and the method calls used by page handlers. Query receives the request context, method name, JSON arguments and actor public key, and returns the method's JSON-shaped result. Policy, URL, slug and identity describe the relay rendered into each page.

type ChatActivityReader

type ChatActivityReader interface {
	ReadChatActivity(context.Context, string, string, string) (any, error)
}

type CustomHost

type CustomHost = policy.CustomHost

type CustomView

type CustomView = views.View

CustomView describes a fenced-block renderer offered by the backend.

type CustomViewSource

type CustomViewSource interface {
	CustomViews() []CustomView
}

CustomViewSource is the optional backend summary of the custom views the renderers show in place of fenced code blocks: names and languages only.

type Delivery

type Delivery = policy.Delivery

type Features

type Features = policy.Features

type FileLimits

type FileLimits = policy.FileLimits

type Inbox

type Inbox = policy.Inbox

type MemberInvites

type MemberInvites = policy.MemberInvites

type NameDirectory

type NameDirectory interface {
	// NameFor returns the member name for a pubkey, or "" when it has none.
	NameFor(ctx context.Context, pubkey string) string
	// PubKeyFor returns the pubkey behind a member name, or "" when unknown.
	PubKeyFor(ctx context.Context, name string) string
}

NameDirectory resolves relay member names for URLs. It is optional; a backend without it writes and reads hex pubkeys only.

type Notify

type Notify = policy.Notify

type Options

type Options struct {
	Actor ActorResolver
	QR    QRProvider
	// Version and Revision identify the relay build on the health page.
	Version, Revision string
}

type PageData

type PageData struct {
	Activity      chatActivityPage
	SharePresence bool
	Version       string
	Revision      string
	Title         string
	URL           string
	Slug          string
	Identity      string
	Policy        policy.Policy
	Actor         string
	Owner         bool
	Tab           string
	Notice        string
	Error         string
	Methods       []string
	Event         any
	Invite        string
	View          string
	Feed          []any
	Base          string
	Query         url.Values
	Private       bool
	Path          string
	Readme        template.HTML
	Tree          []any
	// Merge is the browsewikimerge detail shown by a wiki page compare view.
	Merge any
	// Connections lists the ways to open this relay in client apps that the
	// viewer may see, with link placeholders already resolved.
	Connections []any
	// Rooms lists the rooms the viewer may see; the rooms rail renders it.
	Rooms []any
	// Callbacks lists the event callbacks shown beside each agent.
	Callbacks []any
	// CustomViews lists the owner's custom views on the Views page.
	CustomViews []any
	// Requests lists the access requests on the People page, pending first;
	// Review is true when the viewer may decide them.
	Requests []any
	Review   bool
}

type Policy

type Policy = policy.Policy

Policy contains the relay settings displayed by the client. These aliases let embedders implement Backend without importing a relay-internal package.

func DefaultPolicy

func DefaultPolicy(owner string) Policy

DefaultPolicy returns the standard presentation and feature settings.

type PrivateReader

type PrivateReader interface {
	ReadAllowed(context.Context, string) error
}

PrivateReader is implemented by the daemon so web pages use the same membership check as websocket and HTTP data paths. It is optional to keep webui usable with small embedders; those embedders get owner-only fallback.

type QRProvider

type QRProvider func(text string) ([]byte, error)

QRProvider encodes text as an image for pages that show a connection QR.

type RemoteOptions

type RemoteOptions struct {
	BackendURL string
	PublicURL  string
	// Transport optionally supplies TLS roots or other HTTP transport settings.
	Transport http.RoundTripper
}

RemoteOptions connects a standalone renderer to one relay tenant. PublicURL is the browser-facing URL; BackendURL is its upstream HTTP address. Use the same tenant path at both addresses so cookies and signed URLs retain scope.

type RetentionRule

type RetentionRule = policy.RetentionRule

type RoomList

type RoomList struct {
	// Rooms contains visible room summaries in ascending ID order.
	Rooms []RoomSummary `json:"rooms"`
	// NextCursor is the last room ID when another page is available.
	NextCursor string `json:"next_cursor,omitempty"`
}

type RoomMember

type RoomMember struct {
	PubKey   string `json:"pubkey"`
	Role     string `json:"role,omitempty"`
	Agent    bool   `json:"agent,omitempty"`
	Operator string `json:"operator,omitempty"`
	AddedAt  int64  `json:"added_at,omitempty"`
}

type RoomMessage

type RoomMessage = nostr.Event

type RoomPage

type RoomPage struct {
	Room     RoomSummary   `json:"room"`
	Members  []RoomMember  `json:"members,omitempty"`
	Messages []RoomMessage `json:"messages,omitempty"`
	// ReplyCounts and ReactionCounts carry folded interaction summaries for
	// the roots in Messages without expanding every reply into the timeline.
	ReplyCounts    map[string]int            `json:"reply_counts,omitempty"`
	ReactionCounts map[string]map[string]int `json:"reaction_counts,omitempty"`
	Replies        []RoomMessage             `json:"replies,omitempty"`
	Edits          []RoomMessage             `json:"edits,omitempty"`
	Root           *RoomMessage              `json:"root,omitempty"`
	NextCursor     string                    `json:"next_cursor,omitempty"`
}

RoomPage is the typed result for a room or thread read. A room read fills Members and Messages; a thread read fills Root and Replies. Edits contains the newest visible edit for the returned targets.

type RoomSummary

type RoomSummary struct {
	ID            string `json:"id"`
	Name          string `json:"name,omitempty"`
	Access        string `json:"access,omitempty"`
	About         string `json:"about,omitempty"`
	Picture       string `json:"picture,omitempty"`
	CreatedBy     string `json:"created_by,omitempty"`
	CreatedAt     int64  `json:"created_at,omitempty"`
	EventID       string `json:"event_id,omitempty"`
	DeletedAt     int64  `json:"deleted_at,omitempty"`
	Members       int    `json:"members,omitempty"`
	LastMessageAt int64  `json:"last_message_at,omitempty"`
	Role          string `json:"role,omitempty"`
}

RoomSummary is the room metadata shown in the rail and room header.

type RoomsReader

type RoomsReader interface {
	// ListRooms returns up to limit visible room summaries after cursor.
	ListRooms(ctx context.Context, actor, cursor string, limit int) (RoomList, error)
	// ReadRoom returns one visible room, its members, messages and edits after cursor.
	ReadRoom(ctx context.Context, actor, roomID, cursor string, limit int) (RoomPage, error)
	// ReadThread returns one room thread root, replies and edits after cursor.
	ReadThread(ctx context.Context, actor, roomID, rootID, cursor string, limit int) (RoomPage, error)
}

RoomsReader supplies the typed reads used by room pages. actor is the authenticated public key, or an empty string for an anonymous request. cursor is empty for the newest page and otherwise is the value returned in NextCursor. limit requests at most that many rows. The daemon adapter uses 100 when the requested limit falls outside 1 through 100.

ListRooms returns visible room summaries ordered by room ID and a cursor for the next page. ReadRoom returns the room summary, visible members, newest room messages, message edits and a cursor for older messages. ReadThread returns the room summary, one root event, its replies, edits and a cursor for older replies. Reads enforce tenant and room access and return errors for missing rooms, invalid cursors or an inaccessible room. ReadThread also returns an error when rootID is not a lower-case hexadecimal event ID or does not identify a message in roomID.

type Sites

type Sites = policy.Sites

type Succession

type Succession = policy.Succession

Jump to

Keyboard shortcuts

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