@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/connect-gmail",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Gmail API connector CLI - A TypeScript wrapper for Gmail with OAuth2 authentication",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 (handles refresh automatically)
47
- const accessToken = await getValidAccessToken();
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 - tokens are loaded automatically from config
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
- * Refresh the access token using the refresh token
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 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
-
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: currentTokens.refreshToken,
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
- const tokens: OAuth2Tokens = {
142
+ return {
149
143
  accessToken: data.access_token,
150
- refreshToken: currentTokens.refreshToken, // Keep the original refresh token
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 || currentTokens.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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/connect-stripe",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Stripe API connector - A TypeScript CLI for interacting with the Stripe API with multi-profile support",
5
5
  "type": "module",
6
6
  "bin": {
@@ -68,8 +68,15 @@ export class Connector {
68
68
  }
69
69
 
70
70
  /**
71
- * Create a client from environment variables
72
- * Looks for STRIPE_API_KEY and optionally STRIPE_API_SECRET
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;
@@ -1,7 +1,7 @@
1
1
  // Stripe Connector API
2
2
  // A TypeScript wrapper for the Stripe API
3
3
 
4
- export { Connector } from './api';
4
+ export { Connector, Connector as Stripe } from './api';
5
5
  export * from './types';
6
6
 
7
7
  // Re-export individual API classes for advanced usage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/connectors",
3
- "version": "1.3.13",
3
+ "version": "1.3.14",
4
4
  "description": "Open source connector library - Install API connectors with a single command",
5
5
  "type": "module",
6
6
  "bin": {