@hasna/connectors 1.1.5 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +1 -1
- package/bin/mcp.js +1 -1
- package/connectors/connect-gmail/src/api/attachments.ts +143 -0
- package/connectors/connect-gmail/src/api/bulk.ts +713 -0
- package/connectors/connect-gmail/src/api/client.ts +131 -0
- package/connectors/connect-gmail/src/api/drafts.ts +198 -0
- package/connectors/connect-gmail/src/api/export.ts +312 -0
- package/connectors/connect-gmail/src/api/filters.ts +127 -0
- package/connectors/connect-gmail/src/api/index.ts +63 -0
- package/connectors/connect-gmail/src/api/labels.ts +123 -0
- package/connectors/connect-gmail/src/api/messages.ts +527 -0
- package/connectors/connect-gmail/src/api/profile.ts +72 -0
- package/connectors/connect-gmail/src/api/threads.ts +85 -0
- package/connectors/connect-gmail/src/cli/index.ts +2389 -0
- package/connectors/connect-gmail/src/index.ts +30 -0
- package/connectors/connect-gmail/src/types/index.ts +202 -0
- package/connectors/connect-gmail/src/utils/auth.ts +256 -0
- package/connectors/connect-gmail/src/utils/config.ts +466 -0
- package/connectors/connect-gmail/src/utils/contacts.ts +147 -0
- package/connectors/connect-gmail/src/utils/markdown.ts +119 -0
- package/connectors/connect-gmail/src/utils/output.ts +119 -0
- package/connectors/connect-gmail/src/utils/settings.ts +87 -0
- package/connectors/connect-gmail/tsconfig.json +16 -0
- package/connectors/connect-telegram/src/api/bot.ts +223 -0
- package/connectors/connect-telegram/src/api/chats.ts +290 -0
- package/connectors/connect-telegram/src/api/client.ts +134 -0
- package/connectors/connect-telegram/src/api/index.ts +66 -0
- package/connectors/connect-telegram/src/api/inline.ts +63 -0
- package/connectors/connect-telegram/src/api/messages.ts +781 -0
- package/connectors/connect-telegram/src/api/updates.ts +97 -0
- package/connectors/connect-telegram/src/cli/index.ts +690 -0
- package/connectors/connect-telegram/src/index.ts +22 -0
- package/connectors/connect-telegram/src/types/index.ts +617 -0
- package/connectors/connect-telegram/src/utils/config.ts +197 -0
- package/connectors/connect-telegram/src/utils/output.ts +119 -0
- package/connectors/connect-telegram/tsconfig.json +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Gmail API Connector
|
|
2
|
+
// A TypeScript wrapper for Gmail with OAuth2 authentication
|
|
3
|
+
|
|
4
|
+
export { Gmail } from './api';
|
|
5
|
+
export * from './types';
|
|
6
|
+
|
|
7
|
+
// Re-export individual API classes for advanced usage
|
|
8
|
+
export {
|
|
9
|
+
GmailClient,
|
|
10
|
+
MessagesApi,
|
|
11
|
+
LabelsApi,
|
|
12
|
+
ThreadsApi,
|
|
13
|
+
ProfileApi,
|
|
14
|
+
} from './api';
|
|
15
|
+
|
|
16
|
+
// Export auth utilities
|
|
17
|
+
export {
|
|
18
|
+
getAuthUrl,
|
|
19
|
+
startCallbackServer,
|
|
20
|
+
refreshAccessToken,
|
|
21
|
+
getValidAccessToken,
|
|
22
|
+
} from './utils/auth';
|
|
23
|
+
|
|
24
|
+
// Export config utilities
|
|
25
|
+
export {
|
|
26
|
+
isAuthenticated,
|
|
27
|
+
loadTokens,
|
|
28
|
+
saveTokens,
|
|
29
|
+
clearConfig,
|
|
30
|
+
} from './utils/config';
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Gmail API Connector Types
|
|
2
|
+
|
|
3
|
+
// ============================================
|
|
4
|
+
// Configuration
|
|
5
|
+
// ============================================
|
|
6
|
+
|
|
7
|
+
export interface GmailConfig {
|
|
8
|
+
clientId: string;
|
|
9
|
+
clientSecret: string;
|
|
10
|
+
accessToken?: string;
|
|
11
|
+
refreshToken?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface OAuth2Tokens {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
refreshToken: string;
|
|
17
|
+
expiresAt: number;
|
|
18
|
+
tokenType: string;
|
|
19
|
+
scope: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CliConfig {
|
|
23
|
+
clientId?: string;
|
|
24
|
+
clientSecret?: string;
|
|
25
|
+
tokens?: OAuth2Tokens;
|
|
26
|
+
userEmail?: string;
|
|
27
|
+
userName?: string; // Display name for sending emails
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ============================================
|
|
31
|
+
// Common Types
|
|
32
|
+
// ============================================
|
|
33
|
+
|
|
34
|
+
export type OutputFormat = 'json' | 'table' | 'pretty';
|
|
35
|
+
|
|
36
|
+
// ============================================
|
|
37
|
+
// Gmail Message Types
|
|
38
|
+
// ============================================
|
|
39
|
+
|
|
40
|
+
export interface GmailMessage {
|
|
41
|
+
id: string;
|
|
42
|
+
threadId: string;
|
|
43
|
+
labelIds: string[];
|
|
44
|
+
snippet: string;
|
|
45
|
+
historyId: string;
|
|
46
|
+
internalDate: string;
|
|
47
|
+
payload?: MessagePart;
|
|
48
|
+
sizeEstimate: number;
|
|
49
|
+
raw?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface MessagePart {
|
|
53
|
+
partId: string;
|
|
54
|
+
mimeType: string;
|
|
55
|
+
filename: string;
|
|
56
|
+
headers: MessageHeader[];
|
|
57
|
+
body: MessagePartBody;
|
|
58
|
+
parts?: MessagePart[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface MessageHeader {
|
|
62
|
+
name: string;
|
|
63
|
+
value: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface MessagePartBody {
|
|
67
|
+
attachmentId?: string;
|
|
68
|
+
size: number;
|
|
69
|
+
data?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface MessageListResponse {
|
|
73
|
+
messages: Array<{ id: string; threadId: string }>;
|
|
74
|
+
nextPageToken?: string;
|
|
75
|
+
resultSizeEstimate: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============================================
|
|
79
|
+
// Gmail Label Types
|
|
80
|
+
// ============================================
|
|
81
|
+
|
|
82
|
+
export interface GmailLabel {
|
|
83
|
+
id: string;
|
|
84
|
+
name: string;
|
|
85
|
+
messageListVisibility?: 'show' | 'hide';
|
|
86
|
+
labelListVisibility?: 'labelShow' | 'labelShowIfUnread' | 'labelHide';
|
|
87
|
+
type: 'system' | 'user';
|
|
88
|
+
messagesTotal?: number;
|
|
89
|
+
messagesUnread?: number;
|
|
90
|
+
threadsTotal?: number;
|
|
91
|
+
threadsUnread?: number;
|
|
92
|
+
color?: LabelColor;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface LabelColor {
|
|
96
|
+
textColor: string;
|
|
97
|
+
backgroundColor: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================
|
|
101
|
+
// Gmail Thread Types
|
|
102
|
+
// ============================================
|
|
103
|
+
|
|
104
|
+
export interface GmailThread {
|
|
105
|
+
id: string;
|
|
106
|
+
historyId: string;
|
|
107
|
+
messages: GmailMessage[];
|
|
108
|
+
snippet?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface ThreadListResponse {
|
|
112
|
+
threads: Array<{ id: string; snippet: string; historyId: string }>;
|
|
113
|
+
nextPageToken?: string;
|
|
114
|
+
resultSizeEstimate: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============================================
|
|
118
|
+
// Gmail Draft Types
|
|
119
|
+
// ============================================
|
|
120
|
+
|
|
121
|
+
export interface GmailDraft {
|
|
122
|
+
id: string;
|
|
123
|
+
message: GmailMessage;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================
|
|
127
|
+
// Send Message Types
|
|
128
|
+
// ============================================
|
|
129
|
+
|
|
130
|
+
export interface SendMessageOptions {
|
|
131
|
+
to: string | string[];
|
|
132
|
+
cc?: string | string[];
|
|
133
|
+
bcc?: string | string[];
|
|
134
|
+
subject: string;
|
|
135
|
+
body: string;
|
|
136
|
+
isHtml?: boolean;
|
|
137
|
+
threadId?: string;
|
|
138
|
+
attachments?: Attachment[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface Attachment {
|
|
142
|
+
filename: string;
|
|
143
|
+
mimeType: string;
|
|
144
|
+
data: string; // base64 encoded
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============================================
|
|
148
|
+
// Search/Query Types
|
|
149
|
+
// ============================================
|
|
150
|
+
|
|
151
|
+
export interface ListMessagesOptions {
|
|
152
|
+
maxResults?: number;
|
|
153
|
+
pageToken?: string;
|
|
154
|
+
q?: string; // Gmail search query
|
|
155
|
+
labelIds?: string[];
|
|
156
|
+
includeSpamTrash?: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface ListThreadsOptions {
|
|
160
|
+
maxResults?: number;
|
|
161
|
+
pageToken?: string;
|
|
162
|
+
q?: string;
|
|
163
|
+
labelIds?: string[];
|
|
164
|
+
includeSpamTrash?: boolean;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============================================
|
|
168
|
+
// Profile Types
|
|
169
|
+
// ============================================
|
|
170
|
+
|
|
171
|
+
export interface GmailProfile {
|
|
172
|
+
emailAddress: string;
|
|
173
|
+
messagesTotal: number;
|
|
174
|
+
threadsTotal: number;
|
|
175
|
+
historyId: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ============================================
|
|
179
|
+
// API Error Types
|
|
180
|
+
// ============================================
|
|
181
|
+
|
|
182
|
+
export interface GmailError {
|
|
183
|
+
code: number;
|
|
184
|
+
message: string;
|
|
185
|
+
errors?: Array<{
|
|
186
|
+
domain: string;
|
|
187
|
+
reason: string;
|
|
188
|
+
message: string;
|
|
189
|
+
}>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export class GmailApiError extends Error {
|
|
193
|
+
public readonly statusCode: number;
|
|
194
|
+
public readonly errors?: GmailError['errors'];
|
|
195
|
+
|
|
196
|
+
constructor(message: string, statusCode: number, errors?: GmailError['errors']) {
|
|
197
|
+
super(message);
|
|
198
|
+
this.name = 'GmailApiError';
|
|
199
|
+
this.statusCode = statusCode;
|
|
200
|
+
this.errors = errors;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import type { OAuth2Tokens } from '../types';
|
|
3
|
+
import { saveTokens, getClientId, getClientSecret, loadTokens } from './config';
|
|
4
|
+
|
|
5
|
+
// Google OAuth2 endpoints
|
|
6
|
+
const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
7
|
+
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
8
|
+
|
|
9
|
+
// Gmail API scopes
|
|
10
|
+
const GMAIL_SCOPES = [
|
|
11
|
+
'https://www.googleapis.com/auth/gmail.readonly',
|
|
12
|
+
'https://www.googleapis.com/auth/gmail.send',
|
|
13
|
+
'https://www.googleapis.com/auth/gmail.compose',
|
|
14
|
+
'https://www.googleapis.com/auth/gmail.modify',
|
|
15
|
+
'https://www.googleapis.com/auth/gmail.labels',
|
|
16
|
+
'https://www.googleapis.com/auth/gmail.settings.basic', // Filter management
|
|
17
|
+
'https://mail.google.com/', // Full access
|
|
18
|
+
].join(' ');
|
|
19
|
+
|
|
20
|
+
const REDIRECT_PORT = 8089;
|
|
21
|
+
// For Desktop/Installed apps, use loopback IP without path
|
|
22
|
+
const REDIRECT_URI = `http://127.0.0.1:${REDIRECT_PORT}`;
|
|
23
|
+
|
|
24
|
+
export interface AuthResult {
|
|
25
|
+
success: boolean;
|
|
26
|
+
tokens?: OAuth2Tokens;
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generate the OAuth2 authorization URL
|
|
32
|
+
*/
|
|
33
|
+
export function getAuthUrl(): string {
|
|
34
|
+
const clientId = getClientId();
|
|
35
|
+
if (!clientId) {
|
|
36
|
+
throw new Error('Client ID not configured. Run "connect-gmail config set-credentials" first.');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const params = new URLSearchParams({
|
|
40
|
+
client_id: clientId,
|
|
41
|
+
redirect_uri: REDIRECT_URI,
|
|
42
|
+
response_type: 'code',
|
|
43
|
+
scope: GMAIL_SCOPES,
|
|
44
|
+
access_type: 'offline',
|
|
45
|
+
prompt: 'consent', // Force consent to get refresh token
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Exchange authorization code for tokens
|
|
53
|
+
*/
|
|
54
|
+
export async function exchangeCodeForTokens(code: string): Promise<OAuth2Tokens> {
|
|
55
|
+
const clientId = getClientId();
|
|
56
|
+
const clientSecret = getClientSecret();
|
|
57
|
+
|
|
58
|
+
if (!clientId || !clientSecret) {
|
|
59
|
+
throw new Error('OAuth credentials not configured');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
66
|
+
},
|
|
67
|
+
body: new URLSearchParams({
|
|
68
|
+
code,
|
|
69
|
+
client_id: clientId,
|
|
70
|
+
client_secret: clientSecret,
|
|
71
|
+
redirect_uri: REDIRECT_URI,
|
|
72
|
+
grant_type: 'authorization_code',
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const errorText = await response.text();
|
|
78
|
+
let errorMessage = `Token exchange failed: ${response.status} ${response.statusText}`;
|
|
79
|
+
try {
|
|
80
|
+
const error = JSON.parse(errorText);
|
|
81
|
+
errorMessage = `Token exchange failed: ${error.error_description || error.error || response.statusText}`;
|
|
82
|
+
console.error('Token exchange error details:', error);
|
|
83
|
+
} catch {
|
|
84
|
+
errorMessage = `Token exchange failed: ${errorText || response.statusText}`;
|
|
85
|
+
}
|
|
86
|
+
throw new Error(errorMessage);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const data = await response.json();
|
|
90
|
+
|
|
91
|
+
const tokens: OAuth2Tokens = {
|
|
92
|
+
accessToken: data.access_token,
|
|
93
|
+
refreshToken: data.refresh_token,
|
|
94
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
95
|
+
tokenType: data.token_type,
|
|
96
|
+
scope: data.scope,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Don't save here - let CLI save after determining correct profile from email
|
|
100
|
+
return tokens;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Refresh the access token using the refresh token
|
|
105
|
+
*/
|
|
106
|
+
export async function refreshAccessToken(): Promise<OAuth2Tokens> {
|
|
107
|
+
const clientId = getClientId();
|
|
108
|
+
const clientSecret = getClientSecret();
|
|
109
|
+
const currentTokens = loadTokens();
|
|
110
|
+
|
|
111
|
+
if (!clientId || !clientSecret) {
|
|
112
|
+
throw new Error('OAuth credentials not configured');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!currentTokens?.refreshToken) {
|
|
116
|
+
throw new Error('No refresh token available. Please login again.');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
123
|
+
},
|
|
124
|
+
body: new URLSearchParams({
|
|
125
|
+
client_id: clientId,
|
|
126
|
+
client_secret: clientSecret,
|
|
127
|
+
refresh_token: currentTokens.refreshToken,
|
|
128
|
+
grant_type: 'refresh_token',
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
let errorMessage = `Token refresh failed: ${response.status} ${response.statusText}`;
|
|
134
|
+
try {
|
|
135
|
+
const errorBody = await response.json();
|
|
136
|
+
const detail = errorBody.error_description || errorBody.error;
|
|
137
|
+
if (detail) {
|
|
138
|
+
errorMessage = `Token refresh failed: ${detail}. Please run "connect-gmail auth login" again.`;
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// Response was not JSON; keep the status-based message
|
|
142
|
+
}
|
|
143
|
+
throw new Error(errorMessage);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const data = await response.json();
|
|
147
|
+
|
|
148
|
+
const tokens: OAuth2Tokens = {
|
|
149
|
+
accessToken: data.access_token,
|
|
150
|
+
refreshToken: currentTokens.refreshToken, // Keep the original refresh token
|
|
151
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
152
|
+
tokenType: data.token_type,
|
|
153
|
+
scope: data.scope || currentTokens.scope,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
saveTokens(tokens);
|
|
157
|
+
return tokens;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Start a local HTTP server to receive the OAuth callback
|
|
162
|
+
*/
|
|
163
|
+
export function startCallbackServer(): Promise<AuthResult> {
|
|
164
|
+
return new Promise((resolve) => {
|
|
165
|
+
const server = createServer(async (req, res) => {
|
|
166
|
+
const url = new URL(req.url || '', `http://127.0.0.1:${REDIRECT_PORT}`);
|
|
167
|
+
|
|
168
|
+
// Accept both root path and /callback for flexibility
|
|
169
|
+
if (url.pathname === '/' || url.pathname === '/callback') {
|
|
170
|
+
const code = url.searchParams.get('code');
|
|
171
|
+
const error = url.searchParams.get('error');
|
|
172
|
+
|
|
173
|
+
if (error) {
|
|
174
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
175
|
+
res.end(`
|
|
176
|
+
<html>
|
|
177
|
+
<body style="font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
|
|
178
|
+
<div style="text-align: center;">
|
|
179
|
+
<h1 style="color: #dc3545;">Authentication Failed</h1>
|
|
180
|
+
<p>Error: ${error}</p>
|
|
181
|
+
<p>You can close this window.</p>
|
|
182
|
+
</div>
|
|
183
|
+
</body>
|
|
184
|
+
</html>
|
|
185
|
+
`);
|
|
186
|
+
server.close();
|
|
187
|
+
resolve({ success: false, error });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (code) {
|
|
192
|
+
try {
|
|
193
|
+
const tokens = await exchangeCodeForTokens(code);
|
|
194
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
195
|
+
res.end(`
|
|
196
|
+
<html>
|
|
197
|
+
<body style="font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
|
|
198
|
+
<div style="text-align: center;">
|
|
199
|
+
<h1 style="color: #28a745;">Authentication Successful!</h1>
|
|
200
|
+
<p>You can close this window and return to the terminal.</p>
|
|
201
|
+
</div>
|
|
202
|
+
</body>
|
|
203
|
+
</html>
|
|
204
|
+
`);
|
|
205
|
+
server.close();
|
|
206
|
+
resolve({ success: true, tokens });
|
|
207
|
+
} catch (err) {
|
|
208
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
209
|
+
res.end(`
|
|
210
|
+
<html>
|
|
211
|
+
<body style="font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
|
|
212
|
+
<div style="text-align: center;">
|
|
213
|
+
<h1 style="color: #dc3545;">Authentication Failed</h1>
|
|
214
|
+
<p>Error: ${String(err)}</p>
|
|
215
|
+
<p>You can close this window.</p>
|
|
216
|
+
</div>
|
|
217
|
+
</body>
|
|
218
|
+
</html>
|
|
219
|
+
`);
|
|
220
|
+
server.close();
|
|
221
|
+
resolve({ success: false, error: String(err) });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
server.listen(REDIRECT_PORT, () => {
|
|
228
|
+
// Server is ready
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Timeout after 5 minutes
|
|
232
|
+
setTimeout(() => {
|
|
233
|
+
server.close();
|
|
234
|
+
resolve({ success: false, error: 'Authentication timed out' });
|
|
235
|
+
}, 5 * 60 * 1000);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get a valid access token, refreshing if necessary
|
|
241
|
+
*/
|
|
242
|
+
export async function getValidAccessToken(): Promise<string> {
|
|
243
|
+
const tokens = loadTokens();
|
|
244
|
+
|
|
245
|
+
if (!tokens) {
|
|
246
|
+
throw new Error('Not authenticated. Run "connect-gmail auth login" first.');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Check if token is expired or will expire in next 5 minutes
|
|
250
|
+
if (Date.now() >= tokens.expiresAt - 5 * 60 * 1000) {
|
|
251
|
+
const newTokens = await refreshAccessToken();
|
|
252
|
+
return newTokens.accessToken;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return tokens.accessToken;
|
|
256
|
+
}
|