This is page 3 of 3. Use http://codebase.md/mohalmah/google-appscript-mcp-server?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .env.example
├── .gitattributes
├── .gitignore
├── .gitkeep
├── commands
│ └── tools.js
├── demo
│ └── google app script mcp demo.gif
├── Dockerfile
├── ERROR_HANDLING_ENHANCEMENT.md
├── helpers
│ ├── check-deployments.js
│ ├── check-head-deployment.js
│ ├── convert-to-oauth.js
│ ├── create-proper-webapp.js
│ ├── create-web-app-fixed.js
│ ├── create-web-app.js
│ └── create-webapp-deployment.js
├── index.js
├── lib
│ ├── authHelper.js
│ ├── logger.js
│ ├── oauth-helper.js
│ ├── tokenManager.js
│ └── tools.js
├── LOGGING.md
├── mcpServer.js
├── OAUTH_IMPLEMENTATION.md
├── OAUTH_SETUP.md
├── oauth-setup.js
├── package-lock.json
├── package.json
├── README.md
├── test
│ ├── debug-content-fetch.js
│ ├── debug-deployment.js
│ ├── debug-mcp-deployment.js
│ ├── debug-test.js
│ ├── deploy-complete-webapp.js
│ ├── oauth-setup-broken.js
│ ├── oauth-setup-fixed.js
│ ├── simple-oauth-test.js
│ ├── simple-test.js
│ ├── test-complete-mcp-webapp.js
│ ├── test-fields-issue.js
│ ├── test-logging.js
│ ├── test-mcp-content.js
│ ├── test-mcp-deployment-direct.js
│ ├── test-mcp-deployment-fix.js
│ ├── test-mcp-deployment-get.js
│ ├── test-mcp-errors.js
│ ├── test-mcp-fetch-processes.js
│ ├── test-mcp-processes.js
│ ├── test-mcp-tools.js
│ ├── test-mcp-version-fix.js
│ ├── test-oauth.js
│ ├── test-token-management.js
│ ├── test-versions-list.js
│ ├── update-and-deploy-dark-theme.js
│ ├── update-error-handling.js
│ ├── update-tools.js
│ └── update-webapp-deployment.js
└── tools
├── google-app-script-api
│ └── apps-script-api
│ ├── script-processes-list-script-processes.js
│ ├── script-processes-list.js
│ ├── script-projects-create.js
│ ├── script-projects-deployments-create.js
│ ├── script-projects-deployments-delete.js
│ ├── script-projects-deployments-get.js
│ ├── script-projects-deployments-list.js
│ ├── script-projects-deployments-update.js
│ ├── script-projects-get-content.js
│ ├── script-projects-get-metrics.js
│ ├── script-projects-get.js
│ ├── script-projects-update-content.js
│ ├── script-projects-versions-create.js
│ ├── script-projects-versions-get.js
│ ├── script-projects-versions-list.js
│ └── script-scripts-run.js
└── paths.js
```
# Files
--------------------------------------------------------------------------------
/test/update-and-deploy-dark-theme.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Update script content to dark theme and deploy as web app
5 | */
6 |
7 | import { getOAuthAccessToken } from '../lib/oauth-helper.js';
8 |
9 | const scriptId = '1fSY7y3Rh84FsgJmrFIMm4AUOV3mPgelLRvZ4Dahrv68zyDzX-cGbeYjn';
10 |
11 | // Dark theme HTML content
12 | const darkThemeHTML = `<!DOCTYPE html>
13 | <html>
14 | <head>
15 | <base target="_top">
16 | <title>Hello World App via MCP - Dark Theme</title>
17 | <style>
18 | body {
19 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
20 | max-width: 600px;
21 | margin: 50px auto;
22 | padding: 20px;
23 | background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
24 | color: #e0e0e0;
25 | text-align: center;
26 | min-height: 100vh;
27 | box-sizing: border-box;
28 | }
29 | .container {
30 | background: rgba(255, 255, 255, 0.05);
31 | border: 1px solid rgba(255, 255, 255, 0.1);
32 | border-radius: 20px;
33 | padding: 40px;
34 | backdrop-filter: blur(15px);
35 | box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.3);
36 | }
37 | h1 {
38 | color: #ffffff;
39 | margin-bottom: 30px;
40 | font-size: 2.5em;
41 | text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
42 | }
43 | p {
44 | color: #b0b0b0;
45 | font-size: 1.1em;
46 | margin-bottom: 30px;
47 | }
48 | input {
49 | padding: 14px;
50 | margin: 10px;
51 | border: 2px solid #444;
52 | border-radius: 10px;
53 | font-size: 16px;
54 | width: 250px;
55 | background: #2a2a2a;
56 | color: #e0e0e0;
57 | transition: border-color 0.3s, box-shadow 0.3s;
58 | }
59 | input:focus {
60 | outline: none;
61 | border-color: #64b5f6;
62 | box-shadow: 0 0 0 3px rgba(100, 181, 246, 0.2);
63 | }
64 | input::placeholder {
65 | color: #888;
66 | }
67 | button {
68 | background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
69 | color: white;
70 | border: none;
71 | padding: 14px 28px;
72 | margin: 10px;
73 | border-radius: 10px;
74 | cursor: pointer;
75 | font-size: 16px;
76 | font-weight: 600;
77 | transition: all 0.3s;
78 | box-shadow: 0 4px 15px rgba(33, 150, 243, 0.3);
79 | }
80 | button:hover {
81 | background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
82 | transform: translateY(-2px);
83 | box-shadow: 0 6px 20px rgba(33, 150, 243, 0.4);
84 | }
85 | button:active {
86 | transform: translateY(0);
87 | }
88 | .result {
89 | margin-top: 25px;
90 | padding: 25px;
91 | background: rgba(255, 255, 255, 0.08);
92 | border: 1px solid rgba(255, 255, 255, 0.12);
93 | border-radius: 12px;
94 | min-height: 60px;
95 | display: flex;
96 | align-items: center;
97 | justify-content: center;
98 | color: #f0f0f0;
99 | font-size: 18px;
100 | }
101 | .loading {
102 | display: none;
103 | color: #64b5f6;
104 | font-size: 18px;
105 | font-weight: 600;
106 | }
107 | .timestamp {
108 | margin-top: 15px;
109 | font-size: 0.9em;
110 | color: #888;
111 | font-style: italic;
112 | }
113 | .emoji {
114 | font-size: 1.2em;
115 | margin: 0 5px;
116 | }
117 | @keyframes pulse {
118 | 0% { opacity: 0.6; }
119 | 50% { opacity: 1; }
120 | 100% { opacity: 0.6; }
121 | }
122 | .loading {
123 | animation: pulse 1.5s infinite;
124 | }
125 | </style>
126 | </head>
127 | <body>
128 | <div class="container">
129 | <h1><span class="emoji">🌙</span> Dark Theme MCP App <span class="emoji">🌙</span></h1>
130 | <p>This sleek dark-themed web app was created using MCP (Model Context Protocol) tools!</p>
131 |
132 | <div class="input-group">
133 | <input type="text" id="nameInput" placeholder="Enter your name (optional)" />
134 | </div>
135 |
136 | <div>
137 | <button onclick="sayHello()">🗨️ Say Hello</button>
138 | <button onclick="getTime()">🕒 Get Current Time</button>
139 | </div>
140 |
141 | <div id="result" class="result">
142 | Click a button to experience the dark MCP magic! ✨
143 | </div>
144 |
145 | <div id="loading" class="loading">⏳ Loading...</div>
146 |
147 | <div id="timestamp" class="timestamp"></div>
148 | </div>
149 |
150 | <script>
151 | function sayHello() {
152 | showLoading();
153 | const name = document.getElementById('nameInput').value;
154 |
155 | google.script.run
156 | .withSuccessHandler(onSuccess)
157 | .withFailureHandler(onFailure)
158 | .getGreeting(name);
159 | }
160 |
161 | function getTime() {
162 | showLoading();
163 |
164 | google.script.run
165 | .withSuccessHandler(onTimeSuccess)
166 | .withFailureHandler(onFailure)
167 | .getCurrentTime();
168 | }
169 |
170 | function onSuccess(result) {
171 | hideLoading();
172 | document.getElementById('result').innerHTML = \`
173 | <div style="font-size: 20px; font-weight: bold; color: #64b5f6;">
174 | \${result}
175 | </div>
176 | \`;
177 | updateTimestamp();
178 | }
179 |
180 | function onTimeSuccess(result) {
181 | hideLoading();
182 | document.getElementById('result').innerHTML = \`
183 | <div style="font-size: 18px; color: #81c784;">
184 | 🕒 Current Time: <strong style="color: #fff;">\${result}</strong>
185 | </div>
186 | \`;
187 | updateTimestamp();
188 | }
189 |
190 | function onFailure(error) {
191 | hideLoading();
192 | document.getElementById('result').innerHTML = \`
193 | <div style="color: #f48fb1;">
194 | ❌ Error: \${error.message || 'Something went wrong!'}
195 | </div>
196 | \`;
197 | updateTimestamp();
198 | }
199 |
200 | function showLoading() {
201 | document.getElementById('loading').style.display = 'block';
202 | document.getElementById('result').style.display = 'none';
203 | }
204 |
205 | function hideLoading() {
206 | document.getElementById('loading').style.display = 'none';
207 | document.getElementById('result').style.display = 'flex';
208 | }
209 |
210 | function updateTimestamp() {
211 | document.getElementById('timestamp').textContent =
212 | 'Last updated: ' + new Date().toLocaleTimeString();
213 | }
214 |
215 | // Initial timestamp
216 | updateTimestamp();
217 |
218 | // Add some interactive effects
219 | document.addEventListener('DOMContentLoaded', function() {
220 | const buttons = document.querySelectorAll('button');
221 | buttons.forEach(button => {
222 | button.addEventListener('mouseenter', function() {
223 | this.style.transform = 'translateY(-2px) scale(1.02)';
224 | });
225 | button.addEventListener('mouseleave', function() {
226 | this.style.transform = 'translateY(0) scale(1)';
227 | });
228 | });
229 | });
230 | </script>
231 | </body>
232 | </html>`;
233 |
234 | // Updated server-side code
235 | const serverCode = `/**
236 | * Serves the HTML page when the web app is accessed
237 | */
238 | function doGet() {
239 | return HtmlService.createHtmlOutputFromFile('index')
240 | .setTitle('Hello World App via MCP - Dark Theme')
241 | .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
242 | }
243 |
244 | /**
245 | * Server-side function that can be called from the client
246 | */
247 | function getGreeting(name) {
248 | if (!name) {
249 | name = 'World';
250 | }
251 | return \`Hello, \${name}! This dark-themed web app was created via MCP tools.\`;
252 | }
253 |
254 | /**
255 | * Get current time
256 | */
257 | function getCurrentTime() {
258 | return new Date().toLocaleString();
259 | }`;
260 |
261 | // Configuration
262 | const appsScriptConfig = `{
263 | "timeZone": "America/New_York",
264 | "dependencies": {},
265 | "exceptionLogging": "STACKDRIVER",
266 | "runtimeVersion": "V8",
267 | "webapp": {
268 | "access": "ANYONE",
269 | "executeAs": "USER_ACCESSING"
270 | }
271 | }`;
272 |
273 | async function updateScriptContent() {
274 | try {
275 | const token = await getOAuthAccessToken();
276 | console.log('🔄 Updating script content with dark theme...');
277 |
278 | const files = [
279 | {
280 | name: "appsscript",
281 | type: "JSON",
282 | source: appsScriptConfig
283 | },
284 | {
285 | name: "code",
286 | type: "SERVER_JS",
287 | source: serverCode
288 | },
289 | {
290 | name: "index",
291 | type: "HTML",
292 | source: darkThemeHTML
293 | }
294 | ];
295 |
296 | const response = await fetch(`https://script.googleapis.com/v1/projects/${scriptId}/content`, {
297 | method: 'PUT',
298 | headers: {
299 | 'Authorization': `Bearer ${token}`,
300 | 'Content-Type': 'application/json',
301 | 'Accept': 'application/json'
302 | },
303 | body: JSON.stringify({ files })
304 | });
305 |
306 | if (!response.ok) {
307 | const errorText = await response.text();
308 | console.error('❌ Error updating content:', errorText);
309 | throw new Error(`HTTP ${response.status}: ${errorText}`);
310 | }
311 |
312 | const data = await response.json();
313 | console.log('✅ Script content updated successfully!');
314 | return data;
315 | } catch (error) {
316 | console.error('❌ Error updating script content:', error);
317 | return null;
318 | }
319 | }
320 |
321 | async function createVersion() {
322 | try {
323 | const token = await getOAuthAccessToken();
324 | console.log('📦 Creating new version...');
325 |
326 | const versionData = {
327 | description: "Dark theme version via MCP"
328 | };
329 |
330 | const response = await fetch(`https://script.googleapis.com/v1/projects/${scriptId}/versions`, {
331 | method: 'POST',
332 | headers: {
333 | 'Authorization': `Bearer ${token}`,
334 | 'Content-Type': 'application/json',
335 | 'Accept': 'application/json'
336 | },
337 | body: JSON.stringify(versionData)
338 | });
339 |
340 | if (!response.ok) {
341 | const errorText = await response.text();
342 | console.error('❌ Error creating version:', errorText);
343 | throw new Error(`HTTP ${response.status}: ${errorText}`);
344 | }
345 |
346 | const data = await response.json();
347 | console.log(`✅ Version ${data.versionNumber} created successfully!`);
348 | return data;
349 | } catch (error) {
350 | console.error('❌ Error creating version:', error);
351 | return null;
352 | }
353 | }
354 |
355 | async function createDeployment(versionNumber) {
356 | try {
357 | const token = await getOAuthAccessToken();
358 | console.log('🚀 Creating web app deployment...');
359 |
360 | const deploymentConfig = {
361 | description: "Dark Theme MCP Web App - Public Access",
362 | manifestFileName: "appsscript",
363 | versionNumber: versionNumber
364 | };
365 |
366 | const response = await fetch(`https://script.googleapis.com/v1/projects/${scriptId}/deployments`, {
367 | method: 'POST',
368 | headers: {
369 | 'Authorization': `Bearer ${token}`,
370 | 'Content-Type': 'application/json',
371 | 'Accept': 'application/json'
372 | },
373 | body: JSON.stringify(deploymentConfig)
374 | });
375 |
376 | if (!response.ok) {
377 | const errorText = await response.text();
378 | console.error('❌ Error creating deployment:', errorText);
379 | throw new Error(`HTTP ${response.status}: ${errorText}`);
380 | }
381 |
382 | const data = await response.json();
383 | console.log('✅ Deployment created successfully!');
384 |
385 | // Extract web app URL
386 | if (data.entryPoints && data.entryPoints[0] && data.entryPoints[0].webApp) {
387 | const webAppUrl = data.entryPoints[0].webApp.url;
388 | console.log('🌐 Web App URL:', webAppUrl);
389 | }
390 |
391 | console.log('📋 Deployment Details:', JSON.stringify(data, null, 2));
392 | return data;
393 | } catch (error) {
394 | console.error('❌ Error creating deployment:', error);
395 | return null;
396 | }
397 | }
398 |
399 | async function main() {
400 | console.log('🌙 Starting dark theme update and deployment...');
401 | console.log('='.repeat(60));
402 |
403 | // Step 1: Update script content
404 | const updateResult = await updateScriptContent();
405 | if (!updateResult) {
406 | console.log('❌ Failed to update content. Stopping.');
407 | return;
408 | }
409 |
410 | console.log('\n' + '='.repeat(60) + '\n');
411 |
412 | // Step 2: Create new version
413 | const versionResult = await createVersion();
414 | if (!versionResult) {
415 | console.log('❌ Failed to create version. Stopping.');
416 | return;
417 | }
418 |
419 | console.log('\n' + '='.repeat(60) + '\n');
420 |
421 | // Step 3: Create deployment
422 | const deploymentResult = await createDeployment(versionResult.versionNumber);
423 | if (!deploymentResult) {
424 | console.log('❌ Failed to create deployment. Stopping.');
425 | return;
426 | }
427 |
428 | console.log('\n🎉 Process completed successfully!');
429 | console.log('Your dark-themed web app is now deployed and accessible.');
430 | }
431 |
432 | main().catch(console.error);
433 |
```
--------------------------------------------------------------------------------
/lib/oauth-helper.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * OAuth authentication helper for Google Apps Script API
3 | */
4 |
5 | import 'dotenv/config';
6 | import { google } from 'googleapis';
7 | import { createServer } from 'http';
8 | import open from 'open';
9 | import { URL } from 'url';
10 | import { createConnection } from 'net';
11 | import { TokenManager } from './tokenManager.js';
12 | import { logger } from './logger.js';
13 |
14 | // Configuration - Comprehensive scopes for all Google APIs
15 | const SCOPES = [
16 | // Google Apps Script API - Full access
17 | 'https://www.googleapis.com/auth/script.projects',
18 | 'https://www.googleapis.com/auth/script.projects.readonly',
19 | 'https://www.googleapis.com/auth/script.deployments',
20 | 'https://www.googleapis.com/auth/script.deployments.readonly',
21 | 'https://www.googleapis.com/auth/script.metrics',
22 | 'https://www.googleapis.com/auth/script.processes',
23 | 'https://www.googleapis.com/auth/script.webapp.deploy'
24 |
25 |
26 | ];
27 |
28 | const REDIRECT_URI = 'http://localhost:3001/oauth/callback';
29 | const PORT = 3001;
30 |
31 | // Token manager instance
32 | const tokenManager = new TokenManager();
33 |
34 | /**
35 | * Finds an available port starting from the given port
36 | * @param {number} startPort - Port to start checking from
37 | * @returns {Promise<number>} Available port number
38 | */
39 | async function findAvailablePort(startPort = PORT) {
40 | return new Promise((resolve) => {
41 | const server = createServer();
42 |
43 | server.listen(startPort, () => {
44 | const port = server.address().port;
45 | server.close(() => {
46 | resolve(port);
47 | });
48 | });
49 |
50 | server.on('error', () => {
51 | findAvailablePort(startPort + 1).then(resolve);
52 | });
53 | });
54 | }
55 |
56 | /**
57 | * Creates and configures OAuth2 client
58 | * @returns {OAuth2Client} Configured OAuth2 client
59 | */
60 | function createOAuth2Client() {
61 | const clientId = process.env.GOOGLE_APP_SCRIPT_API_CLIENT_ID;
62 | const clientSecret = process.env.GOOGLE_APP_SCRIPT_API_CLIENT_SECRET;
63 |
64 | if (!clientId || !clientSecret) {
65 | logger.error('AUTH', 'Missing required OAuth credentials', {
66 | hasClientId: !!clientId,
67 | hasClientSecret: !!clientSecret
68 | });
69 | throw new Error('Missing required OAuth credentials: GOOGLE_APP_SCRIPT_API_CLIENT_ID and GOOGLE_APP_SCRIPT_API_CLIENT_SECRET must be set in environment variables');
70 | }
71 |
72 | logger.info('AUTH', 'Creating OAuth2 client', {
73 | clientId: clientId.substring(0, 20) + '...',
74 | redirectUri: REDIRECT_URI,
75 | scopeCount: SCOPES.length,
76 | scopes: SCOPES
77 | });
78 |
79 | console.log('🔐 Creating OAuth2 client...');
80 | console.log(' - Client ID:', clientId);
81 | console.log(' - Redirect URI:', REDIRECT_URI);
82 | console.log(' - Scopes:', SCOPES.length, 'permissions');
83 |
84 | return new google.auth.OAuth2(clientId, clientSecret, REDIRECT_URI);
85 | }
86 |
87 | /**
88 | * Starts OAuth flow with browser automation
89 | * @returns {Promise<Object>} OAuth tokens
90 | */
91 | async function startOAuthFlow() {
92 | console.log('🚀 Starting OAuth flow...');
93 |
94 | const oAuth2Client = createOAuth2Client();
95 |
96 | return new Promise(async (resolve, reject) => {
97 | try {
98 | // Use the exact port that matches Google Cloud Console configuration
99 | const callbackPort = PORT; // Must match Google Cloud Console redirect URI
100 |
101 | console.log(`🌐 Starting OAuth callback server on port ${callbackPort}`);
102 | console.log(`🔗 Redirect URI: ${REDIRECT_URI}`);
103 |
104 | // Create temporary HTTP server for OAuth callback
105 | const server = createServer(async (req, res) => {
106 | console.log('📥 OAuth callback received:', req.url);
107 |
108 | try {
109 | const url = new URL(req.url, `http://localhost:${callbackPort}`);
110 |
111 | if (url.pathname === '/oauth/callback') {
112 | const code = url.searchParams.get('code');
113 | const error = url.searchParams.get('error');
114 |
115 | if (error) {
116 | console.error('❌ OAuth error:', error);
117 | res.writeHead(400, { 'Content-Type': 'text/html' });
118 | res.end(`
119 | <html>
120 | <body style="font-family: Arial, sans-serif; padding: 50px; text-align: center;">
121 | <h2 style="color: #dc3545;">❌ Authentication Failed</h2>
122 | <p>Error: ${error}</p>
123 | <p>You can close this window.</p>
124 | </body>
125 | </html>
126 | `);
127 | server.close();
128 | reject(new Error(`OAuth error: ${error}`));
129 | return;
130 | }
131 |
132 | if (!code) {
133 | console.error('❌ No authorization code received');
134 | res.writeHead(400, { 'Content-Type': 'text/html' });
135 | res.end(`
136 | <html>
137 | <body style="font-family: Arial, sans-serif; padding: 50px; text-align: center;">
138 | <h2 style="color: #dc3545;">❌ No Authorization Code</h2>
139 | <p>No authorization code received from Google.</p>
140 | <p>You can close this window.</p>
141 | </body>
142 | </html>
143 | `);
144 | server.close();
145 | reject(new Error('No authorization code received'));
146 | return;
147 | }
148 |
149 | console.log('🔄 Exchanging authorization code for tokens...');
150 | console.log('🔑 Authorization code:', code.substring(0, 20) + '...');
151 | try {
152 | const { tokens: newTokens } = await oAuth2Client.getToken(code);
153 |
154 | console.log('✅ Token exchange successful!');
155 | console.log('🎟️ Token details:');
156 | console.log(' - Access token:', newTokens.access_token ? '✅ Received' : '❌ Missing');
157 | console.log(' - Refresh token:', newTokens.refresh_token ? '✅ Received' : '❌ Missing');
158 | console.log(' - Token type:', newTokens.token_type || 'Not specified');
159 | console.log(' - Expires in:', newTokens.expiry_date ? new Date(newTokens.expiry_date).toISOString() : 'No expiry');
160 | console.log(' - Scope:', newTokens.scope || 'Not specified');
161 |
162 | // Success response
163 | res.writeHead(200, { 'Content-Type': 'text/html' });
164 | res.end(`
165 | <html>
166 | <body style="font-family: Arial, sans-serif; padding: 50px; text-align: center;">
167 | <h2 style="color: #28a745;">✅ Authentication Successful!</h2>
168 | <p>You have been successfully authenticated with Google Apps Script API.</p>
169 | <p><strong>Access Token:</strong> ${newTokens.access_token ? 'Received ✅' : 'Missing ❌'}</p>
170 | <p><strong>Refresh Token:</strong> ${newTokens.refresh_token ? 'Received ✅' : 'Missing ❌'}</p>
171 | <p><strong>You can now close this window and return to your application.</strong></p>
172 | <script>
173 | setTimeout(() => {
174 | window.close();
175 | }, 5000);
176 | </script>
177 | </body>
178 | </html>
179 | `);
180 |
181 | server.close();
182 | resolve(newTokens);
183 |
184 | } catch (tokenError) {
185 | console.error('❌ Error exchanging code for tokens:', tokenError);
186 | res.writeHead(500, { 'Content-Type': 'text/html' });
187 | res.end(`
188 | <html>
189 | <body style="font-family: Arial, sans-serif; padding: 50px; text-align: center;">
190 | <h2 style="color: #dc3545;">❌ Token Exchange Failed</h2>
191 | <p>Failed to exchange authorization code for tokens.</p>
192 | <pre style="text-align: left; background: #f8f9fa; padding: 20px;">${tokenError.message}</pre>
193 | <p>You can close this window.</p>
194 | </body>
195 | </html>
196 | `);
197 | server.close();
198 | reject(tokenError);
199 | }
200 | } else {
201 | // Handle other paths
202 | res.writeHead(404, { 'Content-Type': 'text/plain' });
203 | res.end('Not Found');
204 | }
205 | } catch (err) {
206 | console.error('❌ Server error:', err);
207 | res.writeHead(500, { 'Content-Type': 'text/plain' });
208 | res.end('Internal Server Error');
209 | server.close();
210 | reject(err);
211 | }
212 | });
213 |
214 | // Start server on the specific port
215 | server.listen(callbackPort, () => {
216 | console.log(`🌐 OAuth callback server started on port ${callbackPort}`);
217 |
218 | // Generate authorization URL
219 | const authUrl = oAuth2Client.generateAuthUrl({
220 | access_type: 'offline',
221 | scope: SCOPES,
222 | prompt: 'consent'
223 | });
224 |
225 | console.log('🔗 Opening OAuth URL in browser...');
226 | console.log('📋 OAuth URL:', authUrl);
227 |
228 | // Open browser
229 | open(authUrl).catch(err => {
230 | console.error('❌ Failed to open browser:', err);
231 | console.log('🔗 Please manually open this URL in your browser:');
232 | console.log(authUrl);
233 | });
234 | });
235 |
236 | // Handle server errors
237 | server.on('error', (err) => {
238 | console.error('❌ Server error:', err);
239 | if (err.code === 'EADDRINUSE') {
240 | reject(new Error(`Port ${callbackPort} is already in use. Please close other applications using this port or update your Google Cloud Console redirect URI to use a different port.`));
241 | } else {
242 | reject(err);
243 | }
244 | });
245 |
246 | // Timeout after 5 minutes
247 | setTimeout(() => {
248 | server.close();
249 | reject(new Error('OAuth flow timed out after 5 minutes'));
250 | }, 5 * 60 * 1000);
251 |
252 | } catch (error) {
253 | reject(error);
254 | }
255 | });
256 | }
257 |
258 | /**
259 | * Gets an OAuth access token using TokenManager
260 | * @returns {Promise<string>} Access token
261 | */
262 | export async function getOAuthAccessToken() {
263 | logger.info('AUTH', 'Requesting OAuth access token');
264 | console.log('🔐 Getting OAuth access token...');
265 |
266 | const clientId = process.env.GOOGLE_APP_SCRIPT_API_CLIENT_ID;
267 | const clientSecret = process.env.GOOGLE_APP_SCRIPT_API_CLIENT_SECRET;
268 |
269 | if (!clientId || !clientSecret) {
270 | logger.error('AUTH', 'Missing OAuth credentials', {
271 | hasClientId: !!clientId,
272 | hasClientSecret: !!clientSecret
273 | });
274 | throw new Error('Missing required OAuth credentials: GOOGLE_APP_SCRIPT_API_CLIENT_ID and GOOGLE_APP_SCRIPT_API_CLIENT_SECRET must be set in environment variables');
275 | }
276 |
277 | try {
278 | const startTime = Date.now();
279 | logger.debug('AUTH', 'Getting valid access token from token manager');
280 |
281 | const accessToken = await tokenManager.getValidAccessToken(clientId, clientSecret);
282 | const duration = Date.now() - startTime;
283 |
284 | logger.info('AUTH', 'Access token obtained successfully', {
285 | duration: `${duration}ms`,
286 | tokenLength: accessToken ? accessToken.length : 0,
287 | tokenPrefix: accessToken ? accessToken.substring(0, 10) + '...' : null
288 | });
289 |
290 | console.log('✅ Access token obtained successfully');
291 | return accessToken;
292 | } catch (error) {
293 | logger.error('AUTH', 'Failed to obtain access token', {
294 | error: {
295 | message: error.message,
296 | stack: error.stack
297 | },
298 | hasStoredTokens: tokenManager.hasStoredTokens()
299 | });
300 |
301 | if (error.message.includes('No tokens found')) {
302 | console.error('❌ No OAuth tokens found.');
303 | console.log('💡 Please run the OAuth setup first:');
304 | console.log(' node oauth-setup.js');
305 | throw new Error('OAuth tokens not found. Please run: node oauth-setup.js');
306 | }
307 |
308 | console.error('❌ Error getting access token:', error);
309 | throw error;
310 | }
311 | }
312 |
313 | /**
314 | * Helper function to get authorization headers for API requests
315 | * @returns {Promise<Object>} Headers object with Authorization
316 | */
317 | export async function getAuthHeaders() {
318 | logger.debug('AUTH', 'Creating authorization headers');
319 | console.log('📋 Creating authorization headers...');
320 |
321 | const accessToken = await getOAuthAccessToken();
322 |
323 | const headers = {
324 | 'Authorization': `Bearer ${accessToken}`,
325 | 'Accept': 'application/json',
326 | 'Content-Type': 'application/json'
327 | };
328 |
329 | logger.debug('AUTH', 'Authorization headers created', {
330 | hasAuthorization: !!headers.Authorization,
331 | authHeaderLength: headers.Authorization ? headers.Authorization.length : 0,
332 | contentType: headers['Content-Type'],
333 | accept: headers.Accept
334 | });
335 |
336 | console.log('✅ Authorization headers created successfully');
337 | return headers;
338 | }
339 |
340 | /**
341 | * Manually trigger OAuth flow (useful for testing)
342 | * @returns {Promise<Object>} OAuth tokens
343 | */
344 | export async function manualOAuthFlow() {
345 | console.log('🔄 Starting manual OAuth flow...');
346 | const tokens = await startOAuthFlow();
347 |
348 | // Save tokens using TokenManager
349 | tokenManager.saveTokens(tokens);
350 |
351 | return tokens;
352 | }
353 |
354 | /**
355 | * Check if we have valid tokens
356 | * @returns {boolean} True if tokens are available
357 | */
358 | export function hasValidTokens() {
359 | return tokenManager.hasStoredTokens();
360 | }
361 |
362 | /**
363 | * Clear stored tokens (logout)
364 | */
365 | export function clearTokens() {
366 | console.log('🚪 Clearing stored tokens...');
367 | tokenManager.clearTokens();
368 | console.log('✅ Tokens cleared successfully');
369 | }
370 |
371 | /**
372 | * Get current token information (for debugging)
373 | * @returns {Object|null} Current token info
374 | */
375 | export function getTokenInfo() {
376 | return tokenManager.getTokenInfo();
377 | }
378 |
```
--------------------------------------------------------------------------------
/oauth-setup.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * OAuth Setup Script for Google Apps Script API
5 | * This script helps you obtain and securely store OAuth tokens
6 | * Enhanced with detailed logging for debugging and monitoring
7 | */
8 |
9 | import 'dotenv/config';
10 | import { manualOAuthFlow } from './lib/oauth-helper.js';
11 | import { TokenManager } from './lib/tokenManager.js';
12 | import { readFileSync } from 'fs';
13 | import { fileURLToPath } from 'url';
14 | import { dirname } from 'path';
15 | import os from 'os';
16 |
17 | // Enhanced logging utilities
18 | const log = {
19 | info: (msg, data = null) => {
20 | const timestamp = new Date().toISOString();
21 | console.log(`[${timestamp}] ℹ️ ${msg}`);
22 | if (data) console.log(`[${timestamp}] 📊 Data:`, data);
23 | },
24 | success: (msg, data = null) => {
25 | const timestamp = new Date().toISOString();
26 | console.log(`[${timestamp}] ✅ ${msg}`);
27 | if (data) console.log(`[${timestamp}] 📊 Data:`, data);
28 | },
29 | error: (msg, error = null) => {
30 | const timestamp = new Date().toISOString();
31 | console.error(`[${timestamp}] ❌ ${msg}`);
32 | if (error) {
33 | console.error(`[${timestamp}] 🐛 Error details:`, error.message);
34 | if (error.stack) console.error(`[${timestamp}] 📚 Stack trace:`, error.stack);
35 | }
36 | },
37 | warn: (msg, data = null) => {
38 | const timestamp = new Date().toISOString();
39 | console.warn(`[${timestamp}] ⚠️ ${msg}`);
40 | if (data) console.warn(`[${timestamp}] 📊 Data:`, data);
41 | },
42 | debug: (msg, data = null) => {
43 | if (process.env.DEBUG || process.argv.includes('--debug')) {
44 | const timestamp = new Date().toISOString();
45 | console.log(`[${timestamp}] 🔍 DEBUG: ${msg}`);
46 | if (data) console.log(`[${timestamp}] 🔍 DEBUG Data:`, JSON.stringify(data, null, 2));
47 | }
48 | },
49 | step: (step, total, msg) => {
50 | const timestamp = new Date().toISOString();
51 | console.log(`[${timestamp}] 🚀 Step ${step}/${total}: ${msg}`);
52 | },
53 | separator: () => {
54 | console.log('═'.repeat(80));
55 | },
56 | subseparator: () => {
57 | console.log('─'.repeat(60));
58 | }
59 | };
60 |
61 | // Performance timing utility
62 | class Timer {
63 | constructor(name) {
64 | this.name = name;
65 | this.start = Date.now();
66 | log.debug(`Timer started: ${name}`);
67 | }
68 |
69 | lap(description) {
70 | const elapsed = Date.now() - this.start;
71 | log.debug(`Timer ${this.name} - ${description}: ${elapsed}ms`);
72 | return elapsed;
73 | }
74 |
75 | end(description = 'completed') {
76 | const elapsed = Date.now() - this.start;
77 | log.info(`Timer ${this.name} ${description} in ${elapsed}ms`);
78 | return elapsed;
79 | }
80 | }
81 |
82 | // System information logging
83 | function logSystemInfo() {
84 | log.separator();
85 | log.info('📋 System Information');
86 | log.subseparator();
87 |
88 | const systemInfo = {
89 | platform: os.platform(),
90 | arch: os.arch(),
91 | nodeVersion: process.version,
92 | workingDirectory: process.cwd(),
93 | scriptPath: fileURLToPath(import.meta.url),
94 | arguments: process.argv.slice(2),
95 | environment: process.env.NODE_ENV || 'development',
96 | timestamp: new Date().toISOString()
97 | };
98 |
99 | Object.entries(systemInfo).forEach(([key, value]) => {
100 | log.info(` ${key}: ${value}`);
101 | });
102 |
103 | log.subseparator();
104 | }
105 |
106 | // Environment validation with detailed logging
107 | function validateEnvironment() {
108 | const timer = new Timer('Environment Validation');
109 | log.step(1, 8, 'Validating environment configuration');
110 |
111 | const envVars = {
112 | 'GOOGLE_APP_SCRIPT_API_CLIENT_ID': process.env.GOOGLE_APP_SCRIPT_API_CLIENT_ID,
113 | 'GOOGLE_APP_SCRIPT_API_CLIENT_SECRET': process.env.GOOGLE_APP_SCRIPT_API_CLIENT_SECRET,
114 | 'GOOGLE_APP_SCRIPT_API_REDIRECT_URI': process.env.GOOGLE_APP_SCRIPT_API_REDIRECT_URI
115 | };
116 |
117 | log.debug('Environment variables loaded:', {
118 | hasClientId: !!envVars.GOOGLE_APP_SCRIPT_API_CLIENT_ID,
119 | hasClientSecret: !!envVars.GOOGLE_APP_SCRIPT_API_CLIENT_SECRET,
120 | hasRedirectUri: !!envVars.GOOGLE_APP_SCRIPT_API_REDIRECT_URI,
121 | clientIdLength: envVars.GOOGLE_APP_SCRIPT_API_CLIENT_ID?.length || 0,
122 | clientSecretLength: envVars.GOOGLE_APP_SCRIPT_API_CLIENT_SECRET?.length || 0
123 | });
124 |
125 | const validation = {
126 | hasClientId: !!envVars.GOOGLE_APP_SCRIPT_API_CLIENT_ID &&
127 | envVars.GOOGLE_APP_SCRIPT_API_CLIENT_ID !== 'your_client_id_here',
128 | hasClientSecret: !!envVars.GOOGLE_APP_SCRIPT_API_CLIENT_SECRET &&
129 | envVars.GOOGLE_APP_SCRIPT_API_CLIENT_SECRET !== 'your_client_secret_here',
130 | hasRedirectUri: !!envVars.GOOGLE_APP_SCRIPT_API_REDIRECT_URI
131 | };
132 |
133 | log.info('Environment validation results:', validation);
134 |
135 | if (!validation.hasClientId) {
136 | log.error('Missing or invalid CLIENT_ID in environment');
137 | throw new Error('GOOGLE_APP_SCRIPT_API_CLIENT_ID is required');
138 | }
139 |
140 | if (!validation.hasClientSecret) {
141 | log.error('Missing or invalid CLIENT_SECRET in environment');
142 | throw new Error('GOOGLE_APP_SCRIPT_API_CLIENT_SECRET is required');
143 | }
144 |
145 | if (!validation.hasRedirectUri) {
146 | log.warn('No REDIRECT_URI specified, using default');
147 | }
148 |
149 | timer.end();
150 | log.success('Environment validation completed successfully');
151 | return envVars;
152 | }
153 |
154 | // File system operations with detailed logging
155 | function validateEnvFile() {
156 | const timer = new Timer('Env File Validation');
157 | log.step(2, 8, 'Validating .env file');
158 |
159 | const envPath = '.env';
160 | let envContent = '';
161 |
162 | try {
163 | log.debug(`Reading .env file from: ${envPath}`);
164 | envContent = readFileSync(envPath, 'utf8');
165 | const lines = envContent.split('\n');
166 | const nonEmptyLines = lines.filter(line => line.trim() && !line.trim().startsWith('#'));
167 |
168 | log.success('Successfully read .env file', {
169 | totalLines: lines.length,
170 | nonEmptyLines: nonEmptyLines.length,
171 | fileSize: envContent.length
172 | });
173 |
174 | // Analyze .env file content
175 | const envAnalysis = {
176 | hasClientId: envContent.includes('GOOGLE_APP_SCRIPT_API_CLIENT_ID='),
177 | hasClientSecret: envContent.includes('GOOGLE_APP_SCRIPT_API_CLIENT_SECRET='),
178 | hasRedirectUri: envContent.includes('GOOGLE_APP_SCRIPT_API_REDIRECT_URI='),
179 | hasComments: envContent.includes('#'),
180 | hasPlaceholders: envContent.includes('your_client_id_here') || envContent.includes('your_client_secret_here')
181 | };
182 |
183 | log.debug('Env file analysis:', envAnalysis);
184 |
185 | if (envAnalysis.hasPlaceholders) {
186 | log.warn('Found placeholder values in .env file - these need to be replaced with actual credentials');
187 | }
188 |
189 | } catch (error) {
190 | log.error('Failed to read .env file', error);
191 | log.info('Expected .env file location:', process.cwd() + '/.env');
192 | log.info('Please create .env file with required credentials');
193 | throw error;
194 | }
195 |
196 | timer.end();
197 | return envContent;
198 | }
199 |
200 | // Token management with detailed logging
201 | function analyzeExistingTokens(tokenManager) {
202 | const timer = new Timer('Token Analysis');
203 | log.step(3, 8, 'Analyzing existing tokens');
204 |
205 | try {
206 | const tokenInfo = tokenManager.getTokenInfo();
207 |
208 | log.debug('Token analysis started');
209 | log.info('Token information retrieved:', {
210 | hasTokens: tokenInfo.hasTokens,
211 | location: tokenInfo.location,
212 | savedAt: tokenInfo.savedAt,
213 | expiresAt: tokenInfo.expiresAt,
214 | status: tokenInfo.status,
215 | isExpired: tokenInfo.isExpired
216 | });
217 |
218 | if (tokenInfo.hasTokens) {
219 | log.success('Found existing tokens');
220 |
221 | if (tokenInfo.isExpired) {
222 | log.warn('Existing tokens are expired');
223 | } else {
224 | log.success('Existing tokens are still valid');
225 | }
226 | // Additional token validation
227 | try {
228 | const tokenData = tokenManager.loadTokens();
229 | if (tokenData) {
230 | log.debug('Token data structure validation:', {
231 | hasAccessToken: !!tokenData.access_token,
232 | hasRefreshToken: !!tokenData.refresh_token,
233 | hasTokenType: !!tokenData.token_type,
234 | hasScope: !!tokenData.scope,
235 | expiresIn: tokenData.expires_in
236 | });
237 | }
238 | } catch (tokenError) {
239 | log.warn('Could not validate token data structure', tokenError);
240 | }
241 | } else {
242 | log.info('No existing tokens found');
243 | }
244 |
245 | timer.end();
246 | return tokenInfo;
247 |
248 | } catch (error) {
249 | log.error('Failed to analyze existing tokens', error);
250 | timer.end();
251 | throw error;
252 | }
253 | }
254 |
255 | // OAuth flow with enhanced logging
256 | async function executeOAuthFlow() {
257 | const timer = new Timer('OAuth Flow');
258 | log.step(4, 8, 'Starting OAuth authorization flow');
259 |
260 | try {
261 | log.info('Initiating manual OAuth flow');
262 | log.debug('OAuth flow configuration:', {
263 | clientId: process.env.GOOGLE_APP_SCRIPT_API_CLIENT_ID?.substring(0, 10) + '...',
264 | redirectUri: process.env.GOOGLE_APP_SCRIPT_API_REDIRECT_URI,
265 | scope: 'https://www.googleapis.com/auth/script.projects'
266 | });
267 |
268 | log.info('🌐 Opening browser for OAuth authorization...');
269 | log.info('📱 Please complete the authorization in your browser');
270 | log.info('⏳ Waiting for OAuth callback...');
271 |
272 | const tokens = await manualOAuthFlow();
273 |
274 | log.debug('OAuth flow completed, analyzing response');
275 | const tokenAnalysis = {
276 | hasAccessToken: !!tokens.access_token,
277 | hasRefreshToken: !!tokens.refresh_token,
278 | hasTokenType: !!tokens.token_type,
279 | hasScope: !!tokens.scope,
280 | hasExpiresIn: !!tokens.expires_in,
281 | accessTokenLength: tokens.access_token?.length || 0,
282 | refreshTokenLength: tokens.refresh_token?.length || 0,
283 | tokenType: tokens.token_type,
284 | scope: tokens.scope,
285 | expiresIn: tokens.expires_in
286 | };
287 |
288 | log.success('OAuth tokens received', tokenAnalysis);
289 |
290 | if (!tokens.refresh_token) {
291 | log.error('No refresh token received - this is required for long-term access');
292 | throw new Error('Refresh token missing from OAuth response');
293 | }
294 |
295 | timer.end();
296 | return tokens;
297 |
298 | } catch (error) {
299 | log.error('OAuth flow failed', error);
300 | timer.end();
301 | throw error;
302 | }
303 | }
304 |
305 | // Token storage with detailed logging
306 | function saveTokensSecurely(tokenManager, tokens) {
307 | const timer = new Timer('Token Storage');
308 | log.step(5, 8, 'Saving tokens securely');
309 |
310 | try {
311 | log.debug('Preparing to save tokens');
312 | log.info('Token storage location:', tokenManager.getTokenInfo().location);
313 |
314 | // Pre-save validation
315 | const preValidation = {
316 | hasAccessToken: !!tokens.access_token,
317 | hasRefreshToken: !!tokens.refresh_token,
318 | tokenManagerReady: !!tokenManager
319 | };
320 |
321 | log.debug('Pre-save validation:', preValidation);
322 |
323 | if (!preValidation.hasAccessToken || !preValidation.hasRefreshToken) {
324 | throw new Error('Invalid token data - missing required tokens');
325 | }
326 |
327 | log.info('💾 Writing tokens to secure storage...');
328 | tokenManager.saveTokens(tokens);
329 |
330 | // Post-save verification
331 | const verification = tokenManager.getTokenInfo();
332 | log.success('Tokens saved successfully', {
333 | location: verification.location,
334 | hasTokens: verification.hasTokens,
335 | savedAt: verification.savedAt,
336 | status: verification.status
337 | });
338 | // Additional verification - try to read back the tokens
339 | try {
340 | const savedTokens = tokenManager.loadTokens();
341 | const verificationCheck = {
342 | canReadBack: !!savedTokens,
343 | accessTokenMatches: savedTokens?.access_token === tokens.access_token,
344 | refreshTokenMatches: savedTokens?.refresh_token === tokens.refresh_token
345 | };
346 |
347 | log.debug('Token verification check:', verificationCheck);
348 |
349 | if (!verificationCheck.canReadBack) {
350 | throw new Error('Cannot read back saved tokens');
351 | }
352 |
353 | if (!verificationCheck.accessTokenMatches || !verificationCheck.refreshTokenMatches) {
354 | log.warn('Saved tokens do not match original tokens');
355 | } else {
356 | log.success('Token integrity verified');
357 | }
358 |
359 | } catch (verificationError) {
360 | log.warn('Could not verify saved tokens', verificationError);
361 | }
362 |
363 | timer.end();
364 | return verification;
365 |
366 | } catch (error) {
367 | log.error('Failed to save tokens securely', error);
368 | timer.end();
369 | throw error;
370 | }
371 | }
372 |
373 | // Main setup function with comprehensive logging
374 | async function setupOAuth() {
375 | const mainTimer = new Timer('Complete OAuth Setup');
376 |
377 | log.separator();
378 | log.info('🔐 Google Apps Script API OAuth Setup - Enhanced Logging Version');
379 | log.separator();
380 |
381 | // Log system information
382 | logSystemInfo();
383 |
384 | try {
385 | log.info('📋 Starting OAuth setup process...');
386 | log.info('📝 This script will guide you through OAuth authentication setup');
387 |
388 | // Initialize token manager
389 | log.step(0, 8, 'Initializing token manager');
390 | const tokenManager = new TokenManager();
391 | log.success('Token manager initialized');
392 |
393 | // Handle command line arguments
394 | const args = process.argv.slice(2);
395 | log.debug('Command line arguments:', args);
396 |
397 | // Handle info command
398 | if (args.includes('--info')) {
399 | log.info('📊 Information mode requested');
400 | const tokenInfo = analyzeExistingTokens(tokenManager);
401 |
402 | console.log('\n🔍 Token Information Summary:');
403 | console.log('═'.repeat(40));
404 |
405 | if (tokenInfo.hasTokens) {
406 | console.log('✅ Status: Tokens found');
407 | console.log(`📁 Location: ${tokenInfo.location}`);
408 | console.log(`💾 Saved: ${tokenInfo.savedAt}`);
409 | console.log(`⏰ Expires: ${tokenInfo.expiresAt}`);
410 | console.log(`📊 Status: ${tokenInfo.status}`);
411 | console.log(`🔐 Scope: ${tokenInfo.scope || 'Not specified'}`);
412 | } else {
413 | console.log('❌ Status: No tokens found');
414 | console.log(`📁 Expected location: ${tokenInfo.location}`);
415 | console.log('\n💡 Run "node oauth-setup.js" to set up OAuth tokens');
416 | }
417 |
418 | mainTimer.end('info command completed');
419 | process.exit(0);
420 | }
421 |
422 | // Handle clear command
423 | if (args.includes('--clear')) {
424 | log.info('🗑️ Clear tokens mode requested');
425 | const tokenInfo = analyzeExistingTokens(tokenManager);
426 |
427 | if (tokenInfo.hasTokens) {
428 | log.info('Clearing existing tokens...');
429 | tokenManager.clearTokens();
430 | log.success('Tokens cleared successfully');
431 | } else {
432 | log.info('No tokens found to clear');
433 | }
434 |
435 | mainTimer.end('clear command completed');
436 | process.exit(0);
437 | }
438 |
439 | // Validate environment
440 | const envVars = validateEnvironment();
441 | const envContent = validateEnvFile();
442 |
443 | // Analyze existing tokens
444 | const tokenInfo = analyzeExistingTokens(tokenManager);
445 |
446 | // Check if we should proceed with OAuth flow
447 | if (tokenInfo.hasTokens && !tokenInfo.isExpired && !args.includes('--force')) {
448 | log.success('Valid tokens already exist');
449 | console.log('\n✅ You already have valid OAuth tokens stored.');
450 | console.log('💡 To force new token generation: node oauth-setup.js --force');
451 | console.log('🗑️ To clear existing tokens: node oauth-setup.js --clear');
452 | console.log('📊 To view token info: node oauth-setup.js --info');
453 |
454 | mainTimer.end('setup skipped - valid tokens exist');
455 | process.exit(0);
456 | }
457 |
458 | if (tokenInfo.hasTokens && args.includes('--force')) {
459 | log.warn('Force mode enabled - will replace existing tokens');
460 | }
461 |
462 | // Execute OAuth flow
463 | const tokens = await executeOAuthFlow();
464 |
465 | // Save tokens securely
466 | const saveResult = saveTokensSecurely(tokenManager, tokens);
467 |
468 | // Final verification and success message
469 | log.step(6, 8, 'Performing final verification');
470 | const finalVerification = analyzeExistingTokens(tokenManager);
471 |
472 | if (finalVerification.hasTokens && !finalVerification.isExpired) {
473 | log.success('Final verification passed - OAuth setup completed successfully');
474 | } else {
475 | log.error('Final verification failed');
476 | throw new Error('Setup completed but final verification failed');
477 | }
478 |
479 | // Success summary
480 | log.step(7, 8, 'Generating setup summary');
481 | log.separator();
482 | log.success('🎉 OAuth Setup Completed Successfully!');
483 | log.subseparator();
484 |
485 | const summary = {
486 | tokenLocation: saveResult.location,
487 | setupDuration: mainTimer.end('setup completed'),
488 | tokenStatus: finalVerification.status,
489 | nextSteps: [
490 | 'Test your setup: node test-token-management.js',
491 | 'Run MCP server: node mcpServer.js',
492 | 'Check token info: node oauth-setup.js --info'
493 | ]
494 | };
495 |
496 | log.info('Setup Summary:', summary);
497 |
498 | console.log('\n🎉 Setup Complete! Next Steps:');
499 | summary.nextSteps.forEach((step, index) => {
500 | console.log(` ${index + 1}. ${step}`);
501 | });
502 |
503 | log.step(8, 8, 'OAuth setup process completed');
504 |
505 | } catch (error) {
506 | log.error('OAuth setup failed', error);
507 |
508 | console.log('\n🔧 Troubleshooting Guide:');
509 | console.log('════════════════════════');
510 | console.log('1. 🌐 Check your internet connection');
511 | console.log('2. 🔑 Verify CLIENT_ID and CLIENT_SECRET are correct');
512 | console.log('3. 🔗 Ensure redirect URI is registered in Google Cloud Console');
513 | console.log('4. 🔌 Confirm Google Apps Script API is enabled');
514 | console.log('5. 🔄 Try revoking and re-creating OAuth credentials');
515 | console.log('6. 🐛 Enable debug mode: node oauth-setup.js --debug');
516 | console.log('\n📖 For detailed instructions, see OAUTH_SETUP.md');
517 |
518 | mainTimer.end('setup failed');
519 | process.exit(1);
520 | }
521 | }
522 |
523 | // Error handling and execution
524 | setupOAuth().catch((error) => {
525 | log.error('💥 Unexpected error during setup', error);
526 | console.error('\n🚨 Critical Error Details:');
527 | console.error('Message:', error.message);
528 | if (error.stack) {
529 | console.error('Stack:', error.stack);
530 | }
531 | process.exit(1);
532 | });
```