gt 2.22.0 → 2.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 2.22.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#2337](https://github.com/generaltranslation/gt/pull/2337) [`61c78cc`](https://github.com/generaltranslation/gt/commit/61c78ccf818f801ed8aa87ee3901b583c7c101e0) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Skip untranslated file paths when localizing static URLs in postprocessing
8
+
9
+ - [#2334](https://github.com/generaltranslation/gt/pull/2334) [`a9d5143`](https://github.com/generaltranslation/gt/commit/a9d51435cdfb05a98253838aa78ffbfa2703fa16) Thanks [@chenxin-yan](https://github.com/chenxin-yan)! - Automatically use device-code login in SSH sessions and display the verification URL on its own line. Clarify login prompts and show browser success or failure only after authentication and credential storage complete.
10
+
3
11
  ## 2.22.0
4
12
 
5
13
  ### Minor Changes
@@ -1 +1 @@
1
- export declare function renderCallbackPage(): string;
1
+ export declare function renderCallbackPage(success: boolean): string;
@@ -6,6 +6,7 @@ const STYLES = `
6
6
  --foreground: #09090b;
7
7
  --muted-foreground: #71717a;
8
8
  --success: #1cca5b;
9
+ --error: #dc2626;
9
10
  }
10
11
  @media (prefers-color-scheme: dark) {
11
12
  :root {
@@ -13,6 +14,7 @@ const STYLES = `
13
14
  --foreground: #fafafa;
14
15
  --muted-foreground: #a1a1aa;
15
16
  --success: #387836;
17
+ --error: #f87171;
16
18
  }
17
19
  }
18
20
  * { box-sizing: border-box; margin: 0; }
@@ -39,6 +41,7 @@ const STYLES = `
39
41
  margin: 0 auto 16px;
40
42
  }
41
43
  .success { color: var(--success); }
44
+ .error { color: var(--error); }
42
45
  h1 {
43
46
  font-size: 18px;
44
47
  line-height: 28px;
@@ -48,25 +51,23 @@ const STYLES = `
48
51
  }
49
52
  p { color: var(--muted-foreground); }
50
53
  `;
51
- const page = {
52
- title: "Sign-in response received",
53
- icon: "<svg class=\"success\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m9 12 2 2 4-4\"/></svg>",
54
- body: "Return to your terminal to check whether sign in completed. You can close this window."
55
- };
56
- function renderCallbackPage() {
54
+ const CIRCLE_CHECK = "<svg class=\"success\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m9 12 2 2 4-4\"/></svg>";
55
+ const CIRCLE_X = "<svg class=\"error\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6m0-6 6 6\"/></svg>";
56
+ function renderCallbackPage(success) {
57
+ const title = success ? "Successfully authenticated gt CLI" : "Authentication failed";
57
58
  return `<!doctype html>
58
59
  <html lang="en">
59
60
  <head>
60
61
  <meta charset="utf-8">
61
62
  <meta name="viewport" content="width=device-width, initial-scale=1">
62
- <title>${page.title}</title>
63
+ <title>${title}</title>
63
64
  <style>${STYLES}</style>
64
65
  </head>
65
66
  <body>
66
67
  <main>
67
- ${page.icon}
68
- <h1>${page.title}</h1>
69
- <p>${page.body}</p>
68
+ ${success ? CIRCLE_CHECK : CIRCLE_X}
69
+ <h1>${title}</h1>
70
+ <p>You may now close this tab and return to the terminal.</p>
70
71
  </main>
71
72
  </body>
72
73
  </html>`;
@@ -1 +1 @@
1
- {"version":3,"file":"callbackPage.js","names":[],"sources":["../../src/auth/callbackPage.ts"],"sourcesContent":["// Mirrors the terminal state of the dashboard's device-approval screen\n// (gt-cloud apps/dashboard/.../signin/device/_components/DeviceApproval.tsx):\n// a 48px lucide status icon, 18px semibold heading, 14px muted body, centered\n// in a 400px column. Tokens are copied from gt-cloud packages/ui shared.css\n// because this page is served from 127.0.0.1 with no external assets.\nconst STYLES = `\n :root {\n color-scheme: light dark;\n --background: #fff;\n --foreground: #09090b;\n --muted-foreground: #71717a;\n --success: #1cca5b;\n }\n @media (prefers-color-scheme: dark) {\n :root {\n --background: #09090b;\n --foreground: #fafafa;\n --muted-foreground: #a1a1aa;\n --success: #387836;\n }\n }\n * { box-sizing: border-box; margin: 0; }\n html, body { height: 100%; }\n body {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 48px 24px;\n background: var(--background);\n color: var(--foreground);\n font: 14px/20px Inter, ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif;\n -webkit-font-smoothing: antialiased;\n }\n main {\n width: 100%;\n max-width: 400px;\n text-align: center;\n }\n svg {\n display: block;\n width: 48px;\n height: 48px;\n margin: 0 auto 16px;\n }\n .success { color: var(--success); }\n h1 {\n font-size: 18px;\n line-height: 28px;\n font-weight: 600;\n letter-spacing: -0.01em;\n margin-bottom: 8px;\n }\n p { color: var(--muted-foreground); }\n`;\n\nconst CIRCLE_CHECK =\n '<svg class=\"success\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m9 12 2 2 4-4\"/></svg>';\nconst page = {\n title: 'Sign-in response received',\n icon: CIRCLE_CHECK,\n body: 'Return to your terminal to check whether sign in completed. You can close this window.',\n};\n\nexport function renderCallbackPage(): string {\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${page.title}</title>\n<style>${STYLES}</style>\n</head>\n<body>\n<main>\n${page.icon}\n<h1>${page.title}</h1>\n<p>${page.body}</p>\n</main>\n</body>\n</html>`;\n}\n"],"mappings":";AAKA,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDf,MAAM,OAAO;CACX,OAAO;CACP,MAAM;CACN,MAAM;CACP;AAED,SAAgB,qBAA6B;AAC3C,QAAO;;;;;SAKA,KAAK,MAAM;SACX,OAAO;;;;EAId,KAAK,KAAK;MACN,KAAK,MAAM;KACZ,KAAK,KAAK"}
1
+ {"version":3,"file":"callbackPage.js","names":[],"sources":["../../src/auth/callbackPage.ts"],"sourcesContent":["// Mirrors the terminal state of the dashboard's device-approval screen\n// (gt-cloud apps/dashboard/.../signin/device/_components/DeviceApproval.tsx):\n// a 48px lucide status icon, 18px semibold heading, 14px muted body, centered\n// in a 400px column. Tokens are copied from gt-cloud packages/ui shared.css\n// because this page is served from 127.0.0.1 with no external assets.\nconst STYLES = `\n :root {\n color-scheme: light dark;\n --background: #fff;\n --foreground: #09090b;\n --muted-foreground: #71717a;\n --success: #1cca5b;\n --error: #dc2626;\n }\n @media (prefers-color-scheme: dark) {\n :root {\n --background: #09090b;\n --foreground: #fafafa;\n --muted-foreground: #a1a1aa;\n --success: #387836;\n --error: #f87171;\n }\n }\n * { box-sizing: border-box; margin: 0; }\n html, body { height: 100%; }\n body {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 48px 24px;\n background: var(--background);\n color: var(--foreground);\n font: 14px/20px Inter, ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif;\n -webkit-font-smoothing: antialiased;\n }\n main {\n width: 100%;\n max-width: 400px;\n text-align: center;\n }\n svg {\n display: block;\n width: 48px;\n height: 48px;\n margin: 0 auto 16px;\n }\n .success { color: var(--success); }\n .error { color: var(--error); }\n h1 {\n font-size: 18px;\n line-height: 28px;\n font-weight: 600;\n letter-spacing: -0.01em;\n margin-bottom: 8px;\n }\n p { color: var(--muted-foreground); }\n`;\n\nconst CIRCLE_CHECK =\n '<svg class=\"success\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m9 12 2 2 4-4\"/></svg>';\nconst CIRCLE_X =\n '<svg class=\"error\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6m0-6 6 6\"/></svg>';\n\nexport function renderCallbackPage(success: boolean): string {\n const title = success\n ? 'Successfully authenticated gt CLI'\n : 'Authentication failed';\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${title}</title>\n<style>${STYLES}</style>\n</head>\n<body>\n<main>\n${success ? CIRCLE_CHECK : CIRCLE_X}\n<h1>${title}</h1>\n<p>You may now close this tab and return to the terminal.</p>\n</main>\n</body>\n</html>`;\n}\n"],"mappings":";AAKA,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDf,MAAM,eACJ;AACF,MAAM,WACJ;AAEF,SAAgB,mBAAmB,SAA0B;CAC3D,MAAM,QAAQ,UACV,sCACA;AACJ,QAAO;;;;;SAKA,MAAM;SACN,OAAO;;;;EAId,UAAU,eAAe,SAAS;MAC9B,MAAM"}
@@ -2,8 +2,8 @@ export declare const LOOPBACK_CALLBACK_PATH = "/callback";
2
2
  export type LoopbackServer = {
3
3
  /** Loopback redirect URI bound to an ephemeral port, e.g. http://127.0.0.1:53211/callback */
4
4
  redirectUri: string;
5
- /** Resolves with the raw callback parameters of the first request to the callback path. */
6
- waitForCallback: (timeoutMs?: number) => Promise<URL>;
5
+ /** Shows the result page only after the callback has been validated and saved. */
6
+ waitForCallback: <T>(onCallback: (url: URL) => Promise<T>, timeoutMs?: number) => Promise<T>;
7
7
  close: () => void;
8
8
  };
9
9
  export declare function startLoopbackServer(): Promise<LoopbackServer>;
@@ -25,18 +25,18 @@ async function startLoopbackServer() {
25
25
  let timeout;
26
26
  return {
27
27
  redirectUri: `${origin}${LOOPBACK_CALLBACK_PATH}`,
28
- waitForCallback(timeoutMs = DEFAULT_CALLBACK_TIMEOUT_MS) {
28
+ waitForCallback(onCallback, timeoutMs = DEFAULT_CALLBACK_TIMEOUT_MS) {
29
29
  return new Promise((resolve, reject) => {
30
30
  let settled = false;
31
31
  timeout = setTimeout(() => {
32
32
  settled = true;
33
33
  server.close();
34
34
  reject(new Error(createDiagnosticMessage({
35
- whatHappened: "Timed out waiting for the browser to sign in",
35
+ whatHappened: "Timed out waiting for authentication",
36
36
  fix: "Run `gt login` again"
37
37
  })));
38
38
  }, timeoutMs);
39
- server.on("request", (request, response) => {
39
+ server.on("request", async (request, response) => {
40
40
  const target = request.url ?? "/";
41
41
  const url = URL.canParse(target, origin) ? new URL(target, origin) : void 0;
42
42
  if (!url || settled || request.method !== "GET" || url.origin !== origin || url.pathname !== "/callback") {
@@ -55,9 +55,16 @@ async function startLoopbackServer() {
55
55
  "content-type": "text/html; charset=utf-8",
56
56
  "x-content-type-options": "nosniff"
57
57
  });
58
- response.end(renderCallbackPage());
59
- server.close();
60
- resolve(url);
58
+ try {
59
+ const result = await onCallback(url);
60
+ response.end(renderCallbackPage(true));
61
+ resolve(result);
62
+ } catch (error) {
63
+ response.end(renderCallbackPage(false));
64
+ reject(error);
65
+ } finally {
66
+ server.close();
67
+ }
61
68
  });
62
69
  });
63
70
  },
@@ -1 +1 @@
1
- {"version":3,"file":"loopback.js","names":[],"sources":["../../src/auth/loopback.ts"],"sourcesContent":["import { createServer, type Server } from 'node:http';\nimport { createDiagnosticMessage } from 'generaltranslation/diagnostics';\nimport { renderCallbackPage } from './callbackPage.js';\n\nexport const LOOPBACK_CALLBACK_PATH = '/callback';\nconst DEFAULT_CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;\n\nexport type LoopbackServer = {\n /** Loopback redirect URI bound to an ephemeral port, e.g. http://127.0.0.1:53211/callback */\n redirectUri: string;\n /** Resolves with the raw callback parameters of the first request to the callback path. */\n waitForCallback: (timeoutMs?: number) => Promise<URL>;\n close: () => void;\n};\n\nfunction listen(server: Server): Promise<number> {\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n // RFC 8252 §7.3: bind the loopback interface on an ephemeral port; the\n // authorization server matches loopback redirect URIs ignoring the port.\n server.listen(0, '127.0.0.1', () => {\n server.off('error', reject);\n const address = server.address();\n if (!address || typeof address === 'string') {\n server.close();\n reject(\n new Error(\n createDiagnosticMessage({\n whatHappened: 'Could not resolve the loopback callback address',\n })\n )\n );\n return;\n }\n resolve(address.port);\n });\n });\n}\n\nexport async function startLoopbackServer(): Promise<LoopbackServer> {\n const server = createServer();\n const port = await listen(server);\n const origin = `http://127.0.0.1:${port}`;\n let timeout: NodeJS.Timeout | undefined;\n\n return {\n redirectUri: `${origin}${LOOPBACK_CALLBACK_PATH}`,\n waitForCallback(timeoutMs = DEFAULT_CALLBACK_TIMEOUT_MS) {\n return new Promise((resolve, reject) => {\n let settled = false;\n timeout = setTimeout(() => {\n settled = true;\n server.close();\n reject(\n new Error(\n createDiagnosticMessage({\n whatHappened: 'Timed out waiting for the browser to sign in',\n fix: 'Run `gt login` again',\n })\n )\n );\n }, timeoutMs);\n\n server.on('request', (request, response) => {\n const target = request.url ?? '/';\n const url = URL.canParse(target, origin)\n ? new URL(target, origin)\n : undefined;\n if (\n !url ||\n settled ||\n request.method !== 'GET' ||\n url.origin !== origin ||\n url.pathname !== LOOPBACK_CALLBACK_PATH\n ) {\n response.writeHead(404, {\n 'cache-control': 'no-store',\n 'content-type': 'text/plain; charset=utf-8',\n });\n response.end('Not found');\n return;\n }\n\n settled = true;\n clearTimeout(timeout);\n response.writeHead(200, {\n 'cache-control': 'no-store',\n 'content-security-policy':\n \"default-src 'none'; style-src 'unsafe-inline'\",\n 'content-type': 'text/html; charset=utf-8',\n 'x-content-type-options': 'nosniff',\n });\n response.end(renderCallbackPage());\n server.close();\n resolve(url);\n });\n });\n },\n close() {\n clearTimeout(timeout);\n server.close();\n },\n };\n}\n"],"mappings":";;;;AAIA,MAAa,yBAAyB;AACtC,MAAM,8BAA8B,MAAS;AAU7C,SAAS,OAAO,QAAiC;AAC/C,QAAO,IAAI,SAAS,SAAS,WAAW;AACtC,SAAO,KAAK,SAAS,OAAO;AAG5B,SAAO,OAAO,GAAG,mBAAmB;AAClC,UAAO,IAAI,SAAS,OAAO;GAC3B,MAAM,UAAU,OAAO,SAAS;AAChC,OAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,WAAO,OAAO;AACd,WACE,IAAI,MACF,wBAAwB,EACtB,cAAc,mDACf,CAAC,CACH,CACF;AACD;;AAEF,WAAQ,QAAQ,KAAK;IACrB;GACF;;AAGJ,eAAsB,sBAA+C;CACnE,MAAM,SAAS,cAAc;CAE7B,MAAM,SAAS,oBAAoB,MADhB,OAAO,OAAO;CAEjC,IAAI;AAEJ,QAAO;EACL,aAAa,GAAG,SAAS;EACzB,gBAAgB,YAAY,6BAA6B;AACvD,UAAO,IAAI,SAAS,SAAS,WAAW;IACtC,IAAI,UAAU;AACd,cAAU,iBAAiB;AACzB,eAAU;AACV,YAAO,OAAO;AACd,YACE,IAAI,MACF,wBAAwB;MACtB,cAAc;MACd,KAAK;MACN,CAAC,CACH,CACF;OACA,UAAU;AAEb,WAAO,GAAG,YAAY,SAAS,aAAa;KAC1C,MAAM,SAAS,QAAQ,OAAO;KAC9B,MAAM,MAAM,IAAI,SAAS,QAAQ,OAAO,GACpC,IAAI,IAAI,QAAQ,OAAO,GACvB,KAAA;AACJ,SACE,CAAC,OACD,WACA,QAAQ,WAAW,SACnB,IAAI,WAAW,UACf,IAAI,aAAA,aACJ;AACA,eAAS,UAAU,KAAK;OACtB,iBAAiB;OACjB,gBAAgB;OACjB,CAAC;AACF,eAAS,IAAI,YAAY;AACzB;;AAGF,eAAU;AACV,kBAAa,QAAQ;AACrB,cAAS,UAAU,KAAK;MACtB,iBAAiB;MACjB,2BACE;MACF,gBAAgB;MAChB,0BAA0B;MAC3B,CAAC;AACF,cAAS,IAAI,oBAAoB,CAAC;AAClC,YAAO,OAAO;AACd,aAAQ,IAAI;MACZ;KACF;;EAEJ,QAAQ;AACN,gBAAa,QAAQ;AACrB,UAAO,OAAO;;EAEjB"}
1
+ {"version":3,"file":"loopback.js","names":[],"sources":["../../src/auth/loopback.ts"],"sourcesContent":["import { createServer, type Server } from 'node:http';\nimport { createDiagnosticMessage } from 'generaltranslation/diagnostics';\nimport { renderCallbackPage } from './callbackPage.js';\n\nexport const LOOPBACK_CALLBACK_PATH = '/callback';\nconst DEFAULT_CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;\n\nexport type LoopbackServer = {\n /** Loopback redirect URI bound to an ephemeral port, e.g. http://127.0.0.1:53211/callback */\n redirectUri: string;\n /** Shows the result page only after the callback has been validated and saved. */\n waitForCallback: <T>(\n onCallback: (url: URL) => Promise<T>,\n timeoutMs?: number\n ) => Promise<T>;\n close: () => void;\n};\n\nfunction listen(server: Server): Promise<number> {\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n // RFC 8252 §7.3: bind the loopback interface on an ephemeral port; the\n // authorization server matches loopback redirect URIs ignoring the port.\n server.listen(0, '127.0.0.1', () => {\n server.off('error', reject);\n const address = server.address();\n if (!address || typeof address === 'string') {\n server.close();\n reject(\n new Error(\n createDiagnosticMessage({\n whatHappened: 'Could not resolve the loopback callback address',\n })\n )\n );\n return;\n }\n resolve(address.port);\n });\n });\n}\n\nexport async function startLoopbackServer(): Promise<LoopbackServer> {\n const server = createServer();\n const port = await listen(server);\n const origin = `http://127.0.0.1:${port}`;\n let timeout: NodeJS.Timeout | undefined;\n\n return {\n redirectUri: `${origin}${LOOPBACK_CALLBACK_PATH}`,\n waitForCallback<T>(\n onCallback: (url: URL) => Promise<T>,\n timeoutMs = DEFAULT_CALLBACK_TIMEOUT_MS\n ) {\n return new Promise<T>((resolve, reject) => {\n let settled = false;\n timeout = setTimeout(() => {\n settled = true;\n server.close();\n reject(\n new Error(\n createDiagnosticMessage({\n whatHappened: 'Timed out waiting for authentication',\n fix: 'Run `gt login` again',\n })\n )\n );\n }, timeoutMs);\n\n server.on('request', async (request, response) => {\n const target = request.url ?? '/';\n const url = URL.canParse(target, origin)\n ? new URL(target, origin)\n : undefined;\n if (\n !url ||\n settled ||\n request.method !== 'GET' ||\n url.origin !== origin ||\n url.pathname !== LOOPBACK_CALLBACK_PATH\n ) {\n response.writeHead(404, {\n 'cache-control': 'no-store',\n 'content-type': 'text/plain; charset=utf-8',\n });\n response.end('Not found');\n return;\n }\n\n settled = true;\n clearTimeout(timeout);\n response.writeHead(200, {\n 'cache-control': 'no-store',\n 'content-security-policy':\n \"default-src 'none'; style-src 'unsafe-inline'\",\n 'content-type': 'text/html; charset=utf-8',\n 'x-content-type-options': 'nosniff',\n });\n try {\n const result = await onCallback(url);\n response.end(renderCallbackPage(true));\n resolve(result);\n } catch (error) {\n response.end(renderCallbackPage(false));\n reject(error);\n } finally {\n server.close();\n }\n });\n });\n },\n close() {\n clearTimeout(timeout);\n server.close();\n },\n };\n}\n"],"mappings":";;;;AAIA,MAAa,yBAAyB;AACtC,MAAM,8BAA8B,MAAS;AAa7C,SAAS,OAAO,QAAiC;AAC/C,QAAO,IAAI,SAAS,SAAS,WAAW;AACtC,SAAO,KAAK,SAAS,OAAO;AAG5B,SAAO,OAAO,GAAG,mBAAmB;AAClC,UAAO,IAAI,SAAS,OAAO;GAC3B,MAAM,UAAU,OAAO,SAAS;AAChC,OAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,WAAO,OAAO;AACd,WACE,IAAI,MACF,wBAAwB,EACtB,cAAc,mDACf,CAAC,CACH,CACF;AACD;;AAEF,WAAQ,QAAQ,KAAK;IACrB;GACF;;AAGJ,eAAsB,sBAA+C;CACnE,MAAM,SAAS,cAAc;CAE7B,MAAM,SAAS,oBAAoB,MADhB,OAAO,OAAO;CAEjC,IAAI;AAEJ,QAAO;EACL,aAAa,GAAG,SAAS;EACzB,gBACE,YACA,YAAY,6BACZ;AACA,UAAO,IAAI,SAAY,SAAS,WAAW;IACzC,IAAI,UAAU;AACd,cAAU,iBAAiB;AACzB,eAAU;AACV,YAAO,OAAO;AACd,YACE,IAAI,MACF,wBAAwB;MACtB,cAAc;MACd,KAAK;MACN,CAAC,CACH,CACF;OACA,UAAU;AAEb,WAAO,GAAG,WAAW,OAAO,SAAS,aAAa;KAChD,MAAM,SAAS,QAAQ,OAAO;KAC9B,MAAM,MAAM,IAAI,SAAS,QAAQ,OAAO,GACpC,IAAI,IAAI,QAAQ,OAAO,GACvB,KAAA;AACJ,SACE,CAAC,OACD,WACA,QAAQ,WAAW,SACnB,IAAI,WAAW,UACf,IAAI,aAAA,aACJ;AACA,eAAS,UAAU,KAAK;OACtB,iBAAiB;OACjB,gBAAgB;OACjB,CAAC;AACF,eAAS,IAAI,YAAY;AACzB;;AAGF,eAAU;AACV,kBAAa,QAAQ;AACrB,cAAS,UAAU,KAAK;MACtB,iBAAiB;MACjB,2BACE;MACF,gBAAgB;MAChB,0BAA0B;MAC3B,CAAC;AACF,SAAI;MACF,MAAM,SAAS,MAAM,WAAW,IAAI;AACpC,eAAS,IAAI,mBAAmB,KAAK,CAAC;AACtC,cAAQ,OAAO;cACR,OAAO;AACd,eAAS,IAAI,mBAAmB,MAAM,CAAC;AACvC,aAAO,MAAM;eACL;AACR,aAAO,OAAO;;MAEhB;KACF;;EAEJ,QAAQ;AACN,gBAAa,QAAQ;AACrB,UAAO,OAAO;;EAEjB"}
@@ -28,7 +28,7 @@ export type LoginOptions = OAuthRequestOptions & {
28
28
  openBrowser?: (url: string) => Promise<unknown>;
29
29
  timeoutMs?: number;
30
30
  };
31
- /** Browser S256/loopback, or device login for --no-browser and bind failure. */
31
+ /** Browser S256/loopback, or device login for SSH, --no-browser and bind failure. */
32
32
  export declare function login(options?: LoginOptions): Promise<OAuthTokens>;
33
33
  /** Whether a stored login exists for this API; does not refresh or contact the server. */
34
34
  export declare function hasLogin(options: UserTokenProviderOptions): Promise<boolean>;
@@ -122,9 +122,12 @@ async function loginWithDeviceCode(options) {
122
122
  await writeOAuthTokens(tokens, authBaseUrl);
123
123
  return tokens;
124
124
  }
125
- /** Browser S256/loopback, or device login for --no-browser and bind failure. */
125
+ /** Browser S256/loopback, or device login for SSH, --no-browser and bind failure. */
126
126
  async function login(options = {}) {
127
- if (options.noBrowser) return loginWithDeviceCode(options);
127
+ if (options.noBrowser || process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY) return loginWithDeviceCode({
128
+ ...options,
129
+ noBrowser: true
130
+ });
128
131
  const authBaseUrl = options.authBaseUrl ?? getAuthBaseUrl();
129
132
  const resource = loginResource(options);
130
133
  const config = await configuration({
@@ -135,7 +138,6 @@ async function login(options = {}) {
135
138
  });
136
139
  const loopback = await startLoopbackServer().catch(() => void 0);
137
140
  if (!loopback) return loginWithDeviceCode(options);
138
- let result;
139
141
  try {
140
142
  const codeVerifier = oidc.randomPKCECodeVerifier();
141
143
  const state = oidc.randomState();
@@ -148,27 +150,27 @@ async function login(options = {}) {
148
150
  code_challenge_method: "S256"
149
151
  });
150
152
  assertEndpoint(authorizationUrl, new URL(authBaseUrl));
151
- const callback = loopback.waitForCallback(options.timeoutMs);
153
+ const callback = loopback.waitForCallback(async (callbackUrl) => {
154
+ const tokens = {
155
+ ...parseTokens(await oidc.authorizationCodeGrant(config, callbackUrl, {
156
+ pkceCodeVerifier: codeVerifier,
157
+ expectedState: state,
158
+ idTokenExpected: true
159
+ }, { resource }).catch((error) => {
160
+ throw oauthError(error, "Failed to authenticate via web browser");
161
+ })),
162
+ resource
163
+ };
164
+ await writeOAuthTokens(tokens, authBaseUrl);
165
+ return tokens;
166
+ }, options.timeoutMs);
152
167
  callback.catch(() => void 0);
153
168
  options.onAuthorizationUrl?.(authorizationUrl.href);
154
169
  (options.openBrowser ?? open)(authorizationUrl.href).catch(() => void 0);
155
- const callbackUrl = await callback;
156
- result = await oidc.authorizationCodeGrant(config, callbackUrl, {
157
- pkceCodeVerifier: codeVerifier,
158
- expectedState: state,
159
- idTokenExpected: true
160
- }, { resource }).catch((error) => {
161
- throw oauthError(error, "Could not validate the sign-in response");
162
- });
170
+ return await callback;
163
171
  } finally {
164
172
  loopback.close();
165
173
  }
166
- const tokens = {
167
- ...parseTokens(result),
168
- resource
169
- };
170
- await writeOAuthTokens(tokens, authBaseUrl);
171
- return tokens;
172
174
  }
173
175
  const pendingRefreshes = /* @__PURE__ */ new Map();
174
176
  function refreshTokens(options) {
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.js","names":[],"sources":["../../src/auth/oauth.ts"],"sourcesContent":["import open from 'open';\nimport * as oidc from 'openid-client';\nimport type { UserTokenProvider } from 'generaltranslation/api';\nimport { createDiagnosticMessage } from 'generaltranslation/diagnostics';\nimport { defaultBaseUrl } from 'generaltranslation/internal';\nimport { GT_DASHBOARD_URL } from '../utils/constants.js';\nimport { logger } from '../console/logger.js';\nimport {\n deleteOAuthTokens,\n readOAuthTokens,\n writeOAuthTokens,\n} from './credentialStore.js';\nimport type { OAuthTokens } from './credentialStore.js';\nimport { UserAuthError, loginRequiredError } from './errors.js';\nimport { startLoopbackServer } from './loopback.js';\n\n/** Public native client seeded by the authorization server; never a secret. */\nconst OAUTH_CLIENT_ID = 'gt-cli';\n/** Explicit on both grants: identity, refresh and the user's dashboard permissions.\n * Must match the seeded gt-cli client; the device endpoint does not default scope.\n */\nconst OAUTH_SCOPE = 'openid profile offline_access gt:*';\n// Refresh before expiry so an in-flight request does not carry an expired token.\nconst TOKEN_REFRESH_BUFFER_MS = 30_000;\n\nexport type DeviceCode = {\n userCode: string;\n verificationUri: string;\n verificationUriComplete?: string;\n};\nexport type UserInfo = { email?: string; name?: string; sub: string };\ntype OAuthRequestOptions = { authBaseUrl?: string; fetch?: typeof fetch };\nexport type UserTokenProviderOptions = OAuthRequestOptions & {\n /** API the requests go to; must match the resource the login was issued for. */\n baseUrl: string;\n};\nexport type LoginOptions = OAuthRequestOptions & {\n /** API the token is issued for; GT_API_URL overrides, then the public API. */\n baseUrl?: string;\n noBrowser?: boolean;\n onDeviceCode?: (deviceCode: DeviceCode) => void;\n signal?: AbortSignal;\n onAuthorizationUrl?: (url: string) => void;\n openBrowser?: (url: string) => Promise<unknown>;\n timeoutMs?: number;\n};\n\nfunction oauthFailure(\n whatHappened: string,\n fix?: string,\n details?: string\n): UserAuthError {\n return new UserAuthError('oauth', whatHappened, fix, details);\n}\n\nfunction oauthError(error: unknown, fallback: string): UserAuthError {\n if (\n error instanceof oidc.AuthorizationResponseError ||\n error instanceof oidc.ResponseBodyError\n ) {\n const descriptions: Record<string, string> = {\n access_denied: 'Sign in was denied in the browser',\n expired_token: 'The sign-in code expired before it was approved',\n invalid_scope: 'The authorization server rejected the requested scopes',\n invalid_grant: 'Your login or sign-in code expired or was already used',\n invalid_client: 'This authorization server does not recognize the gt CLI',\n unauthorized_client:\n 'This authorization server does not recognize the gt CLI',\n };\n return oauthFailure(\n descriptions[error.error] ?? fallback,\n ['invalid_client', 'unauthorized_client'].includes(error.error)\n ? 'Check GT_AUTH_URL or update the server'\n : 'Run `gt login` again',\n // Scope descriptions are useful configuration diagnostics, not token data.\n error.error === 'invalid_scope' ? error.error_description : undefined\n );\n }\n if (error instanceof oidc.ClientError) {\n const status =\n error.cause instanceof Response ? ` (HTTP ${error.cause.status})` : '';\n return oauthFailure(\n `${fallback}${status}`,\n 'Check the authorization server and try again',\n error.code\n );\n }\n if (\n error instanceof Error &&\n ['AbortError', 'TimeoutError'].includes(error.name)\n ) {\n return oauthFailure(\n 'Sign in was cancelled or timed out',\n 'Run `gt login` again'\n );\n }\n // Do not expose transport causes, callback URLs or token responses.\n return oauthFailure(fallback, 'Check the authorization server and try again');\n}\n\nfunction assertEndpoint(url: URL, issuer: URL): void {\n if (\n url.username ||\n url.password ||\n url.hash ||\n (url.protocol !== 'https:' &&\n !(issuer.protocol === 'http:' && url.origin === issuer.origin))\n ) {\n throw oauthFailure(\n 'The authorization server supplied an unsafe endpoint',\n 'Use HTTPS, or same-origin local development HTTP'\n );\n }\n}\n\nasync function configuration(\n {\n authBaseUrl = getAuthBaseUrl(),\n fetch: fetchImplementation = globalThis.fetch,\n }: OAuthRequestOptions,\n signal?: AbortSignal\n): Promise<oidc.Configuration> {\n const issuer = new URL(authBaseUrl);\n const local =\n issuer.hostname === 'localhost' ||\n issuer.hostname.endsWith('.localhost') ||\n issuer.hostname === '127.0.0.1' ||\n issuer.hostname === '[::1]';\n if (\n issuer.username ||\n issuer.password ||\n issuer.search ||\n issuer.hash ||\n (issuer.protocol !== 'https:' && !(issuer.protocol === 'http:' && local))\n ) {\n throw oauthFailure(\n 'GT_AUTH_URL is not a trusted issuer URL',\n 'Use HTTPS, or a local development HTTP issuer without credentials, query or fragment'\n );\n }\n return oidc.discovery(issuer, OAUTH_CLIENT_ID, undefined, oidc.None(), {\n execute:\n issuer.protocol === 'http:'\n ? [oidc.enableNonRepudiationChecks, oidc.allowInsecureRequests]\n : [oidc.enableNonRepudiationChecks],\n [oidc.customFetch]: (url, init) => {\n assertEndpoint(new URL(url), issuer);\n return fetchImplementation(url, {\n ...init,\n // Include operation cancellation in JWKS fetches as well as token requests.\n signal: signal\n ? AbortSignal.any([signal, ...(init.signal ? [init.signal] : [])])\n : init.signal,\n body:\n init.body instanceof Uint8Array\n ? new Uint8Array(init.body)\n : init.body,\n });\n },\n });\n}\n\nfunction parseTokens(\n result: oidc.TokenEndpointResponse & oidc.TokenEndpointResponseHelpers,\n previous?: OAuthTokens\n): Omit<OAuthTokens, 'resource'> {\n const subject = result.claims()?.sub ?? previous?.subject;\n if (!subject || (previous && subject !== previous.subject)) {\n throw oauthFailure(\n 'The authorization server did not confirm the expected account identity',\n 'Run `gt login` again'\n );\n }\n if (\n result.token_type !== 'bearer' ||\n typeof result.expires_in !== 'number' ||\n !Number.isFinite(result.expires_in) ||\n result.expires_in <= 0\n ) {\n throw oauthFailure(\n 'The authorization server returned an unsupported token type or lifetime'\n );\n }\n return {\n accessToken: result.access_token,\n expiresAt: Date.now() + result.expires_in * 1000,\n refreshToken: result.refresh_token ?? previous?.refreshToken ?? '',\n scope: result.scope ?? previous?.scope ?? OAUTH_SCOPE,\n tokenType: result.token_type,\n subject,\n };\n}\n\nfunction getAuthBaseUrl(): string {\n return (process.env.GT_AUTH_URL ?? `${GT_DASHBOARD_URL}/api/auth`).replace(\n /\\/$/,\n ''\n );\n}\n\n/** Resource indicators are the API origin serialized as a URL href (trailing slash). */\nfunction toApiResource(baseUrl: string): string {\n return new URL(baseUrl).href;\n}\n\nfunction loginResource(options: LoginOptions): string {\n return toApiResource(\n process.env.GT_API_URL ?? options.baseUrl ?? defaultBaseUrl\n );\n}\n\nasync function loginWithDeviceCode(\n options: LoginOptions\n): Promise<OAuthTokens> {\n if (options.noBrowser && !options.onDeviceCode) {\n throw oauthFailure(\n 'Device login needs a way to display the verification code',\n 'Provide onDeviceCode when using noBrowser'\n );\n }\n const authBaseUrl = options.authBaseUrl ?? getAuthBaseUrl();\n const resource = loginResource(options);\n const lifetime = new AbortController();\n const signal = options.signal\n ? AbortSignal.any([options.signal, lifetime.signal])\n : lifetime.signal;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let result: oidc.TokenEndpointResponse & oidc.TokenEndpointResponseHelpers;\n try {\n signal.throwIfAborted();\n const config = await configuration({ ...options, authBaseUrl }, signal);\n const started = Date.now();\n const response = await oidc.initiateDeviceAuthorization(config, {\n scope: OAUTH_SCOPE,\n resource,\n });\n const deadline = started + response.expires_in * 1000;\n const checkLifetime = () => {\n if (Date.now() >= deadline)\n lifetime.abort(new DOMException('Device code expired', 'TimeoutError'));\n signal.throwIfAborted();\n };\n checkLifetime();\n timer = setTimeout(\n () =>\n lifetime.abort(new DOMException('Device code expired', 'TimeoutError')),\n deadline - Date.now()\n );\n assertEndpoint(new URL(response.verification_uri), new URL(authBaseUrl));\n if (response.verification_uri_complete)\n assertEndpoint(\n new URL(response.verification_uri_complete),\n new URL(authBaseUrl)\n );\n options.onDeviceCode?.({\n userCode: response.user_code,\n verificationUri: response.verification_uri,\n verificationUriComplete: response.verification_uri_complete,\n });\n checkLifetime();\n if (!options.noBrowser) {\n void (options.openBrowser ?? open)(\n response.verification_uri_complete ?? response.verification_uri\n ).catch(() => undefined);\n }\n result = await oidc.pollDeviceAuthorizationGrant(\n config,\n response,\n { resource },\n { signal }\n );\n checkLifetime();\n } catch (error) {\n throw oauthError(error, 'Could not complete device sign in');\n } finally {\n clearTimeout(timer);\n }\n const tokens = { ...parseTokens(result), resource };\n await writeOAuthTokens(tokens, authBaseUrl);\n return tokens;\n}\n\n/** Browser S256/loopback, or device login for --no-browser and bind failure. */\nexport async function login(options: LoginOptions = {}): Promise<OAuthTokens> {\n if (options.noBrowser) return loginWithDeviceCode(options);\n const authBaseUrl = options.authBaseUrl ?? getAuthBaseUrl();\n const resource = loginResource(options);\n const config = await configuration({ ...options, authBaseUrl }).catch(\n (error: unknown) => {\n throw oauthError(error, 'Could not discover the authorization server');\n }\n );\n const loopback = await startLoopbackServer().catch(() => undefined);\n if (!loopback) return loginWithDeviceCode(options);\n let result: oidc.TokenEndpointResponse & oidc.TokenEndpointResponseHelpers;\n try {\n const codeVerifier = oidc.randomPKCECodeVerifier();\n const state = oidc.randomState();\n const authorizationUrl = oidc.buildAuthorizationUrl(config, {\n redirect_uri: loopback.redirectUri,\n scope: OAUTH_SCOPE,\n resource,\n state,\n code_challenge: await oidc.calculatePKCECodeChallenge(codeVerifier),\n code_challenge_method: 'S256',\n });\n assertEndpoint(authorizationUrl, new URL(authBaseUrl));\n const callback = loopback.waitForCallback(options.timeoutMs);\n // Printing/launching may fail before we await the listener.\n callback.catch(() => undefined);\n options.onAuthorizationUrl?.(authorizationUrl.href);\n void (options.openBrowser ?? open)(authorizationUrl.href).catch(\n () => undefined\n );\n const callbackUrl = await callback;\n result = await oidc\n .authorizationCodeGrant(\n config,\n callbackUrl,\n {\n pkceCodeVerifier: codeVerifier,\n expectedState: state,\n idTokenExpected: true,\n },\n { resource }\n )\n .catch((error: unknown) => {\n throw oauthError(error, 'Could not validate the sign-in response');\n });\n } finally {\n loopback.close();\n }\n const tokens = { ...parseTokens(result), resource };\n await writeOAuthTokens(tokens, authBaseUrl);\n return tokens;\n}\n\nconst pendingRefreshes = new Map<string, Promise<OAuthTokens>>();\nfunction refreshTokens(options: OAuthRequestOptions): Promise<OAuthTokens> {\n const authBaseUrl = options.authBaseUrl ?? getAuthBaseUrl();\n const pending = pendingRefreshes.get(authBaseUrl);\n if (pending) return pending;\n const refresh = exchangeRefreshToken({ ...options, authBaseUrl }).finally(\n () => pendingRefreshes.delete(authBaseUrl)\n );\n pendingRefreshes.set(authBaseUrl, refresh);\n return refresh;\n}\n\nasync function exchangeRefreshToken(\n options: OAuthRequestOptions & { authBaseUrl: string }\n): Promise<OAuthTokens> {\n const current = await readOAuthTokens(options.authBaseUrl);\n if (!current?.refreshToken) throw loginRequiredError();\n const result = await (async () =>\n oidc.refreshTokenGrant(\n await configuration(options),\n current.refreshToken\n ))().catch((error: unknown) => {\n throw oauthError(error, 'Could not refresh your login');\n });\n // The grant reuses the bound resource, so the refreshed token keeps its audience.\n const tokens = {\n ...parseTokens(result, current),\n resource: current.resource,\n };\n await writeOAuthTokens(tokens, options.authBaseUrl);\n return tokens;\n}\n\nasync function validTokens(\n options: OAuthRequestOptions\n): Promise<OAuthTokens | undefined> {\n const tokens = await readOAuthTokens(options.authBaseUrl ?? getAuthBaseUrl());\n if (!tokens || tokens.expiresAt > Date.now() + TOKEN_REFRESH_BUFFER_MS)\n return tokens;\n return refreshTokens(options);\n}\n\n/** Whether a stored login exists for this API; does not refresh or contact the server. */\nexport async function hasLogin(\n options: UserTokenProviderOptions\n): Promise<boolean> {\n const tokens = await readOAuthTokens(options.authBaseUrl ?? getAuthBaseUrl());\n return tokens?.resource === toApiResource(options.baseUrl);\n}\n\n/** Construction is deliberately free of auth/storage/discovery I/O. */\nexport function createUserTokenProvider(\n options: UserTokenProviderOptions\n): UserTokenProvider {\n // Check the audience before any refresh so a stale mismatched login is\n // never rotated on behalf of the wrong API.\n const storedTokens = async (): Promise<OAuthTokens> => {\n const tokens = await readOAuthTokens(\n options.authBaseUrl ?? getAuthBaseUrl()\n );\n if (!tokens) throw loginRequiredError();\n const resource = toApiResource(options.baseUrl);\n if (tokens.resource !== resource) {\n throw new UserAuthError(\n 'resource_mismatch',\n `You are signed in to ${tokens.resource}, but this project uses ${resource}`,\n 'Run `gt login` in this project to sign in to its API'\n );\n }\n return tokens;\n };\n return {\n getAccessToken: async () => {\n const tokens = await storedTokens();\n if (tokens.expiresAt > Date.now() + TOKEN_REFRESH_BUFFER_MS)\n return tokens.accessToken;\n return (await refreshTokens(options)).accessToken;\n },\n refreshAccessToken: async () => {\n await storedTokens();\n return (await refreshTokens(options)).accessToken;\n },\n };\n}\n\nexport async function logout(options: OAuthRequestOptions = {}): Promise<void> {\n const authBaseUrl = options.authBaseUrl ?? getAuthBaseUrl();\n try {\n const tokens = await readOAuthTokens(authBaseUrl);\n if (tokens?.refreshToken)\n await oidc.tokenRevocation(\n await configuration(options),\n tokens.refreshToken,\n { token_type_hint: 'refresh_token' }\n );\n } catch {\n logger.warn(\n createDiagnosticMessage({\n source: 'gt',\n severity: 'Warning',\n whatHappened:\n 'Signed out locally, but remote session revocation could not be confirmed',\n fix: 'Revoke the session in the dashboard if needed',\n })\n );\n } finally {\n await deleteOAuthTokens(authBaseUrl);\n }\n}\n\nexport async function whoAmI(\n options: OAuthRequestOptions = {}\n): Promise<UserInfo> {\n const tokens = await validTokens(options);\n if (!tokens) throw loginRequiredError();\n try {\n const result = await oidc.fetchUserInfo(\n await configuration(options),\n tokens.accessToken,\n tokens.subject\n );\n return { sub: result.sub, email: result.email, name: result.name };\n } catch (error) {\n throw oauthError(error, 'Could not validate your account identity');\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"oauth.js","names":[],"sources":["../../src/auth/oauth.ts"],"sourcesContent":["import open from 'open';\nimport * as oidc from 'openid-client';\nimport type { UserTokenProvider } from 'generaltranslation/api';\nimport { createDiagnosticMessage } from 'generaltranslation/diagnostics';\nimport { defaultBaseUrl } from 'generaltranslation/internal';\nimport { GT_DASHBOARD_URL } from '../utils/constants.js';\nimport { logger } from '../console/logger.js';\nimport {\n deleteOAuthTokens,\n readOAuthTokens,\n writeOAuthTokens,\n} from './credentialStore.js';\nimport type { OAuthTokens } from './credentialStore.js';\nimport { UserAuthError, loginRequiredError } from './errors.js';\nimport { startLoopbackServer } from './loopback.js';\n\n/** Public native client seeded by the authorization server; never a secret. */\nconst OAUTH_CLIENT_ID = 'gt-cli';\n/** Explicit on both grants: identity, refresh and the user's dashboard permissions.\n * Must match the seeded gt-cli client; the device endpoint does not default scope.\n */\nconst OAUTH_SCOPE = 'openid profile offline_access gt:*';\n// Refresh before expiry so an in-flight request does not carry an expired token.\nconst TOKEN_REFRESH_BUFFER_MS = 30_000;\n\nexport type DeviceCode = {\n userCode: string;\n verificationUri: string;\n verificationUriComplete?: string;\n};\nexport type UserInfo = { email?: string; name?: string; sub: string };\ntype OAuthRequestOptions = { authBaseUrl?: string; fetch?: typeof fetch };\nexport type UserTokenProviderOptions = OAuthRequestOptions & {\n /** API the requests go to; must match the resource the login was issued for. */\n baseUrl: string;\n};\nexport type LoginOptions = OAuthRequestOptions & {\n /** API the token is issued for; GT_API_URL overrides, then the public API. */\n baseUrl?: string;\n noBrowser?: boolean;\n onDeviceCode?: (deviceCode: DeviceCode) => void;\n signal?: AbortSignal;\n onAuthorizationUrl?: (url: string) => void;\n openBrowser?: (url: string) => Promise<unknown>;\n timeoutMs?: number;\n};\n\nfunction oauthFailure(\n whatHappened: string,\n fix?: string,\n details?: string\n): UserAuthError {\n return new UserAuthError('oauth', whatHappened, fix, details);\n}\n\nfunction oauthError(error: unknown, fallback: string): UserAuthError {\n if (\n error instanceof oidc.AuthorizationResponseError ||\n error instanceof oidc.ResponseBodyError\n ) {\n const descriptions: Record<string, string> = {\n access_denied: 'Sign in was denied in the browser',\n expired_token: 'The sign-in code expired before it was approved',\n invalid_scope: 'The authorization server rejected the requested scopes',\n invalid_grant: 'Your login or sign-in code expired or was already used',\n invalid_client: 'This authorization server does not recognize the gt CLI',\n unauthorized_client:\n 'This authorization server does not recognize the gt CLI',\n };\n return oauthFailure(\n descriptions[error.error] ?? fallback,\n ['invalid_client', 'unauthorized_client'].includes(error.error)\n ? 'Check GT_AUTH_URL or update the server'\n : 'Run `gt login` again',\n // Scope descriptions are useful configuration diagnostics, not token data.\n error.error === 'invalid_scope' ? error.error_description : undefined\n );\n }\n if (error instanceof oidc.ClientError) {\n const status =\n error.cause instanceof Response ? ` (HTTP ${error.cause.status})` : '';\n return oauthFailure(\n `${fallback}${status}`,\n 'Check the authorization server and try again',\n error.code\n );\n }\n if (\n error instanceof Error &&\n ['AbortError', 'TimeoutError'].includes(error.name)\n ) {\n return oauthFailure(\n 'Sign in was cancelled or timed out',\n 'Run `gt login` again'\n );\n }\n // Do not expose transport causes, callback URLs or token responses.\n return oauthFailure(fallback, 'Check the authorization server and try again');\n}\n\nfunction assertEndpoint(url: URL, issuer: URL): void {\n if (\n url.username ||\n url.password ||\n url.hash ||\n (url.protocol !== 'https:' &&\n !(issuer.protocol === 'http:' && url.origin === issuer.origin))\n ) {\n throw oauthFailure(\n 'The authorization server supplied an unsafe endpoint',\n 'Use HTTPS, or same-origin local development HTTP'\n );\n }\n}\n\nasync function configuration(\n {\n authBaseUrl = getAuthBaseUrl(),\n fetch: fetchImplementation = globalThis.fetch,\n }: OAuthRequestOptions,\n signal?: AbortSignal\n): Promise<oidc.Configuration> {\n const issuer = new URL(authBaseUrl);\n const local =\n issuer.hostname === 'localhost' ||\n issuer.hostname.endsWith('.localhost') ||\n issuer.hostname === '127.0.0.1' ||\n issuer.hostname === '[::1]';\n if (\n issuer.username ||\n issuer.password ||\n issuer.search ||\n issuer.hash ||\n (issuer.protocol !== 'https:' && !(issuer.protocol === 'http:' && local))\n ) {\n throw oauthFailure(\n 'GT_AUTH_URL is not a trusted issuer URL',\n 'Use HTTPS, or a local development HTTP issuer without credentials, query or fragment'\n );\n }\n return oidc.discovery(issuer, OAUTH_CLIENT_ID, undefined, oidc.None(), {\n execute:\n issuer.protocol === 'http:'\n ? [oidc.enableNonRepudiationChecks, oidc.allowInsecureRequests]\n : [oidc.enableNonRepudiationChecks],\n [oidc.customFetch]: (url, init) => {\n assertEndpoint(new URL(url), issuer);\n return fetchImplementation(url, {\n ...init,\n // Include operation cancellation in JWKS fetches as well as token requests.\n signal: signal\n ? AbortSignal.any([signal, ...(init.signal ? [init.signal] : [])])\n : init.signal,\n body:\n init.body instanceof Uint8Array\n ? new Uint8Array(init.body)\n : init.body,\n });\n },\n });\n}\n\nfunction parseTokens(\n result: oidc.TokenEndpointResponse & oidc.TokenEndpointResponseHelpers,\n previous?: OAuthTokens\n): Omit<OAuthTokens, 'resource'> {\n const subject = result.claims()?.sub ?? previous?.subject;\n if (!subject || (previous && subject !== previous.subject)) {\n throw oauthFailure(\n 'The authorization server did not confirm the expected account identity',\n 'Run `gt login` again'\n );\n }\n if (\n result.token_type !== 'bearer' ||\n typeof result.expires_in !== 'number' ||\n !Number.isFinite(result.expires_in) ||\n result.expires_in <= 0\n ) {\n throw oauthFailure(\n 'The authorization server returned an unsupported token type or lifetime'\n );\n }\n return {\n accessToken: result.access_token,\n expiresAt: Date.now() + result.expires_in * 1000,\n refreshToken: result.refresh_token ?? previous?.refreshToken ?? '',\n scope: result.scope ?? previous?.scope ?? OAUTH_SCOPE,\n tokenType: result.token_type,\n subject,\n };\n}\n\nfunction getAuthBaseUrl(): string {\n return (process.env.GT_AUTH_URL ?? `${GT_DASHBOARD_URL}/api/auth`).replace(\n /\\/$/,\n ''\n );\n}\n\n/** Resource indicators are the API origin serialized as a URL href (trailing slash). */\nfunction toApiResource(baseUrl: string): string {\n return new URL(baseUrl).href;\n}\n\nfunction loginResource(options: LoginOptions): string {\n return toApiResource(\n process.env.GT_API_URL ?? options.baseUrl ?? defaultBaseUrl\n );\n}\n\nasync function loginWithDeviceCode(\n options: LoginOptions\n): Promise<OAuthTokens> {\n if (options.noBrowser && !options.onDeviceCode) {\n throw oauthFailure(\n 'Device login needs a way to display the verification code',\n 'Provide onDeviceCode when using noBrowser'\n );\n }\n const authBaseUrl = options.authBaseUrl ?? getAuthBaseUrl();\n const resource = loginResource(options);\n const lifetime = new AbortController();\n const signal = options.signal\n ? AbortSignal.any([options.signal, lifetime.signal])\n : lifetime.signal;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let result: oidc.TokenEndpointResponse & oidc.TokenEndpointResponseHelpers;\n try {\n signal.throwIfAborted();\n const config = await configuration({ ...options, authBaseUrl }, signal);\n const started = Date.now();\n const response = await oidc.initiateDeviceAuthorization(config, {\n scope: OAUTH_SCOPE,\n resource,\n });\n const deadline = started + response.expires_in * 1000;\n const checkLifetime = () => {\n if (Date.now() >= deadline)\n lifetime.abort(new DOMException('Device code expired', 'TimeoutError'));\n signal.throwIfAborted();\n };\n checkLifetime();\n timer = setTimeout(\n () =>\n lifetime.abort(new DOMException('Device code expired', 'TimeoutError')),\n deadline - Date.now()\n );\n assertEndpoint(new URL(response.verification_uri), new URL(authBaseUrl));\n if (response.verification_uri_complete)\n assertEndpoint(\n new URL(response.verification_uri_complete),\n new URL(authBaseUrl)\n );\n options.onDeviceCode?.({\n userCode: response.user_code,\n verificationUri: response.verification_uri,\n verificationUriComplete: response.verification_uri_complete,\n });\n checkLifetime();\n if (!options.noBrowser) {\n void (options.openBrowser ?? open)(\n response.verification_uri_complete ?? response.verification_uri\n ).catch(() => undefined);\n }\n result = await oidc.pollDeviceAuthorizationGrant(\n config,\n response,\n { resource },\n { signal }\n );\n checkLifetime();\n } catch (error) {\n throw oauthError(error, 'Could not complete device sign in');\n } finally {\n clearTimeout(timer);\n }\n const tokens = { ...parseTokens(result), resource };\n await writeOAuthTokens(tokens, authBaseUrl);\n return tokens;\n}\n\n/** Browser S256/loopback, or device login for SSH, --no-browser and bind failure. */\nexport async function login(options: LoginOptions = {}): Promise<OAuthTokens> {\n if (\n options.noBrowser ||\n process.env.SSH_CONNECTION ||\n process.env.SSH_CLIENT ||\n process.env.SSH_TTY\n ) {\n return loginWithDeviceCode({ ...options, noBrowser: true });\n }\n const authBaseUrl = options.authBaseUrl ?? getAuthBaseUrl();\n const resource = loginResource(options);\n const config = await configuration({ ...options, authBaseUrl }).catch(\n (error: unknown) => {\n throw oauthError(error, 'Could not discover the authorization server');\n }\n );\n const loopback = await startLoopbackServer().catch(() => undefined);\n if (!loopback) return loginWithDeviceCode(options);\n try {\n const codeVerifier = oidc.randomPKCECodeVerifier();\n const state = oidc.randomState();\n const authorizationUrl = oidc.buildAuthorizationUrl(config, {\n redirect_uri: loopback.redirectUri,\n scope: OAUTH_SCOPE,\n resource,\n state,\n code_challenge: await oidc.calculatePKCECodeChallenge(codeVerifier),\n code_challenge_method: 'S256',\n });\n assertEndpoint(authorizationUrl, new URL(authBaseUrl));\n const callback = loopback.waitForCallback(async (callbackUrl) => {\n const result = await oidc\n .authorizationCodeGrant(\n config,\n callbackUrl,\n {\n pkceCodeVerifier: codeVerifier,\n expectedState: state,\n idTokenExpected: true,\n },\n { resource }\n )\n .catch((error: unknown) => {\n throw oauthError(error, 'Failed to authenticate via web browser');\n });\n const tokens = { ...parseTokens(result), resource };\n await writeOAuthTokens(tokens, authBaseUrl);\n return tokens;\n }, options.timeoutMs);\n // Printing/launching may fail before we await the listener.\n callback.catch(() => undefined);\n options.onAuthorizationUrl?.(authorizationUrl.href);\n void (options.openBrowser ?? open)(authorizationUrl.href).catch(\n () => undefined\n );\n return await callback;\n } finally {\n loopback.close();\n }\n}\n\nconst pendingRefreshes = new Map<string, Promise<OAuthTokens>>();\nfunction refreshTokens(options: OAuthRequestOptions): Promise<OAuthTokens> {\n const authBaseUrl = options.authBaseUrl ?? getAuthBaseUrl();\n const pending = pendingRefreshes.get(authBaseUrl);\n if (pending) return pending;\n const refresh = exchangeRefreshToken({ ...options, authBaseUrl }).finally(\n () => pendingRefreshes.delete(authBaseUrl)\n );\n pendingRefreshes.set(authBaseUrl, refresh);\n return refresh;\n}\n\nasync function exchangeRefreshToken(\n options: OAuthRequestOptions & { authBaseUrl: string }\n): Promise<OAuthTokens> {\n const current = await readOAuthTokens(options.authBaseUrl);\n if (!current?.refreshToken) throw loginRequiredError();\n const result = await (async () =>\n oidc.refreshTokenGrant(\n await configuration(options),\n current.refreshToken\n ))().catch((error: unknown) => {\n throw oauthError(error, 'Could not refresh your login');\n });\n // The grant reuses the bound resource, so the refreshed token keeps its audience.\n const tokens = {\n ...parseTokens(result, current),\n resource: current.resource,\n };\n await writeOAuthTokens(tokens, options.authBaseUrl);\n return tokens;\n}\n\nasync function validTokens(\n options: OAuthRequestOptions\n): Promise<OAuthTokens | undefined> {\n const tokens = await readOAuthTokens(options.authBaseUrl ?? getAuthBaseUrl());\n if (!tokens || tokens.expiresAt > Date.now() + TOKEN_REFRESH_BUFFER_MS)\n return tokens;\n return refreshTokens(options);\n}\n\n/** Whether a stored login exists for this API; does not refresh or contact the server. */\nexport async function hasLogin(\n options: UserTokenProviderOptions\n): Promise<boolean> {\n const tokens = await readOAuthTokens(options.authBaseUrl ?? getAuthBaseUrl());\n return tokens?.resource === toApiResource(options.baseUrl);\n}\n\n/** Construction is deliberately free of auth/storage/discovery I/O. */\nexport function createUserTokenProvider(\n options: UserTokenProviderOptions\n): UserTokenProvider {\n // Check the audience before any refresh so a stale mismatched login is\n // never rotated on behalf of the wrong API.\n const storedTokens = async (): Promise<OAuthTokens> => {\n const tokens = await readOAuthTokens(\n options.authBaseUrl ?? getAuthBaseUrl()\n );\n if (!tokens) throw loginRequiredError();\n const resource = toApiResource(options.baseUrl);\n if (tokens.resource !== resource) {\n throw new UserAuthError(\n 'resource_mismatch',\n `You are signed in to ${tokens.resource}, but this project uses ${resource}`,\n 'Run `gt login` in this project to sign in to its API'\n );\n }\n return tokens;\n };\n return {\n getAccessToken: async () => {\n const tokens = await storedTokens();\n if (tokens.expiresAt > Date.now() + TOKEN_REFRESH_BUFFER_MS)\n return tokens.accessToken;\n return (await refreshTokens(options)).accessToken;\n },\n refreshAccessToken: async () => {\n await storedTokens();\n return (await refreshTokens(options)).accessToken;\n },\n };\n}\n\nexport async function logout(options: OAuthRequestOptions = {}): Promise<void> {\n const authBaseUrl = options.authBaseUrl ?? getAuthBaseUrl();\n try {\n const tokens = await readOAuthTokens(authBaseUrl);\n if (tokens?.refreshToken)\n await oidc.tokenRevocation(\n await configuration(options),\n tokens.refreshToken,\n { token_type_hint: 'refresh_token' }\n );\n } catch {\n logger.warn(\n createDiagnosticMessage({\n source: 'gt',\n severity: 'Warning',\n whatHappened:\n 'Signed out locally, but remote session revocation could not be confirmed',\n fix: 'Revoke the session in the dashboard if needed',\n })\n );\n } finally {\n await deleteOAuthTokens(authBaseUrl);\n }\n}\n\nexport async function whoAmI(\n options: OAuthRequestOptions = {}\n): Promise<UserInfo> {\n const tokens = await validTokens(options);\n if (!tokens) throw loginRequiredError();\n try {\n const result = await oidc.fetchUserInfo(\n await configuration(options),\n tokens.accessToken,\n tokens.subject\n );\n return { sub: result.sub, email: result.email, name: result.name };\n } catch (error) {\n throw oauthError(error, 'Could not validate your account identity');\n }\n}\n"],"mappings":"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"}
package/dist/cli/base.js CHANGED
@@ -100,7 +100,7 @@ async function loginInteractively(baseUrl, useBrowser = true) {
100
100
  baseUrl,
101
101
  noBrowser: !useBrowser,
102
102
  onDeviceCode: ({ userCode, verificationUri, verificationUriComplete }) => {
103
- logger.message(`${useBrowser ? "Opening your browser. If it does not open, on any device visit" : "On any device, visit"} ${chalk.cyan(verificationUriComplete ?? verificationUri)} and ${verificationUriComplete ? "confirm" : "enter"} the code ${chalk.bold(userCode)}\nWaiting for approval...`);
103
+ logger.message(`Visit:\n\n${chalk.cyan(verificationUriComplete ?? verificationUri)}\n\nThen ${verificationUriComplete ? "confirm" : "enter"} the code ${chalk.bold(userCode)}.\nWaiting for authentication...`);
104
104
  },
105
105
  onAuthorizationUrl: (url) => {
106
106
  logger.message(`Opening your browser to sign in. If it does not open, visit:\n${chalk.cyan(url)}`);
@@ -365,7 +365,7 @@ var BaseCLI = class {
365
365
  try {
366
366
  const baseUrl = resolveConfig(process.cwd())?.config.baseUrl;
367
367
  await loginInteractively(typeof baseUrl === "string" ? baseUrl : void 0, options.browser);
368
- logger.endCommand("Signed in successfully.");
368
+ logger.endCommand("You are now signed in.");
369
369
  } catch (error) {
370
370
  logErrorAndExit(createUserAuthError("Sign in failed", error));
371
371
  }
@@ -598,7 +598,7 @@ See https://www.npmjs.com/package/gt-vue`);
598
598
  const envFramework = framework ?? (isVite ? "vite" : settings.framework);
599
599
  if (!settings.apiKey && !await hasLogin({ baseUrl: settings.baseUrl })) try {
600
600
  await loginInteractively(settings.baseUrl);
601
- logger.message("Signed in successfully.");
601
+ logger.message("You are now signed in.");
602
602
  } catch (error) {
603
603
  logErrorAndExit(createUserAuthError("Sign in failed", error));
604
604
  }