detached 388d3be

services.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package ports
4
5import (
6	"context"
7	"errors"
8	"io"
9	"time"
10
11	"bnema.dev/ermite/internal/domain"
12)
13
14// The interfaces below are the use cases as the inbound adapters (the API,
15// Git over HTTP, the web) and the composition root see them. The types of package
16// usecase implement them, each assertion being made in usecase; the mocks in
17// ports/mocks stand in for them in the tests of the adapters.
18
19// AuthService turns the secret of a request into a principal. It returns
20// domain.ErrUnauthenticated for a secret that is not valid. *usecase.Auth
21// implements it.
22type AuthService interface {
23	Authenticate(ctx context.Context, secret string) (domain.Principal, error)
24}
25
26// Revalidator confirms that a principal obtained earlier is still
27// authorized. *usecase.Auth implements it.
28type Revalidator interface {
29	Revalidate(ctx context.Context, p domain.Principal) error
30}
31
32// RepoLookup is the read side of a single repository. A repository the
33// principal may not read is domain.ErrNotFound. *usecase.Repos implements it, as
34// part of RepoService.
35type RepoLookup interface {
36	Get(ctx context.Context, p domain.Principal, name domain.RepoName) (domain.Repo, error)
37}
38
39// RepoService is the repository use cases the API serves. *usecase.Repos
40// implements it.
41type RepoService interface {
42	RepoLookup
43	ListPage(ctx context.Context, p domain.Principal, size int, token string) (RepoPage, error)
44	View(ctx context.Context, repo domain.Repo) (RepoView, error)
45	Create(ctx context.Context, p domain.Principal, params CreateRepoParams) (domain.Repo, error)
46	Update(ctx context.Context, p domain.Principal, name domain.RepoName, params UpdateRepoParams) (domain.Repo, error)
47	Delete(ctx context.Context, p domain.Principal, name domain.RepoName) error
48}
49
50// TokenService is the access token use cases the API serves. *usecase.Tokens
51// implements it.
52type TokenService interface {
53	Create(ctx context.Context, p domain.Principal, params CreateTokenParams) (CreatedToken, error)
54	ListPage(ctx context.Context, p domain.Principal, size int, token string) (TokenPage, error)
55	Revoke(ctx context.Context, p domain.Principal, id string) error
56}
57
58// GitHubAPIService is the subset of GitHub's REST API that ermite serves under
59// /-/github/api, for the actions that run on it (ADR 0025, CI-2). owner is the
60// owner of the URL: only ERMITE_OWNER names repositories, any other owner is
61// domain.ErrNotFound, as is a repository the principal may not read.
62// *usecase.CI implements it.
63type GitHubAPIService interface {
64	// GetRepo returns the JSON of GET /repos/{owner}/{repo}.
65	GetRepo(ctx context.Context, p domain.Principal, owner, repo string) ([]byte, error)
66	// HashAlgorithm returns the object format of the repository, "sha1".
67	HashAlgorithm(ctx context.Context, p domain.Principal, owner, repo string) (string, error)
68	// Tarball resolves ref (a branch, a tag or a commit id; empty means the
69	// default branch) and writes the gzip-compressed tar of the commit to the
70	// writer that begin returns; the archive token of an action (see
71	// domain.RepoTokenPrincipal) gets the commit it was issued for and no other.
72	// begin is called once, with the file name of the
73	// archive, before the first byte and not at all when the ref does not
74	// resolve (domain.ErrNotFound).
75	Tarball(ctx context.Context, p domain.Principal, owner, repo, ref string, begin func(filename string) io.Writer) error
76	// TarballName is Tarball for a HEAD request: the same access and the same
77	// resolution of ref, then the file name, without producing the archive.
78	TarballName(ctx context.Context, p domain.Principal, owner, repo, ref string) (string, error)
79	// ListPullFiles answers GET /repos/{owner}/{repo}/pulls/{number}/files: the
80	// JSON array of the files of page (1-based) of perPage files (1 to 100) of
81	// the merge request, and the Link header value ("" for a single page). A
82	// public repository is read by anyone; a private one by a principal that
83	// reads it. The job token of the repository needs pull-requests: read; one
84	// that reads the repository without it gets domain.ErrForbidden (GitHub: 403
85	// "Resource not accessible by integration").
86	ListPullFiles(ctx context.Context, p domain.Principal, owner, repo string, number int64, page, perPage int) (body []byte, link string, err error)
87}
88
89// BrowseService is the read side of a repository's content. *usecase.Browse
90// implements it.
91type BrowseService interface {
92	ListRefs(ctx context.Context, p domain.Principal, repo domain.RepoName, params ListRefsParams) (RefPage, error)
93	ResolveRevision(ctx context.Context, p domain.Principal, repo domain.RepoName, rev domain.Revision) (domain.ResolvedRevision, error)
94	GetTree(ctx context.Context, p domain.Principal, repo domain.RepoName, rev domain.Revision, path domain.TreePath, includeLastCommit bool) (domain.Tree, error)
95	ListPaths(ctx context.Context, p domain.Principal, repo domain.RepoName, rev domain.Revision) (domain.PathList, error)
96	OpenBlob(ctx context.Context, p domain.Principal, repo domain.RepoName, rev domain.Revision, path domain.TreePath) (domain.BlobInfo, io.ReadCloser, error)
97	ListCommits(ctx context.Context, p domain.Principal, repo domain.RepoName, params ListCommitsParams) (HistoryPage, error)
98	GetCommit(ctx context.Context, p domain.Principal, repo domain.RepoName, rev domain.Revision) (domain.CommitDetail, error)
99	// RepoStats counts the commits of the branch whose head is given, and
100	// names the latest tag.
101	RepoStats(ctx context.Context, p domain.Principal, repo domain.RepoName, head domain.ObjectID, now time.Time) (RepoStats, error)
102}
103
104// EventService streams the events of the forge. *usecase.Events implements it.
105type EventService interface {
106	Watch(ctx context.Context, p domain.Principal, params WatchParams, send func(WatchEvent) error) error
107}
108
109// MergeRequestService is the merge request use cases. *usecase.MergeRequests
110// implements it.
111type MergeRequestService interface {
112	Create(ctx context.Context, p domain.Principal, name domain.RepoName, params CreateMergeRequestParams) (domain.MergeRequest, error)
113	List(ctx context.Context, p domain.Principal, name domain.RepoName, params ListMergeRequestsParams) (MergeRequestPage, error)
114	Get(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, opts GetMergeRequestOptions) (MergeRequestDetail, error)
115	AddComment(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params AddCommentParams) (domain.Comment, error)
116	EditComment(ctx context.Context, p domain.Principal, name domain.RepoName, number, commentID int64, text string) (domain.Comment, error)
117	DeleteComment(ctx context.Context, p domain.Principal, name domain.RepoName, number, commentID int64) (domain.Comment, error)
118	ResolveComment(ctx context.Context, p domain.Principal, name domain.RepoName, number, commentID int64, resolved bool) (domain.Comment, error)
119	Close(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.MergeRequest, error)
120	MarkRead(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, head domain.ObjectID) (domain.ObjectID, error)
121	Merge(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params MergeParams) (MergeResult, error)
122	// MarkReady takes a draft out of draft: its title loses its "WIP:" prefix.
123	MarkReady(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.MergeRequest, error)
124	// CountOpen is the number of open merge requests of a repository the
125	// principal can read. It reads no Git: the tab of every page shows it.
126	CountOpen(ctx context.Context, p domain.Principal, name domain.RepoName) (int, error)
127}
128
129// IssueService is the issue use cases. *usecase.Issues implements it.
130type IssueService interface {
131	Create(ctx context.Context, p domain.Principal, name domain.RepoName, params CreateIssueParams) (domain.Issue, error)
132	List(ctx context.Context, p domain.Principal, name domain.RepoName, params ListIssuesParams) (IssuePage, error)
133	// Get returns the issue with its comments, oldest first.
134	Get(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (IssueDetail, error)
135	AddComment(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, text string) (domain.IssueComment, error)
136	Close(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error)
137	Reopen(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error)
138}
139
140// IssueModerationService is the owner's moderation of the pending issues.
141// Every call needs owner rights and answers domain.ErrNotFound otherwise.
142// *usecase.IssueModeration implements it.
143type IssueModerationService interface {
144	// ListPending lists the pending issues of a repository, or of every
145	// repository when repo is zero, oldest first.
146	ListPending(ctx context.Context, p domain.Principal, repo domain.RepoName, page ListPendingParams) (PendingIssuePage, error)
147	// Approve opens the pending issue under a new number.
148	Approve(ctx context.Context, p domain.Principal, repo domain.RepoName, id domain.PendingIssueID) (domain.Issue, error)
149	// Reject deletes the pending issue and, with ban, bans its submitter.
150	Reject(ctx context.Context, p domain.Principal, repo domain.RepoName, id domain.PendingIssueID, ban bool) error
151}
152
153// IssueChallenge is what the public form needs to submit: a signed token, the
154// proof-of-work to solve and the timing.
155type IssueChallenge struct {
156	// Token is the signed challenge, sent back untouched.
157	Token string
158	// Nonce is the base64url of the 16 random bytes the browser hashes:
159	// SHA-256(nonce bytes || ":" || decimal counter) must have Difficulty
160	// leading zero bits.
161	Nonce string
162	// Difficulty is the number of leading zero bits required.
163	Difficulty int
164	// FormToken is a hidden field of the form, sent back untouched.
165	FormToken string
166	// IssuedAt, MinSubmitAt and ExpiresAt bound when the form may be sent.
167	IssuedAt, MinSubmitAt, ExpiresAt time.Time
168}
169
170// IssueSubmission is what the browser sends back.
171type IssueSubmission struct {
172	Token string
173	// Solution is the decimal counter that solves the proof of work.
174	Solution  string
175	FormToken string
176	// Honeypot must be empty: a field only a bot fills.
177	Honeypot            string
178	Title, Body, Author string
179}
180
181// IssueSubmissionService is the anonymous issue submission. Both calls answer
182// domain.ErrNotFound for a repository that is unknown, not public or without
183// public issues. Submit refuses with an error matching
184// domain.ErrSubmissionRefused (ErrSubmissionRateLimited, ErrSubmissionsPaused,
185// ErrSubmissionInvalid) or with a *domain.TextError the user can fix.
186// addr is the client key (clientlimit.Key of the address).
187// *usecase.IssueSubmissions implements it.
188type IssueSubmissionService interface {
189	Challenge(ctx context.Context, repo domain.RepoName, addr string) (IssueChallenge, error)
190	Submit(ctx context.Context, repo domain.RepoName, addr string, f IssueSubmission) error
191}
192
193// IssueNumberResolver tells whether a number of a repository names an issue or
194// a merge request, for the callers that follow a "#n" reference. *usecase.Issues
195// implements it.
196type IssueNumberResolver interface {
197	// Kind needs the access of reading the repository and answers
198	// domain.ErrNotFound otherwise, also when issues are off.
199	Kind(ctx context.Context, p domain.Principal, name domain.RepoName, n int64) (domain.NumberKind, error)
200}
201
202// InboxService is the owner's inbox: what is open across every repository.
203// *usecase.Inbox implements it.
204type InboxService interface {
205	// Overview needs owner rights and answers domain.ErrNotFound to anyone
206	// else. The filter, when not zero, restricts the lists to that repository.
207	Overview(ctx context.Context, p domain.Principal, filter domain.RepoName) (InboxOverview, error)
208}
209
210// SigningKeyService tells which key signs the commits of the forge.
211// *usecase.SigningKey implements it.
212type SigningKeyService interface {
213	Info() domain.SigningKeyInfo
214}
215
216// SessionService is the owner's web session use cases the API serves.
217// *usecase.Sessions implements it.
218type SessionService interface {
219	CreateLoginLink(ctx context.Context, p domain.Principal) (LoginLink, error)
220	SignOutEverywhere(ctx context.Context, p domain.Principal) error
221}
222
223// ErrLinkRejected is what WebSessionService.Login returns for a link that is
224// unknown, expired or already used.
225var ErrLinkRejected = errors.New("login link rejected")
226
227// WebSessionService is the session side of the use cases, as the web needs
228// it. The composition root implements it over *usecase.Sessions.
229type WebSessionService interface {
230	// LoginLinkValid reports whether the link is pending, without using it.
231	LoginLinkValid(nonce string) bool
232	// Login uses the link and returns the session token and how long it
233	// lasts, or ErrLinkRejected.
234	Login(ctx context.Context, nonce string) (token string, lifetime time.Duration, err error)
235	// Verify checks a session token. When it is valid it returns the owner,
236	// the principal the web passes to the use cases for that request; when it
237	// is not, ok is false and err nil. An error means the check could not be
238	// made (the request then runs as anonymous).
239	Verify(ctx context.Context, token string) (owner domain.Principal, ok bool, err error)
240}
241
242// PasskeyService is the owner's passkey use cases the API serves.
243// *usecase.Passkeys implements it. Registering a passkey and signing in with
244// one are browser ceremonies and are not part of it.
245type PasskeyService interface {
246	// List returns the passkeys, oldest first.
247	List(ctx context.Context, p domain.Principal) ([]domain.Passkey, error)
248	// Remove deletes the passkey with that credential id, or returns
249	// domain.ErrNotFound, and signs every browser out.
250	Remove(ctx context.Context, p domain.Principal, id []byte) error
251}
252
253// WebPasskeyService is the passkey side of the use cases, as the web needs
254// it: the API's List and Remove, and the browser ceremonies. The composition
255// root implements it over *usecase.Passkeys. A ceremony is the opaque
256// identifier BeginLogin and BeginRegistration return; options is the JSON the
257// browser passes to the WebAuthn API.
258type WebPasskeyService interface {
259	PasskeyService
260	// BeginLogin starts a sign-in. Anonymous callers may. client is the client
261	// key of the request (clientlimit.Key of the address the failure limiter
262	// uses); it bounds how many sign-ins a client keeps waiting.
263	BeginLogin(ctx context.Context, client string) (ceremony string, options []byte, err error)
264	// FinishLogin checks the browser's response and returns the session token
265	// and how long it lasts, or ErrLinkRejected for any refusal.
266	FinishLogin(ctx context.Context, ceremony string, response []byte) (token string, lifetime time.Duration, err error)
267	// BeginRegistration starts adding a passkey. Only the owner may. name is
268	// the name the passkey will get (empty means the default): a name that
269	// breaks the rules is refused here, before any ceremony starts, and is the
270	// one FinishRegistration stores.
271	BeginRegistration(ctx context.Context, p domain.Principal, name string) (ceremony string, options []byte, err error)
272	// FinishRegistration checks the browser's response and stores the passkey
273	// under the name given to BeginRegistration.
274	FinishRegistration(ctx context.Context, p domain.Principal, ceremony string, response []byte) error
275}
276
277// PushService opens a repository for a push: it is domain.ErrUnauthenticated
278// for an anonymous caller, domain.ErrNotFound for a repository the caller may
279// not write and cannot see (or that is missing and may not be created),
280// domain.ErrForbidden for one it can see but not write. With create, a
281// missing repository the principal may write is created. *usecase.Pushes
282// implements it.
283type PushService interface {
284	Open(ctx context.Context, p domain.Principal, name domain.RepoName, create bool) error
285}
286
287// PushHookService is what the hook socket asks of the use cases.
288// *usecase.Pushes implements it.
289type PushHookService interface {
290	// Check decides a push before Git applies it; an error matching
291	// domain.ErrRejected refuses it, any other error refuses it as a failure.
292	// objects are the objects of the push in progress (the quarantine of git).
293	Check(ctx context.Context, p domain.Principal, name domain.RepoName, updates []domain.RefUpdate, objects PushObjects) error
294	// Record handles an accepted push.
295	Record(ctx context.Context, p domain.Principal, name domain.RepoName, updates []domain.RefUpdate) error
296}
297
298// MaintenanceScheduler queues repository maintenance. *usecase.Maintainer
299// implements it.
300type MaintenanceScheduler interface {
301	Schedule(ctx context.Context, name domain.RepoName)
302}
303
304// HookTokens hands out the per-request tokens the hooks of a push present
305// back to ermite, and the socket they present them on. The Git HTTP adapter
306// implements it.
307type HookTokens interface {
308	// Register binds a new token to the repository and principal of a push.
309	// release revokes it and must be called when the request ends.
310	Register(ctx context.Context, repo domain.RepoName, p domain.Principal) (token string, release func())
311	// Socket is the path of the hook socket.
312	Socket() string
313}
314
315// FailureLimiter counts the failed authentications per client. The adapters
316// that authenticate share one, so that a client has one budget of failures.
317// *clientlimit.Limiter implements it.
318type FailureLimiter interface {
319	Blocked(ctx context.Context, addr string) (bool, time.Duration)
320	Fail(ctx context.Context, addr string)
321}
322
323// SubmissionLimiter limits the submissions of the public issue form per
324// client. *clientlimit.SubmissionLimiter implements it.
325type SubmissionLimiter interface {
326	// Reserve atomically checks the limits for addr and, if it may submit,
327	// counts a submission. When it may not, retryAfter says when to retry.
328	// release refunds the reservation (only when the submission was not
329	// stored); it is never nil and safe to call once.
330	Reserve(ctx context.Context, addr string) (ok bool, retryAfter time.Duration, release func())
331	// Recent returns the submissions of the last hour from addr and from all
332	// clients.
333	Recent(ctx context.Context, addr string) (perAddr, global int)
334}
335
336// SecretPage is one page of secrets.
337type SecretPage struct {
338	Secrets       []domain.Secret
339	NextPageToken string
340}
341
342// VariablePage is one page of variables.
343type VariablePage struct {
344	Variables     []domain.Variable
345	NextPageToken string
346}
347
348// EnvironmentPage is one page of environments.
349type EnvironmentPage struct {
350	Environments  []domain.Environment
351	NextPageToken string
352}
353
354// SecretService is the secret use cases the API serves. They need the
355// secrets:write scope, never on a token limited to branches. Values are never
356// returned. *usecase.CISettings implements it.
357type SecretService interface {
358	SetSecret(ctx context.Context, p domain.Principal, scope domain.SettingsScope, name, value string) (domain.Secret, error)
359	ListSecrets(ctx context.Context, p domain.Principal, scope domain.SettingsScope, size int, token string) (SecretPage, error)
360	DeleteSecret(ctx context.Context, p domain.Principal, scope domain.SettingsScope, name string) error
361}
362
363// VariableService is the variable use cases the API serves, with the access
364// rules of SecretService. *usecase.CISettings implements it.
365type VariableService interface {
366	SetVariable(ctx context.Context, p domain.Principal, scope domain.SettingsScope, name, value string) (domain.Variable, error)
367	GetVariable(ctx context.Context, p domain.Principal, scope domain.SettingsScope, name string) (domain.Variable, error)
368	ListVariables(ctx context.Context, p domain.Principal, scope domain.SettingsScope, size int, token string) (VariablePage, error)
369	DeleteVariable(ctx context.Context, p domain.Principal, scope domain.SettingsScope, name string) error
370}
371
372// EnvironmentService is the environment use cases the API serves, with the
373// access rules of SecretService. *usecase.CISettings implements it.
374type EnvironmentService interface {
375	CreateEnvironment(ctx context.Context, p domain.Principal, repo domain.RepoName, name string) (domain.Environment, error)
376	ListEnvironments(ctx context.Context, p domain.Principal, repo domain.RepoName, size int, token string) (EnvironmentPage, error)
377	DeleteEnvironment(ctx context.Context, p domain.Principal, repo domain.RepoName, name string) error
378}

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 4

Tags 45

↑↓ moveenter openesc close