@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 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/providers/telegram.ts
2766
- const TELEGRAM_AUTH_URL = "https://oauth.telegram.org/auth";
2767
- const telegramConfig = shared("provider.telegram.config", () => atom(null));
2768
- const telegramAuthUrl = shared("provider.telegram.authUrl", () => atom(null));
2769
- const telegramRedirectUri = (config) => config.redirectUri ?? window.location.origin;
2770
- const legacyAuthUrl = (config) => `${TELEGRAM_AUTH_URL}?bot_id=${encodeURIComponent(config.botId)}&origin=${encodeURIComponent(window.location.origin)}&return_to=${encodeURIComponent(window.location.origin + location.pathname)}`;
2771
- const oidcAuthUrl = async (config) => {
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: config.botId,
2774
- redirect_uri: telegramRedirectUri(config),
2780
+ client_id: request.clientId,
2781
+ redirect_uri: request.redirectUri,
2775
2782
  response_type: "code",
2776
- scope: "openid profile",
2777
- state: getOauthState("telegram"),
2778
- code_challenge: await pkceChallenge(getPkceVerifier("telegram")),
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 `${TELEGRAM_AUTH_URL}?${params.toString()}`;
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
- oidcAuthUrl(config).then((url) => telegramAuthUrl.set(url));
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 PKCE verifier and redirect URI the backend needs to redeem a Telegram authorization code. */
2801
- const telegramCodeExchange = () => {
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
- if (!config) return void 0;
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
- const OAUTH_PROVIDERS$1 = [
2829
- "google",
2830
- "apple",
2831
- "microsoft"
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
- if (urlParams.has("tgWebAppData")) return "tgWebAppData";
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
- * Gets the authentication token or code from the URL based on the provider
2849
- * @param provider The authentication provider
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, tgAuthResult, tgWebAppData.";
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 `id_token` / Telegram payload. `/auth/v1/providers/connect`
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, token, options) => {
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
- code_verifier: options?.codeVerifier,
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 ensureValidAccessToken = async (refresh) => {
3357
- if (accessToken.get() && !isTokenExpired(expireAt.get())) return;
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 refreshAccessToken(refresh);
3360
- } catch (refreshError) {
3361
- console.error("Token refresh failed in getToken:", refreshError);
3362
- removeTokens();
3363
- throw refreshError;
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
- const currentRefreshToken = refreshToken.get();
3373
- if (!currentRefreshToken) return void 0;
3374
- await ensureValidAccessToken(currentRefreshToken);
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 === "tgAuthResult" || provider === "tgWebAppData" ? "telegram" : 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, token) => {
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
- code_verifier: exchange?.codeVerifier,
3821
- redirect_uri: exchange?.redirectUri
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 (provider === "telegram" && hasSocialConnectAttempt(provider)) return void 0;
3949
- const token = getProviderToken(provider);
3950
- if (!token) return void 0;
3951
- await loginWithProviderToken(provider, token);
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 loginWithProviderToken = async (provider, token) => {
4101
+ const loginWithProviderCredential = async (provider, credential) => {
3954
4102
  try {
3955
- await exchangeProviderToken(provider, token);
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 token = getProviderToken(provider);
3966
- if (!token) return void 0;
4113
+ const credential = getProviderCredential(provider);
4114
+ if (!credential) return void 0;
3967
4115
  try {
3968
- if (hasSocialConnectAttempt(provider)) await connectSocialInternal(provider, token);
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: { token: refreshToken.get() },
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