@feelflow/ffid-sdk 5.12.0 → 5.14.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.
@@ -122,6 +122,9 @@ function normalizeUserinfo(raw) {
122
122
  return {
123
123
  sub: raw.sub,
124
124
  email: raw.email,
125
+ // #3791: undefined → undefined を保つ(false に丸めない)。古い FFID は wire に
126
+ // 含めないため、consumer は undefined を「未確認」ではなく「不明」として扱える。
127
+ emailVerified: raw.email_verified,
125
128
  name: raw.name,
126
129
  picture: raw.picture,
127
130
  companyName: raw.company_name ?? null,
@@ -377,6 +380,9 @@ function createVerifyAccessToken(deps) {
377
380
  const base = {
378
381
  sub: introspectResponse.sub,
379
382
  email: introspectResponse.email ?? null,
383
+ // #3791: preserve undefined (do NOT coerce to false) so introspect-strategy
384
+ // callers can distinguish "verified" / "not verified" / "older server".
385
+ email_verified: introspectResponse.email_verified,
380
386
  name: introspectResponse.name ?? null,
381
387
  picture: introspectResponse.picture ?? null,
382
388
  company_name: introspectResponse.company_name ?? null,
@@ -931,8 +937,163 @@ function createProfileMethods(deps) {
931
937
  return { getProfile, updateProfile };
932
938
  }
933
939
 
940
+ // src/client/login-history-methods.ts
941
+ var EXT_LOGIN_HISTORY_ENDPOINT = "/api/v1/users/ext/me/login-history";
942
+ function createLoginHistoryMethods(deps) {
943
+ const { fetchWithAuth, createError } = deps;
944
+ async function getLoginHistory(params) {
945
+ if (params?.limit !== void 0 && (!Number.isInteger(params.limit) || params.limit <= 0)) {
946
+ return {
947
+ error: createError(
948
+ "VALIDATION_ERROR",
949
+ "limit \u306F1\u4EE5\u4E0A\u306E\u6574\u6570\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044"
950
+ )
951
+ };
952
+ }
953
+ const query = new URLSearchParams();
954
+ if (params?.userId !== void 0) {
955
+ query.set("userId", params.userId);
956
+ }
957
+ if (params?.limit !== void 0) {
958
+ query.set("limit", String(params.limit));
959
+ }
960
+ const queryString = query.toString();
961
+ const endpoint = queryString ? `${EXT_LOGIN_HISTORY_ENDPOINT}?${queryString}` : EXT_LOGIN_HISTORY_ENDPOINT;
962
+ return fetchWithAuth(endpoint);
963
+ }
964
+ return { getLoginHistory };
965
+ }
966
+
967
+ // src/client/organization-domains-methods.ts
968
+ var EXT_DOMAINS_ENDPOINT = "/api/v1/organizations/ext/domains";
969
+ function createOrganizationDomainsMethods(deps) {
970
+ const { fetchWithAuth, createError, serviceCode } = deps;
971
+ function buildQuery(organizationId) {
972
+ return new URLSearchParams({ organizationId, serviceCode }).toString();
973
+ }
974
+ async function getOrganizationDomains(params) {
975
+ if (!params.organizationId) {
976
+ return {
977
+ error: createError("VALIDATION_ERROR", "organizationId \u306F\u5FC5\u9808\u3067\u3059")
978
+ };
979
+ }
980
+ return fetchWithAuth(
981
+ `${EXT_DOMAINS_ENDPOINT}?${buildQuery(params.organizationId)}`
982
+ );
983
+ }
984
+ return { getOrganizationDomains };
985
+ }
986
+
987
+ // src/client/notification-preferences-methods.ts
988
+ var EXT_NOTIFICATION_PREFERENCES_ENDPOINT = "/api/v1/organizations/ext/notification-preferences";
989
+ function createNotificationPreferencesMethods(deps) {
990
+ const { fetchWithAuth, createError, serviceCode } = deps;
991
+ function buildQuery(organizationId) {
992
+ return new URLSearchParams({ organizationId, serviceCode }).toString();
993
+ }
994
+ async function getNotificationPreferences(params) {
995
+ if (!params.organizationId) {
996
+ return {
997
+ error: createError("VALIDATION_ERROR", "organizationId \u306F\u5FC5\u9808\u3067\u3059")
998
+ };
999
+ }
1000
+ return fetchWithAuth(
1001
+ `${EXT_NOTIFICATION_PREFERENCES_ENDPOINT}?${buildQuery(params.organizationId)}`
1002
+ );
1003
+ }
1004
+ async function updateNotificationPreferences(params) {
1005
+ if (!params.organizationId) {
1006
+ return {
1007
+ error: createError("VALIDATION_ERROR", "organizationId \u306F\u5FC5\u9808\u3067\u3059")
1008
+ };
1009
+ }
1010
+ if (params.preferences === null || typeof params.preferences !== "object" || Array.isArray(params.preferences)) {
1011
+ return {
1012
+ error: createError("VALIDATION_ERROR", "preferences \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059")
1013
+ };
1014
+ }
1015
+ if (Object.keys(params.preferences).length === 0) {
1016
+ return {
1017
+ error: createError("VALIDATION_ERROR", "\u66F4\u65B0\u3059\u308B\u30D5\u30A3\u30FC\u30EB\u30C9\u30921\u3064\u4EE5\u4E0A\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044")
1018
+ };
1019
+ }
1020
+ return fetchWithAuth(
1021
+ `${EXT_NOTIFICATION_PREFERENCES_ENDPOINT}?${buildQuery(params.organizationId)}`,
1022
+ {
1023
+ method: "PATCH",
1024
+ body: JSON.stringify({ preferences: params.preferences })
1025
+ }
1026
+ );
1027
+ }
1028
+ return { getNotificationPreferences, updateNotificationPreferences };
1029
+ }
1030
+
1031
+ // src/client/invite-methods.ts
1032
+ var EXT_INVITE_ENDPOINT = "/api/v1/organizations/ext/invite";
1033
+ function createInviteMethods(deps) {
1034
+ const { fetchWithAuth, createError, serviceCode } = deps;
1035
+ const assignableRoles = /* @__PURE__ */ new Set(["admin", "member", "viewer"]);
1036
+ async function inviteMember(params) {
1037
+ if (!params.organizationId || !params.email) {
1038
+ return {
1039
+ error: createError("VALIDATION_ERROR", "organizationId \u3068 email \u306F\u5FC5\u9808\u3067\u3059")
1040
+ };
1041
+ }
1042
+ if (params.role !== void 0 && !assignableRoles.has(params.role)) {
1043
+ return {
1044
+ error: createError(
1045
+ "VALIDATION_ERROR",
1046
+ "role \u306F admin\u3001member\u3001viewer \u306E\u3044\u305A\u308C\u304B\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044"
1047
+ )
1048
+ };
1049
+ }
1050
+ const query = new URLSearchParams({
1051
+ organizationId: params.organizationId,
1052
+ serviceCode
1053
+ }).toString();
1054
+ return fetchWithAuth(`${EXT_INVITE_ENDPOINT}?${query}`, {
1055
+ method: "POST",
1056
+ body: JSON.stringify({ email: params.email, role: params.role })
1057
+ });
1058
+ }
1059
+ return { inviteMember };
1060
+ }
1061
+
1062
+ // src/client/org-membership-methods.ts
1063
+ var EXT_USER_ORGANIZATIONS_ENDPOINT = "/api/v1/users/ext/me/organizations";
1064
+ function createOrgMembershipMethods(deps) {
1065
+ const { fetchWithAuth, createError } = deps;
1066
+ async function leaveOrganization(params) {
1067
+ if (!params || !params.organizationId) {
1068
+ return {
1069
+ error: createError("VALIDATION_ERROR", "organizationId \u306F\u5FC5\u9808\u3067\u3059")
1070
+ };
1071
+ }
1072
+ let endpoint = `${EXT_USER_ORGANIZATIONS_ENDPOINT}/${encodeURIComponent(params.organizationId)}`;
1073
+ if (params.userId) {
1074
+ endpoint += `?${new URLSearchParams({ userId: params.userId }).toString()}`;
1075
+ }
1076
+ return fetchWithAuth(endpoint, {
1077
+ method: "DELETE"
1078
+ });
1079
+ }
1080
+ return { leaveOrganization };
1081
+ }
1082
+
1083
+ // src/client/noncontract-methods.ts
1084
+ function createNonContractMethods(deps) {
1085
+ const { fetchWithAuth, createError, serviceCode } = deps;
1086
+ return {
1087
+ ...createLoginHistoryMethods({ fetchWithAuth, createError }),
1088
+ ...createOrganizationDomainsMethods({ fetchWithAuth, createError, serviceCode }),
1089
+ ...createNotificationPreferencesMethods({ fetchWithAuth, createError, serviceCode }),
1090
+ ...createInviteMethods({ fetchWithAuth, createError, serviceCode }),
1091
+ ...createOrgMembershipMethods({ fetchWithAuth, createError })
1092
+ };
1093
+ }
1094
+
934
1095
  // src/client/version-check.ts
935
- var SDK_VERSION = "5.12.0";
1096
+ var SDK_VERSION = "5.14.0";
936
1097
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
937
1098
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
938
1099
  function sdkHeaders() {
@@ -1004,9 +1165,20 @@ function createOAuthTokenMethods(deps) {
1004
1165
  logger,
1005
1166
  errorCodes
1006
1167
  } = deps;
1007
- async function exchangeCodeForTokens(code, codeVerifier) {
1168
+ async function exchangeCodeForTokens(code, codeVerifier, opts) {
1008
1169
  const url = `${baseUrl}${OAUTH_TOKEN_ENDPOINT}`;
1009
1170
  logger.debug("Exchanging code for tokens:", url);
1171
+ if (opts?.expectedState !== void 0 || opts?.actualState !== void 0) {
1172
+ if (opts?.expectedState !== opts?.actualState) {
1173
+ logger.error("Token exchange aborted: OAuth state mismatch or omitted (possible CSRF)");
1174
+ return {
1175
+ error: {
1176
+ code: errorCodes.STATE_MISMATCH_ERROR,
1177
+ message: "OAuth state \u304C\u4E00\u81F4\u3057\u307E\u305B\u3093\u3002CSRF \u306E\u53EF\u80FD\u6027\u304C\u3042\u308B\u305F\u3081\u8A8D\u8A3C\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F"
1178
+ }
1179
+ };
1180
+ }
1181
+ }
1010
1182
  const effectiveRedirectUri = resolvedRedirectUri ?? (typeof window !== "undefined" ? window.location.origin + window.location.pathname : null);
1011
1183
  if (!effectiveRedirectUri) {
1012
1184
  logger.error("redirectUri is required for token exchange in SSR environments. Set config.redirectUri explicitly.");
@@ -1490,6 +1662,102 @@ function base64UrlEncode(buffer) {
1490
1662
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1491
1663
  }
1492
1664
 
1665
+ // src/auth/state.ts
1666
+ var STATE_STORAGE_KEY = "ffid_oauth_state";
1667
+ var STATE_FALLBACK_STORAGE_KEY = "ffid_oauth_state_fb";
1668
+ var STATE_FALLBACK_TIMESTAMP_KEY = "ffid_oauth_state_fb_ts";
1669
+ var OAUTH_STATE_MAX_AGE_MS = 5 * 60 * 1e3;
1670
+ function storeState(state, logger) {
1671
+ if (typeof window === "undefined") {
1672
+ logger?.warn("storeState: storage is not available in SSR context");
1673
+ return false;
1674
+ }
1675
+ let sessionStored = false;
1676
+ try {
1677
+ window.sessionStorage.setItem(STATE_STORAGE_KEY, state);
1678
+ sessionStored = true;
1679
+ } catch (error) {
1680
+ logger?.warn("storeState: sessionStorage \u3078\u306E\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1681
+ }
1682
+ let localStored = false;
1683
+ try {
1684
+ window.localStorage.setItem(STATE_FALLBACK_STORAGE_KEY, state);
1685
+ window.localStorage.setItem(STATE_FALLBACK_TIMESTAMP_KEY, String(Date.now()));
1686
+ localStored = true;
1687
+ } catch (error) {
1688
+ logger?.warn("storeState: localStorage fallback \u3078\u306E\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1689
+ }
1690
+ return sessionStored || localStored;
1691
+ }
1692
+ function retrieveState(logger) {
1693
+ if (typeof window === "undefined") return null;
1694
+ let sessionState = null;
1695
+ try {
1696
+ sessionState = window.sessionStorage.getItem(STATE_STORAGE_KEY);
1697
+ } catch (error) {
1698
+ logger?.warn("retrieveState: sessionStorage \u304B\u3089\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1699
+ }
1700
+ if (sessionState) {
1701
+ cleanupStateStorage(logger);
1702
+ return sessionState;
1703
+ }
1704
+ let fallbackState = null;
1705
+ let fallbackTimestamp = null;
1706
+ try {
1707
+ fallbackState = window.localStorage.getItem(STATE_FALLBACK_STORAGE_KEY);
1708
+ fallbackTimestamp = window.localStorage.getItem(STATE_FALLBACK_TIMESTAMP_KEY);
1709
+ } catch (error) {
1710
+ logger?.warn("retrieveState: localStorage fallback \u304B\u3089\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1711
+ cleanupStateStorage(logger);
1712
+ return null;
1713
+ }
1714
+ if (!fallbackState || !fallbackTimestamp) {
1715
+ cleanupStateStorage(logger);
1716
+ return null;
1717
+ }
1718
+ const ts = Number(fallbackTimestamp);
1719
+ const age = Date.now() - ts;
1720
+ if (!Number.isFinite(ts) || age < 0 || age > OAUTH_STATE_MAX_AGE_MS) {
1721
+ cleanupStateStorage(logger);
1722
+ return null;
1723
+ }
1724
+ logger?.warn(
1725
+ "retrieveState: sessionStorage was empty, recovered OAuth state from localStorage fallback"
1726
+ );
1727
+ cleanupStateStorage(logger);
1728
+ return fallbackState;
1729
+ }
1730
+ function isStateStorageAvailable() {
1731
+ if (typeof window === "undefined") return false;
1732
+ const probeKey = "__ffid_oauth_state_probe__";
1733
+ try {
1734
+ window.sessionStorage.setItem(probeKey, "1");
1735
+ window.sessionStorage.removeItem(probeKey);
1736
+ return true;
1737
+ } catch {
1738
+ }
1739
+ try {
1740
+ window.localStorage.setItem(probeKey, "1");
1741
+ window.localStorage.removeItem(probeKey);
1742
+ return true;
1743
+ } catch {
1744
+ return false;
1745
+ }
1746
+ }
1747
+ function cleanupStateStorage(logger) {
1748
+ try {
1749
+ window.sessionStorage.removeItem(STATE_STORAGE_KEY);
1750
+ } catch (error) {
1751
+ logger?.warn("retrieveState: sessionStorage \u306E\u30AF\u30EA\u30FC\u30F3\u30A2\u30C3\u30D7\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1752
+ }
1753
+ try {
1754
+ window.localStorage.removeItem(STATE_FALLBACK_STORAGE_KEY);
1755
+ window.localStorage.removeItem(STATE_FALLBACK_TIMESTAMP_KEY);
1756
+ } catch (error) {
1757
+ logger?.warn("retrieveState: localStorage \u306E\u30AF\u30EA\u30FC\u30F3\u30A2\u30C3\u30D7\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1758
+ }
1759
+ }
1760
+
1493
1761
  // src/client/redirect.ts
1494
1762
  var OAUTH_AUTHORIZE_ENDPOINT = "/api/v1/oauth/authorize";
1495
1763
  var AUTH_LOGOUT_ENDPOINT = "/api/v1/auth/logout";
@@ -1621,6 +1889,7 @@ function createRedirectMethods(deps) {
1621
1889
  const recentCount = getRecentRedirectCount(authorizeKey, now, logger);
1622
1890
  if (recentCount >= REDIRECT_LOOP_THRESHOLD) {
1623
1891
  cleanupVerifierStorage(logger);
1892
+ cleanupStateStorage(logger);
1624
1893
  logger.warn("[FFID SDK] redirect loop detected \u2014 \u81EA\u52D5\u30EA\u30C0\u30A4\u30EC\u30AF\u30C8\u3092\u505C\u6B62\u3057\u307E\u3057\u305F", {
1625
1894
  authorizeKey,
1626
1895
  recentCount,
@@ -1644,6 +1913,11 @@ function createRedirectMethods(deps) {
1644
1913
  return { success: false, error: errorMessage };
1645
1914
  }
1646
1915
  const state = generateRandomState();
1916
+ if (!storeState(state, logger)) {
1917
+ logger.error(
1918
+ "redirectToAuthorize: OAuth state \u3092\u4FDD\u5B58\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\uFF08storage \u5229\u7528\u4E0D\u53EF\uFF09\u3002\u30B3\u30FC\u30EB\u30D0\u30C3\u30AF\u3067\u306E CSRF \u691C\u8A3C\u304C\u5931\u6557\u3057\u307E\u3059"
1919
+ );
1920
+ }
1647
1921
  const redirectUri = resolvedRedirectUri ?? window.location.origin + window.location.pathname;
1648
1922
  const params = new URLSearchParams({
1649
1923
  response_type: "code",
@@ -2433,7 +2707,8 @@ var FFID_ERROR_CODES = {
2433
2707
  TOKEN_EXCHANGE_ERROR: "TOKEN_EXCHANGE_ERROR",
2434
2708
  TOKEN_REFRESH_ERROR: "TOKEN_REFRESH_ERROR",
2435
2709
  NO_TOKENS: "NO_TOKENS",
2436
- TOKEN_VERIFICATION_ERROR: "TOKEN_VERIFICATION_ERROR"
2710
+ TOKEN_VERIFICATION_ERROR: "TOKEN_VERIFICATION_ERROR",
2711
+ STATE_MISMATCH_ERROR: "STATE_MISMATCH_ERROR"
2437
2712
  };
2438
2713
  var EXT_CHECK_ENDPOINT = "/api/v1/subscriptions/ext/check";
2439
2714
  var DEFAULT_ALLOW_GRACE = true;
@@ -2821,6 +3096,18 @@ function createFFIDClient(config) {
2821
3096
  fetchWithAuth,
2822
3097
  createError
2823
3098
  });
3099
+ const {
3100
+ getLoginHistory,
3101
+ getOrganizationDomains,
3102
+ getNotificationPreferences,
3103
+ updateNotificationPreferences,
3104
+ inviteMember,
3105
+ leaveOrganization
3106
+ } = createNonContractMethods({
3107
+ fetchWithAuth,
3108
+ createError,
3109
+ serviceCode: config.serviceCode
3110
+ });
2824
3111
  const { getAnalyticsConfig } = createAnalyticsMethods({
2825
3112
  fetchWithAuth,
2826
3113
  createError
@@ -2901,6 +3188,13 @@ function createFFIDClient(config) {
2901
3188
  removeMember,
2902
3189
  getProfile,
2903
3190
  updateProfile,
3191
+ // Non-contract ext API coverage (#3783)
3192
+ getLoginHistory,
3193
+ getOrganizationDomains,
3194
+ getNotificationPreferences,
3195
+ updateNotificationPreferences,
3196
+ inviteMember,
3197
+ leaveOrganization,
2904
3198
  getAnalyticsConfig,
2905
3199
  createCheckoutSession,
2906
3200
  createPortalSession,
@@ -2951,6 +3245,99 @@ function createFFIDClient(config) {
2951
3245
  redirectUri: resolvedRedirectUri
2952
3246
  };
2953
3247
  }
3248
+
3249
+ // src/client/callback.ts
3250
+ var CALLBACK_ERROR_CODES = {
3251
+ /** Stored state missing or did not match the callback `state` (CSRF guard) */
3252
+ STATE_MISMATCH: "STATE_MISMATCH_ERROR",
3253
+ /**
3254
+ * Web storage is unavailable, so `state` could never be persisted across the
3255
+ * OAuth bounce. An environment issue (e.g. iOS private mode), not an attack —
3256
+ * surfaced distinctly so callers don't mistake it for a CSRF mismatch.
3257
+ */
3258
+ STATE_UNAVAILABLE: "STATE_STORAGE_UNAVAILABLE",
3259
+ /** Callback URL had no `code` param */
3260
+ MISSING_CODE: "MISSING_AUTHORIZATION_CODE",
3261
+ /** SSR with no `options.url` — cannot resolve callback params */
3262
+ CALLBACK_URL_UNAVAILABLE: "CALLBACK_URL_UNAVAILABLE"
3263
+ };
3264
+ function extractSearchParams(source) {
3265
+ try {
3266
+ return new URL(source).searchParams;
3267
+ } catch {
3268
+ const queryStart = source.indexOf("?");
3269
+ const query = queryStart >= 0 ? source.slice(queryStart + 1) : source;
3270
+ return new URLSearchParams(query);
3271
+ }
3272
+ }
3273
+ function cleanCallbackUrlParams(logger) {
3274
+ try {
3275
+ const cleanUrl = new URL(window.location.href);
3276
+ cleanUrl.searchParams.delete("code");
3277
+ cleanUrl.searchParams.delete("state");
3278
+ window.history.replaceState({}, "", cleanUrl.toString());
3279
+ } catch (error) {
3280
+ logger.warn("handleRedirectCallback: URL \u306E\u30AF\u30EA\u30FC\u30F3\u30A2\u30C3\u30D7\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
3281
+ }
3282
+ }
3283
+ async function handleRedirectCallback(client, options) {
3284
+ const { logger } = client;
3285
+ const hasWindow = typeof window !== "undefined";
3286
+ const source = options?.url ?? (hasWindow ? window.location.search : null);
3287
+ if (source === null) {
3288
+ logger.warn("handleRedirectCallback: SSR \u74B0\u5883\u3067\u306F options.url \u304C\u5FC5\u8981\u3067\u3059");
3289
+ return {
3290
+ error: client.createError(
3291
+ CALLBACK_ERROR_CODES.CALLBACK_URL_UNAVAILABLE,
3292
+ "\u30B3\u30FC\u30EB\u30D0\u30C3\u30AF URL \u3092\u89E3\u6C7A\u3067\u304D\u307E\u305B\u3093\u3002SSR \u74B0\u5883\u3067\u306F options.url \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044"
3293
+ )
3294
+ };
3295
+ }
3296
+ const params = extractSearchParams(source);
3297
+ const code = params.get("code");
3298
+ const urlState = params.get("state");
3299
+ if (!code) {
3300
+ logger.warn("handleRedirectCallback: \u8A8D\u53EF\u30B3\u30FC\u30C9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093");
3301
+ return {
3302
+ error: client.createError(
3303
+ CALLBACK_ERROR_CODES.MISSING_CODE,
3304
+ "\u30B3\u30FC\u30EB\u30D0\u30C3\u30AF URL \u306B\u8A8D\u53EF\u30B3\u30FC\u30C9\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u305B\u3093"
3305
+ )
3306
+ };
3307
+ }
3308
+ const storedState = retrieveState(logger);
3309
+ if (storedState !== null || urlState !== null) {
3310
+ const stateMatches = storedState !== null && storedState === urlState;
3311
+ if (!stateMatches) {
3312
+ if (storedState === null && !isStateStorageAvailable()) {
3313
+ logger.error(
3314
+ "handleRedirectCallback: OAuth state \u3092\u4FDD\u5B58\u3067\u304D\u308B storage \u304C\u5229\u7528\u3067\u304D\u306A\u3044\u305F\u3081 CSRF \u691C\u8A3C\u304C\u3067\u304D\u307E\u305B\u3093\uFF08iOS private mode \u7B49\uFF09"
3315
+ );
3316
+ return {
3317
+ error: client.createError(
3318
+ CALLBACK_ERROR_CODES.STATE_UNAVAILABLE,
3319
+ "OAuth state \u3092\u4FDD\u5B58\u3067\u304D\u308B storage \u304C\u5229\u7528\u3067\u304D\u306A\u3044\u305F\u3081\u8A8D\u8A3C\u3092\u5B8C\u4E86\u3067\u304D\u307E\u305B\u3093\u3002\u30D6\u30E9\u30A6\u30B6\u306E\u30D7\u30E9\u30A4\u30D9\u30FC\u30C8\u30E2\u30FC\u30C9\u3084\u30B9\u30C8\u30EC\u30FC\u30B8\u8A2D\u5B9A\u3092\u3054\u78BA\u8A8D\u304F\u3060\u3055\u3044"
3320
+ )
3321
+ };
3322
+ }
3323
+ logger.error(
3324
+ "handleRedirectCallback: OAuth state mismatch or omitted (possible CSRF) \u2014 \u8A8D\u8A3C\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F"
3325
+ );
3326
+ return {
3327
+ error: client.createError(
3328
+ CALLBACK_ERROR_CODES.STATE_MISMATCH,
3329
+ "OAuth state \u304C\u4E00\u81F4\u3057\u307E\u305B\u3093\u3002CSRF \u306E\u53EF\u80FD\u6027\u304C\u3042\u308B\u305F\u3081\u8A8D\u8A3C\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F"
3330
+ )
3331
+ };
3332
+ }
3333
+ }
3334
+ const verifier = retrieveCodeVerifier(logger);
3335
+ const result = await client.exchangeCodeForTokens(code, verifier ?? void 0);
3336
+ if (!result.error && hasWindow) {
3337
+ cleanCallbackUrlParams(logger);
3338
+ }
3339
+ return result;
3340
+ }
2954
3341
  var DEFAULT_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
2955
3342
  var TOKEN_REFRESH_RATIO = 0.8;
2956
3343
  var FFIDContext = createContext(null);
@@ -3125,8 +3512,7 @@ function FFIDProvider({
3125
3512
  const code = urlParams.get("code");
3126
3513
  if (!code) return;
3127
3514
  client.logger.debug("Authorization code detected, exchanging for tokens");
3128
- const codeVerifier = retrieveCodeVerifier(client.logger);
3129
- client.exchangeCodeForTokens(code, codeVerifier ?? void 0).then((result) => {
3515
+ handleRedirectCallback(client).then((result) => {
3130
3516
  if (result.error) {
3131
3517
  client.logger.error("Token exchange failed:", result.error);
3132
3518
  setError(result.error);
@@ -3134,10 +3520,6 @@ function FFIDProvider({
3134
3520
  setIsLoading(false);
3135
3521
  return;
3136
3522
  }
3137
- const cleanUrl = new URL(window.location.href);
3138
- cleanUrl.searchParams.delete("code");
3139
- cleanUrl.searchParams.delete("state");
3140
- window.history.replaceState({}, "", cleanUrl.toString());
3141
3523
  client.logger.debug("Token exchange successful, refreshing session");
3142
3524
  return refresh();
3143
3525
  }).catch((err) => {
@@ -4938,4 +5320,4 @@ function FFIDInquiryForm({
4938
5320
  );
4939
5321
  }
4940
5322
 
4941
- export { ACCESS_GRANTING_EFFECTIVE_STATUSES, BLOCKING_EFFECTIVE_STATUSES, DEFAULT_API_BASE_URL, DEFAULT_OAUTH_SCOPES, EFFECTIVE_SUBSCRIPTION_STATUSES, FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDProvider, FFIDSDKError, FFIDSubscriptionBadge, FFIDUserMenu, FFID_ANNOUNCEMENTS_ERROR_CODES, FFID_INQUIRY_CATEGORIES, FFID_INQUIRY_CATEGORIES_SITE_2026, computeEffectiveStatusFromSession, createFFIDAnnouncementsClient, createFFIDClient, createTokenStore, generateCodeChallenge, generateCodeVerifier, hasAccessFromUserinfo, isBlockedFromUserinfo, isFFIDInquiryCategorySite2026, normalizeRedirectUri, retrieveCodeVerifier, storeCodeVerifier, useFFID, useFFIDAnnouncements, useFFIDContext, useSubscription, withSubscription };
5323
+ export { ACCESS_GRANTING_EFFECTIVE_STATUSES, BLOCKING_EFFECTIVE_STATUSES, DEFAULT_API_BASE_URL, DEFAULT_OAUTH_SCOPES, EFFECTIVE_SUBSCRIPTION_STATUSES, FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDProvider, FFIDSDKError, FFIDSubscriptionBadge, FFIDUserMenu, FFID_ANNOUNCEMENTS_ERROR_CODES, FFID_INQUIRY_CATEGORIES, FFID_INQUIRY_CATEGORIES_SITE_2026, cleanupStateStorage, computeEffectiveStatusFromSession, createFFIDAnnouncementsClient, createFFIDClient, createTokenStore, generateCodeChallenge, generateCodeVerifier, handleRedirectCallback, hasAccessFromUserinfo, isBlockedFromUserinfo, isFFIDInquiryCategorySite2026, normalizeRedirectUri, retrieveCodeVerifier, retrieveState, storeCodeVerifier, storeState, useFFID, useFFIDAnnouncements, useFFIDContext, useSubscription, withSubscription };