@feelflow/ffid-sdk 10.1.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.
package/README.md CHANGED
@@ -481,6 +481,10 @@ await ffid.updateMemberRole({
481
481
  - `addMember` は既存 FFID ユーザーを active member として追加します。招待メール送信や token 発行は行いません。
482
482
  - `role` は `admin` / `member` / `viewer` のみ指定可能です。`owner` は追加・昇格 API では扱いません。
483
483
  - agency client と同じ呼び味が必要な場合は `ffid.addMember(organizationId, { email, role })` も使えます。
484
+ - 未登録の人を招待する(招待メールと受諾リンクを FFID が送る)ときは `inviteMember({ organizationId, email, role, inviterUserId })` を使います。`inviterUserId` は、サービスで招待を操作した人の FFID ユーザー ID です。FFID の「保留中の招待」と招待メールに、その人が招待者として出ます。
485
+ - service-key モードでは `inviterUserId` を渡してください。その人が対象の組織の owner / admin でなければ 403 `FORBIDDEN`(`details.reason: 'inviter_not_organization_admin'`)で、招待は作られません。省略すると従来どおり組織の owner が招待者になり、メールの招待者名は `service:<serviceCode>` です。
486
+ - token モードでは省略します(ログイン中の利用者が招待者になります)。別の人の ID を渡すと 400 `VALIDATION_ERROR` です。
487
+ - `inviterUserId` は「誰を招待者として記録するか」の指定で、招待できる範囲は増えません。FFID が確かめるのは「招待できる立場か」までなので、実際に操作した本人の ID だけを渡してください。
484
488
 
485
489
  ### Organization profile / public logo wall
486
490
 
@@ -950,6 +954,18 @@ SDK の `package.json` で `react` / `react-dom` は `optional: true` に設定
950
954
  }
951
955
  ```
952
956
 
957
+ ## サーバー側のアクセストークン検証の照会障害(SDK 11)
958
+
959
+ `verifyAccessToken()` の introspect 照会(JWT検証後の `includeProfile: true` を含む)が
960
+ HTTP429 / 5xx を返す場合は、本文のエラーコードやJSONの可否によらず
961
+ `NETWORK_ERROR` と `details.status` を返します。SDKの診断ログにはHTTPステータスだけを
962
+ 記録し、上流本文・アクセストークンをコピーしません。自動再試行は行いません。
963
+
964
+ 連携先は、この照会障害を本人の認証不正と区別し、利用は拒否したうえで一時的な利用不可
965
+ (例: HTTP503)として案内してください。HTTP200の `active: false` は引き続き
966
+ `TOKEN_VERIFICATION_ERROR`、HTTP400 / 401 / 403の明示エラーコードは従来どおり保持します。
967
+ このメソッドによる本人確認とは別に、契約の利用可否と本人のactiveな席を確認する必要があります。
968
+
953
969
  ## E2E テストモード(`@feelflow/ffid-sdk/server/test`)
954
970
 
955
971
  > **⚠️ SECURITY NOTICE** — テストモードは Bearer トークン検証を **意図的にバイパス** する仕組みです。本番環境で誤って有効化すると、登録されたバイパストークンを持つ任意のリクエストが認証通過します。
@@ -124,6 +124,20 @@ interface FFIDProvisionOrganizationRequest {
124
124
  slug?: string;
125
125
  /** 請求先メールアドレス */
126
126
  billingEmail?: string;
127
+ /**
128
+ * 冪等キー(1〜255 文字の表示可能な ASCII。空白不可)。
129
+ *
130
+ * 指定すると、同じ代理店・同じキーの再送(24 時間以内)は**組織を作らずに**
131
+ * 初回の結果を返す。タイムアウトや `PARSE_ERROR` で応答を受け取れなかったときは、
132
+ * **同じキーのまま**再送すれば二重作成にならない。申込番号など、作りたい組織 1 つに
133
+ * 1 つ対応する値を使う。
134
+ *
135
+ * - 同じキーで内容(`name` / `ownerEmail` / `slug` / `billingEmail`)が違うと 422
136
+ * `IDEMPOTENCY_KEY_REUSED`(別の組織を作るなら別のキーを使う)
137
+ * - 省略すると従来どおり毎回作成する
138
+ * - HTTP では `Idempotency-Key` ヘッダーでも渡せる(本文と両方に入れる場合は同じ値)
139
+ */
140
+ idempotencyKey?: string;
127
141
  }
128
142
  /** 案内メールの送信結果。組織作成の成否とは独立している。 */
129
143
  type FFIDWelcomeEmailOutcome = 'sent' | 'failed';
@@ -453,9 +467,17 @@ interface FFIDAgencyServerClient {
453
467
  * そのほか:
454
468
  * - スコープ不足 / 代理店が停止中 → `FORBIDDEN`(403)
455
469
  * - slug が既存組織と衝突 → `ORGANIZATION_SLUG_CONFLICT`(409、組織は作成されない)
470
+ * - slug が予約語(`admin` / `api` / `login` など)→ `ORGANIZATION_SLUG_RESERVED`
471
+ * (400、組織は作成されない。別の値を指定する)
472
+ * - `ownerEmail` の既存アカウントが停止・削除済み → `OWNER_ACCOUNT_UNAVAILABLE`
473
+ * (422、**組織は作成されない**。別のメールアドレスを指定する)
456
474
  * - 認証段階の一時障害 → `SERVICE_UNAVAILABLE`(503、**安全に再試行できる**)
457
475
  * - `PARSE_ERROR` / `NETWORK_ERROR` — **組織が作成された可能性がある**。
458
- * 再試行の前に組織一覧を確認すること(#5165 で冪等キーを導入予定)
476
+ * `idempotencyKey` を付けていれば**同じキーで**再送してよい(作成済みなら
477
+ * 初回の結果が返り、組織は増えない。#5165)。付けていない場合は再試行の前に
478
+ * 組織一覧を確認すること
479
+ * - 同じ `idempotencyKey` で内容が違う → `IDEMPOTENCY_KEY_REUSED`(422)
480
+ * - `idempotencyKey` の形式不正 → `INVALID_IDEMPOTENCY_KEY`(400)
459
481
  */
460
482
  provisionCustomerOrganization(data: FFIDProvisionOrganizationRequest): Promise<FFIDAgencyServerClientResponse<FFIDProvisionedOrganization>>;
461
483
  }
@@ -124,6 +124,20 @@ interface FFIDProvisionOrganizationRequest {
124
124
  slug?: string;
125
125
  /** 請求先メールアドレス */
126
126
  billingEmail?: string;
127
+ /**
128
+ * 冪等キー(1〜255 文字の表示可能な ASCII。空白不可)。
129
+ *
130
+ * 指定すると、同じ代理店・同じキーの再送(24 時間以内)は**組織を作らずに**
131
+ * 初回の結果を返す。タイムアウトや `PARSE_ERROR` で応答を受け取れなかったときは、
132
+ * **同じキーのまま**再送すれば二重作成にならない。申込番号など、作りたい組織 1 つに
133
+ * 1 つ対応する値を使う。
134
+ *
135
+ * - 同じキーで内容(`name` / `ownerEmail` / `slug` / `billingEmail`)が違うと 422
136
+ * `IDEMPOTENCY_KEY_REUSED`(別の組織を作るなら別のキーを使う)
137
+ * - 省略すると従来どおり毎回作成する
138
+ * - HTTP では `Idempotency-Key` ヘッダーでも渡せる(本文と両方に入れる場合は同じ値)
139
+ */
140
+ idempotencyKey?: string;
127
141
  }
128
142
  /** 案内メールの送信結果。組織作成の成否とは独立している。 */
129
143
  type FFIDWelcomeEmailOutcome = 'sent' | 'failed';
@@ -453,9 +467,17 @@ interface FFIDAgencyServerClient {
453
467
  * そのほか:
454
468
  * - スコープ不足 / 代理店が停止中 → `FORBIDDEN`(403)
455
469
  * - slug が既存組織と衝突 → `ORGANIZATION_SLUG_CONFLICT`(409、組織は作成されない)
470
+ * - slug が予約語(`admin` / `api` / `login` など)→ `ORGANIZATION_SLUG_RESERVED`
471
+ * (400、組織は作成されない。別の値を指定する)
472
+ * - `ownerEmail` の既存アカウントが停止・削除済み → `OWNER_ACCOUNT_UNAVAILABLE`
473
+ * (422、**組織は作成されない**。別のメールアドレスを指定する)
456
474
  * - 認証段階の一時障害 → `SERVICE_UNAVAILABLE`(503、**安全に再試行できる**)
457
475
  * - `PARSE_ERROR` / `NETWORK_ERROR` — **組織が作成された可能性がある**。
458
- * 再試行の前に組織一覧を確認すること(#5165 で冪等キーを導入予定)
476
+ * `idempotencyKey` を付けていれば**同じキーで**再送してよい(作成済みなら
477
+ * 初回の結果が返り、組織は増えない。#5165)。付けていない場合は再試行の前に
478
+ * 組織一覧を確認すること
479
+ * - 同じ `idempotencyKey` で内容が違う → `IDEMPOTENCY_KEY_REUSED`(422)
480
+ * - `idempotencyKey` の形式不正 → `INVALID_IDEMPOTENCY_KEY`(400)
459
481
  */
460
482
  provisionCustomerOrganization(data: FFIDProvisionOrganizationRequest): Promise<FFIDAgencyServerClientResponse<FFIDProvisionedOrganization>>;
461
483
  }
@@ -308,6 +308,9 @@ var OAUTH_INTROSPECT_ENDPOINT = "/api/v1/oauth/introspect";
308
308
  var CACHE_KEY_PREFIX = "ffid:introspect:";
309
309
  var HEX_BASE = 16;
310
310
  var HEX_BYTE_WIDTH = 2;
311
+ var HTTP_TOO_MANY_REQUESTS = 429;
312
+ var HTTP_SERVER_ERROR_MIN = 500;
313
+ var HTTP_SERVER_ERROR_LIMIT = 600;
311
314
  function createVerifyAccessToken(deps) {
312
315
  const { authMode, baseUrl, serviceCode, serviceApiKey, verifyStrategy, logger, createError, errorCodes, cache, timeout } = deps;
313
316
  let jwtVerify2 = null;
@@ -362,6 +365,16 @@ function createVerifyAccessToken(deps) {
362
365
  }
363
366
  return verifyViaIntrospect(accessToken);
364
367
  }
368
+ function introspectionUnavailable(status) {
369
+ logger.error("Token introspection unavailable", { status });
370
+ return {
371
+ error: {
372
+ code: errorCodes.NETWORK_ERROR,
373
+ 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})`,
374
+ details: { status }
375
+ }
376
+ };
377
+ }
365
378
  async function verifyViaIntrospect(accessToken, cacheKeySuffix) {
366
379
  if (!serviceApiKey) {
367
380
  return {
@@ -419,10 +432,12 @@ function createVerifyAccessToken(deps) {
419
432
  }
420
433
  };
421
434
  }
435
+ const upstreamUnavailable = response.status === HTTP_TOO_MANY_REQUESTS || response.status >= HTTP_SERVER_ERROR_MIN && response.status < HTTP_SERVER_ERROR_LIMIT;
422
436
  let introspectResponse;
423
437
  try {
424
438
  introspectResponse = await response.json();
425
439
  } catch (parseError) {
440
+ if (upstreamUnavailable) return introspectionUnavailable(response.status);
426
441
  logger.error("Parse error during token verification:", parseError);
427
442
  return {
428
443
  error: {
@@ -431,6 +446,7 @@ function createVerifyAccessToken(deps) {
431
446
  }
432
447
  };
433
448
  }
449
+ if (upstreamUnavailable) return introspectionUnavailable(response.status);
434
450
  if (!response.ok) {
435
451
  const errorBody = introspectResponse;
436
452
  return {
@@ -1374,7 +1390,7 @@ var FFID_ORGANIZATION_PROFILE_ERROR_CODES = {
1374
1390
  };
1375
1391
 
1376
1392
  // src/client/version-check.ts
1377
- var SDK_VERSION = "10.1.0";
1393
+ var SDK_VERSION = "11.0.0";
1378
1394
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
1379
1395
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
1380
1396
  function sdkHeaders() {
@@ -1942,7 +1958,7 @@ function createInviteMethods(deps) {
1942
1958
  }).toString();
1943
1959
  return fetchWithAuth(`${EXT_INVITE_ENDPOINT}?${query}`, {
1944
1960
  method: "POST",
1945
- body: JSON.stringify({ email: params.email, role: params.role })
1961
+ body: JSON.stringify({ email: params.email, role: params.role, inviterUserId: params.inviterUserId })
1946
1962
  });
1947
1963
  }
1948
1964
  return { inviteMember };
@@ -2245,6 +2261,25 @@ function createOAuthTokenMethods(deps) {
2245
2261
  return { exchangeCodeForTokens, refreshAccessToken, signOutToken };
2246
2262
  }
2247
2263
 
2264
+ // src/client/request-timeout.ts
2265
+ function withRequestTimeout(options, timeout) {
2266
+ if (timeout === void 0 || options.signal?.aborted) return options;
2267
+ const deadline = AbortSignal.timeout(timeout);
2268
+ if (!options.signal) return { ...options, signal: deadline };
2269
+ const callerSignal = options.signal;
2270
+ const controller = new AbortController();
2271
+ const abortFromCaller = () => abort(callerSignal.reason);
2272
+ const abortFromDeadline = () => abort(deadline.reason);
2273
+ function abort(reason) {
2274
+ callerSignal.removeEventListener("abort", abortFromCaller);
2275
+ deadline.removeEventListener("abort", abortFromDeadline);
2276
+ controller.abort(reason);
2277
+ }
2278
+ callerSignal.addEventListener("abort", abortFromCaller, { once: true });
2279
+ deadline.addEventListener("abort", abortFromDeadline, { once: true });
2280
+ return { ...options, signal: controller.signal };
2281
+ }
2282
+
2248
2283
  // src/client/session.ts
2249
2284
  var SESSION_ENDPOINT = "/api/v1/auth/session";
2250
2285
  var LOGOUT_ENDPOINT = "/api/v1/auth/signout";
@@ -3979,6 +4014,7 @@ function toServiceAccessDecision(response, allowGrace) {
3979
4014
  const isBlocked = response.isBlocked ?? (effectiveStatus === null || effectiveStatus !== null && BLOCKING_EFFECTIVE_STATUSES.includes(effectiveStatus));
3980
4015
  return {
3981
4016
  hasAccess,
4017
+ ...response.seatAssigned !== void 0 ? { seatAssigned: response.seatAssigned } : {},
3982
4018
  effectiveStatus,
3983
4019
  isGrace,
3984
4020
  isBlocked: isBlocked || !hasAccess,
@@ -4046,6 +4082,7 @@ var FFID_SUBSCRIPTION_ERROR_CODES = {
4046
4082
  var FFID_CREDITS_ERROR_CODES = {
4047
4083
  CREDITS_INSUFFICIENT: "CREDITS_INSUFFICIENT",
4048
4084
  CREDITS_IDEMPOTENCY_CONFLICT: "CREDITS_IDEMPOTENCY_CONFLICT",
4085
+ CREDITS_TURN_CANCELLED: "CREDITS_TURN_CANCELLED",
4049
4086
  CREDITS_NOT_APPLICABLE: "CREDITS_NOT_APPLICABLE",
4050
4087
  CREDITS_BALANCE_CAP_EXCEEDED: "CREDITS_BALANCE_CAP_EXCEEDED",
4051
4088
  SEAT_BILLING_NOT_APPLICABLE: "SEAT_BILLING_NOT_APPLICABLE",
@@ -4210,9 +4247,10 @@ function createFFIDClient(config) {
4210
4247
  async function fetchWithAuth(endpoint, options = {}, authOverride) {
4211
4248
  const url = `${baseUrl}${endpoint}`;
4212
4249
  logger.debug("Fetching:", url);
4213
- const fetchOptions = buildFetchOptions(options, authOverride);
4250
+ let fetchOptions = buildFetchOptions(options, authOverride);
4214
4251
  let response;
4215
4252
  try {
4253
+ fetchOptions = withRequestTimeout(fetchOptions, timeout);
4216
4254
  response = await fetch(url, fetchOptions);
4217
4255
  } catch (error) {
4218
4256
  logger.error("Network error:", error);
@@ -4227,9 +4265,9 @@ function createFFIDClient(config) {
4227
4265
  const refreshResult = await refreshAccessToken();
4228
4266
  if (!refreshResult.error) {
4229
4267
  logger.debug("Token refreshed, retrying request");
4230
- const retryOptions = buildFetchOptions(options);
4231
4268
  try {
4232
- response = await fetch(url, retryOptions);
4269
+ fetchOptions = withRequestTimeout(buildFetchOptions(options), timeout);
4270
+ response = await fetch(url, fetchOptions);
4233
4271
  } catch (retryError) {
4234
4272
  logger.error("Network error on retry:", retryError);
4235
4273
  return {
@@ -4248,6 +4286,15 @@ function createFFIDClient(config) {
4248
4286
  try {
4249
4287
  raw = await response.json();
4250
4288
  } catch (parseError) {
4289
+ if (fetchOptions.signal?.aborted) {
4290
+ logger.error("Response body aborted:", parseError);
4291
+ return {
4292
+ error: {
4293
+ code: FFID_ERROR_CODES.NETWORK_ERROR,
4294
+ message: "\u30EC\u30B9\u30DD\u30F3\u30B9\u306E\u53D7\u4FE1\u304C\u4E2D\u65AD\u3055\u308C\u307E\u3057\u305F"
4295
+ }
4296
+ };
4297
+ }
4251
4298
  logger.error("Parse error:", parseError, "Status:", response.status);
4252
4299
  return {
4253
4300
  error: {
@@ -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.1.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: {
@@ -1,34 +1,34 @@
1
1
  'use strict';
2
2
 
3
- var chunkVHOIA25Y_cjs = require('../chunk-VHOIA25Y.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 chunkVHOIA25Y_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 chunkVHOIA25Y_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 chunkVHOIA25Y_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 chunkVHOIA25Y_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 chunkVHOIA25Y_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 chunkVHOIA25Y_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 chunkVHOIA25Y_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-CWGoIgYL.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-CWGoIgYL.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-ITSQIAO5.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
  /**
@@ -1045,7 +1054,10 @@ interface FFIDServiceAccessError {
1045
1054
  details?: unknown;
1046
1055
  }
1047
1056
  interface FFIDServiceAccessDecision {
1057
+ /** Contract access; user access also requires seatAssigned === true. */
1048
1058
  hasAccess: boolean;
1059
+ /** User's active seat, passed through when returned. Gate user access with === true. */
1060
+ seatAssigned?: boolean | null;
1049
1061
  effectiveStatus: EffectiveSubscriptionStatus | null;
1050
1062
  isGrace: boolean;
1051
1063
  isBlocked: boolean;
@@ -1210,9 +1222,18 @@ interface FFIDAddMemberRequest {
1210
1222
  interface FFIDAddMemberParams extends FFIDAddMemberRequest {
1211
1223
  organizationId: string;
1212
1224
  }
1225
+ /** Seat assignment outcome after membership creation; membership exists in every case. */
1226
+ type FFIDMemberSeatAssignmentResult = 'assigned' | 'not_applicable' | 'member_limit_reached' | 'failed';
1213
1227
  /** Response from addMember (ext) */
1214
1228
  interface FFIDAddMemberResponse {
1215
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;
1216
1237
  }
1217
1238
  /** Response from updateMemberRole (ext) */
1218
1239
  interface FFIDUpdateMemberRoleResponse {
@@ -2431,11 +2452,10 @@ interface FFIDConfig {
2431
2452
  */
2432
2453
  cache?: FFIDCacheConfig | undefined;
2433
2454
  /**
2434
- * Request timeout in milliseconds for FFID API calls. Applies to server-side
2435
- * token verification / introspection and, in `token` mode, to the client-side
2436
- * OAuth operations (code exchange / refresh / revoke) — including via the
2437
- * `FFIDProvider` `timeout` prop (#4260). On timeout the fetch is aborted and
2438
- * 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.
2439
2459
  * @default undefined (no timeout, uses fetch default)
2440
2460
  */
2441
2461
  timeout?: number | undefined;
@@ -2801,6 +2821,7 @@ declare function createFFIDClient(config: FFIDConfig): {
2801
2821
  organizationId: string;
2802
2822
  email: string;
2803
2823
  role?: FFIDAssignableMemberRole;
2824
+ inviterUserId?: string;
2804
2825
  }) => Promise<FFIDApiResponse<FFIDInviteMemberResponse>>;
2805
2826
  leaveOrganization: (params: FFIDLeaveOrganizationParams) => Promise<FFIDApiResponse<FFIDLeaveOrganizationResponse>>;
2806
2827
  getAnalyticsConfig: (serviceCode: string, options?: FFIDProfileCallOptions) => Promise<FFIDApiResponse<FFIDAnalyticsConfig>>;
@@ -2887,4 +2908,4 @@ declare function createFFIDClient(config: FFIDConfig): {
2887
2908
  /** Type of the FFID client */
2888
2909
  type FFIDClient = ReturnType<typeof createFFIDClient>;
2889
2910
 
2890
- 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 FFIDError 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 FFIDLogger 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 };