@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.
@@ -124,6 +124,9 @@ function normalizeUserinfo(raw) {
124
124
  return {
125
125
  sub: raw.sub,
126
126
  email: raw.email,
127
+ // #3791: undefined → undefined を保つ(false に丸めない)。古い FFID は wire に
128
+ // 含めないため、consumer は undefined を「未確認」ではなく「不明」として扱える。
129
+ emailVerified: raw.email_verified,
127
130
  name: raw.name,
128
131
  picture: raw.picture,
129
132
  companyName: raw.company_name ?? null,
@@ -379,6 +382,9 @@ function createVerifyAccessToken(deps) {
379
382
  const base = {
380
383
  sub: introspectResponse.sub,
381
384
  email: introspectResponse.email ?? null,
385
+ // #3791: preserve undefined (do NOT coerce to false) so introspect-strategy
386
+ // callers can distinguish "verified" / "not verified" / "older server".
387
+ email_verified: introspectResponse.email_verified,
382
388
  name: introspectResponse.name ?? null,
383
389
  picture: introspectResponse.picture ?? null,
384
390
  company_name: introspectResponse.company_name ?? null,
@@ -933,8 +939,163 @@ function createProfileMethods(deps) {
933
939
  return { getProfile, updateProfile };
934
940
  }
935
941
 
942
+ // src/client/login-history-methods.ts
943
+ var EXT_LOGIN_HISTORY_ENDPOINT = "/api/v1/users/ext/me/login-history";
944
+ function createLoginHistoryMethods(deps) {
945
+ const { fetchWithAuth, createError } = deps;
946
+ async function getLoginHistory(params) {
947
+ if (params?.limit !== void 0 && (!Number.isInteger(params.limit) || params.limit <= 0)) {
948
+ return {
949
+ error: createError(
950
+ "VALIDATION_ERROR",
951
+ "limit \u306F1\u4EE5\u4E0A\u306E\u6574\u6570\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044"
952
+ )
953
+ };
954
+ }
955
+ const query = new URLSearchParams();
956
+ if (params?.userId !== void 0) {
957
+ query.set("userId", params.userId);
958
+ }
959
+ if (params?.limit !== void 0) {
960
+ query.set("limit", String(params.limit));
961
+ }
962
+ const queryString = query.toString();
963
+ const endpoint = queryString ? `${EXT_LOGIN_HISTORY_ENDPOINT}?${queryString}` : EXT_LOGIN_HISTORY_ENDPOINT;
964
+ return fetchWithAuth(endpoint);
965
+ }
966
+ return { getLoginHistory };
967
+ }
968
+
969
+ // src/client/organization-domains-methods.ts
970
+ var EXT_DOMAINS_ENDPOINT = "/api/v1/organizations/ext/domains";
971
+ function createOrganizationDomainsMethods(deps) {
972
+ const { fetchWithAuth, createError, serviceCode } = deps;
973
+ function buildQuery(organizationId) {
974
+ return new URLSearchParams({ organizationId, serviceCode }).toString();
975
+ }
976
+ async function getOrganizationDomains(params) {
977
+ if (!params.organizationId) {
978
+ return {
979
+ error: createError("VALIDATION_ERROR", "organizationId \u306F\u5FC5\u9808\u3067\u3059")
980
+ };
981
+ }
982
+ return fetchWithAuth(
983
+ `${EXT_DOMAINS_ENDPOINT}?${buildQuery(params.organizationId)}`
984
+ );
985
+ }
986
+ return { getOrganizationDomains };
987
+ }
988
+
989
+ // src/client/notification-preferences-methods.ts
990
+ var EXT_NOTIFICATION_PREFERENCES_ENDPOINT = "/api/v1/organizations/ext/notification-preferences";
991
+ function createNotificationPreferencesMethods(deps) {
992
+ const { fetchWithAuth, createError, serviceCode } = deps;
993
+ function buildQuery(organizationId) {
994
+ return new URLSearchParams({ organizationId, serviceCode }).toString();
995
+ }
996
+ async function getNotificationPreferences(params) {
997
+ if (!params.organizationId) {
998
+ return {
999
+ error: createError("VALIDATION_ERROR", "organizationId \u306F\u5FC5\u9808\u3067\u3059")
1000
+ };
1001
+ }
1002
+ return fetchWithAuth(
1003
+ `${EXT_NOTIFICATION_PREFERENCES_ENDPOINT}?${buildQuery(params.organizationId)}`
1004
+ );
1005
+ }
1006
+ async function updateNotificationPreferences(params) {
1007
+ if (!params.organizationId) {
1008
+ return {
1009
+ error: createError("VALIDATION_ERROR", "organizationId \u306F\u5FC5\u9808\u3067\u3059")
1010
+ };
1011
+ }
1012
+ if (params.preferences === null || typeof params.preferences !== "object" || Array.isArray(params.preferences)) {
1013
+ return {
1014
+ error: createError("VALIDATION_ERROR", "preferences \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059")
1015
+ };
1016
+ }
1017
+ if (Object.keys(params.preferences).length === 0) {
1018
+ return {
1019
+ 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")
1020
+ };
1021
+ }
1022
+ return fetchWithAuth(
1023
+ `${EXT_NOTIFICATION_PREFERENCES_ENDPOINT}?${buildQuery(params.organizationId)}`,
1024
+ {
1025
+ method: "PATCH",
1026
+ body: JSON.stringify({ preferences: params.preferences })
1027
+ }
1028
+ );
1029
+ }
1030
+ return { getNotificationPreferences, updateNotificationPreferences };
1031
+ }
1032
+
1033
+ // src/client/invite-methods.ts
1034
+ var EXT_INVITE_ENDPOINT = "/api/v1/organizations/ext/invite";
1035
+ function createInviteMethods(deps) {
1036
+ const { fetchWithAuth, createError, serviceCode } = deps;
1037
+ const assignableRoles = /* @__PURE__ */ new Set(["admin", "member", "viewer"]);
1038
+ async function inviteMember(params) {
1039
+ if (!params.organizationId || !params.email) {
1040
+ return {
1041
+ error: createError("VALIDATION_ERROR", "organizationId \u3068 email \u306F\u5FC5\u9808\u3067\u3059")
1042
+ };
1043
+ }
1044
+ if (params.role !== void 0 && !assignableRoles.has(params.role)) {
1045
+ return {
1046
+ error: createError(
1047
+ "VALIDATION_ERROR",
1048
+ "role \u306F admin\u3001member\u3001viewer \u306E\u3044\u305A\u308C\u304B\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044"
1049
+ )
1050
+ };
1051
+ }
1052
+ const query = new URLSearchParams({
1053
+ organizationId: params.organizationId,
1054
+ serviceCode
1055
+ }).toString();
1056
+ return fetchWithAuth(`${EXT_INVITE_ENDPOINT}?${query}`, {
1057
+ method: "POST",
1058
+ body: JSON.stringify({ email: params.email, role: params.role })
1059
+ });
1060
+ }
1061
+ return { inviteMember };
1062
+ }
1063
+
1064
+ // src/client/org-membership-methods.ts
1065
+ var EXT_USER_ORGANIZATIONS_ENDPOINT = "/api/v1/users/ext/me/organizations";
1066
+ function createOrgMembershipMethods(deps) {
1067
+ const { fetchWithAuth, createError } = deps;
1068
+ async function leaveOrganization(params) {
1069
+ if (!params || !params.organizationId) {
1070
+ return {
1071
+ error: createError("VALIDATION_ERROR", "organizationId \u306F\u5FC5\u9808\u3067\u3059")
1072
+ };
1073
+ }
1074
+ let endpoint = `${EXT_USER_ORGANIZATIONS_ENDPOINT}/${encodeURIComponent(params.organizationId)}`;
1075
+ if (params.userId) {
1076
+ endpoint += `?${new URLSearchParams({ userId: params.userId }).toString()}`;
1077
+ }
1078
+ return fetchWithAuth(endpoint, {
1079
+ method: "DELETE"
1080
+ });
1081
+ }
1082
+ return { leaveOrganization };
1083
+ }
1084
+
1085
+ // src/client/noncontract-methods.ts
1086
+ function createNonContractMethods(deps) {
1087
+ const { fetchWithAuth, createError, serviceCode } = deps;
1088
+ return {
1089
+ ...createLoginHistoryMethods({ fetchWithAuth, createError }),
1090
+ ...createOrganizationDomainsMethods({ fetchWithAuth, createError, serviceCode }),
1091
+ ...createNotificationPreferencesMethods({ fetchWithAuth, createError, serviceCode }),
1092
+ ...createInviteMethods({ fetchWithAuth, createError, serviceCode }),
1093
+ ...createOrgMembershipMethods({ fetchWithAuth, createError })
1094
+ };
1095
+ }
1096
+
936
1097
  // src/client/version-check.ts
937
- var SDK_VERSION = "5.12.0";
1098
+ var SDK_VERSION = "5.14.0";
938
1099
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
939
1100
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
940
1101
  function sdkHeaders() {
@@ -1006,9 +1167,20 @@ function createOAuthTokenMethods(deps) {
1006
1167
  logger,
1007
1168
  errorCodes
1008
1169
  } = deps;
1009
- async function exchangeCodeForTokens(code, codeVerifier) {
1170
+ async function exchangeCodeForTokens(code, codeVerifier, opts) {
1010
1171
  const url = `${baseUrl}${OAUTH_TOKEN_ENDPOINT}`;
1011
1172
  logger.debug("Exchanging code for tokens:", url);
1173
+ if (opts?.expectedState !== void 0 || opts?.actualState !== void 0) {
1174
+ if (opts?.expectedState !== opts?.actualState) {
1175
+ logger.error("Token exchange aborted: OAuth state mismatch or omitted (possible CSRF)");
1176
+ return {
1177
+ error: {
1178
+ code: errorCodes.STATE_MISMATCH_ERROR,
1179
+ 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"
1180
+ }
1181
+ };
1182
+ }
1183
+ }
1012
1184
  const effectiveRedirectUri = resolvedRedirectUri ?? (typeof window !== "undefined" ? window.location.origin + window.location.pathname : null);
1013
1185
  if (!effectiveRedirectUri) {
1014
1186
  logger.error("redirectUri is required for token exchange in SSR environments. Set config.redirectUri explicitly.");
@@ -1492,6 +1664,102 @@ function base64UrlEncode(buffer) {
1492
1664
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1493
1665
  }
1494
1666
 
1667
+ // src/auth/state.ts
1668
+ var STATE_STORAGE_KEY = "ffid_oauth_state";
1669
+ var STATE_FALLBACK_STORAGE_KEY = "ffid_oauth_state_fb";
1670
+ var STATE_FALLBACK_TIMESTAMP_KEY = "ffid_oauth_state_fb_ts";
1671
+ var OAUTH_STATE_MAX_AGE_MS = 5 * 60 * 1e3;
1672
+ function storeState(state, logger) {
1673
+ if (typeof window === "undefined") {
1674
+ logger?.warn("storeState: storage is not available in SSR context");
1675
+ return false;
1676
+ }
1677
+ let sessionStored = false;
1678
+ try {
1679
+ window.sessionStorage.setItem(STATE_STORAGE_KEY, state);
1680
+ sessionStored = true;
1681
+ } catch (error) {
1682
+ logger?.warn("storeState: sessionStorage \u3078\u306E\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1683
+ }
1684
+ let localStored = false;
1685
+ try {
1686
+ window.localStorage.setItem(STATE_FALLBACK_STORAGE_KEY, state);
1687
+ window.localStorage.setItem(STATE_FALLBACK_TIMESTAMP_KEY, String(Date.now()));
1688
+ localStored = true;
1689
+ } catch (error) {
1690
+ logger?.warn("storeState: localStorage fallback \u3078\u306E\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1691
+ }
1692
+ return sessionStored || localStored;
1693
+ }
1694
+ function retrieveState(logger) {
1695
+ if (typeof window === "undefined") return null;
1696
+ let sessionState = null;
1697
+ try {
1698
+ sessionState = window.sessionStorage.getItem(STATE_STORAGE_KEY);
1699
+ } catch (error) {
1700
+ logger?.warn("retrieveState: sessionStorage \u304B\u3089\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1701
+ }
1702
+ if (sessionState) {
1703
+ cleanupStateStorage(logger);
1704
+ return sessionState;
1705
+ }
1706
+ let fallbackState = null;
1707
+ let fallbackTimestamp = null;
1708
+ try {
1709
+ fallbackState = window.localStorage.getItem(STATE_FALLBACK_STORAGE_KEY);
1710
+ fallbackTimestamp = window.localStorage.getItem(STATE_FALLBACK_TIMESTAMP_KEY);
1711
+ } catch (error) {
1712
+ logger?.warn("retrieveState: localStorage fallback \u304B\u3089\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1713
+ cleanupStateStorage(logger);
1714
+ return null;
1715
+ }
1716
+ if (!fallbackState || !fallbackTimestamp) {
1717
+ cleanupStateStorage(logger);
1718
+ return null;
1719
+ }
1720
+ const ts = Number(fallbackTimestamp);
1721
+ const age = Date.now() - ts;
1722
+ if (!Number.isFinite(ts) || age < 0 || age > OAUTH_STATE_MAX_AGE_MS) {
1723
+ cleanupStateStorage(logger);
1724
+ return null;
1725
+ }
1726
+ logger?.warn(
1727
+ "retrieveState: sessionStorage was empty, recovered OAuth state from localStorage fallback"
1728
+ );
1729
+ cleanupStateStorage(logger);
1730
+ return fallbackState;
1731
+ }
1732
+ function isStateStorageAvailable() {
1733
+ if (typeof window === "undefined") return false;
1734
+ const probeKey = "__ffid_oauth_state_probe__";
1735
+ try {
1736
+ window.sessionStorage.setItem(probeKey, "1");
1737
+ window.sessionStorage.removeItem(probeKey);
1738
+ return true;
1739
+ } catch {
1740
+ }
1741
+ try {
1742
+ window.localStorage.setItem(probeKey, "1");
1743
+ window.localStorage.removeItem(probeKey);
1744
+ return true;
1745
+ } catch {
1746
+ return false;
1747
+ }
1748
+ }
1749
+ function cleanupStateStorage(logger) {
1750
+ try {
1751
+ window.sessionStorage.removeItem(STATE_STORAGE_KEY);
1752
+ } catch (error) {
1753
+ logger?.warn("retrieveState: sessionStorage \u306E\u30AF\u30EA\u30FC\u30F3\u30A2\u30C3\u30D7\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1754
+ }
1755
+ try {
1756
+ window.localStorage.removeItem(STATE_FALLBACK_STORAGE_KEY);
1757
+ window.localStorage.removeItem(STATE_FALLBACK_TIMESTAMP_KEY);
1758
+ } catch (error) {
1759
+ logger?.warn("retrieveState: localStorage \u306E\u30AF\u30EA\u30FC\u30F3\u30A2\u30C3\u30D7\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1760
+ }
1761
+ }
1762
+
1495
1763
  // src/client/redirect.ts
1496
1764
  var OAUTH_AUTHORIZE_ENDPOINT = "/api/v1/oauth/authorize";
1497
1765
  var AUTH_LOGOUT_ENDPOINT = "/api/v1/auth/logout";
@@ -1623,6 +1891,7 @@ function createRedirectMethods(deps) {
1623
1891
  const recentCount = getRecentRedirectCount(authorizeKey, now, logger);
1624
1892
  if (recentCount >= REDIRECT_LOOP_THRESHOLD) {
1625
1893
  cleanupVerifierStorage(logger);
1894
+ cleanupStateStorage(logger);
1626
1895
  logger.warn("[FFID SDK] redirect loop detected \u2014 \u81EA\u52D5\u30EA\u30C0\u30A4\u30EC\u30AF\u30C8\u3092\u505C\u6B62\u3057\u307E\u3057\u305F", {
1627
1896
  authorizeKey,
1628
1897
  recentCount,
@@ -1646,6 +1915,11 @@ function createRedirectMethods(deps) {
1646
1915
  return { success: false, error: errorMessage };
1647
1916
  }
1648
1917
  const state = generateRandomState();
1918
+ if (!storeState(state, logger)) {
1919
+ logger.error(
1920
+ "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"
1921
+ );
1922
+ }
1649
1923
  const redirectUri = resolvedRedirectUri ?? window.location.origin + window.location.pathname;
1650
1924
  const params = new URLSearchParams({
1651
1925
  response_type: "code",
@@ -2435,7 +2709,8 @@ var FFID_ERROR_CODES = {
2435
2709
  TOKEN_EXCHANGE_ERROR: "TOKEN_EXCHANGE_ERROR",
2436
2710
  TOKEN_REFRESH_ERROR: "TOKEN_REFRESH_ERROR",
2437
2711
  NO_TOKENS: "NO_TOKENS",
2438
- TOKEN_VERIFICATION_ERROR: "TOKEN_VERIFICATION_ERROR"
2712
+ TOKEN_VERIFICATION_ERROR: "TOKEN_VERIFICATION_ERROR",
2713
+ STATE_MISMATCH_ERROR: "STATE_MISMATCH_ERROR"
2439
2714
  };
2440
2715
  var EXT_CHECK_ENDPOINT = "/api/v1/subscriptions/ext/check";
2441
2716
  var DEFAULT_ALLOW_GRACE = true;
@@ -2823,6 +3098,18 @@ function createFFIDClient(config) {
2823
3098
  fetchWithAuth,
2824
3099
  createError
2825
3100
  });
3101
+ const {
3102
+ getLoginHistory,
3103
+ getOrganizationDomains,
3104
+ getNotificationPreferences,
3105
+ updateNotificationPreferences,
3106
+ inviteMember,
3107
+ leaveOrganization
3108
+ } = createNonContractMethods({
3109
+ fetchWithAuth,
3110
+ createError,
3111
+ serviceCode: config.serviceCode
3112
+ });
2826
3113
  const { getAnalyticsConfig } = createAnalyticsMethods({
2827
3114
  fetchWithAuth,
2828
3115
  createError
@@ -2903,6 +3190,13 @@ function createFFIDClient(config) {
2903
3190
  removeMember,
2904
3191
  getProfile,
2905
3192
  updateProfile,
3193
+ // Non-contract ext API coverage (#3783)
3194
+ getLoginHistory,
3195
+ getOrganizationDomains,
3196
+ getNotificationPreferences,
3197
+ updateNotificationPreferences,
3198
+ inviteMember,
3199
+ leaveOrganization,
2906
3200
  getAnalyticsConfig,
2907
3201
  createCheckoutSession,
2908
3202
  createPortalSession,
@@ -2953,6 +3247,99 @@ function createFFIDClient(config) {
2953
3247
  redirectUri: resolvedRedirectUri
2954
3248
  };
2955
3249
  }
3250
+
3251
+ // src/client/callback.ts
3252
+ var CALLBACK_ERROR_CODES = {
3253
+ /** Stored state missing or did not match the callback `state` (CSRF guard) */
3254
+ STATE_MISMATCH: "STATE_MISMATCH_ERROR",
3255
+ /**
3256
+ * Web storage is unavailable, so `state` could never be persisted across the
3257
+ * OAuth bounce. An environment issue (e.g. iOS private mode), not an attack —
3258
+ * surfaced distinctly so callers don't mistake it for a CSRF mismatch.
3259
+ */
3260
+ STATE_UNAVAILABLE: "STATE_STORAGE_UNAVAILABLE",
3261
+ /** Callback URL had no `code` param */
3262
+ MISSING_CODE: "MISSING_AUTHORIZATION_CODE",
3263
+ /** SSR with no `options.url` — cannot resolve callback params */
3264
+ CALLBACK_URL_UNAVAILABLE: "CALLBACK_URL_UNAVAILABLE"
3265
+ };
3266
+ function extractSearchParams(source) {
3267
+ try {
3268
+ return new URL(source).searchParams;
3269
+ } catch {
3270
+ const queryStart = source.indexOf("?");
3271
+ const query = queryStart >= 0 ? source.slice(queryStart + 1) : source;
3272
+ return new URLSearchParams(query);
3273
+ }
3274
+ }
3275
+ function cleanCallbackUrlParams(logger) {
3276
+ try {
3277
+ const cleanUrl = new URL(window.location.href);
3278
+ cleanUrl.searchParams.delete("code");
3279
+ cleanUrl.searchParams.delete("state");
3280
+ window.history.replaceState({}, "", cleanUrl.toString());
3281
+ } catch (error) {
3282
+ logger.warn("handleRedirectCallback: URL \u306E\u30AF\u30EA\u30FC\u30F3\u30A2\u30C3\u30D7\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
3283
+ }
3284
+ }
3285
+ async function handleRedirectCallback(client, options) {
3286
+ const { logger } = client;
3287
+ const hasWindow = typeof window !== "undefined";
3288
+ const source = options?.url ?? (hasWindow ? window.location.search : null);
3289
+ if (source === null) {
3290
+ logger.warn("handleRedirectCallback: SSR \u74B0\u5883\u3067\u306F options.url \u304C\u5FC5\u8981\u3067\u3059");
3291
+ return {
3292
+ error: client.createError(
3293
+ CALLBACK_ERROR_CODES.CALLBACK_URL_UNAVAILABLE,
3294
+ "\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"
3295
+ )
3296
+ };
3297
+ }
3298
+ const params = extractSearchParams(source);
3299
+ const code = params.get("code");
3300
+ const urlState = params.get("state");
3301
+ if (!code) {
3302
+ logger.warn("handleRedirectCallback: \u8A8D\u53EF\u30B3\u30FC\u30C9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093");
3303
+ return {
3304
+ error: client.createError(
3305
+ CALLBACK_ERROR_CODES.MISSING_CODE,
3306
+ "\u30B3\u30FC\u30EB\u30D0\u30C3\u30AF URL \u306B\u8A8D\u53EF\u30B3\u30FC\u30C9\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u305B\u3093"
3307
+ )
3308
+ };
3309
+ }
3310
+ const storedState = retrieveState(logger);
3311
+ if (storedState !== null || urlState !== null) {
3312
+ const stateMatches = storedState !== null && storedState === urlState;
3313
+ if (!stateMatches) {
3314
+ if (storedState === null && !isStateStorageAvailable()) {
3315
+ logger.error(
3316
+ "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"
3317
+ );
3318
+ return {
3319
+ error: client.createError(
3320
+ CALLBACK_ERROR_CODES.STATE_UNAVAILABLE,
3321
+ "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"
3322
+ )
3323
+ };
3324
+ }
3325
+ logger.error(
3326
+ "handleRedirectCallback: OAuth state mismatch or omitted (possible CSRF) \u2014 \u8A8D\u8A3C\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F"
3327
+ );
3328
+ return {
3329
+ error: client.createError(
3330
+ CALLBACK_ERROR_CODES.STATE_MISMATCH,
3331
+ "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"
3332
+ )
3333
+ };
3334
+ }
3335
+ }
3336
+ const verifier = retrieveCodeVerifier(logger);
3337
+ const result = await client.exchangeCodeForTokens(code, verifier ?? void 0);
3338
+ if (!result.error && hasWindow) {
3339
+ cleanCallbackUrlParams(logger);
3340
+ }
3341
+ return result;
3342
+ }
2956
3343
  var DEFAULT_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
2957
3344
  var TOKEN_REFRESH_RATIO = 0.8;
2958
3345
  var FFIDContext = react.createContext(null);
@@ -3127,8 +3514,7 @@ function FFIDProvider({
3127
3514
  const code = urlParams.get("code");
3128
3515
  if (!code) return;
3129
3516
  client.logger.debug("Authorization code detected, exchanging for tokens");
3130
- const codeVerifier = retrieveCodeVerifier(client.logger);
3131
- client.exchangeCodeForTokens(code, codeVerifier ?? void 0).then((result) => {
3517
+ handleRedirectCallback(client).then((result) => {
3132
3518
  if (result.error) {
3133
3519
  client.logger.error("Token exchange failed:", result.error);
3134
3520
  setError(result.error);
@@ -3136,10 +3522,6 @@ function FFIDProvider({
3136
3522
  setIsLoading(false);
3137
3523
  return;
3138
3524
  }
3139
- const cleanUrl = new URL(window.location.href);
3140
- cleanUrl.searchParams.delete("code");
3141
- cleanUrl.searchParams.delete("state");
3142
- window.history.replaceState({}, "", cleanUrl.toString());
3143
3525
  client.logger.debug("Token exchange successful, refreshing session");
3144
3526
  return refresh();
3145
3527
  }).catch((err) => {
@@ -4957,18 +5339,22 @@ exports.FFIDUserMenu = FFIDUserMenu;
4957
5339
  exports.FFID_ANNOUNCEMENTS_ERROR_CODES = FFID_ANNOUNCEMENTS_ERROR_CODES;
4958
5340
  exports.FFID_INQUIRY_CATEGORIES = FFID_INQUIRY_CATEGORIES;
4959
5341
  exports.FFID_INQUIRY_CATEGORIES_SITE_2026 = FFID_INQUIRY_CATEGORIES_SITE_2026;
5342
+ exports.cleanupStateStorage = cleanupStateStorage;
4960
5343
  exports.computeEffectiveStatusFromSession = computeEffectiveStatusFromSession;
4961
5344
  exports.createFFIDAnnouncementsClient = createFFIDAnnouncementsClient;
4962
5345
  exports.createFFIDClient = createFFIDClient;
4963
5346
  exports.createTokenStore = createTokenStore;
4964
5347
  exports.generateCodeChallenge = generateCodeChallenge;
4965
5348
  exports.generateCodeVerifier = generateCodeVerifier;
5349
+ exports.handleRedirectCallback = handleRedirectCallback;
4966
5350
  exports.hasAccessFromUserinfo = hasAccessFromUserinfo;
4967
5351
  exports.isBlockedFromUserinfo = isBlockedFromUserinfo;
4968
5352
  exports.isFFIDInquiryCategorySite2026 = isFFIDInquiryCategorySite2026;
4969
5353
  exports.normalizeRedirectUri = normalizeRedirectUri;
4970
5354
  exports.retrieveCodeVerifier = retrieveCodeVerifier;
5355
+ exports.retrieveState = retrieveState;
4971
5356
  exports.storeCodeVerifier = storeCodeVerifier;
5357
+ exports.storeState = storeState;
4972
5358
  exports.useFFID = useFFID;
4973
5359
  exports.useFFIDAnnouncements = useFFIDAnnouncements;
4974
5360
  exports.useFFIDContext = useFFIDContext;
@@ -1,34 +1,34 @@
1
1
  'use strict';
2
2
 
3
- var chunkAOJDV3UO_cjs = require('../chunk-AOJDV3UO.cjs');
3
+ var chunkBVDUQQHP_cjs = require('../chunk-BVDUQQHP.cjs');
4
4
 
5
5
 
6
6
 
7
7
  Object.defineProperty(exports, "FFIDAnnouncementBadge", {
8
8
  enumerable: true,
9
- get: function () { return chunkAOJDV3UO_cjs.FFIDAnnouncementBadge; }
9
+ get: function () { return chunkBVDUQQHP_cjs.FFIDAnnouncementBadge; }
10
10
  });
11
11
  Object.defineProperty(exports, "FFIDAnnouncementList", {
12
12
  enumerable: true,
13
- get: function () { return chunkAOJDV3UO_cjs.FFIDAnnouncementList; }
13
+ get: function () { return chunkBVDUQQHP_cjs.FFIDAnnouncementList; }
14
14
  });
15
15
  Object.defineProperty(exports, "FFIDInquiryForm", {
16
16
  enumerable: true,
17
- get: function () { return chunkAOJDV3UO_cjs.FFIDInquiryForm; }
17
+ get: function () { return chunkBVDUQQHP_cjs.FFIDInquiryForm; }
18
18
  });
19
19
  Object.defineProperty(exports, "FFIDLoginButton", {
20
20
  enumerable: true,
21
- get: function () { return chunkAOJDV3UO_cjs.FFIDLoginButton; }
21
+ get: function () { return chunkBVDUQQHP_cjs.FFIDLoginButton; }
22
22
  });
23
23
  Object.defineProperty(exports, "FFIDOrganizationSwitcher", {
24
24
  enumerable: true,
25
- get: function () { return chunkAOJDV3UO_cjs.FFIDOrganizationSwitcher; }
25
+ get: function () { return chunkBVDUQQHP_cjs.FFIDOrganizationSwitcher; }
26
26
  });
27
27
  Object.defineProperty(exports, "FFIDSubscriptionBadge", {
28
28
  enumerable: true,
29
- get: function () { return chunkAOJDV3UO_cjs.FFIDSubscriptionBadge; }
29
+ get: function () { return chunkBVDUQQHP_cjs.FFIDSubscriptionBadge; }
30
30
  });
31
31
  Object.defineProperty(exports, "FFIDUserMenu", {
32
32
  enumerable: true,
33
- get: function () { return chunkAOJDV3UO_cjs.FFIDUserMenu; }
33
+ get: function () { return chunkBVDUQQHP_cjs.FFIDUserMenu; }
34
34
  });
@@ -1,3 +1,3 @@
1
- export { I as FFIDAnnouncementBadge, ai as FFIDAnnouncementBadgeClassNames, aj as FFIDAnnouncementBadgeProps, J as FFIDAnnouncementList, ak as FFIDAnnouncementListClassNames, al as FFIDAnnouncementListProps, S as FFIDInquiryForm, T as FFIDInquiryFormCategoryItem, U as FFIDInquiryFormClassNames, V as FFIDInquiryFormLegalLayout, W as FFIDInquiryFormOrganization, X as FFIDInquiryFormPlaceholderContext, Y as FFIDInquiryFormPrefill, Z as FFIDInquiryFormProps, _ as FFIDInquiryFormSubmitData, $ as FFIDInquiryFormSubmitResult, a1 as FFIDLoginButton, am as FFIDLoginButtonProps, a4 as FFIDOrganizationSwitcher, an as FFIDOrganizationSwitcherClassNames, ao as FFIDOrganizationSwitcherProps, a9 as FFIDSubscriptionBadge, ap as FFIDSubscriptionBadgeClassNames, aq as FFIDSubscriptionBadgeProps, ab as FFIDUserMenu, ar as FFIDUserMenuClassNames, as as FFIDUserMenuProps } from '../index-DqsWKU16.cjs';
1
+ export { D as FFIDAnnouncementBadge, af as FFIDAnnouncementBadgeClassNames, ag as FFIDAnnouncementBadgeProps, G as FFIDAnnouncementList, ah as FFIDAnnouncementListClassNames, ai as FFIDAnnouncementListProps, P as FFIDInquiryForm, Q as FFIDInquiryFormCategoryItem, R as FFIDInquiryFormClassNames, S as FFIDInquiryFormLegalLayout, T as FFIDInquiryFormOrganization, U as FFIDInquiryFormPlaceholderContext, V as FFIDInquiryFormPrefill, W as FFIDInquiryFormProps, X as FFIDInquiryFormSubmitData, Y as FFIDInquiryFormSubmitResult, _ as FFIDLoginButton, aj as FFIDLoginButtonProps, a1 as FFIDOrganizationSwitcher, ak as FFIDOrganizationSwitcherClassNames, al as FFIDOrganizationSwitcherProps, a6 as FFIDSubscriptionBadge, am as FFIDSubscriptionBadgeClassNames, an as FFIDSubscriptionBadgeProps, a8 as FFIDUserMenu, ao as FFIDUserMenuClassNames, ap as FFIDUserMenuProps } from '../index-CsVJTuPv.cjs';
2
2
  import 'react/jsx-runtime';
3
3
  import 'react';
@@ -1,3 +1,3 @@
1
- export { I as FFIDAnnouncementBadge, ai as FFIDAnnouncementBadgeClassNames, aj as FFIDAnnouncementBadgeProps, J as FFIDAnnouncementList, ak as FFIDAnnouncementListClassNames, al as FFIDAnnouncementListProps, S as FFIDInquiryForm, T as FFIDInquiryFormCategoryItem, U as FFIDInquiryFormClassNames, V as FFIDInquiryFormLegalLayout, W as FFIDInquiryFormOrganization, X as FFIDInquiryFormPlaceholderContext, Y as FFIDInquiryFormPrefill, Z as FFIDInquiryFormProps, _ as FFIDInquiryFormSubmitData, $ as FFIDInquiryFormSubmitResult, a1 as FFIDLoginButton, am as FFIDLoginButtonProps, a4 as FFIDOrganizationSwitcher, an as FFIDOrganizationSwitcherClassNames, ao as FFIDOrganizationSwitcherProps, a9 as FFIDSubscriptionBadge, ap as FFIDSubscriptionBadgeClassNames, aq as FFIDSubscriptionBadgeProps, ab as FFIDUserMenu, ar as FFIDUserMenuClassNames, as as FFIDUserMenuProps } from '../index-DqsWKU16.js';
1
+ export { D as FFIDAnnouncementBadge, af as FFIDAnnouncementBadgeClassNames, ag as FFIDAnnouncementBadgeProps, G as FFIDAnnouncementList, ah as FFIDAnnouncementListClassNames, ai as FFIDAnnouncementListProps, P as FFIDInquiryForm, Q as FFIDInquiryFormCategoryItem, R as FFIDInquiryFormClassNames, S as FFIDInquiryFormLegalLayout, T as FFIDInquiryFormOrganization, U as FFIDInquiryFormPlaceholderContext, V as FFIDInquiryFormPrefill, W as FFIDInquiryFormProps, X as FFIDInquiryFormSubmitData, Y as FFIDInquiryFormSubmitResult, _ as FFIDLoginButton, aj as FFIDLoginButtonProps, a1 as FFIDOrganizationSwitcher, ak as FFIDOrganizationSwitcherClassNames, al as FFIDOrganizationSwitcherProps, a6 as FFIDSubscriptionBadge, am as FFIDSubscriptionBadgeClassNames, an as FFIDSubscriptionBadgeProps, a8 as FFIDUserMenu, ao as FFIDUserMenuClassNames, ap as FFIDUserMenuProps } from '../index-CsVJTuPv.js';
2
2
  import 'react/jsx-runtime';
3
3
  import 'react';
@@ -1 +1 @@
1
- export { FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDSubscriptionBadge, FFIDUserMenu } from '../chunk-KB2JG64Z.js';
1
+ export { FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDSubscriptionBadge, FFIDUserMenu } from '../chunk-4II4R7NR.js';