@google/clasp
Version:
Develop Apps Script Projects locally
107 lines (106 loc) • 5.5 kB
JavaScript
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file implements the `AuthorizationCodeFlow` for local development
// environments. It starts a local HTTP server to receive the authorization
// code after the user grants permission.
import { createServer } from 'http';
import open from 'open';
import enableDestroy from 'server-destroy';
import { intl } from '../intl.js';
import { AuthorizationCodeFlow, parseAuthResponseUrl } from './auth_code_flow.js';
/**
* Implements the Authorization Code Flow by starting a local HTTP server
* to act as the redirect URI. This is suitable for CLI environments
* where a browser can be opened and a local server can receive the
* authorization code.
*/
export class LocalServerAuthorizationCodeFlow extends AuthorizationCodeFlow {
constructor(oauth2client, port) {
super(oauth2client);
this.port = 0;
this.port = port;
}
/**
* Starts a local HTTP server and returns its address as the redirect URI.
* The server will listen on the configured port (or a random available port if 0).
* @returns {Promise<string>} The local redirect URI (e.g., "http://localhost:1234").
* @throws {Error} If the server cannot be started (e.g., port in use).
*/
async getRedirectUri() {
this.server = await new Promise((resolve, reject) => {
const s = createServer();
enableDestroy(s); // Allows the server to be destroyed gracefully.
// Try to listen on the specified port (or a random one if port is 0).
s.listen(this.port, () => resolve(s)).on('error', (err) => {
// Handle common server errors like port already in use.
if (err.code === 'EADDRINUSE') {
const msg = intl.formatMessage({ id: "smVcjx", defaultMessage: [{ type: 0, value: "Error: Port " }, { type: 1, value: "port" }, { type: 0, value: " is already in use. Please specify a different port with --redirect-port" }] }, {
port: this.port,
});
console.error(msg);
}
else {
const msg = intl.formatMessage({ id: "3X2J9l", defaultMessage: [{ type: 0, value: "Error: Unable to start the server on port " }, { type: 1, value: "port" }] }, {
port: this.port,
errorMessage: err.message,
});
console.error(msg, err.message);
}
reject(err);
});
});
const { port } = this.server.address();
return `http://localhost:${port}`;
}
/**
* Prompts the user to authorize by opening the provided authorization URL
* in their default web browser. It then waits for the local server (started by
* `getRedirectUri`) to receive the callback containing the authorization code.
* @param {string} authorizationUrl - The URL to open for user authorization.
* @returns {Promise<string>} The authorization code extracted from the redirect.
* @throws {Error} If the server is not started, the request URL is missing, or an error
* parameter is present in the redirect URL.
*/
async promptAndReturnCode(authorizationUrl) {
return await new Promise((resolve, reject) => {
if (!this.server) {
reject(new Error('Server not started'));
return;
}
this.server.on('request', (request, response) => {
if (!request.url) {
reject(new Error('Missing URL in request'));
return;
}
const { code, error } = parseAuthResponseUrl(request.url); // Extract code or error from the redirect URL.
if (code) {
resolve(code); // Successfully obtained the authorization code.
}
else {
reject(error); // An error occurred during authorization.
}
// Send a simple response to the browser.
const msg = intl.formatMessage({ id: "ZT8LeG", defaultMessage: [{ type: 0, value: "Logged in! You may close this page." }] });
response.end(msg);
});
// Open the authorization URL in the user's default browser.
void open(authorizationUrl);
// Log the authorization URL to the console as a fallback or for visibility.
const msg = intl.formatMessage({ id: "NbCrKh", defaultMessage: [{ type: 0, value: "`\uD83D\uDD11 Authorize clasp by visiting this url: " }, { type: 1, value: "url" }] }, {
url: authorizationUrl,
});
console.log(msg);
}).finally(() => { var _a; return (_a = this.server) === null || _a === void 0 ? void 0 : _a.destroy(); }); // Ensure the server is destroyed after completion or error.
}
}