Skip to content
71 linesCodeBlameRaw
1/**
2 * How a g1t says where its parts are: `GET <site>/.well-known/g1t.json`.
3 * The phone app reads it when someone points it at their own g1t, so they
4 * type only the site's address and the app finds the API (the OAuth
5 * issuer), wherever that g1t put it. g1t.sh answers it too.
6 *
7 * Public and the same for everyone: no account, nothing about a person.
8 * No imports, so the site and the phone share it.
9 */
10
11/** Where the document is served, under a site's origin. */
12export const DISCOVERY_PATH = "/.well-known/g1t.json";
13
14export type G1tDiscovery = {
15 /** Always "g1t", so a stray JSON file at the address is not mistaken for one. */
16 name: "g1t";
17 /** The site's origin: where people sign in and approve applications. */
18 site: string;
19 /** The REST API's origin, which is also the OAuth issuer. */
20 api: string;
21 /** The MCP server's URL. */
22 mcp: string;
23 /** Whether this is g1t.sh, rather than a g1t someone runs themselves. */
24 hosted: boolean;
25 /**
26 * What the phone app can use here: `chat`, Chat with the app's own
27 * sign-in (the `chat:*` scopes); `push`, notifications on the phone. A g1t
28 * from before the phone app has no document at all.
29 */
30 phone: { chat: boolean; push: boolean };
31};
32
33function origin(value: unknown): string | null {
34 if (typeof value !== "string") return null;
35 try {
36 const url = new URL(value);
37 if (url.protocol !== "https:" && url.protocol !== "http:") return null;
38 return url.origin;
39 } catch {
40 return null;
41 }
42}
43
44/** A discovery document as read, checked; null when it is not one. */
45export function parseDiscovery(value: unknown): G1tDiscovery | null {
46 if (!value || typeof value !== "object") return null;
47 const v = value as Record<string, unknown>;
48 if (v.name !== "g1t") return null;
49 const site = origin(v.site);
50 const api = origin(v.api);
51 if (!site || !api) return null;
52 const mcp = typeof v.mcp === "string" ? v.mcp.trim().replace(/\/+$/, "") : "";
53 const phone = (v.phone && typeof v.phone === "object" ? v.phone : {}) as Record<string, unknown>;
54 return {
55 name: "g1t",
56 site,
57 api,
58 mcp,
59 hosted: v.hosted === true,
60 phone: { chat: phone.chat === true, push: phone.push === true },
61 };
62}
63
64/**
65 * A link that opens the phone app pointed at a g1t: `g1t://connect?server=<site>`.
66 * A self-hosted g1t shows it, and its QR code, so its people need not type
67 * the address.
68 */
69export function connectLink(site: string): string {
70 return `g1t://connect?server=${encodeURIComponent(site)}`;
71}