@feelflow/ffid-sdk 8.1.0 → 10.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 +1010 -960
- package/dist/{FFIDCookieLink-Bwr0FBrV.d.cts → FFIDCookieLink-yt5SUr-W.d.cts} +47 -3
- package/dist/{FFIDCookieLink-Bwr0FBrV.d.ts → FFIDCookieLink-yt5SUr-W.d.ts} +47 -3
- package/dist/{chunk-CPXTIRB6.cjs → chunk-2LU2F3F2.cjs} +283 -11
- package/dist/{chunk-P5A6C5SK.js → chunk-OXW6QSTO.js} +270 -12
- package/dist/{chunk-XELXXL2F.cjs → chunk-RZCQ22GH.cjs} +29 -9
- package/dist/{chunk-RHBZHSRD.js → chunk-S2AEBJRX.js} +29 -9
- 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/consent/index.cjs +63 -63
- package/dist/consent/index.d.cts +2 -2
- package/dist/consent/index.d.ts +2 -2
- package/dist/consent/index.js +1 -1
- package/dist/{ffid-client-B0cORHac.d.ts → ffid-client-5uxXRgIO.d.ts} +686 -376
- package/dist/{ffid-client-6aLs9Fqc.d.cts → ffid-client-CumygXXz.d.cts} +686 -376
- package/dist/{index-BG7g99pK.d.ts → index-KjewcNn1.d.cts} +813 -2
- package/dist/{index-BG7g99pK.d.cts → index-KjewcNn1.d.ts} +813 -2
- package/dist/index.cjs +151 -95
- package/dist/index.d.cts +116 -505
- package/dist/index.d.ts +116 -505
- package/dist/index.js +3 -3
- package/dist/server/index.cjs +253 -11
- package/dist/server/index.d.cts +2 -2
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +253 -11
- package/dist/server/test/index.d.cts +1 -1
- package/dist/server/test/index.d.ts +1 -1
- package/dist/webhooks/index.d.cts +112 -7
- package/dist/webhooks/index.d.ts +112 -7
- package/package.json +131 -131
package/README.md
CHANGED
|
@@ -1,960 +1,1010 @@
|
|
|
1
|
-
# @feelflow/ffid-sdk
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@feelflow/ffid-sdk)
|
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
|
|
6
|
-
FeelFlow ID Platform SDK — React/Next.js 向け + サーバーサイドモジュールはフレームワーク非依存。
|
|
7
|
-
|
|
8
|
-
**5行のコードでFFID認証を導入!**
|
|
9
|
-
|
|
10
|
-
## インストール
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
npm install @feelflow/ffid-sdk
|
|
14
|
-
# or
|
|
15
|
-
yarn add @feelflow/ffid-sdk
|
|
16
|
-
# or
|
|
17
|
-
pnpm add @feelflow/ffid-sdk
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## 統合ガイド
|
|
21
|
-
|
|
22
|
-
サービスを FFID Platform に統合する詳細なガイドは **[Integration Guide](./docs/INTEGRATION_GUIDE.md)** を参照してください。OAuth フロー、フロントエンド/バックエンド実装パターン、セキュリティチェックリスト、アンチパターン集を網羅しています。
|
|
23
|
-
|
|
24
|
-
### Cookie 同意管理基盤 (v5.0.0+)
|
|
25
|
-
|
|
26
|
-
GDPR / ePrivacy / 改正電気通信事業法 / APPI 準拠の Cookie 同意 UI を **15 分以内** に導入できます (opt-in、v4.x 完全互換)。
|
|
27
|
-
|
|
28
|
-
- **[Cookie Consent Integration Guide](./docs/cookie-consent-guide.md)** — 17 章の詳細統合手順 (App Router / Pages Router / Vite / Consent Mode v2 / Sentry replay / RSC / i18n / A11y / エラーハンドリング)
|
|
29
|
-
- **[v4 → v5 Migration Guide](./docs/MIGRATION_v5.md)** — minimal / full アップグレード手順、自前 gtag からの移譲、Rollback
|
|
30
|
-
- **[Troubleshooting Q&A](./docs/cookie-consent-troubleshooting.md)** — Banner / GA / Cookie / TypeScript / API / Sentry のよくある詰まりポイント
|
|
31
|
-
- **動作サンプル**: [App Router](./examples/cookie-consent/nextjs-app-router/) / [Pages Router](./examples/cookie-consent/nextjs-pages-router/) / [Vite + React](./examples/cookie-consent/vite-react/)
|
|
32
|
-
|
|
33
|
-
```tsx
|
|
34
|
-
// app/layout.tsx (App Router、最短 3 ステップ)
|
|
35
|
-
import {
|
|
36
|
-
FFIDProvider,
|
|
37
|
-
FFIDAnalyticsProvider,
|
|
38
|
-
FFIDCookieBanner,
|
|
39
|
-
DEFAULT_OAUTH_SCOPES,
|
|
40
|
-
} from '@feelflow/ffid-sdk'
|
|
41
|
-
|
|
42
|
-
export default function RootLayout({ children }) {
|
|
43
|
-
return (
|
|
44
|
-
<FFIDProvider serviceCode="your-service" scope={DEFAULT_OAUTH_SCOPES}>
|
|
45
|
-
<FFIDAnalyticsProvider
|
|
46
|
-
baseUrl={process.env.NEXT_PUBLIC_FFID_BASE_URL!}
|
|
47
|
-
serviceApiKey={process.env.FFID_SERVICE_API_KEY!}
|
|
48
|
-
gaMeasurementId={process.env.NEXT_PUBLIC_GA_ID ?? null}
|
|
49
|
-
// Production-only measurement (v5.16.0+): suppress GA on
|
|
50
|
-
// preview/staging/local so non-prod traffic never pollutes the
|
|
51
|
-
// production GA4 property. Defaults to true if omitted.
|
|
52
|
-
gaEnabled={process.env.VERCEL_ENV === 'production'}
|
|
53
|
-
>
|
|
54
|
-
{children}
|
|
55
|
-
<FFIDCookieBanner />
|
|
56
|
-
</FFIDAnalyticsProvider>
|
|
57
|
-
</FFIDProvider>
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## クイックスタート
|
|
63
|
-
|
|
64
|
-
### 1. プロバイダーを設定(5行で完了!)
|
|
65
|
-
|
|
66
|
-
```tsx
|
|
67
|
-
// app/layout.tsx
|
|
68
|
-
import { FFIDProvider, DEFAULT_OAUTH_SCOPES } from '@feelflow/ffid-sdk'
|
|
69
|
-
|
|
70
|
-
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
71
|
-
return (
|
|
72
|
-
<html lang="ja">
|
|
73
|
-
<body>
|
|
74
|
-
<FFIDProvider serviceCode="chatbot" scope={DEFAULT_OAUTH_SCOPES}>{children}</FFIDProvider>
|
|
75
|
-
</body>
|
|
76
|
-
</html>
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### 2. 認証情報を使用
|
|
82
|
-
|
|
83
|
-
```tsx
|
|
84
|
-
import { useFFID, useSubscription } from '@feelflow/ffid-sdk'
|
|
85
|
-
|
|
86
|
-
function Dashboard() {
|
|
87
|
-
const { user, isAuthenticated, login, logout } = useFFID()
|
|
88
|
-
const { isActive, planCode } = useSubscription()
|
|
89
|
-
|
|
90
|
-
if (!isAuthenticated) {
|
|
91
|
-
return <button onClick={login}>ログイン</button>
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<div>
|
|
96
|
-
<p>Welcome, {user.displayName ?? user.email}!</p>
|
|
97
|
-
<p>プラン: {planCode}</p>
|
|
98
|
-
<button onClick={logout}>ログアウト</button>
|
|
99
|
-
</div>
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## UIコンポーネント
|
|
105
|
-
|
|
106
|
-
```tsx
|
|
107
|
-
import {
|
|
108
|
-
FFIDLoginButton,
|
|
109
|
-
FFIDUserMenu,
|
|
110
|
-
FFIDOrganizationSwitcher,
|
|
111
|
-
FFIDSubscriptionBadge,
|
|
112
|
-
} from '@feelflow/ffid-sdk/components'
|
|
113
|
-
|
|
114
|
-
function Header() {
|
|
115
|
-
return (
|
|
116
|
-
<header>
|
|
117
|
-
<FFIDLoginButton>ログイン</FFIDLoginButton>
|
|
118
|
-
<FFIDOrganizationSwitcher />
|
|
119
|
-
<FFIDSubscriptionBadge />
|
|
120
|
-
<FFIDUserMenu />
|
|
121
|
-
</header>
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### コンポーネントのスタイリング(classNames パターン)
|
|
127
|
-
|
|
128
|
-
各UIコンポーネントは [Radix UI スタイルパターン](https://www.radix-ui.com/primitives/docs/guides/styling) に従った `classNames` プロップをサポートしています。これにより、コンポーネントの各パーツに個別のクラスを適用できます。
|
|
129
|
-
|
|
130
|
-
#### FFIDUserMenu
|
|
131
|
-
|
|
132
|
-
```tsx
|
|
133
|
-
<FFIDUserMenu
|
|
134
|
-
classNames={{
|
|
135
|
-
container: 'relative', // ラッパー要素
|
|
136
|
-
button: 'focus:ring-2', // アバターボタン(トリガー)
|
|
137
|
-
avatar: 'rounded-full', // アバター画像/フォールバック
|
|
138
|
-
menu: 'shadow-lg', // ドロップダウンメニュー
|
|
139
|
-
userInfo: 'border-b', // ユーザー情報セクション
|
|
140
|
-
menuItem: 'hover:bg-gray-100', // カスタムメニュー項目
|
|
141
|
-
logout: 'text-red-600', // ログアウトボタン
|
|
142
|
-
}}
|
|
143
|
-
/>
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
#### FFIDOrganizationSwitcher
|
|
147
|
-
|
|
148
|
-
```tsx
|
|
149
|
-
<FFIDOrganizationSwitcher
|
|
150
|
-
classNames={{
|
|
151
|
-
container: 'relative', // ラッパー要素
|
|
152
|
-
button: 'border rounded', // トリガーボタン
|
|
153
|
-
dropdown: 'shadow-md', // ドロップダウンメニュー
|
|
154
|
-
option: 'px-4 py-2', // 各組織オプション
|
|
155
|
-
optionSelected: 'bg-blue-50', // 選択中の組織(optionに加えて適用)
|
|
156
|
-
}}
|
|
157
|
-
/>
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
#### FFIDSubscriptionBadge
|
|
161
|
-
|
|
162
|
-
```tsx
|
|
163
|
-
<FFIDSubscriptionBadge
|
|
164
|
-
classNames={{
|
|
165
|
-
badge: 'font-semibold', // バッジspan要素
|
|
166
|
-
}}
|
|
167
|
-
/>
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
> **Note**: `FFIDLoginButton` はシンプルな単一要素コンポーネントのため、標準の `className` プロップのみをサポートしています。
|
|
171
|
-
|
|
172
|
-
## API リファレンス
|
|
173
|
-
|
|
174
|
-
### FFIDProvider
|
|
175
|
-
|
|
176
|
-
アプリケーション全体をラップするプロバイダーコンポーネント。
|
|
177
|
-
|
|
178
|
-
```tsx
|
|
179
|
-
<FFIDProvider
|
|
180
|
-
serviceCode="chatbot" // 必須: サービスコード
|
|
181
|
-
scope={DEFAULT_OAUTH_SCOPES} // 必須: OAuth scope (v3.0.0+)
|
|
182
|
-
apiBaseUrl="..." // オプション: カスタムAPIエンドポイント
|
|
183
|
-
debug={true} // オプション: デバッグログ有効化(非推奨、loggerを使用)
|
|
184
|
-
logger={customLogger} // オプション: カスタムロガー(下記参照)
|
|
185
|
-
refreshInterval={300000} // オプション: セッション更新間隔(ms)
|
|
186
|
-
onAuthStateChange={(user) => {}} // オプション: 認証状態変更時コールバック
|
|
187
|
-
onError={(error) => {}} // オプション: エラー時コールバック
|
|
188
|
-
cleanCallbackUrl={true} // オプション: token モードの callback URL cleanup(false で consumer に委譲、v5.22.0+)
|
|
189
|
-
>
|
|
190
|
-
{children}
|
|
191
|
-
</FFIDProvider>
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
### カスタムロガー
|
|
195
|
-
|
|
196
|
-
SDKのデバッグ出力をカスタマイズできます。デフォルトではログは出力されません(サイレント)。
|
|
197
|
-
|
|
198
|
-
```tsx
|
|
199
|
-
import type { FFIDLogger } from '@feelflow/ffid-sdk'
|
|
200
|
-
import pino from 'pino' // または winston, bunyan 等
|
|
201
|
-
|
|
202
|
-
// アプリケーションのロガーインスタンス
|
|
203
|
-
const appLogger = pino({ level: 'debug' })
|
|
204
|
-
|
|
205
|
-
// FFID SDK用にラップ
|
|
206
|
-
const ffidLogger: FFIDLogger = {
|
|
207
|
-
debug: (...args) => appLogger.debug({ sdk: 'ffid' }, ...args),
|
|
208
|
-
info: (...args) => appLogger.info({ sdk: 'ffid' }, ...args),
|
|
209
|
-
warn: (...args) => appLogger.warn({ sdk: 'ffid' }, ...args),
|
|
210
|
-
error: (...args) => appLogger.error({ sdk: 'ffid' }, ...args),
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// 使用例
|
|
214
|
-
<FFIDProvider serviceCode="chatbot" scope={DEFAULT_OAUTH_SCOPES} logger={ffidLogger}>
|
|
215
|
-
{children}
|
|
216
|
-
</FFIDProvider>
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
**ロガー優先順位:**
|
|
220
|
-
|
|
221
|
-
1. `logger` が指定されている場合 → カスタムロガーを使用
|
|
222
|
-
2. `debug: true` で `logger` なし → `console` を使用(後方互換性)
|
|
223
|
-
3. 両方なし → サイレント(no-op)
|
|
224
|
-
|
|
225
|
-
### useFFID()
|
|
226
|
-
|
|
227
|
-
ユーザー・組織情報を取得するフック。
|
|
228
|
-
|
|
229
|
-
```tsx
|
|
230
|
-
const {
|
|
231
|
-
user, // FFIDUser | null - 現在のユーザー
|
|
232
|
-
organizations, // FFIDOrganization[] - 所属組織一覧
|
|
233
|
-
currentOrganization, // FFIDOrganization | null - 現在の組織
|
|
234
|
-
isLoading, // boolean - ロード中
|
|
235
|
-
isAuthenticated, // boolean - 認証済み
|
|
236
|
-
login, // () => void - ログインページへリダイレクト
|
|
237
|
-
logout, // () => Promise<void> - ログアウト
|
|
238
|
-
switchOrganization, // (id: string) => void - 組織切り替え
|
|
239
|
-
refresh, // () => Promise<void> - セッション更新
|
|
240
|
-
} = useFFID()
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### useSubscription()
|
|
244
|
-
|
|
245
|
-
契約情報を取得するフック。
|
|
246
|
-
|
|
247
|
-
```tsx
|
|
248
|
-
const {
|
|
249
|
-
subscription, // FFIDSubscription | null - 現在のサブスクリプション
|
|
250
|
-
planCode, // string | null - プランコード
|
|
251
|
-
isActive, // boolean - DB ステータスが 'active'
|
|
252
|
-
isTrialing, // boolean - トライアル中
|
|
253
|
-
isCanceled, // boolean - 解約済み
|
|
254
|
-
isTrialExpired, // boolean - トライアル期間超過
|
|
255
|
-
effectiveStatus, // EffectiveSubscriptionStatus | null - 意味論的アクセス制御値
|
|
256
|
-
isBlocked, // boolean - blocked / expired / canceled / trial_expired
|
|
257
|
-
isGrace, // boolean - past_due_grace (支払い失敗の猶予期間中)
|
|
258
|
-
hasPlan, // (plans: string | string[]) => boolean - プラン確認
|
|
259
|
-
hasAccess, // () => boolean - アクセス権確認 (active || past_due_grace)
|
|
260
|
-
hasAccessLegacy, // () => boolean - 旧セマンティクス (active || trialing, pre-2.19)
|
|
261
|
-
} = useSubscription()
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
`effectiveStatus` は `/api/v1/subscriptions/ext/check` が返す意味論的ステータスと同じ値を取る。詳細は [契約期限切れハンドリング](#契約期限切れハンドリング) を参照。
|
|
265
|
-
|
|
266
|
-
### useRequireActiveSubscription()
|
|
267
|
-
|
|
268
|
-
契約が期限切れ/遮断状態のときに自動でリダイレクトするフック。
|
|
269
|
-
|
|
270
|
-
```tsx
|
|
271
|
-
'use client'
|
|
272
|
-
import { useRouter } from 'next/navigation'
|
|
273
|
-
import { useRequireActiveSubscription } from '@feelflow/ffid-sdk'
|
|
274
|
-
|
|
275
|
-
function ProtectedShell({ children }: { children: React.ReactNode }) {
|
|
276
|
-
const router = useRouter()
|
|
277
|
-
const { loading } = useRequireActiveSubscription({
|
|
278
|
-
redirectTo: (status) =>
|
|
279
|
-
status === 'canceled' ? '/contract-ended' : '/contract-required',
|
|
280
|
-
onRedirect: (url) => router.replace(url),
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
if (loading) return <FullPageSpinner />
|
|
284
|
-
return <>{children}</>
|
|
285
|
-
}
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
オプション:
|
|
289
|
-
|
|
290
|
-
- `redirectTo`: `string` または `(status: EffectiveSubscriptionStatus) => string`
|
|
291
|
-
- `allowGrace` (default: `true`): `past_due_grace` を通過させるか
|
|
292
|
-
- `onRedirect` (optional): 独自のリダイレクト関数(未指定時は `window.location.href`)
|
|
293
|
-
|
|
294
|
-
### withSubscription()
|
|
295
|
-
|
|
296
|
-
サブスクリプション確認HOC。
|
|
297
|
-
|
|
298
|
-
```tsx
|
|
299
|
-
const PremiumFeature = withSubscription(MyComponent, {
|
|
300
|
-
plans: ['pro', 'enterprise'],
|
|
301
|
-
fallback: <UpgradePrompt />,
|
|
302
|
-
loading: <Spinner />,
|
|
303
|
-
})
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
## 契約期限切れハンドリング
|
|
307
|
-
|
|
308
|
-
契約が失効したり解約されたとき、外部サービス側で適切にアクセスを遮断し、ユーザーを再契約動線に案内する必要があります。SDK は 3 層構成(トークン検証 / 契約チェック / Webhook 受信)をサポートしています。
|
|
309
|
-
|
|
310
|
-
### 最小構成(Next.js App Router)
|
|
311
|
-
|
|
312
|
-
```tsx
|
|
313
|
-
'use client'
|
|
314
|
-
import { useRouter } from 'next/navigation'
|
|
315
|
-
import { useRequireActiveSubscription } from '@feelflow/ffid-sdk'
|
|
316
|
-
|
|
317
|
-
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
318
|
-
const router = useRouter()
|
|
319
|
-
const { loading, effectiveStatus } = useRequireActiveSubscription({
|
|
320
|
-
redirectTo: '/contract-required',
|
|
321
|
-
onRedirect: (url) => router.replace(url),
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
if (loading) return <Spinner />
|
|
325
|
-
if (effectiveStatus !== 'active' && effectiveStatus !== 'past_due_grace') {
|
|
326
|
-
return null // リダイレクト発火中
|
|
327
|
-
}
|
|
328
|
-
return <>{children}</>
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
**詳細な実装レシピ**(middleware、Express、UI 分岐、Webhook 受信、fixture API を使ったテスト、`EffectiveSubscriptionStatus` 別の UI 推奨動作まで) → [docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md](https://github.com/feel-flow/feelflow-id-platform/blob/develop/docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md)
|
|
333
|
-
|
|
334
|
-
### Server-side service access decision
|
|
335
|
-
|
|
336
|
-
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 を再実装しない。
|
|
337
|
-
|
|
338
|
-
```ts
|
|
339
|
-
import { createFFIDClient } from '@feelflow/ffid-sdk'
|
|
340
|
-
|
|
341
|
-
const ffid = createFFIDClient({
|
|
342
|
-
serviceCode: 'flow-board-ai',
|
|
343
|
-
scope: '',
|
|
344
|
-
authMode: 'service-key',
|
|
345
|
-
serviceApiKey: process.env.FFID_SERVICE_API_KEY!,
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
const { data: access, error } = await ffid.checkServiceAccess({
|
|
349
|
-
userId,
|
|
350
|
-
organizationId,
|
|
351
|
-
allowGrace: true,
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
if (error || !access?.hasAccess) {
|
|
355
|
-
return Response.redirect('/contract-required')
|
|
356
|
-
}
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
- `checkServiceAccess()` は **`data.hasAccess` が唯一の gate**。`result.error` は入力 validation など SDK が decision を作れない場合にだけ返る。
|
|
360
|
-
- `hasAccess`: FFID が決めた canonical access decision。`allowGrace=false` のときだけ SDK 側で `past_due_grace` を deny に変換する。
|
|
361
|
-
- `effectiveStatus`: `active` / `past_due_grace` / `blocked` / `canceled` / `trial_expired` / `expired`。
|
|
362
|
-
- `gracePeriodEndsAt`: `past_due_grace` が `blocked` に変わる時刻。表示・警告用であり、アクセス判定の source of truth にしない。
|
|
363
|
-
- `failPolicy`: 現在は `failClosed` 固定。FFID に到達できない、または canonical decision を取得できない場合は `result.error` ではなく `data.hasAccess=false` / `denialReason='ffid_unreachable'` / `data.error` を返す。呼び出し側は `result.error` だけで通過判定しない。
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
if (
|
|
436
|
-
throw new Error(
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
await ffid.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
### ⚠️
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
import {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
```
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
`
|
|
780
|
-
|
|
781
|
-
##
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
```
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
```
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1
|
+
# @feelflow/ffid-sdk
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@feelflow/ffid-sdk)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
FeelFlow ID Platform SDK — React/Next.js 向け + サーバーサイドモジュールはフレームワーク非依存。
|
|
7
|
+
|
|
8
|
+
**5行のコードでFFID認証を導入!**
|
|
9
|
+
|
|
10
|
+
## インストール
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @feelflow/ffid-sdk
|
|
14
|
+
# or
|
|
15
|
+
yarn add @feelflow/ffid-sdk
|
|
16
|
+
# or
|
|
17
|
+
pnpm add @feelflow/ffid-sdk
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 統合ガイド
|
|
21
|
+
|
|
22
|
+
サービスを FFID Platform に統合する詳細なガイドは **[Integration Guide](./docs/INTEGRATION_GUIDE.md)** を参照してください。OAuth フロー、フロントエンド/バックエンド実装パターン、セキュリティチェックリスト、アンチパターン集を網羅しています。
|
|
23
|
+
|
|
24
|
+
### Cookie 同意管理基盤 (v5.0.0+)
|
|
25
|
+
|
|
26
|
+
GDPR / ePrivacy / 改正電気通信事業法 / APPI 準拠の Cookie 同意 UI を **15 分以内** に導入できます (opt-in、v4.x 完全互換)。
|
|
27
|
+
|
|
28
|
+
- **[Cookie Consent Integration Guide](./docs/cookie-consent-guide.md)** — 17 章の詳細統合手順 (App Router / Pages Router / Vite / Consent Mode v2 / Sentry replay / RSC / i18n / A11y / エラーハンドリング)
|
|
29
|
+
- **[v4 → v5 Migration Guide](./docs/MIGRATION_v5.md)** — minimal / full アップグレード手順、自前 gtag からの移譲、Rollback
|
|
30
|
+
- **[Troubleshooting Q&A](./docs/cookie-consent-troubleshooting.md)** — Banner / GA / Cookie / TypeScript / API / Sentry のよくある詰まりポイント
|
|
31
|
+
- **動作サンプル**: [App Router](./examples/cookie-consent/nextjs-app-router/) / [Pages Router](./examples/cookie-consent/nextjs-pages-router/) / [Vite + React](./examples/cookie-consent/vite-react/)
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// app/layout.tsx (App Router、最短 3 ステップ)
|
|
35
|
+
import {
|
|
36
|
+
FFIDProvider,
|
|
37
|
+
FFIDAnalyticsProvider,
|
|
38
|
+
FFIDCookieBanner,
|
|
39
|
+
DEFAULT_OAUTH_SCOPES,
|
|
40
|
+
} from '@feelflow/ffid-sdk'
|
|
41
|
+
|
|
42
|
+
export default function RootLayout({ children }) {
|
|
43
|
+
return (
|
|
44
|
+
<FFIDProvider serviceCode="your-service" scope={DEFAULT_OAUTH_SCOPES}>
|
|
45
|
+
<FFIDAnalyticsProvider
|
|
46
|
+
baseUrl={process.env.NEXT_PUBLIC_FFID_BASE_URL!}
|
|
47
|
+
serviceApiKey={process.env.FFID_SERVICE_API_KEY!}
|
|
48
|
+
gaMeasurementId={process.env.NEXT_PUBLIC_GA_ID ?? null}
|
|
49
|
+
// Production-only measurement (v5.16.0+): suppress GA on
|
|
50
|
+
// preview/staging/local so non-prod traffic never pollutes the
|
|
51
|
+
// production GA4 property. Defaults to true if omitted.
|
|
52
|
+
gaEnabled={process.env.VERCEL_ENV === 'production'}
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
<FFIDCookieBanner />
|
|
56
|
+
</FFIDAnalyticsProvider>
|
|
57
|
+
</FFIDProvider>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## クイックスタート
|
|
63
|
+
|
|
64
|
+
### 1. プロバイダーを設定(5行で完了!)
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// app/layout.tsx
|
|
68
|
+
import { FFIDProvider, DEFAULT_OAUTH_SCOPES } from '@feelflow/ffid-sdk'
|
|
69
|
+
|
|
70
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
71
|
+
return (
|
|
72
|
+
<html lang="ja">
|
|
73
|
+
<body>
|
|
74
|
+
<FFIDProvider serviceCode="chatbot" scope={DEFAULT_OAUTH_SCOPES}>{children}</FFIDProvider>
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2. 認証情報を使用
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { useFFID, useSubscription } from '@feelflow/ffid-sdk'
|
|
85
|
+
|
|
86
|
+
function Dashboard() {
|
|
87
|
+
const { user, isAuthenticated, login, logout } = useFFID()
|
|
88
|
+
const { isActive, planCode } = useSubscription()
|
|
89
|
+
|
|
90
|
+
if (!isAuthenticated) {
|
|
91
|
+
return <button onClick={login}>ログイン</button>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div>
|
|
96
|
+
<p>Welcome, {user.displayName ?? user.email}!</p>
|
|
97
|
+
<p>プラン: {planCode}</p>
|
|
98
|
+
<button onClick={logout}>ログアウト</button>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## UIコンポーネント
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import {
|
|
108
|
+
FFIDLoginButton,
|
|
109
|
+
FFIDUserMenu,
|
|
110
|
+
FFIDOrganizationSwitcher,
|
|
111
|
+
FFIDSubscriptionBadge,
|
|
112
|
+
} from '@feelflow/ffid-sdk/components'
|
|
113
|
+
|
|
114
|
+
function Header() {
|
|
115
|
+
return (
|
|
116
|
+
<header>
|
|
117
|
+
<FFIDLoginButton>ログイン</FFIDLoginButton>
|
|
118
|
+
<FFIDOrganizationSwitcher />
|
|
119
|
+
<FFIDSubscriptionBadge />
|
|
120
|
+
<FFIDUserMenu />
|
|
121
|
+
</header>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### コンポーネントのスタイリング(classNames パターン)
|
|
127
|
+
|
|
128
|
+
各UIコンポーネントは [Radix UI スタイルパターン](https://www.radix-ui.com/primitives/docs/guides/styling) に従った `classNames` プロップをサポートしています。これにより、コンポーネントの各パーツに個別のクラスを適用できます。
|
|
129
|
+
|
|
130
|
+
#### FFIDUserMenu
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
<FFIDUserMenu
|
|
134
|
+
classNames={{
|
|
135
|
+
container: 'relative', // ラッパー要素
|
|
136
|
+
button: 'focus:ring-2', // アバターボタン(トリガー)
|
|
137
|
+
avatar: 'rounded-full', // アバター画像/フォールバック
|
|
138
|
+
menu: 'shadow-lg', // ドロップダウンメニュー
|
|
139
|
+
userInfo: 'border-b', // ユーザー情報セクション
|
|
140
|
+
menuItem: 'hover:bg-gray-100', // カスタムメニュー項目
|
|
141
|
+
logout: 'text-red-600', // ログアウトボタン
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### FFIDOrganizationSwitcher
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
<FFIDOrganizationSwitcher
|
|
150
|
+
classNames={{
|
|
151
|
+
container: 'relative', // ラッパー要素
|
|
152
|
+
button: 'border rounded', // トリガーボタン
|
|
153
|
+
dropdown: 'shadow-md', // ドロップダウンメニュー
|
|
154
|
+
option: 'px-4 py-2', // 各組織オプション
|
|
155
|
+
optionSelected: 'bg-blue-50', // 選択中の組織(optionに加えて適用)
|
|
156
|
+
}}
|
|
157
|
+
/>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### FFIDSubscriptionBadge
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
<FFIDSubscriptionBadge
|
|
164
|
+
classNames={{
|
|
165
|
+
badge: 'font-semibold', // バッジspan要素
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
> **Note**: `FFIDLoginButton` はシンプルな単一要素コンポーネントのため、標準の `className` プロップのみをサポートしています。
|
|
171
|
+
|
|
172
|
+
## API リファレンス
|
|
173
|
+
|
|
174
|
+
### FFIDProvider
|
|
175
|
+
|
|
176
|
+
アプリケーション全体をラップするプロバイダーコンポーネント。
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
<FFIDProvider
|
|
180
|
+
serviceCode="chatbot" // 必須: サービスコード
|
|
181
|
+
scope={DEFAULT_OAUTH_SCOPES} // 必須: OAuth scope (v3.0.0+)
|
|
182
|
+
apiBaseUrl="..." // オプション: カスタムAPIエンドポイント
|
|
183
|
+
debug={true} // オプション: デバッグログ有効化(非推奨、loggerを使用)
|
|
184
|
+
logger={customLogger} // オプション: カスタムロガー(下記参照)
|
|
185
|
+
refreshInterval={300000} // オプション: セッション更新間隔(ms)
|
|
186
|
+
onAuthStateChange={(user) => {}} // オプション: 認証状態変更時コールバック
|
|
187
|
+
onError={(error) => {}} // オプション: エラー時コールバック
|
|
188
|
+
cleanCallbackUrl={true} // オプション: token モードの callback URL cleanup(false で consumer に委譲、v5.22.0+)
|
|
189
|
+
>
|
|
190
|
+
{children}
|
|
191
|
+
</FFIDProvider>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### カスタムロガー
|
|
195
|
+
|
|
196
|
+
SDKのデバッグ出力をカスタマイズできます。デフォルトではログは出力されません(サイレント)。
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
import type { FFIDLogger } from '@feelflow/ffid-sdk'
|
|
200
|
+
import pino from 'pino' // または winston, bunyan 等
|
|
201
|
+
|
|
202
|
+
// アプリケーションのロガーインスタンス
|
|
203
|
+
const appLogger = pino({ level: 'debug' })
|
|
204
|
+
|
|
205
|
+
// FFID SDK用にラップ
|
|
206
|
+
const ffidLogger: FFIDLogger = {
|
|
207
|
+
debug: (...args) => appLogger.debug({ sdk: 'ffid' }, ...args),
|
|
208
|
+
info: (...args) => appLogger.info({ sdk: 'ffid' }, ...args),
|
|
209
|
+
warn: (...args) => appLogger.warn({ sdk: 'ffid' }, ...args),
|
|
210
|
+
error: (...args) => appLogger.error({ sdk: 'ffid' }, ...args),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 使用例
|
|
214
|
+
<FFIDProvider serviceCode="chatbot" scope={DEFAULT_OAUTH_SCOPES} logger={ffidLogger}>
|
|
215
|
+
{children}
|
|
216
|
+
</FFIDProvider>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**ロガー優先順位:**
|
|
220
|
+
|
|
221
|
+
1. `logger` が指定されている場合 → カスタムロガーを使用
|
|
222
|
+
2. `debug: true` で `logger` なし → `console` を使用(後方互換性)
|
|
223
|
+
3. 両方なし → サイレント(no-op)
|
|
224
|
+
|
|
225
|
+
### useFFID()
|
|
226
|
+
|
|
227
|
+
ユーザー・組織情報を取得するフック。
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
const {
|
|
231
|
+
user, // FFIDUser | null - 現在のユーザー
|
|
232
|
+
organizations, // FFIDOrganization[] - 所属組織一覧
|
|
233
|
+
currentOrganization, // FFIDOrganization | null - 現在の組織
|
|
234
|
+
isLoading, // boolean - ロード中
|
|
235
|
+
isAuthenticated, // boolean - 認証済み
|
|
236
|
+
login, // () => void - ログインページへリダイレクト
|
|
237
|
+
logout, // () => Promise<void> - ログアウト
|
|
238
|
+
switchOrganization, // (id: string) => void - 組織切り替え
|
|
239
|
+
refresh, // () => Promise<void> - セッション更新
|
|
240
|
+
} = useFFID()
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### useSubscription()
|
|
244
|
+
|
|
245
|
+
契約情報を取得するフック。
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
const {
|
|
249
|
+
subscription, // FFIDSubscription | null - 現在のサブスクリプション
|
|
250
|
+
planCode, // string | null - プランコード
|
|
251
|
+
isActive, // boolean - DB ステータスが 'active'
|
|
252
|
+
isTrialing, // boolean - トライアル中
|
|
253
|
+
isCanceled, // boolean - 解約済み
|
|
254
|
+
isTrialExpired, // boolean - トライアル期間超過
|
|
255
|
+
effectiveStatus, // EffectiveSubscriptionStatus | null - 意味論的アクセス制御値
|
|
256
|
+
isBlocked, // boolean - blocked / expired / canceled / trial_expired
|
|
257
|
+
isGrace, // boolean - past_due_grace (支払い失敗の猶予期間中)
|
|
258
|
+
hasPlan, // (plans: string | string[]) => boolean - プラン確認
|
|
259
|
+
hasAccess, // () => boolean - アクセス権確認 (active || past_due_grace)
|
|
260
|
+
hasAccessLegacy, // () => boolean - 旧セマンティクス (active || trialing, pre-2.19)
|
|
261
|
+
} = useSubscription()
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`effectiveStatus` は `/api/v1/subscriptions/ext/check` が返す意味論的ステータスと同じ値を取る。詳細は [契約期限切れハンドリング](#契約期限切れハンドリング) を参照。
|
|
265
|
+
|
|
266
|
+
### useRequireActiveSubscription()
|
|
267
|
+
|
|
268
|
+
契約が期限切れ/遮断状態のときに自動でリダイレクトするフック。
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
'use client'
|
|
272
|
+
import { useRouter } from 'next/navigation'
|
|
273
|
+
import { useRequireActiveSubscription } from '@feelflow/ffid-sdk'
|
|
274
|
+
|
|
275
|
+
function ProtectedShell({ children }: { children: React.ReactNode }) {
|
|
276
|
+
const router = useRouter()
|
|
277
|
+
const { loading } = useRequireActiveSubscription({
|
|
278
|
+
redirectTo: (status) =>
|
|
279
|
+
status === 'canceled' ? '/contract-ended' : '/contract-required',
|
|
280
|
+
onRedirect: (url) => router.replace(url),
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
if (loading) return <FullPageSpinner />
|
|
284
|
+
return <>{children}</>
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
オプション:
|
|
289
|
+
|
|
290
|
+
- `redirectTo`: `string` または `(status: EffectiveSubscriptionStatus) => string`
|
|
291
|
+
- `allowGrace` (default: `true`): `past_due_grace` を通過させるか
|
|
292
|
+
- `onRedirect` (optional): 独自のリダイレクト関数(未指定時は `window.location.href`)
|
|
293
|
+
|
|
294
|
+
### withSubscription()
|
|
295
|
+
|
|
296
|
+
サブスクリプション確認HOC。
|
|
297
|
+
|
|
298
|
+
```tsx
|
|
299
|
+
const PremiumFeature = withSubscription(MyComponent, {
|
|
300
|
+
plans: ['pro', 'enterprise'],
|
|
301
|
+
fallback: <UpgradePrompt />,
|
|
302
|
+
loading: <Spinner />,
|
|
303
|
+
})
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## 契約期限切れハンドリング
|
|
307
|
+
|
|
308
|
+
契約が失効したり解約されたとき、外部サービス側で適切にアクセスを遮断し、ユーザーを再契約動線に案内する必要があります。SDK は 3 層構成(トークン検証 / 契約チェック / Webhook 受信)をサポートしています。
|
|
309
|
+
|
|
310
|
+
### 最小構成(Next.js App Router)
|
|
311
|
+
|
|
312
|
+
```tsx
|
|
313
|
+
'use client'
|
|
314
|
+
import { useRouter } from 'next/navigation'
|
|
315
|
+
import { useRequireActiveSubscription } from '@feelflow/ffid-sdk'
|
|
316
|
+
|
|
317
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
318
|
+
const router = useRouter()
|
|
319
|
+
const { loading, effectiveStatus } = useRequireActiveSubscription({
|
|
320
|
+
redirectTo: '/contract-required',
|
|
321
|
+
onRedirect: (url) => router.replace(url),
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
if (loading) return <Spinner />
|
|
325
|
+
if (effectiveStatus !== 'active' && effectiveStatus !== 'past_due_grace') {
|
|
326
|
+
return null // リダイレクト発火中
|
|
327
|
+
}
|
|
328
|
+
return <>{children}</>
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**詳細な実装レシピ**(middleware、Express、UI 分岐、Webhook 受信、fixture API を使ったテスト、`EffectiveSubscriptionStatus` 別の UI 推奨動作まで) → [docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md](https://github.com/feel-flow/feelflow-id-platform/blob/develop/docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md)
|
|
333
|
+
|
|
334
|
+
### Server-side service access decision
|
|
335
|
+
|
|
336
|
+
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 を再実装しない。
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
import { createFFIDClient } from '@feelflow/ffid-sdk'
|
|
340
|
+
|
|
341
|
+
const ffid = createFFIDClient({
|
|
342
|
+
serviceCode: 'flow-board-ai',
|
|
343
|
+
scope: '',
|
|
344
|
+
authMode: 'service-key',
|
|
345
|
+
serviceApiKey: process.env.FFID_SERVICE_API_KEY!,
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const { data: access, error } = await ffid.checkServiceAccess({
|
|
349
|
+
userId,
|
|
350
|
+
organizationId,
|
|
351
|
+
allowGrace: true,
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
if (error || !access?.hasAccess) {
|
|
355
|
+
return Response.redirect('/contract-required')
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
- `checkServiceAccess()` は **`data.hasAccess` が唯一の gate**。`result.error` は入力 validation など SDK が decision を作れない場合にだけ返る。
|
|
360
|
+
- `hasAccess`: FFID が決めた canonical access decision。`allowGrace=false` のときだけ SDK 側で `past_due_grace` を deny に変換する。
|
|
361
|
+
- `effectiveStatus`: `active` / `past_due_grace` / `blocked` / `canceled` / `trial_expired` / `expired`。
|
|
362
|
+
- `gracePeriodEndsAt`: `past_due_grace` が `blocked` に変わる時刻。表示・警告用であり、アクセス判定の source of truth にしない。
|
|
363
|
+
- `failPolicy`: 現在は `failClosed` 固定。FFID に到達できない、または canonical decision を取得できない場合は `result.error` ではなく `data.hasAccess=false` / `denialReason='ffid_unreachable'` / `data.error` を返す。呼び出し側は `result.error` だけで通過判定しない。
|
|
364
|
+
- `denialReason` には `member_limit_reached`(10.0.0)も含まれる。flat プランの人数枠が満員で席を付けられない利用者は、ログイン時に OAuth の `denial_reason: 'member_limit_reached'` で止まる。`checkServiceAccess()` 自体はこの値を返さず、人数枠・クレジット残高は `hasAccess` に影響しない。`memberLimitReached` / `credits` / `pricingModel` は情報として decision に載る(古い FFID は返さない)。
|
|
365
|
+
|
|
366
|
+
### Credits(flat プラン、v10.0.0)
|
|
367
|
+
|
|
368
|
+
`pricingModel: 'flat'`(固定料金 + 人数枠 + 月次 AI クレジット)の契約では、AI を実行するたびに FFID のクレジット台帳へ消費を報告する。契約の正本は FFID の [CREDITS_API.md](https://github.com/feel-flow/feelflow-id-platform/blob/develop/docs/04-api/CREDITS_API.md)、手順は [docs/INTEGRATION_GUIDE.md の Credits 節](./docs/INTEGRATION_GUIDE.md#credits-flat-plans)。
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
import { createFFIDClient, FFID_CREDITS_ERROR_CODES, type FFIDCreditsInsufficientDetails } from '@feelflow/ffid-sdk'
|
|
372
|
+
|
|
373
|
+
// サービスのバックエンド(consume は service-key モード専用)
|
|
374
|
+
const ffid = createFFIDClient({
|
|
375
|
+
serviceCode: 'chatbot-v2',
|
|
376
|
+
scope: '',
|
|
377
|
+
authMode: 'service-key',
|
|
378
|
+
serviceApiKey: process.env.FFID_SERVICE_API_KEY!,
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
const { data, error } = await ffid.credits.consume({
|
|
382
|
+
subscriptionId,
|
|
383
|
+
amount: 50,
|
|
384
|
+
idempotencyKey: `chat.reply:${requestId}`, // AI 実行 1 回につき 1 つ。再試行でも同じキー
|
|
385
|
+
feature: 'chat.reply',
|
|
386
|
+
metadata: { model: 'example', tokens: 1200 }, // 個人情報は入れない
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
if (error?.code === FFID_CREDITS_ERROR_CODES.CREDITS_INSUFFICIENT) {
|
|
390
|
+
const details = error.details as unknown as FFIDCreditsInsufficientDetails
|
|
391
|
+
// 自動で再試行しない。購入導線(owner / admin)か管理者への依頼を出す
|
|
392
|
+
return { blocked: true, available: details.available }
|
|
393
|
+
}
|
|
394
|
+
if (error) {
|
|
395
|
+
// 5xx / NETWORK_ERROR は同じキー・同じ amount で再試行してよい(duplicate が返る)
|
|
396
|
+
throw new Error(error.message)
|
|
397
|
+
}
|
|
398
|
+
// data.status は 'consumed' | 'duplicate'
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
| メソッド | 認証モード | 用途 |
|
|
402
|
+
|---|---|---|
|
|
403
|
+
| `credits.consume(params)` | service-key のみ | AI 実行 1 回ぶんの消費を報告 |
|
|
404
|
+
| `credits.getBalance(subscriptionId)` | token / service-key | 残高・当期・内訳・`isLow` / `isExhausted` |
|
|
405
|
+
| `credits.listHistory(subscriptionId, { cursor?, limit?, type? })` | token / service-key | 履歴(新しい順) |
|
|
406
|
+
| `credits.listPacks(serviceCode?)` | token / service-key | 購入できるパック |
|
|
407
|
+
| `credits.createPackCheckout(params)` | token のみ(owner / admin) | パック購入の Stripe Checkout URL |
|
|
408
|
+
| `credits.getBuyCreditsUrl({ subscriptionId, orgId? })` / `redirectToBuyCredits` | どれでも | FFID の契約詳細で購入ダイアログを開く URL(UUID か `ffid:<uuid>`。`sub_...` は不可) |
|
|
409
|
+
| `getSeats(subscriptionId)` | token / service-key | 人数枠(`maxMembers` / `assignedMembers` / `availableMembers`、`null` = 無制限) |
|
|
410
|
+
|
|
411
|
+
- AI 機能のゲートは **残高**(`consume` の結果・`getBalance()`・`checkServiceAccess()` の `credits`)で行い、`hasAccess` では行わない。クレジットが 0 でも `hasAccess` は `true` のまま。
|
|
412
|
+
- `consume` を service-key 以外で、`createPackCheckout` を token 以外で呼ぶと、ネットワークに出る前に `FORBIDDEN`(`details.reason: 'api_key_required'` / `'bearer_required'`)が返る。
|
|
413
|
+
- 残高の変化は webhook `credits.granted` / `credits.low` / `credits.exhausted` / `credits.pack_purchased` でも届く(`@feelflow/ffid-sdk/webhooks`)。
|
|
414
|
+
|
|
415
|
+
### Organization member management
|
|
416
|
+
|
|
417
|
+
`organization:read` / `organization:write` scope を持つ service-key または token で、組織メンバーの参照・追加・ロール変更・削除ができます。
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
import { createFFIDClient } from '@feelflow/ffid-sdk/server'
|
|
421
|
+
|
|
422
|
+
const ffid = createFFIDClient({
|
|
423
|
+
serviceCode: 'flow-board-ai',
|
|
424
|
+
scope: '',
|
|
425
|
+
authMode: 'service-key',
|
|
426
|
+
serviceApiKey: process.env.FFID_SERVICE_API_KEY!,
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
const addResult = await ffid.addMember({
|
|
430
|
+
organizationId,
|
|
431
|
+
email: 'new-member@example.com',
|
|
432
|
+
role: 'member',
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
if (addResult.error) {
|
|
436
|
+
throw new Error(addResult.error.message)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
await ffid.updateMemberRole({
|
|
440
|
+
organizationId,
|
|
441
|
+
userId: addResult.data.member.userId,
|
|
442
|
+
role: 'admin',
|
|
443
|
+
})
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
- `addMember` は既存 FFID ユーザーを active member として追加します。招待メール送信や token 発行は行いません。
|
|
447
|
+
- `role` は `admin` / `member` / `viewer` のみ指定可能です。`owner` は追加・昇格 API では扱いません。
|
|
448
|
+
- agency client と同じ呼び味が必要な場合は `ffid.addMember(organizationId, { email, role })` も使えます。
|
|
449
|
+
|
|
450
|
+
### Organization profile / public logo wall
|
|
451
|
+
|
|
452
|
+
hub 等の外部サービスが企業プロフィールを登録・照会し、FeelFlow 全サービス共通のロゴウォールを表示するためのメソッド(#5485 / #5547)。
|
|
453
|
+
|
|
454
|
+
- **`getOrganizationProfile` / `upsertOrganizationProfile` / `uploadOrganizationLogo`** は **`authMode: 'service-key'` 専用**(scope: `organization:read` / `organization:write`)。`token` / `cookie` モードで呼ぶとサーバーへ往復せず `VALIDATION_ERROR`。
|
|
455
|
+
- **`getPublicLogoWall`** は認証不要。service-key を送らない。エンドポイントは `GET /api/v1/organizations/logo-wall`(公開 API なので SDK を使わず直接 GET してもよいが、型付きエラーが欲しければ SDK 経由を推奨)。
|
|
456
|
+
- `upsert` の `listingConsent.consented: true` では `consentVersion` と `consentedByUserId` が必須。同意操作者が対象 org の active メンバーでないとサーバーが `CONSENTED_BY_NOT_MEMBER` を返す。
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
import {
|
|
460
|
+
createFFIDClient,
|
|
461
|
+
FFID_ORGANIZATION_PROFILE_ERROR_CODES,
|
|
462
|
+
} from '@feelflow/ffid-sdk/server'
|
|
463
|
+
|
|
464
|
+
const ffid = createFFIDClient({
|
|
465
|
+
serviceCode: 'feel-agent',
|
|
466
|
+
scope: '',
|
|
467
|
+
authMode: 'service-key',
|
|
468
|
+
serviceApiKey: process.env.FFID_SERVICE_API_KEY!,
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
const current = await ffid.getOrganizationProfile(organizationId)
|
|
472
|
+
if (current.error) throw new Error(current.error.message)
|
|
473
|
+
|
|
474
|
+
const upsert = await ffid.upsertOrganizationProfile(organizationId, {
|
|
475
|
+
displayName: '株式会社サンプル',
|
|
476
|
+
industry: 'it-software',
|
|
477
|
+
companySize: '11-50',
|
|
478
|
+
websiteUrl: 'https://example.co.jp',
|
|
479
|
+
listingConsent: {
|
|
480
|
+
consented: true,
|
|
481
|
+
consentVersion: 'v1.0',
|
|
482
|
+
consentedByUserId: userId,
|
|
483
|
+
},
|
|
484
|
+
})
|
|
485
|
+
if (upsert.error?.code === FFID_ORGANIZATION_PROFILE_ERROR_CODES.CONSENTED_BY_NOT_MEMBER) {
|
|
486
|
+
throw new Error(upsert.error.message)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
await ffid.uploadOrganizationLogo(organizationId, logoFile)
|
|
490
|
+
|
|
491
|
+
const wall = await ffid.getPublicLogoWall()
|
|
492
|
+
// wall.data.entries: { displayName, logoUrl, websiteUrl }[]
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
位置引数と params オブジェクトの両方を受け付ける(`addMember` と同じ)。
|
|
496
|
+
|
|
497
|
+
### Provisioning(users / organizations)
|
|
498
|
+
|
|
499
|
+
外部サービスからの一括移行向けに、**service-key 認証**でユーザー・組織を冪等にプロビジョニングするメソッド。REST を直叩きせず `createFFIDClient` 経由で呼べる(SDK-first、#3790 / #4127)。
|
|
500
|
+
|
|
501
|
+
- **`authMode: 'service-key'`(`X-Service-Api-Key`)専用**。`token`(Bearer)/ `cookie` モードで呼ぶとサーバーへ往復せず即座に `VALIDATION_ERROR` を返す(サーバー側エンドポイントが service-key 認証のみを受け付けるため)。
|
|
502
|
+
- 必要 scope: `provisionUser` → `user:provision`、`provisionOrganization` → `organization:write`。
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
import { createFFIDClient } from '@feelflow/ffid-sdk/server'
|
|
506
|
+
|
|
507
|
+
const ffid = createFFIDClient({
|
|
508
|
+
serviceCode: 'praxis',
|
|
509
|
+
scope: '',
|
|
510
|
+
authMode: 'service-key',
|
|
511
|
+
serviceApiKey: process.env.FFID_SERVICE_API_KEY!,
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
// 1. ユーザーを冪等に作成(新規なら created:true / 既存なら created:false)
|
|
515
|
+
const userRes = await ffid.provisionUser({
|
|
516
|
+
email: 'owner@example.com',
|
|
517
|
+
profile: { displayName: '山田 太郎' },
|
|
518
|
+
})
|
|
519
|
+
if (userRes.error) throw new Error(userRes.error.message)
|
|
520
|
+
if (!userRes.data.dryRun) {
|
|
521
|
+
console.log(userRes.data.created ? '新規作成' : '既存ユーザー', userRes.data.user.id)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// 2. その owner の組織を作成し、メンバーを冪等に追加
|
|
525
|
+
const orgRes = await ffid.provisionOrganization({
|
|
526
|
+
name: 'Example 組織',
|
|
527
|
+
ownerEmail: 'owner@example.com',
|
|
528
|
+
members: [
|
|
529
|
+
{ email: 'member1@example.com', role: 'member' },
|
|
530
|
+
{ email: 'member2@example.com', role: 'viewer' },
|
|
531
|
+
],
|
|
532
|
+
})
|
|
533
|
+
if (orgRes.error) {
|
|
534
|
+
// owner が未登録なら error.code === 'OWNER_NOT_FOUND'(先に provisionUser が必要)
|
|
535
|
+
throw new Error(orgRes.error.message)
|
|
536
|
+
}
|
|
537
|
+
if (!orgRes.data.dryRun) {
|
|
538
|
+
for (const m of orgRes.data.members) {
|
|
539
|
+
console.log(m.email, m.status) // 'added' | 'already_member' | 'user_not_found'
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
#### 冪等セマンティクス
|
|
545
|
+
|
|
546
|
+
- `provisionUser`: **既存メールは `created: false`**(HTTP 200、no-op)、新規メールは `created: true`(HTTP 201、パスワードレス・メール確認済み)。メールはサーバー側で normalize(trim + lowercase)されるため、大文字小文字違いの再送でも同一ユーザーに解決する。新規作成で `profile` を渡した場合のみ `profileWritten` が付き、`false` は「ユーザーは作成されたがプロフィール書き込みに失敗」→ 再送を推奨。
|
|
547
|
+
- `provisionOrganization`: owner が同名の組織を既に持つ場合は `created: false`(HTTP 200)。`members` は冪等に追加され、各要素の `status` が `added` / `already_member` / `user_not_found` を返す。
|
|
548
|
+
|
|
549
|
+
#### dryRun
|
|
550
|
+
|
|
551
|
+
`dryRun: true` を渡すと **一切書き込まず**、実行時に何が起きるかだけを返す。レスポンスは `dryRun` 判別のタグ付きユニオン。
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
const preview = await ffid.provisionUser({ email: 'owner@example.com', dryRun: true })
|
|
555
|
+
if (!preview.error && preview.data.dryRun) {
|
|
556
|
+
console.log(preview.data.wouldCreate) // true = 実行すれば新規作成される
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const orgPreview = await ffid.provisionOrganization({
|
|
560
|
+
name: 'Example 組織',
|
|
561
|
+
ownerEmail: 'owner@example.com',
|
|
562
|
+
members: [{ email: 'member1@example.com', role: 'member' }],
|
|
563
|
+
dryRun: true,
|
|
564
|
+
})
|
|
565
|
+
if (!orgPreview.error && orgPreview.data.dryRun) {
|
|
566
|
+
console.log(orgPreview.data.wouldCreate) // 組織を新規作成するか
|
|
567
|
+
// per-member plan status: 'would_add' | 'already_member' | 'user_not_found'
|
|
568
|
+
orgPreview.data.members.forEach((m) => console.log(m.email, m.status))
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
`if (res.data.dryRun)` で TypeScript が型を絞り込むため、dry-run 分岐では `wouldCreate`、実行分岐では `created` がそれぞれ型安全に参照できる。
|
|
573
|
+
|
|
574
|
+
### getProfile() / updateProfile()
|
|
575
|
+
|
|
576
|
+
ログイン中ユーザー自身のプロフィールを取得・更新するメソッド(`createFFIDClient` から呼び出し)。
|
|
577
|
+
|
|
578
|
+
- エンドポイント: `GET /api/v1/users/ext/me` / `PUT /api/v1/users/ext/me`
|
|
579
|
+
- 対応 authMode: `token`(Bearer)のみが SDK 経由で成立する
|
|
580
|
+
- **cookie モードは非対応** — ext エンドポイントはクロスオリジン用途のため、Bearer token または `X-Service-Api-Key` のどちらかが必須。FFID 自身の UI(同一オリジン)は従来通り `/api/v1/users/me` を使う
|
|
581
|
+
- **service-key モードも SDK 経由では非対応** — API Key 認証時はバックエンドが `?userId=<uuid>` クエリを要求するが、`getProfile()` / `updateProfile()` は自分自身(ambient user)を前提とするため、`userId` を受け取らない。サーバー間で任意ユーザーのプロフィールを操作したい場合は `fetchWithAuth` で直接エンドポイントを叩いてください
|
|
582
|
+
- 外部サービス(hub 等)のフロントエンドから token モードで呼ぶのが想定される主要パターン
|
|
583
|
+
|
|
584
|
+
```tsx
|
|
585
|
+
import { createFFIDClient } from '@feelflow/ffid-sdk'
|
|
586
|
+
|
|
587
|
+
const client = createFFIDClient({
|
|
588
|
+
serviceCode: 'hub',
|
|
589
|
+
authMode: 'token',
|
|
590
|
+
apiBaseUrl: 'https://id.feelflow.net',
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
// 取得
|
|
594
|
+
const { data: profile, error } = await client.getProfile()
|
|
595
|
+
if (error) {
|
|
596
|
+
console.error('プロフィール取得失敗:', error.message)
|
|
597
|
+
} else {
|
|
598
|
+
console.log(profile.email, profile.displayName, profile.timezone)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// 更新(部分更新 — 渡したフィールドだけ差し替え)
|
|
602
|
+
const { data: updated, error: updateError } = await client.updateProfile({
|
|
603
|
+
displayName: '山田 太郎',
|
|
604
|
+
timezone: 'Asia/Tokyo',
|
|
605
|
+
locale: 'ja',
|
|
606
|
+
preferences: { theme: 'dark' },
|
|
607
|
+
})
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
`updateProfile` に空オブジェクト `{}` を渡すと `VALIDATION_ERROR` が返ります(無意味なラウンドトリップを防止)。
|
|
611
|
+
|
|
612
|
+
#### フィールドのクリア(null を渡す)
|
|
613
|
+
|
|
614
|
+
対応する optional フィールドに `null` を渡すと、FFID backend 側で該当カラムを
|
|
615
|
+
SQL NULL にクリアします(v2.16.0〜 / #2354)。個人プロフィールの
|
|
616
|
+
`companyName` は廃止予定で、旧クライアントからの値は受理しても無視します。
|
|
617
|
+
|
|
618
|
+
```tsx
|
|
619
|
+
// 部署をクリア
|
|
620
|
+
await client.updateProfile({
|
|
621
|
+
department: null,
|
|
622
|
+
})
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
値のセマンティクス:
|
|
626
|
+
|
|
627
|
+
| 渡す値 | FFID backend の挙動 |
|
|
628
|
+
| --- | --- |
|
|
629
|
+
| `undefined` / キー未指定 | 未変更(partial update) |
|
|
630
|
+
| `null` | クリア(SQL NULL を書き込む) |
|
|
631
|
+
| `""`(空文字列) | 空文字リテラルをそのまま保存(`null` 扱いには**ならない**) |
|
|
632
|
+
|
|
633
|
+
対応フィールド: `displayName` / `phone` / `department` / `jobTitle` / `preferences`。`timezone` / `locale` は application-level invariant(サーバー側 normalization が string 前提)のため null 非許容。クリアは不可 — キー未指定で現状維持、もしくは新しい有効な値を渡す。
|
|
634
|
+
|
|
635
|
+
### getAnalyticsConfig() (#2347)
|
|
636
|
+
|
|
637
|
+
外部サービスが自身に割り当てられた GA4 Measurement ID を取得するメソッド。
|
|
638
|
+
|
|
639
|
+
- エンドポイント: `GET /api/v1/ext/analytics/config?service=<code>`
|
|
640
|
+
- 必要 scope: `analytics:read`(Bearer / API Key 両方で enforce)
|
|
641
|
+
- レスポンス: `FFIDAnalyticsConfig` (`{ code, measurementId, displayName, isActive }`)
|
|
642
|
+
- **archived service** (`isActive: false`) でも 200 を返す — caller が次回 deploy で tracking 停止判断する間、in-flight events が 5xx を起こさないように measurementId は引き続き返す
|
|
643
|
+
|
|
644
|
+
```ts
|
|
645
|
+
import { createFFIDClient } from '@feelflow/ffid-sdk/server'
|
|
646
|
+
|
|
647
|
+
const client = createFFIDClient({
|
|
648
|
+
serviceCode: 'flow-board-ai',
|
|
649
|
+
authMode: 'service-key',
|
|
650
|
+
serviceApiKey: process.env.FFID_SERVICE_API_KEY!,
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
const result = await client.getAnalyticsConfig('feel-agent-ai')
|
|
654
|
+
if (result.error) {
|
|
655
|
+
if (result.error.code === 'SERVICE_NOT_FOUND') {
|
|
656
|
+
// 該当 GA4 stream がまだ sync されていない / typo
|
|
657
|
+
} else {
|
|
658
|
+
console.error(`[FFID] analytics config fetch failed: ${result.error.code}`)
|
|
659
|
+
}
|
|
660
|
+
} else if (result.data.isActive) {
|
|
661
|
+
// GA4 タグを描画して events を送信
|
|
662
|
+
loadGA4Script(result.data.measurementId)
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
エラーコード:
|
|
667
|
+
- `VALIDATION_ERROR` — `serviceCode` が空 / kebab-case 形式違反(SDK 側 pre-validate)
|
|
668
|
+
- `INSUFFICIENT_SCOPE` (403) — `analytics:read` scope なし
|
|
669
|
+
- `SERVICE_NOT_FOUND` (404) — DB に未登録の service code
|
|
670
|
+
- `INVALID_PARAM` (400) — server 側で形式違反を検出(pre-validate 通過後の boundary)
|
|
671
|
+
|
|
672
|
+
## 代理店の招待リンク(`?invite=`)
|
|
673
|
+
|
|
674
|
+
代理店の招待リンクから来た顧客が FFID で登録すると、作られた組織がその代理店に紐付きます。SDK の役割は**トークンを signup URL まで運ぶこと**だけで、紐付けの実務は FFID 側が行います。
|
|
675
|
+
|
|
676
|
+
> 代理店ポータルが発行するリンクは FFID の origin(`https://id.feelflow.net/signup?invite=…`)を指します。サービスサイトを着地点にする形(`https://<あなたのサービス>/?invite=…`)は、代理店がトークンを自分のキャンペーン URL に貼って成立させる導線です。この節が効くのはその形です。
|
|
677
|
+
|
|
678
|
+
### React 利用者(`FFIDProvider` を mount している場合)
|
|
679
|
+
|
|
680
|
+
**配線は不要です。** provider が mount 時に着地 URL の `invite` を捕捉し、first-party cookie へ保存します。`getSignupUrl()` が自動でトークンを載せます。
|
|
681
|
+
|
|
682
|
+
ただし provider は捕捉結果を返さないので、**cookie の保持に失敗したことを利用者側で観測する経路はありません**(debug ログには落ちます)。検知したい場合は provider に加えて自分で `captureAgencyInviteToken()` を呼び、戻り値の `persisted` を見てください。
|
|
683
|
+
|
|
684
|
+
### React 以外 / provider を使わない場合
|
|
685
|
+
|
|
686
|
+
`captureAgencyInviteToken()` を**ページ遷移ごとに**呼びます。
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
import { captureAgencyInviteToken } from '@feelflow/ffid-sdk'
|
|
690
|
+
|
|
691
|
+
// ページ読み込みごと(SPA ならルート遷移ごと)に呼ぶ
|
|
692
|
+
const { token, source, persisted } = captureAgencyInviteToken()
|
|
693
|
+
|
|
694
|
+
if (persisted === false) {
|
|
695
|
+
// cookie がブラウザに落とされた。今回の URL 経由では紐付くが、
|
|
696
|
+
// パラメータの無い別ページへ回遊すると失われる。
|
|
697
|
+
} else if (persisted === undefined && source === 'url') {
|
|
698
|
+
// 書ける環境が無かった(SSR)。ブラウザ側で捕捉し直す必要がある。
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
呼ばなかった場合は「着地ページで即 signup した人だけ紐付く」まで**劣化します**(壊れるのではなく、下記の回遊経路を救えなくなります)。
|
|
703
|
+
|
|
704
|
+
### なぜページ遷移ごとに呼ぶ必要があるのか
|
|
705
|
+
|
|
706
|
+
実際の導線は次の形です。
|
|
707
|
+
|
|
708
|
+
```
|
|
709
|
+
/?invite=abc に着地 → 料金ページを見る → /pricing で「登録」を押す
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
`getSignupUrl()` が呼ばれる時点の URL には `invite` がありません。着地時点で捕捉して保持する以外にこの経路を救う方法がなく、**SDK 自身はページ読み込みを hook できない**ため、起動点は利用者側にあります。
|
|
713
|
+
|
|
714
|
+
### ⚠️ 効かない経路: SSR(Server Component)で signup URL を組む場合
|
|
715
|
+
|
|
716
|
+
サーバーには `window.location.search` も `document.cookie` もありません。`getSignupUrl()` をそのまま呼ぶとトークンは載らないので、呼び出し側が読んだ値を渡してください(渡さない場合はプロセスあたり 1 度 warn します)。
|
|
717
|
+
|
|
718
|
+
```typescript
|
|
719
|
+
// Server Component
|
|
720
|
+
import { cookies } from 'next/headers'
|
|
721
|
+
import { AGENCY_INVITE_COOKIE_NAME } from '@feelflow/ffid-sdk'
|
|
722
|
+
|
|
723
|
+
const inviteToken = (await cookies()).get(AGENCY_INVITE_COOKIE_NAME)?.value
|
|
724
|
+
const href = ffid.getSignupUrl(undefined, { inviteToken })
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### `authMode: 'token'`(OAuth モード)は v7.4.0 で対応済み
|
|
728
|
+
|
|
729
|
+
この設定では signup 意図が `/api/v1/oauth/authorize` を経由するため `getSignupUrl()` を通りません。**v7.4.0 以降は `redirectToAuthorize` が `invite` を authorize URL へ転送し、FFID 側が `/signup` へ引き継ぎます**(`redirectToLogin({ screenHint: 'signup' })` は token モードでは `redirectToAuthorize` へ委譲されるので、こちらも同様に載ります)。配線は不要です。
|
|
730
|
+
|
|
731
|
+
v7.3.x までは token モードでトークンが FFID に 1 度も届かず、紐付けが無言で落ちていました。**v7.4.0 未満を使っている場合は、SDK と FFID 本体の両方を更新してください** — 転送は SDK が `invite` を送り、FFID が `/signup` へ stamp する 2 段で成立します。
|
|
732
|
+
|
|
733
|
+
⚠️ 転送の gate は `screenHint === 'signup'` です。`prompt` に `'create'` を指定して signup 画面へ向かう経路は SDK の `FFIDPrompt`(`'select_account' | 'login'`)が受け付けないため到達しません。
|
|
734
|
+
|
|
735
|
+
### cookie の仕様
|
|
736
|
+
|
|
737
|
+
| 項目 | 値 |
|
|
738
|
+
|---|---|
|
|
739
|
+
| 名前 | `ffid_invite`(first-party・consumer 自身のホスト) |
|
|
740
|
+
| 有効期限 | 30 日 |
|
|
741
|
+
| `Path` | `/` |
|
|
742
|
+
| `SameSite` | `Lax` |
|
|
743
|
+
| `Secure` | `NODE_ENV === 'production'` のときだけ(`options.secure` で上書き可) |
|
|
744
|
+
| `Domain` | 既定は現在のホストのみ(`options.domain` で指定可) |
|
|
745
|
+
|
|
746
|
+
`Secure` を本番限定にしているのは、**https でない独自ドメイン**(LAN IP や plain http の検証機)でブラウザが `Secure` cookie を黙って捨てるためです。`http://localhost` は secure context として扱われるので `Secure` でも保持されます(ローカル開発では上書き不要)。
|
|
747
|
+
|
|
748
|
+
URL と cookie の両方にトークンがある場合は **URL が勝ち、cookie も更新されます**(最後に踏んだリンクが勝つ)。
|
|
749
|
+
|
|
750
|
+
**削除には書き込み時と同じ `path` / `domain` / `secure` が必要です。** ブラウザが一致を要求するため、既定以外で書いた cookie は既定の `clearAgencyInviteToken()` では消えません。
|
|
751
|
+
|
|
752
|
+
### ⚠️ 保持期間 30 日の副作用
|
|
753
|
+
|
|
754
|
+
招待リンクは既定で使用回数が無制限です。したがって保持中の cookie は、**同じブラウザで後から作られた組織も同じ代理店に紐付けます**。共有ブラウザではこれが誤紐付けになります。
|
|
755
|
+
|
|
756
|
+
SDK は登録完了時に cookie を消しません(登録完了は FFID のドメインで起こるため consumer から検知しにくい)。単回にしたい場合は、登録完了を検知できる地点で明示的に消してください。
|
|
757
|
+
|
|
758
|
+
```typescript
|
|
759
|
+
import { clearAgencyInviteToken } from '@feelflow/ffid-sdk'
|
|
760
|
+
|
|
761
|
+
clearAgencyInviteToken() // 削除が read-back で確認できたら true / SSR では undefined
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### ⚠️ トークンをログに出さないこと
|
|
765
|
+
|
|
766
|
+
このトークンは URL に載る**平文の資格情報**で、**単回使用ではありません**(招待リンクの使用回数上限は既定で無制限)。漏れたトークンは、新規組織が作られる限り何度でも紐付けに使えます。
|
|
767
|
+
|
|
768
|
+
SDK 内部ではログ・エラーレポート・アナリティクスに値を出しません。`getSignupUrl()` の出力を自前でログや解析イベントに送っている場合は、`redactAgencyInviteToken()` を通してください。
|
|
769
|
+
|
|
770
|
+
```typescript
|
|
771
|
+
import { redactAgencyInviteToken } from '@feelflow/ffid-sdk'
|
|
772
|
+
|
|
773
|
+
logger.info('signup url', redactAgencyInviteToken(ffid.getSignupUrl()))
|
|
774
|
+
// → https://id.feelflow.net/signup?redirect=...&service=svc&invite=[REDACTED]
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
素の `invite=` だけでなく、`redirect` パラメータに着地 URL が percent-encode されて入る**入れ子**(`redirect=…%3Finvite%3D…`)も潰します。覆う encode の深さは **2 段まで**です。GA4 の `page_location` のように URL を丸ごと送るイベントでも同じ経路を通してください。
|
|
778
|
+
|
|
779
|
+
**login URL にも `redirect` 経由で入れ子で載ります。** 専用の `&invite=` は付きませんが(紐付けは新規登録時の組織作成に対して起こるため)、着地 URL が `?invite=…` なら login URL の `redirect` の中に入ります。login URL をログに出す場合も redact してください。
|
|
780
|
+
|
|
781
|
+
## 型定義
|
|
782
|
+
|
|
783
|
+
```typescript
|
|
784
|
+
interface FFIDUser {
|
|
785
|
+
id: string
|
|
786
|
+
email: string
|
|
787
|
+
displayName: string | null
|
|
788
|
+
avatarUrl: string | null
|
|
789
|
+
locale: string | null
|
|
790
|
+
timezone: string | null
|
|
791
|
+
createdAt: string
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
interface FFIDOrganization {
|
|
795
|
+
id: string
|
|
796
|
+
name: string
|
|
797
|
+
slug: string
|
|
798
|
+
role: 'owner' | 'admin' | 'member'
|
|
799
|
+
status: 'active' | 'invited' | 'suspended'
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
interface FFIDSubscription {
|
|
803
|
+
id: string
|
|
804
|
+
serviceCode: string
|
|
805
|
+
serviceName: string
|
|
806
|
+
planCode: string
|
|
807
|
+
planName: string
|
|
808
|
+
status: 'trialing' | 'active' | 'past_due' | 'canceled' | 'paused'
|
|
809
|
+
currentPeriodEnd: string | null
|
|
810
|
+
}
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
### OAuth userinfo の契約要約
|
|
814
|
+
|
|
815
|
+
token mode では SDK は `/api/v1/oauth/userinfo` を呼び出し、基本プロフィールに加えてサービス契約の要約を受け取ります。
|
|
816
|
+
この要約により、追加 API を呼ばずにプラン判定や UI 分岐を行えます。
|
|
817
|
+
|
|
818
|
+
```ts
|
|
819
|
+
interface FFIDOAuthUserInfoSubscription {
|
|
820
|
+
subscriptionId: string | null
|
|
821
|
+
status: 'trialing' | 'active' | 'past_due' | 'canceled' | 'paused' | null
|
|
822
|
+
planCode: string | null
|
|
823
|
+
seatModel: 'organization' | null
|
|
824
|
+
memberRole: 'owner' | 'admin' | 'member' | 'viewer' | null
|
|
825
|
+
organizationId: string | null
|
|
826
|
+
}
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
`seatModel` はシートモデル識別用であり、organization と role は userinfo の解決済み組織文脈として扱います。
|
|
830
|
+
|
|
831
|
+
## React 以外の環境で使う
|
|
832
|
+
|
|
833
|
+
本 SDK は React/Next.js 向けに設計されていますが、一部のモジュールはフレームワーク非依存で利用できます。
|
|
834
|
+
|
|
835
|
+
### サーバーサイドモジュール(React 依存なし)
|
|
836
|
+
|
|
837
|
+
以下の subpath exports は React に一切依存しません。Node.js、Deno、Bun 等で即座に利用できます。
|
|
838
|
+
|
|
839
|
+
```typescript
|
|
840
|
+
// 利用規約・法的文書
|
|
841
|
+
import { createFFIDLegalClient } from '@feelflow/ffid-sdk/legal'
|
|
842
|
+
|
|
843
|
+
// Agency(代理店)管理
|
|
844
|
+
import { createFFIDAgencyClient } from '@feelflow/ffid-sdk/agency'
|
|
845
|
+
|
|
846
|
+
// お知らせ取得
|
|
847
|
+
import { createFFIDAnnouncementsClient } from '@feelflow/ffid-sdk/announcements'
|
|
848
|
+
|
|
849
|
+
// Webhook 署名検証・ハンドラー(※ Node.js crypto が必要)
|
|
850
|
+
import { createFFIDWebhookHandler, verifyWebhookSignature } from '@feelflow/ffid-sdk/webhooks'
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
> **Note**: `webhooks` モジュールは Node.js の `crypto` モジュールと `Buffer` を使用します。Cloudflare Workers で利用する場合は [`nodejs_compat` 互換フラグ](https://developers.cloudflare.com/workers/runtime-apis/nodejs/)を有効にしてください。
|
|
854
|
+
|
|
855
|
+
これらのモジュールは独立した subpath export として公開されているため、メインエントリ (`@feelflow/ffid-sdk`) を経由せず、React の依存が伝播しません。
|
|
856
|
+
|
|
857
|
+
### `createFFIDClient` を非 React 環境で使う
|
|
858
|
+
|
|
859
|
+
非 React 環境では、可能な限り上記の subpath exports(`/legal`、`/webhooks` 等)の個別クライアントを使用してください。メインエントリの `createFFIDClient` を使う必要がある場合、`createFFIDClient` 自体は React を使用しませんが、メインエントリに含まれるため **bundler 環境では React を external に指定する**必要があります。
|
|
860
|
+
|
|
861
|
+
#### Cloudflare Workers
|
|
862
|
+
|
|
863
|
+
ビルドコマンドで `--external` を指定するか、カスタム esbuild 設定で `external` を設定してください。
|
|
864
|
+
|
|
865
|
+
```bash
|
|
866
|
+
# wrangler のビルドコマンド例
|
|
867
|
+
esbuild src/index.ts --bundle --format=esm --external:react --external:react-dom
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
#### Vue / Nuxt(Vite)
|
|
871
|
+
|
|
872
|
+
```typescript
|
|
873
|
+
// vite.config.ts
|
|
874
|
+
export default defineConfig({
|
|
875
|
+
build: {
|
|
876
|
+
rollupOptions: {
|
|
877
|
+
external: ['react', 'react-dom'],
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
})
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
#### esbuild
|
|
884
|
+
|
|
885
|
+
```bash
|
|
886
|
+
esbuild src/index.ts --bundle --external:react --external:react-dom
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
#### webpack
|
|
890
|
+
|
|
891
|
+
```javascript
|
|
892
|
+
// webpack.config.js
|
|
893
|
+
module.exports = {
|
|
894
|
+
externals: {
|
|
895
|
+
react: 'react',
|
|
896
|
+
'react-dom': 'react-dom',
|
|
897
|
+
},
|
|
898
|
+
}
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
> **Note**: サーバーサイドで bundler を使わずに実行する場合、**subpath exports(`@feelflow/ffid-sdk/legal` 等)を使用すれば** external 設定なしで React がインストールされていなくても動作します。メインエントリ(`@feelflow/ffid-sdk`)を ESM で import する場合は、モジュールグラフが静的に解決されるため React が必要です。
|
|
902
|
+
|
|
903
|
+
### `peerDependencies` は optional です
|
|
904
|
+
|
|
905
|
+
SDK の `package.json` で `react` / `react-dom` は `optional: true` に設定済みです(利用者側での設定は不要)。React をインストールしなくても `npm install` 時に warning は発生しません。
|
|
906
|
+
|
|
907
|
+
```json
|
|
908
|
+
// SDK の package.json に設定済み(参考)
|
|
909
|
+
{
|
|
910
|
+
"peerDependenciesMeta": {
|
|
911
|
+
"react": { "optional": true },
|
|
912
|
+
"react-dom": { "optional": true }
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
## E2E テストモード(`@feelflow/ffid-sdk/server/test`)
|
|
918
|
+
|
|
919
|
+
> **⚠️ SECURITY NOTICE** — テストモードは Bearer トークン検証を **意図的にバイパス** する仕組みです。本番環境で誤って有効化すると、登録されたバイパストークンを持つ任意のリクエストが認証通過します。
|
|
920
|
+
|
|
921
|
+
各サービスで E2E テストを書く際、`verifyAccessToken` の introspect 呼び出しをモックする実装を独自に持つ必要がなくなります。SDK 側で **多重 production guard 付き** の bypass クライアントを提供します。
|
|
922
|
+
|
|
923
|
+
```ts
|
|
924
|
+
import { createFFIDClient } from '@feelflow/ffid-sdk/server'
|
|
925
|
+
import { createTestFFIDClient } from '@feelflow/ffid-sdk/server/test'
|
|
926
|
+
|
|
927
|
+
const isE2E =
|
|
928
|
+
process.env.NODE_ENV !== 'production' &&
|
|
929
|
+
process.env.FFID_TEST_MODE === 'true'
|
|
930
|
+
|
|
931
|
+
const client = isE2E
|
|
932
|
+
? createTestFFIDClient({
|
|
933
|
+
users: [
|
|
934
|
+
{
|
|
935
|
+
bypassToken: process.env.E2E_TEST_BYPASS_SECRET!,
|
|
936
|
+
userInfo: {
|
|
937
|
+
sub: 'e2e-test-sub',
|
|
938
|
+
email: 'e2e@example.com',
|
|
939
|
+
name: 'E2E Test User',
|
|
940
|
+
picture: null,
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
],
|
|
944
|
+
})
|
|
945
|
+
: createFFIDClient({ /* normal production options */ })
|
|
946
|
+
|
|
947
|
+
const result = await client.verifyAccessToken(bearerToken)
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
### Built-in production guards (defense-in-depth)
|
|
951
|
+
|
|
952
|
+
- `NODE_ENV` を **trim + lowercase** 後に比較。`"production "`(改行混入)や `"Production"` も production として扱う(Vercel 環境変数の copy/paste 事故対策)
|
|
953
|
+
- `process.env` を露出しない runtime(Edge / Cloudflare Workers / browser)では **fail-close** で構築拒否
|
|
954
|
+
- `bypassToken` の重複検知 / 空チェック / `userInfo.sub` 必須チェック(構築時 throw)
|
|
955
|
+
- 未登録 token は **fail-close**(実 introspect への暗黙 fallthrough は行わない)
|
|
956
|
+
- 構築時点で `users` のスナップショットを取り、入力配列の post-construction mutation は無視
|
|
957
|
+
- 各 `verifyAccessToken` 呼び出しは新しいオブジェクトを返却(caller mutation が後続呼び出しを汚染しない)
|
|
958
|
+
|
|
959
|
+
### `allowInProduction` escape hatch
|
|
960
|
+
|
|
961
|
+
staging が `NODE_ENV=production` をミラーするケース等のみ、明示的な ack 文字列で有効化できます:
|
|
962
|
+
|
|
963
|
+
```ts
|
|
964
|
+
import {
|
|
965
|
+
createTestFFIDClient,
|
|
966
|
+
TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK,
|
|
967
|
+
} from '@feelflow/ffid-sdk/server/test'
|
|
968
|
+
|
|
969
|
+
createTestFFIDClient({
|
|
970
|
+
users: [...],
|
|
971
|
+
allowInProduction: TEST_CLIENT_ALLOW_IN_PRODUCTION_ACK,
|
|
972
|
+
})
|
|
973
|
+
// → process.emitWarning(..., 'FFIDTestModeInProduction') を毎構築時に発火
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
`boolean` ではなく **literal string ack** を要求する型なので、`allowInProduction: someBooleanFlag` のような誤代入はコンパイルエラーになります。ack 文字列は grep 可能で監査も容易です。
|
|
977
|
+
|
|
978
|
+
### サブパス分離
|
|
979
|
+
|
|
980
|
+
`createTestFFIDClient` は **`@feelflow/ffid-sdk/server/test` からのみ** import 可能です(`@feelflow/ffid-sdk/server` にも main entry にも含まれない)。本番コードからの誤 import は ESLint の `no-restricted-imports` 等で検知することを推奨します:
|
|
981
|
+
|
|
982
|
+
```js
|
|
983
|
+
// eslint.config.mjs (flat config)
|
|
984
|
+
import { defineConfig } from 'eslint/config'
|
|
985
|
+
|
|
986
|
+
export default defineConfig([
|
|
987
|
+
{
|
|
988
|
+
files: ['src/**/*.{ts,tsx}'], // production code only
|
|
989
|
+
ignores: ['**/__tests__/**', 'tests/e2e/**'],
|
|
990
|
+
rules: {
|
|
991
|
+
'no-restricted-imports': ['error', {
|
|
992
|
+
patterns: ['@feelflow/ffid-sdk/server/test'],
|
|
993
|
+
}],
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
])
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
## 環境変数
|
|
1000
|
+
|
|
1001
|
+
オプションで環境変数を使用してデフォルト設定を上書きできます:
|
|
1002
|
+
|
|
1003
|
+
```bash
|
|
1004
|
+
NEXT_PUBLIC_FFID_API_URL=https://id.feelflow.net
|
|
1005
|
+
NEXT_PUBLIC_FFID_SERVICE_CODE=chatbot
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
## ライセンス
|
|
1009
|
+
|
|
1010
|
+
MIT
|