v0.2.0 41cf2ec

api.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3// Package rpc serves the ermite API contract (bnema.dev/ermite-api) with
4// ConnectRPC: handlers that map the messages of the contract to the use cases
5// of app and back, the interceptor that authenticates callers, and the error
6// mapping. The same handlers serve remote clients over HTTP and the web in
7// process (see InProcessClient).
8package rpc
9
10import (
11	"context"
12	"net/http"
13	"net/netip"
14
15	"connectrpc.com/connect"
16	"github.com/bnema/zerowrap"
17
18	"bnema.dev/ermite-api/gen/ermite/ermiteconnect"
19	"bnema.dev/ermite/internal/app"
20	"bnema.dev/ermite/internal/clientlimit"
21	"bnema.dev/ermite/internal/domain"
22)
23
24// MaxRequestBytes bounds the size of a request message. Responses are not
25// bounded: a diff has its own limits and blobs are streamed.
26const MaxRequestBytes = 1 << 20
27
28// Features are the optional capabilities of the contract that this server
29// implements, as GetVersion lists them.
30var Features = []string{"repo.update", "events.resume", "merge_requests", "signing_key", "session"}
31
32// Authenticator turns a token secret into a principal, or
33// domain.ErrUnauthenticated. app.Auth fits.
34type Authenticator interface {
35	Authenticate(ctx context.Context, secret string) (domain.Principal, error)
36}
37
38// Config configures the API.
39type Config struct {
40	Auth   Authenticator
41	Repos  *app.Repos
42	Tokens *app.Tokens
43	Browse *app.Browse
44	Events *app.Events
45	// MergeRequests serves MergeRequestService, SigningKey GetSigningKey.
46	MergeRequests *app.MergeRequests
47	SigningKey    *app.SigningKey
48	Version       string
49	// TrustedProxies are the networks allowed to set X-Forwarded-For.
50	TrustedProxies []netip.Prefix
51	// Limiter counts failed authentications; nil builds a default one. Share
52	// it with the other adapters that authenticate, so that a client has one
53	// budget of failures.
54	Limiter *clientlimit.Limiter
55	// Sessions are the owner's web sessions; nil serves no SessionService.
56	Sessions *app.Sessions
57}
58
59// API is the set of Connect handlers of the contract.
60type API struct {
61	handler http.Handler
62}
63
64// New builds the API.
65func New(cfg Config) *API {
66	if cfg.Limiter == nil {
67		cfg.Limiter = clientlimit.NewLimiter(nil)
68	}
69	opts := []connect.HandlerOption{
70		connect.WithInterceptors(newInterceptor(cfg)),
71		connect.WithReadMaxBytes(MaxRequestBytes),
72		connect.WithRecover(func(ctx context.Context, spec connect.Spec, _ http.Header, panicValue any) error {
73			log := zerowrap.FromCtx(ctx)
74			log.Error().Interface("panic", panicValue).Str("procedure", spec.Procedure).Msg("api handler panicked")
75			return connect.NewError(connect.CodeInternal, errInternal)
76		}),
77	}
78	mux := http.NewServeMux()
79	mux.Handle(ermiteconnect.NewRepoServiceHandler(&repoService{cfg.Repos}, opts...))
80	mux.Handle(ermiteconnect.NewGitServiceHandler(&gitService{cfg.Browse}, opts...))
81	mux.Handle(ermiteconnect.NewTokenServiceHandler(&tokenService{cfg.Tokens}, opts...))
82	mux.Handle(ermiteconnect.NewEventServiceHandler(&eventService{cfg.Events}, opts...))
83	mux.Handle(ermiteconnect.NewServerServiceHandler(&serverService{version: cfg.Version, key: cfg.SigningKey}, opts...))
84	mux.Handle(ermiteconnect.NewMergeRequestServiceHandler(&mergeRequestService{cfg.MergeRequests}, opts...))
85	if cfg.Sessions != nil {
86		mux.Handle(ermiteconnect.NewSessionServiceHandler(&sessionService{cfg.Sessions}, opts...))
87	}
88	return &API{handler: mux}
89}
90
91// Handler returns the handler of all services. Its paths are the procedure
92// paths of the contract ("/ermite.RepoService/ListRepos"); httpserver mounts
93// it under /-/api/ and strips the prefix.
94func (a *API) Handler() http.Handler { return a.handler }
95
96// caller returns the principal the interceptor (or the in-process caller)
97// put in ctx; anonymous when there is none.
98func caller(ctx context.Context) domain.Principal {
99	p, _ := domain.PrincipalFromContext(ctx)
100	return p
101}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 44

↑↓ moveenter openesc close