@feelflow/ffid-sdk 10.0.0 → 11.0.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.
@@ -310,6 +310,9 @@ var OAUTH_INTROSPECT_ENDPOINT = "/api/v1/oauth/introspect";
310
310
  var CACHE_KEY_PREFIX = "ffid:introspect:";
311
311
  var HEX_BASE = 16;
312
312
  var HEX_BYTE_WIDTH = 2;
313
+ var HTTP_TOO_MANY_REQUESTS = 429;
314
+ var HTTP_SERVER_ERROR_MIN = 500;
315
+ var HTTP_SERVER_ERROR_LIMIT = 600;
313
316
  function createVerifyAccessToken(deps) {
314
317
  const { authMode, baseUrl, serviceCode, serviceApiKey, verifyStrategy, logger, createError, errorCodes, cache, timeout } = deps;
315
318
  let jwtVerify2 = null;
@@ -364,6 +367,16 @@ function createVerifyAccessToken(deps) {
364
367
  }
365
368
  return verifyViaIntrospect(accessToken);
366
369
  }
370
+ function introspectionUnavailable(status) {
371
+ logger.error("Token introspection unavailable", { status });
372
+ return {
373
+ error: {
374
+ code: errorCodes.NETWORK_ERROR,
375
+ message: `\u30C8\u30FC\u30AF\u30F3\u691C\u8A3C\u30B5\u30FC\u30D3\u30B9\u304C\u4E00\u6642\u7684\u306B\u5229\u7528\u3067\u304D\u307E\u305B\u3093 (status: ${status})`,
376
+ details: { status }
377
+ }
378
+ };
379
+ }
367
380
  async function verifyViaIntrospect(accessToken, cacheKeySuffix) {
368
381
  if (!serviceApiKey) {
369
382
  return {
@@ -421,10 +434,12 @@ function createVerifyAccessToken(deps) {
421
434
  }
422
435
  };
423
436
  }
437
+ const upstreamUnavailable = response.status === HTTP_TOO_MANY_REQUESTS || response.status >= HTTP_SERVER_ERROR_MIN && response.status < HTTP_SERVER_ERROR_LIMIT;
424
438
  let introspectResponse;
425
439
  try {
426
440
  introspectResponse = await response.json();
427
441
  } catch (parseError) {
442
+ if (upstreamUnavailable) return introspectionUnavailable(response.status);
428
443
  logger.error("Parse error during token verification:", parseError);
429
444
  return {
430
445
  error: {
@@ -433,6 +448,7 @@ function createVerifyAccessToken(deps) {
433
448
  }
434
449
  };
435
450
  }
451
+ if (upstreamUnavailable) return introspectionUnavailable(response.status);
436
452
  if (!response.ok) {
437
453
  const errorBody = introspectResponse;
438
454
  return {
@@ -1376,7 +1392,7 @@ var FFID_ORGANIZATION_PROFILE_ERROR_CODES = {
1376
1392
  };
1377
1393
 
1378
1394
  // src/client/version-check.ts
1379
- var SDK_VERSION = "10.0.0";
1395
+ var SDK_VERSION = "11.0.0";
1380
1396
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
1381
1397
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
1382
1398
  function sdkHeaders() {
@@ -1944,7 +1960,7 @@ function createInviteMethods(deps) {
1944
1960
  }).toString();
1945
1961
  return fetchWithAuth(`${EXT_INVITE_ENDPOINT}?${query}`, {
1946
1962
  method: "POST",
1947
- body: JSON.stringify({ email: params.email, role: params.role })
1963
+ body: JSON.stringify({ email: params.email, role: params.role, inviterUserId: params.inviterUserId })
1948
1964
  });
1949
1965
  }
1950
1966
  return { inviteMember };
@@ -2247,6 +2263,25 @@ function createOAuthTokenMethods(deps) {
2247
2263
  return { exchangeCodeForTokens, refreshAccessToken, signOutToken };
2248
2264
  }
2249
2265
 
2266
+ // src/client/request-timeout.ts
2267
+ function withRequestTimeout(options, timeout) {
2268
+ if (timeout === void 0 || options.signal?.aborted) return options;
2269
+ const deadline = AbortSignal.timeout(timeout);
2270
+ if (!options.signal) return { ...options, signal: deadline };
2271
+ const callerSignal = options.signal;
2272
+ const controller = new AbortController();
2273
+ const abortFromCaller = () => abort(callerSignal.reason);
2274
+ const abortFromDeadline = () => abort(deadline.reason);
2275
+ function abort(reason) {
2276
+ callerSignal.removeEventListener("abort", abortFromCaller);
2277
+ deadline.removeEventListener("abort", abortFromDeadline);
2278
+ controller.abort(reason);
2279
+ }
2280
+ callerSignal.addEventListener("abort", abortFromCaller, { once: true });
2281
+ deadline.addEventListener("abort", abortFromDeadline, { once: true });
2282
+ return { ...options, signal: controller.signal };
2283
+ }
2284
+
2250
2285
  // src/client/session.ts
2251
2286
  var SESSION_ENDPOINT = "/api/v1/auth/session";
2252
2287
  var LOGOUT_ENDPOINT = "/api/v1/auth/signout";
@@ -3981,6 +4016,7 @@ function toServiceAccessDecision(response, allowGrace) {
3981
4016
  const isBlocked = response.isBlocked ?? (effectiveStatus === null || effectiveStatus !== null && BLOCKING_EFFECTIVE_STATUSES.includes(effectiveStatus));
3982
4017
  return {
3983
4018
  hasAccess,
4019
+ ...response.seatAssigned !== void 0 ? { seatAssigned: response.seatAssigned } : {},
3984
4020
  effectiveStatus,
3985
4021
  isGrace,
3986
4022
  isBlocked: isBlocked || !hasAccess,
@@ -4048,6 +4084,7 @@ var FFID_SUBSCRIPTION_ERROR_CODES = {
4048
4084
  var FFID_CREDITS_ERROR_CODES = {
4049
4085
  CREDITS_INSUFFICIENT: "CREDITS_INSUFFICIENT",
4050
4086
  CREDITS_IDEMPOTENCY_CONFLICT: "CREDITS_IDEMPOTENCY_CONFLICT",
4087
+ CREDITS_TURN_CANCELLED: "CREDITS_TURN_CANCELLED",
4051
4088
  CREDITS_NOT_APPLICABLE: "CREDITS_NOT_APPLICABLE",
4052
4089
  CREDITS_BALANCE_CAP_EXCEEDED: "CREDITS_BALANCE_CAP_EXCEEDED",
4053
4090
  SEAT_BILLING_NOT_APPLICABLE: "SEAT_BILLING_NOT_APPLICABLE",
@@ -4212,9 +4249,10 @@ function createFFIDClient(config) {
4212
4249
  async function fetchWithAuth(endpoint, options = {}, authOverride) {
4213
4250
  const url = `${baseUrl}${endpoint}`;
4214
4251
  logger.debug("Fetching:", url);
4215
- const fetchOptions = buildFetchOptions(options, authOverride);
4252
+ let fetchOptions = buildFetchOptions(options, authOverride);
4216
4253
  let response;
4217
4254
  try {
4255
+ fetchOptions = withRequestTimeout(fetchOptions, timeout);
4218
4256
  response = await fetch(url, fetchOptions);
4219
4257
  } catch (error) {
4220
4258
  logger.error("Network error:", error);
@@ -4229,9 +4267,9 @@ function createFFIDClient(config) {
4229
4267
  const refreshResult = await refreshAccessToken();
4230
4268
  if (!refreshResult.error) {
4231
4269
  logger.debug("Token refreshed, retrying request");
4232
- const retryOptions = buildFetchOptions(options);
4233
4270
  try {
4234
- response = await fetch(url, retryOptions);
4271
+ fetchOptions = withRequestTimeout(buildFetchOptions(options), timeout);
4272
+ response = await fetch(url, fetchOptions);
4235
4273
  } catch (retryError) {
4236
4274
  logger.error("Network error on retry:", retryError);
4237
4275
  return {
@@ -4250,6 +4288,15 @@ function createFFIDClient(config) {
4250
4288
  try {
4251
4289
  raw = await response.json();
4252
4290
  } catch (parseError) {
4291
+ if (fetchOptions.signal?.aborted) {
4292
+ logger.error("Response body aborted:", parseError);
4293
+ return {
4294
+ error: {
4295
+ code: FFID_ERROR_CODES.NETWORK_ERROR,
4296
+ message: "\u30EC\u30B9\u30DD\u30F3\u30B9\u306E\u53D7\u4FE1\u304C\u4E2D\u65AD\u3055\u308C\u307E\u3057\u305F"
4297
+ }
4298
+ };
4299
+ }
4253
4300
  logger.error("Parse error:", parseError, "Status:", response.status);
4254
4301
  return {
4255
4302
  error: {
@@ -4611,6 +4658,50 @@ function createFFIDClient(config) {
4611
4658
  };
4612
4659
  }
4613
4660
 
4661
+ // src/client/authorization-error.ts
4662
+ var FFID_OAUTH_DENIAL_REASONS = [
4663
+ /** The user is not a member of the organization */
4664
+ "no_membership",
4665
+ /** The organization has no usable subscription for the service */
4666
+ "no_active_subscription",
4667
+ /** The organization has a subscription, but the user has no seat in it */
4668
+ "no_seat_assignment",
4669
+ /** The subscription has expired (canceled / past due beyond grace) */
4670
+ "subscription_expired",
4671
+ /** The flat plan's member limit is full, so no seat could be given */
4672
+ "member_limit_reached"
4673
+ ];
4674
+ function isFFIDOAuthDenialReason(value) {
4675
+ return typeof value === "string" && FFID_OAUTH_DENIAL_REASONS.includes(value);
4676
+ }
4677
+ var DEFAULT_AUTHORIZATION_ERROR_MESSAGE = "\u8A8D\u53EF\u30B5\u30FC\u30D0\u30FC\u304C\u30A8\u30E9\u30FC\u3092\u8FD4\u3057\u307E\u3057\u305F";
4678
+ function buildAuthorizationError(params) {
4679
+ const oauthError = params.get("error");
4680
+ if (!oauthError) return null;
4681
+ const rawReason = params.get("denial_reason");
4682
+ const errorDescription = params.get("error_description");
4683
+ const details = {
4684
+ source: "authorization",
4685
+ denialReason: isFFIDOAuthDenialReason(rawReason) ? rawReason : null,
4686
+ errorDescription
4687
+ };
4688
+ return {
4689
+ code: oauthError,
4690
+ message: errorDescription ?? DEFAULT_AUTHORIZATION_ERROR_MESSAGE,
4691
+ details: { ...details }
4692
+ };
4693
+ }
4694
+ function getFFIDAuthorizationError(error) {
4695
+ const details = error?.details;
4696
+ if (!error || !details || details.source !== "authorization") return null;
4697
+ const description = details.errorDescription;
4698
+ return {
4699
+ oauthError: error.code,
4700
+ denialReason: isFFIDOAuthDenialReason(details.denialReason) ? details.denialReason : null,
4701
+ errorDescription: typeof description === "string" ? description : null
4702
+ };
4703
+ }
4704
+
4614
4705
  // src/client/callback.ts
4615
4706
  var CALLBACK_ERROR_CODES = {
4616
4707
  /** Stored state missing or did not match the callback `state` (CSRF guard) */
@@ -4635,16 +4726,43 @@ function extractSearchParams(source) {
4635
4726
  return new URLSearchParams(query);
4636
4727
  }
4637
4728
  }
4638
- function cleanCallbackUrlParams(logger) {
4729
+ var CODE_CALLBACK_PARAMS = ["code", "state"];
4730
+ var ERROR_CALLBACK_CONSUMED_PARAMS = ["state"];
4731
+ function cleanCallbackUrlParams(logger, names) {
4639
4732
  try {
4640
4733
  const cleanUrl = new URL(window.location.href);
4641
- cleanUrl.searchParams.delete("code");
4642
- cleanUrl.searchParams.delete("state");
4734
+ for (const name of names) cleanUrl.searchParams.delete(name);
4643
4735
  window.history.replaceState({}, "", cleanUrl.toString());
4644
4736
  } catch (error) {
4645
4737
  logger.warn("handleRedirectCallback: URL \u306E\u30AF\u30EA\u30FC\u30F3\u30A2\u30C3\u30D7\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
4646
4738
  }
4647
4739
  }
4740
+ function validateCallbackState(client, urlState) {
4741
+ const { logger } = client;
4742
+ const storedState = retrieveState(logger);
4743
+ if (storedState === null && urlState === null) return null;
4744
+ if (storedState !== null && storedState === urlState) return null;
4745
+ if (storedState === null && !isStateStorageAvailable()) {
4746
+ logger.error(
4747
+ "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"
4748
+ );
4749
+ return {
4750
+ error: client.createError(
4751
+ CALLBACK_ERROR_CODES.STATE_UNAVAILABLE,
4752
+ "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"
4753
+ )
4754
+ };
4755
+ }
4756
+ logger.error(
4757
+ "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"
4758
+ );
4759
+ return {
4760
+ error: client.createError(
4761
+ CALLBACK_ERROR_CODES.STATE_MISMATCH,
4762
+ "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"
4763
+ )
4764
+ };
4765
+ }
4648
4766
  async function handleRedirectCallback(client, options) {
4649
4767
  const { logger } = client;
4650
4768
  const hasWindow = typeof window !== "undefined";
@@ -4661,6 +4779,24 @@ async function handleRedirectCallback(client, options) {
4661
4779
  const params = extractSearchParams(source);
4662
4780
  const code = params.get("code");
4663
4781
  const urlState = params.get("state");
4782
+ const authorizationError = buildAuthorizationError(params);
4783
+ if (authorizationError && urlState !== null) {
4784
+ const stateError2 = validateCallbackState(client, urlState);
4785
+ if (stateError2) return stateError2;
4786
+ retrieveCodeVerifier(logger);
4787
+ logger.warn("handleRedirectCallback: \u8A8D\u53EF\u30B5\u30FC\u30D0\u30FC\u304C\u30A8\u30E9\u30FC\u3092\u8FD4\u3057\u307E\u3057\u305F:", {
4788
+ error: authorizationError.code,
4789
+ denialReason: params.get("denial_reason")
4790
+ });
4791
+ if (hasWindow) {
4792
+ if (options?.cleanUrl !== false) {
4793
+ cleanCallbackUrlParams(logger, ERROR_CALLBACK_CONSUMED_PARAMS);
4794
+ } else {
4795
+ logger.debug("handleRedirectCallback: cleanUrl=false \u306E\u305F\u3081 URL cleanup \u3092 consumer \u306B\u59D4\u8B72\u3057\u307E\u3057\u305F");
4796
+ }
4797
+ }
4798
+ return { error: authorizationError };
4799
+ }
4664
4800
  if (!code) {
4665
4801
  logger.warn("handleRedirectCallback: \u8A8D\u53EF\u30B3\u30FC\u30C9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093");
4666
4802
  return {
@@ -4670,37 +4806,13 @@ async function handleRedirectCallback(client, options) {
4670
4806
  )
4671
4807
  };
4672
4808
  }
4673
- const storedState = retrieveState(logger);
4674
- if (storedState !== null || urlState !== null) {
4675
- const stateMatches = storedState !== null && storedState === urlState;
4676
- if (!stateMatches) {
4677
- if (storedState === null && !isStateStorageAvailable()) {
4678
- logger.error(
4679
- "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"
4680
- );
4681
- return {
4682
- error: client.createError(
4683
- CALLBACK_ERROR_CODES.STATE_UNAVAILABLE,
4684
- "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"
4685
- )
4686
- };
4687
- }
4688
- logger.error(
4689
- "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"
4690
- );
4691
- return {
4692
- error: client.createError(
4693
- CALLBACK_ERROR_CODES.STATE_MISMATCH,
4694
- "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"
4695
- )
4696
- };
4697
- }
4698
- }
4809
+ const stateError = validateCallbackState(client, urlState);
4810
+ if (stateError) return stateError;
4699
4811
  const verifier = retrieveCodeVerifier(logger);
4700
4812
  const result = await client.exchangeCodeForTokens(code, verifier ?? void 0);
4701
4813
  if (!result.error && hasWindow) {
4702
4814
  if (options?.cleanUrl !== false) {
4703
- cleanCallbackUrlParams(logger);
4815
+ cleanCallbackUrlParams(logger, CODE_CALLBACK_PARAMS);
4704
4816
  } else {
4705
4817
  logger.debug("handleRedirectCallback: cleanUrl=false \u306E\u305F\u3081 URL cleanup \u3092 consumer \u306B\u59D4\u8B72\u3057\u307E\u3057\u305F");
4706
4818
  }
@@ -4709,6 +4821,13 @@ async function handleRedirectCallback(client, options) {
4709
4821
  }
4710
4822
  var DEFAULT_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
4711
4823
  var TOKEN_REFRESH_RATIO = 0.8;
4824
+ function resolveCallbackKey(params) {
4825
+ const code = params.get("code");
4826
+ if (code) return code;
4827
+ const error = params.get("error");
4828
+ const state = params.get("state");
4829
+ return error && state ? `error:${error}:${state}` : null;
4830
+ }
4712
4831
  var FFIDContext = react.createContext(null);
4713
4832
  var FFIDClientContext = react.createContext(null);
4714
4833
  function FFIDProvider({
@@ -4888,19 +5007,26 @@ function FFIDProvider({
4888
5007
  },
4889
5008
  [organizations, client]
4890
5009
  );
4891
- const handledCodeRef = react.useRef(null);
5010
+ const handledCallbackRef = react.useRef(null);
5011
+ const callbackPendingRef = react.useRef(false);
5012
+ const authorizationDeniedRef = react.useRef(false);
4892
5013
  react.useEffect(() => {
4893
5014
  if (client.authMode !== "token") return;
4894
5015
  if (typeof window === "undefined") return;
4895
- const urlParams = new URLSearchParams(window.location.search);
4896
- const code = urlParams.get("code");
4897
- if (!code) return;
4898
- if (handledCodeRef.current === code) return;
4899
- handledCodeRef.current = code;
4900
- client.logger.debug("Authorization code detected, exchanging for tokens");
5016
+ const callbackKey = resolveCallbackKey(new URLSearchParams(window.location.search));
5017
+ if (!callbackKey) return;
5018
+ if (handledCallbackRef.current === callbackKey) return;
5019
+ handledCallbackRef.current = callbackKey;
5020
+ callbackPendingRef.current = true;
5021
+ client.logger.debug("OAuth callback detected, handling it");
4901
5022
  handleRedirectCallback(client, { cleanUrl: cleanCallbackUrl }).then((result) => {
4902
5023
  if (result.error) {
4903
- client.logger.error("Token exchange failed:", result.error);
5024
+ if (getFFIDAuthorizationError(result.error)) {
5025
+ client.logger.warn("FFID \u304C\u30ED\u30B0\u30A4\u30F3\u3092\u62D2\u5426\u3057\u307E\u3057\u305F:", result.error);
5026
+ authorizationDeniedRef.current = true;
5027
+ } else {
5028
+ client.logger.error("Token exchange failed:", result.error);
5029
+ }
4904
5030
  if (result.error.code === FFID_ERROR_CODES.STATE_MISMATCH_ERROR && client.tokenStore.getTokens()) {
4905
5031
  client.logger.debug(
4906
5032
  "Stale callback rejected but stored tokens present, restoring session via refresh"
@@ -4918,13 +5044,15 @@ function FFIDProvider({
4918
5044
  }).catch((err) => {
4919
5045
  client.logger.error("Token exchange error:", err);
4920
5046
  setIsLoading(false);
5047
+ }).finally(() => {
5048
+ callbackPendingRef.current = false;
4921
5049
  });
4922
5050
  }, [client]);
4923
5051
  react.useEffect(() => {
4924
5052
  if (client.authMode === "token") {
5053
+ if (callbackPendingRef.current || authorizationDeniedRef.current) return;
4925
5054
  if (typeof window !== "undefined") {
4926
- const urlParams = new URLSearchParams(window.location.search);
4927
- if (urlParams.has("code")) {
5055
+ if (resolveCallbackKey(new URLSearchParams(window.location.search))) {
4928
5056
  return;
4929
5057
  }
4930
5058
  }
@@ -6785,6 +6913,7 @@ exports.FFID_INQUIRY_CATEGORIES = FFID_INQUIRY_CATEGORIES;
6785
6913
  exports.FFID_INQUIRY_CATEGORIES_SITE_2026 = FFID_INQUIRY_CATEGORIES_SITE_2026;
6786
6914
  exports.FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_BYTES = FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_BYTES;
6787
6915
  exports.FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_MB = FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_MB;
6916
+ exports.FFID_OAUTH_DENIAL_REASONS = FFID_OAUTH_DENIAL_REASONS;
6788
6917
  exports.FFID_ORGANIZATION_PROFILE_ERROR_CODES = FFID_ORGANIZATION_PROFILE_ERROR_CODES;
6789
6918
  exports.FFID_SUBSCRIPTION_ERROR_CODES = FFID_SUBSCRIPTION_ERROR_CODES;
6790
6919
  exports.canonicalizeCatalogValue = canonicalizeCatalogValue;
@@ -6803,12 +6932,14 @@ exports.createFFIDClient = createFFIDClient;
6803
6932
  exports.createTokenStore = createTokenStore;
6804
6933
  exports.generateCodeChallenge = generateCodeChallenge;
6805
6934
  exports.generateCodeVerifier = generateCodeVerifier;
6935
+ exports.getFFIDAuthorizationError = getFFIDAuthorizationError;
6806
6936
  exports.handleRedirectCallback = handleRedirectCallback;
6807
6937
  exports.hasAccessFromUserinfo = hasAccessFromUserinfo;
6808
6938
  exports.hashCanonicalJson = hashCanonicalJson;
6809
6939
  exports.isBlockedFromUserinfo = isBlockedFromUserinfo;
6810
6940
  exports.isFFIDInquiryAcquisitionSource = isFFIDInquiryAcquisitionSource;
6811
6941
  exports.isFFIDInquiryCategorySite2026 = isFFIDInquiryCategorySite2026;
6942
+ exports.isFFIDOAuthDenialReason = isFFIDOAuthDenialReason;
6812
6943
  exports.normalizeRedirectUri = normalizeRedirectUri;
6813
6944
  exports.readAgencyInviteCookie = readAgencyInviteCookie;
6814
6945
  exports.readAgencyInviteToken = readAgencyInviteToken;
@@ -1,34 +1,34 @@
1
1
  'use strict';
2
2
 
3
- var chunk2LU2F3F2_cjs = require('../chunk-2LU2F3F2.cjs');
3
+ var chunkVR3QR64A_cjs = require('../chunk-VR3QR64A.cjs');
4
4
 
5
5
 
6
6
 
7
7
  Object.defineProperty(exports, "FFIDAnnouncementBadge", {
8
8
  enumerable: true,
9
- get: function () { return chunk2LU2F3F2_cjs.FFIDAnnouncementBadge; }
9
+ get: function () { return chunkVR3QR64A_cjs.FFIDAnnouncementBadge; }
10
10
  });
11
11
  Object.defineProperty(exports, "FFIDAnnouncementList", {
12
12
  enumerable: true,
13
- get: function () { return chunk2LU2F3F2_cjs.FFIDAnnouncementList; }
13
+ get: function () { return chunkVR3QR64A_cjs.FFIDAnnouncementList; }
14
14
  });
15
15
  Object.defineProperty(exports, "FFIDInquiryForm", {
16
16
  enumerable: true,
17
- get: function () { return chunk2LU2F3F2_cjs.FFIDInquiryForm; }
17
+ get: function () { return chunkVR3QR64A_cjs.FFIDInquiryForm; }
18
18
  });
19
19
  Object.defineProperty(exports, "FFIDLoginButton", {
20
20
  enumerable: true,
21
- get: function () { return chunk2LU2F3F2_cjs.FFIDLoginButton; }
21
+ get: function () { return chunkVR3QR64A_cjs.FFIDLoginButton; }
22
22
  });
23
23
  Object.defineProperty(exports, "FFIDOrganizationSwitcher", {
24
24
  enumerable: true,
25
- get: function () { return chunk2LU2F3F2_cjs.FFIDOrganizationSwitcher; }
25
+ get: function () { return chunkVR3QR64A_cjs.FFIDOrganizationSwitcher; }
26
26
  });
27
27
  Object.defineProperty(exports, "FFIDSubscriptionBadge", {
28
28
  enumerable: true,
29
- get: function () { return chunk2LU2F3F2_cjs.FFIDSubscriptionBadge; }
29
+ get: function () { return chunkVR3QR64A_cjs.FFIDSubscriptionBadge; }
30
30
  });
31
31
  Object.defineProperty(exports, "FFIDUserMenu", {
32
32
  enumerable: true,
33
- get: function () { return chunk2LU2F3F2_cjs.FFIDUserMenu; }
33
+ get: function () { return chunkVR3QR64A_cjs.FFIDUserMenu; }
34
34
  });
@@ -1,3 +1,3 @@
1
- export { a8 as FFIDAnnouncementBadge, bk as FFIDAnnouncementBadgeClassNames, bl as FFIDAnnouncementBadgeProps, a9 as FFIDAnnouncementList, bm as FFIDAnnouncementListClassNames, bn as FFIDAnnouncementListProps, ar as FFIDInquiryForm, as as FFIDInquiryFormCategoryItem, at as FFIDInquiryFormClassNames, au as FFIDInquiryFormLegalLayout, av as FFIDInquiryFormOrganization, aw as FFIDInquiryFormPlaceholderContext, ax as FFIDInquiryFormPrefill, ay as FFIDInquiryFormProps, az as FFIDInquiryFormSubmitData, aA as FFIDInquiryFormSubmitResult, aC as FFIDLoginButton, bo as FFIDLoginButtonProps, aE as FFIDOrganizationSwitcher, bp as FFIDOrganizationSwitcherClassNames, bq as FFIDOrganizationSwitcherProps, aS as FFIDSubscriptionBadge, br as FFIDSubscriptionBadgeClassNames, bs as FFIDSubscriptionBadgeProps, aW as FFIDUserMenu, bt as FFIDUserMenuClassNames, bu as FFIDUserMenuProps } from '../index-KjewcNn1.cjs';
1
+ export { a8 as FFIDAnnouncementBadge, bk as FFIDAnnouncementBadgeClassNames, bl as FFIDAnnouncementBadgeProps, a9 as FFIDAnnouncementList, bm as FFIDAnnouncementListClassNames, bn as FFIDAnnouncementListProps, ar as FFIDInquiryForm, as as FFIDInquiryFormCategoryItem, at as FFIDInquiryFormClassNames, au as FFIDInquiryFormLegalLayout, av as FFIDInquiryFormOrganization, aw as FFIDInquiryFormPlaceholderContext, ax as FFIDInquiryFormPrefill, ay as FFIDInquiryFormProps, az as FFIDInquiryFormSubmitData, aA as FFIDInquiryFormSubmitResult, aC as FFIDLoginButton, bo as FFIDLoginButtonProps, aE as FFIDOrganizationSwitcher, bp as FFIDOrganizationSwitcherClassNames, bq as FFIDOrganizationSwitcherProps, aS as FFIDSubscriptionBadge, br as FFIDSubscriptionBadgeClassNames, bs as FFIDSubscriptionBadgeProps, aW as FFIDUserMenu, bt as FFIDUserMenuClassNames, bu as FFIDUserMenuProps } from '../index-BknVfXhZ.cjs';
2
2
  import 'react/jsx-runtime';
3
3
  import 'react';
@@ -1,3 +1,3 @@
1
- export { a8 as FFIDAnnouncementBadge, bk as FFIDAnnouncementBadgeClassNames, bl as FFIDAnnouncementBadgeProps, a9 as FFIDAnnouncementList, bm as FFIDAnnouncementListClassNames, bn as FFIDAnnouncementListProps, ar as FFIDInquiryForm, as as FFIDInquiryFormCategoryItem, at as FFIDInquiryFormClassNames, au as FFIDInquiryFormLegalLayout, av as FFIDInquiryFormOrganization, aw as FFIDInquiryFormPlaceholderContext, ax as FFIDInquiryFormPrefill, ay as FFIDInquiryFormProps, az as FFIDInquiryFormSubmitData, aA as FFIDInquiryFormSubmitResult, aC as FFIDLoginButton, bo as FFIDLoginButtonProps, aE as FFIDOrganizationSwitcher, bp as FFIDOrganizationSwitcherClassNames, bq as FFIDOrganizationSwitcherProps, aS as FFIDSubscriptionBadge, br as FFIDSubscriptionBadgeClassNames, bs as FFIDSubscriptionBadgeProps, aW as FFIDUserMenu, bt as FFIDUserMenuClassNames, bu as FFIDUserMenuProps } from '../index-KjewcNn1.js';
1
+ export { a8 as FFIDAnnouncementBadge, bk as FFIDAnnouncementBadgeClassNames, bl as FFIDAnnouncementBadgeProps, a9 as FFIDAnnouncementList, bm as FFIDAnnouncementListClassNames, bn as FFIDAnnouncementListProps, ar as FFIDInquiryForm, as as FFIDInquiryFormCategoryItem, at as FFIDInquiryFormClassNames, au as FFIDInquiryFormLegalLayout, av as FFIDInquiryFormOrganization, aw as FFIDInquiryFormPlaceholderContext, ax as FFIDInquiryFormPrefill, ay as FFIDInquiryFormProps, az as FFIDInquiryFormSubmitData, aA as FFIDInquiryFormSubmitResult, aC as FFIDLoginButton, bo as FFIDLoginButtonProps, aE as FFIDOrganizationSwitcher, bp as FFIDOrganizationSwitcherClassNames, bq as FFIDOrganizationSwitcherProps, aS as FFIDSubscriptionBadge, br as FFIDSubscriptionBadgeClassNames, bs as FFIDSubscriptionBadgeProps, aW as FFIDUserMenu, bt as FFIDUserMenuClassNames, bu as FFIDUserMenuProps } from '../index-BknVfXhZ.js';
2
2
  import 'react/jsx-runtime';
3
3
  import 'react';
@@ -1 +1 @@
1
- export { FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDSubscriptionBadge, FFIDUserMenu } from '../chunk-OXW6QSTO.js';
1
+ export { FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDSubscriptionBadge, FFIDUserMenu } from '../chunk-QORWLPWG.js';
@@ -961,12 +961,19 @@ interface FFIDBuyCreditsUrlParams {
961
961
  type FFIDSubscriptionStatus = 'trialing' | 'active' | 'past_due' | 'canceled' | 'pending_invoice' | 'paused' | 'incomplete' | 'incomplete_expired' | 'unpaid';
962
962
  interface FFIDSubscriptionCheckResponse {
963
963
  hasActiveSubscription: boolean;
964
+ /**
965
+ * Active seat for the requested user; null for organization-only checks.
966
+ * Older servers omit this field. User access requires hasAccess AND
967
+ * seatAssigned === true; an omitted/null value is not proof of a seat.
968
+ */
969
+ seatAssigned?: boolean | null;
964
970
  /**
965
971
  * Canonical access decision returned by FFID's `/subscriptions/ext/check`.
966
972
  *
967
973
  * This is the server-side source of truth for service gates. Consumers
968
974
  * should not recompute access from `currentPeriodEnd`, `past_due_since`, or
969
975
  * local payment timestamps.
976
+ * This is contract access; user access also requires seatAssigned === true.
970
977
  */
971
978
  hasAccess?: boolean;
972
979
  /** True when `effectiveStatus === 'past_due_grace'`. */
@@ -996,9 +1003,11 @@ interface FFIDSubscriptionCheckResponse {
996
1003
  */
997
1004
  pricingModel?: FFIDPricingModel | null;
998
1005
  /**
999
- * Credit balance summary — flat plans only (10.0.0). `null` for per_seat, for no
1000
- * subscription, **and** when FFID could not read the ledger, so `null` does not
1001
- * mean "not flat" (use `pricingModel`). Never affects `hasAccess`.
1006
+ * Credit balance summary — paid flat plans only (10.0.0). `null` for per_seat, for a
1007
+ * `free` plan (flat on the unified ladder since #6547, but outside credits — consume /
1008
+ * balance return 409 `CREDITS_NOT_APPLICABLE`), for no subscription, **and** when FFID
1009
+ * could not read the ledger, so `null` does not mean "not flat" (use `pricingModel`).
1010
+ * Never affects `hasAccess`.
1002
1011
  */
1003
1012
  credits?: FFIDSubscriptionCheckCredits | null;
1004
1013
  /**
@@ -1014,7 +1023,8 @@ type FFIDServiceAccessFailPolicy = 'failClosed';
1014
1023
  *
1015
1024
  * `member_limit_reached` (10.0.0) is FFID's denial reason when a flat plan's member
1016
1025
  * limit is full and the user has no seat. It reaches you through OAuth
1017
- * (`denial_reason` on authorize / userinfo / introspect); `checkServiceAccess()`
1026
+ * (`denial_reason` on the authorize redirect / userinfo 403; see
1027
+ * `FFIDOAuthDenialReason` for that vocabulary); `checkServiceAccess()`
1018
1028
  * derives its decision from `effectiveStatus` and never returns it, because a full
1019
1029
  * member limit does not change `hasAccess` (CREDITS_API §1-5). It is in this union so
1020
1030
  * one exhaustive `switch` over denial reasons covers both paths.
@@ -1044,7 +1054,10 @@ interface FFIDServiceAccessError {
1044
1054
  details?: unknown;
1045
1055
  }
1046
1056
  interface FFIDServiceAccessDecision {
1057
+ /** Contract access; user access also requires seatAssigned === true. */
1047
1058
  hasAccess: boolean;
1059
+ /** User's active seat, passed through when returned. Gate user access with === true. */
1060
+ seatAssigned?: boolean | null;
1048
1061
  effectiveStatus: EffectiveSubscriptionStatus | null;
1049
1062
  isGrace: boolean;
1050
1063
  isBlocked: boolean;
@@ -1209,9 +1222,18 @@ interface FFIDAddMemberRequest {
1209
1222
  interface FFIDAddMemberParams extends FFIDAddMemberRequest {
1210
1223
  organizationId: string;
1211
1224
  }
1225
+ /** Seat assignment outcome after membership creation; membership exists in every case. */
1226
+ type FFIDMemberSeatAssignmentResult = 'assigned' | 'not_applicable' | 'member_limit_reached' | 'failed';
1212
1227
  /** Response from addMember (ext) */
1213
1228
  interface FFIDAddMemberResponse {
1214
1229
  member: FFIDOrganizationMember;
1230
+ /**
1231
+ * Membership creation can succeed even when seat assignment fails.
1232
+ * Omitted by older servers. `not_applicable`/undefined do not prove an active seat.
1233
+ * Check the authenticated user's hasAccess and seatAssigned before granting access;
1234
+ * do not automatically restore a removed seat on a duplicate-member retry.
1235
+ */
1236
+ seatAssignment?: FFIDMemberSeatAssignmentResult;
1215
1237
  }
1216
1238
  /** Response from updateMemberRole (ext) */
1217
1239
  interface FFIDUpdateMemberRoleResponse {
@@ -2430,11 +2452,10 @@ interface FFIDConfig {
2430
2452
  */
2431
2453
  cache?: FFIDCacheConfig | undefined;
2432
2454
  /**
2433
- * Request timeout in milliseconds for FFID API calls. Applies to server-side
2434
- * token verification / introspection and, in `token` mode, to the client-side
2435
- * OAuth operations (code exchange / refresh / revoke) — including via the
2436
- * `FFIDProvider` `timeout` prop (#4260). On timeout the fetch is aborted and
2437
- * surfaced through the normal error path (e.g. `NETWORK_ERROR`).
2455
+ * Per-HTTP timeout (ms): shared JSON APIs (e.g. checkSubscription/getProfile),
2456
+ * token verification/introspection, token OAuth exchange/refresh/revoke; also FFIDProvider.timeout.
2457
+ * Shared JSON includes body and preserves signals; refresh/retry have separate deadlines.
2458
+ * Excludes token getSession, cookie signOut, public/newsletter/inquiry; not a method deadline.
2438
2459
  * @default undefined (no timeout, uses fetch default)
2439
2460
  */
2440
2461
  timeout?: number | undefined;
@@ -2800,6 +2821,7 @@ declare function createFFIDClient(config: FFIDConfig): {
2800
2821
  organizationId: string;
2801
2822
  email: string;
2802
2823
  role?: FFIDAssignableMemberRole;
2824
+ inviterUserId?: string;
2803
2825
  }) => Promise<FFIDApiResponse<FFIDInviteMemberResponse>>;
2804
2826
  leaveOrganization: (params: FFIDLeaveOrganizationParams) => Promise<FFIDApiResponse<FFIDLeaveOrganizationResponse>>;
2805
2827
  getAnalyticsConfig: (serviceCode: string, options?: FFIDProfileCallOptions) => Promise<FFIDApiResponse<FFIDAnalyticsConfig>>;
@@ -2886,4 +2908,4 @@ declare function createFFIDClient(config: FFIDConfig): {
2886
2908
  /** Type of the FFID client */
2887
2909
  type FFIDClient = ReturnType<typeof createFFIDClient>;
2888
2910
 
2889
- export { type FFIDProvisionedOrganization as $, type FFIDOrganization as A, type FFIDOrganizationLogoUploadResponse as B, type FFIDOrganizationMember as C, type FFIDOrganizationProfile as D, type FFIDOrganizationProfileErrorCode as E, type FFIDLogger as F, type FFIDOrganizationProfileResponse as G, type FFIDOtpSendResponse as H, type FFIDOtpVerifyResponse as I, type FFIDPasswordResetConfirmResponse as J, type FFIDPasswordResetResponse as K, type FFIDPasswordResetVerifyResponse as L, type FFIDProfileCallOptions as M, type FFIDProvisionMemberPlan as N, type FFIDProvisionMemberPlanStatus as O, type FFIDProvisionMemberResult as P, type FFIDProvisionMemberStatus as Q, type FFIDProvisionOrganizationDryRun as R, type FFIDProvisionOrganizationMemberInput as S, type FFIDProvisionOrganizationOutcome as T, type FFIDProvisionOrganizationParams as U, type FFIDProvisionOrganizationResponse as V, type FFIDProvisionUserDryRun as W, type FFIDProvisionUserOutcome as X, type FFIDProvisionUserParams as Y, type FFIDProvisionUserProfileInput as Z, type FFIDProvisionUserResponse as _, type FFIDError as a, type FFIDProvisionedUser as a0, type FFIDPublishedPlan as a1, type FFIDRemoveMemberResponse as a2, type FFIDResetSessionResponse as a3, type FFIDSubscription as a4, type FFIDUpdateMemberRoleResponse as a5, type FFIDUpdateUserProfileRequest as a6, type FFIDUploadOrganizationLogoParams as a7, type FFIDUpsertOrganizationProfileParams as a8, type FFIDUpsertOrganizationProfileRequest as a9, type FFIDUser as aa, type FFIDUserProfile as ab, FFID_ALLOWED_ORGANIZATION_LOGO_MIME_TYPES as ac, FFID_CATALOG_ENVIRONMENTS as ad, FFID_CATALOG_PUBLICATION_STATUSES as ae, FFID_COMPANY_SIZE_CODES as af, FFID_INDUSTRY_CODES as ag, FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_BYTES as ah, FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_MB as ai, FFID_ORGANIZATION_PROFILE_ERROR_CODES as aj, type TokenData as ak, type TokenStore as al, createFFIDClient as am, createTokenStore as an, type FFIDCacheAdapter as b, type FFIDVerifyAccessTokenOptions as c, type FFIDApiResponse as d, type FFIDOAuthUserInfo as e, type FFIDCatalogEnvironment as f, type FFIDBillingInterval as g, type FFIDTaxBehavior as h, type FFIDPublishedCatalog as i, type FFIDAddMemberParams as j, type FFIDAddMemberRequest as k, type FFIDAddMemberResponse as l, type FFIDAllowedLogoMimeType as m, type FFIDAssignableMemberRole as n, type FFIDCacheConfig as o, type FFIDCatalogPublication as p, type FFIDCatalogPublicationStatus as q, type FFIDClient as r, type FFIDCompanySizeCode as s, type FFIDConfig as t, type FFIDIndustryCode as u, type FFIDListMembersResponse as v, type FFIDListingConsentInput as w, type FFIDLogoWallEntry as x, type FFIDLogoWallResponse as y, type FFIDMemberRole as z };
2911
+ export { type FFIDProvisionUserResponse as $, type FFIDMemberSeatAssignmentResult as A, type FFIDOrganization as B, type FFIDOrganizationLogoUploadResponse as C, type FFIDOrganizationMember as D, type FFIDOrganizationProfile as E, type FFIDError as F, type FFIDOrganizationProfileErrorCode as G, type FFIDOrganizationProfileResponse as H, type FFIDOtpSendResponse as I, type FFIDOtpVerifyResponse as J, type FFIDPasswordResetConfirmResponse as K, type FFIDPasswordResetResponse as L, type FFIDPasswordResetVerifyResponse as M, type FFIDProfileCallOptions as N, type FFIDProvisionMemberPlan as O, type FFIDProvisionMemberPlanStatus as P, type FFIDProvisionMemberResult as Q, type FFIDProvisionMemberStatus as R, type FFIDProvisionOrganizationDryRun as S, type FFIDProvisionOrganizationMemberInput as T, type FFIDProvisionOrganizationOutcome as U, type FFIDProvisionOrganizationParams as V, type FFIDProvisionOrganizationResponse as W, type FFIDProvisionUserDryRun as X, type FFIDProvisionUserOutcome as Y, type FFIDProvisionUserParams as Z, type FFIDProvisionUserProfileInput as _, type FFIDLogger as a, type FFIDProvisionedOrganization as a0, type FFIDProvisionedUser as a1, type FFIDPublishedPlan as a2, type FFIDRemoveMemberResponse as a3, type FFIDResetSessionResponse as a4, type FFIDSubscription as a5, type FFIDUpdateMemberRoleResponse as a6, type FFIDUpdateUserProfileRequest as a7, type FFIDUploadOrganizationLogoParams as a8, type FFIDUpsertOrganizationProfileParams as a9, type FFIDUpsertOrganizationProfileRequest as aa, type FFIDUser as ab, type FFIDUserProfile as ac, FFID_ALLOWED_ORGANIZATION_LOGO_MIME_TYPES as ad, FFID_CATALOG_ENVIRONMENTS as ae, FFID_CATALOG_PUBLICATION_STATUSES as af, FFID_COMPANY_SIZE_CODES as ag, FFID_INDUSTRY_CODES as ah, FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_BYTES as ai, FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_MB as aj, FFID_ORGANIZATION_PROFILE_ERROR_CODES as ak, type TokenData as al, type TokenStore as am, createFFIDClient as an, createTokenStore as ao, type FFIDCacheAdapter as b, type FFIDVerifyAccessTokenOptions as c, type FFIDApiResponse as d, type FFIDOAuthUserInfo as e, type FFIDCatalogEnvironment as f, type FFIDBillingInterval as g, type FFIDTaxBehavior as h, type FFIDPublishedCatalog as i, type FFIDAddMemberParams as j, type FFIDAddMemberRequest as k, type FFIDAddMemberResponse as l, type FFIDAllowedLogoMimeType as m, type FFIDAssignableMemberRole as n, type FFIDCacheConfig as o, type FFIDCatalogPublication as p, type FFIDCatalogPublicationStatus as q, type FFIDClient as r, type FFIDCompanySizeCode as s, type FFIDConfig as t, type FFIDIndustryCode as u, type FFIDListMembersResponse as v, type FFIDListingConsentInput as w, type FFIDLogoWallEntry as x, type FFIDLogoWallResponse as y, type FFIDMemberRole as z };