anvilsign in

collin/anvil

1//! Persisted domain types, defined as Toasty models. Tables are created from
2//! these definitions via [`crate::db`]'s `push_schema`.
3//!
4//! Foreign keys are kept as plain scalar fields (`owner_id`, `user_id`) and
5//! queried explicitly, rather than declaring Toasty relations — simpler and a
6//! good fit for our small schema.
7
8/// A registered user account.
9#[derive(Debug, toasty::Model)]
10pub struct User {
11 #[key]
12 #[auto]
13 pub id: i64,
14 #[unique]
15 pub username: String,
16 pub email: String,
17 /// Argon2 PHC-format password hash. Empty for an account that has only
18 /// ever signed in through the identity provider — no password can hash to
19 /// it, so [`crate::users::verify_password`] refuses every guess.
20 pub password_hash: String,
21 pub is_admin: bool,
22 /// Unix timestamp (seconds) of account creation.
23 pub created_at: i64,
24 /// The OIDC `sub` claim this account is linked to, or empty if it isn't.
25 /// Accounts are keyed on `sub` rather than email because `sub` is the one
26 /// claim the provider promises never changes. New columns go last so
27 /// `ALTER TABLE ADD COLUMN` on existing databases agrees with the
28 /// fresh-schema column order.
29 pub sso_sub: String,
30}
31
32/// A hosted repository, owned by a [`User`].
33#[derive(Debug, toasty::Model)]
34pub struct Repository {
35 #[key]
36 #[auto]
37 pub id: i64,
38 #[index]
39 pub owner_id: i64,
40 pub name: String,
41 pub description: String,
42 pub is_private: bool,
43 /// Short name of the default branch, e.g. `main`.
44 pub default_branch: String,
45 pub created_at: i64,
46 /// Push-mirror remote: after every successful push, refs are mirrored to
47 /// this git URL (`git push --mirror`). Empty disables mirroring. New
48 /// columns go last so `ALTER TABLE ADD COLUMN` on existing databases
49 /// agrees with the fresh-schema column order.
50 pub mirror_url: String,
51 /// SHA-256 hash of the preview image extracted from README (empty if none).
52 pub preview_image_hash: String,
53 /// Primary language detected in the repository (e.g. "Rust", empty if no files).
54 pub primary_language: String,
55 /// JSON array of language percentages: [{"lang": "Rust", "percent": 75.5}, ...].
56 pub languages_json: String,
57}
58
59/// A CI run for a pushed commit.
60///
61/// `status` is one of `queued`, `running`, `success`, `failure` (a step exited
62/// non-zero), or `error` (the runner itself failed). `started_at`/`finished_at`
63/// are 0 until they occur.
64#[derive(Clone, Debug, toasty::Model)]
65pub struct CiRun {
66 #[key]
67 #[auto]
68 pub id: i64,
69 #[index]
70 pub repo_id: i64,
71 /// Full commit SHA the run is for.
72 pub commit: String,
73 /// Short branch name that was pushed (e.g. `main`).
74 pub ref_name: String,
75 pub status: String,
76 /// Accumulated run log.
77 pub log: String,
78 pub created_at: i64,
79 pub started_at: i64,
80 pub finished_at: i64,
81}
82
83/// One artifact produced by a CI run, stored on disk under
84/// `data_dir/artifacts/{repo_id}/{commit}/` (see `docs/ci-artifacts.md`).
85///
86/// `commit` is denormalized from the run so per-commit lookups (the
87/// latest-on-branch alias) don't join through runs.
88#[derive(Clone, Debug, toasty::Model)]
89pub struct CiArtifact {
90 #[key]
91 #[auto]
92 pub id: i64,
93 #[index]
94 pub run_id: i64,
95 #[index]
96 pub repo_id: i64,
97 /// Full commit SHA the producing run was for.
98 pub commit: String,
99 /// Declared artifact name (unique within a pipeline, not globally).
100 pub name: String,
101 /// Total size in bytes (summed over files for directory artifacts).
102 pub size: i64,
103 /// Directory artifact (stored as a tarball, or extracted when `browse`).
104 pub is_dir: bool,
105 /// Served as a browsable static site rather than a download.
106 pub browse: bool,
107 /// JSON object of metadata-extractor key → output.
108 pub meta: String,
109 pub created_at: i64,
110}
111
112/// One agent session: a long-lived container running tmux plus an agent CLI
113/// against a repository, attachable from the browser.
114///
115/// `status` is one of `starting`, `running`, `exited` (the agent finished on
116/// its own), `failed` (the supervisor could not start or keep it), or `reaped`
117/// (a timeout, an operator stop, or a server restart took it). The transcript
118/// is *not* a column — it lives at
119/// [`storage::session_transcript_path`](crate::storage::session_transcript_path),
120/// because a terminal stream grows continuously and [`CiRun::log`] is rewritten
121/// whole on every append.
122#[derive(Clone, Debug, toasty::Model)]
123pub struct AgentSession {
124 #[key]
125 #[auto]
126 pub id: i64,
127 #[index]
128 pub repo_id: i64,
129 /// Account that started the session; its access decides who may attach.
130 pub user_id: i64,
131 #[index]
132 pub status: String,
133 /// `interactive` (a human drives it) or `autonomous` (started with a
134 /// prompt and left to run).
135 pub kind: String,
136 /// Branch name the session was started from, e.g. `main`.
137 pub base_ref: String,
138 /// Full commit SHA the workspace was seeded at.
139 pub base_commit: String,
140 /// Branch the agent's work lands on (`agent/{id}`). Empty until the
141 /// session has something to push.
142 pub branch: String,
143 /// Opening prompt for an autonomous session; empty for an interactive one.
144 pub prompt: String,
145 /// Docker container id, empty before it is created.
146 pub container_id: String,
147 /// Image the container was created from, recorded so a session's
148 /// provenance survives a config change.
149 pub image: String,
150 pub created_at: i64,
151 pub started_at: i64,
152 pub finished_at: i64,
153 /// Last time a viewer was attached, driving the idle sweep.
154 pub last_attach_at: i64,
155 /// Container exit code once it stops; 0 until then.
156 pub exit_code: i64,
157 /// Supervisor-facing failure detail, empty when there is none.
158 pub error: String,
159}
160
161/// An issue on a repository. `number` is the user-facing per-repo sequence
162/// (`#1`, `#2`, …); `id` stays the global key. `state` is `open` or `closed`.
163///
164/// Numbering is assigned as max+1 at creation; with a single server process
165/// (our deployment shape) that cannot race.
166#[derive(Clone, Debug, toasty::Model)]
167pub struct Issue {
168 #[key]
169 #[auto]
170 pub id: i64,
171 #[index]
172 pub repo_id: i64,
173 pub number: i64,
174 pub title: String,
175 /// Markdown body (may be empty).
176 pub body: String,
177 pub author_id: i64,
178 pub state: String,
179 pub created_at: i64,
180 /// Bumped on comments and state changes, for "recently active" ordering.
181 pub updated_at: i64,
182}
183
184/// A comment on an [`Issue`].
185#[derive(Clone, Debug, toasty::Model)]
186pub struct IssueComment {
187 #[key]
188 #[auto]
189 pub id: i64,
190 #[index]
191 pub issue_id: i64,
192 pub author_id: i64,
193 /// Markdown body.
194 pub body: String,
195 pub created_at: i64,
196}
197
198/// A web login session, keyed by an opaque random token stored in a cookie.
199#[derive(Debug, toasty::Model)]
200pub struct Session {
201 #[key]
202 pub token: String,
203 #[index]
204 pub user_id: i64,
205 pub created_at: i64,
206 /// Unix timestamp (seconds) after which the session is invalid.
207 pub expires_at: i64,
208}
209
210/// An uploaded file (e.g. an image pasted into the file editor), stored
211/// outside git at `data_dir/attachments/{repo_id}/{hash}` so large binaries
212/// never enter the repository's history. Markdown carries only the serve URL.
213///
214/// Content-addressed: `hash` is the lowercase hex SHA-256 of the bytes, so the
215/// same content uploaded twice to a repo dedupes to one file. Lookups and GC
216/// scope by `repo_id`, which also gates serving by the repo's read access.
217#[derive(Clone, Debug, toasty::Model)]
218pub struct Attachment {
219 #[key]
220 #[auto]
221 pub id: i64,
222 #[index]
223 pub repo_id: i64,
224 /// Lowercase hex SHA-256 of the content — both the dedup key and the path
225 /// component under the repo's attachment directory.
226 pub hash: String,
227 /// MIME type to serve the bytes with (e.g. `image/png`).
228 pub content_type: String,
229 pub size: i64,
230 /// The user who first uploaded this content to the repo.
231 pub uploader_id: i64,
232 pub created_at: i64,
233}
234
235/// A personal access token: a long-lived, scoped bearer credential for
236/// non-browser API clients (e.g. tooling that fetches attachments). Only the
237/// SHA-256 hash of the token is stored; the plaintext is shown once at
238/// creation. A PAT is least-privilege by design — its `scopes` bound what it
239/// can do, and the only scope today (`read`) authenticates safe (GET/HEAD)
240/// requests only, so a leaked token can never mutate.
241#[derive(Clone, Debug, toasty::Model)]
242pub struct ApiToken {
243 #[key]
244 #[auto]
245 pub id: i64,
246 #[index]
247 pub user_id: i64,
248 /// A human label for the token (e.g. "claude"), for listing/revoking.
249 pub name: String,
250 /// Lowercase hex SHA-256 of the token; the lookup key.
251 #[unique]
252 pub token_hash: String,
253 /// Comma-separated scopes granted to this token (e.g. `read`).
254 pub scopes: String,
255 pub created_at: i64,
256}
257
258/// A registered SSH public key, used to authenticate git-over-SSH connections.
259#[derive(Debug, toasty::Model)]
260pub struct SshKey {
261 #[key]
262 #[auto]
263 pub id: i64,
264 #[index]
265 pub user_id: i64,
266 pub title: String,
267 /// Canonical SHA256 fingerprint, e.g. `SHA256:…`.
268 #[unique]
269 pub fingerprint: String,
270 /// Normalized OpenSSH public-key line.
271 pub content: String,
272 pub created_at: i64,
273}
274
275/// A per-repository secret, stored only as a sealed envelope.
276///
277/// The server cannot read `envelope`: it is encrypted to the owner's
278/// ssh-ed25519 keys by the client that set it (see [`crate::secrets`]).
279/// `recipients` denormalizes the envelope's fingerprints so the UI can tell,
280/// without opening anything, which secrets a newly registered key still cannot
281/// decrypt — those need `anvild secret rekey`.
282#[derive(Clone, Debug, toasty::Model)]
283pub struct RepoSecret {
284 #[key]
285 #[auto]
286 pub id: i64,
287 #[index]
288 pub repo_id: i64,
289 /// Environment variable name, e.g. `DEPLOY_TOKEN`. Unique per repository.
290 pub name: String,
291 /// The sealed envelope, as JSON (`anvil-secret-v1`).
292 pub envelope: String,
293 /// Comma-separated SSH fingerprints the envelope is sealed to.
294 pub recipients: String,
295 pub created_at: i64,
296 pub updated_at: i64,
297}
298
299/// Cached admin metrics computed periodically (e.g., disk usage snapshot).
300#[derive(Clone, Debug, toasty::Model)]
301pub struct AdminCache {
302 #[key]
303 #[auto]
304 pub id: i64,
305 /// Cache key (e.g., "disk_usage").
306 pub key: String,
307 /// JSON-encoded cached data.
308 pub value: String,
309 /// Unix timestamp (seconds) of when this snapshot was taken.
310 pub computed_at: i64,
311}