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 +33 -2
- package/dist/client.js +169 -192
- package/dist/generated/graphql.d.ts +77 -2
- package/dist/generated/graphql.js +66 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/mutations.js +44 -1
- package/dist/queries.js +22 -0
- package/dist/tokenManager.d.ts +49 -0
- package/dist/tokenManager.js +174 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
37
|
+
uri: this.options.baseUrl,
|
|
38
|
+
credentials: (_a = this.options.credentials) !== null && _a !== void 0 ? _a : 'include',
|
|
31
39
|
fetchOptions: {
|
|
32
|
-
cache: 'no-store',
|
|
40
|
+
cache: 'no-store',
|
|
33
41
|
},
|
|
34
42
|
});
|
|
35
|
-
// Auth link
|
|
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.
|
|
45
|
+
const token = this.tokenManager.getToken();
|
|
38
46
|
return {
|
|
39
47
|
headers: {
|
|
40
48
|
...headers,
|
|
41
|
-
|
|
49
|
+
...(token ? { authorization: `JWT ${token}` } : {}),
|
|
42
50
|
},
|
|
43
51
|
};
|
|
44
52
|
});
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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 (
|
|
201
|
-
|
|
131
|
+
catch (_h) {
|
|
132
|
+
// Ignore parse errors
|
|
202
133
|
}
|
|
203
134
|
}
|
|
204
|
-
(
|
|
135
|
+
(_g = (_f = this.options).onError) === null || _g === void 0 ? void 0 : _g.call(_f, networkError);
|
|
205
136
|
}
|
|
206
137
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
+
/** @deprecated Use tokenManager.refresh() instead */
|
|
243
220
|
async refreshToken() {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
259
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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
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;
|