@feelflow/ffid-sdk 7.1.0 → 8.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 +115 -5
- package/dist/{chunk-CI4N7KJK.cjs → chunk-RNCK5E3Y.cjs} +211 -14
- package/dist/{chunk-Y4TUICQZ.js → chunk-YKBAFPX5.js} +202 -15
- 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-QNEFA3mv.d.ts → ffid-client-5GAPcRIG.d.ts} +105 -23
- package/dist/{ffid-client-BFgoVPOZ.d.cts → ffid-client-BaEmw7jP.d.cts} +105 -23
- package/dist/{index-BMKhKhzF.d.cts → index-BG7g99pK.d.cts} +1 -0
- package/dist/{index-BMKhKhzF.d.ts → index-BG7g99pK.d.ts} +1 -0
- package/dist/index.cjs +104 -64
- package/dist/index.d.cts +378 -26
- package/dist/index.d.ts +378 -26
- package/dist/index.js +2 -2
- package/dist/server/index.cjs +135 -12
- package/dist/server/index.d.cts +2 -2
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +135 -12
- package/dist/server/test/index.d.cts +1 -1
- package/dist/server/test/index.d.ts +1 -1
- package/dist/webhooks/index.d.cts +55 -6
- package/dist/webhooks/index.d.ts +55 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -464,7 +464,7 @@ const ffid = createFFIDClient({
|
|
|
464
464
|
// 1. ユーザーを冪等に作成(新規なら created:true / 既存なら created:false)
|
|
465
465
|
const userRes = await ffid.provisionUser({
|
|
466
466
|
email: 'owner@example.com',
|
|
467
|
-
profile: { displayName: '山田 太郎'
|
|
467
|
+
profile: { displayName: '山田 太郎' },
|
|
468
468
|
})
|
|
469
469
|
if (userRes.error) throw new Error(userRes.error.message)
|
|
470
470
|
if (!userRes.data.dryRun) {
|
|
@@ -561,12 +561,13 @@ const { data: updated, error: updateError } = await client.updateProfile({
|
|
|
561
561
|
|
|
562
562
|
#### フィールドのクリア(null を渡す)
|
|
563
563
|
|
|
564
|
-
optional フィールドに `null` を渡すと、FFID backend 側で該当カラムを
|
|
564
|
+
対応する optional フィールドに `null` を渡すと、FFID backend 側で該当カラムを
|
|
565
|
+
SQL NULL にクリアします(v2.16.0〜 / #2354)。個人プロフィールの
|
|
566
|
+
`companyName` は廃止予定で、旧クライアントからの値は受理しても無視します。
|
|
565
567
|
|
|
566
568
|
```tsx
|
|
567
|
-
//
|
|
569
|
+
// 部署をクリア
|
|
568
570
|
await client.updateProfile({
|
|
569
|
-
companyName: null,
|
|
570
571
|
department: null,
|
|
571
572
|
})
|
|
572
573
|
```
|
|
@@ -579,7 +580,7 @@ await client.updateProfile({
|
|
|
579
580
|
| `null` | クリア(SQL NULL を書き込む) |
|
|
580
581
|
| `""`(空文字列) | 空文字リテラルをそのまま保存(`null` 扱いには**ならない**) |
|
|
581
582
|
|
|
582
|
-
対応フィールド: `displayName` / `phone` / `
|
|
583
|
+
対応フィールド: `displayName` / `phone` / `department` / `jobTitle` / `preferences`。`timezone` / `locale` は application-level invariant(サーバー側 normalization が string 前提)のため null 非許容。クリアは不可 — キー未指定で現状維持、もしくは新しい有効な値を渡す。
|
|
583
584
|
|
|
584
585
|
### getAnalyticsConfig() (#2347)
|
|
585
586
|
|
|
@@ -618,6 +619,115 @@ if (result.error) {
|
|
|
618
619
|
- `SERVICE_NOT_FOUND` (404) — DB に未登録の service code
|
|
619
620
|
- `INVALID_PARAM` (400) — server 側で形式違反を検出(pre-validate 通過後の boundary)
|
|
620
621
|
|
|
622
|
+
## 代理店の招待リンク(`?invite=`)
|
|
623
|
+
|
|
624
|
+
代理店の招待リンクから来た顧客が FFID で登録すると、作られた組織がその代理店に紐付きます。SDK の役割は**トークンを signup URL まで運ぶこと**だけで、紐付けの実務は FFID 側が行います。
|
|
625
|
+
|
|
626
|
+
> 代理店ポータルが発行するリンクは FFID の origin(`https://id.feelflow.net/signup?invite=…`)を指します。サービスサイトを着地点にする形(`https://<あなたのサービス>/?invite=…`)は、代理店がトークンを自分のキャンペーン URL に貼って成立させる導線です。この節が効くのはその形です。
|
|
627
|
+
|
|
628
|
+
### React 利用者(`FFIDProvider` を mount している場合)
|
|
629
|
+
|
|
630
|
+
**配線は不要です。** provider が mount 時に着地 URL の `invite` を捕捉し、first-party cookie へ保存します。`getSignupUrl()` が自動でトークンを載せます。
|
|
631
|
+
|
|
632
|
+
ただし provider は捕捉結果を返さないので、**cookie の保持に失敗したことを利用者側で観測する経路はありません**(debug ログには落ちます)。検知したい場合は provider に加えて自分で `captureAgencyInviteToken()` を呼び、戻り値の `persisted` を見てください。
|
|
633
|
+
|
|
634
|
+
### React 以外 / provider を使わない場合
|
|
635
|
+
|
|
636
|
+
`captureAgencyInviteToken()` を**ページ遷移ごとに**呼びます。
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
import { captureAgencyInviteToken } from '@feelflow/ffid-sdk'
|
|
640
|
+
|
|
641
|
+
// ページ読み込みごと(SPA ならルート遷移ごと)に呼ぶ
|
|
642
|
+
const { token, source, persisted } = captureAgencyInviteToken()
|
|
643
|
+
|
|
644
|
+
if (persisted === false) {
|
|
645
|
+
// cookie がブラウザに落とされた。今回の URL 経由では紐付くが、
|
|
646
|
+
// パラメータの無い別ページへ回遊すると失われる。
|
|
647
|
+
} else if (persisted === undefined && source === 'url') {
|
|
648
|
+
// 書ける環境が無かった(SSR)。ブラウザ側で捕捉し直す必要がある。
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
呼ばなかった場合は「着地ページで即 signup した人だけ紐付く」まで**劣化します**(壊れるのではなく、下記の回遊経路を救えなくなります)。
|
|
653
|
+
|
|
654
|
+
### なぜページ遷移ごとに呼ぶ必要があるのか
|
|
655
|
+
|
|
656
|
+
実際の導線は次の形です。
|
|
657
|
+
|
|
658
|
+
```
|
|
659
|
+
/?invite=abc に着地 → 料金ページを見る → /pricing で「登録」を押す
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
`getSignupUrl()` が呼ばれる時点の URL には `invite` がありません。着地時点で捕捉して保持する以外にこの経路を救う方法がなく、**SDK 自身はページ読み込みを hook できない**ため、起動点は利用者側にあります。
|
|
663
|
+
|
|
664
|
+
### ⚠️ 効かない経路: SSR(Server Component)で signup URL を組む場合
|
|
665
|
+
|
|
666
|
+
サーバーには `window.location.search` も `document.cookie` もありません。`getSignupUrl()` をそのまま呼ぶとトークンは載らないので、呼び出し側が読んだ値を渡してください(渡さない場合はプロセスあたり 1 度 warn します)。
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
// Server Component
|
|
670
|
+
import { cookies } from 'next/headers'
|
|
671
|
+
import { AGENCY_INVITE_COOKIE_NAME } from '@feelflow/ffid-sdk'
|
|
672
|
+
|
|
673
|
+
const inviteToken = (await cookies()).get(AGENCY_INVITE_COOKIE_NAME)?.value
|
|
674
|
+
const href = ffid.getSignupUrl(undefined, { inviteToken })
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### `authMode: 'token'`(OAuth モード)は v7.4.0 で対応済み
|
|
678
|
+
|
|
679
|
+
この設定では signup 意図が `/api/v1/oauth/authorize` を経由するため `getSignupUrl()` を通りません。**v7.4.0 以降は `redirectToAuthorize` が `invite` を authorize URL へ転送し、FFID 側が `/signup` へ引き継ぎます**(`redirectToLogin({ screenHint: 'signup' })` は token モードでは `redirectToAuthorize` へ委譲されるので、こちらも同様に載ります)。配線は不要です。
|
|
680
|
+
|
|
681
|
+
v7.3.x までは token モードでトークンが FFID に 1 度も届かず、紐付けが無言で落ちていました。**v7.4.0 未満を使っている場合は、SDK と FFID 本体の両方を更新してください** — 転送は SDK が `invite` を送り、FFID が `/signup` へ stamp する 2 段で成立します。
|
|
682
|
+
|
|
683
|
+
⚠️ 転送の gate は `screenHint === 'signup'` です。`prompt` に `'create'` を指定して signup 画面へ向かう経路は SDK の `FFIDPrompt`(`'select_account' | 'login'`)が受け付けないため到達しません。
|
|
684
|
+
|
|
685
|
+
### cookie の仕様
|
|
686
|
+
|
|
687
|
+
| 項目 | 値 |
|
|
688
|
+
|---|---|
|
|
689
|
+
| 名前 | `ffid_invite`(first-party・consumer 自身のホスト) |
|
|
690
|
+
| 有効期限 | 30 日 |
|
|
691
|
+
| `Path` | `/` |
|
|
692
|
+
| `SameSite` | `Lax` |
|
|
693
|
+
| `Secure` | `NODE_ENV === 'production'` のときだけ(`options.secure` で上書き可) |
|
|
694
|
+
| `Domain` | 既定は現在のホストのみ(`options.domain` で指定可) |
|
|
695
|
+
|
|
696
|
+
`Secure` を本番限定にしているのは、**https でない独自ドメイン**(LAN IP や plain http の検証機)でブラウザが `Secure` cookie を黙って捨てるためです。`http://localhost` は secure context として扱われるので `Secure` でも保持されます(ローカル開発では上書き不要)。
|
|
697
|
+
|
|
698
|
+
URL と cookie の両方にトークンがある場合は **URL が勝ち、cookie も更新されます**(最後に踏んだリンクが勝つ)。
|
|
699
|
+
|
|
700
|
+
**削除には書き込み時と同じ `path` / `domain` / `secure` が必要です。** ブラウザが一致を要求するため、既定以外で書いた cookie は既定の `clearAgencyInviteToken()` では消えません。
|
|
701
|
+
|
|
702
|
+
### ⚠️ 保持期間 30 日の副作用
|
|
703
|
+
|
|
704
|
+
招待リンクは既定で使用回数が無制限です。したがって保持中の cookie は、**同じブラウザで後から作られた組織も同じ代理店に紐付けます**。共有ブラウザではこれが誤紐付けになります。
|
|
705
|
+
|
|
706
|
+
SDK は登録完了時に cookie を消しません(登録完了は FFID のドメインで起こるため consumer から検知しにくい)。単回にしたい場合は、登録完了を検知できる地点で明示的に消してください。
|
|
707
|
+
|
|
708
|
+
```typescript
|
|
709
|
+
import { clearAgencyInviteToken } from '@feelflow/ffid-sdk'
|
|
710
|
+
|
|
711
|
+
clearAgencyInviteToken() // 削除が read-back で確認できたら true / SSR では undefined
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### ⚠️ トークンをログに出さないこと
|
|
715
|
+
|
|
716
|
+
このトークンは URL に載る**平文の資格情報**で、**単回使用ではありません**(招待リンクの使用回数上限は既定で無制限)。漏れたトークンは、新規組織が作られる限り何度でも紐付けに使えます。
|
|
717
|
+
|
|
718
|
+
SDK 内部ではログ・エラーレポート・アナリティクスに値を出しません。`getSignupUrl()` の出力を自前でログや解析イベントに送っている場合は、`redactAgencyInviteToken()` を通してください。
|
|
719
|
+
|
|
720
|
+
```typescript
|
|
721
|
+
import { redactAgencyInviteToken } from '@feelflow/ffid-sdk'
|
|
722
|
+
|
|
723
|
+
logger.info('signup url', redactAgencyInviteToken(ffid.getSignupUrl()))
|
|
724
|
+
// → https://id.feelflow.net/signup?redirect=...&service=svc&invite=[REDACTED]
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
素の `invite=` だけでなく、`redirect` パラメータに着地 URL が percent-encode されて入る**入れ子**(`redirect=…%3Finvite%3D…`)も潰します。覆う encode の深さは **2 段まで**です。GA4 の `page_location` のように URL を丸ごと送るイベントでも同じ経路を通してください。
|
|
728
|
+
|
|
729
|
+
**login URL にも `redirect` 経由で入れ子で載ります。** 専用の `&invite=` は付きませんが(紐付けは新規登録時の組織作成に対して起こるため)、着地 URL が `?invite=…` なら login URL の `redirect` の中に入ります。login URL をログに出す場合も redact してください。
|
|
730
|
+
|
|
621
731
|
## 型定義
|
|
622
732
|
|
|
623
733
|
```typescript
|
|
@@ -160,7 +160,7 @@ function normalizeUserinfo(raw) {
|
|
|
160
160
|
emailVerified: raw.email_verified,
|
|
161
161
|
name: raw.name,
|
|
162
162
|
picture: raw.picture,
|
|
163
|
-
companyName:
|
|
163
|
+
companyName: null,
|
|
164
164
|
department: raw.department ?? null,
|
|
165
165
|
position: raw.position ?? null,
|
|
166
166
|
phoneNumber: raw.phone_number ?? null,
|
|
@@ -228,13 +228,18 @@ function mapUserinfoSubscriptionToSession(userinfo, serviceCode) {
|
|
|
228
228
|
];
|
|
229
229
|
}
|
|
230
230
|
var JWKS_ENDPOINT = "/.well-known/jwks.json";
|
|
231
|
-
var
|
|
231
|
+
var LEGACY_JWT_ISSUER = "https://id.feelflow.net";
|
|
232
232
|
var JWT_ALGORITHM = "ES256";
|
|
233
233
|
var JWT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
234
|
+
function resolveAcceptedIssuers(baseUrl) {
|
|
235
|
+
const deployedOrigin = new URL(baseUrl).origin;
|
|
236
|
+
return deployedOrigin === LEGACY_JWT_ISSUER ? [LEGACY_JWT_ISSUER] : [deployedOrigin, LEGACY_JWT_ISSUER];
|
|
237
|
+
}
|
|
234
238
|
function createJwtVerifier(deps) {
|
|
235
239
|
const { baseUrl, serviceCode, logger, createError, errorCodes } = deps;
|
|
236
240
|
const jwksUrl = new URL(JWKS_ENDPOINT, baseUrl);
|
|
237
241
|
const jwks = jose.createRemoteJWKSet(jwksUrl);
|
|
242
|
+
const acceptedIssuers = resolveAcceptedIssuers(baseUrl);
|
|
238
243
|
async function verifyJwt(accessToken) {
|
|
239
244
|
if (!accessToken || !accessToken.trim()) {
|
|
240
245
|
return {
|
|
@@ -247,7 +252,7 @@ function createJwtVerifier(deps) {
|
|
|
247
252
|
try {
|
|
248
253
|
const { payload } = await jose.jwtVerify(accessToken, jwks, {
|
|
249
254
|
algorithms: [JWT_ALGORITHM],
|
|
250
|
-
issuer:
|
|
255
|
+
issuer: acceptedIssuers,
|
|
251
256
|
audience: serviceCode,
|
|
252
257
|
clockTolerance: JWT_CLOCK_TOLERANCE_SECONDS
|
|
253
258
|
});
|
|
@@ -464,7 +469,7 @@ function createVerifyAccessToken(deps) {
|
|
|
464
469
|
email_verified: introspectResponse.email_verified,
|
|
465
470
|
name: introspectResponse.name ?? null,
|
|
466
471
|
picture: introspectResponse.picture ?? null,
|
|
467
|
-
company_name:
|
|
472
|
+
company_name: null,
|
|
468
473
|
department: introspectResponse.department ?? null,
|
|
469
474
|
position: introspectResponse.position ?? null,
|
|
470
475
|
phone_number: introspectResponse.phone_number ?? null,
|
|
@@ -715,6 +720,24 @@ function validateSeatChangePreview(preview) {
|
|
|
715
720
|
`SDK: server returned malformed SeatChangePreview \u2014 estimateReason must be one of 'no_stripe_data' | 'custom_pricing' | 'stripe_error' (got ${JSON.stringify(p.estimateReason)})`
|
|
716
721
|
);
|
|
717
722
|
}
|
|
723
|
+
if (p.willApplyAtPeriodEnd !== void 0 && typeof p.willApplyAtPeriodEnd !== "boolean") {
|
|
724
|
+
throw new FFIDSDKError(
|
|
725
|
+
MALFORMED_SEAT_PREVIEW_CODE,
|
|
726
|
+
`SDK: server returned malformed SeatChangePreview \u2014 willApplyAtPeriodEnd must be boolean when present (got ${typeof p.willApplyAtPeriodEnd})`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
if (p.effectiveDate !== void 0 && p.effectiveDate !== null && typeof p.effectiveDate !== "string") {
|
|
730
|
+
throw new FFIDSDKError(
|
|
731
|
+
MALFORMED_SEAT_PREVIEW_CODE,
|
|
732
|
+
`SDK: server returned malformed SeatChangePreview \u2014 effectiveDate must be string or null when present (got ${typeof p.effectiveDate})`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
if (p.willApplyAtPeriodEnd === true && p.proratedAmount !== 0) {
|
|
736
|
+
throw new FFIDSDKError(
|
|
737
|
+
MALFORMED_SEAT_PREVIEW_CODE,
|
|
738
|
+
`SDK: server returned malformed SeatChangePreview \u2014 willApplyAtPeriodEnd=true requires proratedAmount=0 (got ${JSON.stringify(p.proratedAmount)})`
|
|
739
|
+
);
|
|
740
|
+
}
|
|
718
741
|
if (!Array.isArray(p.lineItems)) {
|
|
719
742
|
throw new FFIDSDKError(
|
|
720
743
|
MALFORMED_SEAT_PREVIEW_CODE,
|
|
@@ -1355,7 +1378,7 @@ var FFID_ORGANIZATION_PROFILE_ERROR_CODES = {
|
|
|
1355
1378
|
};
|
|
1356
1379
|
|
|
1357
1380
|
// src/client/version-check.ts
|
|
1358
|
-
var SDK_VERSION = "
|
|
1381
|
+
var SDK_VERSION = "8.0.0";
|
|
1359
1382
|
var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
|
|
1360
1383
|
var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
|
|
1361
1384
|
function sdkHeaders() {
|
|
@@ -2609,6 +2632,129 @@ function cleanupStateStorage(logger) {
|
|
|
2609
2632
|
}
|
|
2610
2633
|
}
|
|
2611
2634
|
|
|
2635
|
+
// src/client/agency-invite-token.ts
|
|
2636
|
+
var AGENCY_INVITE_TOKEN_PARAM = "invite";
|
|
2637
|
+
var AGENCY_INVITE_COOKIE_NAME = "ffid_invite";
|
|
2638
|
+
var SECONDS_PER_MINUTE = 60;
|
|
2639
|
+
var MINUTES_PER_HOUR = 60;
|
|
2640
|
+
var HOURS_PER_DAY = 24;
|
|
2641
|
+
var RETENTION_DAYS = 30;
|
|
2642
|
+
var AGENCY_INVITE_COOKIE_MAX_AGE_SEC = SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY * RETENTION_DAYS;
|
|
2643
|
+
var AGENCY_INVITE_TOKEN_REDACTION = "[REDACTED]";
|
|
2644
|
+
function hasDocument() {
|
|
2645
|
+
return typeof document !== "undefined";
|
|
2646
|
+
}
|
|
2647
|
+
function defaultSecure() {
|
|
2648
|
+
if (typeof process === "undefined") return true;
|
|
2649
|
+
return process.env?.NODE_ENV === "production";
|
|
2650
|
+
}
|
|
2651
|
+
function toQueryString(source) {
|
|
2652
|
+
const queryStart = source.indexOf("?");
|
|
2653
|
+
if (queryStart < 0) return "";
|
|
2654
|
+
const afterQuery = source.slice(queryStart);
|
|
2655
|
+
const fragmentStart = afterQuery.indexOf("#");
|
|
2656
|
+
return fragmentStart < 0 ? afterQuery : afterQuery.slice(0, fragmentStart);
|
|
2657
|
+
}
|
|
2658
|
+
function readTokenFromUrl(options) {
|
|
2659
|
+
const source = options.url ?? (typeof window !== "undefined" ? window.location.search : void 0);
|
|
2660
|
+
if (source === void 0) return void 0;
|
|
2661
|
+
const params = new URLSearchParams(toQueryString(source));
|
|
2662
|
+
return params.get(AGENCY_INVITE_TOKEN_PARAM)?.trim() || void 0;
|
|
2663
|
+
}
|
|
2664
|
+
function readAgencyInviteCookie() {
|
|
2665
|
+
if (!hasDocument()) return void 0;
|
|
2666
|
+
const prefix = `${AGENCY_INVITE_COOKIE_NAME}=`;
|
|
2667
|
+
const raw = document.cookie.split(";").map((entry) => entry.trim()).find((entry) => entry.startsWith(prefix));
|
|
2668
|
+
if (!raw) return void 0;
|
|
2669
|
+
try {
|
|
2670
|
+
return decodeURIComponent(raw.slice(prefix.length)).trim() || void 0;
|
|
2671
|
+
} catch {
|
|
2672
|
+
return void 0;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
function readAgencyInviteToken(options = {}) {
|
|
2676
|
+
return readTokenFromUrl(options) ?? readAgencyInviteCookie();
|
|
2677
|
+
}
|
|
2678
|
+
function buildAgencyInviteCookie(value, maxAgeSec, options) {
|
|
2679
|
+
const attrs = [
|
|
2680
|
+
`${AGENCY_INVITE_COOKIE_NAME}=${value}`,
|
|
2681
|
+
`Max-Age=${maxAgeSec}`,
|
|
2682
|
+
`Path=${options.path ?? "/"}`,
|
|
2683
|
+
"SameSite=Lax"
|
|
2684
|
+
];
|
|
2685
|
+
if (options.domain) attrs.push(`Domain=${options.domain}`);
|
|
2686
|
+
if (options.secure ?? defaultSecure()) attrs.push("Secure");
|
|
2687
|
+
return attrs.join("; ");
|
|
2688
|
+
}
|
|
2689
|
+
function writeAgencyInviteCookie(token, options) {
|
|
2690
|
+
if (!hasDocument()) return void 0;
|
|
2691
|
+
document.cookie = buildAgencyInviteCookie(
|
|
2692
|
+
encodeURIComponent(token),
|
|
2693
|
+
AGENCY_INVITE_COOKIE_MAX_AGE_SEC,
|
|
2694
|
+
options
|
|
2695
|
+
);
|
|
2696
|
+
return readAgencyInviteCookie() === token;
|
|
2697
|
+
}
|
|
2698
|
+
function clearAgencyInviteToken(options = {}) {
|
|
2699
|
+
if (!hasDocument()) return void 0;
|
|
2700
|
+
const CLEAR_MAX_AGE = 0;
|
|
2701
|
+
document.cookie = buildAgencyInviteCookie("", CLEAR_MAX_AGE, options);
|
|
2702
|
+
return readAgencyInviteCookie() === void 0;
|
|
2703
|
+
}
|
|
2704
|
+
function captureAgencyInviteToken(options = {}) {
|
|
2705
|
+
const fromUrl = readTokenFromUrl(options);
|
|
2706
|
+
if (fromUrl !== void 0) {
|
|
2707
|
+
return { source: "url", token: fromUrl, persisted: writeAgencyInviteCookie(fromUrl, options) };
|
|
2708
|
+
}
|
|
2709
|
+
const fromCookie = readAgencyInviteCookie();
|
|
2710
|
+
return fromCookie !== void 0 ? { source: "cookie", token: fromCookie, persisted: void 0 } : { source: "none", token: void 0, persisted: void 0 };
|
|
2711
|
+
}
|
|
2712
|
+
var REDACT_BOUNDARIES = [
|
|
2713
|
+
"%2526",
|
|
2714
|
+
"%2523",
|
|
2715
|
+
"%253B",
|
|
2716
|
+
"%26",
|
|
2717
|
+
"%23",
|
|
2718
|
+
"%3B",
|
|
2719
|
+
"&",
|
|
2720
|
+
"#",
|
|
2721
|
+
";"
|
|
2722
|
+
];
|
|
2723
|
+
var REDACT_LEADS = ["%253F", "%3F", "?", ...REDACT_BOUNDARIES];
|
|
2724
|
+
var REDACT_SEPARATORS = ["%253D", "%3D", "="];
|
|
2725
|
+
function escapeForRegExp(literal) {
|
|
2726
|
+
return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2727
|
+
}
|
|
2728
|
+
var REDACT_PATTERN = new RegExp(
|
|
2729
|
+
[
|
|
2730
|
+
// 先頭区切り。`\b` が使えないのは、入れ子形 (`%3Finvite%3D`) では `F` と `i` が
|
|
2731
|
+
// どちらも単語文字なので単語境界が存在しないため。
|
|
2732
|
+
`(^|${REDACT_LEADS.map(escapeForRegExp).join("|")})`,
|
|
2733
|
+
// パラメータ名と区切りはキャプチャして再出力する。`i` フラグがあるので、
|
|
2734
|
+
// 入力の `INVITE=` / `%3d` などの元の表記を保つ必要がある。
|
|
2735
|
+
`(${AGENCY_INVITE_TOKEN_PARAM})`,
|
|
2736
|
+
`(${REDACT_SEPARATORS.map(escapeForRegExp).join("|")})`,
|
|
2737
|
+
// 値。境界の直前まで貪欲に食う。`\s` は終端にしない — 生の空白や改行を含む
|
|
2738
|
+
// トークンでマスクが途中で切れ、後半が漏れる。
|
|
2739
|
+
`(?:(?!${REDACT_BOUNDARIES.map(escapeForRegExp).join("|")})[^&#;])*`
|
|
2740
|
+
].join(""),
|
|
2741
|
+
"gi"
|
|
2742
|
+
);
|
|
2743
|
+
function redactAgencyInviteToken(url) {
|
|
2744
|
+
return url.replace(
|
|
2745
|
+
REDACT_PATTERN,
|
|
2746
|
+
// 文字列 replacement(`'$1$2$3[REDACTED]'`)にしないのは、
|
|
2747
|
+
// `AGENCY_INVITE_TOKEN_REDACTION` に `$` が入った瞬間に置換パターンとして
|
|
2748
|
+
// 解釈される罠を作らないため。
|
|
2749
|
+
(_match, lead, name, separator) => `${lead}${name}${separator}${AGENCY_INVITE_TOKEN_REDACTION}`
|
|
2750
|
+
);
|
|
2751
|
+
}
|
|
2752
|
+
function appendAgencyInviteToken(signupUrl, token) {
|
|
2753
|
+
if (token === void 0) return signupUrl;
|
|
2754
|
+
const separator = signupUrl.includes("?") ? "&" : "?";
|
|
2755
|
+
return `${signupUrl}${separator}${AGENCY_INVITE_TOKEN_PARAM}=${encodeURIComponent(token)}`;
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2612
2758
|
// src/client/redirect.ts
|
|
2613
2759
|
var OAUTH_AUTHORIZE_ENDPOINT = "/api/v1/oauth/authorize";
|
|
2614
2760
|
var AUTH_LOGOUT_ENDPOINT = "/api/v1/auth/logout";
|
|
@@ -2621,6 +2767,7 @@ var REDIRECT_LOOP_WINDOW_MS = 6e4;
|
|
|
2621
2767
|
var REDIRECT_LOOP_THRESHOLD = 3;
|
|
2622
2768
|
var storageReadFailureLogged = false;
|
|
2623
2769
|
var storageWriteFailureLogged = false;
|
|
2770
|
+
var ssrInviteTokenUnavailableLogged = false;
|
|
2624
2771
|
function isLoopDetectionDegraded() {
|
|
2625
2772
|
return storageReadFailureLogged || storageWriteFailureLogged;
|
|
2626
2773
|
}
|
|
@@ -2799,12 +2946,16 @@ function createRedirectMethods(deps) {
|
|
|
2799
2946
|
}
|
|
2800
2947
|
if (options?.screenHint === SCREEN_HINT_SIGNUP) {
|
|
2801
2948
|
params.set(SCREEN_HINT_PARAM, SCREEN_HINT_SIGNUP);
|
|
2949
|
+
const inviteToken = readAgencyInviteToken();
|
|
2950
|
+
if (inviteToken !== void 0) {
|
|
2951
|
+
params.set(AGENCY_INVITE_TOKEN_PARAM, inviteToken);
|
|
2952
|
+
}
|
|
2802
2953
|
}
|
|
2803
2954
|
if (options?.prompt) {
|
|
2804
2955
|
params.set("prompt", options.prompt);
|
|
2805
2956
|
}
|
|
2806
2957
|
const authorizeUrl = `${baseUrl}${OAUTH_AUTHORIZE_ENDPOINT}?${params.toString()}`;
|
|
2807
|
-
logger.debug("Redirecting to authorize:", authorizeUrl);
|
|
2958
|
+
logger.debug("Redirecting to authorize:", redactAgencyInviteToken(authorizeUrl));
|
|
2808
2959
|
recordRedirectAttempt(authorizeKey, now, logger);
|
|
2809
2960
|
try {
|
|
2810
2961
|
window.location.href = authorizeUrl;
|
|
@@ -2823,7 +2974,7 @@ function createRedirectMethods(deps) {
|
|
|
2823
2974
|
return redirectToAuthorize(options);
|
|
2824
2975
|
}
|
|
2825
2976
|
const targetUrl = options?.screenHint === SCREEN_HINT_SIGNUP ? getSignupUrl() : getLoginUrl(void 0, options?.prompt ? { prompt: options.prompt } : void 0);
|
|
2826
|
-
logger.debug("Redirecting to auth page:", targetUrl);
|
|
2977
|
+
logger.debug("Redirecting to auth page:", redactAgencyInviteToken(targetUrl));
|
|
2827
2978
|
window.location.href = targetUrl;
|
|
2828
2979
|
return { success: true };
|
|
2829
2980
|
}
|
|
@@ -2840,7 +2991,7 @@ function createRedirectMethods(deps) {
|
|
|
2840
2991
|
const promptSuffix = options?.prompt ? `&prompt=${encodeURIComponent(options.prompt)}` : "";
|
|
2841
2992
|
return `${baseUrl}/login?redirect=${encodeURIComponent(redirect)}&service=${encodeURIComponent(serviceCode)}${promptSuffix}`;
|
|
2842
2993
|
}
|
|
2843
|
-
function getSignupUrl(redirectUrl) {
|
|
2994
|
+
function getSignupUrl(redirectUrl, options) {
|
|
2844
2995
|
let redirect;
|
|
2845
2996
|
if (redirectUrl != null) {
|
|
2846
2997
|
redirect = redirectUrl;
|
|
@@ -2850,7 +3001,22 @@ function createRedirectMethods(deps) {
|
|
|
2850
3001
|
logger.warn("getSignupUrl: SSR \u74B0\u5883\u3067 redirectUrl \u304C\u672A\u6307\u5B9A\u306E\u305F\u3081\u7A7A\u6587\u5B57\u306B\u30D5\u30A9\u30FC\u30EB\u30D0\u30C3\u30AF\u3057\u307E\u3059");
|
|
2851
3002
|
redirect = "";
|
|
2852
3003
|
}
|
|
2853
|
-
|
|
3004
|
+
let inviteToken = options?.inviteToken;
|
|
3005
|
+
if (inviteToken === void 0) {
|
|
3006
|
+
inviteToken = readAgencyInviteToken();
|
|
3007
|
+
if (inviteToken === void 0 && typeof window === "undefined") {
|
|
3008
|
+
if (!ssrInviteTokenUnavailableLogged) {
|
|
3009
|
+
ssrInviteTokenUnavailableLogged = true;
|
|
3010
|
+
logger.warn(
|
|
3011
|
+
"[FFID SDK] getSignupUrl: SSR \u3067\u306F\u62DB\u5F85\u30C8\u30FC\u30AF\u30F3\uFF08invite\uFF09\u3092\u8AAD\u3081\u307E\u305B\u3093\u3002\u4EE3\u7406\u5E97\u306E\u62DB\u5F85\u30EA\u30F3\u30AF\u3092\u4F7F\u3046\u5834\u5408\u306F options.inviteToken \u306B\u6E21\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4F7F\u3063\u3066\u3044\u306A\u3044\u5834\u5408\u306F\u3053\u306E\u8B66\u544A\u3092\u7121\u8996\u3057\u3066\u3088\u3044\uFF09"
|
|
3012
|
+
);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
return appendAgencyInviteToken(
|
|
3017
|
+
`${baseUrl}/signup?redirect=${encodeURIComponent(redirect)}&service=${encodeURIComponent(serviceCode)}`,
|
|
3018
|
+
inviteToken
|
|
3019
|
+
);
|
|
2854
3020
|
}
|
|
2855
3021
|
function getLogoutUrl(postLogoutRedirectUri) {
|
|
2856
3022
|
const url = new URL(`${baseUrl}${AUTH_LOGOUT_ENDPOINT}`);
|
|
@@ -2866,7 +3032,7 @@ function createRedirectMethods(deps) {
|
|
|
2866
3032
|
return { success: false, error: "SSR \u74B0\u5883\u3067\u306F\u30EA\u30C0\u30A4\u30EC\u30AF\u30C8\u3067\u304D\u307E\u305B\u3093" };
|
|
2867
3033
|
}
|
|
2868
3034
|
const logoutUrl = getLogoutUrl(postLogoutRedirectUri);
|
|
2869
|
-
logger.debug("Redirecting to logout:", logoutUrl);
|
|
3035
|
+
logger.debug("Redirecting to logout:", redactAgencyInviteToken(logoutUrl));
|
|
2870
3036
|
window.location.href = logoutUrl;
|
|
2871
3037
|
return { success: true };
|
|
2872
3038
|
}
|
|
@@ -3479,11 +3645,11 @@ var EFFECTIVE_SUBSCRIPTION_STATUSES = [
|
|
|
3479
3645
|
"expired"
|
|
3480
3646
|
];
|
|
3481
3647
|
var PAST_DUE_GRACE_PERIOD_DAYS = 7;
|
|
3482
|
-
var
|
|
3483
|
-
var
|
|
3484
|
-
var
|
|
3648
|
+
var HOURS_PER_DAY2 = 24;
|
|
3649
|
+
var MINUTES_PER_HOUR2 = 60;
|
|
3650
|
+
var SECONDS_PER_MINUTE2 = 60;
|
|
3485
3651
|
var MS_PER_SECOND2 = 1e3;
|
|
3486
|
-
var MS_PER_DAY =
|
|
3652
|
+
var MS_PER_DAY = HOURS_PER_DAY2 * MINUTES_PER_HOUR2 * SECONDS_PER_MINUTE2 * MS_PER_SECOND2;
|
|
3487
3653
|
|
|
3488
3654
|
// src/subscriptions/compute-effective-status.ts
|
|
3489
3655
|
function isExpired(isoTimestamp, nowMs) {
|
|
@@ -3636,6 +3802,18 @@ var FFID_ERROR_CODES = {
|
|
|
3636
3802
|
TOKEN_EXPIRED: "TOKEN_EXPIRED",
|
|
3637
3803
|
STATE_MISMATCH_ERROR: "STATE_MISMATCH_ERROR"
|
|
3638
3804
|
};
|
|
3805
|
+
var FFID_SUBSCRIPTION_ERROR_CODES = {
|
|
3806
|
+
CANCELLATION_SCHEDULED: "CANCELLATION_SCHEDULED",
|
|
3807
|
+
PENDING_CHANGE_EXISTS: "PENDING_CHANGE_EXISTS",
|
|
3808
|
+
CHANGE_DIRECTION_MIXED: "CHANGE_DIRECTION_MIXED",
|
|
3809
|
+
CHANGE_PAYMENT_FAILED: "CHANGE_PAYMENT_FAILED",
|
|
3810
|
+
SAME_PLAN_SELECTED: "SAME_PLAN_SELECTED",
|
|
3811
|
+
NO_PENDING_DOWNGRADE: "NO_PENDING_DOWNGRADE",
|
|
3812
|
+
SUBSCRIPTION_NOT_CANCELABLE: "SUBSCRIPTION_NOT_CANCELABLE",
|
|
3813
|
+
SUBSCRIPTION_ALREADY_CANCELED: "SUBSCRIPTION_ALREADY_CANCELED",
|
|
3814
|
+
FREE_DOWNGRADE_NOT_ALLOWED: "FREE_DOWNGRADE_NOT_ALLOWED",
|
|
3815
|
+
PAYMENT_METHOD_REQUIRED: "PAYMENT_METHOD_REQUIRED"
|
|
3816
|
+
};
|
|
3639
3817
|
|
|
3640
3818
|
// src/client/ffid-client.ts
|
|
3641
3819
|
var UNAUTHORIZED_STATUS2 = 401;
|
|
@@ -4319,6 +4497,15 @@ function FFIDProvider({
|
|
|
4319
4497
|
}),
|
|
4320
4498
|
[serviceCode, scope, apiBaseUrl, debug, logger, authMode, clientId, timeout, tokenStore]
|
|
4321
4499
|
);
|
|
4500
|
+
react.useEffect(() => {
|
|
4501
|
+
const captured = captureAgencyInviteToken();
|
|
4502
|
+
if (captured.persisted === false) {
|
|
4503
|
+
client.logger.debug(
|
|
4504
|
+
"[FFID SDK] \u62DB\u5F85\u30C8\u30FC\u30AF\u30F3\u306E cookie \u4FDD\u6301\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF08\u5225\u30DA\u30FC\u30B8\u3078\u56DE\u904A\u3059\u308B\u3068\u4EE3\u7406\u5E97\u306E\u7D10\u4ED8\u3051\u304C\u5931\u308F\u308C\u307E\u3059\uFF09",
|
|
4505
|
+
{ source: captured.source }
|
|
4506
|
+
);
|
|
4507
|
+
}
|
|
4508
|
+
}, [client]);
|
|
4322
4509
|
const refresh = react.useCallback(async () => {
|
|
4323
4510
|
client.logger.debug("Refreshing session...");
|
|
4324
4511
|
try {
|
|
@@ -6295,6 +6482,10 @@ function FFIDInquiryForm({
|
|
|
6295
6482
|
}
|
|
6296
6483
|
|
|
6297
6484
|
exports.ACCESS_GRANTING_EFFECTIVE_STATUSES = ACCESS_GRANTING_EFFECTIVE_STATUSES;
|
|
6485
|
+
exports.AGENCY_INVITE_COOKIE_MAX_AGE_SEC = AGENCY_INVITE_COOKIE_MAX_AGE_SEC;
|
|
6486
|
+
exports.AGENCY_INVITE_COOKIE_NAME = AGENCY_INVITE_COOKIE_NAME;
|
|
6487
|
+
exports.AGENCY_INVITE_TOKEN_PARAM = AGENCY_INVITE_TOKEN_PARAM;
|
|
6488
|
+
exports.AGENCY_INVITE_TOKEN_REDACTION = AGENCY_INVITE_TOKEN_REDACTION;
|
|
6298
6489
|
exports.BLOCKING_EFFECTIVE_STATUSES = BLOCKING_EFFECTIVE_STATUSES;
|
|
6299
6490
|
exports.CATALOG_HASH_PATTERN = CATALOG_HASH_PATTERN;
|
|
6300
6491
|
exports.DEFAULT_API_BASE_URL = DEFAULT_API_BASE_URL;
|
|
@@ -6323,8 +6514,11 @@ exports.FFID_INQUIRY_CATEGORIES_SITE_2026 = FFID_INQUIRY_CATEGORIES_SITE_2026;
|
|
|
6323
6514
|
exports.FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_BYTES = FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_BYTES;
|
|
6324
6515
|
exports.FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_MB = FFID_MAX_ORGANIZATION_LOGO_FILE_SIZE_MB;
|
|
6325
6516
|
exports.FFID_ORGANIZATION_PROFILE_ERROR_CODES = FFID_ORGANIZATION_PROFILE_ERROR_CODES;
|
|
6517
|
+
exports.FFID_SUBSCRIPTION_ERROR_CODES = FFID_SUBSCRIPTION_ERROR_CODES;
|
|
6326
6518
|
exports.canonicalizeCatalogValue = canonicalizeCatalogValue;
|
|
6519
|
+
exports.captureAgencyInviteToken = captureAgencyInviteToken;
|
|
6327
6520
|
exports.cleanupStateStorage = cleanupStateStorage;
|
|
6521
|
+
exports.clearAgencyInviteToken = clearAgencyInviteToken;
|
|
6328
6522
|
exports.computeEffectiveStatusFromSession = computeEffectiveStatusFromSession;
|
|
6329
6523
|
exports.computeFFIDCheckoutCopyHash = computeFFIDCheckoutCopyHash;
|
|
6330
6524
|
exports.computeLegalDisclosureHash = computeLegalDisclosureHash;
|
|
@@ -6344,6 +6538,9 @@ exports.isBlockedFromUserinfo = isBlockedFromUserinfo;
|
|
|
6344
6538
|
exports.isFFIDInquiryAcquisitionSource = isFFIDInquiryAcquisitionSource;
|
|
6345
6539
|
exports.isFFIDInquiryCategorySite2026 = isFFIDInquiryCategorySite2026;
|
|
6346
6540
|
exports.normalizeRedirectUri = normalizeRedirectUri;
|
|
6541
|
+
exports.readAgencyInviteCookie = readAgencyInviteCookie;
|
|
6542
|
+
exports.readAgencyInviteToken = readAgencyInviteToken;
|
|
6543
|
+
exports.redactAgencyInviteToken = redactAgencyInviteToken;
|
|
6347
6544
|
exports.retrieveCodeVerifier = retrieveCodeVerifier;
|
|
6348
6545
|
exports.retrieveState = retrieveState;
|
|
6349
6546
|
exports.sha256Hex = sha256Hex;
|