@dongtran/google-calendar-mcp 2.4.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.
@@ -0,0 +1,1185 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __esm = (fn, res) => function __init() {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ };
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+
22
+ // src/auth/paths.js
23
+ var paths_exports = {};
24
+ __export(paths_exports, {
25
+ getAccountMode: () => getAccountMode,
26
+ getLegacyTokenPath: () => getLegacyTokenPath,
27
+ getSecureTokenPath: () => getSecureTokenPath,
28
+ validateAccountId: () => validateAccountId
29
+ });
30
+ import path from "path";
31
+ import { homedir } from "os";
32
+ function getSecureTokenPath() {
33
+ if (process.env.GOOGLE_CALENDAR_MCP_TOKEN_PATH) {
34
+ return path.resolve(process.env.GOOGLE_CALENDAR_MCP_TOKEN_PATH);
35
+ }
36
+ const configDir = process.env.XDG_CONFIG_HOME || path.join(homedir(), ".config");
37
+ return path.join(configDir, "google-calendar-mcp", "tokens.json");
38
+ }
39
+ function getLegacyTokenPath() {
40
+ return path.join(process.cwd(), ".gcp-saved-tokens.json");
41
+ }
42
+ function validateAccountId(accountId) {
43
+ if (!accountId || accountId.length === 0) {
44
+ throw new Error("Invalid account ID. Must be 1-64 characters: lowercase letters, numbers, dashes, underscores only.");
45
+ }
46
+ if (RESERVED_NAMES.includes(accountId)) {
47
+ throw new Error(`Account ID "${accountId}" is reserved and cannot be used.`);
48
+ }
49
+ if (!/^[a-z0-9_-]{1,64}$/.test(accountId)) {
50
+ throw new Error("Invalid account ID. Must be 1-64 characters: lowercase letters, numbers, dashes, underscores only.");
51
+ }
52
+ return accountId;
53
+ }
54
+ function getAccountMode() {
55
+ const explicitMode = process.env.GOOGLE_ACCOUNT_MODE;
56
+ if (explicitMode !== void 0 && explicitMode !== null) {
57
+ return validateAccountId(explicitMode);
58
+ }
59
+ if (process.env.NODE_ENV === "test") {
60
+ return "test";
61
+ }
62
+ return "normal";
63
+ }
64
+ var RESERVED_NAMES;
65
+ var init_paths = __esm({
66
+ "src/auth/paths.js"() {
67
+ "use strict";
68
+ RESERVED_NAMES = [
69
+ ".",
70
+ "..",
71
+ "con",
72
+ "prn",
73
+ "aux",
74
+ "nul",
75
+ "com1",
76
+ "com2",
77
+ "com3",
78
+ "com4",
79
+ "lpt1",
80
+ "lpt2",
81
+ "lpt3"
82
+ ];
83
+ }
84
+ });
85
+
86
+ // src/auth/client.ts
87
+ import { OAuth2Client } from "google-auth-library";
88
+ import * as fs from "fs/promises";
89
+
90
+ // src/auth/utils.ts
91
+ import * as path2 from "path";
92
+ init_paths();
93
+ import { fileURLToPath } from "url";
94
+ function getProjectRoot() {
95
+ const __dirname2 = path2.dirname(fileURLToPath(import.meta.url));
96
+ const projectRoot = path2.join(__dirname2, "..");
97
+ return path2.resolve(projectRoot);
98
+ }
99
+ function getAccountMode2() {
100
+ return getAccountMode();
101
+ }
102
+ function getSecureTokenPath2() {
103
+ return getSecureTokenPath();
104
+ }
105
+ function getLegacyTokenPath2() {
106
+ return getLegacyTokenPath();
107
+ }
108
+ function getKeysFilePath() {
109
+ const envCredentialsPath = process.env.GOOGLE_OAUTH_CREDENTIALS;
110
+ if (envCredentialsPath) {
111
+ return path2.resolve(envCredentialsPath);
112
+ }
113
+ const projectRoot = getProjectRoot();
114
+ const keysPath = path2.join(projectRoot, "gcp-oauth.keys.json");
115
+ return keysPath;
116
+ }
117
+ function generateCredentialsErrorMessage() {
118
+ return `
119
+ OAuth credentials not found. Please provide credentials using one of these methods:
120
+
121
+ 1. Environment variable:
122
+ Set GOOGLE_OAUTH_CREDENTIALS to the path of your credentials file:
123
+ export GOOGLE_OAUTH_CREDENTIALS="/path/to/gcp-oauth.keys.json"
124
+
125
+ 2. Default file path:
126
+ Place your gcp-oauth.keys.json file in the package root directory.
127
+
128
+ Token storage:
129
+ - Tokens are saved to: ${getSecureTokenPath2()}
130
+ - To use a custom token location, set GOOGLE_CALENDAR_MCP_TOKEN_PATH environment variable
131
+
132
+ To get OAuth credentials:
133
+ 1. Go to the Google Cloud Console (https://console.cloud.google.com/)
134
+ 2. Create or select a project
135
+ 3. Enable the Google Calendar API
136
+ 4. Create OAuth 2.0 credentials
137
+ 5. Download the credentials file as gcp-oauth.keys.json
138
+ `.trim();
139
+ }
140
+
141
+ // src/auth/client.ts
142
+ async function loadCredentialsFromFile() {
143
+ const keysContent = await fs.readFile(getKeysFilePath(), "utf-8");
144
+ const keys = JSON.parse(keysContent);
145
+ if (keys.installed) {
146
+ const { client_id, client_secret, redirect_uris } = keys.installed;
147
+ return { client_id, client_secret, redirect_uris };
148
+ } else if (keys.client_id && keys.client_secret) {
149
+ return {
150
+ client_id: keys.client_id,
151
+ client_secret: keys.client_secret,
152
+ redirect_uris: keys.redirect_uris || ["http://localhost:3000/oauth2callback"]
153
+ };
154
+ } else {
155
+ throw new Error('Invalid credentials file format. Expected either "installed" object or direct client_id/client_secret fields.');
156
+ }
157
+ }
158
+ async function loadCredentialsWithFallback() {
159
+ try {
160
+ return await loadCredentialsFromFile();
161
+ } catch (fileError) {
162
+ const errorMessage = generateCredentialsErrorMessage();
163
+ throw new Error(`${errorMessage}
164
+
165
+ Original error: ${fileError instanceof Error ? fileError.message : fileError}`);
166
+ }
167
+ }
168
+ async function initializeOAuth2Client() {
169
+ try {
170
+ const credentials = await loadCredentialsWithFallback();
171
+ return new OAuth2Client({
172
+ clientId: credentials.client_id,
173
+ clientSecret: credentials.client_secret,
174
+ redirectUri: credentials.redirect_uris[0]
175
+ });
176
+ } catch (error) {
177
+ throw new Error(`Error loading OAuth keys: ${error instanceof Error ? error.message : error}`);
178
+ }
179
+ }
180
+ async function loadCredentials() {
181
+ try {
182
+ const credentials = await loadCredentialsWithFallback();
183
+ if (!credentials.client_id || !credentials.client_secret) {
184
+ throw new Error("Client ID or Client Secret missing in credentials.");
185
+ }
186
+ return {
187
+ client_id: credentials.client_id,
188
+ client_secret: credentials.client_secret
189
+ };
190
+ } catch (error) {
191
+ throw new Error(`Error loading credentials: ${error instanceof Error ? error.message : error}`);
192
+ }
193
+ }
194
+
195
+ // src/auth/server.ts
196
+ import { OAuth2Client as OAuth2Client3 } from "google-auth-library";
197
+
198
+ // src/auth/tokenManager.ts
199
+ import { OAuth2Client as OAuth2Client2 } from "google-auth-library";
200
+ import fs2 from "fs/promises";
201
+ import { GaxiosError } from "gaxios";
202
+ import { mkdir } from "fs/promises";
203
+ import { dirname as dirname2 } from "path";
204
+ var TokenManager = class {
205
+ oauth2Client;
206
+ tokenPath;
207
+ accountMode;
208
+ accounts = /* @__PURE__ */ new Map();
209
+ credentials;
210
+ writeQueue = Promise.resolve();
211
+ constructor(oauth2Client) {
212
+ this.oauth2Client = oauth2Client;
213
+ this.tokenPath = getSecureTokenPath2();
214
+ this.accountMode = getAccountMode2();
215
+ this.credentials = {
216
+ clientId: oauth2Client._clientId,
217
+ clientSecret: oauth2Client._clientSecret,
218
+ redirectUri: oauth2Client._redirectUri
219
+ };
220
+ this.setupTokenRefresh();
221
+ }
222
+ // Method to expose the token path
223
+ getTokenPath() {
224
+ return this.tokenPath;
225
+ }
226
+ // Method to get current account mode
227
+ getAccountMode() {
228
+ return this.accountMode;
229
+ }
230
+ // Method to switch account mode (supports arbitrary account IDs)
231
+ setAccountMode(mode) {
232
+ this.accountMode = mode;
233
+ }
234
+ async ensureTokenDirectoryExists() {
235
+ try {
236
+ await mkdir(dirname2(this.tokenPath), { recursive: true });
237
+ } catch (error) {
238
+ process.stderr.write(`Failed to create token directory: ${error}
239
+ `);
240
+ }
241
+ }
242
+ isFileNotFoundError(error) {
243
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
244
+ }
245
+ async writeTokenFile(tokens) {
246
+ await this.ensureTokenDirectoryExists();
247
+ await fs2.writeFile(this.tokenPath, JSON.stringify(tokens, null, 2), { mode: 384 });
248
+ }
249
+ async loadMultiAccountTokens() {
250
+ try {
251
+ const fileContent = await fs2.readFile(this.tokenPath, "utf-8");
252
+ const parsed = JSON.parse(fileContent);
253
+ if (parsed.access_token || parsed.refresh_token) {
254
+ const multiAccountTokens = {
255
+ normal: parsed
256
+ };
257
+ await this.saveMultiAccountTokens(multiAccountTokens);
258
+ return multiAccountTokens;
259
+ }
260
+ return parsed;
261
+ } catch (error) {
262
+ if (this.isFileNotFoundError(error)) {
263
+ return {};
264
+ }
265
+ throw error;
266
+ }
267
+ }
268
+ /**
269
+ * Raw token file read without migration logic.
270
+ * Used for atomic read-modify-write operations where we need to re-read current state.
271
+ */
272
+ async loadMultiAccountTokensRaw() {
273
+ try {
274
+ const fileContent = await fs2.readFile(this.tokenPath, "utf-8");
275
+ return JSON.parse(fileContent);
276
+ } catch (error) {
277
+ if (this.isFileNotFoundError(error)) {
278
+ return {};
279
+ }
280
+ throw error;
281
+ }
282
+ }
283
+ async saveMultiAccountTokens(multiAccountTokens) {
284
+ return this.enqueueTokenWrite(async () => {
285
+ await this.writeTokenFile(multiAccountTokens);
286
+ });
287
+ }
288
+ enqueueTokenWrite(operation) {
289
+ const pendingWrite = this.writeQueue.catch(() => void 0).then(operation);
290
+ this.writeQueue = pendingWrite.catch((error) => {
291
+ process.stderr.write(`Error writing token file: ${error instanceof Error ? error.message : error}
292
+ `);
293
+ throw error;
294
+ }).catch(() => void 0);
295
+ return pendingWrite;
296
+ }
297
+ setupTokenRefresh() {
298
+ this.setupTokenRefreshForAccount(this.oauth2Client, this.accountMode);
299
+ }
300
+ /**
301
+ * Set up token refresh handler for a specific account
302
+ * Uses enqueueTokenWrite to prevent race conditions when multiple accounts refresh simultaneously
303
+ */
304
+ setupTokenRefreshForAccount(client, accountId) {
305
+ client.on("tokens", async (newTokens) => {
306
+ try {
307
+ await this.enqueueTokenWrite(async () => {
308
+ const multiAccountTokens = await this.loadMultiAccountTokens();
309
+ const currentTokens = multiAccountTokens[accountId] || {};
310
+ const updatedTokens = {
311
+ ...currentTokens,
312
+ ...newTokens,
313
+ refresh_token: newTokens.refresh_token || currentTokens.refresh_token
314
+ };
315
+ multiAccountTokens[accountId] = updatedTokens;
316
+ await this.writeTokenFile(multiAccountTokens);
317
+ });
318
+ if (process.env.NODE_ENV !== "test") {
319
+ process.stderr.write(`Tokens updated and saved for ${accountId} account
320
+ `);
321
+ }
322
+ } catch (error) {
323
+ process.stderr.write("Error saving updated tokens: ");
324
+ if (error instanceof Error) {
325
+ process.stderr.write(error.message);
326
+ } else if (typeof error === "string") {
327
+ process.stderr.write(error);
328
+ }
329
+ process.stderr.write("\n");
330
+ }
331
+ });
332
+ }
333
+ async migrateLegacyTokens() {
334
+ const legacyPath = getLegacyTokenPath2();
335
+ try {
336
+ if (!await fs2.access(legacyPath).then(() => true).catch(() => false)) {
337
+ return false;
338
+ }
339
+ const legacyTokens = JSON.parse(await fs2.readFile(legacyPath, "utf-8"));
340
+ if (!legacyTokens || typeof legacyTokens !== "object") {
341
+ process.stderr.write("Invalid legacy token format, skipping migration\n");
342
+ return false;
343
+ }
344
+ await this.writeTokenFile(legacyTokens);
345
+ process.stderr.write(`Migrated tokens from legacy location: ${legacyPath} to: ${this.tokenPath}
346
+ `);
347
+ try {
348
+ await fs2.unlink(legacyPath);
349
+ process.stderr.write("Removed legacy token file\n");
350
+ } catch (unlinkErr) {
351
+ process.stderr.write(`Warning: Could not remove legacy token file: ${unlinkErr}
352
+ `);
353
+ }
354
+ return true;
355
+ } catch (error) {
356
+ process.stderr.write(`Error migrating legacy tokens: ${error}
357
+ `);
358
+ return false;
359
+ }
360
+ }
361
+ async loadSavedTokens() {
362
+ try {
363
+ await this.ensureTokenDirectoryExists();
364
+ const tokenExists = await fs2.access(this.tokenPath).then(() => true).catch(() => false);
365
+ if (!tokenExists) {
366
+ const migrated = await this.migrateLegacyTokens();
367
+ if (!migrated) {
368
+ process.stderr.write(`No token file found at: ${this.tokenPath}
369
+ `);
370
+ return false;
371
+ }
372
+ }
373
+ const multiAccountTokens = await this.loadMultiAccountTokens();
374
+ const tokens = multiAccountTokens[this.accountMode];
375
+ if (!tokens || typeof tokens !== "object") {
376
+ process.stderr.write(`No tokens found for ${this.accountMode} account in file: ${this.tokenPath}
377
+ `);
378
+ return false;
379
+ }
380
+ this.oauth2Client.setCredentials(tokens);
381
+ process.stderr.write(`Loaded tokens for ${this.accountMode} account
382
+ `);
383
+ return true;
384
+ } catch (error) {
385
+ process.stderr.write(`Error loading tokens for ${this.accountMode} account: `);
386
+ if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
387
+ try {
388
+ await fs2.unlink(this.tokenPath);
389
+ process.stderr.write("Removed potentially corrupted token file\n");
390
+ } catch (unlinkErr) {
391
+ }
392
+ }
393
+ return false;
394
+ }
395
+ }
396
+ async refreshTokensIfNeeded() {
397
+ const expiryDate = this.oauth2Client.credentials.expiry_date;
398
+ const isExpired = expiryDate ? Date.now() >= expiryDate - 5 * 60 * 1e3 : !this.oauth2Client.credentials.access_token;
399
+ if (isExpired && this.oauth2Client.credentials.refresh_token) {
400
+ if (process.env.NODE_ENV !== "test") {
401
+ process.stderr.write(`Auth token expired or nearing expiry for ${this.accountMode} account, refreshing...
402
+ `);
403
+ }
404
+ try {
405
+ const response = await this.oauth2Client.refreshAccessToken();
406
+ const newTokens = response.credentials;
407
+ if (!newTokens.access_token) {
408
+ throw new Error("Received invalid tokens during refresh");
409
+ }
410
+ this.oauth2Client.setCredentials(newTokens);
411
+ if (process.env.NODE_ENV !== "test") {
412
+ process.stderr.write(`Token refreshed successfully for ${this.accountMode} account
413
+ `);
414
+ }
415
+ return true;
416
+ } catch (refreshError) {
417
+ if (refreshError instanceof GaxiosError && refreshError.response?.data?.error === "invalid_grant") {
418
+ process.stderr.write(`Error refreshing auth token for ${this.accountMode} account: Invalid grant. Token likely expired or revoked. Please re-authenticate.
419
+ `);
420
+ return false;
421
+ } else {
422
+ process.stderr.write(`Error refreshing auth token for ${this.accountMode} account: `);
423
+ if (refreshError instanceof Error) {
424
+ process.stderr.write(refreshError.message);
425
+ } else if (typeof refreshError === "string") {
426
+ process.stderr.write(refreshError);
427
+ }
428
+ process.stderr.write("\n");
429
+ return false;
430
+ }
431
+ }
432
+ } else if (!this.oauth2Client.credentials.access_token && !this.oauth2Client.credentials.refresh_token) {
433
+ process.stderr.write(`No access or refresh token available for ${this.accountMode} account. Please re-authenticate.
434
+ `);
435
+ return false;
436
+ } else {
437
+ return true;
438
+ }
439
+ }
440
+ async validateTokens(accountMode) {
441
+ const modeToValidate = accountMode || this.accountMode;
442
+ const currentMode = this.accountMode;
443
+ try {
444
+ if (modeToValidate !== currentMode) {
445
+ this.accountMode = modeToValidate;
446
+ }
447
+ if (!this.oauth2Client.credentials || !this.oauth2Client.credentials.access_token) {
448
+ if (!await this.loadSavedTokens()) {
449
+ return false;
450
+ }
451
+ if (!this.oauth2Client.credentials || !this.oauth2Client.credentials.access_token) {
452
+ return false;
453
+ }
454
+ }
455
+ const result = await this.refreshTokensIfNeeded();
456
+ return result;
457
+ } finally {
458
+ if (modeToValidate !== currentMode) {
459
+ this.accountMode = currentMode;
460
+ }
461
+ }
462
+ }
463
+ async saveTokens(tokens, email) {
464
+ try {
465
+ await this.enqueueTokenWrite(async () => {
466
+ const multiAccountTokens = await this.loadMultiAccountTokens();
467
+ const cachedTokens = { ...tokens };
468
+ if (email) {
469
+ cachedTokens.cached_email = email;
470
+ }
471
+ multiAccountTokens[this.accountMode] = cachedTokens;
472
+ await this.writeTokenFile(multiAccountTokens);
473
+ });
474
+ this.oauth2Client.setCredentials(tokens);
475
+ process.stderr.write(`Tokens saved successfully for ${this.accountMode} account to: ${this.tokenPath}
476
+ `);
477
+ } catch (error) {
478
+ process.stderr.write(`Error saving tokens for ${this.accountMode} account: ${error}
479
+ `);
480
+ throw error;
481
+ }
482
+ }
483
+ async clearTokens() {
484
+ try {
485
+ this.oauth2Client.setCredentials({});
486
+ await this.enqueueTokenWrite(async () => {
487
+ const multiAccountTokens = await this.loadMultiAccountTokens();
488
+ delete multiAccountTokens[this.accountMode];
489
+ if (Object.keys(multiAccountTokens).length === 0) {
490
+ await fs2.unlink(this.tokenPath);
491
+ process.stderr.write(`All tokens cleared, file deleted
492
+ `);
493
+ } else {
494
+ await this.writeTokenFile(multiAccountTokens);
495
+ process.stderr.write(`Tokens cleared for ${this.accountMode} account
496
+ `);
497
+ }
498
+ });
499
+ } catch (error) {
500
+ if (this.isFileNotFoundError(error)) {
501
+ process.stderr.write("Token file already deleted\n");
502
+ } else {
503
+ process.stderr.write(`Error clearing tokens for ${this.accountMode} account: ${error}
504
+ `);
505
+ }
506
+ }
507
+ }
508
+ // Method to list available accounts
509
+ async listAvailableAccounts() {
510
+ try {
511
+ const multiAccountTokens = await this.loadMultiAccountTokens();
512
+ return Object.keys(multiAccountTokens);
513
+ } catch (error) {
514
+ return [];
515
+ }
516
+ }
517
+ /**
518
+ * Remove a specific account's tokens from storage.
519
+ * @param accountId - The account ID to remove
520
+ * @throws Error if account doesn't exist or removal fails
521
+ */
522
+ async removeAccount(accountId) {
523
+ const normalizedId = accountId.toLowerCase();
524
+ await this.enqueueTokenWrite(async () => {
525
+ const multiAccountTokens = await this.loadMultiAccountTokens();
526
+ if (!multiAccountTokens[normalizedId]) {
527
+ throw new Error(`Account "${normalizedId}" not found`);
528
+ }
529
+ delete multiAccountTokens[normalizedId];
530
+ if (Object.keys(multiAccountTokens).length === 0) {
531
+ await fs2.unlink(this.tokenPath);
532
+ process.stderr.write(`All tokens cleared, file deleted
533
+ `);
534
+ } else {
535
+ await this.writeTokenFile(multiAccountTokens);
536
+ process.stderr.write(`Account "${normalizedId}" removed successfully
537
+ `);
538
+ }
539
+ this.accounts.delete(normalizedId);
540
+ });
541
+ }
542
+ // Method to switch to a different account (supports arbitrary account IDs)
543
+ async switchAccount(newMode) {
544
+ this.accountMode = newMode;
545
+ return this.loadSavedTokens();
546
+ }
547
+ /**
548
+ * Load all authenticated accounts from token file
549
+ * Returns a Map of account ID to OAuth2Client
550
+ *
551
+ * Reuses existing OAuth2Client instances to prevent memory leaks
552
+ * Sets up token refresh handlers for new accounts
553
+ */
554
+ async loadAllAccounts() {
555
+ try {
556
+ const multiAccountTokens = await this.loadMultiAccountTokens();
557
+ for (const accountId of this.accounts.keys()) {
558
+ if (!multiAccountTokens[accountId]) {
559
+ const client = this.accounts.get(accountId);
560
+ if (client) {
561
+ client.removeAllListeners("tokens");
562
+ }
563
+ this.accounts.delete(accountId);
564
+ }
565
+ }
566
+ for (const [accountId, tokens] of Object.entries(multiAccountTokens)) {
567
+ try {
568
+ const { validateAccountId: validateAccountId2 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
569
+ validateAccountId2(accountId);
570
+ if (!tokens || typeof tokens !== "object" || !tokens.access_token) {
571
+ continue;
572
+ }
573
+ let client = this.accounts.get(accountId);
574
+ if (!client) {
575
+ client = new OAuth2Client2(
576
+ this.credentials.clientId,
577
+ this.credentials.clientSecret,
578
+ this.credentials.redirectUri
579
+ );
580
+ this.setupTokenRefreshForAccount(client, accountId);
581
+ this.accounts.set(accountId, client);
582
+ }
583
+ client.setCredentials(tokens);
584
+ } catch (error) {
585
+ if (process.env.NODE_ENV !== "test") {
586
+ process.stderr.write(`Skipping invalid account "${accountId}": ${error}
587
+ `);
588
+ }
589
+ continue;
590
+ }
591
+ }
592
+ return this.accounts;
593
+ } catch (error) {
594
+ if (error && error.code === "ENOENT") {
595
+ return /* @__PURE__ */ new Map();
596
+ }
597
+ throw error;
598
+ }
599
+ }
600
+ /**
601
+ * Get OAuth2Client for a specific account
602
+ * @param accountId The account ID to retrieve
603
+ * @throws Error if account not found or invalid
604
+ */
605
+ getClient(accountId) {
606
+ const { validateAccountId: validateAccountId2 } = (init_paths(), __toCommonJS(paths_exports));
607
+ validateAccountId2(accountId);
608
+ const client = this.accounts.get(accountId);
609
+ if (!client) {
610
+ throw new Error(`Account "${accountId}" not found. Please authenticate this account first.`);
611
+ }
612
+ return client;
613
+ }
614
+ /**
615
+ * List all authenticated accounts with their email addresses, status, and calendars
616
+ * Uses cached data when available to avoid repeated API calls
617
+ */
618
+ async listAccounts() {
619
+ try {
620
+ const multiAccountTokens = await this.loadMultiAccountTokens();
621
+ const accountList = [];
622
+ let tokensUpdated = false;
623
+ const CALENDAR_CACHE_TTL = 5 * 60 * 1e3;
624
+ for (const [accountId, tokens] of Object.entries(multiAccountTokens)) {
625
+ if (!tokens || typeof tokens !== "object") {
626
+ continue;
627
+ }
628
+ let client = null;
629
+ if (tokens.access_token || tokens.refresh_token) {
630
+ try {
631
+ client = new OAuth2Client2(
632
+ this.credentials.clientId,
633
+ this.credentials.clientSecret,
634
+ this.credentials.redirectUri
635
+ );
636
+ client.setCredentials(tokens);
637
+ if (tokens.refresh_token && (!tokens.access_token || tokens.expiry_date && tokens.expiry_date < Date.now())) {
638
+ try {
639
+ const response = await client.refreshAccessToken();
640
+ client.setCredentials(response.credentials);
641
+ Object.assign(tokens, response.credentials);
642
+ tokensUpdated = true;
643
+ } catch {
644
+ }
645
+ }
646
+ } catch {
647
+ client = null;
648
+ }
649
+ }
650
+ let email = tokens.cached_email || "unknown";
651
+ if (!tokens.cached_email && client) {
652
+ try {
653
+ email = await this.getUserEmail(client);
654
+ if (email !== "unknown") {
655
+ tokens.cached_email = email;
656
+ tokensUpdated = true;
657
+ }
658
+ } catch {
659
+ }
660
+ }
661
+ let calendars = tokens.cached_calendars || [];
662
+ const cacheExpired = !tokens.calendars_cached_at || Date.now() - tokens.calendars_cached_at > CALENDAR_CACHE_TTL;
663
+ if (cacheExpired && client) {
664
+ try {
665
+ calendars = await this.fetchCalendarsForClient(client);
666
+ tokens.cached_calendars = calendars;
667
+ tokens.calendars_cached_at = Date.now();
668
+ tokensUpdated = true;
669
+ } catch {
670
+ }
671
+ }
672
+ let status = "active";
673
+ if (!tokens.refresh_token) {
674
+ if (!tokens.access_token || tokens.expiry_date && tokens.expiry_date < Date.now()) {
675
+ status = "expired";
676
+ }
677
+ }
678
+ accountList.push({ id: accountId, email, status, calendars });
679
+ }
680
+ if (tokensUpdated) {
681
+ await this.enqueueTokenWrite(async () => {
682
+ const latestTokens = await this.loadMultiAccountTokensRaw();
683
+ for (const accountId of Object.keys(multiAccountTokens)) {
684
+ const localUpdates = multiAccountTokens[accountId];
685
+ const latestAccount = latestTokens[accountId];
686
+ if (latestAccount && localUpdates) {
687
+ if (localUpdates.cached_email) {
688
+ latestAccount.cached_email = localUpdates.cached_email;
689
+ }
690
+ if (localUpdates.cached_calendars) {
691
+ latestAccount.cached_calendars = localUpdates.cached_calendars;
692
+ latestAccount.calendars_cached_at = localUpdates.calendars_cached_at;
693
+ }
694
+ }
695
+ }
696
+ await this.writeTokenFile(latestTokens);
697
+ });
698
+ }
699
+ return accountList;
700
+ } catch (error) {
701
+ return [];
702
+ }
703
+ }
704
+ /**
705
+ * Fetch calendars for a specific OAuth2Client
706
+ */
707
+ async fetchCalendarsForClient(client) {
708
+ const { google } = await import("googleapis");
709
+ const calendar = google.calendar({ version: "v3", auth: client });
710
+ const response = await calendar.calendarList.list();
711
+ const items = response.data.items || [];
712
+ const calendars = items.map((cal) => ({
713
+ id: cal.id || "",
714
+ summary: cal.summary || "",
715
+ summaryOverride: cal.summaryOverride || void 0,
716
+ accessRole: cal.accessRole || "reader",
717
+ primary: cal.primary || false,
718
+ backgroundColor: cal.backgroundColor || void 0
719
+ }));
720
+ calendars.sort((a, b) => {
721
+ if (a.primary && !b.primary) return -1;
722
+ if (!a.primary && b.primary) return 1;
723
+ return (a.summaryOverride || a.summary).localeCompare(b.summaryOverride || b.summary);
724
+ });
725
+ return calendars;
726
+ }
727
+ /**
728
+ * Get user email address from OAuth2Client
729
+ * First tries getTokenInfo, then falls back to primary calendar ID
730
+ */
731
+ async getUserEmail(client) {
732
+ try {
733
+ const tokenInfo = await client.getTokenInfo(client.credentials.access_token || "");
734
+ if (tokenInfo.email) {
735
+ return tokenInfo.email;
736
+ }
737
+ } catch {
738
+ }
739
+ try {
740
+ const { google } = await import("googleapis");
741
+ const calendar = google.calendar({ version: "v3", auth: client });
742
+ const response = await calendar.calendars.get({ calendarId: "primary" });
743
+ const primaryId = response.data.id;
744
+ if (primaryId && primaryId.includes("@")) {
745
+ return primaryId;
746
+ }
747
+ } catch {
748
+ }
749
+ return "unknown";
750
+ }
751
+ };
752
+
753
+ // src/auth/server.ts
754
+ import http from "http";
755
+ import { URL } from "url";
756
+ import open from "open";
757
+
758
+ // src/web/templates.ts
759
+ import fs3 from "fs/promises";
760
+ import path3 from "path";
761
+ import { fileURLToPath as fileURLToPath2 } from "url";
762
+ var __filename = fileURLToPath2(import.meta.url);
763
+ var __dirname = path3.dirname(__filename);
764
+ function escapeHtml(text) {
765
+ const htmlEscapes = {
766
+ "&": "&amp;",
767
+ "<": "&lt;",
768
+ ">": "&gt;",
769
+ '"': "&quot;",
770
+ "'": "&#39;"
771
+ };
772
+ return text.replace(/[&<>"']/g, (char) => htmlEscapes[char]);
773
+ }
774
+ async function loadWebFile(fileName) {
775
+ const locations = [
776
+ path3.join(__dirname, fileName),
777
+ // src/web/file.html (source)
778
+ path3.join(__dirname, "web", fileName)
779
+ // build/web/file.html (bundled)
780
+ ];
781
+ for (const filePath of locations) {
782
+ try {
783
+ await fs3.access(filePath);
784
+ return fs3.readFile(filePath, "utf-8");
785
+ } catch {
786
+ }
787
+ }
788
+ throw new Error(`Web file not found: ${fileName}. Tried: ${locations.join(", ")}`);
789
+ }
790
+ async function loadTemplate(templateName) {
791
+ return loadWebFile(templateName);
792
+ }
793
+ async function renderAuthSuccess(params) {
794
+ const template = await loadTemplate("auth-success.html");
795
+ const safeAccountId = escapeHtml(params.accountId);
796
+ let accountInfoSection;
797
+ if (params.email) {
798
+ accountInfoSection = `
799
+ <p class="account-email">${escapeHtml(params.email)}</p>
800
+ <p class="account-label">Saved as <code>${safeAccountId}</code></p>`;
801
+ } else {
802
+ accountInfoSection = `
803
+ <p class="account-email">Account connected</p>
804
+ <p class="account-label">Saved as <code>${safeAccountId}</code></p>`;
805
+ }
806
+ const closeButtonSection = params.showCloseButton ? `<button onclick="window.close()">Close Window</button>` : "";
807
+ const scriptSection = params.postMessageOrigin ? `<script>
808
+ if (window.opener) {
809
+ window.opener.postMessage({ type: 'auth-success', accountId: '${safeAccountId}' }, '${escapeHtml(params.postMessageOrigin)}');
810
+ }
811
+ setTimeout(() => window.close(), 3000);
812
+ </script>` : "";
813
+ return template.replace("{{accountInfo}}", accountInfoSection).replace("{{closeButton}}", closeButtonSection).replace("{{script}}", scriptSection);
814
+ }
815
+ async function renderAuthError(params) {
816
+ const template = await loadTemplate("auth-error.html");
817
+ const safeError = escapeHtml(params.errorMessage);
818
+ const closeButtonSection = params.showCloseButton ? `<button onclick="window.close()">Close Window</button>` : "";
819
+ return template.replace("{{errorMessage}}", safeError).replace("{{closeButton}}", closeButtonSection);
820
+ }
821
+ async function renderAuthLanding(params) {
822
+ const template = await loadTemplate("auth-landing.html");
823
+ const safeAccountId = escapeHtml(params.accountId);
824
+ const safeAuthUrl = escapeHtml(params.authUrl);
825
+ return template.replace(/\{\{accountId\}\}/g, safeAccountId).replace("{{authUrl}}", safeAuthUrl);
826
+ }
827
+
828
+ // src/auth/server.ts
829
+ var AuthServer = class {
830
+ baseOAuth2Client;
831
+ // Used by TokenManager for validation/refresh
832
+ flowOAuth2Client = null;
833
+ // Used specifically for the auth code flow
834
+ server = null;
835
+ tokenManager;
836
+ portRange;
837
+ activeConnections = /* @__PURE__ */ new Set();
838
+ // Track active socket connections
839
+ authCompletedSuccessfully = false;
840
+ // Flag for standalone script
841
+ mcpToolTimeout = null;
842
+ // Timeout for MCP tool auth flow
843
+ autoShutdownOnSuccess = false;
844
+ // Whether to auto-shutdown after successful auth
845
+ constructor(oauth2Client) {
846
+ this.baseOAuth2Client = oauth2Client;
847
+ this.tokenManager = new TokenManager(oauth2Client);
848
+ this.portRange = { start: 3500, end: 3505 };
849
+ }
850
+ /**
851
+ * Creates the flow-specific OAuth2Client with the correct redirect URI.
852
+ */
853
+ async createFlowOAuth2Client(port) {
854
+ const { client_id, client_secret } = await loadCredentials();
855
+ return new OAuth2Client3(
856
+ client_id,
857
+ client_secret,
858
+ `http://localhost:${port}/oauth2callback`
859
+ );
860
+ }
861
+ /**
862
+ * Generates an OAuth authorization URL with standard settings.
863
+ */
864
+ generateOAuthUrl(client) {
865
+ return client.generateAuthUrl({
866
+ access_type: "offline",
867
+ scope: ["https://www.googleapis.com/auth/calendar"],
868
+ prompt: "consent"
869
+ });
870
+ }
871
+ createServer() {
872
+ const server = http.createServer(async (req, res) => {
873
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
874
+ if (url.pathname === "/styles.css") {
875
+ const css = await loadWebFile("styles.css");
876
+ res.writeHead(200, { "Content-Type": "text/css; charset=utf-8" });
877
+ res.end(css);
878
+ } else if (url.pathname === "/") {
879
+ const clientForUrl = this.flowOAuth2Client || this.baseOAuth2Client;
880
+ const authUrl = this.generateOAuthUrl(clientForUrl);
881
+ const accountMode = getAccountMode2();
882
+ const landingHtml = await renderAuthLanding({
883
+ accountId: accountMode,
884
+ authUrl
885
+ });
886
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
887
+ res.end(landingHtml);
888
+ } else if (url.pathname === "/oauth2callback") {
889
+ const code = url.searchParams.get("code");
890
+ if (!code) {
891
+ const errorHtml = await renderAuthError({
892
+ errorMessage: "Authorization code missing"
893
+ });
894
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
895
+ res.end(errorHtml);
896
+ return;
897
+ }
898
+ if (!this.flowOAuth2Client) {
899
+ const errorHtml = await renderAuthError({
900
+ errorMessage: "Authentication flow not properly initiated."
901
+ });
902
+ res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
903
+ res.end(errorHtml);
904
+ return;
905
+ }
906
+ try {
907
+ const { tokens } = await this.flowOAuth2Client.getToken(code);
908
+ await this.tokenManager.saveTokens(tokens);
909
+ this.authCompletedSuccessfully = true;
910
+ const tokenPath = this.tokenManager.getTokenPath();
911
+ const accountMode = this.tokenManager.getAccountMode();
912
+ if (this.autoShutdownOnSuccess) {
913
+ if (this.mcpToolTimeout) {
914
+ clearTimeout(this.mcpToolTimeout);
915
+ this.mcpToolTimeout = null;
916
+ }
917
+ setTimeout(() => {
918
+ this.stop().catch(() => {
919
+ });
920
+ }, 2e3);
921
+ }
922
+ const successHtml = await renderAuthSuccess({
923
+ accountId: accountMode,
924
+ tokenPath
925
+ });
926
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
927
+ res.end(successHtml);
928
+ } catch (error) {
929
+ this.authCompletedSuccessfully = false;
930
+ const message = error instanceof Error ? error.message : "Unknown error";
931
+ process.stderr.write(`\u2717 Token save failed: ${message}
932
+ `);
933
+ const errorHtml = await renderAuthError({
934
+ errorMessage: message
935
+ });
936
+ res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
937
+ res.end(errorHtml);
938
+ }
939
+ } else {
940
+ res.writeHead(404, { "Content-Type": "text/plain" });
941
+ res.end("Not Found");
942
+ }
943
+ });
944
+ server.on("connection", (socket) => {
945
+ this.activeConnections.add(socket);
946
+ socket.on("close", () => {
947
+ this.activeConnections.delete(socket);
948
+ });
949
+ });
950
+ return server;
951
+ }
952
+ async start(openBrowser = true) {
953
+ return Promise.race([
954
+ this.startWithTimeout(openBrowser),
955
+ new Promise((_, reject) => {
956
+ setTimeout(() => reject(new Error("Auth server start timed out after 10 seconds")), 1e4);
957
+ })
958
+ ]).catch(() => false);
959
+ }
960
+ async startWithTimeout(openBrowser = true) {
961
+ if (await this.tokenManager.validateTokens()) {
962
+ this.authCompletedSuccessfully = true;
963
+ return true;
964
+ }
965
+ const port = await this.startServerOnAvailablePort();
966
+ if (port === null) {
967
+ process.stderr.write(`Could not start auth server on available port. Please check port availability (${this.portRange.start}-${this.portRange.end}) and try again.
968
+ `);
969
+ this.authCompletedSuccessfully = false;
970
+ return false;
971
+ }
972
+ try {
973
+ this.flowOAuth2Client = await this.createFlowOAuth2Client(port);
974
+ } catch (error) {
975
+ this.authCompletedSuccessfully = false;
976
+ await this.stop();
977
+ return false;
978
+ }
979
+ const authorizeUrl = this.generateOAuthUrl(this.flowOAuth2Client);
980
+ process.stderr.write(`
981
+ \u{1F517} Authentication URL: ${authorizeUrl}
982
+
983
+ `);
984
+ process.stderr.write(`Or visit: http://localhost:${port}
985
+
986
+ `);
987
+ if (openBrowser) {
988
+ try {
989
+ await open(authorizeUrl);
990
+ process.stderr.write(`Browser opened automatically. If it didn't open, use the URL above.
991
+ `);
992
+ } catch (error) {
993
+ process.stderr.write(`Could not open browser automatically. Please use the URL above.
994
+ `);
995
+ }
996
+ } else {
997
+ process.stderr.write(`Please visit the URL above to complete authentication.
998
+ `);
999
+ }
1000
+ return true;
1001
+ }
1002
+ async startServerOnAvailablePort() {
1003
+ for (let port = this.portRange.start; port <= this.portRange.end; port++) {
1004
+ try {
1005
+ await new Promise((resolve2, reject) => {
1006
+ const testServer = this.createServer();
1007
+ testServer.listen(port, () => {
1008
+ this.server = testServer;
1009
+ resolve2();
1010
+ });
1011
+ testServer.on("error", (err) => {
1012
+ if (err.code === "EADDRINUSE") {
1013
+ testServer.close(() => reject(err));
1014
+ } else {
1015
+ reject(err);
1016
+ }
1017
+ });
1018
+ });
1019
+ return port;
1020
+ } catch (error) {
1021
+ if (!(error instanceof Error && "code" in error && error.code === "EADDRINUSE")) {
1022
+ return null;
1023
+ }
1024
+ }
1025
+ }
1026
+ return null;
1027
+ }
1028
+ getRunningPort() {
1029
+ if (this.server) {
1030
+ const address = this.server.address();
1031
+ if (typeof address === "object" && address !== null) {
1032
+ return address.port;
1033
+ }
1034
+ }
1035
+ return null;
1036
+ }
1037
+ async stop() {
1038
+ if (this.mcpToolTimeout) {
1039
+ clearTimeout(this.mcpToolTimeout);
1040
+ this.mcpToolTimeout = null;
1041
+ }
1042
+ this.autoShutdownOnSuccess = false;
1043
+ return new Promise((resolve2, reject) => {
1044
+ if (this.server) {
1045
+ for (const connection of this.activeConnections) {
1046
+ connection.destroy();
1047
+ }
1048
+ this.activeConnections.clear();
1049
+ const timeout = setTimeout(() => {
1050
+ process.stderr.write("Server close timeout, forcing exit...\n");
1051
+ this.server = null;
1052
+ resolve2();
1053
+ }, 2e3);
1054
+ this.server.close((err) => {
1055
+ clearTimeout(timeout);
1056
+ if (err) {
1057
+ reject(err);
1058
+ } else {
1059
+ this.server = null;
1060
+ resolve2();
1061
+ }
1062
+ });
1063
+ } else {
1064
+ resolve2();
1065
+ }
1066
+ });
1067
+ }
1068
+ /**
1069
+ * Start the auth server for use by an MCP tool.
1070
+ *
1071
+ * Unlike the regular start() method:
1072
+ * - Does not open the browser automatically
1073
+ * - Returns the auth URL for the MCP tool to return to the user
1074
+ * - Auto-shutdowns after successful auth or timeout (5 minutes)
1075
+ * - Does not validate existing tokens (allows adding new accounts)
1076
+ *
1077
+ * @param accountId - The account ID to authenticate
1078
+ * @returns Result with auth URL on success, or error on failure
1079
+ */
1080
+ async startForMcpTool(accountId) {
1081
+ if (this.server) {
1082
+ await this.stop();
1083
+ }
1084
+ this.tokenManager.setAccountMode(accountId);
1085
+ const port = await this.startServerOnAvailablePort();
1086
+ if (port === null) {
1087
+ return {
1088
+ success: false,
1089
+ error: `Could not start auth server. Ports ${this.portRange.start}-${this.portRange.end} may be in use.`
1090
+ };
1091
+ }
1092
+ try {
1093
+ this.flowOAuth2Client = await this.createFlowOAuth2Client(port);
1094
+ } catch (error) {
1095
+ await this.stop();
1096
+ return {
1097
+ success: false,
1098
+ error: `Failed to load OAuth credentials: ${error instanceof Error ? error.message : "Unknown error"}`
1099
+ };
1100
+ }
1101
+ const authUrl = this.generateOAuthUrl(this.flowOAuth2Client);
1102
+ this.autoShutdownOnSuccess = true;
1103
+ this.authCompletedSuccessfully = false;
1104
+ this.mcpToolTimeout = setTimeout(async () => {
1105
+ if (!this.authCompletedSuccessfully) {
1106
+ process.stderr.write(`Auth timeout for account "${accountId}" - shutting down auth server
1107
+ `);
1108
+ await this.stop();
1109
+ }
1110
+ }, 5 * 60 * 1e3);
1111
+ return {
1112
+ success: true,
1113
+ authUrl,
1114
+ callbackUrl: `http://localhost:${port}/oauth2callback`
1115
+ };
1116
+ }
1117
+ };
1118
+
1119
+ // src/auth-server.ts
1120
+ var args = process.argv.slice(2);
1121
+ if (args.length > 0) {
1122
+ process.env.GOOGLE_ACCOUNT_MODE = args[0];
1123
+ }
1124
+ async function runAuthServer() {
1125
+ let authServer = null;
1126
+ try {
1127
+ const oauth2Client = await initializeOAuth2Client();
1128
+ authServer = new AuthServer(oauth2Client);
1129
+ const success = await authServer.start(true);
1130
+ if (!success && !authServer.authCompletedSuccessfully) {
1131
+ process.stderr.write("Authentication failed. Could not start server or validate existing tokens.\n");
1132
+ process.exit(1);
1133
+ } else if (authServer.authCompletedSuccessfully) {
1134
+ process.stderr.write("Authentication successful.\n");
1135
+ process.exit(0);
1136
+ }
1137
+ process.stderr.write("Authentication server started. Please complete the authentication in your browser...\n");
1138
+ process.stderr.write(`Waiting for OAuth callback on port ${authServer.getRunningPort()}...
1139
+ `);
1140
+ let lastDebugLog = 0;
1141
+ const pollInterval = setInterval(async () => {
1142
+ try {
1143
+ if (authServer?.authCompletedSuccessfully) {
1144
+ process.stderr.write("Authentication completed successfully detected. Stopping server...\n");
1145
+ clearInterval(pollInterval);
1146
+ await authServer.stop();
1147
+ process.stderr.write("Authentication successful. Server stopped.\n");
1148
+ process.exit(0);
1149
+ } else {
1150
+ const now = Date.now();
1151
+ if (now - lastDebugLog > 1e4) {
1152
+ process.stderr.write("Still waiting for authentication to complete...\n");
1153
+ lastDebugLog = now;
1154
+ }
1155
+ }
1156
+ } catch (error) {
1157
+ process.stderr.write(`Error in polling interval: ${error instanceof Error ? error.message : "Unknown error"}
1158
+ `);
1159
+ clearInterval(pollInterval);
1160
+ if (authServer) await authServer.stop();
1161
+ process.exit(1);
1162
+ }
1163
+ }, 5e3);
1164
+ process.on("SIGINT", async () => {
1165
+ clearInterval(pollInterval);
1166
+ if (authServer) {
1167
+ await authServer.stop();
1168
+ }
1169
+ process.exit(0);
1170
+ });
1171
+ } catch (error) {
1172
+ process.stderr.write(`Authentication error: ${error instanceof Error ? error.message : "Unknown error"}
1173
+ `);
1174
+ if (authServer) await authServer.stop();
1175
+ process.exit(1);
1176
+ }
1177
+ }
1178
+ if (import.meta.url.endsWith("auth-server.js")) {
1179
+ runAuthServer().catch((error) => {
1180
+ process.stderr.write(`Unhandled error: ${error instanceof Error ? error.message : "Unknown error"}
1181
+ `);
1182
+ process.exit(1);
1183
+ });
1184
+ }
1185
+ //# sourceMappingURL=auth-server.js.map