@feelflow/ffid-sdk
v5.7.0
Published
FeelFlow ID Platform SDK for React/Next.js applications
Maintainers
Readme
@feelflow/ffid-sdk
FeelFlow ID Platform SDK — React/Next.js 向け + サーバーサイドモジュールはフレームワーク非依存。
5行のコードでFFID認証を導入!
インストール
npm install @feelflow/ffid-sdk
# or
yarn add @feelflow/ffid-sdk
# or
pnpm add @feelflow/ffid-sdk統合ガイド
サービスを FFID Platform に統合する詳細なガイドは Integration Guide を参照してください。OAuth フロー、フロントエンド/バックエンド実装パターン、セキュリティチェックリスト、アンチパターン集を網羅しています。
Cookie 同意管理基盤 (v5.0.0+)
GDPR / ePrivacy / 改正電気通信事業法 / APPI 準拠の Cookie 同意 UI を 15 分以内 に導入できます (opt-in、v4.x 完全互換)。
- Cookie Consent Integration Guide — 17 章の詳細統合手順 (App Router / Pages Router / Vite / Consent Mode v2 / Sentry replay / RSC / i18n / A11y / エラーハンドリング)
- v4 → v5 Migration Guide — minimal / full アップグレード手順、自前 gtag からの移譲、Rollback
- Troubleshooting Q&A — Banner / GA / Cookie / TypeScript / API / Sentry のよくある詰まりポイント
- 動作サンプル: App Router / Pages Router / Vite + React
// app/layout.tsx (App Router、最短 3 ステップ)
import {
FFIDProvider,
FFIDAnalyticsProvider,
FFIDCookieBanner,
DEFAULT_OAUTH_SCOPES,
} from '@feelflow/ffid-sdk'
export default function RootLayout({ children }) {
return (
<FFIDProvider serviceCode="your-service" scope={DEFAULT_OAUTH_SCOPES}>
<FFIDAnalyticsProvider
baseUrl={process.env.NEXT_PUBLIC_FFID_BASE_URL!}
serviceApiKey={process.env.FFID_SERVICE_API_KEY!}
gaMeasurementId={process.env.NEXT_PUBLIC_GA_ID ?? null}
>
{children}
<FFIDCookieBanner />
</FFIDAnalyticsProvider>
</FFIDProvider>
)
}クイックスタート
1. プロバイダーを設定(5行で完了!)
// app/layout.tsx
import { FFIDProvider, DEFAULT_OAUTH_SCOPES } from '@feelflow/ffid-sdk'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja">
<body>
<FFIDProvider serviceCode="chatbot" scope={DEFAULT_OAUTH_SCOPES}>{children}</FFIDProvider>
</body>
</html>
)
}2. 認証情報を使用
import { useFFID, useSubscription } from '@feelflow/ffid-sdk'
function Dashboard() {
const { user, isAuthenticated, login, logout } = useFFID()
const { isActive, planCode } = useSubscription()
if (!isAuthenticated) {
return <button onClick={login}>ログイン</button>
}
return (
<div>
<p>Welcome, {user.displayName ?? user.email}!</p>
<p>プラン: {planCode}</p>
<button onClick={logout}>ログアウト</button>
</div>
)
}UIコンポーネント
import {
FFIDLoginButton,
FFIDUserMenu,
FFIDOrganizationSwitcher,
FFIDSubscriptionBadge,
} from '@feelflow/ffid-sdk/components'
function Header() {
return (
<header>
<FFIDLoginButton>ログイン</FFIDLoginButton>
<FFIDOrganizationSwitcher />
<FFIDSubscriptionBadge />
<FFIDUserMenu />
</header>
)
}コンポーネントのスタイリング(classNames パターン)
各UIコンポーネントは Radix UI スタイルパターン に従った classNames プロップをサポートしています。これにより、コンポーネントの各パーツに個別のクラスを適用できます。
FFIDUserMenu
<FFIDUserMenu
classNames={{
container: 'relative', // ラッパー要素
button: 'focus:ring-2', // アバターボタン(トリガー)
avatar: 'rounded-full', // アバター画像/フォールバック
menu: 'shadow-lg', // ドロップダウンメニュー
userInfo: 'border-b', // ユーザー情報セクション
menuItem: 'hover:bg-gray-100', // カスタムメニュー項目
logout: 'text-red-600', // ログアウトボタン
}}
/>FFIDOrganizationSwitcher
<FFIDOrganizationSwitcher
classNames={{
container: 'relative', // ラッパー要素
button: 'border rounded', // トリガーボタン
dropdown: 'shadow-md', // ドロップダウンメニュー
option: 'px-4 py-2', // 各組織オプション
optionSelected: 'bg-blue-50', // 選択中の組織(optionに加えて適用)
}}
/>FFIDSubscriptionBadge
<FFIDSubscriptionBadge
classNames={{
badge: 'font-semibold', // バッジspan要素
}}
/>Note:
FFIDLoginButtonはシンプルな単一要素コンポーネントのため、標準のclassNameプロップのみをサポートしています。
API リファレンス
FFIDProvider
アプリケーション全体をラップするプロバイダーコンポーネント。
<FFIDProvider
serviceCode="chatbot" // 必須: サービスコード
scope={DEFAULT_OAUTH_SCOPES} // 必須: OAuth scope (v3.0.0+)
apiBaseUrl="..." // オプション: カスタムAPIエンドポイント
debug={true} // オプション: デバッグログ有効化(非推奨、loggerを使用)
logger={customLogger} // オプション: カスタムロガー(下記参照)
refreshInterval={300000} // オプション: セッション更新間隔(ms)
onAuthStateChange={(user) => {}} // オプション: 認証状態変更時コールバック
onError={(error) => {}} // オプション: エラー時コールバック
>
{children}
</FFIDProvider>カスタムロガー
SDKのデバッグ出力をカスタマイズできます。デフォルトではログは出力されません(サイレント)。
import type { FFIDLogger } from '@feelflow/ffid-sdk'
import pino from 'pino' // または winston, bunyan 等
// アプリケーションのロガーインスタンス
const appLogger = pino({ level: 'debug' })
// FFID SDK用にラップ
const ffidLogger: FFIDLogger = {
debug: (...args) => appLogger.debug({ sdk: 'ffid' }, ...args),
info: (...args) => appLogger.info({ sdk: 'ffid' }, ...args),
warn: (...args) => appLogger.warn({ sdk: 'ffid' }, ...args),
error: (...args) => appLogger.error({ sdk: 'ffid' }, ...args),
}
// 使用例
<FFIDProvider serviceCode="chatbot" scope={DEFAULT_OAUTH_SCOPES} logger={ffidLogger}>
{children}
</FFIDProvider>ロガー優先順位:
loggerが指定されている場合 → カスタムロガーを使用debug: trueでloggerなし →consoleを使用(後方互換性)- 両方なし → サイレント(no-op)
useFFID()
ユーザー・組織情報を取得するフック。
const {
user, // FFIDUser | null - 現在のユーザー
organizations, // FFIDOrganization[] - 所属組織一覧
currentOrganization, // FFIDOrganization | null - 現在の組織
isLoading, // boolean - ロード中
isAuthenticated, // boolean - 認証済み
login, // () => void - ログインページへリダイレクト
logout, // () => Promise<void> - ログアウト
switchOrganization, // (id: string) => void - 組織切り替え
refresh, // () => Promise<void> - セッション更新
} = useFFID()useSubscription()
契約情報を取得するフック。
const {
subscription, // FFIDSubscription | null - 現在のサブスクリプション
planCode, // string | null - プランコード
isActive, // boolean - DB ステータスが 'active'
isTrialing, // boolean - トライアル中
isCanceled, // boolean - 解約済み
isTrialExpired, // boolean - トライアル期間超過
effectiveStatus, // EffectiveSubscriptionStatus | null - 意味論的アクセス制御値
isBlocked, // boolean - blocked / expired / canceled / trial_expired
isGrace, // boolean - past_due_grace (支払い失敗の猶予期間中)
hasPlan, // (plans: string | string[]) => boolean - プラン確認
hasAccess, // () => boolean - アクセス権確認 (active || past_due_grace)
hasAccessLegacy, // () => boolean - 旧セマンティクス (active || trialing, pre-2.19)
} = useSubscription()effectiveStatus は /api/v1/subscriptions/ext/check が返す意味論的ステータスと同じ値を取る。詳細は 契約期限切れハンドリング を参照。
useRequireActiveSubscription()
契約が期限切れ/遮断状態のときに自動でリダイレクトするフック。
'use client'
import { useRouter } from 'next/navigation'
import { useRequireActiveSubscription } from '@feelflow/ffid-sdk'
function ProtectedShell({ children }: { children: React.ReactNode }) {
const router = useRouter()
const { loading } = useRequireActiveSubscription({
redirectTo: (status) =>
status === 'canceled' ? '/contract-ended' : '/contract-required',
onRedirect: (url) => router.replace(url),
})
if (loading) return <FullPageSpinner />
return <>{children}</>
}オプション:
redirectTo:stringまたは(status: EffectiveSubscriptionStatus) => stringallowGrace(default:true):past_due_graceを通過させるかonRedirect(optional): 独自のリダイレクト関数(未指定時はwindow.location.href)
withSubscription()
サブスクリプション確認HOC。
const PremiumFeature = withSubscription(MyComponent, {
plans: ['pro', 'enterprise'],
fallback: <UpgradePrompt />,
loading: <Spinner />,
})契約期限切れハンドリング
契約が失効したり解約されたとき、外部サービス側で適切にアクセスを遮断し、ユーザーを再契約動線に案内する必要があります。SDK は 3 層構成(トークン検証 / 契約チェック / Webhook 受信)をサポートしています。
最小構成(Next.js App Router)
'use client'
import { useRouter } from 'next/navigation'
import { useRequireActiveSubscription } from '@feelflow/ffid-sdk'
export default function Layout({ children }: { children: React.ReactNode }) {
const router = useRouter()
const { loading, effectiveStatus } = useRequireActiveSubscription({
redirectTo: '/contract-required',
onRedirect: (url) => router.replace(url),
})
if (loading) return <Spinner />
if (effectiveStatus !== 'active' && effectiveStatus !== 'past_due_grace') {
return null // リダイレクト発火中
}
return <>{children}</>
}詳細な実装レシピ(middleware、Express、UI 分岐、Webhook 受信、fixture API を使ったテスト、EffectiveSubscriptionStatus 別の UI 推奨動作まで) → docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md
Server-side service access decision
API route / middleware では checkServiceAccess() を使う。FFID の /api/v1/subscriptions/ext/check が返す canonical decision をそのまま受け取り、外部サービス側で past_due_since + 7d、current_period_end + 7d、payment_failed_at + 7d のような lifecycle date math を再実装しない。
import { createFFIDClient } from '@feelflow/ffid-sdk'
const ffid = createFFIDClient({
serviceCode: 'flow-board-ai',
scope: '',
authMode: 'service-key',
serviceApiKey: process.env.FFID_SERVICE_API_KEY!,
})
const { data: access, error } = await ffid.checkServiceAccess({
userId,
organizationId,
allowGrace: true,
})
if (error || !access?.hasAccess) {
return Response.redirect('/contract-required')
}checkServiceAccess()はdata.hasAccessが唯一の gate。result.errorは入力 validation など SDK が decision を作れない場合にだけ返る。hasAccess: FFID が決めた canonical access decision。allowGrace=falseのときだけ SDK 側でpast_due_graceを deny に変換する。effectiveStatus:active/past_due_grace/blocked/canceled/trial_expired/expired。gracePeriodEndsAt:past_due_graceがblockedに変わる時刻。表示・警告用であり、アクセス判定の source of truth にしない。failPolicy: 現在はfailClosed固定。FFID に到達できない、または canonical decision を取得できない場合はresult.errorではなくdata.hasAccess=false/denialReason='ffid_unreachable'/data.errorを返す。呼び出し側はresult.errorだけで通過判定しない。
Organization member management
organization:read / organization:write scope を持つ service-key または token で、組織メンバーの参照・追加・ロール変更・削除ができます。
import { createFFIDClient } from '@feelflow/ffid-sdk/server'
const ffid = createFFIDClient({
serviceCode: 'flow-board-ai',
scope: '',
authMode: 'service-key',
serviceApiKey: process.env.FFID_SERVICE_API_KEY!,
})
const addResult = await ffid.addMember({
organizationId,
email: '[email protected]',
role: 'member',
})
if (addResult.error) {
throw new Error(addResult.error.message)
}
await ffid.updateMemberRole({
organizationId,
userId: addResult.data.member.userId,
role: 'admin',
})addMemberは既存 FFID ユーザーを active member として追加します。招待メール送信や token 発行は行いません。roleはadmin/member/viewerのみ指定可能です。ownerは追加・昇格 API では扱いません。- agency client と同じ呼び味が必要な場合は
ffid.addMember(organizationId, { email, role })も使えます。
getProfile() / updateProfile()
ログイン中ユーザー自身のプロフィールを取得・更新するメソッド(createFFIDClient から呼び出し)。
- エンドポイント:
GET /api/v1/users/ext/me/PUT /api/v1/users/ext/me - 対応 authMode:
token(Bearer)のみが SDK 経由で成立する- cookie モードは非対応 — ext エンドポイントはクロスオリジン用途のため、Bearer token または
X-Service-Api-Keyのどちらかが必須。FFID 自身の UI(同一オリジン)は従来通り/api/v1/users/meを使う - service-key モードも SDK 経由では非対応 — API Key 認証時はバックエンドが
?userId=<uuid>クエリを要求するが、getProfile()/updateProfile()は自分自身(ambient user)を前提とするため、userIdを受け取らない。サーバー間で任意ユーザーのプロフィールを操作したい場合はfetchWithAuthで直接エンドポイントを叩いてください
- cookie モードは非対応 — ext エンドポイントはクロスオリジン用途のため、Bearer token または
- 外部サービス(hub 等)のフロントエンドから token モードで呼ぶのが想定される主要パターン
import { createFFIDClient } from '@feelflow/ffid-sdk'
const client = createFFIDClient({
serviceCode: 'hub',
authMode: 'token',
apiBaseUrl: 'https://id.feelflow.net',
})
// 取得
const { data: profile, error } = await client.getProfile()
if (error) {
console.error('プロフィール取得失敗:', error.message)
} else {
console.log(profile.email, profile.displayName, profile.timezone)
}
// 更新(部分更新 — 渡したフィールドだけ差し替え)
const { data: updated, error: updateError } = await client.updateProfile({
displayName: '山田 太郎',
timezone: 'Asia/Tokyo',
locale: 'ja',
preferences: { theme: 'dark' },
})updateProfile に空オブジェクト {} を渡すと VALIDATION_ERROR が返ります(無意味なラウンドトリップを防止)。
フィールドのクリア(null を渡す)
optional フィールドに null を渡すと、FFID backend 側で該当カラムを SQL NULL にクリアします(v2.16.0〜 / #2354)。
// 会社名と部署をクリア
await client.updateProfile({
companyName: null,
department: null,
})値のセマンティクス:
| 渡す値 | FFID backend の挙動 |
| --- | --- |
| undefined / キー未指定 | 未変更(partial update) |
| null | クリア(SQL NULL を書き込む) |
| ""(空文字列) | 空文字リテラルをそのまま保存(null 扱いにはならない) |
対応フィールド: displayName / phone / companyName / department / jobTitle / preferences。timezone / locale は application-level invariant(サーバー側 normalization が string 前提)のため null 非許容。クリアは不可 — キー未指定で現状維持、もしくは新しい有効な値を渡す。
getAnalyticsConfig() (#2347)
外部サービスが自身に割り当てられた GA4 Measurement ID を取得するメソッド。
- エンドポイント:
GET /api/v1/ext/analytics/config?service=<code> - 必要 scope:
analytics:read(Bearer / API Key 両方で enforce) - レスポンス:
FFIDAnalyticsConfig({ code, measurementId, displayName, isActive }) - archived service (
isActive: false) でも 200 を返す — caller が次回 deploy で tracking 停止判断する間、in-flight events が 5xx を起こさないように measurementId は引き続き返す
import { createFFIDClient } from '@feelflow/ffid-sdk/server'
const client = createFFIDClient({
serviceCode: 'flow-board-ai',
authMode: 'service-key',
serviceApiKey: process.env.FFID_SERVICE_API_KEY!,
})
const result = await client.getAnalyticsConfig('feel-agent-ai')
if (result.error) {
if (result.error.code === 'SERVICE_NOT_FOUND') {
// 該当 GA4 stream がまだ sync されていない / typo
} else {
console.error(`[FFID] analytics config fetch failed: ${result.error.code}`)
}
} else if (result.data.isActive) {
// GA4 タグを描画して events を送信
loadGA4Script(result.data.measurementId)
}エラーコード:
VALIDATION_ERROR—serviceCodeが空 / kebab-case 形式違反(SDK 側 pre-validate)INSUFFICIENT_SCOPE(403) —analytics:readscope なしSERVICE_NOT_FOUND(404) — DB に未登録の service codeINVALID_PARAM(400) — server 側で形式違反を検出(pre-validate 通過後の boundary)
型定義
interface FFIDUser {
id: string
email: string
displayName: string | null
avatarUrl: string | null
locale: string | null
timezone: string | null
createdAt: string
}
interface FFIDOrganization {
id: string
name: string
slug: string
role: 'owner' | 'admin' | 'member'
status: 'active' | 'invited' | 'suspended'
}
interface FFIDSubscription {
id: string
serviceCode: string
serviceName: string
planCode: string
planName: string
status: 'trialing' | 'active' | 'past_due' | 'canceled' | 'paused'
currentPeriodEnd: string | null
}OAuth userinfo の契約要約
token mode では SDK は /api/v1/oauth/userinfo を呼び出し、基本プロフィールに加えてサービス契約の要約を受け取ります。
この要約により、追加 API を呼ばずにプラン判定や UI 分岐を行えます。
interface FFIDOAuthUserInfoSubscription {
subscriptionId: string | null
status: 'trialing' | 'active' | 'past_due' | 'canceled' | 'paused' | null
planCode: string | null
seatModel: 'organization' | null
memberRole: 'owner' | 'admin' | 'member' | 'viewer' | null
organizationId: string | null
}seatModel はシートモデル識別用であり、organization と role は userinfo の解決済み組織文脈として扱います。
React 以外の環境で使う
本 SDK は React/Next.js 向けに設計されていますが、一部のモジュールはフレームワーク非依存で利用できます。
サーバーサイドモジュール(React 依存なし)
以下の subpath exports は React に一切依存しません。Node.js、Deno、Bun 等で即座に利用できます。
// 利用規約・法的文書
import { createFFIDLegalClient } from '@feelflow/ffid-sdk/legal'
// Agency(代理店)管理
import { createFFIDAgencyClient } from '@feelflow/ffid-sdk/agency'
// お知らせ取得
import { createFFIDAnnouncementsClient } from '@feelflow/ffid-sdk/announcements'
// Webhook 署名検証・ハンドラー(※ Node.js crypto が必要)
import { createFFIDWebhookHandler, verifyWebhookSignature } from '@feelflow/ffid-sdk/webhooks'Note:
webhooksモジュールは Node.js のcryptoモジュールとBufferを使用します。Cloudflare Workers で利用する場合はnodejs_compat互換フラグを有効にしてください。
これらのモジュールは独立した subpath export として公開されているため、メインエントリ (@feelflow/ffid-sdk) を経由せず、React の依存が伝播しません。
createFFIDClient を非 React 環境で使う
非 React 環境では、可能な限り上記の subpath exports(/legal、/webhooks 等)の個別クライアントを使用してください。メインエントリの createFFIDClient を使う必要がある場合、createFFIDClient 自体は React を使用しませんが、メインエントリに含まれるため bundler 環境では React を external に指定する必要があります。
Cloudflare Workers
ビルドコマンドで --external を指定するか、カスタム esbuild 設定で external を設定してください。
# wrangler のビルドコマンド例
esbuild src/index.ts --bundle --format=esm --external:react --external:react-domVue / Nuxt(Vite)
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
external: ['react', 'react-dom'],
},
},
})esbuild
esbuild src/index.ts --bundle --external:react --external:react-domwebpack
// webpack.config.js
module.exports = {
externals: {
react: 'react',
'react-dom': 'react-dom',
},
}Note: サーバーサイドで bundler を使わずに実行する場合、subpath exports(
@feelflow/ffid-sdk/legal等)を使用すれば external 設定なしで React がインストールされていなくても動作します。メインエントリ(@feelflow/ffid-sdk)を ESM で import する場合は、モジュールグラフが静的に解決されるため React が必要です。
peerDependencies は optional です
SDK の package.json で react / react-dom は optional: true に設定済みです(利用者側での設定は不要)。React をインストールしなくても npm install 時に warning は発生しません。
// SDK の package.json に設定済み(参考)
{
"peerDependenciesMeta": {
"react": { "optional": true },
"react-dom": { "optional": true }
}
}E2E テストモード(@feelflow/ffid-sdk/server/test)
⚠️ SECURITY NOTICE — テストモードは Bearer トークン検証を 意図的にバイパス する仕組みです。本番環境で誤って有効化すると、登録されたバイパストークンを持つ任意のリクエストが認証通過します。
各サービスで E2E テストを書く際、verifyAccessToken の introspect 呼び出しをモックする実装を独自に持つ必要がなくなります。SDK 側で 多重 production guard 付き の bypass クライアントを提供します。
import { createFFIDClient } from '@feelflow/ffid-sdk/server'
import { createTestFFIDClient } from '@feelflow/ffid-sdk/server/test'
const isE2E =
process.env.NODE_ENV !== 'production' &&
process.env.FFID_TEST_MODE === 'true'
const client = isE2E
? createTestFFIDClient({
users: [
{
bypassToken: process.env.E2E_TEST_BYPASS_SECRET!,
userInfo: {
sub: 'e2e-test-sub',
email: '[email protected]',
name: 'E2E Test User',
picture: null,
},
},
],
})
: createFFIDClient({ /* normal production options */ })
const result = await client.verifyAccessToken(bearerToken)Built-in production guards (defense-in-depth)
NODE_ENVを trim + lowercase 後に比較。"production "(改行混入)や"Production"も production として扱う(Vercel 環境変数の copy/paste 事故対策)process.envを露出しない runtime(Edge / Cloudflare Workers / browser)では fail-close で構築拒否bypassTokenの重複検知 / 空チェック /userInfo.sub必須チェック(構築時 throw)- 未登録 token は fail-close(実 introspect への暗黙 fallthrough は行わない)
- 構築時点で
usersのスナップショットを取り、入力配列の post-construction mutation は無視 - 各
verifyAccessToken呼び出しは新しいオブジェクトを返却(caller mutation が後続呼び出しを汚染しない)
allowInProduction escape hatch
staging が NODE_ENV=production をミラーするケース等のみ、明示的な ack 文字列で有効化できます:
import {
createTestFFIDClient,
TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK,
} from '@feelflow/ffid-sdk/server/test'
createTestFFIDClient({
users: [...],
allowInProduction: TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK,
})
// → process.emitWarning(..., 'FFIDTestModeInProduction') を毎構築時に発火boolean ではなく literal string ack を要求する型なので、allowInProduction: someBooleanFlag のような誤代入はコンパイルエラーになります。ack 文字列は grep 可能で監査も容易です。
サブパス分離
createTestFFIDClient は @feelflow/ffid-sdk/server/test からのみ import 可能です(@feelflow/ffid-sdk/server にも main entry にも含まれない)。本番コードからの誤 import は ESLint の no-restricted-imports 等で検知することを推奨します:
// eslint.config.mjs (flat config)
import { defineConfig } from 'eslint/config'
export default defineConfig([
{
files: ['src/**/*.{ts,tsx}'], // production code only
ignores: ['**/__tests__/**', 'tests/e2e/**'],
rules: {
'no-restricted-imports': ['error', {
patterns: ['@feelflow/ffid-sdk/server/test'],
}],
},
},
])環境変数
オプションで環境変数を使用してデフォルト設定を上書きできます:
NEXT_PUBLIC_FFID_API_URL=https://id.feelflow.net
NEXT_PUBLIC_FFID_SERVICE_CODE=chatbotライセンス
MIT
