Skip to content

Commit

Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)

# Conflicts: # apps/api/src/operations.rs

syntaqxcommitted Parents7cf200c4ae6eceBrowse files
51 files+199−350/51 viewed
+32−1
4545 ReportQueueArgs, ReportReviewArgs,
4646 };
4747 use g1t_contracts::identity::AgentScope;
48−use g1t_contracts::{Failure, FailureCode, Outcome, PrincipalKind, Viewer};
48+use g1t_contracts::{Failure, FailureCode, Outcome, PrincipalKind, User, Viewer};
4949 use g1t_kit::wire;
5050 use serde_json::{Value, json};
5151 use worker::{Context, Env, Method, Request, Response, Result, event};
9494 }
9595 }
9696
97+/// What a person whose account has not confirmed its email address may
98+/// call: who they are, their addresses, and confirming one with the code
99+/// from the email. Nothing over MCP.
100+fn pending_may(method: &str, path: &str, on_mcp: bool) -> bool {
101+ !on_mcp
102+ && matches!(
103+ (method, path.trim_end_matches('/')),
104+ ("GET", "/user") | ("GET", "/user/emails") | ("POST", "/user/emails/confirm")
105+ )
106+}
107+
97108 /// An error in the shape every endpoint uses.
98109 fn failure(failure: &Failure) -> Result<Response> {
99110 Ok(reply(&json!({ "error": failure }))?.with_status(failure.code.http_status()))
614625 Ok(viewer) => viewer,
615626 Err(refused) => return Ok(refused),
616627 };
628+ // A person who has not confirmed their email address: who they are,
629+ // their addresses, and confirming one, nothing else (REST or MCP).
630+ if viewer.as_ref().is_some_and(User::awaits_confirmation) && !pending_may(method, &path, on_mcp) {
631+ return fail(
632+ FailureCode::Forbidden,
633+ &g1t_contracts::accounts::confirm_email_first(&services.addresses.site),
634+ );
635+ }
617636 services.audit = audit::AuditContext::of(&request, on_mcp);
618637 // An agent's token: what it may do comes with it, on the composite
619638 // identity identity resolved it to.
868887 use serde_json::json;
869888
870889 #[test]
890+ fn an_unconfirmed_account_may_only_see_itself_and_confirm_its_address() {
891+ assert!(super::pending_may("GET", "/user", false));
892+ assert!(super::pending_may("GET", "/user/emails/", false));
893+ assert!(super::pending_may("POST", "/user/emails/confirm", false));
894+ assert!(!super::pending_may("POST", "/user/emails", false));
895+ assert!(!super::pending_may("POST", "/workspaces", false));
896+ assert!(!super::pending_may("GET", "/repos/acme/rocket", false));
897+ assert!(!super::pending_may("POST", "/user/emails/confirm", true));
898+ assert!(!super::pending_may("POST", "/", true));
899+ }
900+
901+ #[test]
871902 fn a_job_spec_gets_the_toolkits_variables() {
872903 // As the actions service sends it, converted as the API does.
873904 let sent = json!({ "variables": { "GITHUB_SHA": "abc" }, "runtime": { "token": "h.p.s", "idToken": true } });
+2−1
2626 (
2727 "Accounts",
2828 "Signing in from a tool, who a token acts as, and your email addresses.",
29− &[Op::Whoami, Op::ListEmails, Op::AddEmail, Op::RemoveEmail, Op::UpdateEmailSettings],
29+ &[Op::Whoami, Op::ListEmails, Op::AddEmail, Op::ConfirmEmail, Op::RemoveEmail, Op::UpdateEmailSettings],
3030 ),
3131 (
3232 "Notifications",
541541 Op::LeaveWorkspace => "Leave a workspace",
542542 Op::ListEmails => "List your email addresses",
543543 Op::AddEmail => "Add an email address",
544+ Op::ConfirmEmail => "Confirm an email address",
544545 Op::RemoveEmail => "Remove an email address",
545546 Op::UpdateEmailSettings => "Change your email settings",
546547 Op::ListInvites => "List your invites",
+25−1
114114 LeaveWorkspace,
115115 ListEmails,
116116 AddEmail,
117+ ConfirmEmail,
117118 RemoveEmail,
118119 UpdateEmailSettings,
119120 ListInvites,
682683 }
683684
684685 impl Op {
685− pub const ALL: [Op; 314] = [
686+ pub const ALL: [Op; 315] = [
686687 Op::Whoami,
687688 Op::GetWorkspace,
688689 Op::CreateWorkspace,
695696 Op::LeaveWorkspace,
696697 Op::ListEmails,
697698 Op::AddEmail,
699+ Op::ConfirmEmail,
698700 Op::RemoveEmail,
699701 Op::UpdateEmailSettings,
700702 Op::ListInvites,
10181020 Op::LeaveWorkspace => "leave_workspace",
10191021 Op::ListEmails => "list_emails",
10201022 Op::AddEmail => "add_email",
1023+ Op::ConfirmEmail => "confirm_email",
10211024 Op::RemoveEmail => "remove_email",
10221025 Op::UpdateEmailSettings => "update_email_settings",
10231026 Op::ListInvites => "list_invites",
12211224 Op::AddEmail => {
12221225 "Add an email address to your account. g1t emails it a link to confirm it; until then it cannot be primary and does not sign you in. Adding an address you added before and have not confirmed sends the link again. An address another account has confirmed cannot be added. An account has at most 10. Needs your account `password`; your confirmed addresses are told. People only."
12231226 }
1227+ Op::ConfirmEmail => {
1228+ "Confirm an email address with the six-digit `code` from the confirmation email g1t sent it. The same email has a link that does the same; either one works, once, for 60 minutes, and asking for a new email ends both. A new account must confirm its address before it can do anything else: until then this, `GET /user` and `GET /user/emails` are the only calls its token can make, and everything else, MCP included, is refused with `403`. Confirming a new account's address also joins the workspace its invite named, when the invite still applies: the answer's `joined` names it, or `invite_lapsed` says why not. Ten wrong codes in an hour pause checking for the account. People only."
1229+ }
12241230 Op::RemoveEmail => {
12251231 "Remove an email address from your account. Never your primary address (make another primary first) and never your last confirmed one. Needs your account `password`; every confirmed address, the removed one included, is told. People only."
12261232 }
17751781 &[],
17761782 ),
17771783 Op::ListEmails => object(json!({}), &[]),
1784+ Op::ConfirmEmail => object(
1785+ json!({
1786+ "code": {
1787+ "type": "string",
1788+ "description": "The six-digit code from the confirmation email. Spaces and hyphens are ignored.",
1789+ },
1790+ }),
1791+ &["code"],
1792+ ),
17781793 Op::AddEmail => object(
17791794 json!({
17801795 "email": { "type": "string", "description": "The address to add." },
33033318 | Op::LeaveWorkspace
33043319 | Op::ListEmails
33053320 | Op::AddEmail
3321+ | Op::ConfirmEmail
33063322 | Op::RemoveEmail
33073323 | Op::UpdateEmailSettings
33083324 | Op::ListInvites
35783594 // A person's addresses: identity refuses anyone but a person, and
35793595 // the password is the proof a sensitive change needs.
35803596 Op::ListEmails => pass(identity, "list_emails", &json!({ "user": actor() })).await,
3597+ Op::ConfirmEmail => {
3598+ pass(
3599+ identity,
3600+ "confirm_email_code",
3601+ &g1t_contracts::accounts::ConfirmEmailCodeArgs { user: actor(), code: text(input, "code"), client: None },
3602+ )
3603+ .await
3604+ }
35813605 Op::AddEmail | Op::RemoveEmail => {
35823606 let method = if self == Op::AddEmail { "add_email" } else { "remove_email" };
35833607 pass(
+14−1
282282 "commit_email": "6c1d0efg+ada@users.noreply.g1t.sh",
283283 "limit": 10
284284 },
285− "notes": "Answers `403` with code `reauth_required` when `password` is missing or wrong, and `409` when another account has confirmed the address. The new address stays unconfirmed until its link is followed."
285+ "notes": "Answers `403` with code `reauth_required` when `password` is missing or wrong, and `409` when another account has confirmed the address. g1t emails the address a six-digit code and a link; it stays unconfirmed until the code is sent to [`confirm_email`](/reference/api/accounts/confirm-email/) or the link is followed."
286+ },
287+ "confirm_email": {
288+ "request": {
289+ "code": "482913"
290+ },
291+ "response": {
292+ "username": "ada",
293+ "email": "ada@example.com",
294+ "verified": true,
295+ "joined": "acme",
296+ "invite_lapsed": null
297+ },
298+ "notes": "The code confirms the address it was sent to, and ends the link sent with it. A wrong, used or expired code answers `422` with one message for all three; after ten wrong codes in an hour the account answers `409` for a while, and the link in the email still works. `verified` says whether the account is confirmed: whether its primary address is. `joined` is set when confirming a new account's address joined the workspace its invite named; when the invite was revoked or expired, or the workspace deleted, while the account waited, the address is confirmed all the same and `invite_lapsed` says so. See [Confirming your email address](/guides/authentication/#confirming-your-email-address)."
286299 },
287300 "remove_email": {
288301 "request": {
+1−0
204204 Op::ListEmails | Op::AddEmail | Op::RemoveEmail | Op::UpdateEmailSettings => {
205205 through::<g1t_contracts::accounts::AccountEmails>(op, sent)
206206 }
207+ Op::ConfirmEmail => through::<g1t_contracts::accounts::EmailConfirmed>(op, sent),
207208 Op::ListInvites => through::<g1t_contracts::identity::InvitesOverview>(op, sent),
208209 Op::CreateInvite | Op::RevokeInvite | Op::InviteMember | Op::RevokeWorkspaceInvite => {
209210 through::<g1t_contracts::identity::Invite>(op, sent)
+1−0
4444 route("DELETE", "/workspaces/:workspace", Op::DeleteWorkspace, &[]),
4545 route("GET", "/user/emails", Op::ListEmails, &[]),
4646 route("POST", "/user/emails", Op::AddEmail, &[]),
47+ route("POST", "/user/emails/confirm", Op::ConfirmEmail, &[]),
4748 route("DELETE", "/user/emails/:email", Op::RemoveEmail, &[]),
4849 route("PATCH", "/user/email-settings", Op::UpdateEmailSettings, &[]),
4950 route("GET", "/user/invites", Op::ListInvites, &[]),
+1−0
500500 a("whoami", Op::Whoami, "Who the token acts as, and its workspaces"),
501501 a("list_emails", Op::ListEmails, "Your addresses"),
502502 a("add_email", Op::AddEmail, "Add an address"),
503+ a("confirm_email", Op::ConfirmEmail, "Confirm an address with the code from its email"),
503504 a("remove_email", Op::RemoveEmail, "Remove an address"),
504505 a("update_email_settings", Op::UpdateEmailSettings, "Primary, backup and privacy"),
505506 a("list_invites", Op::ListInvites, "Your invites to g1t"),
+104−22
1111 on the same page. Usernames are lowercase letters, digits and single
1212 hyphens, up to 39 characters.
1313
14+Before you can do anything else, you [confirm your email
15+address](#confirming-your-email-address) with the code g1t emails you.
16+
1417 Accounts can only be created in a browser. There is no API for it, by
1518 design: it keeps passwords out of scripts and agents, and lets g1t protect
1619 the one place accounts are made.
115118 - works once, for one new account;
116119 - works for 30 days;
117120 - when it was made for an email address, works only with that address;
118−- can be revoked by whoever made it until it is used.
121+- can be revoked by whoever made it until it is used, and after that until
122+ the new account [confirms its email address](#confirming-your-email-address).
119123
120124 ### Using an invite
121125
124128 repository, or just making an account), and finishes the job there:
125129
126130 1. **No account yet**: sign up on the page. When the invite was sent to
127− your address, the email field is filled in and locked, and the address
128− is confirmed already, so no confirmation email follows. Choose a
129− username (one is suggested from your address) and a password, or select
131+ your address, the email field is filled in and locked. Choose a
132+ username (one is suggested from your address) and a password, then
133+ [confirm the address](#confirming-your-email-address) with the code g1t
134+ emails it, even though the invite came there: an invite link can be
135+ forwarded, so it does not prove the inbox is yours. Or select
130136 **Continue with GitHub**: the invite rides along, and the account uses
131− the invited address when GitHub has verified it too.
137+ the invited address when GitHub has verified it too, in which case no
138+ confirmation is needed.
132139 2. **The address already has an account**: select **Sign in to accept**.
133140 After you sign in, the invite is accepted for you.
134141 3. **Signed in as someone else**: an invite sent to one address works only
135142 for an account that has confirmed that address. The page says so and
136143 offers **Sign out and continue**.
137144
138−Once the account exists or you have signed in, you land in the workspace
139−(or the repository) the invite was for, already a member, with a one-time
140−"You're in" banner, and it becomes the workspace your sidebar shows. A
145+Once you have signed in, or your new account has confirmed its address,
146+you land in the workspace (or the repository) the invite was for, already
147+a member, with a one-time "You're in" banner, and it becomes the workspace
148+your sidebar shows.
149+
150+Signing up spends the invite at once, so nobody else can use it while you
151+confirm your address, but you join its workspace only when you confirm, in
152+the same step. Until then the invite shows as **confirming their email** to
153+whoever made it, and they can still revoke it. If the invite is revoked or
154+expires, or its workspace is deleted, before you confirm, your address is
155+confirmed all the same and g1t tells you the invite no longer applies: ask
156+whoever invited you to add you again. A
141157 code typed at [g1t.sh/register](https://g1t.sh/register) goes to the
142158 same page.
143159
155171 Each person can have **5** invites out at a time. Pending and used invites
156172 count; an invite you revoke, or one that expires before anyone uses it,
157173 comes back to you. The list under the form shows each invite's state:
158−pending, joined (with the username of who joined), expired or revoked. You
174+pending, confirming their email (used to sign up by someone who has not
175+confirmed their address yet), joined (with the username of who joined),
176+expired or revoked. You
159177 must confirm your email before you can make invites. An agent's token and
160178 a workspace's token cannot make them.
161179
163181
164182 An owner can invite an email address straight into a workspace from its
165183 People page; see [members and roles](/guides/workspaces/#members-and-roles).
166−When the address has no g1t account, accepting makes the account and joins
167−the workspace in one step, and it uses one invite. Inviting someone who is
184+When the address has no g1t account, the invite makes the account, which
185+joins the workspace once it confirms its email address, and it uses one
186+invite. Inviting someone who is
168187 already on g1t costs nothing.
169188
170189 ### Need more invites?
193212 | --- | --- | --- |
194213 | [`GET /user/invites`](/reference/api/invites/list-invites/) | `account` `list_invites` | Your invites and how many you have left |
195214 | [`POST /user/invites`](/reference/api/invites/create-invite/) | `account` `create_invite` | Make an invite, optionally for one `email` |
196−| [`DELETE /user/invites/{id}`](/reference/api/invites/revoke-invite/) | `account` `revoke_invite` | Revoke a pending invite |
215+| [`DELETE /user/invites/{id}`](/reference/api/invites/revoke-invite/) | `account` `revoke_invite` | Revoke a pending invite, or one whose new account has not confirmed its address |
197216 | [`POST /workspaces/{workspace}/invitations`](/reference/api/invites/invite-member/) | `workspace` `invite_member` | Invite an address into a workspace. Owners only. |
198217
199−## Confirming your email
218+## Confirming your email address
219+
220+A new account confirms its email address before it can do anything else on
221+g1t. Right after you sign up, g1t emails the address from `noreply@g1t.sh`
222+with two ways to confirm it, either one enough:
223+
224+- a **six-digit code**, shown large in the email (and in its subject, so a
225+ phone's notification shows it). Type it on the **Confirm your email**
226+ page g1t takes you to. On a phone, the keyboard offers it from the
227+ message.
228+- a **link**, for when you would rather click than type. It works whether
229+ or not you are signed in, in any browser.
230+
231+The code and the link work for **60 minutes**, once. Using either ends the
232+other. **Send a new code** on the confirmation page sends a fresh code and
233+link, at most once a minute and 10 times an hour, and the ones before stop
234+working.
235+
236+| On the confirmation page | What it does |
237+| --- | --- |
238+| **Confirm email** | Checks the code. A wrong, used or expired code gets the same answer. After 10 wrong codes in an hour, codes for the account are not checked for a while (a minute, then longer); the link in the email still works. Wrong codes from one network are limited the same way. |
239+| **Send a new code** | A new code and link; the ones before stop working. |
240+| **Wrong address? Change it** | Replaces the address you signed up with and sends the new one a code. Only while the account has no confirmed address. |
241+| **Sign out** | Signs out. Sign in again to come back to the page. |
242+
243+### Until you confirm
244+
245+An account that has not confirmed its address can only confirm it:
246+
247+- **The site** sends every page to the confirmation page, and back to where
248+ you were going once you confirm. Signing in and out, password resets,
249+ the confirmation link, and g1t's [policies](https://g1t.sh/policies),
250+ security, support, status and pricing pages stay open.
251+- **The API** answers `403` with a message saying to confirm your address,
252+ except for [`GET /user`](/reference/api/accounts/whoami/),
253+ [`GET /user/emails`](/reference/api/accounts/list-emails/) and
254+ [`POST /user/emails/confirm`](/reference/api/accounts/confirm-email/).
255+- **The MCP server** answers `403` with the same message.
256+- **Git** over HTTPS refuses pushes and fetches with your credentials, with
257+ the same message. Package registries treat them as wrong credentials.
258+- You cannot create a workspace, join the one your invite named, make
259+ invites or tokens, or approve a tool's sign-in.
200260
201−g1t sends a confirmation link from `noreply@g1t.sh`. It works for 24 hours.
261+You cannot make a token before you confirm, so the API and MCP refusals
262+matter only for an account that made one before this rule existed.
202263
203−Until you follow it you can sign in and look around, but you cannot create
204−repositories, push, or open issues and pull requests. Those requests fail with `403` and a
205−message telling you to confirm your address. To get a new link, sign in and
206−use the banner at the top of the site.
264+### Addresses GitHub has confirmed
207265
266+An account made with **Continue with GitHub** starts confirmed: its address
267+is one GitHub has verified, so GitHub has already proved the inbox is
268+yours, and no code is sent.
269+
270+### Accounts that never confirmed
271+
272+Accounts are confirmed once and stay confirmed. An account made before this
273+rule that never confirmed its address, or whose address another account
274+confirmed first, is held at the confirmation page the same way the next
275+time it signs in; **Send a new code** gets it a code, and **Change it**
276+gives it a new address.
277+
278+### Confirming through the API
279+
280+| Route | MCP tool and action | What it does |
281+| --- | --- | --- |
282+| [`POST /user/emails/confirm`](/reference/api/accounts/confirm-email/) | `account` `confirm_email` | Confirm an address with the `code` from its email |
283+
284+The answer says whether the account is now confirmed (`verified`), the
285+workspace confirming joined it to (`joined`), or why its invite no longer
286+applies (`invite_lapsed`).
287+
208288 ## Email addresses
209289
210290 An account can have up to 10 email addresses. Manage them in
215295 | Primary | Get account mail and password reset links. Exactly one, always confirmed once any address is. |
216296 | Confirmed | Sign you in (type it instead of your username), ask for a password reset, and mark commits that carry it as yours. |
217297 | Backup | Get security notices as well as the primary. Optional, and a confirmed address other than the primary. |
218−| Unconfirmed | Nothing yet. It is not yours until you follow the link g1t sent it. |
298+| Unconfirmed | Nothing yet. It is not yours until you enter the code or follow the link g1t sent it. |
219299
220300 A confirmed address belongs to one account. Anyone can add an address they
221−have not confirmed; the first account to follow its link keeps it, and the
301+have not confirmed; the first account to confirm it keeps it, and the
222302 address leaves every other account that added it. An address another
223303 account has confirmed cannot be added.
224304
226306
227307 1. Open [Settings → Emails](https://g1t.sh/settings/emails).
228308 2. Enter the address under **Add an email address** and select **Add**.
229−3. Follow the link g1t sends it. The link works for 24 hours; **Resend
230− link** sends a new one, at most once a minute and 10 times an hour.
309+3. Enter the code g1t emails it, or follow the link in the same email.
310+ Both work for 60 minutes; **Resend link** sends a new code and link, at
311+ most once a minute and 10 times an hour, and ends the ones before.
231312
232313 If your account had no confirmed address yet, the first one you confirm
233314 becomes your primary.
312393 | --- | --- | --- |
313394 | [`GET /user/emails`](/reference/api/accounts/list-emails/) | `account` `list_emails` | Your addresses and email settings |
314395 | [`POST /user/emails`](/reference/api/accounts/add-email/) | `account` `add_email` | Add an address; takes `email` and `password` |
396+| [`POST /user/emails/confirm`](/reference/api/accounts/confirm-email/) | `account` `confirm_email` | Confirm an address with the `code` from its email |
315397 | [`DELETE /user/emails/{email}`](/reference/api/accounts/remove-email/) | `account` `remove_email` | Remove an address; takes `password` |
316398 | [`PATCH /user/email-settings`](/reference/api/accounts/update-email-settings/) | `account` `update_email_settings` | Change `primary`, `backup`, `private_email` or `block_private_pushes` |
317399
+3−1
6262 3. Open [http://localhost:8787](http://localhost:8787) and create an
6363 account.
6464 4. Open the Mailpit inbox at [http://localhost:8025](http://localhost:8025)
65− and follow the link in the confirmation email.
65+ and enter the code from the confirmation email on the page the site
66+ shows you, or follow the link in the same email. Set `IDENTITY_KEY`
67+ (the setup does) so codes are kept as keyed hashes.
6668 5. Create a workspace, then a repository.
6769
6870 ## Push and clone
+6−3
332332 never says which it was.
333333
334334 The email names you and the workspace and links to the invite's page.
335−Someone new signs up right there, with the invited address filled in and
336−already confirmed; someone with an account signs in. Either way they land
337−in the workspace as a member, with a one-time welcome. See
335+Someone new signs up right there, with the invited address filled in, and
336+joins once they confirm it with the code g1t emails them; someone with an
337+account signs in. Either way they land in the workspace as a member, with
338+a one-time welcome. Until a new account confirms its address, its invite
339+shows as **confirming their email** under the members, and you can still
340+revoke it. See
338341 [using an invite](/guides/authentication/#using-an-invite).
339342
340343 Pending invites are listed under the members, with a link to copy and
+1−1
2323
2424 [Sign up](https://g1t.sh/register) with your invite (g1t is invite-only
2525 for now; see [invites](/guides/authentication/#invites)) and confirm your
26− email. Then create a
26+ email with the code g1t sends it (or the link in the same email). Then create a
2727 **workspace**: it owns repositories and is the first part of their
2828 address, `g1t.sh/<workspace>/<repo>`. See [workspaces](/guides/workspaces/).
2929
+5−2
735735 invites while g1t is [invite-only](/guides/authentication/#invites), and
736736 invitations to repositories waiting for you. `whoami` is the default
737737 action, and needs no scope. An agent's token and a workspace's token cannot
738−use the email and invite actions.
738+use the email and invite actions. An account that has not confirmed its
739+email address gets `403` from every tool until it does; see
740+[until you confirm](/guides/authentication/#until-you-confirm).
739741
740742 | Action | What it does | Required | Scope |
741743 | --- | --- | --- | --- |
742744 | [`whoami`](/reference/api/accounts/whoami/) | Who the access token acts as, and the workspaces it can work in. `kind` is `user`, `workspace` or `agent`. | None | None |
743745 | [`list_emails`](/reference/api/accounts/list-emails/) | Your email addresses and email settings. People only. | None | `account:read` |
744−| [`add_email`](/reference/api/accounts/add-email/) | Add an address; g1t emails it a link to confirm it. | `email`, `password` | `account:write` |
746+| [`add_email`](/reference/api/accounts/add-email/) | Add an address; g1t emails it a code and a link to confirm it. | `email`, `password` | `account:write` |
747+| [`confirm_email`](/reference/api/accounts/confirm-email/) | Confirm an address with the six-digit code from its email. See [confirming your email address](/guides/authentication/#confirming-your-email-address). | `code` | `account:write` |
745748 | [`remove_email`](/reference/api/accounts/remove-email/) | Remove an address; never the primary or the last confirmed one. | `email`, `password` | `account:write` |
746749 | [`update_email_settings`](/reference/api/accounts/update-email-settings/) | Change `primary` or `backup` (with `password`), `private_email` or `block_private_pushes`. See [email addresses](/guides/authentication/#email-addresses). | None | `account:write` |
747750 | [`list_invites`](/reference/api/invites/list-invites/) | Your invites, newest first, and how many you have left. | None | `account:read` |
+0−0

Binary or large file; its contents are not shown.

+2−0
137137 switch (invite.status) {
138138 case "pending":
139139 return <Badge tone="lavender">Pending</Badge>;
140+ case "awaiting_confirmation":
141+ return <Badge tone="lavender">Used{invite.redeemedBy ? ` by ${invite.redeemedBy}` : ""}, email not confirmed</Badge>;
140142 case "redeemed":
141143 return <Badge tone="mint">Used{invite.redeemedBy ? ` by ${invite.redeemedBy}` : ""}</Badge>;
142144 case "expired":
+1−1
1919 <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
2020 <span className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs ${TONE[state.tone]}`}>{state.label}</span>
2121 <span className="min-w-0 truncate text-sm">{inviteFor(invite)}</span>
22− {invite.status === "pending" && (
22+ {(invite.status === "pending" || invite.status === "awaiting_confirmation") && (
2323 <Form method="post" className="ml-auto">
2424 <input type="hidden" name="intent" value="revoke-invite" />
2525 <input type="hidden" name="id" value={invite.id} />
+1−1
2525 });
2626
2727 test("a visitor gets the marketing frame on the front, pricing and sign-in pages", () => {
28− for (const path of ["/", "/pricing", "/pricing/", "/login", "/register", "/verify", "/forgot", "/reset", "/device", "/oauth/authorize"]) {
28+ for (const path of ["/", "/pricing", "/pricing/", "/login", "/register", "/verify", "/confirm-email", "/forgot", "/reset", "/device", "/oauth/authorize"]) {
2929 assert.equal(usesAppShell(path, false), false, path);
3030 }
3131 });
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

This change is too large to show in full.