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 +8 -0
- package/dist/auth/callbackPage.d.ts +1 -1
- package/dist/auth/callbackPage.js +11 -10
- package/dist/auth/callbackPage.js.map +1 -1
- package/dist/auth/loopback.d.ts +2 -2
- package/dist/auth/loopback.js +13 -6
- package/dist/auth/loopback.js.map +1 -1
- package/dist/auth/oauth.d.ts +1 -1
- package/dist/auth/oauth.js +20 -18
- package/dist/auth/oauth.js.map +1 -1
- package/dist/cli/base.js +3 -3
- package/dist/cli/base.js.map +1 -1
- package/dist/cli/commands/translate.js +2 -0
- package/dist/cli/commands/translate.js.map +1 -1
- package/dist/formats/files/postprocess/keepUntranslatedPagePaths.d.ts +10 -0
- package/dist/formats/files/postprocess/keepUntranslatedPagePaths.js +101 -0
- package/dist/formats/files/postprocess/keepUntranslatedPagePaths.js.map +1 -0
- package/dist/formats/json/mergeJson.d.ts +8 -1
- package/dist/formats/json/mergeJson.js +13 -8
- package/dist/formats/json/mergeJson.js.map +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/generated/version.js.map +1 -1
- package/dist/utils/localizeStaticUrls.d.ts +1 -0
- package/dist/utils/localizeStaticUrls.js +1 -1
- package/dist/utils/localizeStaticUrls.js.map +1 -1
- package/package.json +1 -1
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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>${
|
|
63
|
+
<title>${title}</title>
|
|
63
64
|
<style>${STYLES}</style>
|
|
64
65
|
</head>
|
|
65
66
|
<body>
|
|
66
67
|
<main>
|
|
67
|
-
${
|
|
68
|
-
<h1>${
|
|
69
|
-
<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
|
|
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"}
|
package/dist/auth/loopback.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
6
|
-
waitForCallback: (timeoutMs?: number) => Promise<
|
|
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>;
|
package/dist/auth/loopback.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 /**
|
|
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"}
|
package/dist/auth/oauth.d.ts
CHANGED
|
@@ -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>;
|
package/dist/auth/oauth.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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) {
|
package/dist/auth/oauth.js.map
CHANGED
|
@@ -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":";;;;;;;;;;;AAiBA,MAAM,kBAAkB;;;;AAIxB,MAAM,cAAc;AAEpB,MAAM,0BAA0B;AAwBhC,SAAS,aACP,cACA,KACA,SACe;AACf,QAAO,IAAI,cAAc,SAAS,cAAc,KAAK,QAAQ;;AAG/D,SAAS,WAAW,OAAgB,UAAiC;AACnE,KACE,iBAAiB,KAAK,8BACtB,iBAAiB,KAAK,kBAWtB,QAAO,aACL;EATA,eAAe;EACf,eAAe;EACf,eAAe;EACf,eAAe;EACf,gBAAgB;EAChB,qBACE;EAGU,CAAC,MAAM,UAAU,UAC7B,CAAC,kBAAkB,sBAAsB,CAAC,SAAS,MAAM,MAAM,GAC3D,2CACA,wBAEJ,MAAM,UAAU,kBAAkB,MAAM,oBAAoB,KAAA,EAC7D;AAEH,KAAI,iBAAiB,KAAK,YAGxB,QAAO,aACL,GAAG,WAFH,MAAM,iBAAiB,WAAW,UAAU,MAAM,MAAM,OAAO,KAAK,MAGpE,gDACA,MAAM,KACP;AAEH,KACE,iBAAiB,SACjB,CAAC,cAAc,eAAe,CAAC,SAAS,MAAM,KAAK,CAEnD,QAAO,aACL,sCACA,uBACD;AAGH,QAAO,aAAa,UAAU,+CAA+C;;AAG/E,SAAS,eAAe,KAAU,QAAmB;AACnD,KACE,IAAI,YACJ,IAAI,YACJ,IAAI,QACH,IAAI,aAAa,YAChB,EAAE,OAAO,aAAa,WAAW,IAAI,WAAW,OAAO,QAEzD,OAAM,aACJ,wDACA,mDACD;;AAIL,eAAe,cACb,EACE,cAAc,gBAAgB,EAC9B,OAAO,sBAAsB,WAAW,SAE1C,QAC6B;CAC7B,MAAM,SAAS,IAAI,IAAI,YAAY;CACnC,MAAM,QACJ,OAAO,aAAa,eACpB,OAAO,SAAS,SAAS,aAAa,IACtC,OAAO,aAAa,eACpB,OAAO,aAAa;AACtB,KACE,OAAO,YACP,OAAO,YACP,OAAO,UACP,OAAO,QACN,OAAO,aAAa,YAAY,EAAE,OAAO,aAAa,WAAW,OAElE,OAAM,aACJ,2CACA,uFACD;AAEH,QAAO,KAAK,UAAU,QAAQ,iBAAiB,KAAA,GAAW,KAAK,MAAM,EAAE;EACrE,SACE,OAAO,aAAa,UAChB,CAAC,KAAK,4BAA4B,KAAK,sBAAsB,GAC7D,CAAC,KAAK,2BAA2B;GACtC,KAAK,eAAe,KAAK,SAAS;AACjC,kBAAe,IAAI,IAAI,IAAI,EAAE,OAAO;AACpC,UAAO,oBAAoB,KAAK;IAC9B,GAAG;IAEH,QAAQ,SACJ,YAAY,IAAI,CAAC,QAAQ,GAAI,KAAK,SAAS,CAAC,KAAK,OAAO,GAAG,EAAE,CAAE,CAAC,GAChE,KAAK;IACT,MACE,KAAK,gBAAgB,aACjB,IAAI,WAAW,KAAK,KAAK,GACzB,KAAK;IACZ,CAAC;;EAEL,CAAC;;AAGJ,SAAS,YACP,QACA,UAC+B;CAC/B,MAAM,UAAU,OAAO,QAAQ,EAAE,OAAO,UAAU;AAClD,KAAI,CAAC,WAAY,YAAY,YAAY,SAAS,QAChD,OAAM,aACJ,0EACA,uBACD;AAEH,KACE,OAAO,eAAe,YACtB,OAAO,OAAO,eAAe,YAC7B,CAAC,OAAO,SAAS,OAAO,WAAW,IACnC,OAAO,cAAc,EAErB,OAAM,aACJ,0EACD;AAEH,QAAO;EACL,aAAa,OAAO;EACpB,WAAW,KAAK,KAAK,GAAG,OAAO,aAAa;EAC5C,cAAc,OAAO,iBAAiB,UAAU,gBAAgB;EAChE,OAAO,OAAO,SAAS,UAAU,SAAS;EAC1C,WAAW,OAAO;EAClB;EACD;;AAGH,SAAS,iBAAyB;AAChC,SAAQ,QAAQ,IAAI,eAAe,gDAAgC,QACjE,OACA,GACD;;;AAIH,SAAS,cAAc,SAAyB;AAC9C,QAAO,IAAI,IAAI,QAAQ,CAAC;;AAG1B,SAAS,cAAc,SAA+B;AACpD,QAAO,cACL,QAAQ,IAAI,cAAc,QAAQ,WAAW,eAC9C;;AAGH,eAAe,oBACb,SACsB;AACtB,KAAI,QAAQ,aAAa,CAAC,QAAQ,aAChC,OAAM,aACJ,6DACA,4CACD;CAEH,MAAM,cAAc,QAAQ,eAAe,gBAAgB;CAC3D,MAAM,WAAW,cAAc,QAAQ;CACvC,MAAM,WAAW,IAAI,iBAAiB;CACtC,MAAM,SAAS,QAAQ,SACnB,YAAY,IAAI,CAAC,QAAQ,QAAQ,SAAS,OAAO,CAAC,GAClD,SAAS;CACb,IAAI;CACJ,IAAI;AACJ,KAAI;AACF,SAAO,gBAAgB;EACvB,MAAM,SAAS,MAAM,cAAc;GAAE,GAAG;GAAS;GAAa,EAAE,OAAO;EACvE,MAAM,UAAU,KAAK,KAAK;EAC1B,MAAM,WAAW,MAAM,KAAK,4BAA4B,QAAQ;GAC9D,OAAO;GACP;GACD,CAAC;EACF,MAAM,WAAW,UAAU,SAAS,aAAa;EACjD,MAAM,sBAAsB;AAC1B,OAAI,KAAK,KAAK,IAAI,SAChB,UAAS,MAAM,IAAI,aAAa,uBAAuB,eAAe,CAAC;AACzE,UAAO,gBAAgB;;AAEzB,iBAAe;AACf,UAAQ,iBAEJ,SAAS,MAAM,IAAI,aAAa,uBAAuB,eAAe,CAAC,EACzE,WAAW,KAAK,KAAK,CACtB;AACD,iBAAe,IAAI,IAAI,SAAS,iBAAiB,EAAE,IAAI,IAAI,YAAY,CAAC;AACxE,MAAI,SAAS,0BACX,gBACE,IAAI,IAAI,SAAS,0BAA0B,EAC3C,IAAI,IAAI,YAAY,CACrB;AACH,UAAQ,eAAe;GACrB,UAAU,SAAS;GACnB,iBAAiB,SAAS;GAC1B,yBAAyB,SAAS;GACnC,CAAC;AACF,iBAAe;AACf,MAAI,CAAC,QAAQ,UACX,EAAM,QAAQ,eAAe,MAC3B,SAAS,6BAA6B,SAAS,iBAChD,CAAC,YAAY,KAAA,EAAU;AAE1B,WAAS,MAAM,KAAK,6BAClB,QACA,UACA,EAAE,UAAU,EACZ,EAAE,QAAQ,CACX;AACD,iBAAe;UACR,OAAO;AACd,QAAM,WAAW,OAAO,oCAAoC;WACpD;AACR,eAAa,MAAM;;CAErB,MAAM,SAAS;EAAE,GAAG,YAAY,OAAO;EAAE;EAAU;AACnD,OAAM,iBAAiB,QAAQ,YAAY;AAC3C,QAAO;;;AAIT,eAAsB,MAAM,UAAwB,EAAE,EAAwB;AAC5E,KACE,QAAQ,aACR,QAAQ,IAAI,kBACZ,QAAQ,IAAI,cACZ,QAAQ,IAAI,QAEZ,QAAO,oBAAoB;EAAE,GAAG;EAAS,WAAW;EAAM,CAAC;CAE7D,MAAM,cAAc,QAAQ,eAAe,gBAAgB;CAC3D,MAAM,WAAW,cAAc,QAAQ;CACvC,MAAM,SAAS,MAAM,cAAc;EAAE,GAAG;EAAS;EAAa,CAAC,CAAC,OAC7D,UAAmB;AAClB,QAAM,WAAW,OAAO,8CAA8C;GAEzE;CACD,MAAM,WAAW,MAAM,qBAAqB,CAAC,YAAY,KAAA,EAAU;AACnE,KAAI,CAAC,SAAU,QAAO,oBAAoB,QAAQ;AAClD,KAAI;EACF,MAAM,eAAe,KAAK,wBAAwB;EAClD,MAAM,QAAQ,KAAK,aAAa;EAChC,MAAM,mBAAmB,KAAK,sBAAsB,QAAQ;GAC1D,cAAc,SAAS;GACvB,OAAO;GACP;GACA;GACA,gBAAgB,MAAM,KAAK,2BAA2B,aAAa;GACnE,uBAAuB;GACxB,CAAC;AACF,iBAAe,kBAAkB,IAAI,IAAI,YAAY,CAAC;EACtD,MAAM,WAAW,SAAS,gBAAgB,OAAO,gBAAgB;GAe/D,MAAM,SAAS;IAAE,GAAG,YAAY,MAdX,KAClB,uBACC,QACA,aACA;KACE,kBAAkB;KAClB,eAAe;KACf,iBAAiB;KAClB,EACD,EAAE,UAAU,CACb,CACA,OAAO,UAAmB;AACzB,WAAM,WAAW,OAAO,yCAAyC;MACjE,CACmC;IAAE;IAAU;AACnD,SAAM,iBAAiB,QAAQ,YAAY;AAC3C,UAAO;KACN,QAAQ,UAAU;AAErB,WAAS,YAAY,KAAA,EAAU;AAC/B,UAAQ,qBAAqB,iBAAiB,KAAK;AACnD,GAAM,QAAQ,eAAe,MAAM,iBAAiB,KAAK,CAAC,YAClD,KAAA,EACP;AACD,SAAO,MAAM;WACL;AACR,WAAS,OAAO;;;AAIpB,MAAM,mCAAmB,IAAI,KAAmC;AAChE,SAAS,cAAc,SAAoD;CACzE,MAAM,cAAc,QAAQ,eAAe,gBAAgB;CAC3D,MAAM,UAAU,iBAAiB,IAAI,YAAY;AACjD,KAAI,QAAS,QAAO;CACpB,MAAM,UAAU,qBAAqB;EAAE,GAAG;EAAS;EAAa,CAAC,CAAC,cAC1D,iBAAiB,OAAO,YAAY,CAC3C;AACD,kBAAiB,IAAI,aAAa,QAAQ;AAC1C,QAAO;;AAGT,eAAe,qBACb,SACsB;CACtB,MAAM,UAAU,MAAM,gBAAgB,QAAQ,YAAY;AAC1D,KAAI,CAAC,SAAS,aAAc,OAAM,oBAAoB;CAStD,MAAM,SAAS;EACb,GAAG,YAAY,OATK,YACpB,KAAK,kBACH,MAAM,cAAc,QAAQ,EAC5B,QAAQ,aACT,GAAG,CAAC,OAAO,UAAmB;AAC/B,SAAM,WAAW,OAAO,+BAA+B;IACvD,EAGuB,QAAQ;EAC/B,UAAU,QAAQ;EACnB;AACD,OAAM,iBAAiB,QAAQ,QAAQ,YAAY;AACnD,QAAO;;AAGT,eAAe,YACb,SACkC;CAClC,MAAM,SAAS,MAAM,gBAAgB,QAAQ,eAAe,gBAAgB,CAAC;AAC7E,KAAI,CAAC,UAAU,OAAO,YAAY,KAAK,KAAK,GAAG,wBAC7C,QAAO;AACT,QAAO,cAAc,QAAQ;;;AAI/B,eAAsB,SACpB,SACkB;AAElB,SAAO,MADc,gBAAgB,QAAQ,eAAe,gBAAgB,CAAC,GAC9D,aAAa,cAAc,QAAQ,QAAQ;;;AAI5D,SAAgB,wBACd,SACmB;CAGnB,MAAM,eAAe,YAAkC;EACrD,MAAM,SAAS,MAAM,gBACnB,QAAQ,eAAe,gBAAgB,CACxC;AACD,MAAI,CAAC,OAAQ,OAAM,oBAAoB;EACvC,MAAM,WAAW,cAAc,QAAQ,QAAQ;AAC/C,MAAI,OAAO,aAAa,SACtB,OAAM,IAAI,cACR,qBACA,wBAAwB,OAAO,SAAS,0BAA0B,YAClE,uDACD;AAEH,SAAO;;AAET,QAAO;EACL,gBAAgB,YAAY;GAC1B,MAAM,SAAS,MAAM,cAAc;AACnC,OAAI,OAAO,YAAY,KAAK,KAAK,GAAG,wBAClC,QAAO,OAAO;AAChB,WAAQ,MAAM,cAAc,QAAQ,EAAE;;EAExC,oBAAoB,YAAY;AAC9B,SAAM,cAAc;AACpB,WAAQ,MAAM,cAAc,QAAQ,EAAE;;EAEzC;;AAGH,eAAsB,OAAO,UAA+B,EAAE,EAAiB;CAC7E,MAAM,cAAc,QAAQ,eAAe,gBAAgB;AAC3D,KAAI;EACF,MAAM,SAAS,MAAM,gBAAgB,YAAY;AACjD,MAAI,QAAQ,aACV,OAAM,KAAK,gBACT,MAAM,cAAc,QAAQ,EAC5B,OAAO,cACP,EAAE,iBAAiB,iBAAiB,CACrC;SACG;AACN,SAAO,KACL,wBAAwB;GACtB,QAAQ;GACR,UAAU;GACV,cACE;GACF,KAAK;GACN,CAAC,CACH;WACO;AACR,QAAM,kBAAkB,YAAY;;;AAIxC,eAAsB,OACpB,UAA+B,EAAE,EACd;CACnB,MAAM,SAAS,MAAM,YAAY,QAAQ;AACzC,KAAI,CAAC,OAAQ,OAAM,oBAAoB;AACvC,KAAI;EACF,MAAM,SAAS,MAAM,KAAK,cACxB,MAAM,cAAc,QAAQ,EAC5B,OAAO,aACP,OAAO,QACR;AACD,SAAO;GAAE,KAAK,OAAO;GAAK,OAAO,OAAO;GAAO,MAAM,OAAO;GAAM;UAC3D,OAAO;AACd,QAAM,WAAW,OAAO,2CAA2C"}
|
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(
|
|
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("
|
|
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("
|
|
601
|
+
logger.message("You are now signed in.");
|
|
602
602
|
} catch (error) {
|
|
603
603
|
logErrorAndExit(createUserAuthError("Sign in failed", error));
|
|
604
604
|
}
|