cf-service-sdk 0.1.18 → 0.1.20

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/dist/client.d.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  import { ApolloClient, NormalizedCacheObject } from '@apollo/client';
2
+ import { TokenManager } from './tokenManager';
2
3
  export interface CloudForgeClientOptions {
3
4
  baseUrl: string;
4
5
  token?: string;
5
6
  onUnauthorized?: () => void | Promise<void>;
6
7
  onError?: (error: Error) => void;
7
8
  onValidationError?: (validationErrors: ValidationError[]) => void;
9
+ /** @deprecated Use the built-in TokenManager instead. Kept for backward compatibility. */
8
10
  refreshToken?: () => Promise<string | null>;
11
+ /** Called when token is refreshed (e.g. to persist to localStorage) */
12
+ onTokenRefreshed?: (token: string) => void;
13
+ /** Request credentials mode for HttpLink. Defaults to 'include' for cookie support. */
14
+ credentials?: RequestCredentials;
9
15
  }
10
16
  export interface ValidationError {
11
17
  field: string;
@@ -14,12 +20,37 @@ export interface ValidationError {
14
20
  export declare class CloudForgeClient {
15
21
  private apolloClient;
16
22
  private options;
17
- private isRefreshing;
23
+ readonly tokenManager: TokenManager;
24
+ private pendingRefreshQueue;
25
+ private isRefreshingLink;
18
26
  constructor(options: CloudForgeClientOptions);
19
27
  private createApolloClient;
28
+ private isAuthError;
29
+ private createErrorLink;
30
+ /**
31
+ * Retry a failed operation after refreshing the token.
32
+ * Uses a queue to ensure only ONE refresh HTTP call is made, even when
33
+ * multiple operations fail simultaneously (e.g. expired JWT).
34
+ * The first caller triggers the refresh; subsequent callers queue up
35
+ * and are retried (or errored) when the single refresh completes.
36
+ */
37
+ private retryWithRefresh;
38
+ /**
39
+ * Drain the pending refresh queue: retry all queued operations with
40
+ * the new token, or error them all if refresh failed.
41
+ */
42
+ private drainQueue;
43
+ /**
44
+ * Update the JWT token. Does NOT recreate the Apollo client -
45
+ * the auth link reads the token dynamically.
46
+ */
20
47
  setToken(token: string): void;
21
48
  getClient(): ApolloClient<NormalizedCacheObject>;
22
49
  getBaseUrl(): string;
50
+ /** @deprecated Use tokenManager.refresh() instead */
23
51
  refreshToken(): Promise<boolean>;
24
- logout(): void;
52
+ /**
53
+ * Logout: revoke refresh token server-side, clear Apollo cache, reset state.
54
+ */
55
+ logout(): Promise<void>;
25
56
  }
package/dist/client.js CHANGED
@@ -4,262 +4,239 @@ exports.CloudForgeClient = void 0;
4
4
  const client_1 = require("@apollo/client");
5
5
  const error_1 = require("@apollo/client/link/error");
6
6
  const context_1 = require("@apollo/client/link/context");
7
+ const tokenManager_1 = require("./tokenManager");
7
8
  const defaultOptions = {
8
9
  baseUrl: 'http://localhost:8000/api/graph/',
9
- };
10
- // Detect if we're in a React Native environment
11
- const isReactNative = () => {
12
- return typeof global.HermesInternal !== 'undefined' ||
13
- typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
10
+ credentials: 'include',
14
11
  };
15
12
  class CloudForgeClient {
16
13
  constructor(options) {
17
- this.isRefreshing = false;
14
+ this.pendingRefreshQueue = [];
15
+ this.isRefreshingLink = false;
18
16
  this.options = { ...defaultOptions, ...options };
17
+ this.tokenManager = new tokenManager_1.TokenManager({
18
+ baseUrl: this.options.baseUrl,
19
+ onTokenRefreshed: (token) => {
20
+ var _a, _b;
21
+ this.options.token = token;
22
+ (_b = (_a = this.options).onTokenRefreshed) === null || _b === void 0 ? void 0 : _b.call(_a, token);
23
+ },
24
+ onSessionExpired: () => {
25
+ var _a, _b;
26
+ (_b = (_a = this.options).onUnauthorized) === null || _b === void 0 ? void 0 : _b.call(_a);
27
+ },
28
+ });
29
+ if (this.options.token) {
30
+ this.tokenManager.setToken(this.options.token);
31
+ }
19
32
  this.apolloClient = this.createApolloClient();
20
33
  }
21
34
  createApolloClient() {
22
- // Mobile device workaround: replace localhost with the device's network IP
23
- // This is needed because localhost on a mobile device refers to the device itself
24
- let apiUrl = this.options.baseUrl;
25
- // Note: We previously modified the URL here, but now we expect the URL to be
26
- // properly configured by the application that uses this SDK, especially for React Native
27
- // applications where localhost means different things on different platforms.
28
- // HTTP link
35
+ var _a;
29
36
  const httpLink = (0, client_1.createHttpLink)({
30
- uri: apiUrl,
37
+ uri: this.options.baseUrl,
38
+ credentials: (_a = this.options.credentials) !== null && _a !== void 0 ? _a : 'include',
31
39
  fetchOptions: {
32
- cache: 'no-store', // Disable browser cache
40
+ cache: 'no-store',
33
41
  },
34
42
  });
35
- // Auth link for adding the token to headers
43
+ // Auth link reads token dynamically - no need to recreate client on token change
36
44
  const authLink = (0, context_1.setContext)((_, { headers }) => {
37
- const token = this.options.token;
45
+ const token = this.tokenManager.getToken();
38
46
  return {
39
47
  headers: {
40
48
  ...headers,
41
- authorization: token ? `JWT ${token}` : '',
49
+ ...(token ? { authorization: `JWT ${token}` } : {}),
42
50
  },
43
51
  };
44
52
  });
45
- // Error handling link
46
- const errorLink = (0, error_1.onError)(({ graphQLErrors, networkError, operation, forward }) => {
47
- var _a, _b, _c, _d;
48
- console.log('==== ERROR LINK ====');
49
- console.log('==== GRAPHQL ERRORS ====');
50
- console.log(graphQLErrors);
51
- console.log('==== NETWORK ERROR ====');
52
- console.log(networkError);
53
- console.log('==== OPERATION ====');
54
- console.log(operation);
53
+ const errorLink = this.createErrorLink();
54
+ const link = client_1.ApolloLink.from([errorLink, authLink, httpLink]);
55
+ return new client_1.ApolloClient({
56
+ link,
57
+ cache: new client_1.InMemoryCache(),
58
+ defaultOptions: {
59
+ watchQuery: {
60
+ fetchPolicy: 'network-only',
61
+ errorPolicy: 'all',
62
+ },
63
+ query: {
64
+ fetchPolicy: 'network-only',
65
+ errorPolicy: 'all',
66
+ },
67
+ mutate: {
68
+ errorPolicy: 'all',
69
+ },
70
+ },
71
+ });
72
+ }
73
+ isAuthError(message) {
74
+ const msg = message.toLowerCase();
75
+ return (msg.includes('signature has expired') ||
76
+ msg.includes('error decoding signature') ||
77
+ (msg.includes('token') && msg.includes('expired')) ||
78
+ msg.includes('authentication'));
79
+ }
80
+ createErrorLink() {
81
+ return (0, error_1.onError)(({ graphQLErrors, networkError, operation, forward }) => {
82
+ var _a, _b, _c, _d, _e, _f, _g;
83
+ // Handle GraphQL-level auth errors
55
84
  if (graphQLErrors) {
56
85
  const validationErrors = [];
57
- graphQLErrors.forEach(({ message, locations, path }) => {
58
- var _a, _b;
59
- console.error(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`);
60
- // Check for unauthorized errors
61
- if (message.includes('Authentication') ||
62
- message.includes('Error decoding signature') ||
63
- message.includes('JWT') ||
64
- message.toLowerCase().includes('token') ||
65
- message.includes('Signature has expired')) {
66
- console.log('==== JWT ERROR DETECTED ====', message);
67
- // Try to refresh the token if a refresh function is provided
68
- if (this.options.refreshToken && !this.isRefreshing) {
69
- this.isRefreshing = true;
70
- // Attempt to refresh the token
71
- this.options.refreshToken()
72
- .then(newToken => {
73
- var _a, _b;
74
- if (newToken) {
75
- console.log('==== TOKEN REFRESHED ====');
76
- // Set the new token
77
- this.setToken(newToken);
78
- // Retry the failed request
79
- const oldHeaders = operation.getContext().headers;
80
- operation.setContext({
81
- headers: {
82
- ...oldHeaders,
83
- authorization: `JWT ${newToken}`,
84
- },
85
- });
86
- // Retry the operation
87
- return forward(operation);
88
- }
89
- else {
90
- console.log('==== TOKEN REFRESH FAILED ====');
91
- // If refresh token failed, call onUnauthorized
92
- (_b = (_a = this.options).onUnauthorized) === null || _b === void 0 ? void 0 : _b.call(_a);
93
- }
94
- })
95
- .catch(error => {
96
- var _a, _b;
97
- console.error('Token refresh failed:', error);
98
- (_b = (_a = this.options).onUnauthorized) === null || _b === void 0 ? void 0 : _b.call(_a);
99
- })
100
- .finally(() => {
101
- this.isRefreshing = false;
102
- });
103
- }
104
- else {
105
- // If no refresh function is provided, just call onUnauthorized
106
- (_b = (_a = this.options).onUnauthorized) === null || _b === void 0 ? void 0 : _b.call(_a);
107
- }
86
+ let hasAuthError = false;
87
+ graphQLErrors.forEach(({ message }) => {
88
+ if (this.isAuthError(message)) {
89
+ hasAuthError = true;
108
90
  }
109
- // Check for validation errors
110
- if (message.includes('got invalid value') || message.includes('Field') && message.includes('was not provided')) {
111
- // Extract field name from validation error
91
+ // Collect validation errors
92
+ if (message.includes('got invalid value') || (message.includes('Field') && message.includes('was not provided'))) {
112
93
  const fieldMatch = message.match(/Field '([^']+)'/);
113
- if (fieldMatch && fieldMatch[1]) {
114
- validationErrors.push({
115
- field: fieldMatch[1],
116
- message: message.trim()
117
- });
94
+ if (fieldMatch === null || fieldMatch === void 0 ? void 0 : fieldMatch[1]) {
95
+ validationErrors.push({ field: fieldMatch[1], message: message.trim() });
118
96
  }
119
97
  }
120
98
  });
121
- // If we found validation errors, call the validation error handler
122
- if (validationErrors.length > 0 && this.options.onValidationError) {
123
- this.options.onValidationError(validationErrors);
99
+ if (validationErrors.length > 0) {
100
+ (_b = (_a = this.options).onValidationError) === null || _b === void 0 ? void 0 : _b.call(_a, validationErrors);
101
+ }
102
+ if (hasAuthError) {
103
+ return this.retryWithRefresh(operation, forward);
124
104
  }
125
105
  }
106
+ // Handle network-level auth errors
126
107
  if (networkError) {
127
- console.error(`[Network error]: ${networkError}`);
128
- // Check if network error is related to expired signature
129
- if (networkError.message && networkError.message.includes('Signature has expired')) {
130
- console.log('==== JWT NETWORK ERROR DETECTED ====', networkError.message);
131
- // Try to refresh the token if a refresh function is provided
132
- if (this.options.refreshToken && !this.isRefreshing) {
133
- this.isRefreshing = true;
134
- // Attempt to refresh the token
135
- this.options.refreshToken()
136
- .then(newToken => {
137
- var _a, _b;
138
- if (newToken) {
139
- console.log('==== TOKEN REFRESHED (NETWORK) ====');
140
- // Set the new token
141
- this.setToken(newToken);
142
- // Retry the failed request
143
- const oldHeaders = operation.getContext().headers;
144
- operation.setContext({
145
- headers: {
146
- ...oldHeaders,
147
- authorization: `JWT ${newToken}`,
148
- },
149
- });
150
- // Retry the operation
151
- return forward(operation);
152
- }
153
- else {
154
- console.log('==== TOKEN REFRESH FAILED (NETWORK) ====');
155
- // If refresh token failed, call onUnauthorized
156
- (_b = (_a = this.options).onUnauthorized) === null || _b === void 0 ? void 0 : _b.call(_a);
157
- }
158
- })
159
- .catch(error => {
160
- var _a, _b;
161
- console.error('Token refresh failed:', error);
162
- (_b = (_a = this.options).onUnauthorized) === null || _b === void 0 ? void 0 : _b.call(_a);
163
- })
164
- .finally(() => {
165
- this.isRefreshing = false;
166
- });
167
- }
168
- else {
169
- // If no refresh function is provided, just call onUnauthorized
170
- (_b = (_a = this.options).onUnauthorized) === null || _b === void 0 ? void 0 : _b.call(_a);
171
- }
108
+ if ((_c = networkError.message) === null || _c === void 0 ? void 0 : _c.includes('Signature has expired')) {
109
+ return this.retryWithRefresh(operation, forward);
172
110
  }
173
- // For server errors (400/500), try to extract more information
111
+ // Extract validation errors from server error responses
174
112
  const serverError = networkError;
175
- if (serverError.name === 'ServerError' &&
176
- serverError.result &&
177
- typeof serverError.result === 'object') {
178
- // Handle validation errors that might be in the response body
113
+ if (serverError.name === 'ServerError' && serverError.result && typeof serverError.result === 'object') {
179
114
  try {
180
115
  const errorResult = serverError.result;
181
116
  if (errorResult.errors && Array.isArray(errorResult.errors)) {
182
117
  const validationErrors = [];
183
118
  errorResult.errors.forEach((error) => {
184
- if (error.message && (error.message.includes('got invalid value') ||
185
- (error.message.includes('Field') && error.message.includes('was not provided')))) {
119
+ if (error.message && (error.message.includes('got invalid value') || (error.message.includes('Field') && error.message.includes('was not provided')))) {
186
120
  const fieldMatch = error.message.match(/Field '([^']+)'/);
187
- if (fieldMatch && fieldMatch[1]) {
188
- validationErrors.push({
189
- field: fieldMatch[1],
190
- message: error.message.trim()
191
- });
121
+ if (fieldMatch === null || fieldMatch === void 0 ? void 0 : fieldMatch[1]) {
122
+ validationErrors.push({ field: fieldMatch[1], message: error.message.trim() });
192
123
  }
193
124
  }
194
125
  });
195
- if (validationErrors.length > 0 && this.options.onValidationError) {
196
- this.options.onValidationError(validationErrors);
126
+ if (validationErrors.length > 0) {
127
+ (_e = (_d = this.options).onValidationError) === null || _e === void 0 ? void 0 : _e.call(_d, validationErrors);
197
128
  }
198
129
  }
199
130
  }
200
- catch (e) {
201
- console.error('Error parsing server error response:', e);
131
+ catch (_h) {
132
+ // Ignore parse errors
202
133
  }
203
134
  }
204
- (_d = (_c = this.options).onError) === null || _d === void 0 ? void 0 : _d.call(_c, networkError);
135
+ (_g = (_f = this.options).onError) === null || _g === void 0 ? void 0 : _g.call(_f, networkError);
205
136
  }
206
137
  });
207
- // Combine the links
208
- const link = client_1.ApolloLink.from([errorLink, authLink, httpLink]);
209
- // Create the Apollo Client
210
- return new client_1.ApolloClient({
211
- link,
212
- cache: new client_1.InMemoryCache(),
213
- defaultOptions: {
214
- watchQuery: {
215
- fetchPolicy: 'network-only',
216
- errorPolicy: 'all',
217
- },
218
- query: {
219
- fetchPolicy: 'network-only',
220
- errorPolicy: 'all',
221
- },
222
- mutate: {
223
- errorPolicy: 'all',
224
- },
225
- },
138
+ }
139
+ /**
140
+ * Retry a failed operation after refreshing the token.
141
+ * Uses a queue to ensure only ONE refresh HTTP call is made, even when
142
+ * multiple operations fail simultaneously (e.g. expired JWT).
143
+ * The first caller triggers the refresh; subsequent callers queue up
144
+ * and are retried (or errored) when the single refresh completes.
145
+ */
146
+ retryWithRefresh(operation, forward) {
147
+ return new client_1.Observable((observer) => {
148
+ this.pendingRefreshQueue.push({ operation, forward, observer });
149
+ // If a refresh is already in-flight, this operation is queued and
150
+ // will be retried when that refresh completes. Don't trigger another.
151
+ if (this.isRefreshingLink)
152
+ return;
153
+ this.isRefreshingLink = true;
154
+ const refreshFn = this.options.refreshToken
155
+ ? this.options.refreshToken
156
+ : () => this.tokenManager.refresh();
157
+ refreshFn()
158
+ .then((newToken) => {
159
+ var _a, _b, _c, _d;
160
+ if (!newToken) {
161
+ this.drainQueue(null, new Error('Token refresh failed'));
162
+ (_b = (_a = this.options).onUnauthorized) === null || _b === void 0 ? void 0 : _b.call(_a);
163
+ return;
164
+ }
165
+ this.options.token = newToken;
166
+ this.tokenManager.setToken(newToken);
167
+ // Legacy refreshToken path bypasses TokenManager._doRefresh() which
168
+ // normally fires onTokenRefreshed. Call it here so consumers (e.g.
169
+ // cf-web persisting to localStorage) still get notified.
170
+ if (this.options.refreshToken) {
171
+ (_d = (_c = this.options).onTokenRefreshed) === null || _d === void 0 ? void 0 : _d.call(_c, newToken);
172
+ }
173
+ this.drainQueue(newToken, null);
174
+ })
175
+ .catch((error) => {
176
+ var _a, _b;
177
+ this.drainQueue(null, error);
178
+ (_b = (_a = this.options).onUnauthorized) === null || _b === void 0 ? void 0 : _b.call(_a);
179
+ })
180
+ .finally(() => {
181
+ this.isRefreshingLink = false;
182
+ });
183
+ });
184
+ }
185
+ /**
186
+ * Drain the pending refresh queue: retry all queued operations with
187
+ * the new token, or error them all if refresh failed.
188
+ */
189
+ drainQueue(newToken, error) {
190
+ const queue = [...this.pendingRefreshQueue];
191
+ this.pendingRefreshQueue = [];
192
+ if (error || !newToken) {
193
+ queue.forEach(({ observer: obs }) => {
194
+ obs.error(error !== null && error !== void 0 ? error : new Error('Token refresh failed'));
195
+ });
196
+ return;
197
+ }
198
+ queue.forEach(({ operation: op, forward: fwd, observer: obs }) => {
199
+ op.setContext(({ headers = {} }) => ({
200
+ headers: { ...headers, authorization: `JWT ${newToken}` },
201
+ }));
202
+ fwd(op).subscribe(obs);
226
203
  });
227
204
  }
228
- // Method to update the token
205
+ /**
206
+ * Update the JWT token. Does NOT recreate the Apollo client -
207
+ * the auth link reads the token dynamically.
208
+ */
229
209
  setToken(token) {
230
210
  this.options.token = token;
231
- // Recreate the client with the new token
232
- this.apolloClient = this.createApolloClient();
211
+ this.tokenManager.setToken(token);
233
212
  }
234
- // Get the Apollo client instance
235
213
  getClient() {
236
214
  return this.apolloClient;
237
215
  }
238
- // Get the base URL
239
216
  getBaseUrl() {
240
217
  return this.options.baseUrl;
241
218
  }
242
- // Refresh token - attempt to get a new token
219
+ /** @deprecated Use tokenManager.refresh() instead */
243
220
  async refreshToken() {
244
- if (this.options.refreshToken) {
245
- try {
246
- const newToken = await this.options.refreshToken();
247
- if (newToken) {
248
- this.setToken(newToken);
249
- return true;
250
- }
251
- }
252
- catch (error) {
253
- console.error('Failed to refresh token:', error);
254
- }
221
+ const newToken = await this.tokenManager.refresh();
222
+ if (newToken) {
223
+ this.setToken(newToken);
224
+ return true;
255
225
  }
256
226
  return false;
257
227
  }
258
- // Logout - clear the token
259
- logout() {
228
+ /**
229
+ * Logout: revoke refresh token server-side, clear Apollo cache, reset state.
230
+ */
231
+ async logout() {
232
+ await this.tokenManager.logout();
260
233
  this.options.token = undefined;
261
- this.apolloClient = this.createApolloClient();
262
- this.apolloClient.resetStore();
234
+ try {
235
+ await this.apolloClient.resetStore();
236
+ }
237
+ catch (_a) {
238
+ // resetStore can throw if there are active queries
239
+ }
263
240
  }
264
241
  }
265
242
  exports.CloudForgeClient = CloudForgeClient;
@@ -2132,6 +2132,13 @@ export type DisconnectNylasIntegration = {
2132
2132
  message?: Maybe<Scalars['String']['output']>;
2133
2133
  success?: Maybe<Scalars['Boolean']['output']>;
2134
2134
  };
2135
+ /** GraphQL input type for an email attachment */
2136
+ export type EmailAttachmentInput = {
2137
+ name: Scalars['String']['input'];
2138
+ size?: InputMaybe<Scalars['Int']['input']>;
2139
+ type?: InputMaybe<Scalars['String']['input']>;
2140
+ url: Scalars['String']['input'];
2141
+ };
2135
2142
  /** Filter input for email campaign report queries. */
2136
2143
  export type EmailCampaignReportFilterInput = {
2137
2144
  /** Date range type: today, yesterday, this_week, last_week, this_month, last_month, custom */
@@ -2462,7 +2469,6 @@ export type Login = {
2462
2469
  id?: Maybe<Scalars['ID']['output']>;
2463
2470
  isAdmin?: Maybe<Scalars['Boolean']['output']>;
2464
2471
  payload?: Maybe<Scalars['JSONString']['output']>;
2465
- refreshToken?: Maybe<Scalars['String']['output']>;
2466
2472
  token?: Maybe<Scalars['String']['output']>;
2467
2473
  };
2468
2474
  /** LoginWithGoogle - Login with Google */
@@ -3271,6 +3277,7 @@ export type MutationSendEmailToEmailThreadArgs = {
3271
3277
  };
3272
3278
  /** Mutations */
3273
3279
  export type MutationSendManualEmailArgs = {
3280
+ attachmentUrls?: InputMaybe<Array<InputMaybe<EmailAttachmentInput>>>;
3274
3281
  bcc?: InputMaybe<Array<InputMaybe<Scalars['String']['input']>>>;
3275
3282
  body: Scalars['String']['input'];
3276
3283
  cc?: InputMaybe<Array<InputMaybe<Scalars['String']['input']>>>;
@@ -10493,7 +10500,6 @@ export type LoginMutation = {
10493
10500
  isAdmin?: boolean | null;
10494
10501
  accountId?: string | null;
10495
10502
  payload?: any | null;
10496
- refreshToken?: string | null;
10497
10503
  account?: {
10498
10504
  __typename?: 'AccountType';
10499
10505
  id?: string | null;
@@ -12001,6 +12007,7 @@ export type SendEmailToEmailThreadMutation = {
12001
12007
  } | null;
12002
12008
  };
12003
12009
  export type SendManualEmailMutationVariables = Exact<{
12010
+ attachmentUrls?: InputMaybe<Array<InputMaybe<EmailAttachmentInput>> | InputMaybe<EmailAttachmentInput>>;
12004
12011
  bcc?: InputMaybe<Array<InputMaybe<Scalars['String']['input']>> | InputMaybe<Scalars['String']['input']>>;
12005
12012
  body: Scalars['String']['input'];
12006
12013
  cc?: InputMaybe<Array<InputMaybe<Scalars['String']['input']>> | InputMaybe<Scalars['String']['input']>>;
@@ -32259,6 +32266,7 @@ export type SendManualEmailMutationFn = Apollo.MutationFunction<SendManualEmailM
32259
32266
  * @example
32260
32267
  * const [sendManualEmailMutation, { data, loading, error }] = useSendManualEmailMutation({
32261
32268
  * variables: {
32269
+ * attachmentUrls: // value for 'attachmentUrls'
32262
32270
  * bcc: // value for 'bcc'
32263
32271
  * body: // value for 'body'
32264
32272
  * cc: // value for 'cc'
@@ -32269,6 +32277,7 @@ export type SendManualEmailMutationFn = Apollo.MutationFunction<SendManualEmailM
32269
32277
  * });
32270
32278
  */
32271
32279
  export declare function useSendManualEmailMutation(baseOptions?: Apollo.MutationHookOptions<SendManualEmailMutation, SendManualEmailMutationVariables>): Apollo.MutationTuple<SendManualEmailMutation, Exact<{
32280
+ attachmentUrls?: InputMaybe<Array<InputMaybe<EmailAttachmentInput>> | InputMaybe<EmailAttachmentInput>>;
32272
32281
  bcc?: InputMaybe<Array<InputMaybe<Scalars["String"]["input"]>> | InputMaybe<Scalars["String"]["input"]>>;
32273
32282
  body: Scalars["String"]["input"];
32274
32283
  cc?: InputMaybe<Array<InputMaybe<Scalars["String"]["input"]>> | InputMaybe<Scalars["String"]["input"]>>;
@@ -6460,7 +6460,6 @@ exports.LoginDocument = (0, client_1.gql) `
6460
6460
  isAdmin
6461
6461
  accountId
6462
6462
  payload
6463
- refreshToken
6464
6463
  }
6465
6464
  }
6466
6465
  `;
@@ -8458,8 +8457,9 @@ function useSendEmailToEmailThreadMutation(baseOptions) {
8458
8457
  return Apollo.useMutation(exports.SendEmailToEmailThreadDocument, options);
8459
8458
  }
8460
8459
  exports.SendManualEmailDocument = (0, client_1.gql) `
8461
- mutation SendManualEmail($bcc: [String], $body: String!, $cc: [String], $logId: ID!, $subject: String!, $to: String!) {
8460
+ mutation SendManualEmail($attachmentUrls: [EmailAttachmentInput], $bcc: [String], $body: String!, $cc: [String], $logId: ID!, $subject: String!, $to: String!) {
8462
8461
  sendManualEmail(
8462
+ attachmentUrls: $attachmentUrls
8463
8463
  bcc: $bcc
8464
8464
  body: $body
8465
8465
  cc: $cc
@@ -8934,6 +8934,7 @@ exports.SendManualEmailDocument = (0, client_1.gql) `
8934
8934
  * @example
8935
8935
  * const [sendManualEmailMutation, { data, loading, error }] = useSendManualEmailMutation({
8936
8936
  * variables: {
8937
+ * attachmentUrls: // value for 'attachmentUrls'
8937
8938
  * bcc: // value for 'bcc'
8938
8939
  * body: // value for 'body'
8939
8940
  * cc: // value for 'cc'
package/dist/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export { CloudForgeSDK } from "./sdk";
2
+ export { TokenManager } from "./tokenManager";
2
3
  export type { CloudForgeClientOptions } from "./client";
4
+ export type { TokenManagerOptions } from "./tokenManager";
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CloudForgeSDK = void 0;
3
+ exports.TokenManager = exports.CloudForgeSDK = void 0;
4
4
  var sdk_1 = require("./sdk");
5
5
  Object.defineProperty(exports, "CloudForgeSDK", { enumerable: true, get: function () { return sdk_1.CloudForgeSDK; } });
6
+ var tokenManager_1 = require("./tokenManager");
7
+ Object.defineProperty(exports, "TokenManager", { enumerable: true, get: function () { return tokenManager_1.TokenManager; } });
package/dist/mutations.js CHANGED
@@ -4410,8 +4410,8 @@ mutation UpdateCombinedCampaignLogs($logIds: [ID]!, $notes: String, $outcome: St
4410
4410
  }
4411
4411
  }`;
4412
4412
  exports.SEND_MANUAL_EMAIL = (0, client_1.gql) `
4413
- mutation SendManualEmail($bcc: [String], $body: String!, $cc: [String], $logId: ID!, $subject: String!, $to: String!) {
4414
- sendManualEmail(bcc: $bcc, body: $body, cc: $cc, logId: $logId, subject: $subject, to: $to) {
4413
+ mutation SendManualEmail($attachmentUrls: [EmailAttachmentInput], $bcc: [String], $body: String!, $cc: [String], $logId: ID!, $subject: String!, $to: String!) {
4414
+ sendManualEmail(attachmentUrls: $attachmentUrls, bcc: $bcc, body: $body, cc: $cc, logId: $logId, subject: $subject, to: $to) {
4415
4415
  log {
4416
4416
  id
4417
4417
  uuid
@@ -6952,7 +6952,6 @@ mutation Login($email: String!, $password: String!) {
6952
6952
  isAdmin
6953
6953
  accountId
6954
6954
  payload
6955
- refreshToken
6956
6955
  }
6957
6956
  }`;
6958
6957
  exports.UPDATE_USER_PROFILE = (0, client_1.gql) `
package/dist/sdk.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CloudForgeClientOptions } from "./client";
2
- import { AddContactToSupplierListInput, AddCustomContactToSupplierInput, AddSupplierToMySuppliersInput, AddSupplierToSupplierListInput, AutomatedProspectingCampaignInput, BulkAssignOwnersInput, CallReportFilterInput, CallScriptTemplateInput, CombinedCampaignFilterInput, CombinedCampaignInput, CombinedCampaignStepInput, CombinedCampaignTemplateInput, CompanyFilterInput, CompanyNoteFilterInput, CompanyNoteInput, CompanyNoteSortInput, CompanyProfileInput, CompanySortInput, CompleteRfqInput, ContactFilterInput, ContactSortInput, CreateCallCampaignLogMutationVariables, CreateCallCampaignMutationVariables, CreateCampaignMutationVariables, CreateCompanyMutationVariables, CreateContactMutationVariables, CreateEmailTemplateMutationVariables, CreateNotificationMutationVariables, CreateSalesGoalInput, CreateSegmentMutationVariables, CreateTaskMutationVariables, CrmCurrentPipelineFilterInput, CrmFilterInput, CustomContactInput, EmailCampaignReportFilterInput, EmailToneInput, FileUploadInput, GenerateCallCampaignScriptInput, HiddenRecordInput, InvitationResponseInput, InviteUserInput, MailLogFilterInput, MicrosoftUserInput, MySupplierInput, PaginationInput, Procurement_MySupplierFilterInput, Procurement_RfqFilterInput, Procurement_RfqLineItemFilterInput, Procurement_SupplierListFilterInput, RecentHistoryFilterInput, RecentHistorySortInput, ReduceSpamInput, RemoveContactFromMySupplierInput, RemoveContactFromSupplierListInput, RemoveCustomContactFromSupplierInput, RemoveSupplierFromSupplierListInput, RfqInput, RfqLineItemInput, SalesGoalFilterInput, SaveNylasConnectionMutationVariables, Scalars, ScheduleRfqInput, SegmentFilterInput, SendRfqInput, SendRfqTestEmailInput, SetLeadAsNotInterestedInput, SignupInput, SubmitFeedbackMutationVariables, SupplierListInput, SupplierSearchFilterInput, SupplierSortInput, TaskFilterInput, UpdateCallCampaignLogMutationVariables, UpdateCallCampaignMutationVariables, UpdateCampaignMutationVariables, UpdateCombinedCampaignInput, UpdateCombinedCampaignLogInput, UpdateCompanyExternalIdentifierInput, UpdateCompanyMutationVariables, UpdateContactMutationVariables, UpdateEmailTemplateMutationVariables, UpdateSalesGoalInput, UpdateSegmentMutationVariables, UpdateTaskMutationVariables, UpdateUserProfileMutationVariables, UserTypeInput } from "./generated/graphql";
2
+ import { AddContactToSupplierListInput, AddCustomContactToSupplierInput, AddSupplierToMySuppliersInput, AddSupplierToSupplierListInput, AutomatedProspectingCampaignInput, BulkAssignOwnersInput, CallReportFilterInput, CallScriptTemplateInput, CombinedCampaignFilterInput, CombinedCampaignInput, CombinedCampaignStepInput, CombinedCampaignTemplateInput, CompanyFilterInput, CompanyNoteFilterInput, CompanyNoteInput, CompanyNoteSortInput, CompanyProfileInput, CompanySortInput, CompleteRfqInput, ContactFilterInput, ContactSortInput, CreateCallCampaignLogMutationVariables, CreateCallCampaignMutationVariables, CreateCampaignMutationVariables, CreateCompanyMutationVariables, CreateContactMutationVariables, CreateEmailTemplateMutationVariables, CreateNotificationMutationVariables, CreateSalesGoalInput, CreateSegmentMutationVariables, CreateTaskMutationVariables, CrmCurrentPipelineFilterInput, CrmFilterInput, CustomContactInput, EmailAttachmentInput, EmailCampaignReportFilterInput, EmailToneInput, FileUploadInput, GenerateCallCampaignScriptInput, HiddenRecordInput, InvitationResponseInput, InviteUserInput, MailLogFilterInput, MicrosoftUserInput, MySupplierInput, PaginationInput, Procurement_MySupplierFilterInput, Procurement_RfqFilterInput, Procurement_RfqLineItemFilterInput, Procurement_SupplierListFilterInput, RecentHistoryFilterInput, RecentHistorySortInput, ReduceSpamInput, RemoveContactFromMySupplierInput, RemoveContactFromSupplierListInput, RemoveCustomContactFromSupplierInput, RemoveSupplierFromSupplierListInput, RfqInput, RfqLineItemInput, SalesGoalFilterInput, SaveNylasConnectionMutationVariables, Scalars, ScheduleRfqInput, SegmentFilterInput, SendRfqInput, SendRfqTestEmailInput, SetLeadAsNotInterestedInput, SignupInput, SubmitFeedbackMutationVariables, SupplierListInput, SupplierSearchFilterInput, SupplierSortInput, TaskFilterInput, UpdateCallCampaignLogMutationVariables, UpdateCallCampaignMutationVariables, UpdateCampaignMutationVariables, UpdateCombinedCampaignInput, UpdateCombinedCampaignLogInput, UpdateCompanyExternalIdentifierInput, UpdateCompanyMutationVariables, UpdateContactMutationVariables, UpdateEmailTemplateMutationVariables, UpdateSalesGoalInput, UpdateSegmentMutationVariables, UpdateTaskMutationVariables, UpdateUserProfileMutationVariables, UserTypeInput } from "./generated/graphql";
3
3
  export declare class CloudForgeSDK {
4
4
  private client;
5
5
  private apolloClient;
@@ -105,7 +105,7 @@ export declare class CloudForgeSDK {
105
105
  scheduleRfq(input: ScheduleRfqInput): Promise<import("@apollo/client").FetchResult<any>>;
106
106
  sendEmailToContact(contactId: string, subject: string, template: string, campaignId?: string, fileAttachments?: Scalars['JSONString']['input'], segmentId?: string): Promise<import("@apollo/client").FetchResult<any>>;
107
107
  sendEmailToEmailThread(action: string, body: string, mailLogId: number, attachments?: Scalars['JSONString']['input'], bccEmails?: string[], ccEmails?: string[], subject?: string, toEmails?: string[]): Promise<import("@apollo/client").FetchResult<any>>;
108
- sendManualEmail(body: string, logId: string, subject: string, to: string, bcc?: string[], cc?: string[]): Promise<import("@apollo/client").FetchResult<any>>;
108
+ sendManualEmail(body: string, logId: string, subject: string, to: string, attachmentUrls?: EmailAttachmentInput[], bcc?: string[], cc?: string[]): Promise<import("@apollo/client").FetchResult<any>>;
109
109
  sendRfq(input: SendRfqInput): Promise<import("@apollo/client").FetchResult<any>>;
110
110
  sendRfqTestEmail(input: SendRfqTestEmailInput): Promise<import("@apollo/client").FetchResult<any>>;
111
111
  sendTestEmail(campaignId: string, emailAddress: string, subject: string, template: string): Promise<import("@apollo/client").FetchResult<any>>;
package/dist/sdk.js CHANGED
@@ -610,10 +610,10 @@ class CloudForgeSDK {
610
610
  variables: { action, body, mailLogId, attachments, bccEmails, ccEmails, subject, toEmails }
611
611
  });
612
612
  }
613
- async sendManualEmail(body, logId, subject, to, bcc, cc) {
613
+ async sendManualEmail(body, logId, subject, to, attachmentUrls, bcc, cc) {
614
614
  return this.apolloClient.mutate({
615
615
  mutation: mutations_1.SEND_MANUAL_EMAIL,
616
- variables: { body, logId, subject, to, bcc, cc }
616
+ variables: { body, logId, subject, to, attachmentUrls, bcc, cc }
617
617
  });
618
618
  }
619
619
  async sendRfq(input) {
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Central token lifecycle manager for CloudForge authentication.
3
+ *
4
+ * Handles:
5
+ * - Calling /api/auth/refresh/ with HttpOnly cookie credentials
6
+ * - Promise deduplication (concurrent 401s trigger only one refresh)
7
+ * - BroadcastChannel multi-tab synchronization
8
+ * - authFetch() wrapper for non-GraphQL requests
9
+ * - REST logout endpoint
10
+ */
11
+ export interface TokenManagerOptions {
12
+ /** GraphQL endpoint URL (e.g. https://api.cloudforgesoftware.com/api/graph/) */
13
+ baseUrl: string;
14
+ /** Called when a new access token is obtained via refresh or tab sync */
15
+ onTokenRefreshed?: (token: string) => void;
16
+ /** Called when auth is irrecoverable (refresh failed, reuse detected, etc.) */
17
+ onSessionExpired?: () => void | Promise<void>;
18
+ }
19
+ export declare class TokenManager {
20
+ private refreshPromise;
21
+ private channel;
22
+ private token;
23
+ private refreshUrl;
24
+ private logoutUrl;
25
+ private options;
26
+ /** Resolve handle to short-circuit an in-flight refresh when another tab broadcasts a token */
27
+ private _broadcastResolve;
28
+ /** AbortController to cancel an in-flight refresh fetch when another tab already refreshed */
29
+ private _refreshAbort;
30
+ constructor(options: TokenManagerOptions);
31
+ getToken(): string | null;
32
+ setToken(token: string): void;
33
+ /**
34
+ * Refresh the access token using the HttpOnly refresh cookie.
35
+ * Deduplicates concurrent calls - only one refresh request is ever in flight.
36
+ */
37
+ refresh(): Promise<string | null>;
38
+ private _doRefresh;
39
+ /**
40
+ * Revoke the refresh token server-side and clear local state.
41
+ */
42
+ logout(): Promise<void>;
43
+ /**
44
+ * Authenticated fetch with automatic token refresh on 401.
45
+ * Drop-in replacement for raw fetch() calls that need JWT auth.
46
+ */
47
+ authFetch(url: string, options?: RequestInit): Promise<Response>;
48
+ destroy(): void;
49
+ }
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ /**
3
+ * Central token lifecycle manager for CloudForge authentication.
4
+ *
5
+ * Handles:
6
+ * - Calling /api/auth/refresh/ with HttpOnly cookie credentials
7
+ * - Promise deduplication (concurrent 401s trigger only one refresh)
8
+ * - BroadcastChannel multi-tab synchronization
9
+ * - authFetch() wrapper for non-GraphQL requests
10
+ * - REST logout endpoint
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.TokenManager = void 0;
14
+ class TokenManager {
15
+ constructor(options) {
16
+ this.refreshPromise = null;
17
+ this.channel = null;
18
+ this.token = null;
19
+ /** Resolve handle to short-circuit an in-flight refresh when another tab broadcasts a token */
20
+ this._broadcastResolve = null;
21
+ /** AbortController to cancel an in-flight refresh fetch when another tab already refreshed */
22
+ this._refreshAbort = null;
23
+ this.options = options;
24
+ // Derive auth endpoints from GraphQL base URL
25
+ const apiBase = options.baseUrl.replace(/\/api\/graph\/?$/, '');
26
+ this.refreshUrl = `${apiBase}/api/auth/refresh/`;
27
+ this.logoutUrl = `${apiBase}/api/auth/logout/`;
28
+ // Multi-tab token synchronization
29
+ if (typeof BroadcastChannel !== 'undefined') {
30
+ try {
31
+ this.channel = new BroadcastChannel('cf_auth');
32
+ this.channel.onmessage = (event) => {
33
+ var _a, _b, _c, _d, _e, _f, _g, _h;
34
+ if (((_a = event.data) === null || _a === void 0 ? void 0 : _a.type) === 'TOKEN_REFRESHED' && event.data.token) {
35
+ this.token = event.data.token;
36
+ // Short-circuit any in-flight refresh: abort the HTTP request
37
+ // and resolve the racing promise so we use the broadcast token
38
+ (_b = this._refreshAbort) === null || _b === void 0 ? void 0 : _b.abort();
39
+ (_c = this._broadcastResolve) === null || _c === void 0 ? void 0 : _c.call(this, event.data.token);
40
+ (_e = (_d = this.options).onTokenRefreshed) === null || _e === void 0 ? void 0 : _e.call(_d, event.data.token);
41
+ }
42
+ else if (((_f = event.data) === null || _f === void 0 ? void 0 : _f.type) === 'LOGOUT') {
43
+ this.token = null;
44
+ (_h = (_g = this.options).onSessionExpired) === null || _h === void 0 ? void 0 : _h.call(_g);
45
+ }
46
+ };
47
+ }
48
+ catch (_a) {
49
+ // BroadcastChannel may throw in some environments (e.g. SSR)
50
+ }
51
+ }
52
+ }
53
+ getToken() {
54
+ return this.token;
55
+ }
56
+ setToken(token) {
57
+ this.token = token;
58
+ }
59
+ /**
60
+ * Refresh the access token using the HttpOnly refresh cookie.
61
+ * Deduplicates concurrent calls - only one refresh request is ever in flight.
62
+ */
63
+ async refresh() {
64
+ if (this.refreshPromise)
65
+ return this.refreshPromise;
66
+ // Race the actual HTTP refresh against a broadcast from another tab.
67
+ // If another tab refreshes first, its broadcast resolves the race
68
+ // immediately and aborts our in-flight fetch.
69
+ const broadcastPromise = new Promise((resolve) => {
70
+ this._broadcastResolve = resolve;
71
+ });
72
+ this.refreshPromise = Promise.race([
73
+ this._doRefresh(),
74
+ broadcastPromise,
75
+ ]);
76
+ try {
77
+ return await this.refreshPromise;
78
+ }
79
+ finally {
80
+ this.refreshPromise = null;
81
+ this._broadcastResolve = null;
82
+ }
83
+ }
84
+ async _doRefresh() {
85
+ var _a, _b, _c;
86
+ this._refreshAbort = new AbortController();
87
+ try {
88
+ const response = await fetch(this.refreshUrl, {
89
+ method: 'POST',
90
+ credentials: 'include',
91
+ signal: this._refreshAbort.signal,
92
+ });
93
+ if (!response.ok) {
94
+ console.error('[CloudForgeSDK] Token refresh failed:', response.status);
95
+ return null;
96
+ }
97
+ const data = await response.json();
98
+ const newToken = data.token;
99
+ if (!newToken || typeof newToken !== 'string') {
100
+ console.error('[CloudForgeSDK] Invalid token in refresh response');
101
+ return null;
102
+ }
103
+ this.token = newToken;
104
+ (_a = this.channel) === null || _a === void 0 ? void 0 : _a.postMessage({ type: 'TOKEN_REFRESHED', token: newToken });
105
+ (_c = (_b = this.options).onTokenRefreshed) === null || _c === void 0 ? void 0 : _c.call(_b, newToken);
106
+ return newToken;
107
+ }
108
+ catch (error) {
109
+ // Aborted because another tab already refreshed - not an error
110
+ if (error instanceof DOMException && error.name === 'AbortError') {
111
+ return this.token;
112
+ }
113
+ console.error('[CloudForgeSDK] Refresh error:', error);
114
+ return null;
115
+ }
116
+ finally {
117
+ this._refreshAbort = null;
118
+ }
119
+ }
120
+ /**
121
+ * Revoke the refresh token server-side and clear local state.
122
+ */
123
+ async logout() {
124
+ var _a, _b;
125
+ // Cancel any in-flight refresh so it can't re-set the token after logout
126
+ (_a = this._refreshAbort) === null || _a === void 0 ? void 0 : _a.abort();
127
+ this._refreshAbort = null;
128
+ this.refreshPromise = null;
129
+ this._broadcastResolve = null;
130
+ try {
131
+ await fetch(this.logoutUrl, {
132
+ method: 'POST',
133
+ credentials: 'include',
134
+ });
135
+ }
136
+ catch (_c) {
137
+ // Best-effort - clear local state regardless
138
+ }
139
+ this.token = null;
140
+ (_b = this.channel) === null || _b === void 0 ? void 0 : _b.postMessage({ type: 'LOGOUT' });
141
+ }
142
+ /**
143
+ * Authenticated fetch with automatic token refresh on 401.
144
+ * Drop-in replacement for raw fetch() calls that need JWT auth.
145
+ */
146
+ async authFetch(url, options = {}) {
147
+ var _a, _b;
148
+ const headers = new Headers(options.headers);
149
+ if (this.token) {
150
+ headers.set('authorization', `JWT ${this.token}`);
151
+ }
152
+ let response = await fetch(url, { ...options, headers });
153
+ if (response.status === 401) {
154
+ const newToken = await this.refresh();
155
+ if (!newToken) {
156
+ (_b = (_a = this.options).onSessionExpired) === null || _b === void 0 ? void 0 : _b.call(_a);
157
+ throw new Error('Session expired');
158
+ }
159
+ headers.set('authorization', `JWT ${newToken}`);
160
+ response = await fetch(url, { ...options, headers });
161
+ }
162
+ return response;
163
+ }
164
+ destroy() {
165
+ var _a, _b;
166
+ (_a = this._refreshAbort) === null || _a === void 0 ? void 0 : _a.abort();
167
+ this._refreshAbort = null;
168
+ this.refreshPromise = null;
169
+ this._broadcastResolve = null;
170
+ (_b = this.channel) === null || _b === void 0 ? void 0 : _b.close();
171
+ this.channel = null;
172
+ }
173
+ }
174
+ exports.TokenManager = TokenManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-service-sdk",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "type": "commonjs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",