@rixl/sdk 0.36.0 → 0.37.0
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 +1 -5
- package/dist/index.js +253 -99
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -14704,12 +14704,8 @@ interface AppleProviderConfig extends OAuthProviderConfig {}
|
|
|
14704
14704
|
//#endregion
|
|
14705
14705
|
//#region src/auth/providers/telegram.d.ts
|
|
14706
14706
|
interface TelegramProviderConfig {
|
|
14707
|
+
/** The bot ID, which is Telegram's OpenID Connect client ID. */
|
|
14707
14708
|
botId: string;
|
|
14708
|
-
/**
|
|
14709
|
-
* Sign in with Telegram's OpenID Connect authorization code flow instead of
|
|
14710
|
-
* the legacy widget redirect. The bot ID is the OIDC client ID.
|
|
14711
|
-
*/
|
|
14712
|
-
oidc?: boolean;
|
|
14713
14709
|
/** Must be registered in BotFather. Defaults to the page origin. */
|
|
14714
14710
|
redirectUri?: string;
|
|
14715
14711
|
}
|
package/dist/index.js
CHANGED
|
@@ -2752,6 +2752,10 @@ function getPkceVerifier(provider) {
|
|
|
2752
2752
|
sessionStorage.setItem(storagePath(provider), verifier);
|
|
2753
2753
|
return verifier;
|
|
2754
2754
|
}
|
|
2755
|
+
/** The stored verifier, without minting one: a callback can only redeem the code it was issued for. */
|
|
2756
|
+
function readPkceVerifier(provider) {
|
|
2757
|
+
return sessionStorage.getItem(storagePath(provider));
|
|
2758
|
+
}
|
|
2755
2759
|
function clearPkceVerifier(provider) {
|
|
2756
2760
|
sessionStorage.removeItem(storagePath(provider));
|
|
2757
2761
|
}
|
|
@@ -2762,58 +2766,81 @@ async function pkceChallenge(verifier) {
|
|
|
2762
2766
|
}
|
|
2763
2767
|
|
|
2764
2768
|
//#endregion
|
|
2765
|
-
//#region src/auth/
|
|
2766
|
-
const
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2769
|
+
//#region src/auth/url.ts
|
|
2770
|
+
const urlParams = new URLSearchParams(typeof window !== "undefined" ? fromURL(window.location.href) : "");
|
|
2771
|
+
function fromURL(urlString) {
|
|
2772
|
+
return urlString.replace(/^[^?#]*[?#]/, "").replace(/[?#]/g, "&");
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
//#endregion
|
|
2776
|
+
//#region src/auth/providers/authorization-code.ts
|
|
2777
|
+
/** Builds an authorization code request carrying the provider's state and a PKCE S256 challenge. */
|
|
2778
|
+
const authorizationCodeUrl = async (provider, request) => {
|
|
2772
2779
|
const params = new URLSearchParams({
|
|
2773
|
-
client_id:
|
|
2774
|
-
redirect_uri:
|
|
2780
|
+
client_id: request.clientId,
|
|
2781
|
+
redirect_uri: request.redirectUri,
|
|
2775
2782
|
response_type: "code",
|
|
2776
|
-
scope:
|
|
2777
|
-
state: getOauthState(
|
|
2778
|
-
code_challenge: await pkceChallenge(getPkceVerifier(
|
|
2783
|
+
scope: request.scope,
|
|
2784
|
+
state: getOauthState(provider),
|
|
2785
|
+
code_challenge: await pkceChallenge(getPkceVerifier(provider)),
|
|
2779
2786
|
code_challenge_method: "S256"
|
|
2780
2787
|
});
|
|
2781
|
-
return `${
|
|
2788
|
+
return `${request.authUrl}?${params.toString()}`;
|
|
2782
2789
|
};
|
|
2790
|
+
/** Whether the URL holds an authorization code answering this provider's state. */
|
|
2791
|
+
const hasAuthorizationCode = (provider) => {
|
|
2792
|
+
const state = urlParams.get("state");
|
|
2793
|
+
return urlParams.has("code") && !!state && validateOAuthState(provider, state);
|
|
2794
|
+
};
|
|
2795
|
+
/** The authorization code in the URL, with the verifier and redirect URI it was requested with. */
|
|
2796
|
+
const readAuthorizationCode = (provider, redirectUri) => {
|
|
2797
|
+
const code = urlParams.get("code");
|
|
2798
|
+
const codeVerifier = readPkceVerifier(provider);
|
|
2799
|
+
if (!code || !codeVerifier || !hasAuthorizationCode(provider)) return void 0;
|
|
2800
|
+
return { authorizationCode: {
|
|
2801
|
+
code,
|
|
2802
|
+
codeVerifier,
|
|
2803
|
+
redirectUri
|
|
2804
|
+
} };
|
|
2805
|
+
};
|
|
2806
|
+
|
|
2807
|
+
//#endregion
|
|
2808
|
+
//#region src/auth/providers/telegram.ts
|
|
2809
|
+
const TELEGRAM_AUTH_URL = "https://oauth.telegram.org/auth";
|
|
2810
|
+
const telegramConfig = shared("provider.telegram.config", () => atom(null));
|
|
2811
|
+
const telegramAuthUrl = shared("provider.telegram.authUrl", () => atom(null));
|
|
2812
|
+
const telegramRedirectUri = (config) => config.redirectUri ?? window.location.origin;
|
|
2813
|
+
const authUrl = (config) => authorizationCodeUrl("telegram", {
|
|
2814
|
+
authUrl: TELEGRAM_AUTH_URL,
|
|
2815
|
+
clientId: config.botId,
|
|
2816
|
+
redirectUri: telegramRedirectUri(config),
|
|
2817
|
+
scope: "openid profile"
|
|
2818
|
+
});
|
|
2783
2819
|
/**
|
|
2784
|
-
* Updates the Telegram authentication URL using the configured settings
|
|
2820
|
+
* Updates the Telegram authentication URL using the configured settings.
|
|
2821
|
+
* Resolves once the URL is set, so initClient() can finish with login() ready.
|
|
2785
2822
|
*/
|
|
2786
|
-
function updateTelegramAuthUrl() {
|
|
2823
|
+
async function updateTelegramAuthUrl() {
|
|
2787
2824
|
const config = telegramConfig.get();
|
|
2788
2825
|
if (!config) {
|
|
2789
2826
|
warnProviderNotConfigured("Telegram");
|
|
2790
2827
|
telegramAuthUrl.set(null);
|
|
2791
2828
|
return;
|
|
2792
2829
|
}
|
|
2793
|
-
if (!config.oidc) {
|
|
2794
|
-
telegramAuthUrl.set(legacyAuthUrl(config));
|
|
2795
|
-
return;
|
|
2796
|
-
}
|
|
2797
2830
|
telegramAuthUrl.set(null);
|
|
2798
|
-
|
|
2831
|
+
try {
|
|
2832
|
+
const url = await authUrl(config);
|
|
2833
|
+
if (telegramConfig.get() === config) telegramAuthUrl.set(url);
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
console.error("Telegram sign-in is unavailable: the PKCE challenge needs a secure (https) page with session storage.", error);
|
|
2836
|
+
}
|
|
2799
2837
|
}
|
|
2800
|
-
/** The
|
|
2801
|
-
const
|
|
2838
|
+
/** The authorization code Telegram sent back, when this page is its OpenID Connect callback. */
|
|
2839
|
+
const readTelegramCode = () => {
|
|
2802
2840
|
const config = telegramConfig.get();
|
|
2803
|
-
|
|
2804
|
-
return {
|
|
2805
|
-
codeVerifier: getPkceVerifier("telegram"),
|
|
2806
|
-
redirectUri: telegramRedirectUri(config)
|
|
2807
|
-
};
|
|
2841
|
+
return config ? readAuthorizationCode("telegram", telegramRedirectUri(config)) : void 0;
|
|
2808
2842
|
};
|
|
2809
2843
|
|
|
2810
|
-
//#endregion
|
|
2811
|
-
//#region src/auth/url.ts
|
|
2812
|
-
const urlParams = new URLSearchParams(typeof window !== "undefined" ? fromURL(window.location.href) : "");
|
|
2813
|
-
function fromURL(urlString) {
|
|
2814
|
-
return urlString.replace(/^[^?#]*[?#]/, "").replace(/[?#]/g, "&");
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
2844
|
//#endregion
|
|
2818
2845
|
//#region src/auth/providers/utils.ts
|
|
2819
2846
|
/**
|
|
@@ -2825,36 +2852,62 @@ function extractProviderFromState(state) {
|
|
|
2825
2852
|
const parts = state.split("_");
|
|
2826
2853
|
if (parts.length >= 1) return parts[0];
|
|
2827
2854
|
}
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2855
|
+
/** Telegram's mini app puts its init data in a parameter named after the provider. */
|
|
2856
|
+
const tokenParam = (provider) => ({
|
|
2857
|
+
provider,
|
|
2858
|
+
matches: () => urlParams.has(provider),
|
|
2859
|
+
read: () => {
|
|
2860
|
+
const token = urlParams.get(provider);
|
|
2861
|
+
return token ? { token } : void 0;
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
/** OpenID Connect providers that return an id_token with the state issued for them. */
|
|
2865
|
+
const idTokenWithState = (provider) => ({
|
|
2866
|
+
provider,
|
|
2867
|
+
matches: () => {
|
|
2868
|
+
const state = urlParams.get("state");
|
|
2869
|
+
if (!urlParams.get("id_token") || !state) return false;
|
|
2870
|
+
return extractProviderFromState(state) === provider && validateOAuthState(provider, state);
|
|
2871
|
+
},
|
|
2872
|
+
read: () => {
|
|
2873
|
+
const token = urlParams.get("id_token");
|
|
2874
|
+
return token ? { token } : void 0;
|
|
2875
|
+
}
|
|
2876
|
+
});
|
|
2877
|
+
const telegramCode = {
|
|
2878
|
+
provider: "telegram",
|
|
2879
|
+
matches: () => hasAuthorizationCode("telegram"),
|
|
2880
|
+
read: readTelegramCode
|
|
2881
|
+
};
|
|
2882
|
+
const CALLBACK_READERS = [
|
|
2883
|
+
tokenParam("tgWebAppData"),
|
|
2884
|
+
telegramCode,
|
|
2885
|
+
idTokenWithState("google"),
|
|
2886
|
+
idTokenWithState("apple"),
|
|
2887
|
+
idTokenWithState("microsoft")
|
|
2832
2888
|
];
|
|
2833
|
-
/**
|
|
2834
|
-
* Detects the authentication provider from URL parameters
|
|
2835
|
-
* @returns The detected authentication provider or undefined if none detected
|
|
2836
|
-
*/
|
|
2889
|
+
/** The provider whose response this page holds, if any. */
|
|
2837
2890
|
function detectProvider() {
|
|
2838
|
-
|
|
2839
|
-
const state = urlParams.get("state");
|
|
2840
|
-
if (urlParams.has("code") && state && validateOAuthState("telegram", state)) return "telegram";
|
|
2841
|
-
if (urlParams.has("tgAuthResult")) return "tgAuthResult";
|
|
2842
|
-
if (urlParams.get("id_token") && state) {
|
|
2843
|
-
const providerFromState = extractProviderFromState(state);
|
|
2844
|
-
return OAUTH_PROVIDERS$1.find((p) => providerFromState === p && validateOAuthState(p, state));
|
|
2845
|
-
}
|
|
2891
|
+
return CALLBACK_READERS.find((reader) => reader.matches())?.provider;
|
|
2846
2892
|
}
|
|
2847
|
-
/**
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
* @returns The token or code, or undefined if not found
|
|
2851
|
-
*/
|
|
2852
|
-
function getProviderToken(provider) {
|
|
2853
|
-
if (provider === "tgWebAppData" || provider === "tgAuthResult") return urlParams.get(provider) || void 0;
|
|
2854
|
-
if (provider === "telegram") return urlParams.get("code") || void 0;
|
|
2855
|
-
if (provider === "google" || provider === "apple" || provider === "microsoft") return urlParams.get("id_token") || void 0;
|
|
2893
|
+
/** The credential a provider's response on this page carries. */
|
|
2894
|
+
function getProviderCredential(provider) {
|
|
2895
|
+
return CALLBACK_READERS.find((reader) => reader.provider === provider)?.read();
|
|
2856
2896
|
}
|
|
2857
2897
|
|
|
2898
|
+
//#endregion
|
|
2899
|
+
//#region src/auth/providers/credential.ts
|
|
2900
|
+
/** The credential as the sign-in and link requests carry it. */
|
|
2901
|
+
const credentialFields = (credential) => {
|
|
2902
|
+
if ("token" in credential) return { token: credential.token };
|
|
2903
|
+
const { code, codeVerifier, redirectUri } = credential.authorizationCode;
|
|
2904
|
+
return {
|
|
2905
|
+
token: code,
|
|
2906
|
+
code_verifier: codeVerifier,
|
|
2907
|
+
redirect_uri: redirectUri
|
|
2908
|
+
};
|
|
2909
|
+
};
|
|
2910
|
+
|
|
2858
2911
|
//#endregion
|
|
2859
2912
|
//#region src/auth/providers/callback.ts
|
|
2860
2913
|
/** Params that carry the credential itself — their presence marks a callback response. */
|
|
@@ -2862,7 +2915,6 @@ const CREDENTIAL_PARAMS = [
|
|
|
2862
2915
|
"id_token",
|
|
2863
2916
|
"code",
|
|
2864
2917
|
"access_token",
|
|
2865
|
-
"tgAuthResult",
|
|
2866
2918
|
"tgWebAppData"
|
|
2867
2919
|
];
|
|
2868
2920
|
/** Everything the providers append to the redirect URL, credential and bookkeeping alike. */
|
|
@@ -2972,7 +3024,7 @@ const claimsOf = (token) => {
|
|
|
2972
3024
|
}
|
|
2973
3025
|
};
|
|
2974
3026
|
const verdictFor = (error) => {
|
|
2975
|
-
if ((typeof error.data === "object" && error.data !== null ? error.data : void 0)?.error === "invalid token type") return "CLIENT: the gateway does not accept this token_type. Expected one of Bearer, google, apple, microsoft,
|
|
3027
|
+
if ((typeof error.data === "object" && error.data !== null ? error.data : void 0)?.error === "invalid token type") return "CLIENT: the gateway does not accept this token_type. Expected one of Bearer, google, apple, microsoft, telegram, tgWebAppData.";
|
|
2976
3028
|
if (error.status === HTTP_STATUS.UNAUTHORIZED) return "BACKEND or CONFIG: the gateway could not verify the credential. Compare `aud` above with the client ID the gateway is configured with, and check `expired`.";
|
|
2977
3029
|
if (error.status === HTTP_STATUS.CONFLICT) return "EXPECTED: this email already belongs to an account created with a different provider.";
|
|
2978
3030
|
if (error.status === HTTP_STATUS.BAD_REQUEST) return "BACKEND: the gateway rejected the request body. Read `response.body` for its reason.";
|
|
@@ -3044,17 +3096,17 @@ const logProviderExchangeFailure = (provider, credential, error) => {
|
|
|
3044
3096
|
*
|
|
3045
3097
|
* Both go to `/auth/v1/token`, which needs no session: `token_type` is the
|
|
3046
3098
|
* `AuthProvider` value and `refresh_token` carries the stored refresh token or
|
|
3047
|
-
* the provider's
|
|
3099
|
+
* the provider's token or authorization code. `/auth/v1/providers/connect`
|
|
3048
3100
|
* links a provider to an already signed-in user, so it cannot sign anyone in.
|
|
3049
3101
|
*/
|
|
3050
|
-
const refreshTokens = async (provider,
|
|
3102
|
+
const refreshTokens = async (provider, credential, options) => {
|
|
3103
|
+
const { token, ...codeExchange } = credentialFields(credential);
|
|
3051
3104
|
const body = {
|
|
3052
3105
|
token_type: provider,
|
|
3053
3106
|
refresh_token: token,
|
|
3054
3107
|
country_code: options?.countryCode,
|
|
3055
3108
|
origin: options?.origin,
|
|
3056
|
-
|
|
3057
|
-
redirect_uri: options?.redirectUri
|
|
3109
|
+
...codeExchange
|
|
3058
3110
|
};
|
|
3059
3111
|
const { data } = await authV1TokenServiceRefreshToken({
|
|
3060
3112
|
body,
|
|
@@ -3131,6 +3183,8 @@ const encodeValue = (value) => encodeURIComponent(value).replace(/%(2[346BF]|3[A
|
|
|
3131
3183
|
//#endregion
|
|
3132
3184
|
//#region src/auth/cookie/index.ts
|
|
3133
3185
|
const initVals = getAllCookiesStartWith(GLOBAL_PREFIX);
|
|
3186
|
+
/** The stored values as they are now, which other tabs may have rewritten since load. */
|
|
3187
|
+
const readStoreCookies = () => getAllCookiesStartWith(GLOBAL_PREFIX);
|
|
3134
3188
|
const setStoreCookie = (key, value) => {
|
|
3135
3189
|
const expires = /* @__PURE__ */ new Date();
|
|
3136
3190
|
expires.setTime(expires.getTime() + 30 * 24 * 60 * 60 * 1e3);
|
|
@@ -3325,6 +3379,22 @@ const isTokenExpired = (expireAt) => {
|
|
|
3325
3379
|
*/
|
|
3326
3380
|
const isLimitedScopeTokenResponse = (result) => "requires_action" in result && Boolean(result.requires_action);
|
|
3327
3381
|
|
|
3382
|
+
//#endregion
|
|
3383
|
+
//#region src/auth/session-channel.ts
|
|
3384
|
+
const LOGOUT = "logout";
|
|
3385
|
+
const channel = shared("sessionChannel", () => typeof window === "undefined" || typeof BroadcastChannel === "undefined" ? null : new BroadcastChannel(`${GLOBAL_PREFIX}_session`));
|
|
3386
|
+
/** Tells the other tabs on this origin that the user logged out. */
|
|
3387
|
+
const announceLogout = () => channel?.postMessage(LOGOUT);
|
|
3388
|
+
/**
|
|
3389
|
+
* Runs `handler` when another tab logs out. The cookie alone cannot say so:
|
|
3390
|
+
* a missing cookie also means the browser blocked it, as in a third-party iframe.
|
|
3391
|
+
*/
|
|
3392
|
+
const onLogoutElsewhere = (handler) => {
|
|
3393
|
+
channel?.addEventListener("message", (event) => {
|
|
3394
|
+
if (event.data === LOGOUT) handler();
|
|
3395
|
+
});
|
|
3396
|
+
};
|
|
3397
|
+
|
|
3328
3398
|
//#endregion
|
|
3329
3399
|
//#region src/auth/authStore.ts
|
|
3330
3400
|
const isLogged = shared("isLogged", () => atom(initVals["isLogged"] === "true"));
|
|
@@ -3350,28 +3420,83 @@ const login = async (provider) => {
|
|
|
3350
3420
|
else throw new Error(`${provider} provider is not configured. Please check your initClient configuration.`);
|
|
3351
3421
|
};
|
|
3352
3422
|
const refreshAccessToken = async (refresh) => {
|
|
3353
|
-
const result = await refreshTokens("Bearer", refresh);
|
|
3423
|
+
const result = await refreshTokens("Bearer", { token: refresh });
|
|
3354
3424
|
if (!isLimitedScopeTokenResponse(result)) setTokens(result.access_token, result.refresh_token, result.expires_in);
|
|
3355
3425
|
};
|
|
3356
|
-
const
|
|
3357
|
-
|
|
3426
|
+
const hasValidAccessToken = () => !!accessToken.get() && !isTokenExpired(expireAt.get());
|
|
3427
|
+
/**
|
|
3428
|
+
* Takes up the session another tab stored after rotating the refresh token.
|
|
3429
|
+
* Each tab holds the tokens it read at load, and the gateway accepts a refresh
|
|
3430
|
+
* token only once, so presenting the stale copy would be rejected and the
|
|
3431
|
+
* resulting logout would clear the cookies every tab shares.
|
|
3432
|
+
* @returns Whether a newer session was adopted
|
|
3433
|
+
*/
|
|
3434
|
+
const adoptStoredSession = () => {
|
|
3435
|
+
const stored = readStoreCookies();
|
|
3436
|
+
const storedRefresh = stored["refreshToken"];
|
|
3437
|
+
if (!storedRefresh || storedRefresh === refreshToken.get()) return false;
|
|
3438
|
+
const storedAccess = stored["accessToken"] ?? "";
|
|
3439
|
+
if (scopeOf(storedAccess) !== scopeOf(accessToken.get())) clearPermissions();
|
|
3440
|
+
accessToken.set(storedAccess);
|
|
3441
|
+
refreshToken.set(storedRefresh);
|
|
3442
|
+
expireAt.set(Number(stored["expireAt"]));
|
|
3443
|
+
isLogged.set(true);
|
|
3444
|
+
return true;
|
|
3445
|
+
};
|
|
3446
|
+
/** The user and organization a token's permissions apply to. */
|
|
3447
|
+
const scopeOf = (token) => {
|
|
3448
|
+
const claims = token ? decodeToken(token) : void 0;
|
|
3449
|
+
return claims ? `${claims.id}:${claims.org_id}` : "";
|
|
3450
|
+
};
|
|
3451
|
+
const REFRESH_LOCK = `${GLOBAL_PREFIX}_refresh`;
|
|
3452
|
+
const REFRESH_LOCK_WAIT_MS = 1e4;
|
|
3453
|
+
/** Runs the refresh under a lock held across tabs, where the browser has one. */
|
|
3454
|
+
const acrossTabs = async (task) => {
|
|
3455
|
+
if (typeof navigator === "undefined" || !navigator.locks) return task();
|
|
3456
|
+
let granted = false;
|
|
3358
3457
|
try {
|
|
3359
|
-
await
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3458
|
+
await navigator.locks.request(REFRESH_LOCK, { signal: AbortSignal.timeout(REFRESH_LOCK_WAIT_MS) }, () => {
|
|
3459
|
+
granted = true;
|
|
3460
|
+
return task();
|
|
3461
|
+
});
|
|
3462
|
+
} catch (error) {
|
|
3463
|
+
if (!granted && error instanceof DOMException && error.name === "TimeoutError") return task();
|
|
3464
|
+
throw error;
|
|
3364
3465
|
}
|
|
3365
3466
|
};
|
|
3467
|
+
const ensureValidAccessToken = async () => {
|
|
3468
|
+
if (hasValidAccessToken()) return;
|
|
3469
|
+
await acrossTabs(async () => {
|
|
3470
|
+
adoptStoredSession();
|
|
3471
|
+
if (hasValidAccessToken()) return;
|
|
3472
|
+
const refresh = refreshToken.get();
|
|
3473
|
+
if (!refresh) return;
|
|
3474
|
+
try {
|
|
3475
|
+
await refreshAccessToken(refresh);
|
|
3476
|
+
} catch (refreshError) {
|
|
3477
|
+
console.error("Token refresh failed in getToken:", refreshError);
|
|
3478
|
+
if (adoptStoredSession()) return;
|
|
3479
|
+
if (isRejectedRefresh(refreshError)) removeTokens();
|
|
3480
|
+
throw refreshError;
|
|
3481
|
+
}
|
|
3482
|
+
});
|
|
3483
|
+
};
|
|
3484
|
+
/**
|
|
3485
|
+
* Whether the gateway refused the refresh token itself, which ends the session.
|
|
3486
|
+
* It answers 400 for an expired, invalid or already-used token and 401 for an
|
|
3487
|
+
* inactive account. A network failure, 429 or 5xx means the token could not be
|
|
3488
|
+
* checked, so the session is kept for the next getToken() to retry.
|
|
3489
|
+
*/
|
|
3490
|
+
const isRejectedRefresh = (error) => error instanceof ApiError && (error.status === HTTP_STATUS.BAD_REQUEST || error.status === HTTP_STATUS.UNAUTHORIZED || error.status === HTTP_STATUS.FORBIDDEN);
|
|
3366
3491
|
const getToken = async () => {
|
|
3367
3492
|
if (inFlight.promise) return inFlight.promise;
|
|
3368
3493
|
inFlight.promise = (async () => {
|
|
3369
3494
|
try {
|
|
3370
3495
|
await initDeferred.promise;
|
|
3371
3496
|
if (requiresAction.get()) return void 0;
|
|
3372
|
-
|
|
3373
|
-
if (!
|
|
3374
|
-
await ensureValidAccessToken(
|
|
3497
|
+
adoptStoredSession();
|
|
3498
|
+
if (!refreshToken.get()) return void 0;
|
|
3499
|
+
await ensureValidAccessToken();
|
|
3375
3500
|
const token = accessToken.get();
|
|
3376
3501
|
if (token) decodeAndSetUser(token);
|
|
3377
3502
|
return token;
|
|
@@ -3445,6 +3570,7 @@ refreshToken.subscribe((value) => setStoreCookie("refreshToken", value));
|
|
|
3445
3570
|
expireAt.subscribe((value) => setStoreCookie("expireAt", value));
|
|
3446
3571
|
requiresAction.subscribe((value) => setStoreCookie("requiresAction", value));
|
|
3447
3572
|
limitedAccessToken.subscribe((value) => setStoreCookie("limitedAccessToken", value));
|
|
3573
|
+
onLogoutElsewhere(removeTokens);
|
|
3448
3574
|
|
|
3449
3575
|
//#endregion
|
|
3450
3576
|
//#region src/auth/api-url.ts
|
|
@@ -3574,6 +3700,16 @@ const publicRoutes = [
|
|
|
3574
3700
|
path: "/platform/auth/v1/refresh"
|
|
3575
3701
|
}
|
|
3576
3702
|
];
|
|
3703
|
+
/**
|
|
3704
|
+
* The request's path below the configured API URL, which may carry its own path
|
|
3705
|
+
* (`https://example.com/api`). Matched against the full path instead, a public
|
|
3706
|
+
* route behind such a prefix would ask for a token, and the refresh request
|
|
3707
|
+
* would wait on the very getToken() call that sent it.
|
|
3708
|
+
*/
|
|
3709
|
+
function routePath(pathname) {
|
|
3710
|
+
const base = new URL(apiURL.get() || "/", "http://base").pathname.replace(/\/+$/, "");
|
|
3711
|
+
return base && pathname.startsWith(`${base}/`) ? pathname.slice(base.length) : pathname;
|
|
3712
|
+
}
|
|
3577
3713
|
function isPublicRoute(method, pathname) {
|
|
3578
3714
|
const match = method.toUpperCase();
|
|
3579
3715
|
return publicRoutes.some(({ method: m, path, prefix }) => match === m && (prefix ? pathname.startsWith(path) : pathname === path));
|
|
@@ -3589,7 +3725,7 @@ function configureSdkClient() {
|
|
|
3589
3725
|
client.interceptors.request.use(async (request) => {
|
|
3590
3726
|
if (request.headers.has("Authorization")) return request;
|
|
3591
3727
|
const { pathname } = new URL(request.url);
|
|
3592
|
-
if (isPublicRoute(request.method, pathname)) return request;
|
|
3728
|
+
if (isPublicRoute(request.method, routePath(pathname))) return request;
|
|
3593
3729
|
const token = await state.tokenResolver();
|
|
3594
3730
|
if (!token) throw new ApiError("No access token available for an authenticated request", HTTP_STATUS.UNAUTHORIZED, pathname);
|
|
3595
3731
|
request.headers.set("Authorization", `Bearer ${token}`);
|
|
@@ -3631,6 +3767,12 @@ function hasSocialConnectAttempt(provider) {
|
|
|
3631
3767
|
function clearSocialConnectAttempt(provider) {
|
|
3632
3768
|
sessionStorage.removeItem(socialStoragePath(provider));
|
|
3633
3769
|
}
|
|
3770
|
+
/** Clears every provider's pending connection flag, so none outlives the session it was set in. */
|
|
3771
|
+
function clearSocialConnectAttempts() {
|
|
3772
|
+
try {
|
|
3773
|
+
Array.from({ length: sessionStorage.length }, (_, i) => sessionStorage.key(i)).filter((key) => key?.startsWith(SOCIAL_CONNECT_KEY_PREFIX)).forEach((key) => sessionStorage.removeItem(key));
|
|
3774
|
+
} catch {}
|
|
3775
|
+
}
|
|
3634
3776
|
|
|
3635
3777
|
//#endregion
|
|
3636
3778
|
//#region src/auth/api/error-handlers.ts
|
|
@@ -3782,7 +3924,7 @@ const PROVIDER_FROM_PROTO = {
|
|
|
3782
3924
|
EXTERNAL_ACCOUNT_PROVIDER_FACEBOOK: "facebook",
|
|
3783
3925
|
EXTERNAL_ACCOUNT_PROVIDER_TELEGRAM: "telegram"
|
|
3784
3926
|
};
|
|
3785
|
-
const normalizeProviderType = (provider) => provider === "
|
|
3927
|
+
const normalizeProviderType = (provider) => provider === "tgWebAppData" ? "telegram" : provider;
|
|
3786
3928
|
const toProtoProvider = (provider) => {
|
|
3787
3929
|
const proto = PROVIDER_TO_PROTO[provider];
|
|
3788
3930
|
if (!proto) throw new Error(`Unknown provider: ${provider}`);
|
|
@@ -3805,20 +3947,20 @@ const listSocials = async () => {
|
|
|
3805
3947
|
});
|
|
3806
3948
|
}, { [HTTP_STATUS.UNAUTHORIZED]: () => /* @__PURE__ */ new Error("User is not authorized to list providers!") });
|
|
3807
3949
|
};
|
|
3808
|
-
const connectSocialInternal = async (provider,
|
|
3950
|
+
const connectSocialInternal = async (provider, credential) => {
|
|
3809
3951
|
return apiCall(async () => {
|
|
3952
|
+
const { token, ...codeExchange } = credentialFields(credential);
|
|
3810
3953
|
const requestBody = validateInput(ConnectProviderSchema, {
|
|
3811
3954
|
provider,
|
|
3812
3955
|
token
|
|
3813
3956
|
});
|
|
3814
3957
|
const sessionToken = await getToken();
|
|
3815
3958
|
const headers = sessionToken ? { Authorization: `Bearer ${sessionToken}` } : {};
|
|
3816
|
-
const exchange = provider === "telegram" ? telegramCodeExchange() : void 0;
|
|
3817
3959
|
const body = {
|
|
3818
3960
|
provider: toProtoProvider(normalizeProviderType(requestBody.provider)),
|
|
3819
3961
|
token: requestBody.token,
|
|
3820
|
-
|
|
3821
|
-
|
|
3962
|
+
origin: window.location.origin,
|
|
3963
|
+
...codeExchange
|
|
3822
3964
|
};
|
|
3823
3965
|
const { data } = await authV1ProvidersServiceConnectProvider({
|
|
3824
3966
|
body,
|
|
@@ -3894,7 +4036,7 @@ const initConfig = async (config) => {
|
|
|
3894
4036
|
}
|
|
3895
4037
|
if (config.telegramProvider) {
|
|
3896
4038
|
telegramConfig.set(config.telegramProvider);
|
|
3897
|
-
updateTelegramAuthUrl();
|
|
4039
|
+
await updateTelegramAuthUrl();
|
|
3898
4040
|
}
|
|
3899
4041
|
};
|
|
3900
4042
|
const applyTokenExchangeResult = (result) => {
|
|
@@ -3934,27 +4076,33 @@ const handleInitPageApiError = (error) => {
|
|
|
3934
4076
|
}
|
|
3935
4077
|
setAuthErrorAndClear("provider_exchange_failed", info.error || info.description, info.email);
|
|
3936
4078
|
};
|
|
3937
|
-
const exchangeProviderToken = async (provider, token) => {
|
|
3938
|
-
const options = provider === "telegram" ? telegramCodeExchange() : void 0;
|
|
3939
|
-
const result = await refreshTokens(provider, token, options);
|
|
3940
|
-
applyTokenExchangeResult(result);
|
|
3941
|
-
};
|
|
3942
4079
|
const initPage = async () => {
|
|
3943
4080
|
const provider = detectProvider();
|
|
3944
4081
|
if (!provider) {
|
|
3945
4082
|
if (hasProviderResponse()) logUnusableProviderResponse();
|
|
3946
4083
|
return;
|
|
3947
4084
|
}
|
|
3948
|
-
if (
|
|
3949
|
-
const
|
|
3950
|
-
if (!
|
|
3951
|
-
await
|
|
4085
|
+
if (isLinkAttempt(provider)) return void 0;
|
|
4086
|
+
const credential = getProviderCredential(provider);
|
|
4087
|
+
if (!credential) return void 0;
|
|
4088
|
+
await loginWithProviderCredential(provider, credential);
|
|
4089
|
+
};
|
|
4090
|
+
/**
|
|
4091
|
+
* Whether this callback answers a link rather than a sign-in. Only a signed-in
|
|
4092
|
+
* user can link, so a flag left by an abandoned link is dropped instead of
|
|
4093
|
+
* swallowing the sign-in that follows it.
|
|
4094
|
+
*/
|
|
4095
|
+
const isLinkAttempt = (provider) => {
|
|
4096
|
+
if (!hasSocialConnectAttempt(provider)) return false;
|
|
4097
|
+
if (refreshToken.get()) return true;
|
|
4098
|
+
clearSocialConnectAttempt(provider);
|
|
4099
|
+
return false;
|
|
3952
4100
|
};
|
|
3953
|
-
const
|
|
4101
|
+
const loginWithProviderCredential = async (provider, credential) => {
|
|
3954
4102
|
try {
|
|
3955
|
-
await
|
|
4103
|
+
applyTokenExchangeResult(await refreshTokens(provider, credential, { origin: window.location.origin }));
|
|
3956
4104
|
} catch (error) {
|
|
3957
|
-
logProviderExchangeFailure(provider, token, error);
|
|
4105
|
+
logProviderExchangeFailure(provider, credentialFields(credential).token, error);
|
|
3958
4106
|
if (!(error instanceof ApiError)) throw error;
|
|
3959
4107
|
handleInitPageApiError(error);
|
|
3960
4108
|
}
|
|
@@ -3962,10 +4110,10 @@ const loginWithProviderToken = async (provider, token) => {
|
|
|
3962
4110
|
const initSocials = async () => {
|
|
3963
4111
|
const provider = detectProvider();
|
|
3964
4112
|
if (!provider) return void 0;
|
|
3965
|
-
const
|
|
3966
|
-
if (!
|
|
4113
|
+
const credential = getProviderCredential(provider);
|
|
4114
|
+
if (!credential) return void 0;
|
|
3967
4115
|
try {
|
|
3968
|
-
if (hasSocialConnectAttempt(provider)) await connectSocialInternal(provider,
|
|
4116
|
+
if (hasSocialConnectAttempt(provider)) await connectSocialInternal(provider, credential);
|
|
3969
4117
|
} finally {
|
|
3970
4118
|
clearSocialConnectAttempt(provider);
|
|
3971
4119
|
}
|
|
@@ -4056,12 +4204,18 @@ const connect = async (config) => {
|
|
|
4056
4204
|
//#region src/auth/auth/logout.ts
|
|
4057
4205
|
const logout = async () => {
|
|
4058
4206
|
try {
|
|
4207
|
+
const body = {
|
|
4208
|
+
token: accessToken.get(),
|
|
4209
|
+
refresh_token: refreshToken.get()
|
|
4210
|
+
};
|
|
4059
4211
|
await authV1TokenServiceLogout({
|
|
4060
|
-
body
|
|
4212
|
+
body,
|
|
4061
4213
|
throwOnError: true
|
|
4062
4214
|
});
|
|
4063
4215
|
} catch {} finally {
|
|
4064
4216
|
removeTokens();
|
|
4217
|
+
clearSocialConnectAttempts();
|
|
4218
|
+
announceLogout();
|
|
4065
4219
|
}
|
|
4066
4220
|
};
|
|
4067
4221
|
|