@feelflow/ffid-sdk 9.0.0 → 10.1.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
@@ -1,5 +1,8 @@
1
1
  # @feelflow/ffid-sdk
2
2
 
3
+ <!-- markdownlint の既定からの差分(理由): MD013 = 日本語本文・表・コードを含む公開文書で、折り返せる語境界がなく、表と URL は折り返せない -->
4
+ <!-- markdownlint-configure-file {"MD013": false} -->
5
+
3
6
  [![npm version](https://img.shields.io/npm/v/@feelflow/ffid-sdk.svg)](https://www.npmjs.com/package/@feelflow/ffid-sdk)
4
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
8
 
@@ -331,6 +334,38 @@ export default function Layout({ children }: { children: React.ReactNode }) {
331
334
 
332
335
  **詳細な実装レシピ**(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
336
 
337
+ ### ログインを拒否されたとき(`?error=access_denied` と `denial_reason`、v10.1.0)
338
+
339
+ 組織に席が無い・人数枠が満員などで FFID がログインを拒否すると、通常はまず FFID の案内ページ(席が無いときは `/subscriptions/seat-required`)が表示されます。そこから「サービスへ戻る」を押したとき、または `prompt=none` で authorize したときに、サービスの `redirect_uri` へ `code` の代わりに `?error=access_denied&denial_reason=<理由>&state=...` が返ります(FFID [oauth.md「契約・席が無いとき」](https://github.com/feel-flow/feelflow-id-platform/blob/develop/docs/04-api/oauth.md))。
340
+
341
+ - token モードの `FFIDProvider` は、これを `useFFID().error` と `onError` に出します(`state` を照合した後。一致しなければ `STATE_MISMATCH_ERROR`)
342
+ - 理由は `getFFIDAuthorizationError(error)` で読みます。`denialReason` は `no_membership` / `no_active_subscription` / `no_seat_assignment` / `subscription_expired` / `member_limit_reached`(`FFIDOAuthDenialReason`)か、理由が無い・未知のときは `null`
343
+ - SDK が URL から外すのは使用済みの `state` だけです。`error` / `denial_reason` は残るので、URL を自分で読んでいる実装もそのまま動きます
344
+ - **このエラーで `login()` を自動で呼び直さないでください。** FFID は同じ理由で拒否します。案内を出し、利用者の操作で再試行させます
345
+
346
+ ```tsx
347
+ 'use client'
348
+ import { useFFID, getFFIDAuthorizationError } from '@feelflow/ffid-sdk'
349
+
350
+ export function LoginDeniedNotice() {
351
+ const { error } = useFFID()
352
+ const denial = getFFIDAuthorizationError(error)
353
+ if (!denial) return null
354
+ switch (denial.denialReason) {
355
+ case 'no_seat_assignment':
356
+ case 'member_limit_reached':
357
+ return <p>このサービスの利用者に追加されていません。組織のオーナー / 管理者に追加を依頼してください。</p>
358
+ case 'subscription_expired':
359
+ case 'no_active_subscription':
360
+ return <p>組織の契約が有効ではありません。</p>
361
+ default:
362
+ return <p>ログインできませんでした。</p>
363
+ }
364
+ }
365
+ ```
366
+
367
+ サーバー(BFF)の `createServerAuthClient().handleCallback()` も、拒否のときは `error.code` に OAuth の `error`(`access_denied` など)を入れ、同じ `getFFIDAuthorizationError(result.error)` で理由を読めます(`@feelflow/ffid-sdk/server` から import)。
368
+
334
369
  ### Server-side service access decision
335
370
 
336
371
  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 を再実装しない。
@@ -361,6 +396,56 @@ if (error || !access?.hasAccess) {
361
396
  - `effectiveStatus`: `active` / `past_due_grace` / `blocked` / `canceled` / `trial_expired` / `expired`。
362
397
  - `gracePeriodEndsAt`: `past_due_grace` が `blocked` に変わる時刻。表示・警告用であり、アクセス判定の source of truth にしない。
363
398
  - `failPolicy`: 現在は `failClosed` 固定。FFID に到達できない、または canonical decision を取得できない場合は `result.error` ではなく `data.hasAccess=false` / `denialReason='ffid_unreachable'` / `data.error` を返す。呼び出し側は `result.error` だけで通過判定しない。
399
+ - `denialReason` には `member_limit_reached`(10.0.0)も含まれる。flat プランの人数枠が満員で席を付けられない利用者は、ログイン時に OAuth の `denial_reason: 'member_limit_reached'` で止まる。`checkServiceAccess()` 自体はこの値を返さず、人数枠・クレジット残高は `hasAccess` に影響しない。`memberLimitReached` / `credits` / `pricingModel` は情報として decision に載る(古い FFID は返さない)。
400
+
401
+ ### Credits(flat プラン、v10.0.0)
402
+
403
+ `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)。
404
+
405
+ ```ts
406
+ import { createFFIDClient, FFID_CREDITS_ERROR_CODES, type FFIDCreditsInsufficientDetails } from '@feelflow/ffid-sdk'
407
+
408
+ // サービスのバックエンド(consume は service-key モード専用)
409
+ const ffid = createFFIDClient({
410
+ serviceCode: 'chatbot-v2',
411
+ scope: '',
412
+ authMode: 'service-key',
413
+ serviceApiKey: process.env.FFID_SERVICE_API_KEY!,
414
+ })
415
+
416
+ const { data, error } = await ffid.credits.consume({
417
+ subscriptionId,
418
+ amount: 50,
419
+ idempotencyKey: `chat.reply:${requestId}`, // AI 実行 1 回につき 1 つ。再試行でも同じキー
420
+ feature: 'chat.reply',
421
+ metadata: { model: 'example', tokens: 1200 }, // 個人情報は入れない
422
+ })
423
+
424
+ if (error?.code === FFID_CREDITS_ERROR_CODES.CREDITS_INSUFFICIENT) {
425
+ const details = error.details as unknown as FFIDCreditsInsufficientDetails
426
+ // 自動で再試行しない。購入導線(owner / admin)か管理者への依頼を出す
427
+ return { blocked: true, available: details.available }
428
+ }
429
+ if (error) {
430
+ // 5xx / NETWORK_ERROR は同じキー・同じ amount で再試行してよい(duplicate が返る)
431
+ throw new Error(error.message)
432
+ }
433
+ // data.status は 'consumed' | 'duplicate'
434
+ ```
435
+
436
+ | メソッド | 認証モード | 用途 |
437
+ | --- | --- | --- |
438
+ | `credits.consume(params)` | service-key のみ | AI 実行 1 回ぶんの消費を報告 |
439
+ | `credits.getBalance(subscriptionId)` | token / service-key | 残高・当期・内訳・`isLow` / `isExhausted` |
440
+ | `credits.listHistory(subscriptionId, { cursor?, limit?, type? })` | token / service-key | 履歴(新しい順) |
441
+ | `credits.listPacks(serviceCode?)` | token / service-key | 購入できるパック |
442
+ | `credits.createPackCheckout(params)` | token のみ(owner / admin) | パック購入の Stripe Checkout URL |
443
+ | `credits.getBuyCreditsUrl({ subscriptionId, orgId? })` / `redirectToBuyCredits` | どれでも | FFID の契約詳細で購入ダイアログを開く URL(UUID か `ffid:<uuid>`。`sub_...` は不可) |
444
+ | `getSeats(subscriptionId)` | token / service-key | 人数枠(`maxMembers` / `assignedMembers` / `availableMembers`、`null` = 無制限) |
445
+
446
+ - AI 機能のゲートは **残高**(`consume` の結果・`getBalance()`・`checkServiceAccess()` の `credits`)で行い、`hasAccess` では行わない。クレジットが 0 でも `hasAccess` は `true` のまま。
447
+ - `consume` を service-key 以外で、`createPackCheckout` を token 以外で呼ぶと、ネットワークに出る前に `FORBIDDEN`(`details.reason: 'api_key_required'` / `'bearer_required'`)が返る。
448
+ - 残高の変化は webhook `credits.granted` / `credits.low` / `credits.exhausted` / `credits.pack_purchased` でも届く(`@feelflow/ffid-sdk/webhooks`)。
364
449
 
365
450
  ### Organization member management
366
451
 
@@ -614,6 +699,7 @@ if (result.error) {
614
699
  ```
615
700
 
616
701
  エラーコード:
702
+
617
703
  - `VALIDATION_ERROR` — `serviceCode` が空 / kebab-case 形式違反(SDK 側 pre-validate)
618
704
  - `INSUFFICIENT_SCOPE` (403) — `analytics:read` scope なし
619
705
  - `SERVICE_NOT_FOUND` (404) — DB に未登録の service code
@@ -655,7 +741,7 @@ if (persisted === false) {
655
741
 
656
742
  実際の導線は次の形です。
657
743
 
658
- ```
744
+ ```text
659
745
  /?invite=abc に着地 → 料金ページを見る → /pricing で「登録」を押す
660
746
  ```
661
747
 
@@ -685,7 +771,7 @@ v7.3.x までは token モードでトークンが FFID に 1 度も届かず、
685
771
  ### cookie の仕様
686
772
 
687
773
  | 項目 | 値 |
688
- |---|---|
774
+ | --- | --- |
689
775
  | 名前 | `ffid_invite`(first-party・consumer 自身のホスト) |
690
776
  | 有効期限 | 30 日 |
691
777
  | `Path` | `/` |