Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 1 | /** |
| 2 | * The workspace gate: everything on g1t lives in a workspace, so a confirmed | |
| 3 | * person with none is sent to make one or answer an invitation to one | |
| 4 | * (`/workspaces/new`, "Create your workspace or ask to join one"), from | |
| 5 | * every page but the few that needs, and is returned to where they were | |
| 6 | * going afterwards. Mission control is never shown without a workspace. | |
| 7 | * No Workers or React imports, so it can be tested under Node. | |
| 8 | */ | |
| 9 | ||
| 10 | /** Where someone without a workspace makes one, or answers an invitation. */ | |
| 11 | export const NO_WORKSPACE_PATH = "/workspaces/new"; | |
| 12 | ||
| 13 | /** Pages a signed-in person can use before they have a workspace. */ | |
| 14 | const BEFORE_WORKSPACE = new Set([ | |
| 15 | NO_WORKSPACE_PATH, | |
| 16 | "/invitations", | |
| 17 | "/settings", | |
| 18 | "/verify", | |
| 19 | "/logout", | |
| 20 | "/auth/github", | |
| 21 | "/auth/github/callback", | |
| g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them. | 22 | "/notifications", |
| 23 | // The old address, which leads to Notifications. | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 24 | "/inbox", |
| 25 | "/inbox.json", | |
| 26 | "/settings/menu.json", | |
| 27 | // Who makes g1t and the promises it keeps. | |
| 28 | "/policies", | |
| 29 | "/security", | |
| 30 | "/support", | |
| 31 | "/pricing", | |
| 32 | ]); | |
| 33 | ||
| 34 | type Someone = | |
| 35 | | { | |
| 36 | verified?: boolean; | |
| 37 | kind?: string; | |
| 38 | workspaces?: unknown[] | null; | |
| 39 | grants?: unknown[] | null; | |
| 40 | held?: unknown[] | null; | |
| 41 | } | |
| 42 | | null | |
| 43 | | undefined; | |
| 44 | ||
| 45 | /** Whether `viewer` is a confirmed person who belongs to no workspace and has nothing else to use. */ | |
| 46 | export function hasNoWorkspace(viewer: Someone): boolean { | |
| 47 | if (!viewer?.verified) return false; | |
| 48 | if (viewer.kind && viewer.kind !== "user") return false; | |
| 49 | return ( | |
| 50 | (viewer.workspaces ?? []).length === 0 && | |
| 51 | // Someone a repository is shared with can use it without a workspace. | |
| 52 | (viewer.grants ?? []).length === 0 && | |
| 53 | // Someone held out of their workspaces until they meet its policy is | |
| 54 | // told so, and sent to turn on two-factor authentication, not to make one. | |
| 55 | (viewer.held ?? []).length === 0 | |
| 56 | ); | |
| 57 | } | |
| 58 | ||
| 59 | /** | |
| 60 | * Where to send `viewer` instead of `page` (a page's path; a data request's | |
| 61 | * page, from `pageOf`) + `search`: the page to make a workspace or answer | |
| 62 | * an invitation, with where they were going as `next`. Null when they have | |
| 63 | * a workspace or the page is theirs to open without one. | |
| 64 | */ | |
| 65 | export function workspaceGate(page: string, search: string, viewer: Someone): string | null { | |
| 66 | if (!hasNoWorkspace(viewer)) return null; | |
| 67 | const path = page.length > 1 ? page.replace(/\/+$/, "") : page; | |
| 68 | if ( | |
| 69 | BEFORE_WORKSPACE.has(path) || | |
| 70 | path.startsWith("/settings/") || | |
| 71 | path.startsWith("/policies/") || | |
| 72 | path.startsWith("/-/") || | |
| 73 | path.startsWith("/u/") || | |
| 74 | // An invite to a workspace is how someone without one gets one, and an | |
| 75 | // invitation to a repository is answered before anything else. | |
| 76 | path.startsWith("/invite/") || | |
| 77 | /^\/[^/]+\/[^/]+\/invitations$/.test(path) | |
| 78 | ) { | |
| 79 | return null; | |
| 80 | } | |
| 81 | const params = new URLSearchParams(search); | |
| 82 | params.delete("_routes"); | |
| 83 | const query = params.toString(); | |
| 84 | const next = path === "/" && !query ? "" : `?next=${encodeURIComponent(path + (query ? `?${query}` : ""))}`; | |
| 85 | return `${NO_WORKSPACE_PATH}${next}`; | |
| 86 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.