@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 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: '山田 太郎', companyName: 'Example Inc.' },
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 側で該当カラムを SQL NULL にクリアします(v2.16.0〜 / #2354)。
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` / `companyName` / `department` / `jobTitle` / `preferences`。`timezone` / `locale` は application-level invariant(サーバー側 normalization が string 前提)のため null 非許容。クリアは不可 — キー未指定で現状維持、もしくは新しい有効な値を渡す。
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: raw.company_name ?? null,
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 JWT_ISSUER = "https://id.feelflow.net";
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: JWT_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: introspectResponse.company_name ?? null,
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 = "7.1.0";
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
- return `${baseUrl}/signup?redirect=${encodeURIComponent(redirect)}&service=${encodeURIComponent(serviceCode)}`;
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 HOURS_PER_DAY = 24;
3483
- var MINUTES_PER_HOUR = 60;
3484
- var SECONDS_PER_MINUTE = 60;
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 = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND2;
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;