squarefi-bff-api-module 1.36.17 → 1.36.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/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { squarefi_bff_api_client } from './api';
|
|
2
2
|
export * from './utils/apiClientFactory';
|
|
3
|
+
export { setAccessTokenProvider, setOnTwoFactorRequired, setOnUnauthorized, type AccessTokenProvider, type ResolveTokenOptions, type TwoFactorRequiredHandler, type UnauthorizedHandler, } from './utils/accessTokenProvider';
|
|
3
4
|
export * from './utils/tokensFactory';
|
|
4
5
|
export * from './utils/fileStorage';
|
|
5
6
|
export * from './constants';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { squarefi_bff_api_client } from './api';
|
|
2
2
|
export * from './utils/apiClientFactory';
|
|
3
|
+
export { setAccessTokenProvider, setOnTwoFactorRequired, setOnUnauthorized, } from './utils/accessTokenProvider';
|
|
3
4
|
export * from './utils/tokensFactory';
|
|
4
5
|
export * from './utils/fileStorage';
|
|
5
6
|
export * from './constants';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type ResolveTokenOptions = {
|
|
2
|
+
/** Force the provider to mint a fresh token, bypassing any cache (used on 401 retry). */
|
|
3
|
+
forceRefresh?: boolean;
|
|
4
|
+
};
|
|
5
|
+
export type AccessTokenProvider = (options?: ResolveTokenOptions) => Promise<string | null | undefined>;
|
|
6
|
+
export type UnauthorizedHandler = () => void;
|
|
7
|
+
export type TwoFactorRequiredHandler = () => void;
|
|
8
|
+
/**
|
|
9
|
+
* Register an external source of the access token (e.g. Clerk `getToken`).
|
|
10
|
+
* When set, the API clients stop reading tokens from localStorage and delegate
|
|
11
|
+
* token acquisition (and refresh) to the provider. Pass `null` to fall back to
|
|
12
|
+
* the legacy localStorage flow.
|
|
13
|
+
*/
|
|
14
|
+
export declare const setAccessTokenProvider: (provider: AccessTokenProvider | null) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Register a callback invoked when a request is unauthorized and cannot be
|
|
17
|
+
* recovered by refreshing the token (e.g. to trigger a Clerk sign-out).
|
|
18
|
+
*/
|
|
19
|
+
export declare const setOnUnauthorized: (handler: UnauthorizedHandler | null) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Register a callback invoked when the backend rejects a request because
|
|
22
|
+
* two-factor authentication is required but not yet satisfied (HTTP 403 with a
|
|
23
|
+
* `two_factor_required` error code). The consumer typically redirects the user
|
|
24
|
+
* to the 2FA setup flow. Pass `null` to unregister.
|
|
25
|
+
*/
|
|
26
|
+
export declare const setOnTwoFactorRequired: (handler: TwoFactorRequiredHandler | null) => void;
|
|
27
|
+
/** Whether an external token provider is active (Clerk mode). */
|
|
28
|
+
export declare const isExternalAuthMode: () => boolean;
|
|
29
|
+
export declare const resolveAccessToken: (options?: ResolveTokenOptions) => Promise<string | null>;
|
|
30
|
+
export declare const triggerUnauthorized: () => void;
|
|
31
|
+
export declare const triggerTwoFactorRequired: () => void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getFromLocalStorage } from './storage';
|
|
2
|
+
let accessTokenProvider = null;
|
|
3
|
+
let unauthorizedHandler = null;
|
|
4
|
+
let twoFactorRequiredHandler = null;
|
|
5
|
+
let inFlightRefresh = null;
|
|
6
|
+
/**
|
|
7
|
+
* Register an external source of the access token (e.g. Clerk `getToken`).
|
|
8
|
+
* When set, the API clients stop reading tokens from localStorage and delegate
|
|
9
|
+
* token acquisition (and refresh) to the provider. Pass `null` to fall back to
|
|
10
|
+
* the legacy localStorage flow.
|
|
11
|
+
*/
|
|
12
|
+
export const setAccessTokenProvider = (provider) => {
|
|
13
|
+
accessTokenProvider = provider;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Register a callback invoked when a request is unauthorized and cannot be
|
|
17
|
+
* recovered by refreshing the token (e.g. to trigger a Clerk sign-out).
|
|
18
|
+
*/
|
|
19
|
+
export const setOnUnauthorized = (handler) => {
|
|
20
|
+
unauthorizedHandler = handler;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Register a callback invoked when the backend rejects a request because
|
|
24
|
+
* two-factor authentication is required but not yet satisfied (HTTP 403 with a
|
|
25
|
+
* `two_factor_required` error code). The consumer typically redirects the user
|
|
26
|
+
* to the 2FA setup flow. Pass `null` to unregister.
|
|
27
|
+
*/
|
|
28
|
+
export const setOnTwoFactorRequired = (handler) => {
|
|
29
|
+
twoFactorRequiredHandler = handler;
|
|
30
|
+
};
|
|
31
|
+
/** Whether an external token provider is active (Clerk mode). */
|
|
32
|
+
export const isExternalAuthMode = () => accessTokenProvider !== null;
|
|
33
|
+
export const resolveAccessToken = async (options) => {
|
|
34
|
+
if (accessTokenProvider) {
|
|
35
|
+
// Coalesce concurrent force-refreshes so a burst of 401s triggers a single provider refresh.
|
|
36
|
+
if (options?.forceRefresh) {
|
|
37
|
+
if (!inFlightRefresh) {
|
|
38
|
+
const provider = accessTokenProvider;
|
|
39
|
+
inFlightRefresh = Promise.resolve()
|
|
40
|
+
.then(() => provider(options))
|
|
41
|
+
.then((token) => token ?? null)
|
|
42
|
+
.catch(() => null)
|
|
43
|
+
.finally(() => {
|
|
44
|
+
inFlightRefresh = null;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return inFlightRefresh;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return (await accessTokenProvider(options)) ?? null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return getFromLocalStorage('access_token');
|
|
57
|
+
};
|
|
58
|
+
export const triggerUnauthorized = () => {
|
|
59
|
+
unauthorizedHandler?.();
|
|
60
|
+
};
|
|
61
|
+
export const triggerTwoFactorRequired = () => {
|
|
62
|
+
twoFactorRequiredHandler?.();
|
|
63
|
+
};
|
|
@@ -3,8 +3,11 @@ import { isTMA } from '@telegram-apps/sdk-react';
|
|
|
3
3
|
import axios from 'axios';
|
|
4
4
|
import { telegramSignUpPath, telegramSignInPath, refreshTokenPath } from '../api/auth';
|
|
5
5
|
import { AppEnviroment, ResponseStatus } from '../constants';
|
|
6
|
+
import { isExternalAuthMode, resolveAccessToken, triggerTwoFactorRequired, triggerUnauthorized, } from './accessTokenProvider';
|
|
6
7
|
import { deleteTokens, getTokens, refreshTokens } from './tokensFactory';
|
|
7
8
|
// eslint-disable-next-line no-constant-condition
|
|
9
|
+
// Backend signals that a still-unsatisfied 2FA requirement blocks the request with this error code.
|
|
10
|
+
const TWO_FACTOR_REQUIRED_ERROR_CODE = 'two_factor_required';
|
|
8
11
|
const apiV1BaseURL = process.env.API_URL ?? 'ENV variable API_URL is not defined';
|
|
9
12
|
const apiV2BaseURL = process.env.API_V2_URL ?? 'ENV variable API_V2_URL is not defined';
|
|
10
13
|
const apiTOTPBaseURL = process.env.API_TOTP_URL ?? 'ENV variable API_TOTP_URL is not defined';
|
|
@@ -17,8 +20,8 @@ export const createApiClient = ({ baseURL, isBearerToken, tenantId }) => {
|
|
|
17
20
|
baseURL,
|
|
18
21
|
timeout: 60000,
|
|
19
22
|
});
|
|
20
|
-
instance.interceptors.request.use((config) => {
|
|
21
|
-
const
|
|
23
|
+
instance.interceptors.request.use(async (config) => {
|
|
24
|
+
const access_token = await resolveAccessToken();
|
|
22
25
|
const modifiedHeaders = {
|
|
23
26
|
...config.headers,
|
|
24
27
|
'x-tenant-id': tenantId,
|
|
@@ -40,11 +43,35 @@ export const createApiClient = ({ baseURL, isBearerToken, tenantId }) => {
|
|
|
40
43
|
if (typeof window === 'undefined') {
|
|
41
44
|
return Promise.reject(error);
|
|
42
45
|
}
|
|
46
|
+
if (error?.response?.status === ResponseStatus.FORBIDDEN &&
|
|
47
|
+
error?.response?.data?.code === TWO_FACTOR_REQUIRED_ERROR_CODE) {
|
|
48
|
+
triggerTwoFactorRequired();
|
|
49
|
+
return Promise.reject(error);
|
|
50
|
+
}
|
|
43
51
|
if (error?.response?.status === ResponseStatus.UNAUTHORIZED &&
|
|
44
52
|
!error?.response?.config.context?.bypassUnauthorizedHandler) {
|
|
45
53
|
const { response, config: failedRequestConfig } = error;
|
|
46
|
-
const { refresh_token } = getTokens();
|
|
47
54
|
const isRetryRequest = failedRequestConfig.context?.isRetryRequest;
|
|
55
|
+
// External auth mode (e.g. Clerk): the provider owns token refresh. Force a fresh token and
|
|
56
|
+
// retry once; sign out if there is no fresh token or the retry still fails.
|
|
57
|
+
if (isExternalAuthMode()) {
|
|
58
|
+
if (isRetryRequest) {
|
|
59
|
+
triggerUnauthorized();
|
|
60
|
+
return Promise.reject(error);
|
|
61
|
+
}
|
|
62
|
+
return resolveAccessToken({ forceRefresh: true }).then((freshToken) => {
|
|
63
|
+
if (!freshToken) {
|
|
64
|
+
triggerUnauthorized();
|
|
65
|
+
return Promise.reject(error);
|
|
66
|
+
}
|
|
67
|
+
failedRequestConfig.context = {
|
|
68
|
+
...failedRequestConfig.context,
|
|
69
|
+
isRetryRequest: true,
|
|
70
|
+
};
|
|
71
|
+
return instance(failedRequestConfig);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const { refresh_token } = getTokens();
|
|
48
75
|
const isRefreshTokenRequest = failedRequestConfig.url?.includes(refreshTokenPath);
|
|
49
76
|
const isTelegramSignInRequest = failedRequestConfig.url?.includes(telegramSignInPath);
|
|
50
77
|
const isTelegramSignUpRequest = failedRequestConfig.url?.includes(telegramSignUpPath);
|
|
@@ -144,8 +171,8 @@ export const createFetchApiClient = ({ baseURL, isBearerToken, tenantId }) => {
|
|
|
144
171
|
});
|
|
145
172
|
return `${fullUrl}?${searchParams.toString()}`;
|
|
146
173
|
};
|
|
147
|
-
const getHeaders = (additionalHeaders) => {
|
|
148
|
-
const
|
|
174
|
+
const getHeaders = async (additionalHeaders) => {
|
|
175
|
+
const access_token = await resolveAccessToken();
|
|
149
176
|
const headers = {
|
|
150
177
|
'Content-Type': 'application/json',
|
|
151
178
|
'x-tenant-id': tenantId,
|
|
@@ -162,6 +189,9 @@ export const createFetchApiClient = ({ baseURL, isBearerToken, tenantId }) => {
|
|
|
162
189
|
let errorMessage = `HTTP error! status: ${response.status}`;
|
|
163
190
|
try {
|
|
164
191
|
const errorData = await response.json();
|
|
192
|
+
if (response.status === ResponseStatus.FORBIDDEN && errorData?.code === TWO_FACTOR_REQUIRED_ERROR_CODE) {
|
|
193
|
+
triggerTwoFactorRequired();
|
|
194
|
+
}
|
|
165
195
|
errorMessage = errorData.message || errorData.error || errorMessage;
|
|
166
196
|
}
|
|
167
197
|
catch {
|
|
@@ -186,7 +216,7 @@ export const createFetchApiClient = ({ baseURL, isBearerToken, tenantId }) => {
|
|
|
186
216
|
const fullUrl = buildUrl(url, config?.params);
|
|
187
217
|
const response = await fetch(fullUrl, {
|
|
188
218
|
method: 'GET',
|
|
189
|
-
headers: getHeaders(config?.headers),
|
|
219
|
+
headers: await getHeaders(config?.headers),
|
|
190
220
|
});
|
|
191
221
|
return handleResponse(response, config?.responseType);
|
|
192
222
|
};
|
|
@@ -194,7 +224,7 @@ export const createFetchApiClient = ({ baseURL, isBearerToken, tenantId }) => {
|
|
|
194
224
|
const fullUrl = buildUrl(url);
|
|
195
225
|
const response = await fetch(fullUrl, {
|
|
196
226
|
method: 'POST',
|
|
197
|
-
headers: getHeaders(config?.headers),
|
|
227
|
+
headers: await getHeaders(config?.headers),
|
|
198
228
|
body: config?.data ? JSON.stringify(config.data) : undefined,
|
|
199
229
|
});
|
|
200
230
|
return handleResponse(response, config?.responseType);
|
|
@@ -203,7 +233,7 @@ export const createFetchApiClient = ({ baseURL, isBearerToken, tenantId }) => {
|
|
|
203
233
|
const fullUrl = buildUrl(url);
|
|
204
234
|
const response = await fetch(fullUrl, {
|
|
205
235
|
method: 'PATCH',
|
|
206
|
-
headers: getHeaders(config?.headers),
|
|
236
|
+
headers: await getHeaders(config?.headers),
|
|
207
237
|
body: config?.data ? JSON.stringify(config.data) : undefined,
|
|
208
238
|
});
|
|
209
239
|
return handleResponse(response, config?.responseType);
|
|
@@ -212,7 +242,7 @@ export const createFetchApiClient = ({ baseURL, isBearerToken, tenantId }) => {
|
|
|
212
242
|
const fullUrl = buildUrl(url);
|
|
213
243
|
const response = await fetch(fullUrl, {
|
|
214
244
|
method: 'DELETE',
|
|
215
|
-
headers: getHeaders(config?.headers),
|
|
245
|
+
headers: await getHeaders(config?.headers),
|
|
216
246
|
body: config?.data ? JSON.stringify(config.data) : undefined,
|
|
217
247
|
});
|
|
218
248
|
return handleResponse(response, config?.responseType);
|