@feelflow/ffid-sdk 5.19.0 → 5.22.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.
Files changed (34) hide show
  1. package/README.md +1 -0
  2. package/dist/agency/index.cjs +4 -2
  3. package/dist/agency/index.js +4 -2
  4. package/dist/announcements/index.cjs +2 -1
  5. package/dist/announcements/index.js +2 -1
  6. package/dist/{chunk-D3PZ6SZB.js → chunk-7NRO2TBG.js} +108 -53
  7. package/dist/{chunk-TW2FXASO.cjs → chunk-EM5EGNGE.cjs} +108 -53
  8. package/dist/components/index.cjs +8 -8
  9. package/dist/components/index.d.cts +1 -1
  10. package/dist/components/index.d.ts +1 -1
  11. package/dist/components/index.js +1 -1
  12. package/dist/{ffid-client-BuDM5tmq.d.cts → ffid-client-CEJMpqRZ.d.cts} +55 -9
  13. package/dist/{ffid-client-D0x8TBS5.d.ts → ffid-client-DM0ZhIDV.d.ts} +55 -9
  14. package/dist/{index-Bkul-fRw.d.cts → index-btouGGKr.d.cts} +56 -9
  15. package/dist/{index-Bkul-fRw.d.ts → index-btouGGKr.d.ts} +56 -9
  16. package/dist/index.cjs +42 -42
  17. package/dist/index.d.cts +44 -8
  18. package/dist/index.d.ts +44 -8
  19. package/dist/index.js +2 -2
  20. package/dist/legal/index.cjs +2 -1
  21. package/dist/legal/index.js +2 -1
  22. package/dist/server/index.cjs +94 -48
  23. package/dist/server/index.d.cts +3 -3
  24. package/dist/server/index.d.ts +3 -3
  25. package/dist/server/index.js +94 -48
  26. package/dist/server/test/index.d.cts +2 -2
  27. package/dist/server/test/index.d.ts +2 -2
  28. package/dist/{types-BeVl-z12.d.cts → types-Cjb9J0rL.d.cts} +2 -1
  29. package/dist/{types-BeVl-z12.d.ts → types-Cjb9J0rL.d.ts} +2 -1
  30. package/dist/webhooks/index.cjs +2 -2
  31. package/dist/webhooks/index.d.cts +1 -1
  32. package/dist/webhooks/index.d.ts +1 -1
  33. package/dist/webhooks/index.js +2 -2
  34. package/package.json +1 -1
package/README.md CHANGED
@@ -185,6 +185,7 @@ function Header() {
185
185
  refreshInterval={300000} // オプション: セッション更新間隔(ms)
186
186
  onAuthStateChange={(user) => {}} // オプション: 認証状態変更時コールバック
187
187
  onError={(error) => {}} // オプション: エラー時コールバック
188
+ cleanCallbackUrl={true} // オプション: token モードの callback URL cleanup(false で consumer に委譲、v5.22.0+)
188
189
  >
189
190
  {children}
190
191
  </FFIDProvider>
@@ -16,8 +16,10 @@ var noopLogger = {
16
16
  },
17
17
  info: () => {
18
18
  },
19
- warn: (...args) => console.warn(SDK_LOG_PREFIX, ...args),
20
- error: (...args) => console.error(SDK_LOG_PREFIX, ...args)
19
+ warn: () => {
20
+ },
21
+ error: () => {
22
+ }
21
23
  };
22
24
  var consoleLogger = {
23
25
  debug: (...args) => console.debug(SDK_LOG_PREFIX, ...args),
@@ -15,8 +15,10 @@ var noopLogger = {
15
15
  },
16
16
  info: () => {
17
17
  },
18
- warn: (...args) => console.warn(SDK_LOG_PREFIX, ...args),
19
- error: (...args) => console.error(SDK_LOG_PREFIX, ...args)
18
+ warn: () => {
19
+ },
20
+ error: () => {
21
+ }
20
22
  };
21
23
  var consoleLogger = {
22
24
  debug: (...args) => console.debug(SDK_LOG_PREFIX, ...args),
@@ -20,7 +20,8 @@ var quietLogger = {
20
20
  },
21
21
  warn: () => {
22
22
  },
23
- error: (...args) => console.error(SDK_LOG_PREFIX, ...args)
23
+ error: () => {
24
+ }
24
25
  };
25
26
  var consoleLogger = {
26
27
  debug: (...args) => console.debug(SDK_LOG_PREFIX, ...args),
@@ -18,7 +18,8 @@ var quietLogger = {
18
18
  },
19
19
  warn: () => {
20
20
  },
21
- error: (...args) => console.error(SDK_LOG_PREFIX, ...args)
21
+ error: () => {
22
+ }
22
23
  };
23
24
  var consoleLogger = {
24
25
  debug: (...args) => console.debug(SDK_LOG_PREFIX, ...args),
@@ -264,10 +264,12 @@ var OAUTH_INTROSPECT_ENDPOINT = "/api/v1/oauth/introspect";
264
264
  var CACHE_KEY_PREFIX = "ffid:introspect:";
265
265
  var HEX_BASE = 16;
266
266
  var HEX_BYTE_WIDTH = 2;
267
- var TOKEN_LOG_PREFIX_LENGTH = 8;
268
267
  function createVerifyAccessToken(deps) {
269
268
  const { authMode, baseUrl, serviceCode, serviceApiKey, verifyStrategy, logger, createError, errorCodes, cache, timeout } = deps;
270
269
  let jwtVerify2 = null;
270
+ let cacheReadFailureEscalated = false;
271
+ let cacheWriteFailureEscalated = false;
272
+ let missingSubtleCryptoLogged = false;
271
273
  function getJwtVerifier() {
272
274
  if (!jwtVerify2) {
273
275
  jwtVerify2 = createJwtVerifier({
@@ -327,8 +329,9 @@ function createVerifyAccessToken(deps) {
327
329
  }
328
330
  const url = `${baseUrl}${OAUTH_INTROSPECT_ENDPOINT}`;
329
331
  logger.debug("Verifying access token:", url);
330
- const cacheKey = cache ? await buildCacheKey(accessToken, cacheKeySuffix) : "";
331
- if (cache) {
332
+ const cacheKey = cache ? await buildCacheKey(accessToken, cacheKeySuffix) : null;
333
+ let verificationCacheDegraded = cache !== void 0 && cacheKey === null;
334
+ if (cache && cacheKey !== null) {
332
335
  try {
333
336
  const cached = await cache.adapter.get(cacheKey);
334
337
  if (cached && typeof cached === "object" && "sub" in cached) {
@@ -336,7 +339,16 @@ function createVerifyAccessToken(deps) {
336
339
  return { data: cached };
337
340
  }
338
341
  } catch (cacheError) {
339
- logger.warn("Cache read failed, falling back to API call:", cacheError);
342
+ verificationCacheDegraded = true;
343
+ if (!cacheReadFailureEscalated) {
344
+ cacheReadFailureEscalated = true;
345
+ logger.error(
346
+ "Cache read failed \u2014 falling back to API call. Redis \u7B49\u306E cache adapter \u969C\u5BB3\u6642\u306F\u5168 verification \u304C introspect \u306B degrade \u3057\u307E\u3059\uFF08\u3053\u306E error \u306F instance \u3054\u3068\u306B\u521D\u56DE\u306E\u307F\u3001\u4EE5\u964D\u306F warn\uFF09:",
347
+ cacheError
348
+ );
349
+ } else {
350
+ logger.warn("Cache read failed, falling back to API call:", cacheError);
351
+ }
340
352
  }
341
353
  }
342
354
  const fetchOptions = {
@@ -431,31 +443,40 @@ function createVerifyAccessToken(deps) {
431
443
  }
432
444
  } : base;
433
445
  const userinfo = normalizeUserinfo(raw);
434
- if (cache) {
446
+ if (cache && cacheKey !== null) {
435
447
  try {
436
448
  await cache.adapter.set(cacheKey, userinfo, cache.ttl);
437
449
  } catch (cacheError) {
438
- logger.warn("Cache write failed (result still returned):", cacheError);
450
+ verificationCacheDegraded = true;
451
+ if (!cacheWriteFailureEscalated) {
452
+ cacheWriteFailureEscalated = true;
453
+ logger.error(
454
+ "Cache write failed \u2014 result still returned. cache adapter \u969C\u5BB3\u6642\u306F introspect \u7D50\u679C\u304C\u30AD\u30E3\u30C3\u30B7\u30E5\u3055\u308C\u305A\u5168 verification \u304C API \u547C\u3073\u51FA\u3057\u306B degrade \u3057\u307E\u3059\uFF08\u3053\u306E error \u306F instance \u3054\u3068\u306B\u521D\u56DE\u306E\u307F\u3001\u4EE5\u964D\u306F warn\uFF09:",
455
+ cacheError
456
+ );
457
+ } else {
458
+ logger.warn("Cache write failed (result still returned):", cacheError);
459
+ }
439
460
  }
440
461
  }
441
- return { data: userinfo };
462
+ return verificationCacheDegraded ? { data: userinfo, meta: { verificationCacheDegraded: true } } : { data: userinfo };
442
463
  }
443
464
  async function buildCacheKey(token, suffix) {
444
- let key;
445
- if (typeof globalThis.crypto?.subtle?.digest === "function") {
446
- const encoder = new TextEncoder();
447
- const data = encoder.encode(token);
448
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
449
- const hashArray = Array.from(new Uint8Array(hashBuffer));
450
- const hashHex = hashArray.map((b) => b.toString(HEX_BASE).padStart(HEX_BYTE_WIDTH, "0")).join("");
451
- key = CACHE_KEY_PREFIX + hashHex;
452
- } else {
453
- const tokenPrefix = token.length > TOKEN_LOG_PREFIX_LENGTH ? token.substring(0, TOKEN_LOG_PREFIX_LENGTH) + "..." : "***";
454
- logger.warn(
455
- `crypto.subtle \u304C\u5229\u7528\u3067\u304D\u306A\u3044\u305F\u3081\u3001\u30AD\u30E3\u30C3\u30B7\u30E5\u30AD\u30FC\u306B\u30CF\u30C3\u30B7\u30E5\u5316\u3055\u308C\u3066\u3044\u306A\u3044\u30C8\u30FC\u30AF\u30F3\u3092\u4F7F\u7528\u3057\u307E\u3059 (token prefix: ${tokenPrefix})`
456
- );
457
- key = CACHE_KEY_PREFIX + token;
458
- }
465
+ if (typeof globalThis.crypto?.subtle?.digest !== "function") {
466
+ if (!missingSubtleCryptoLogged) {
467
+ missingSubtleCryptoLogged = true;
468
+ logger.error(
469
+ "crypto.subtle \u304C\u5229\u7528\u3067\u304D\u306A\u3044\u305F\u3081 introspect \u7D50\u679C\u306E\u30AD\u30E3\u30C3\u30B7\u30E5\u3092\u7121\u52B9\u5316\u3057\u307E\u3057\u305F\uFF08\u30CF\u30C3\u30B7\u30E5\u5316\u3055\u308C\u3066\u3044\u306A\u3044\u30C8\u30FC\u30AF\u30F3\u3092 cache key \u306B\u4F7F\u308F\u306A\u3044\u305F\u3081\u306E\u63AA\u7F6E #4119\uFF09\u3002\u5168 verification \u304C API \u547C\u3073\u51FA\u3057\u306B\u306A\u308A\u307E\u3059"
470
+ );
471
+ }
472
+ return null;
473
+ }
474
+ const encoder = new TextEncoder();
475
+ const data = encoder.encode(token);
476
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
477
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
478
+ const hashHex = hashArray.map((b) => b.toString(HEX_BASE).padStart(HEX_BYTE_WIDTH, "0")).join("");
479
+ const key = CACHE_KEY_PREFIX + hashHex;
459
480
  return suffix ? `${key}:${suffix}` : key;
460
481
  }
461
482
  return verifyAccessToken;
@@ -1204,7 +1225,7 @@ function createNonContractMethods(deps) {
1204
1225
  }
1205
1226
 
1206
1227
  // src/client/version-check.ts
1207
- var SDK_VERSION = "5.19.0";
1228
+ var SDK_VERSION = "5.22.0";
1208
1229
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
1209
1230
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
1210
1231
  function sdkHeaders() {
@@ -1446,33 +1467,48 @@ function createOAuthTokenMethods(deps) {
1446
1467
  async function signOutToken() {
1447
1468
  const tokens = tokenStore.getTokens();
1448
1469
  tokenStore.clearTokens();
1470
+ const allRevoked = {
1471
+ revoked: true,
1472
+ accessTokenRevoked: true,
1473
+ refreshTokenRevoked: true
1474
+ };
1449
1475
  if (!tokens) {
1450
1476
  logger.debug("No tokens to revoke");
1451
- return { data: void 0 };
1477
+ return { data: allRevoked };
1452
1478
  }
1453
- const url = `${baseUrl}${OAUTH_REVOKE_ENDPOINT}`;
1454
- logger.debug("Revoking token:", url);
1455
- try {
1456
- const response = await fetch(url, {
1457
- method: "POST",
1458
- credentials: "omit",
1459
- headers: { "Content-Type": "application/x-www-form-urlencoded", ...sdkHeaders() },
1460
- body: new URLSearchParams({
1461
- token: tokens.accessToken,
1462
- client_id: clientId
1463
- }).toString()
1464
- });
1465
- if (!response.ok) {
1466
- logger.warn(
1467
- "\u30C8\u30FC\u30AF\u30F3\u7121\u52B9\u5316\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u30B5\u30FC\u30D0\u30FC\u3067\u5931\u6557\u3057\u307E\u3057\u305F:",
1468
- `status=${response.status}`
1469
- );
1479
+ async function revokeOAuthToken(token, tokenTypeHint) {
1480
+ const url = `${baseUrl}${OAUTH_REVOKE_ENDPOINT}`;
1481
+ try {
1482
+ const response = await fetch(url, {
1483
+ method: "POST",
1484
+ credentials: "omit",
1485
+ headers: { "Content-Type": "application/x-www-form-urlencoded", ...sdkHeaders() },
1486
+ body: new URLSearchParams({
1487
+ token,
1488
+ client_id: clientId,
1489
+ token_type_hint: tokenTypeHint
1490
+ }).toString()
1491
+ });
1492
+ return response.ok;
1493
+ } catch {
1494
+ return false;
1470
1495
  }
1471
- } catch (error) {
1472
- logger.warn("\u30C8\u30FC\u30AF\u30F3\u7121\u52B9\u5316\u306E\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30EA\u30AF\u30A8\u30B9\u30C8\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1496
+ }
1497
+ logger.debug("Revoking tokens:", `${baseUrl}${OAUTH_REVOKE_ENDPOINT}`);
1498
+ const accessTokenRevoked = await revokeOAuthToken(tokens.accessToken, "access_token");
1499
+ if (!accessTokenRevoked) {
1500
+ logger.warn("\u30A2\u30AF\u30BB\u30B9\u30C8\u30FC\u30AF\u30F3\u306E\u7121\u52B9\u5316\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u30B5\u30FC\u30D0\u30FC\u3067\u5931\u6557\u3057\u307E\u3057\u305F");
1501
+ }
1502
+ const refreshTokenRevoked = tokens.refreshToken ? await revokeOAuthToken(tokens.refreshToken, "refresh_token") : true;
1503
+ if (tokens.refreshToken && !refreshTokenRevoked) {
1504
+ logger.warn("\u30EA\u30D5\u30EC\u30C3\u30B7\u30E5\u30C8\u30FC\u30AF\u30F3\u306E\u7121\u52B9\u5316\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u30B5\u30FC\u30D0\u30FC\u3067\u5931\u6557\u3057\u307E\u3057\u305F");
1505
+ }
1506
+ const revoked = accessTokenRevoked && refreshTokenRevoked;
1507
+ if (!revoked) {
1508
+ logger.warn("\u30C8\u30FC\u30AF\u30F3\u7121\u52B9\u5316\u304C\u4E00\u90E8\u307E\u305F\u306F\u5168\u90E8\u5931\u6557\u3057\u307E\u3057\u305F\uFF08\u30ED\u30FC\u30AB\u30EB\u30C8\u30FC\u30AF\u30F3\u306F\u30AF\u30EA\u30A2\u6E08\u307F\uFF09");
1473
1509
  }
1474
1510
  logger.debug("Token sign-out completed");
1475
- return { data: void 0 };
1511
+ return { data: { revoked, accessTokenRevoked, refreshTokenRevoked } };
1476
1512
  }
1477
1513
  return { exchangeCodeForTokens, refreshAccessToken, signOutToken };
1478
1514
  }
@@ -1636,7 +1672,7 @@ function createSessionMethods(deps) {
1636
1672
  }
1637
1673
  if (response.status === NO_CONTENT_STATUS) {
1638
1674
  logger.debug("Response: 204 No Content");
1639
- return { data: void 0 };
1675
+ return { data: { revoked: true } };
1640
1676
  }
1641
1677
  if (!response.ok) {
1642
1678
  try {
@@ -1663,7 +1699,7 @@ function createSessionMethods(deps) {
1663
1699
  }
1664
1700
  }
1665
1701
  logger.debug("Response:", response.status);
1666
- return { data: void 0 };
1702
+ return { data: { revoked: true } };
1667
1703
  }
1668
1704
  return { getSession, signOutCookie };
1669
1705
  }
@@ -1881,6 +1917,9 @@ var REDIRECT_LOOP_WINDOW_MS = 6e4;
1881
1917
  var REDIRECT_LOOP_THRESHOLD = 3;
1882
1918
  var storageReadFailureLogged = false;
1883
1919
  var storageWriteFailureLogged = false;
1920
+ function isLoopDetectionDegraded() {
1921
+ return storageReadFailureLogged || storageWriteFailureLogged;
1922
+ }
1884
1923
  function logStorageReadFailure(logger, err) {
1885
1924
  if (storageReadFailureLogged) return;
1886
1925
  storageReadFailureLogged = true;
@@ -2025,9 +2064,16 @@ function createRedirectMethods(deps) {
2025
2064
  }
2026
2065
  const state = generateRandomState();
2027
2066
  if (!storeState(state, logger)) {
2067
+ cleanupVerifierStorage(logger);
2068
+ cleanupStateStorage(logger);
2028
2069
  logger.error(
2029
- "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"
2070
+ "redirectToAuthorize: OAuth state \u3092\u4FDD\u5B58\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\uFF08storage \u5229\u7528\u4E0D\u53EF\uFF09\u3002\u8A8D\u53EF\u30EA\u30C0\u30A4\u30EC\u30AF\u30C8\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F"
2030
2071
  );
2072
+ return {
2073
+ success: false,
2074
+ code: "state_persistence_failed",
2075
+ error: "OAuth state \u3092\u4FDD\u5B58\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\uFF08storage \u5229\u7528\u4E0D\u53EF\uFF09\u3002\u8A8D\u53EF\u30EA\u30C0\u30A4\u30EC\u30AF\u30C8\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F"
2076
+ };
2031
2077
  }
2032
2078
  const redirectUri = resolvedRedirectUri ?? window.location.origin + window.location.pathname;
2033
2079
  const params = new URLSearchParams({
@@ -2062,7 +2108,7 @@ function createRedirectMethods(deps) {
2062
2108
  rollbackLastRedirectAttempt(authorizeKey, logger);
2063
2109
  throw err;
2064
2110
  }
2065
- return { success: true };
2111
+ return isLoopDetectionDegraded() ? { success: true, loopDetectionDisabled: true } : { success: true };
2066
2112
  }
2067
2113
  async function redirectToLogin(options) {
2068
2114
  if (typeof window === "undefined") {
@@ -3452,7 +3498,7 @@ async function handleRedirectCallback(client, options) {
3452
3498
  };
3453
3499
  }
3454
3500
  logger.error(
3455
- "handleRedirectCallback: OAuth state mismatch or omitted (possible CSRF) \u2014 \u8A8D\u8A3C\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F"
3501
+ "handleRedirectCallback: OAuth state mismatch or omitted (possible CSRF) \u2014 \u8A8D\u8A3C\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F\u3002\u6D88\u8CBB\u6E08\u307F callback \u30D1\u30E9\u30E1\u30FC\u30BF\u306E\u518D\u51E6\u7406\uFF08`cleanUrl: false` \u5229\u7528\u6642\u306E cleanup \u6F0F\u308C\u3084 reload \u7B49\uFF09\u3067\u3082\u767A\u751F\u3057\u307E\u3059"
3456
3502
  );
3457
3503
  return {
3458
3504
  error: client.createError(
@@ -3465,7 +3511,11 @@ async function handleRedirectCallback(client, options) {
3465
3511
  const verifier = retrieveCodeVerifier(logger);
3466
3512
  const result = await client.exchangeCodeForTokens(code, verifier ?? void 0);
3467
3513
  if (!result.error && hasWindow) {
3468
- cleanCallbackUrlParams(logger);
3514
+ if (options?.cleanUrl !== false) {
3515
+ cleanCallbackUrlParams(logger);
3516
+ } else {
3517
+ logger.debug("handleRedirectCallback: cleanUrl=false \u306E\u305F\u3081 URL cleanup \u3092 consumer \u306B\u59D4\u8B72\u3057\u307E\u3057\u305F");
3518
+ }
3469
3519
  }
3470
3520
  return result;
3471
3521
  }
@@ -3484,7 +3534,8 @@ function FFIDProvider({
3484
3534
  onAuthStateChange,
3485
3535
  onError,
3486
3536
  authMode,
3487
- clientId
3537
+ clientId,
3538
+ cleanCallbackUrl = true
3488
3539
  }) {
3489
3540
  const [user, setUser] = useState(null);
3490
3541
  const [organizations, setOrganizations] = useState([]);
@@ -3636,14 +3687,17 @@ function FFIDProvider({
3636
3687
  },
3637
3688
  [organizations, client]
3638
3689
  );
3690
+ const handledCodeRef = useRef(null);
3639
3691
  useEffect(() => {
3640
3692
  if (client.authMode !== "token") return;
3641
3693
  if (typeof window === "undefined") return;
3642
3694
  const urlParams = new URLSearchParams(window.location.search);
3643
3695
  const code = urlParams.get("code");
3644
3696
  if (!code) return;
3697
+ if (handledCodeRef.current === code) return;
3698
+ handledCodeRef.current = code;
3645
3699
  client.logger.debug("Authorization code detected, exchanging for tokens");
3646
- handleRedirectCallback(client).then((result) => {
3700
+ handleRedirectCallback(client, { cleanUrl: cleanCallbackUrl }).then((result) => {
3647
3701
  if (result.error) {
3648
3702
  client.logger.error("Token exchange failed:", result.error);
3649
3703
  setError(result.error);
@@ -4329,7 +4383,8 @@ var quietLogger = {
4329
4383
  },
4330
4384
  warn: () => {
4331
4385
  },
4332
- error: (...args) => console.error(SDK_LOG_PREFIX2, ...args)
4386
+ error: () => {
4387
+ }
4333
4388
  };
4334
4389
  var consoleLogger2 = {
4335
4390
  debug: (...args) => console.debug(SDK_LOG_PREFIX2, ...args),
@@ -266,10 +266,12 @@ var OAUTH_INTROSPECT_ENDPOINT = "/api/v1/oauth/introspect";
266
266
  var CACHE_KEY_PREFIX = "ffid:introspect:";
267
267
  var HEX_BASE = 16;
268
268
  var HEX_BYTE_WIDTH = 2;
269
- var TOKEN_LOG_PREFIX_LENGTH = 8;
270
269
  function createVerifyAccessToken(deps) {
271
270
  const { authMode, baseUrl, serviceCode, serviceApiKey, verifyStrategy, logger, createError, errorCodes, cache, timeout } = deps;
272
271
  let jwtVerify2 = null;
272
+ let cacheReadFailureEscalated = false;
273
+ let cacheWriteFailureEscalated = false;
274
+ let missingSubtleCryptoLogged = false;
273
275
  function getJwtVerifier() {
274
276
  if (!jwtVerify2) {
275
277
  jwtVerify2 = createJwtVerifier({
@@ -329,8 +331,9 @@ function createVerifyAccessToken(deps) {
329
331
  }
330
332
  const url = `${baseUrl}${OAUTH_INTROSPECT_ENDPOINT}`;
331
333
  logger.debug("Verifying access token:", url);
332
- const cacheKey = cache ? await buildCacheKey(accessToken, cacheKeySuffix) : "";
333
- if (cache) {
334
+ const cacheKey = cache ? await buildCacheKey(accessToken, cacheKeySuffix) : null;
335
+ let verificationCacheDegraded = cache !== void 0 && cacheKey === null;
336
+ if (cache && cacheKey !== null) {
334
337
  try {
335
338
  const cached = await cache.adapter.get(cacheKey);
336
339
  if (cached && typeof cached === "object" && "sub" in cached) {
@@ -338,7 +341,16 @@ function createVerifyAccessToken(deps) {
338
341
  return { data: cached };
339
342
  }
340
343
  } catch (cacheError) {
341
- logger.warn("Cache read failed, falling back to API call:", cacheError);
344
+ verificationCacheDegraded = true;
345
+ if (!cacheReadFailureEscalated) {
346
+ cacheReadFailureEscalated = true;
347
+ logger.error(
348
+ "Cache read failed \u2014 falling back to API call. Redis \u7B49\u306E cache adapter \u969C\u5BB3\u6642\u306F\u5168 verification \u304C introspect \u306B degrade \u3057\u307E\u3059\uFF08\u3053\u306E error \u306F instance \u3054\u3068\u306B\u521D\u56DE\u306E\u307F\u3001\u4EE5\u964D\u306F warn\uFF09:",
349
+ cacheError
350
+ );
351
+ } else {
352
+ logger.warn("Cache read failed, falling back to API call:", cacheError);
353
+ }
342
354
  }
343
355
  }
344
356
  const fetchOptions = {
@@ -433,31 +445,40 @@ function createVerifyAccessToken(deps) {
433
445
  }
434
446
  } : base;
435
447
  const userinfo = normalizeUserinfo(raw);
436
- if (cache) {
448
+ if (cache && cacheKey !== null) {
437
449
  try {
438
450
  await cache.adapter.set(cacheKey, userinfo, cache.ttl);
439
451
  } catch (cacheError) {
440
- logger.warn("Cache write failed (result still returned):", cacheError);
452
+ verificationCacheDegraded = true;
453
+ if (!cacheWriteFailureEscalated) {
454
+ cacheWriteFailureEscalated = true;
455
+ logger.error(
456
+ "Cache write failed \u2014 result still returned. cache adapter \u969C\u5BB3\u6642\u306F introspect \u7D50\u679C\u304C\u30AD\u30E3\u30C3\u30B7\u30E5\u3055\u308C\u305A\u5168 verification \u304C API \u547C\u3073\u51FA\u3057\u306B degrade \u3057\u307E\u3059\uFF08\u3053\u306E error \u306F instance \u3054\u3068\u306B\u521D\u56DE\u306E\u307F\u3001\u4EE5\u964D\u306F warn\uFF09:",
457
+ cacheError
458
+ );
459
+ } else {
460
+ logger.warn("Cache write failed (result still returned):", cacheError);
461
+ }
441
462
  }
442
463
  }
443
- return { data: userinfo };
464
+ return verificationCacheDegraded ? { data: userinfo, meta: { verificationCacheDegraded: true } } : { data: userinfo };
444
465
  }
445
466
  async function buildCacheKey(token, suffix) {
446
- let key;
447
- if (typeof globalThis.crypto?.subtle?.digest === "function") {
448
- const encoder = new TextEncoder();
449
- const data = encoder.encode(token);
450
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
451
- const hashArray = Array.from(new Uint8Array(hashBuffer));
452
- const hashHex = hashArray.map((b) => b.toString(HEX_BASE).padStart(HEX_BYTE_WIDTH, "0")).join("");
453
- key = CACHE_KEY_PREFIX + hashHex;
454
- } else {
455
- const tokenPrefix = token.length > TOKEN_LOG_PREFIX_LENGTH ? token.substring(0, TOKEN_LOG_PREFIX_LENGTH) + "..." : "***";
456
- logger.warn(
457
- `crypto.subtle \u304C\u5229\u7528\u3067\u304D\u306A\u3044\u305F\u3081\u3001\u30AD\u30E3\u30C3\u30B7\u30E5\u30AD\u30FC\u306B\u30CF\u30C3\u30B7\u30E5\u5316\u3055\u308C\u3066\u3044\u306A\u3044\u30C8\u30FC\u30AF\u30F3\u3092\u4F7F\u7528\u3057\u307E\u3059 (token prefix: ${tokenPrefix})`
458
- );
459
- key = CACHE_KEY_PREFIX + token;
460
- }
467
+ if (typeof globalThis.crypto?.subtle?.digest !== "function") {
468
+ if (!missingSubtleCryptoLogged) {
469
+ missingSubtleCryptoLogged = true;
470
+ logger.error(
471
+ "crypto.subtle \u304C\u5229\u7528\u3067\u304D\u306A\u3044\u305F\u3081 introspect \u7D50\u679C\u306E\u30AD\u30E3\u30C3\u30B7\u30E5\u3092\u7121\u52B9\u5316\u3057\u307E\u3057\u305F\uFF08\u30CF\u30C3\u30B7\u30E5\u5316\u3055\u308C\u3066\u3044\u306A\u3044\u30C8\u30FC\u30AF\u30F3\u3092 cache key \u306B\u4F7F\u308F\u306A\u3044\u305F\u3081\u306E\u63AA\u7F6E #4119\uFF09\u3002\u5168 verification \u304C API \u547C\u3073\u51FA\u3057\u306B\u306A\u308A\u307E\u3059"
472
+ );
473
+ }
474
+ return null;
475
+ }
476
+ const encoder = new TextEncoder();
477
+ const data = encoder.encode(token);
478
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
479
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
480
+ const hashHex = hashArray.map((b) => b.toString(HEX_BASE).padStart(HEX_BYTE_WIDTH, "0")).join("");
481
+ const key = CACHE_KEY_PREFIX + hashHex;
461
482
  return suffix ? `${key}:${suffix}` : key;
462
483
  }
463
484
  return verifyAccessToken;
@@ -1206,7 +1227,7 @@ function createNonContractMethods(deps) {
1206
1227
  }
1207
1228
 
1208
1229
  // src/client/version-check.ts
1209
- var SDK_VERSION = "5.19.0";
1230
+ var SDK_VERSION = "5.22.0";
1210
1231
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
1211
1232
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
1212
1233
  function sdkHeaders() {
@@ -1448,33 +1469,48 @@ function createOAuthTokenMethods(deps) {
1448
1469
  async function signOutToken() {
1449
1470
  const tokens = tokenStore.getTokens();
1450
1471
  tokenStore.clearTokens();
1472
+ const allRevoked = {
1473
+ revoked: true,
1474
+ accessTokenRevoked: true,
1475
+ refreshTokenRevoked: true
1476
+ };
1451
1477
  if (!tokens) {
1452
1478
  logger.debug("No tokens to revoke");
1453
- return { data: void 0 };
1479
+ return { data: allRevoked };
1454
1480
  }
1455
- const url = `${baseUrl}${OAUTH_REVOKE_ENDPOINT}`;
1456
- logger.debug("Revoking token:", url);
1457
- try {
1458
- const response = await fetch(url, {
1459
- method: "POST",
1460
- credentials: "omit",
1461
- headers: { "Content-Type": "application/x-www-form-urlencoded", ...sdkHeaders() },
1462
- body: new URLSearchParams({
1463
- token: tokens.accessToken,
1464
- client_id: clientId
1465
- }).toString()
1466
- });
1467
- if (!response.ok) {
1468
- logger.warn(
1469
- "\u30C8\u30FC\u30AF\u30F3\u7121\u52B9\u5316\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u30B5\u30FC\u30D0\u30FC\u3067\u5931\u6557\u3057\u307E\u3057\u305F:",
1470
- `status=${response.status}`
1471
- );
1481
+ async function revokeOAuthToken(token, tokenTypeHint) {
1482
+ const url = `${baseUrl}${OAUTH_REVOKE_ENDPOINT}`;
1483
+ try {
1484
+ const response = await fetch(url, {
1485
+ method: "POST",
1486
+ credentials: "omit",
1487
+ headers: { "Content-Type": "application/x-www-form-urlencoded", ...sdkHeaders() },
1488
+ body: new URLSearchParams({
1489
+ token,
1490
+ client_id: clientId,
1491
+ token_type_hint: tokenTypeHint
1492
+ }).toString()
1493
+ });
1494
+ return response.ok;
1495
+ } catch {
1496
+ return false;
1472
1497
  }
1473
- } catch (error) {
1474
- logger.warn("\u30C8\u30FC\u30AF\u30F3\u7121\u52B9\u5316\u306E\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30EA\u30AF\u30A8\u30B9\u30C8\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
1498
+ }
1499
+ logger.debug("Revoking tokens:", `${baseUrl}${OAUTH_REVOKE_ENDPOINT}`);
1500
+ const accessTokenRevoked = await revokeOAuthToken(tokens.accessToken, "access_token");
1501
+ if (!accessTokenRevoked) {
1502
+ logger.warn("\u30A2\u30AF\u30BB\u30B9\u30C8\u30FC\u30AF\u30F3\u306E\u7121\u52B9\u5316\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u30B5\u30FC\u30D0\u30FC\u3067\u5931\u6557\u3057\u307E\u3057\u305F");
1503
+ }
1504
+ const refreshTokenRevoked = tokens.refreshToken ? await revokeOAuthToken(tokens.refreshToken, "refresh_token") : true;
1505
+ if (tokens.refreshToken && !refreshTokenRevoked) {
1506
+ logger.warn("\u30EA\u30D5\u30EC\u30C3\u30B7\u30E5\u30C8\u30FC\u30AF\u30F3\u306E\u7121\u52B9\u5316\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u30B5\u30FC\u30D0\u30FC\u3067\u5931\u6557\u3057\u307E\u3057\u305F");
1507
+ }
1508
+ const revoked = accessTokenRevoked && refreshTokenRevoked;
1509
+ if (!revoked) {
1510
+ logger.warn("\u30C8\u30FC\u30AF\u30F3\u7121\u52B9\u5316\u304C\u4E00\u90E8\u307E\u305F\u306F\u5168\u90E8\u5931\u6557\u3057\u307E\u3057\u305F\uFF08\u30ED\u30FC\u30AB\u30EB\u30C8\u30FC\u30AF\u30F3\u306F\u30AF\u30EA\u30A2\u6E08\u307F\uFF09");
1475
1511
  }
1476
1512
  logger.debug("Token sign-out completed");
1477
- return { data: void 0 };
1513
+ return { data: { revoked, accessTokenRevoked, refreshTokenRevoked } };
1478
1514
  }
1479
1515
  return { exchangeCodeForTokens, refreshAccessToken, signOutToken };
1480
1516
  }
@@ -1638,7 +1674,7 @@ function createSessionMethods(deps) {
1638
1674
  }
1639
1675
  if (response.status === NO_CONTENT_STATUS) {
1640
1676
  logger.debug("Response: 204 No Content");
1641
- return { data: void 0 };
1677
+ return { data: { revoked: true } };
1642
1678
  }
1643
1679
  if (!response.ok) {
1644
1680
  try {
@@ -1665,7 +1701,7 @@ function createSessionMethods(deps) {
1665
1701
  }
1666
1702
  }
1667
1703
  logger.debug("Response:", response.status);
1668
- return { data: void 0 };
1704
+ return { data: { revoked: true } };
1669
1705
  }
1670
1706
  return { getSession, signOutCookie };
1671
1707
  }
@@ -1883,6 +1919,9 @@ var REDIRECT_LOOP_WINDOW_MS = 6e4;
1883
1919
  var REDIRECT_LOOP_THRESHOLD = 3;
1884
1920
  var storageReadFailureLogged = false;
1885
1921
  var storageWriteFailureLogged = false;
1922
+ function isLoopDetectionDegraded() {
1923
+ return storageReadFailureLogged || storageWriteFailureLogged;
1924
+ }
1886
1925
  function logStorageReadFailure(logger, err) {
1887
1926
  if (storageReadFailureLogged) return;
1888
1927
  storageReadFailureLogged = true;
@@ -2027,9 +2066,16 @@ function createRedirectMethods(deps) {
2027
2066
  }
2028
2067
  const state = generateRandomState();
2029
2068
  if (!storeState(state, logger)) {
2069
+ cleanupVerifierStorage(logger);
2070
+ cleanupStateStorage(logger);
2030
2071
  logger.error(
2031
- "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"
2072
+ "redirectToAuthorize: OAuth state \u3092\u4FDD\u5B58\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\uFF08storage \u5229\u7528\u4E0D\u53EF\uFF09\u3002\u8A8D\u53EF\u30EA\u30C0\u30A4\u30EC\u30AF\u30C8\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F"
2032
2073
  );
2074
+ return {
2075
+ success: false,
2076
+ code: "state_persistence_failed",
2077
+ error: "OAuth state \u3092\u4FDD\u5B58\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\uFF08storage \u5229\u7528\u4E0D\u53EF\uFF09\u3002\u8A8D\u53EF\u30EA\u30C0\u30A4\u30EC\u30AF\u30C8\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F"
2078
+ };
2033
2079
  }
2034
2080
  const redirectUri = resolvedRedirectUri ?? window.location.origin + window.location.pathname;
2035
2081
  const params = new URLSearchParams({
@@ -2064,7 +2110,7 @@ function createRedirectMethods(deps) {
2064
2110
  rollbackLastRedirectAttempt(authorizeKey, logger);
2065
2111
  throw err;
2066
2112
  }
2067
- return { success: true };
2113
+ return isLoopDetectionDegraded() ? { success: true, loopDetectionDisabled: true } : { success: true };
2068
2114
  }
2069
2115
  async function redirectToLogin(options) {
2070
2116
  if (typeof window === "undefined") {
@@ -3454,7 +3500,7 @@ async function handleRedirectCallback(client, options) {
3454
3500
  };
3455
3501
  }
3456
3502
  logger.error(
3457
- "handleRedirectCallback: OAuth state mismatch or omitted (possible CSRF) \u2014 \u8A8D\u8A3C\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F"
3503
+ "handleRedirectCallback: OAuth state mismatch or omitted (possible CSRF) \u2014 \u8A8D\u8A3C\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F\u3002\u6D88\u8CBB\u6E08\u307F callback \u30D1\u30E9\u30E1\u30FC\u30BF\u306E\u518D\u51E6\u7406\uFF08`cleanUrl: false` \u5229\u7528\u6642\u306E cleanup \u6F0F\u308C\u3084 reload \u7B49\uFF09\u3067\u3082\u767A\u751F\u3057\u307E\u3059"
3458
3504
  );
3459
3505
  return {
3460
3506
  error: client.createError(
@@ -3467,7 +3513,11 @@ async function handleRedirectCallback(client, options) {
3467
3513
  const verifier = retrieveCodeVerifier(logger);
3468
3514
  const result = await client.exchangeCodeForTokens(code, verifier ?? void 0);
3469
3515
  if (!result.error && hasWindow) {
3470
- cleanCallbackUrlParams(logger);
3516
+ if (options?.cleanUrl !== false) {
3517
+ cleanCallbackUrlParams(logger);
3518
+ } else {
3519
+ logger.debug("handleRedirectCallback: cleanUrl=false \u306E\u305F\u3081 URL cleanup \u3092 consumer \u306B\u59D4\u8B72\u3057\u307E\u3057\u305F");
3520
+ }
3471
3521
  }
3472
3522
  return result;
3473
3523
  }
@@ -3486,7 +3536,8 @@ function FFIDProvider({
3486
3536
  onAuthStateChange,
3487
3537
  onError,
3488
3538
  authMode,
3489
- clientId
3539
+ clientId,
3540
+ cleanCallbackUrl = true
3490
3541
  }) {
3491
3542
  const [user, setUser] = react.useState(null);
3492
3543
  const [organizations, setOrganizations] = react.useState([]);
@@ -3638,14 +3689,17 @@ function FFIDProvider({
3638
3689
  },
3639
3690
  [organizations, client]
3640
3691
  );
3692
+ const handledCodeRef = react.useRef(null);
3641
3693
  react.useEffect(() => {
3642
3694
  if (client.authMode !== "token") return;
3643
3695
  if (typeof window === "undefined") return;
3644
3696
  const urlParams = new URLSearchParams(window.location.search);
3645
3697
  const code = urlParams.get("code");
3646
3698
  if (!code) return;
3699
+ if (handledCodeRef.current === code) return;
3700
+ handledCodeRef.current = code;
3647
3701
  client.logger.debug("Authorization code detected, exchanging for tokens");
3648
- handleRedirectCallback(client).then((result) => {
3702
+ handleRedirectCallback(client, { cleanUrl: cleanCallbackUrl }).then((result) => {
3649
3703
  if (result.error) {
3650
3704
  client.logger.error("Token exchange failed:", result.error);
3651
3705
  setError(result.error);
@@ -4331,7 +4385,8 @@ var quietLogger = {
4331
4385
  },
4332
4386
  warn: () => {
4333
4387
  },
4334
- error: (...args) => console.error(SDK_LOG_PREFIX2, ...args)
4388
+ error: () => {
4389
+ }
4335
4390
  };
4336
4391
  var consoleLogger2 = {
4337
4392
  debug: (...args) => console.debug(SDK_LOG_PREFIX2, ...args),