cf-service-sdk 0.1.17 → 0.1.19

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;
@@ -318,6 +318,7 @@ export type AutomatedProspectingConfigObject = {
318
318
  };
319
319
  export type AutomatedProspectingContactObject = {
320
320
  __typename?: 'AutomatedProspectingContactObject';
321
+ companyId?: Maybe<Scalars['ID']['output']>;
321
322
  contactCompanyName?: Maybe<Scalars['String']['output']>;
322
323
  contactEmail?: Maybe<Scalars['String']['output']>;
323
324
  contactFirstName?: Maybe<Scalars['String']['output']>;
@@ -328,6 +329,16 @@ export type AutomatedProspectingContactObject = {
328
329
  createdAt?: Maybe<Scalars['DateTime']['output']>;
329
330
  hasReplied?: Maybe<Scalars['Boolean']['output']>;
330
331
  id?: Maybe<Scalars['ID']['output']>;
332
+ isCustomer?: Maybe<Scalars['Boolean']['output']>;
333
+ isProspect?: Maybe<Scalars['Boolean']['output']>;
334
+ lastContactedAt?: Maybe<Scalars['DateTime']['output']>;
335
+ lastContactedByFirstName?: Maybe<Scalars['String']['output']>;
336
+ lastContactedByLastName?: Maybe<Scalars['String']['output']>;
337
+ lastContactedViaActionId?: Maybe<Scalars['ID']['output']>;
338
+ lastContactedViaCampaignName?: Maybe<Scalars['String']['output']>;
339
+ ownerName?: Maybe<Scalars['String']['output']>;
340
+ phone?: Maybe<Scalars['String']['output']>;
341
+ recentlyContacted?: Maybe<Scalars['Boolean']['output']>;
331
342
  };
332
343
  export type AutomatedProspectingStatsObject = {
333
344
  __typename?: 'AutomatedProspectingStatsObject';
@@ -2451,7 +2462,6 @@ export type Login = {
2451
2462
  id?: Maybe<Scalars['ID']['output']>;
2452
2463
  isAdmin?: Maybe<Scalars['Boolean']['output']>;
2453
2464
  payload?: Maybe<Scalars['JSONString']['output']>;
2454
- refreshToken?: Maybe<Scalars['String']['output']>;
2455
2465
  token?: Maybe<Scalars['String']['output']>;
2456
2466
  };
2457
2467
  /** LoginWithGoogle - Login with Google */
@@ -5697,6 +5707,17 @@ export type AddContactToAutomatedProspectingMutation = {
5697
5707
  contactEmail?: string | null;
5698
5708
  contactTitle?: string | null;
5699
5709
  contactCompanyName?: string | null;
5710
+ companyId?: string | null;
5711
+ ownerName?: string | null;
5712
+ isCustomer?: boolean | null;
5713
+ isProspect?: boolean | null;
5714
+ phone?: string | null;
5715
+ lastContactedAt?: any | null;
5716
+ lastContactedByFirstName?: string | null;
5717
+ lastContactedByLastName?: string | null;
5718
+ lastContactedViaCampaignName?: string | null;
5719
+ lastContactedViaActionId?: string | null;
5720
+ recentlyContacted?: boolean | null;
5700
5721
  hasReplied?: boolean | null;
5701
5722
  createdAt?: any | null;
5702
5723
  } | null;
@@ -6659,6 +6680,17 @@ export type ComposeAutomatedProspectingEmailMutation = {
6659
6680
  contactEmail?: string | null;
6660
6681
  contactTitle?: string | null;
6661
6682
  contactCompanyName?: string | null;
6683
+ companyId?: string | null;
6684
+ ownerName?: string | null;
6685
+ isCustomer?: boolean | null;
6686
+ isProspect?: boolean | null;
6687
+ phone?: string | null;
6688
+ lastContactedAt?: any | null;
6689
+ lastContactedByFirstName?: string | null;
6690
+ lastContactedByLastName?: string | null;
6691
+ lastContactedViaCampaignName?: string | null;
6692
+ lastContactedViaActionId?: string | null;
6693
+ recentlyContacted?: boolean | null;
6662
6694
  hasReplied?: boolean | null;
6663
6695
  createdAt?: any | null;
6664
6696
  } | null> | null;
@@ -10429,6 +10461,17 @@ export type LaunchAutomatedProspectingMutation = {
10429
10461
  contactEmail?: string | null;
10430
10462
  contactTitle?: string | null;
10431
10463
  contactCompanyName?: string | null;
10464
+ companyId?: string | null;
10465
+ ownerName?: string | null;
10466
+ isCustomer?: boolean | null;
10467
+ isProspect?: boolean | null;
10468
+ phone?: string | null;
10469
+ lastContactedAt?: any | null;
10470
+ lastContactedByFirstName?: string | null;
10471
+ lastContactedByLastName?: string | null;
10472
+ lastContactedViaCampaignName?: string | null;
10473
+ lastContactedViaActionId?: string | null;
10474
+ recentlyContacted?: boolean | null;
10432
10475
  hasReplied?: boolean | null;
10433
10476
  createdAt?: any | null;
10434
10477
  } | null> | null;
@@ -10449,7 +10492,6 @@ export type LoginMutation = {
10449
10492
  isAdmin?: boolean | null;
10450
10493
  accountId?: string | null;
10451
10494
  payload?: any | null;
10452
- refreshToken?: string | null;
10453
10495
  account?: {
10454
10496
  __typename?: 'AccountType';
10455
10497
  id?: string | null;
@@ -12802,6 +12844,17 @@ export type UpdateAutomatedProspectingCampaignMutation = {
12802
12844
  contactEmail?: string | null;
12803
12845
  contactTitle?: string | null;
12804
12846
  contactCompanyName?: string | null;
12847
+ companyId?: string | null;
12848
+ ownerName?: string | null;
12849
+ isCustomer?: boolean | null;
12850
+ isProspect?: boolean | null;
12851
+ phone?: string | null;
12852
+ lastContactedAt?: any | null;
12853
+ lastContactedByFirstName?: string | null;
12854
+ lastContactedByLastName?: string | null;
12855
+ lastContactedViaCampaignName?: string | null;
12856
+ lastContactedViaActionId?: string | null;
12857
+ recentlyContacted?: boolean | null;
12805
12858
  hasReplied?: boolean | null;
12806
12859
  createdAt?: any | null;
12807
12860
  } | null> | null;
@@ -16728,6 +16781,17 @@ export type AutomatedProspectingCampaignQuery = {
16728
16781
  contactEmail?: string | null;
16729
16782
  contactTitle?: string | null;
16730
16783
  contactCompanyName?: string | null;
16784
+ companyId?: string | null;
16785
+ ownerName?: string | null;
16786
+ isCustomer?: boolean | null;
16787
+ isProspect?: boolean | null;
16788
+ phone?: string | null;
16789
+ lastContactedAt?: any | null;
16790
+ lastContactedByFirstName?: string | null;
16791
+ lastContactedByLastName?: string | null;
16792
+ lastContactedViaCampaignName?: string | null;
16793
+ lastContactedViaActionId?: string | null;
16794
+ recentlyContacted?: boolean | null;
16731
16795
  hasReplied?: boolean | null;
16732
16796
  createdAt?: any | null;
16733
16797
  } | null> | null;
@@ -16779,6 +16843,17 @@ export type AutomatedProspectingCampaignsQuery = {
16779
16843
  contactEmail?: string | null;
16780
16844
  contactTitle?: string | null;
16781
16845
  contactCompanyName?: string | null;
16846
+ companyId?: string | null;
16847
+ ownerName?: string | null;
16848
+ isCustomer?: boolean | null;
16849
+ isProspect?: boolean | null;
16850
+ phone?: string | null;
16851
+ lastContactedAt?: any | null;
16852
+ lastContactedByFirstName?: string | null;
16853
+ lastContactedByLastName?: string | null;
16854
+ lastContactedViaCampaignName?: string | null;
16855
+ lastContactedViaActionId?: string | null;
16856
+ recentlyContacted?: boolean | null;
16782
16857
  hasReplied?: boolean | null;
16783
16858
  createdAt?: any | null;
16784
16859
  } | null> | null;
@@ -783,6 +783,17 @@ exports.AddContactToAutomatedProspectingDocument = (0, client_1.gql) `
783
783
  contactEmail
784
784
  contactTitle
785
785
  contactCompanyName
786
+ companyId
787
+ ownerName
788
+ isCustomer
789
+ isProspect
790
+ phone
791
+ lastContactedAt
792
+ lastContactedByFirstName
793
+ lastContactedByLastName
794
+ lastContactedViaCampaignName
795
+ lastContactedViaActionId
796
+ recentlyContacted
786
797
  hasReplied
787
798
  createdAt
788
799
  }
@@ -2057,6 +2068,17 @@ exports.ComposeAutomatedProspectingEmailDocument = (0, client_1.gql) `
2057
2068
  contactEmail
2058
2069
  contactTitle
2059
2070
  contactCompanyName
2071
+ companyId
2072
+ ownerName
2073
+ isCustomer
2074
+ isProspect
2075
+ phone
2076
+ lastContactedAt
2077
+ lastContactedByFirstName
2078
+ lastContactedByLastName
2079
+ lastContactedViaCampaignName
2080
+ lastContactedViaActionId
2081
+ recentlyContacted
2060
2082
  hasReplied
2061
2083
  createdAt
2062
2084
  }
@@ -6376,6 +6398,17 @@ exports.LaunchAutomatedProspectingDocument = (0, client_1.gql) `
6376
6398
  contactEmail
6377
6399
  contactTitle
6378
6400
  contactCompanyName
6401
+ companyId
6402
+ ownerName
6403
+ isCustomer
6404
+ isProspect
6405
+ phone
6406
+ lastContactedAt
6407
+ lastContactedByFirstName
6408
+ lastContactedByLastName
6409
+ lastContactedViaCampaignName
6410
+ lastContactedViaActionId
6411
+ recentlyContacted
6379
6412
  hasReplied
6380
6413
  createdAt
6381
6414
  }
@@ -6427,7 +6460,6 @@ exports.LoginDocument = (0, client_1.gql) `
6427
6460
  isAdmin
6428
6461
  accountId
6429
6462
  payload
6430
- refreshToken
6431
6463
  }
6432
6464
  }
6433
6465
  `;
@@ -9483,6 +9515,17 @@ exports.UpdateAutomatedProspectingCampaignDocument = (0, client_1.gql) `
9483
9515
  contactEmail
9484
9516
  contactTitle
9485
9517
  contactCompanyName
9518
+ companyId
9519
+ ownerName
9520
+ isCustomer
9521
+ isProspect
9522
+ phone
9523
+ lastContactedAt
9524
+ lastContactedByFirstName
9525
+ lastContactedByLastName
9526
+ lastContactedViaCampaignName
9527
+ lastContactedViaActionId
9528
+ recentlyContacted
9486
9529
  hasReplied
9487
9530
  createdAt
9488
9531
  }
@@ -13711,6 +13754,17 @@ exports.AutomatedProspectingCampaignDocument = (0, client_1.gql) `
13711
13754
  contactEmail
13712
13755
  contactTitle
13713
13756
  contactCompanyName
13757
+ companyId
13758
+ ownerName
13759
+ isCustomer
13760
+ isProspect
13761
+ phone
13762
+ lastContactedAt
13763
+ lastContactedByFirstName
13764
+ lastContactedByLastName
13765
+ lastContactedViaCampaignName
13766
+ lastContactedViaActionId
13767
+ recentlyContacted
13714
13768
  hasReplied
13715
13769
  createdAt
13716
13770
  }
@@ -13786,6 +13840,17 @@ exports.AutomatedProspectingCampaignsDocument = (0, client_1.gql) `
13786
13840
  contactEmail
13787
13841
  contactTitle
13788
13842
  contactCompanyName
13843
+ companyId
13844
+ ownerName
13845
+ isCustomer
13846
+ isProspect
13847
+ phone
13848
+ lastContactedAt
13849
+ lastContactedByFirstName
13850
+ lastContactedByLastName
13851
+ lastContactedViaCampaignName
13852
+ lastContactedViaActionId
13853
+ recentlyContacted
13789
13854
  hasReplied
13790
13855
  createdAt
13791
13856
  }
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
@@ -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) `
@@ -9484,6 +9483,17 @@ mutation ComposeAutomatedProspectingEmail($configId: ID!) {
9484
9483
  contactEmail
9485
9484
  contactTitle
9486
9485
  contactCompanyName
9486
+ companyId
9487
+ ownerName
9488
+ isCustomer
9489
+ isProspect
9490
+ phone
9491
+ lastContactedAt
9492
+ lastContactedByFirstName
9493
+ lastContactedByLastName
9494
+ lastContactedViaCampaignName
9495
+ lastContactedViaActionId
9496
+ recentlyContacted
9487
9497
  hasReplied
9488
9498
  createdAt
9489
9499
  }
@@ -9528,6 +9538,17 @@ mutation LaunchAutomatedProspecting($campaignId: ID!, $scheduledFor: DateTime) {
9528
9538
  contactEmail
9529
9539
  contactTitle
9530
9540
  contactCompanyName
9541
+ companyId
9542
+ ownerName
9543
+ isCustomer
9544
+ isProspect
9545
+ phone
9546
+ lastContactedAt
9547
+ lastContactedByFirstName
9548
+ lastContactedByLastName
9549
+ lastContactedViaCampaignName
9550
+ lastContactedViaActionId
9551
+ recentlyContacted
9531
9552
  hasReplied
9532
9553
  createdAt
9533
9554
  }
@@ -9559,6 +9580,17 @@ mutation AddContactToAutomatedProspecting($campaignId: ID!, $contactId: ID!) {
9559
9580
  contactEmail
9560
9581
  contactTitle
9561
9582
  contactCompanyName
9583
+ companyId
9584
+ ownerName
9585
+ isCustomer
9586
+ isProspect
9587
+ phone
9588
+ lastContactedAt
9589
+ lastContactedByFirstName
9590
+ lastContactedByLastName
9591
+ lastContactedViaCampaignName
9592
+ lastContactedViaActionId
9593
+ recentlyContacted
9562
9594
  hasReplied
9563
9595
  createdAt
9564
9596
  }
@@ -9605,6 +9637,17 @@ mutation UpdateAutomatedProspectingCampaign($attachments: String, $campaignId: I
9605
9637
  contactEmail
9606
9638
  contactTitle
9607
9639
  contactCompanyName
9640
+ companyId
9641
+ ownerName
9642
+ isCustomer
9643
+ isProspect
9644
+ phone
9645
+ lastContactedAt
9646
+ lastContactedByFirstName
9647
+ lastContactedByLastName
9648
+ lastContactedViaCampaignName
9649
+ lastContactedViaActionId
9650
+ recentlyContacted
9608
9651
  hasReplied
9609
9652
  createdAt
9610
9653
  }
package/dist/queries.js CHANGED
@@ -8938,6 +8938,17 @@ query AutomatedProspectingCampaigns($accountId: ID!, $status: String, $page: Int
8938
8938
  contactEmail
8939
8939
  contactTitle
8940
8940
  contactCompanyName
8941
+ companyId
8942
+ ownerName
8943
+ isCustomer
8944
+ isProspect
8945
+ phone
8946
+ lastContactedAt
8947
+ lastContactedByFirstName
8948
+ lastContactedByLastName
8949
+ lastContactedViaCampaignName
8950
+ lastContactedViaActionId
8951
+ recentlyContacted
8941
8952
  hasReplied
8942
8953
  createdAt
8943
8954
  }
@@ -8980,6 +8991,17 @@ query AutomatedProspectingCampaign($campaignId: ID!) {
8980
8991
  contactEmail
8981
8992
  contactTitle
8982
8993
  contactCompanyName
8994
+ companyId
8995
+ ownerName
8996
+ isCustomer
8997
+ isProspect
8998
+ phone
8999
+ lastContactedAt
9000
+ lastContactedByFirstName
9001
+ lastContactedByLastName
9002
+ lastContactedViaCampaignName
9003
+ lastContactedViaActionId
9004
+ recentlyContacted
8983
9005
  hasReplied
8984
9006
  createdAt
8985
9007
  }
@@ -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.17",
3
+ "version": "0.1.19",
4
4
  "type": "commonjs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",