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.
| The phone app's scaffolding, in apps/mobile: Expo SDK 57 with Expo Router, Android first, with Chat, Code and Customize. It signs in with g1t's own OAuth 2.1 (PKCE, a client id that encodes "g1t for Android" and g1t://oauth/callback, so nothing is registered), keeps the server and credentials in the phone's keystore, refreshes the 30-day token a minute early and shares the single-use refresh between requests. A personal access token is the other way in, and a self-hosted g1t the other server. | 1 | /** |
| 2 | * Which g1t the app talks to. Hosted g1t is the site `https://g1t.sh` and | |
| 3 | * the API `https://api.g1t.sh`; a self-hosted g1t sets its own (SITE_URL | |
| 4 | * and API_URL, apps/api/src/addresses.rs), so the app keeps both. | |
| 5 | * | |
| 6 | * The site is where people sign in and approve the app, and where Chat's | |
| 7 | * routes are; the API is the REST API and the OAuth issuer. | |
| 8 | * | |
| 9 | * Kept free of React Native imports so `npm test` runs it under Node. | |
| 10 | */ | |
| 11 | ||
| 12 | export type Server = { | |
| 13 | /** An origin, no trailing slash: `https://g1t.sh`. */ | |
| 14 | site: string; | |
| 15 | /** An origin, no trailing slash: `https://api.g1t.sh`. */ | |
| 16 | api: string; | |
| 17 | }; | |
| 18 | ||
| 19 | export const HOSTED: Server = { site: "https://g1t.sh", api: "https://api.g1t.sh" }; | |
| 20 | ||
| 21 | /** | |
| 22 | * An origin as typed: http or https with a host, `https://` when no scheme | |
| 23 | * is given; only the origin is kept. Null when it is not a web address. | |
| 24 | * Plain http is accepted for a g1t on this network, as in development. | |
| 25 | */ | |
| 26 | export function parseOrigin(text: string): string | null { | |
| 27 | const trimmed = text.trim(); | |
| 28 | if (!trimmed) return null; | |
| 29 | const withScheme = trimmed.includes("://") ? trimmed : `https://${trimmed}`; | |
| 30 | let url: URL; | |
| 31 | try { | |
| 32 | url = new URL(withScheme); | |
| 33 | } catch { | |
| 34 | return null; | |
| 35 | } | |
| 36 | if (url.protocol !== "https:" && url.protocol !== "http:") return null; | |
| 37 | if (!url.hostname) return null; | |
| 38 | return `${url.protocol}//${url.host}`; | |
| 39 | } | |
| 40 | ||
| 41 | /** | |
| 42 | * The API's address for a site, when only the site was given: hosted | |
| 43 | * g1t's own, or `api.` in front of a self-hosted site's host, which is | |
| 44 | * how deploy/self-host names it. | |
| 45 | */ | |
| 46 | export function apiFor(site: string): string { | |
| 47 | if (site === HOSTED.site) return HOSTED.api; | |
| 48 | const url = new URL(site); | |
| 49 | return `${url.protocol}//api.${url.host}`; | |
| 50 | } | |
| 51 | ||
| 52 | /** A server from what was typed: the site, and the API if it was given. */ | |
| 53 | export function parseServer(siteText: string, apiText?: string): Server | null { | |
| 54 | const site = parseOrigin(siteText); | |
| 55 | if (!site) return null; | |
| 56 | if (apiText && apiText.trim()) { | |
| 57 | const api = parseOrigin(apiText); | |
| 58 | if (!api) return null; | |
| 59 | return { site, api }; | |
| 60 | } | |
| 61 | return { site, api: apiFor(site) }; | |
| 62 | } | |
| 63 | ||
| 64 | export function isHosted(server: Server): boolean { | |
| 65 | return server.site === HOSTED.site && server.api === HOSTED.api; | |
| 66 | } | |
| 67 | ||
| 68 | /** The site's address for a path, such as a page to open in the browser. */ | |
| 69 | export function siteUrl(server: Server, path: string): string { | |
| 70 | return server.site + (path.startsWith("/") ? path : `/${path}`); | |
| 71 | } | |
| 72 | ||
| 73 | /** The socket address for one of the site's live paths. */ | |
| 74 | export function socketUrl(server: Server, path: string): string { | |
| 75 | return siteUrl(server, path).replace(/^http/, "ws"); | |
| 76 | } |