@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.
- package/README.md +1062 -1010
- package/dist/agency/index.d.cts +23 -1
- package/dist/agency/index.d.ts +23 -1
- package/dist/{chunk-OXW6QSTO.js → chunk-QORWLPWG.js} +174 -46
- package/dist/{chunk-2LU2F3F2.cjs → chunk-VR3QR64A.cjs} +176 -45
- package/dist/components/index.cjs +8 -8
- package/dist/components/index.d.cts +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +1 -1
- package/dist/{ffid-client-5uxXRgIO.d.ts → ffid-client-By_vcKYu.d.ts} +32 -10
- package/dist/{ffid-client-CumygXXz.d.cts → ffid-client-C3A8L9uW.d.cts} +32 -10
- package/dist/{index-KjewcNn1.d.cts → index-BknVfXhZ.d.cts} +21 -9
- package/dist/{index-KjewcNn1.d.ts → index-BknVfXhZ.d.ts} +21 -9
- package/dist/index.cjs +100 -88
- package/dist/index.d.cts +117 -7
- package/dist/index.d.ts +117 -7
- package/dist/index.js +2 -2
- package/dist/server/index.cjs +101 -13
- package/dist/server/index.d.cts +74 -3
- package/dist/server/index.d.ts +74 -3
- package/dist/server/index.js +99 -14
- package/dist/server/test/index.d.cts +1 -1
- package/dist/server/test/index.d.ts +1 -1
- package/package.json +131 -131
package/dist/agency/index.d.cts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
}
|
package/dist/agency/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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 = "
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
@@ -4609,6 +4656,50 @@ function createFFIDClient(config) {
|
|
|
4609
4656
|
};
|
|
4610
4657
|
}
|
|
4611
4658
|
|
|
4659
|
+
// src/client/authorization-error.ts
|
|
4660
|
+
var FFID_OAUTH_DENIAL_REASONS = [
|
|
4661
|
+
/** The user is not a member of the organization */
|
|
4662
|
+
"no_membership",
|
|
4663
|
+
/** The organization has no usable subscription for the service */
|
|
4664
|
+
"no_active_subscription",
|
|
4665
|
+
/** The organization has a subscription, but the user has no seat in it */
|
|
4666
|
+
"no_seat_assignment",
|
|
4667
|
+
/** The subscription has expired (canceled / past due beyond grace) */
|
|
4668
|
+
"subscription_expired",
|
|
4669
|
+
/** The flat plan's member limit is full, so no seat could be given */
|
|
4670
|
+
"member_limit_reached"
|
|
4671
|
+
];
|
|
4672
|
+
function isFFIDOAuthDenialReason(value) {
|
|
4673
|
+
return typeof value === "string" && FFID_OAUTH_DENIAL_REASONS.includes(value);
|
|
4674
|
+
}
|
|
4675
|
+
var DEFAULT_AUTHORIZATION_ERROR_MESSAGE = "\u8A8D\u53EF\u30B5\u30FC\u30D0\u30FC\u304C\u30A8\u30E9\u30FC\u3092\u8FD4\u3057\u307E\u3057\u305F";
|
|
4676
|
+
function buildAuthorizationError(params) {
|
|
4677
|
+
const oauthError = params.get("error");
|
|
4678
|
+
if (!oauthError) return null;
|
|
4679
|
+
const rawReason = params.get("denial_reason");
|
|
4680
|
+
const errorDescription = params.get("error_description");
|
|
4681
|
+
const details = {
|
|
4682
|
+
source: "authorization",
|
|
4683
|
+
denialReason: isFFIDOAuthDenialReason(rawReason) ? rawReason : null,
|
|
4684
|
+
errorDescription
|
|
4685
|
+
};
|
|
4686
|
+
return {
|
|
4687
|
+
code: oauthError,
|
|
4688
|
+
message: errorDescription ?? DEFAULT_AUTHORIZATION_ERROR_MESSAGE,
|
|
4689
|
+
details: { ...details }
|
|
4690
|
+
};
|
|
4691
|
+
}
|
|
4692
|
+
function getFFIDAuthorizationError(error) {
|
|
4693
|
+
const details = error?.details;
|
|
4694
|
+
if (!error || !details || details.source !== "authorization") return null;
|
|
4695
|
+
const description = details.errorDescription;
|
|
4696
|
+
return {
|
|
4697
|
+
oauthError: error.code,
|
|
4698
|
+
denialReason: isFFIDOAuthDenialReason(details.denialReason) ? details.denialReason : null,
|
|
4699
|
+
errorDescription: typeof description === "string" ? description : null
|
|
4700
|
+
};
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4612
4703
|
// src/client/callback.ts
|
|
4613
4704
|
var CALLBACK_ERROR_CODES = {
|
|
4614
4705
|
/** Stored state missing or did not match the callback `state` (CSRF guard) */
|
|
@@ -4633,16 +4724,43 @@ function extractSearchParams(source) {
|
|
|
4633
4724
|
return new URLSearchParams(query);
|
|
4634
4725
|
}
|
|
4635
4726
|
}
|
|
4636
|
-
|
|
4727
|
+
var CODE_CALLBACK_PARAMS = ["code", "state"];
|
|
4728
|
+
var ERROR_CALLBACK_CONSUMED_PARAMS = ["state"];
|
|
4729
|
+
function cleanCallbackUrlParams(logger, names) {
|
|
4637
4730
|
try {
|
|
4638
4731
|
const cleanUrl = new URL(window.location.href);
|
|
4639
|
-
cleanUrl.searchParams.delete(
|
|
4640
|
-
cleanUrl.searchParams.delete("state");
|
|
4732
|
+
for (const name of names) cleanUrl.searchParams.delete(name);
|
|
4641
4733
|
window.history.replaceState({}, "", cleanUrl.toString());
|
|
4642
4734
|
} catch (error) {
|
|
4643
4735
|
logger.warn("handleRedirectCallback: URL \u306E\u30AF\u30EA\u30FC\u30F3\u30A2\u30C3\u30D7\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", error);
|
|
4644
4736
|
}
|
|
4645
4737
|
}
|
|
4738
|
+
function validateCallbackState(client, urlState) {
|
|
4739
|
+
const { logger } = client;
|
|
4740
|
+
const storedState = retrieveState(logger);
|
|
4741
|
+
if (storedState === null && urlState === null) return null;
|
|
4742
|
+
if (storedState !== null && storedState === urlState) return null;
|
|
4743
|
+
if (storedState === null && !isStateStorageAvailable()) {
|
|
4744
|
+
logger.error(
|
|
4745
|
+
"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"
|
|
4746
|
+
);
|
|
4747
|
+
return {
|
|
4748
|
+
error: client.createError(
|
|
4749
|
+
CALLBACK_ERROR_CODES.STATE_UNAVAILABLE,
|
|
4750
|
+
"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"
|
|
4751
|
+
)
|
|
4752
|
+
};
|
|
4753
|
+
}
|
|
4754
|
+
logger.error(
|
|
4755
|
+
"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"
|
|
4756
|
+
);
|
|
4757
|
+
return {
|
|
4758
|
+
error: client.createError(
|
|
4759
|
+
CALLBACK_ERROR_CODES.STATE_MISMATCH,
|
|
4760
|
+
"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"
|
|
4761
|
+
)
|
|
4762
|
+
};
|
|
4763
|
+
}
|
|
4646
4764
|
async function handleRedirectCallback(client, options) {
|
|
4647
4765
|
const { logger } = client;
|
|
4648
4766
|
const hasWindow = typeof window !== "undefined";
|
|
@@ -4659,6 +4777,24 @@ async function handleRedirectCallback(client, options) {
|
|
|
4659
4777
|
const params = extractSearchParams(source);
|
|
4660
4778
|
const code = params.get("code");
|
|
4661
4779
|
const urlState = params.get("state");
|
|
4780
|
+
const authorizationError = buildAuthorizationError(params);
|
|
4781
|
+
if (authorizationError && urlState !== null) {
|
|
4782
|
+
const stateError2 = validateCallbackState(client, urlState);
|
|
4783
|
+
if (stateError2) return stateError2;
|
|
4784
|
+
retrieveCodeVerifier(logger);
|
|
4785
|
+
logger.warn("handleRedirectCallback: \u8A8D\u53EF\u30B5\u30FC\u30D0\u30FC\u304C\u30A8\u30E9\u30FC\u3092\u8FD4\u3057\u307E\u3057\u305F:", {
|
|
4786
|
+
error: authorizationError.code,
|
|
4787
|
+
denialReason: params.get("denial_reason")
|
|
4788
|
+
});
|
|
4789
|
+
if (hasWindow) {
|
|
4790
|
+
if (options?.cleanUrl !== false) {
|
|
4791
|
+
cleanCallbackUrlParams(logger, ERROR_CALLBACK_CONSUMED_PARAMS);
|
|
4792
|
+
} else {
|
|
4793
|
+
logger.debug("handleRedirectCallback: cleanUrl=false \u306E\u305F\u3081 URL cleanup \u3092 consumer \u306B\u59D4\u8B72\u3057\u307E\u3057\u305F");
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
return { error: authorizationError };
|
|
4797
|
+
}
|
|
4662
4798
|
if (!code) {
|
|
4663
4799
|
logger.warn("handleRedirectCallback: \u8A8D\u53EF\u30B3\u30FC\u30C9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093");
|
|
4664
4800
|
return {
|
|
@@ -4668,37 +4804,13 @@ async function handleRedirectCallback(client, options) {
|
|
|
4668
4804
|
)
|
|
4669
4805
|
};
|
|
4670
4806
|
}
|
|
4671
|
-
const
|
|
4672
|
-
if (
|
|
4673
|
-
const stateMatches = storedState !== null && storedState === urlState;
|
|
4674
|
-
if (!stateMatches) {
|
|
4675
|
-
if (storedState === null && !isStateStorageAvailable()) {
|
|
4676
|
-
logger.error(
|
|
4677
|
-
"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"
|
|
4678
|
-
);
|
|
4679
|
-
return {
|
|
4680
|
-
error: client.createError(
|
|
4681
|
-
CALLBACK_ERROR_CODES.STATE_UNAVAILABLE,
|
|
4682
|
-
"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"
|
|
4683
|
-
)
|
|
4684
|
-
};
|
|
4685
|
-
}
|
|
4686
|
-
logger.error(
|
|
4687
|
-
"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"
|
|
4688
|
-
);
|
|
4689
|
-
return {
|
|
4690
|
-
error: client.createError(
|
|
4691
|
-
CALLBACK_ERROR_CODES.STATE_MISMATCH,
|
|
4692
|
-
"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"
|
|
4693
|
-
)
|
|
4694
|
-
};
|
|
4695
|
-
}
|
|
4696
|
-
}
|
|
4807
|
+
const stateError = validateCallbackState(client, urlState);
|
|
4808
|
+
if (stateError) return stateError;
|
|
4697
4809
|
const verifier = retrieveCodeVerifier(logger);
|
|
4698
4810
|
const result = await client.exchangeCodeForTokens(code, verifier ?? void 0);
|
|
4699
4811
|
if (!result.error && hasWindow) {
|
|
4700
4812
|
if (options?.cleanUrl !== false) {
|
|
4701
|
-
cleanCallbackUrlParams(logger);
|
|
4813
|
+
cleanCallbackUrlParams(logger, CODE_CALLBACK_PARAMS);
|
|
4702
4814
|
} else {
|
|
4703
4815
|
logger.debug("handleRedirectCallback: cleanUrl=false \u306E\u305F\u3081 URL cleanup \u3092 consumer \u306B\u59D4\u8B72\u3057\u307E\u3057\u305F");
|
|
4704
4816
|
}
|
|
@@ -4707,6 +4819,13 @@ async function handleRedirectCallback(client, options) {
|
|
|
4707
4819
|
}
|
|
4708
4820
|
var DEFAULT_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
4709
4821
|
var TOKEN_REFRESH_RATIO = 0.8;
|
|
4822
|
+
function resolveCallbackKey(params) {
|
|
4823
|
+
const code = params.get("code");
|
|
4824
|
+
if (code) return code;
|
|
4825
|
+
const error = params.get("error");
|
|
4826
|
+
const state = params.get("state");
|
|
4827
|
+
return error && state ? `error:${error}:${state}` : null;
|
|
4828
|
+
}
|
|
4710
4829
|
var FFIDContext = createContext(null);
|
|
4711
4830
|
var FFIDClientContext = createContext(null);
|
|
4712
4831
|
function FFIDProvider({
|
|
@@ -4886,19 +5005,26 @@ function FFIDProvider({
|
|
|
4886
5005
|
},
|
|
4887
5006
|
[organizations, client]
|
|
4888
5007
|
);
|
|
4889
|
-
const
|
|
5008
|
+
const handledCallbackRef = useRef(null);
|
|
5009
|
+
const callbackPendingRef = useRef(false);
|
|
5010
|
+
const authorizationDeniedRef = useRef(false);
|
|
4890
5011
|
useEffect(() => {
|
|
4891
5012
|
if (client.authMode !== "token") return;
|
|
4892
5013
|
if (typeof window === "undefined") return;
|
|
4893
|
-
const
|
|
4894
|
-
|
|
4895
|
-
if (
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
client.logger.debug("
|
|
5014
|
+
const callbackKey = resolveCallbackKey(new URLSearchParams(window.location.search));
|
|
5015
|
+
if (!callbackKey) return;
|
|
5016
|
+
if (handledCallbackRef.current === callbackKey) return;
|
|
5017
|
+
handledCallbackRef.current = callbackKey;
|
|
5018
|
+
callbackPendingRef.current = true;
|
|
5019
|
+
client.logger.debug("OAuth callback detected, handling it");
|
|
4899
5020
|
handleRedirectCallback(client, { cleanUrl: cleanCallbackUrl }).then((result) => {
|
|
4900
5021
|
if (result.error) {
|
|
4901
|
-
|
|
5022
|
+
if (getFFIDAuthorizationError(result.error)) {
|
|
5023
|
+
client.logger.warn("FFID \u304C\u30ED\u30B0\u30A4\u30F3\u3092\u62D2\u5426\u3057\u307E\u3057\u305F:", result.error);
|
|
5024
|
+
authorizationDeniedRef.current = true;
|
|
5025
|
+
} else {
|
|
5026
|
+
client.logger.error("Token exchange failed:", result.error);
|
|
5027
|
+
}
|
|
4902
5028
|
if (result.error.code === FFID_ERROR_CODES.STATE_MISMATCH_ERROR && client.tokenStore.getTokens()) {
|
|
4903
5029
|
client.logger.debug(
|
|
4904
5030
|
"Stale callback rejected but stored tokens present, restoring session via refresh"
|
|
@@ -4916,13 +5042,15 @@ function FFIDProvider({
|
|
|
4916
5042
|
}).catch((err) => {
|
|
4917
5043
|
client.logger.error("Token exchange error:", err);
|
|
4918
5044
|
setIsLoading(false);
|
|
5045
|
+
}).finally(() => {
|
|
5046
|
+
callbackPendingRef.current = false;
|
|
4919
5047
|
});
|
|
4920
5048
|
}, [client]);
|
|
4921
5049
|
useEffect(() => {
|
|
4922
5050
|
if (client.authMode === "token") {
|
|
5051
|
+
if (callbackPendingRef.current || authorizationDeniedRef.current) return;
|
|
4923
5052
|
if (typeof window !== "undefined") {
|
|
4924
|
-
|
|
4925
|
-
if (urlParams.has("code")) {
|
|
5053
|
+
if (resolveCallbackKey(new URLSearchParams(window.location.search))) {
|
|
4926
5054
|
return;
|
|
4927
5055
|
}
|
|
4928
5056
|
}
|
|
@@ -6737,4 +6865,4 @@ function FFIDInquiryForm({
|
|
|
6737
6865
|
);
|
|
6738
6866
|
}
|
|
6739
6867
|
|
|
6740
|
-
export { ACCESS_GRANTING_EFFECTIVE_STATUSES, AGENCY_INVITE_COOKIE_MAX_AGE_SEC, AGENCY_INVITE_COOKIE_NAME, AGENCY_INVITE_TOKEN_PARAM, AGENCY_INVITE_TOKEN_REDACTION, BLOCKING_EFFECTIVE_STATUSES, CATALOG_HASH_PATTERN, DEFAULT_API_BASE_URL, DEFAULT_OAUTH_SCOPES, EFFECTIVE_SUBSCRIPTION_STATUSES, FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDCatalogHashError, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDProvider, FFIDSDKError, FFIDSubscriptionBadge, FFIDUserMenu, FFID_ALLOWED_ORGANIZATION_LOGO_MIME_TYPES, FFID_ANNOUNCEMENTS_ERROR_CODES, FFID_CATALOG_ENVIRONMENTS, FFID_CATALOG_PUBLICATION_STATUSES, FFID_COMPANY_SIZE_CODES, FFID_CREDITS_CONSUME_MAX_AMOUNT, FFID_CREDITS_CONSUME_MIN_AMOUNT, FFID_CREDITS_ERROR_CODES, FFID_CREDITS_FEATURE_PATTERN, FFID_CREDITS_HISTORY_DEFAULT_LIMIT, FFID_CREDITS_HISTORY_MAX_LIMIT, FFID_CREDITS_HISTORY_MIN_LIMIT, FFID_CREDITS_IDEMPOTENCY_KEY_PATTERN, FFID_CREDITS_LOW_THRESHOLD_PERCENT, FFID_CREDITS_METADATA_MAX_BYTES, FFID_CREDIT_GRANT_KINDS, FFID_CREDIT_HISTORY_TYPES, FFID_CREDIT_PACK_MAX_QUANTITY, FFID_CREDIT_PACK_MIN_QUANTITY, FFID_ERROR_CODES, FFID_INDUSTRY_CODES, FFID_INQUIRY_ACQUISITION_SOURCES, FFID_INQUIRY_CATEGORIES, FFID_INQUIRY_CATEGORIES_SITE_2026, FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_BYTES, FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_MB, FFID_ORGANIZATION_PROFILE_ERROR_CODES, FFID_SUBSCRIPTION_ERROR_CODES, canonicalizeCatalogValue, captureAgencyInviteToken, cleanupStateStorage, clearAgencyInviteToken, computeEffectiveStatusFromSession, computeFFIDCheckoutCopyHash, computeLegalDisclosureHash, computePaymentConfigurationHash, computePraxisCopyHash, computeScopeHash, computeTaxConfigurationHash, createFFIDAnnouncementsClient, createFFIDClient, createTokenStore, generateCodeChallenge, generateCodeVerifier, handleRedirectCallback, hasAccessFromUserinfo, hashCanonicalJson, isBlockedFromUserinfo, isFFIDInquiryAcquisitionSource, isFFIDInquiryCategorySite2026, normalizeRedirectUri, readAgencyInviteCookie, readAgencyInviteToken, redactAgencyInviteToken, retrieveCodeVerifier, retrieveState, sha256Hex, storeCodeVerifier, storeState, useFFID, useFFIDAnnouncements, useFFIDContext, useSubscription, validatePublishedCatalog, withSubscription };
|
|
6868
|
+
export { ACCESS_GRANTING_EFFECTIVE_STATUSES, AGENCY_INVITE_COOKIE_MAX_AGE_SEC, AGENCY_INVITE_COOKIE_NAME, AGENCY_INVITE_TOKEN_PARAM, AGENCY_INVITE_TOKEN_REDACTION, BLOCKING_EFFECTIVE_STATUSES, CATALOG_HASH_PATTERN, DEFAULT_API_BASE_URL, DEFAULT_OAUTH_SCOPES, EFFECTIVE_SUBSCRIPTION_STATUSES, FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDCatalogHashError, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDProvider, FFIDSDKError, FFIDSubscriptionBadge, FFIDUserMenu, FFID_ALLOWED_ORGANIZATION_LOGO_MIME_TYPES, FFID_ANNOUNCEMENTS_ERROR_CODES, FFID_CATALOG_ENVIRONMENTS, FFID_CATALOG_PUBLICATION_STATUSES, FFID_COMPANY_SIZE_CODES, FFID_CREDITS_CONSUME_MAX_AMOUNT, FFID_CREDITS_CONSUME_MIN_AMOUNT, FFID_CREDITS_ERROR_CODES, FFID_CREDITS_FEATURE_PATTERN, FFID_CREDITS_HISTORY_DEFAULT_LIMIT, FFID_CREDITS_HISTORY_MAX_LIMIT, FFID_CREDITS_HISTORY_MIN_LIMIT, FFID_CREDITS_IDEMPOTENCY_KEY_PATTERN, FFID_CREDITS_LOW_THRESHOLD_PERCENT, FFID_CREDITS_METADATA_MAX_BYTES, FFID_CREDIT_GRANT_KINDS, FFID_CREDIT_HISTORY_TYPES, FFID_CREDIT_PACK_MAX_QUANTITY, FFID_CREDIT_PACK_MIN_QUANTITY, FFID_ERROR_CODES, FFID_INDUSTRY_CODES, FFID_INQUIRY_ACQUISITION_SOURCES, FFID_INQUIRY_CATEGORIES, FFID_INQUIRY_CATEGORIES_SITE_2026, FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_BYTES, FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_MB, FFID_OAUTH_DENIAL_REASONS, FFID_ORGANIZATION_PROFILE_ERROR_CODES, FFID_SUBSCRIPTION_ERROR_CODES, canonicalizeCatalogValue, captureAgencyInviteToken, cleanupStateStorage, clearAgencyInviteToken, computeEffectiveStatusFromSession, computeFFIDCheckoutCopyHash, computeLegalDisclosureHash, computePaymentConfigurationHash, computePraxisCopyHash, computeScopeHash, computeTaxConfigurationHash, createFFIDAnnouncementsClient, createFFIDClient, createTokenStore, generateCodeChallenge, generateCodeVerifier, getFFIDAuthorizationError, handleRedirectCallback, hasAccessFromUserinfo, hashCanonicalJson, isBlockedFromUserinfo, isFFIDInquiryAcquisitionSource, isFFIDInquiryCategorySite2026, isFFIDOAuthDenialReason, normalizeRedirectUri, readAgencyInviteCookie, readAgencyInviteToken, redactAgencyInviteToken, retrieveCodeVerifier, retrieveState, sha256Hex, storeCodeVerifier, storeState, useFFID, useFFIDAnnouncements, useFFIDContext, useSubscription, validatePublishedCatalog, withSubscription };
|