@feelflow/ffid-sdk 5.18.0 → 5.21.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
@@ -396,6 +396,83 @@ await ffid.updateMemberRole({
396
396
  - `role` は `admin` / `member` / `viewer` のみ指定可能です。`owner` は追加・昇格 API では扱いません。
397
397
  - agency client と同じ呼び味が必要な場合は `ffid.addMember(organizationId, { email, role })` も使えます。
398
398
 
399
+ ### Provisioning(users / organizations)
400
+
401
+ 外部サービスからの一括移行向けに、**service-key 認証**でユーザー・組織を冪等にプロビジョニングするメソッド。REST を直叩きせず `createFFIDClient` 経由で呼べる(SDK-first、#3790 / #4127)。
402
+
403
+ - **`authMode: 'service-key'`(`X-Service-Api-Key`)専用**。`token`(Bearer)/ `cookie` モードで呼ぶとサーバーへ往復せず即座に `VALIDATION_ERROR` を返す(サーバー側エンドポイントが service-key 認証のみを受け付けるため)。
404
+ - 必要 scope: `provisionUser` → `user:provision`、`provisionOrganization` → `organization:write`。
405
+
406
+ ```ts
407
+ import { createFFIDClient } from '@feelflow/ffid-sdk/server'
408
+
409
+ const ffid = createFFIDClient({
410
+ serviceCode: 'praxis',
411
+ scope: '',
412
+ authMode: 'service-key',
413
+ serviceApiKey: process.env.FFID_SERVICE_API_KEY!,
414
+ })
415
+
416
+ // 1. ユーザーを冪等に作成(新規なら created:true / 既存なら created:false)
417
+ const userRes = await ffid.provisionUser({
418
+ email: 'owner@example.com',
419
+ profile: { displayName: '山田 太郎', companyName: 'Example Inc.' },
420
+ })
421
+ if (userRes.error) throw new Error(userRes.error.message)
422
+ if (!userRes.data.dryRun) {
423
+ console.log(userRes.data.created ? '新規作成' : '既存ユーザー', userRes.data.user.id)
424
+ }
425
+
426
+ // 2. その owner の組織を作成し、メンバーを冪等に追加
427
+ const orgRes = await ffid.provisionOrganization({
428
+ name: 'Example 組織',
429
+ ownerEmail: 'owner@example.com',
430
+ members: [
431
+ { email: 'member1@example.com', role: 'member' },
432
+ { email: 'member2@example.com', role: 'viewer' },
433
+ ],
434
+ })
435
+ if (orgRes.error) {
436
+ // owner が未登録なら error.code === 'OWNER_NOT_FOUND'(先に provisionUser が必要)
437
+ throw new Error(orgRes.error.message)
438
+ }
439
+ if (!orgRes.data.dryRun) {
440
+ for (const m of orgRes.data.members) {
441
+ console.log(m.email, m.status) // 'added' | 'already_member' | 'user_not_found'
442
+ }
443
+ }
444
+ ```
445
+
446
+ #### 冪等セマンティクス
447
+
448
+ - `provisionUser`: **既存メールは `created: false`**(HTTP 200、no-op)、新規メールは `created: true`(HTTP 201、パスワードレス・メール確認済み)。メールはサーバー側で normalize(trim + lowercase)されるため、大文字小文字違いの再送でも同一ユーザーに解決する。新規作成で `profile` を渡した場合のみ `profileWritten` が付き、`false` は「ユーザーは作成されたがプロフィール書き込みに失敗」→ 再送を推奨。
449
+ - `provisionOrganization`: owner が同名の組織を既に持つ場合は `created: false`(HTTP 200)。`members` は冪等に追加され、各要素の `status` が `added` / `already_member` / `user_not_found` を返す。
450
+
451
+ #### dryRun
452
+
453
+ `dryRun: true` を渡すと **一切書き込まず**、実行時に何が起きるかだけを返す。レスポンスは `dryRun` 判別のタグ付きユニオン。
454
+
455
+ ```ts
456
+ const preview = await ffid.provisionUser({ email: 'owner@example.com', dryRun: true })
457
+ if (!preview.error && preview.data.dryRun) {
458
+ console.log(preview.data.wouldCreate) // true = 実行すれば新規作成される
459
+ }
460
+
461
+ const orgPreview = await ffid.provisionOrganization({
462
+ name: 'Example 組織',
463
+ ownerEmail: 'owner@example.com',
464
+ members: [{ email: 'member1@example.com', role: 'member' }],
465
+ dryRun: true,
466
+ })
467
+ if (!orgPreview.error && orgPreview.data.dryRun) {
468
+ console.log(orgPreview.data.wouldCreate) // 組織を新規作成するか
469
+ // per-member plan status: 'would_add' | 'already_member' | 'user_not_found'
470
+ orgPreview.data.members.forEach((m) => console.log(m.email, m.status))
471
+ }
472
+ ```
473
+
474
+ `if (res.data.dryRun)` で TypeScript が型を絞り込むため、dry-run 分岐では `wouldCreate`、実行分岐では `created` がそれぞれ型安全に参照できる。
475
+
399
476
  ### getProfile() / updateProfile()
400
477
 
401
478
  ログイン中ユーザー自身のプロフィールを取得・更新するメソッド(`createFFIDClient` から呼び出し)。
@@ -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),
@@ -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;
@@ -875,6 +896,93 @@ function createMembersMethods(deps) {
875
896
  return { listMembers, addMember, updateMemberRole, removeMember };
876
897
  }
877
898
 
899
+ // src/client/provisioning-methods.ts
900
+ var USER_PROVISION_ENDPOINT = "/api/v1/ext/users/provision";
901
+ var ORGANIZATION_PROVISION_ENDPOINT = "/api/v1/ext/organizations/provision";
902
+ var MAX_PROVISION_MEMBERS = 100;
903
+ var ASSIGNABLE_MEMBER_ROLES = {
904
+ admin: true,
905
+ member: true,
906
+ viewer: true
907
+ };
908
+ function isAssignableMemberRole(role) {
909
+ return Object.hasOwn(ASSIGNABLE_MEMBER_ROLES, role);
910
+ }
911
+ function createProvisioningMethods(deps) {
912
+ const { fetchWithAuth, createError, authMode } = deps;
913
+ function serviceKeyModeError() {
914
+ if (authMode === "service-key") return null;
915
+ return createError(
916
+ "VALIDATION_ERROR",
917
+ "provisionUser / provisionOrganization \u306F service-key \u8A8D\u8A3C\uFF08X-Service-Api-Key\uFF09\u3067\u306E\u307F\u5229\u7528\u3067\u304D\u307E\u3059\u3002Bearer / cookie \u30E2\u30FC\u30C9\u3067\u306F\u547C\u3073\u51FA\u305B\u307E\u305B\u3093"
918
+ );
919
+ }
920
+ async function provisionUser(params) {
921
+ const modeError = serviceKeyModeError();
922
+ if (modeError) return { error: modeError };
923
+ if (!params.email || !params.email.trim()) {
924
+ return { error: createError("VALIDATION_ERROR", "email \u306F\u5FC5\u9808\u3067\u3059") };
925
+ }
926
+ const body = { email: params.email.trim() };
927
+ if (params.profile !== void 0) body.profile = params.profile;
928
+ if (params.dryRun !== void 0) body.dryRun = params.dryRun;
929
+ return fetchWithAuth(USER_PROVISION_ENDPOINT, {
930
+ method: "POST",
931
+ body: JSON.stringify(body)
932
+ });
933
+ }
934
+ async function provisionOrganization(params) {
935
+ const modeError = serviceKeyModeError();
936
+ if (modeError) return { error: modeError };
937
+ if (!params.name || !params.name.trim()) {
938
+ return { error: createError("VALIDATION_ERROR", "name \u306F\u5FC5\u9808\u3067\u3059") };
939
+ }
940
+ if (!params.ownerEmail || !params.ownerEmail.trim()) {
941
+ return { error: createError("VALIDATION_ERROR", "ownerEmail \u306F\u5FC5\u9808\u3067\u3059") };
942
+ }
943
+ if (params.members) {
944
+ if (params.members.length > MAX_PROVISION_MEMBERS) {
945
+ return {
946
+ error: createError(
947
+ "VALIDATION_ERROR",
948
+ `members \u306F1\u30EA\u30AF\u30A8\u30B9\u30C8\u3042\u305F\u308A\u6700\u5927 ${MAX_PROVISION_MEMBERS} \u4EF6\u307E\u3067\u3067\u3059`
949
+ )
950
+ };
951
+ }
952
+ for (const member of params.members) {
953
+ if (!member.email || !member.email.trim()) {
954
+ return { error: createError("VALIDATION_ERROR", "members[].email \u306F\u5FC5\u9808\u3067\u3059") };
955
+ }
956
+ if (!isAssignableMemberRole(member.role)) {
957
+ return {
958
+ error: createError(
959
+ "VALIDATION_ERROR",
960
+ "members[].role \u306F admin\u3001member\u3001viewer \u306E\u3044\u305A\u308C\u304B\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044"
961
+ )
962
+ };
963
+ }
964
+ }
965
+ }
966
+ const body = {
967
+ name: params.name.trim(),
968
+ ownerEmail: params.ownerEmail.trim()
969
+ };
970
+ if (params.billingEmail !== void 0) body.billingEmail = params.billingEmail.trim();
971
+ if (params.members !== void 0) {
972
+ body.members = params.members.map((member) => ({
973
+ email: member.email.trim(),
974
+ role: member.role
975
+ }));
976
+ }
977
+ if (params.dryRun !== void 0) body.dryRun = params.dryRun;
978
+ return fetchWithAuth(ORGANIZATION_PROVISION_ENDPOINT, {
979
+ method: "POST",
980
+ body: JSON.stringify(body)
981
+ });
982
+ }
983
+ return { provisionUser, provisionOrganization };
984
+ }
985
+
878
986
  // src/client/seat-methods.ts
879
987
  function seatsEndpoint(subscriptionId) {
880
988
  return `/api/v1/subscriptions/ext/${encodeURIComponent(subscriptionId)}/seats/assignments`;
@@ -1119,7 +1227,7 @@ function createNonContractMethods(deps) {
1119
1227
  }
1120
1228
 
1121
1229
  // src/client/version-check.ts
1122
- var SDK_VERSION = "5.18.0";
1230
+ var SDK_VERSION = "5.21.0";
1123
1231
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
1124
1232
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
1125
1233
  function sdkHeaders() {
@@ -1361,33 +1469,48 @@ function createOAuthTokenMethods(deps) {
1361
1469
  async function signOutToken() {
1362
1470
  const tokens = tokenStore.getTokens();
1363
1471
  tokenStore.clearTokens();
1472
+ const allRevoked = {
1473
+ revoked: true,
1474
+ accessTokenRevoked: true,
1475
+ refreshTokenRevoked: true
1476
+ };
1364
1477
  if (!tokens) {
1365
1478
  logger.debug("No tokens to revoke");
1366
- return { data: void 0 };
1479
+ return { data: allRevoked };
1367
1480
  }
1368
- const url = `${baseUrl}${OAUTH_REVOKE_ENDPOINT}`;
1369
- logger.debug("Revoking token:", url);
1370
- try {
1371
- const response = await fetch(url, {
1372
- method: "POST",
1373
- credentials: "omit",
1374
- headers: { "Content-Type": "application/x-www-form-urlencoded", ...sdkHeaders() },
1375
- body: new URLSearchParams({
1376
- token: tokens.accessToken,
1377
- client_id: clientId
1378
- }).toString()
1379
- });
1380
- if (!response.ok) {
1381
- logger.warn(
1382
- "\u30C8\u30FC\u30AF\u30F3\u7121\u52B9\u5316\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u30B5\u30FC\u30D0\u30FC\u3067\u5931\u6557\u3057\u307E\u3057\u305F:",
1383
- `status=${response.status}`
1384
- );
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;
1385
1497
  }
1386
- } catch (error) {
1387
- 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");
1388
1511
  }
1389
1512
  logger.debug("Token sign-out completed");
1390
- return { data: void 0 };
1513
+ return { data: { revoked, accessTokenRevoked, refreshTokenRevoked } };
1391
1514
  }
1392
1515
  return { exchangeCodeForTokens, refreshAccessToken, signOutToken };
1393
1516
  }
@@ -1551,7 +1674,7 @@ function createSessionMethods(deps) {
1551
1674
  }
1552
1675
  if (response.status === NO_CONTENT_STATUS) {
1553
1676
  logger.debug("Response: 204 No Content");
1554
- return { data: void 0 };
1677
+ return { data: { revoked: true } };
1555
1678
  }
1556
1679
  if (!response.ok) {
1557
1680
  try {
@@ -1578,7 +1701,7 @@ function createSessionMethods(deps) {
1578
1701
  }
1579
1702
  }
1580
1703
  logger.debug("Response:", response.status);
1581
- return { data: void 0 };
1704
+ return { data: { revoked: true } };
1582
1705
  }
1583
1706
  return { getSession, signOutCookie };
1584
1707
  }
@@ -1796,6 +1919,9 @@ var REDIRECT_LOOP_WINDOW_MS = 6e4;
1796
1919
  var REDIRECT_LOOP_THRESHOLD = 3;
1797
1920
  var storageReadFailureLogged = false;
1798
1921
  var storageWriteFailureLogged = false;
1922
+ function isLoopDetectionDegraded() {
1923
+ return storageReadFailureLogged || storageWriteFailureLogged;
1924
+ }
1799
1925
  function logStorageReadFailure(logger, err) {
1800
1926
  if (storageReadFailureLogged) return;
1801
1927
  storageReadFailureLogged = true;
@@ -1940,9 +2066,16 @@ function createRedirectMethods(deps) {
1940
2066
  }
1941
2067
  const state = generateRandomState();
1942
2068
  if (!storeState(state, logger)) {
2069
+ cleanupVerifierStorage(logger);
2070
+ cleanupStateStorage(logger);
1943
2071
  logger.error(
1944
- "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"
1945
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
+ };
1946
2079
  }
1947
2080
  const redirectUri = resolvedRedirectUri ?? window.location.origin + window.location.pathname;
1948
2081
  const params = new URLSearchParams({
@@ -1977,7 +2110,7 @@ function createRedirectMethods(deps) {
1977
2110
  rollbackLastRedirectAttempt(authorizeKey, logger);
1978
2111
  throw err;
1979
2112
  }
1980
- return { success: true };
2113
+ return isLoopDetectionDegraded() ? { success: true, loopDetectionDisabled: true } : { success: true };
1981
2114
  }
1982
2115
  async function redirectToLogin(options) {
1983
2116
  if (typeof window === "undefined") {
@@ -3130,6 +3263,11 @@ function createFFIDClient(config) {
3130
3263
  createError,
3131
3264
  serviceCode: config.serviceCode
3132
3265
  });
3266
+ const { provisionUser, provisionOrganization } = createProvisioningMethods({
3267
+ fetchWithAuth,
3268
+ createError,
3269
+ authMode
3270
+ });
3133
3271
  const { getProfile, updateProfile } = createProfileMethods({
3134
3272
  fetchWithAuth,
3135
3273
  createError
@@ -3225,6 +3363,8 @@ function createFFIDClient(config) {
3225
3363
  addMember,
3226
3364
  updateMemberRole,
3227
3365
  removeMember,
3366
+ provisionUser,
3367
+ provisionOrganization,
3228
3368
  getProfile,
3229
3369
  updateProfile,
3230
3370
  // Non-contract ext API coverage (#3783)
@@ -4237,7 +4377,8 @@ var quietLogger = {
4237
4377
  },
4238
4378
  warn: () => {
4239
4379
  },
4240
- error: (...args) => console.error(SDK_LOG_PREFIX2, ...args)
4380
+ error: () => {
4381
+ }
4241
4382
  };
4242
4383
  var consoleLogger2 = {
4243
4384
  debug: (...args) => console.debug(SDK_LOG_PREFIX2, ...args),