Go · 2895 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 // Package lifecycle owns the post-creation mutations of a repo: rename,
4 // transfer, archive/unarchive, visibility flip, soft-delete, restore,
5 // and hard-delete. Every operation is a single function with strict
6 // ordering of DB and FS work; recovery semantics are documented at
7 // each call site.
8 //
9 // The package is policy-agnostic — callers (web handlers, the
10 // hard-delete worker) are expected to have already passed the request
11 // through policy.Can with the appropriate Action. Lifecycle's job is
12 // to *execute* the change correctly, not decide who's allowed.
13 package lifecycle
14
15 import (
16 "errors"
17 "log/slog"
18 "time"
19
20 "github.com/jackc/pgx/v5/pgxpool"
21
22 "github.com/tenseleyFlow/shithub/internal/auth/audit"
23 "github.com/tenseleyFlow/shithub/internal/infra/storage"
24 )
25
26 // Deps wires the orchestrator. Construct once and pass to every Op.
27 // All Ops are stateless w.r.t. Deps so a single value can serve many
28 // concurrent requests.
29 type Deps struct {
30 Pool *pgxpool.Pool
31 RepoFS *storage.RepoFS
32 Audit *audit.Recorder
33 Logger *slog.Logger
34 Now func() time.Time
35 }
36
37 // now returns deps.Now() or wall time. Tests pin Now for determinism.
38 func (d Deps) now() time.Time {
39 if d.Now != nil {
40 return d.Now()
41 }
42 return time.Now()
43 }
44
45 // Errors surfaced by lifecycle ops. Handlers map these to HTTP status
46 // codes and friendly user messages.
47 var (
48 ErrInvalidName = errors.New("lifecycle: invalid name")
49 ErrReservedName = errors.New("lifecycle: name is reserved")
50 ErrNameTaken = errors.New("lifecycle: name already taken on owner")
51 ErrRenameRateLimited = errors.New("lifecycle: rename rate limit exceeded")
52 ErrSameName = errors.New("lifecycle: new name equals current")
53 ErrTransferToSelf = errors.New("lifecycle: transfer recipient is the same owner")
54 ErrTransferTerminal = errors.New("lifecycle: transfer no longer pending")
55 ErrTransferExpired = errors.New("lifecycle: transfer expired")
56 ErrAlreadyArchived = errors.New("lifecycle: repo is already archived")
57 ErrNotArchived = errors.New("lifecycle: repo is not archived")
58 ErrAlreadyDeleted = errors.New("lifecycle: repo is already soft-deleted")
59 ErrNotDeleted = errors.New("lifecycle: repo is not soft-deleted")
60 ErrPastGrace = errors.New("lifecycle: soft-delete grace expired; restore unavailable")
61 )
62
63 // renameRateLimitWindow / renameRateLimitMax mirror the spec's lean.
64 // Adjust together; the SQL counts redirects within the window.
65 const (
66 renameRateLimitWindow = 30 * 24 * time.Hour
67 renameRateLimitMax = 5
68
69 // transferTTL is the time a recipient has to accept before the
70 // offer auto-expires.
71 transferTTL = 7 * 24 * time.Hour
72
73 // softDeleteGrace is the window between soft-delete and the worker
74 // hard-deleting. Mirrored in the SQL `interval '7 days'`.
75 softDeleteGrace = 7 * 24 * time.Hour
76 )
77