@hasna/connectors 0.3.2 → 0.3.4

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 CHANGED
@@ -6701,7 +6701,7 @@ var PRESETS = {
6701
6701
  commerce: { description: "Commerce and finance", connectors: ["stripe", "shopify", "revolut", "mercury", "pandadoc"] }
6702
6702
  };
6703
6703
  var program2 = new Command;
6704
- program2.name("connectors").description("Install API connectors for your project").version("0.3.2");
6704
+ program2.name("connectors").description("Install API connectors for your project").version("0.3.4");
6705
6705
  program2.command("interactive", { isDefault: true }).alias("i").description("Interactive connector browser").action(() => {
6706
6706
  if (!isTTY) {
6707
6707
  console.log(`Non-interactive environment detected. Use a subcommand:
package/bin/mcp.js CHANGED
@@ -20309,7 +20309,7 @@ async function getConnectorCommandHelp(name, command) {
20309
20309
  loadConnectorVersions();
20310
20310
  var server = new McpServer({
20311
20311
  name: "connectors",
20312
- version: "0.3.2"
20312
+ version: "0.3.4"
20313
20313
  });
20314
20314
  server.registerTool("search_connectors", {
20315
20315
  title: "Search Connectors",
@@ -1,5 +1,6 @@
1
1
  import type { GoogleConfig, OutputFormat } from '../types';
2
2
  import { GoogleApiError } from '../types';
3
+ import { getValidAccessToken } from '../utils/config';
3
4
 
4
5
  // Google API base URLs
5
6
  const BASE_URLS = {
@@ -21,14 +22,16 @@ export interface RequestOptions {
21
22
  }
22
23
 
23
24
  export class GoogleClient {
24
- private readonly accessToken: string;
25
+ private accessToken: string;
25
26
  private readonly baseUrls: Record<GoogleService, string>;
27
+ private readonly useAutoRefresh: boolean;
26
28
 
27
29
  constructor(config: GoogleConfig) {
28
30
  if (!config.accessToken) {
29
31
  throw new Error('Access token is required');
30
32
  }
31
33
  this.accessToken = config.accessToken;
34
+ this.useAutoRefresh = config.autoRefresh !== false; // defaults to true
32
35
  this.baseUrls = {
33
36
  gmail: config.baseUrls?.gmail || BASE_URLS.gmail,
34
37
  drive: config.baseUrls?.drive || BASE_URLS.drive,
@@ -58,6 +61,15 @@ export class GoogleClient {
58
61
  async request<T>(service: GoogleService, path: string, options: RequestOptions = {}): Promise<T> {
59
62
  const { method = 'GET', params, body, headers = {} } = options;
60
63
 
64
+ // Auto-refresh token if enabled
65
+ if (this.useAutoRefresh) {
66
+ try {
67
+ this.accessToken = await getValidAccessToken();
68
+ } catch {
69
+ // Fall through to use current token if refresh fails
70
+ }
71
+ }
72
+
61
73
  const url = this.buildUrl(service, path, params);
62
74
 
63
75
  const requestHeaders: Record<string, string> = {
@@ -6,6 +6,7 @@
6
6
 
7
7
  export interface GoogleConfig {
8
8
  accessToken: string;
9
+ autoRefresh?: boolean; // Enable auto-refresh of access token (default: true)
9
10
  baseUrls?: {
10
11
  gmail?: string;
11
12
  drive?: string;
@@ -11,6 +11,7 @@ export interface ProfileConfig {
11
11
  refreshToken?: string;
12
12
  clientId?: string;
13
13
  clientSecret?: string;
14
+ expiresAt?: number;
14
15
  }
15
16
 
16
17
  // Store for --profile flag override (set by CLI before commands run)
@@ -219,6 +220,96 @@ export function setRefreshToken(refreshToken: string): void {
219
220
  saveProfile(config);
220
221
  }
221
222
 
223
+ // ============================================
224
+ // Token Refresh
225
+ // ============================================
226
+
227
+ const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
228
+
229
+ export function getClientId(): string | undefined {
230
+ return process.env.GOOGLE_CLIENT_ID || loadProfile().clientId;
231
+ }
232
+
233
+ export function getClientSecret(): string | undefined {
234
+ return process.env.GOOGLE_CLIENT_SECRET || loadProfile().clientSecret;
235
+ }
236
+
237
+ /**
238
+ * Refresh the access token using the refresh token and client credentials
239
+ */
240
+ export async function refreshAccessToken(): Promise<ProfileConfig> {
241
+ const profile = loadProfile();
242
+ const clientId = process.env.GOOGLE_CLIENT_ID || profile.clientId;
243
+ const clientSecret = process.env.GOOGLE_CLIENT_SECRET || profile.clientSecret;
244
+ const refreshToken = process.env.GOOGLE_REFRESH_TOKEN || profile.refreshToken;
245
+
246
+ if (!clientId || !clientSecret) {
247
+ throw new Error('OAuth client credentials not configured. Set clientId/clientSecret in profile or GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET env vars.');
248
+ }
249
+
250
+ if (!refreshToken) {
251
+ throw new Error('No refresh token available. Please re-authenticate.');
252
+ }
253
+
254
+ const response = await fetch(GOOGLE_TOKEN_URL, {
255
+ method: 'POST',
256
+ headers: {
257
+ 'Content-Type': 'application/x-www-form-urlencoded',
258
+ },
259
+ body: new URLSearchParams({
260
+ client_id: clientId,
261
+ client_secret: clientSecret,
262
+ refresh_token: refreshToken,
263
+ grant_type: 'refresh_token',
264
+ }),
265
+ });
266
+
267
+ if (!response.ok) {
268
+ const error = await response.json() as { error_description?: string; error?: string };
269
+ throw new Error(`Token refresh failed: ${error.error_description || error.error}`);
270
+ }
271
+
272
+ const data = await response.json() as { access_token: string; expires_in: number };
273
+
274
+ const updatedProfile: ProfileConfig = {
275
+ ...profile,
276
+ accessToken: data.access_token,
277
+ refreshToken: refreshToken, // Keep original refresh token
278
+ expiresAt: Date.now() + data.expires_in * 1000,
279
+ };
280
+
281
+ saveProfile(updatedProfile);
282
+ return updatedProfile;
283
+ }
284
+
285
+ /**
286
+ * Get a valid access token, refreshing if necessary.
287
+ * Returns the current token if not expired, otherwise refreshes it.
288
+ */
289
+ export async function getValidAccessToken(): Promise<string> {
290
+ // Env var override always wins (no refresh possible)
291
+ if (process.env.GOOGLE_ACCESS_TOKEN) {
292
+ return process.env.GOOGLE_ACCESS_TOKEN;
293
+ }
294
+
295
+ const profile = loadProfile();
296
+
297
+ if (!profile.accessToken) {
298
+ throw new Error('Not authenticated. Run "connect-google config set-token <token>" or set GOOGLE_ACCESS_TOKEN.');
299
+ }
300
+
301
+ // If we have expiry info and a refresh token, check if refresh is needed
302
+ if (profile.expiresAt && profile.refreshToken) {
303
+ // Refresh if token expires within 5 minutes
304
+ if (Date.now() >= profile.expiresAt - 5 * 60 * 1000) {
305
+ const updated = await refreshAccessToken();
306
+ return updated.accessToken!;
307
+ }
308
+ }
309
+
310
+ return profile.accessToken;
311
+ }
312
+
222
313
  // ============================================
223
314
  // Utility Functions
224
315
  // ============================================
@@ -1,5 +1,6 @@
1
1
  import type { GoogleCalendarConfig, OutputFormat, GoogleApiErrorResponse } from '../types';
2
2
  import { GoogleCalendarApiError } from '../types';
3
+ import { isTokenExpired, setTokens } from '../utils/config';
3
4
 
4
5
  const DEFAULT_BASE_URL = 'https://www.googleapis.com/calendar/v3';
5
6
  const OAUTH_TOKEN_URL = 'https://oauth2.googleapis.com/token';
@@ -141,6 +142,13 @@ export class GoogleCalendarClient {
141
142
  const data = await response.json();
142
143
  this.accessToken = data.access_token;
143
144
 
145
+ // Save refreshed tokens to disk
146
+ setTokens({
147
+ accessToken: data.access_token,
148
+ refreshToken: this.refreshToken,
149
+ expiresIn: data.expires_in,
150
+ });
151
+
144
152
  return {
145
153
  accessToken: data.access_token,
146
154
  expiresIn: data.expires_in,
@@ -167,6 +175,11 @@ export class GoogleCalendarClient {
167
175
  async request<T>(path: string, options: RequestOptions = {}): Promise<T> {
168
176
  const { method = 'GET', params, body, headers = {} } = options;
169
177
 
178
+ // Auto-refresh token if expired
179
+ if (isTokenExpired() && this.refreshToken && this.clientId && this.clientSecret) {
180
+ await this.refreshAccessToken();
181
+ }
182
+
170
183
  const url = this.buildUrl(path, params);
171
184
 
172
185
  const requestHeaders: Record<string, string> = {
@@ -1,5 +1,6 @@
1
1
  import type { GoogleContactsConfig, OAuthTokenResponse, OAuthTokens } from '../types';
2
2
  import { GoogleContactsApiError, AuthError, RateLimitError } from '../types';
3
+ import { saveTokens } from '../utils/config';
3
4
 
4
5
  const DEFAULT_BASE_URL = 'https://people.googleapis.com';
5
6
  const OAUTH_TOKEN_URL = 'https://oauth2.googleapis.com/token';
@@ -118,6 +119,14 @@ export class GoogleContactsClient {
118
119
  }
119
120
 
120
121
  this.tokens = newTokens;
122
+
123
+ // Persist refreshed tokens to disk so they survive process exit
124
+ saveTokens({
125
+ accessToken: newTokens.accessToken,
126
+ refreshToken: newTokens.refreshToken,
127
+ expiresIn: newTokens.expiresIn,
128
+ });
129
+
121
130
  return this.tokens;
122
131
  }
123
132
 
@@ -1,5 +1,6 @@
1
1
  import type { GoogleDocsConfig, OutputFormat } from '../types';
2
2
  import { GoogleDocsApiError } from '../types';
3
+ import { getValidAccessToken } from '../utils/config';
3
4
 
4
5
  const DEFAULT_BASE_URL = 'https://docs.googleapis.com/v1';
5
6
 
@@ -13,8 +14,9 @@ export interface RequestOptions {
13
14
 
14
15
  export class GoogleDocsClient {
15
16
  private readonly apiKey?: string;
16
- private readonly accessToken?: string;
17
+ private accessToken?: string;
17
18
  private readonly baseUrl: string;
19
+ private readonly useAutoRefresh: boolean;
18
20
 
19
21
  constructor(config: GoogleDocsConfig) {
20
22
  if (!config.apiKey && !config.accessToken) {
@@ -22,6 +24,7 @@ export class GoogleDocsClient {
22
24
  }
23
25
  this.apiKey = config.apiKey;
24
26
  this.accessToken = config.accessToken;
27
+ this.useAutoRefresh = config.autoRefresh !== false; // defaults to true
25
28
  this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
26
29
  }
27
30
 
@@ -51,6 +54,15 @@ export class GoogleDocsClient {
51
54
  async request<T>(path: string, options: RequestOptions = {}): Promise<T> {
52
55
  const { method = 'GET', params, body, headers = {} } = options;
53
56
 
57
+ // Auto-refresh token if enabled and using OAuth
58
+ if (this.accessToken && this.useAutoRefresh) {
59
+ try {
60
+ this.accessToken = await getValidAccessToken();
61
+ } catch {
62
+ // Fall through to use current token if refresh fails
63
+ }
64
+ }
65
+
54
66
  const url = this.buildUrl(path, params);
55
67
 
56
68
  const requestHeaders: Record<string, string> = {
@@ -8,6 +8,7 @@
8
8
  export interface GoogleDocsConfig {
9
9
  apiKey?: string; // API key for read-only public documents
10
10
  accessToken?: string; // OAuth2 access token for full access
11
+ autoRefresh?: boolean; // Enable auto-refresh of access token (default: true)
11
12
  baseUrl?: string; // Override default base URL
12
13
  }
13
14
 
@@ -8,6 +8,10 @@ const DEFAULT_PROFILE = 'default';
8
8
  export interface ProfileConfig {
9
9
  apiKey?: string;
10
10
  accessToken?: string;
11
+ refreshToken?: string;
12
+ clientId?: string;
13
+ clientSecret?: string;
14
+ expiresAt?: number;
11
15
  }
12
16
 
13
17
  // Store for --profile flag override (set by CLI before commands run)
@@ -191,6 +195,100 @@ export function setAccessToken(accessToken: string): void {
191
195
  saveProfile(config);
192
196
  }
193
197
 
198
+ // ============================================
199
+ // Token Refresh
200
+ // ============================================
201
+
202
+ const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
203
+
204
+ export function getClientId(): string | undefined {
205
+ return process.env.GOOGLE_CLIENT_ID || loadProfile().clientId;
206
+ }
207
+
208
+ export function getClientSecret(): string | undefined {
209
+ return process.env.GOOGLE_CLIENT_SECRET || loadProfile().clientSecret;
210
+ }
211
+
212
+ export function getRefreshToken(): string | undefined {
213
+ return process.env.GOOGLE_REFRESH_TOKEN || loadProfile().refreshToken;
214
+ }
215
+
216
+ /**
217
+ * Refresh the access token using the refresh token and client credentials
218
+ */
219
+ export async function refreshAccessToken(): Promise<ProfileConfig> {
220
+ const profile = loadProfile();
221
+ const clientId = process.env.GOOGLE_CLIENT_ID || profile.clientId;
222
+ const clientSecret = process.env.GOOGLE_CLIENT_SECRET || profile.clientSecret;
223
+ const refreshToken = process.env.GOOGLE_REFRESH_TOKEN || profile.refreshToken;
224
+
225
+ if (!clientId || !clientSecret) {
226
+ throw new Error('OAuth client credentials not configured. Set clientId/clientSecret in profile or GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET env vars.');
227
+ }
228
+
229
+ if (!refreshToken) {
230
+ throw new Error('No refresh token available. Please re-authenticate.');
231
+ }
232
+
233
+ const response = await fetch(GOOGLE_TOKEN_URL, {
234
+ method: 'POST',
235
+ headers: {
236
+ 'Content-Type': 'application/x-www-form-urlencoded',
237
+ },
238
+ body: new URLSearchParams({
239
+ client_id: clientId,
240
+ client_secret: clientSecret,
241
+ refresh_token: refreshToken,
242
+ grant_type: 'refresh_token',
243
+ }),
244
+ });
245
+
246
+ if (!response.ok) {
247
+ const error = await response.json() as { error_description?: string; error?: string };
248
+ throw new Error(`Token refresh failed: ${error.error_description || error.error}`);
249
+ }
250
+
251
+ const data = await response.json() as { access_token: string; expires_in: number };
252
+
253
+ const updatedProfile: ProfileConfig = {
254
+ ...profile,
255
+ accessToken: data.access_token,
256
+ refreshToken: refreshToken, // Keep original refresh token
257
+ expiresAt: Date.now() + data.expires_in * 1000,
258
+ };
259
+
260
+ saveProfile(updatedProfile);
261
+ return updatedProfile;
262
+ }
263
+
264
+ /**
265
+ * Get a valid access token, refreshing if necessary.
266
+ * Returns the current token if not expired, otherwise refreshes it.
267
+ */
268
+ export async function getValidAccessToken(): Promise<string> {
269
+ // Env var override always wins (no refresh possible)
270
+ if (process.env.GOOGLE_ACCESS_TOKEN) {
271
+ return process.env.GOOGLE_ACCESS_TOKEN;
272
+ }
273
+
274
+ const profile = loadProfile();
275
+
276
+ if (!profile.accessToken) {
277
+ throw new Error('Not authenticated. Run "connect-googledocs config set-token <token>" or set GOOGLE_ACCESS_TOKEN.');
278
+ }
279
+
280
+ // If we have expiry info and a refresh token, check if refresh is needed
281
+ if (profile.expiresAt && profile.refreshToken) {
282
+ // Refresh if token expires within 5 minutes
283
+ if (Date.now() >= profile.expiresAt - 5 * 60 * 1000) {
284
+ const updated = await refreshAccessToken();
285
+ return updated.accessToken!;
286
+ }
287
+ }
288
+
289
+ return profile.accessToken;
290
+ }
291
+
194
292
  // ============================================
195
293
  // Utility Functions
196
294
  // ============================================
@@ -1,5 +1,6 @@
1
1
  import type { GoogleSheetsConfig, OutputFormat } from '../types';
2
2
  import { GoogleSheetsApiError } from '../types';
3
+ import { setAccessToken as saveAccessToken } from '../utils/config';
3
4
 
4
5
  const DEFAULT_BASE_URL = 'https://sheets.googleapis.com/v4';
5
6
 
@@ -89,6 +90,9 @@ export class GoogleSheetsClient {
89
90
  this.accessToken = data.access_token;
90
91
  this.tokenExpiresAt = Date.now() + data.expires_in * 1000;
91
92
 
93
+ // Persist refreshed token to disk so it survives process exit
94
+ saveAccessToken(data.access_token, this.tokenExpiresAt);
95
+
92
96
  return data.access_token;
93
97
  }
94
98
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/connectors",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Open source connector library - Install API connectors with a single command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,7 @@
26
26
  "scripts": {
27
27
  "build": "cd dashboard && bun run build && cd .. && bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk --external conf && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/serve.js --target bun && bun build ./src/index.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --outDir ./dist",
28
28
  "build:dashboard": "cd dashboard && bun run build",
29
- "postinstall": "[ \"$SKIP_DASHBOARD\" = \"1\" ] || [ -d dashboard/node_modules ] || (cd dashboard && bun install)",
29
+ "postinstall": "[ \"$SKIP_DASHBOARD\" = \"1\" ] || [ ! -f dashboard/package.json ] || [ -d dashboard/node_modules ] || (cd dashboard && bun install)",
30
30
  "dev": "bun run ./src/cli/index.tsx",
31
31
  "typecheck": "tsc --noEmit",
32
32
  "test": "bun test",