@hasna/connectors 1.3.13 → 1.3.14
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 +62 -17
- package/connectors/connect-gmail/bin/index.js +7027 -0
- package/connectors/connect-gmail/dist/index.js +2174 -0
- package/connectors/connect-gmail/package.json +1 -1
- package/connectors/connect-gmail/src/api/client.ts +13 -3
- package/connectors/connect-gmail/src/api/index.ts +98 -3
- package/connectors/connect-gmail/src/index.ts +4 -0
- package/connectors/connect-gmail/src/utils/auth.ts +31 -18
- package/connectors/connect-stripe/package.json +1 -1
- package/connectors/connect-stripe/src/api/index.ts +9 -2
- package/connectors/connect-stripe/src/index.ts +1 -1
- package/package.json +1 -1
|
@@ -12,11 +12,19 @@ export interface RequestOptions {
|
|
|
12
12
|
format?: OutputFormat;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export interface GmailClientOptions {
|
|
16
|
+
/** Custom token provider — if set, used instead of file-based auth */
|
|
17
|
+
tokenProvider?: () => Promise<string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
export class GmailClient {
|
|
16
21
|
private accessToken?: string;
|
|
17
22
|
private userId: string = 'me'; // Default to authenticated user
|
|
23
|
+
private readonly tokenProvider?: () => Promise<string>;
|
|
18
24
|
|
|
19
|
-
constructor() {
|
|
25
|
+
constructor(options?: GmailClientOptions) {
|
|
26
|
+
this.tokenProvider = options?.tokenProvider;
|
|
27
|
+
}
|
|
20
28
|
|
|
21
29
|
setUserId(userId: string): void {
|
|
22
30
|
this.userId = userId;
|
|
@@ -43,8 +51,10 @@ export class GmailClient {
|
|
|
43
51
|
async request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
44
52
|
const { method = 'GET', params, body, headers = {} } = options;
|
|
45
53
|
|
|
46
|
-
// Get fresh access token
|
|
47
|
-
const accessToken =
|
|
54
|
+
// Get fresh access token — use injected tokenProvider if available, otherwise file-based auth
|
|
55
|
+
const accessToken = this.tokenProvider
|
|
56
|
+
? await this.tokenProvider()
|
|
57
|
+
: await getValidAccessToken();
|
|
48
58
|
|
|
49
59
|
const url = this.buildUrl(path, params);
|
|
50
60
|
|
|
@@ -8,6 +8,17 @@ import { FiltersApi } from './filters';
|
|
|
8
8
|
import { AttachmentsApi } from './attachments';
|
|
9
9
|
import { ExportApi } from './export';
|
|
10
10
|
import { BulkApi } from './bulk';
|
|
11
|
+
import { refreshTokens } from '../utils/auth';
|
|
12
|
+
|
|
13
|
+
/** Tokens passed to Gmail.createWithTokens() */
|
|
14
|
+
export interface GmailTokens {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
refreshToken: string;
|
|
17
|
+
clientId: string;
|
|
18
|
+
clientSecret: string;
|
|
19
|
+
/** Unix timestamp (ms) when accessToken expires. If omitted, token is always refreshed. */
|
|
20
|
+
expiresAt?: number;
|
|
21
|
+
}
|
|
11
22
|
|
|
12
23
|
export class Gmail {
|
|
13
24
|
private readonly client: GmailClient;
|
|
@@ -23,8 +34,8 @@ export class Gmail {
|
|
|
23
34
|
public readonly export: ExportApi;
|
|
24
35
|
public readonly bulk: BulkApi;
|
|
25
36
|
|
|
26
|
-
constructor() {
|
|
27
|
-
this.client = new GmailClient();
|
|
37
|
+
constructor(client?: GmailClient) {
|
|
38
|
+
this.client = client ?? new GmailClient();
|
|
28
39
|
this.messages = new MessagesApi(this.client);
|
|
29
40
|
this.labels = new LabelsApi(this.client);
|
|
30
41
|
this.threads = new ThreadsApi(this.client);
|
|
@@ -37,12 +48,96 @@ export class Gmail {
|
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
/**
|
|
40
|
-
* Create a Gmail client
|
|
51
|
+
* Create a Gmail client — tokens are loaded automatically from config
|
|
41
52
|
*/
|
|
42
53
|
static create(): Gmail {
|
|
43
54
|
return new Gmail();
|
|
44
55
|
}
|
|
45
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Create a Gmail client from environment variables.
|
|
59
|
+
*
|
|
60
|
+
* Supports two modes:
|
|
61
|
+
* - Full OAuth: GMAIL_REFRESH_TOKEN + GMAIL_CLIENT_ID + GMAIL_CLIENT_SECRET
|
|
62
|
+
* (optionally GMAIL_ACCESS_TOKEN + GMAIL_TOKEN_EXPIRES_AT to skip initial refresh)
|
|
63
|
+
* - Static token: GMAIL_ACCESS_TOKEN only (no auto-refresh)
|
|
64
|
+
*/
|
|
65
|
+
static fromEnv(): Gmail {
|
|
66
|
+
const accessToken = process.env.GMAIL_ACCESS_TOKEN;
|
|
67
|
+
const refreshToken = process.env.GMAIL_REFRESH_TOKEN;
|
|
68
|
+
const clientId = process.env.GMAIL_CLIENT_ID;
|
|
69
|
+
const clientSecret = process.env.GMAIL_CLIENT_SECRET;
|
|
70
|
+
const expiresAt = process.env.GMAIL_TOKEN_EXPIRES_AT
|
|
71
|
+
? parseInt(process.env.GMAIL_TOKEN_EXPIRES_AT, 10)
|
|
72
|
+
: undefined;
|
|
73
|
+
|
|
74
|
+
if (refreshToken && clientId && clientSecret) {
|
|
75
|
+
return Gmail.createWithTokens({
|
|
76
|
+
accessToken: accessToken ?? '',
|
|
77
|
+
refreshToken,
|
|
78
|
+
clientId,
|
|
79
|
+
clientSecret,
|
|
80
|
+
// Force immediate refresh if no access token was provided
|
|
81
|
+
expiresAt: accessToken ? expiresAt : 0,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (accessToken) {
|
|
86
|
+
// Static token only — no auto-refresh
|
|
87
|
+
const client = new GmailClient({ tokenProvider: async () => accessToken });
|
|
88
|
+
return new Gmail(client);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new Error(
|
|
92
|
+
'Missing Gmail env vars. Provide GMAIL_ACCESS_TOKEN, ' +
|
|
93
|
+
'or GMAIL_REFRESH_TOKEN + GMAIL_CLIENT_ID + GMAIL_CLIENT_SECRET',
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create a Gmail client using explicit tokens instead of file-based auth.
|
|
99
|
+
* Automatically refreshes the access token when expired and notifies via onRefresh.
|
|
100
|
+
*
|
|
101
|
+
* @param tokens - Initial token set (accessToken, refreshToken, clientId, clientSecret, expiresAt?)
|
|
102
|
+
* @param onRefresh - Called whenever tokens are refreshed so callers can persist the new tokens
|
|
103
|
+
*/
|
|
104
|
+
static createWithTokens(
|
|
105
|
+
tokens: GmailTokens,
|
|
106
|
+
onRefresh?: (newTokens: GmailTokens) => void,
|
|
107
|
+
): Gmail {
|
|
108
|
+
// Mutable state for the closure — updated on each refresh
|
|
109
|
+
let current = { ...tokens };
|
|
110
|
+
|
|
111
|
+
const tokenProvider = async (): Promise<string> => {
|
|
112
|
+
const isExpired =
|
|
113
|
+
current.expiresAt === undefined ||
|
|
114
|
+
Date.now() >= current.expiresAt - 5 * 60 * 1000;
|
|
115
|
+
|
|
116
|
+
if (isExpired) {
|
|
117
|
+
const refreshed = await refreshTokens(
|
|
118
|
+
current.clientId,
|
|
119
|
+
current.clientSecret,
|
|
120
|
+
current.refreshToken,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
current = {
|
|
124
|
+
accessToken: refreshed.accessToken,
|
|
125
|
+
refreshToken: refreshed.refreshToken,
|
|
126
|
+
clientId: current.clientId,
|
|
127
|
+
clientSecret: current.clientSecret,
|
|
128
|
+
expiresAt: refreshed.expiresAt,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
onRefresh?.(current);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return current.accessToken;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const client = new GmailClient({ tokenProvider });
|
|
138
|
+
return new Gmail(client);
|
|
139
|
+
}
|
|
140
|
+
|
|
46
141
|
/**
|
|
47
142
|
* Get the underlying client for direct API access
|
|
48
143
|
*/
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// A TypeScript wrapper for Gmail with OAuth2 authentication
|
|
3
3
|
|
|
4
4
|
export { Gmail } from './api';
|
|
5
|
+
export type { GmailTokens } from './api';
|
|
5
6
|
export * from './types';
|
|
6
7
|
|
|
7
8
|
// Re-export individual API classes for advanced usage
|
|
@@ -18,9 +19,12 @@ export {
|
|
|
18
19
|
getAuthUrl,
|
|
19
20
|
startCallbackServer,
|
|
20
21
|
refreshAccessToken,
|
|
22
|
+
refreshTokens,
|
|
21
23
|
getValidAccessToken,
|
|
22
24
|
} from './utils/auth';
|
|
23
25
|
|
|
26
|
+
export type { GmailClientOptions } from './api/client';
|
|
27
|
+
|
|
24
28
|
// Export config utilities
|
|
25
29
|
export {
|
|
26
30
|
isAuthenticated,
|
|
@@ -101,21 +101,15 @@ export async function exchangeCodeForTokens(code: string): Promise<OAuth2Tokens>
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
|
-
*
|
|
104
|
+
* Pure function to refresh an OAuth2 access token using provided credentials.
|
|
105
|
+
* Does NOT touch the file system — no saveTokens, no loadTokens.
|
|
105
106
|
*/
|
|
106
|
-
export async function
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
107
|
+
export async function refreshTokens(
|
|
108
|
+
clientId: string,
|
|
109
|
+
clientSecret: string,
|
|
110
|
+
refreshToken: string,
|
|
111
|
+
currentScope?: string,
|
|
112
|
+
): Promise<OAuth2Tokens> {
|
|
119
113
|
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
120
114
|
method: 'POST',
|
|
121
115
|
headers: {
|
|
@@ -124,7 +118,7 @@ export async function refreshAccessToken(): Promise<OAuth2Tokens> {
|
|
|
124
118
|
body: new URLSearchParams({
|
|
125
119
|
client_id: clientId,
|
|
126
120
|
client_secret: clientSecret,
|
|
127
|
-
refresh_token:
|
|
121
|
+
refresh_token: refreshToken,
|
|
128
122
|
grant_type: 'refresh_token',
|
|
129
123
|
}),
|
|
130
124
|
});
|
|
@@ -145,14 +139,33 @@ export async function refreshAccessToken(): Promise<OAuth2Tokens> {
|
|
|
145
139
|
|
|
146
140
|
const data = await response.json();
|
|
147
141
|
|
|
148
|
-
|
|
142
|
+
return {
|
|
149
143
|
accessToken: data.access_token,
|
|
150
|
-
refreshToken
|
|
144
|
+
refreshToken, // Keep the original refresh token
|
|
151
145
|
expiresAt: Date.now() + data.expires_in * 1000,
|
|
152
146
|
tokenType: data.token_type,
|
|
153
|
-
scope: data.scope ||
|
|
147
|
+
scope: data.scope || currentScope || '',
|
|
154
148
|
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Refresh the access token using the refresh token (file-based auth).
|
|
153
|
+
* Thin wrapper around refreshTokens() that reads/writes config files.
|
|
154
|
+
*/
|
|
155
|
+
export async function refreshAccessToken(): Promise<OAuth2Tokens> {
|
|
156
|
+
const clientId = getClientId();
|
|
157
|
+
const clientSecret = getClientSecret();
|
|
158
|
+
const currentTokens = loadTokens();
|
|
159
|
+
|
|
160
|
+
if (!clientId || !clientSecret) {
|
|
161
|
+
throw new Error('OAuth credentials not configured');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!currentTokens?.refreshToken) {
|
|
165
|
+
throw new Error('No refresh token available. Please login again.');
|
|
166
|
+
}
|
|
155
167
|
|
|
168
|
+
const tokens = await refreshTokens(clientId, clientSecret, currentTokens.refreshToken, currentTokens.scope);
|
|
156
169
|
saveTokens(tokens);
|
|
157
170
|
return tokens;
|
|
158
171
|
}
|
|
@@ -68,8 +68,15 @@ export class Connector {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Create a client from
|
|
72
|
-
|
|
71
|
+
* Create a client from an API key directly.
|
|
72
|
+
*/
|
|
73
|
+
static fromApiKey(apiKey: string, options?: Omit<ConnectorConfig, 'apiKey'>): Connector {
|
|
74
|
+
return new Connector({ apiKey, ...options });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a client from environment variables.
|
|
79
|
+
* Looks for STRIPE_API_KEY and optionally STRIPE_API_SECRET.
|
|
73
80
|
*/
|
|
74
81
|
static fromEnv(): Connector {
|
|
75
82
|
const apiKey = process.env.STRIPE_API_KEY;
|