descope-byos-builder について
descope-byos-builderは、Descopeのフローエンジンを維持しながら、エクスポートしたflow JSONからReact BYOS画面を構築するためのガイドです。画面やサブフローの解析、UI実装、認証フローの検証と、よくある失敗への対処を扱います。
主な機能
- 01エクスポートしたflow JSONの解析
- 02画面とインタラクションの対応付け
- 03React BYOS画面の構築
- 04サブフローと画面名の競合の追跡
- 05認証フローと一般的な問題の検証
活用シーン
- 01Descopeフロー向けのカスタムUI構築
- 02フロー変更後のBYOS更新
- 03認証状態や画面ハンドラーの問題調査
- 04カスタムUIとホスト画面の動作確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ descope/ skills --skill descope-byos-builderインストール後、Claude Code で/descope-byos-builderを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
parse-flow.mjsreferences/byos-component-patterns.mdgotchas.md詳細
Descope BYOS ビルダー
Descope フローの JSON エクスポートを、state.next(interactionId, form) を呼び出す実用的な React BYOS 画面に変換します。以下の失敗例は実際の BYOS セッションで記録されたもので、どれも初回は15〜60分のロスにつながりました。
使用する場面
- Descope のフローエンジンを維持しながら、Descope フロー上にカスタム UI を構築する場合(クライアント側での JWT 解析は不要で、フローのロジックもすべて維持されます)
- Descope コンソールで基盤となるフローが変更された後に、既存の BYOS 実装を変更する場合
- 「フローは完了するのにセッションの状態が正しくない」「ボタンを押しても何も起きない」「セッションが匿名のまま」「パスキーの認証手続きが中断される」といった症状をデバッグする場合
- 既存の BYOS がホスト画面と同等の動作をしているか監査する場合
logged-inの後に実行される認証後の昇格サブフロー(例:add-passkeys)を追加する場合
次の場合は使用しないでください: ホストされた <Descope flowId=... /> ウィジェットだけでフローが完全に動作する場合(BYOS はトレードオフがあり、コードを変更しない限りフローの編集が反映されなくなります)。
絶対に守るルール
すべての BYOS コンポーネントは、エクスポートされたフロー JSON を根拠にしてください。 インタラクション ID、出力キー名、画面名を推測しないでください。カタログにあるすべての失敗は、もっともらしく見える値を誰かが勝手に作ったことに端を発しています。
必要な入力(まずユーザーに確認してください)
このスキルでは Descope コンソールからフローを取得できません。エージェントにコンソールへのアクセス権がないためです。作業を始める前に、以下をユーザーに提供するよう依頼してください。
- フロー JSON ファイル — メインフローと、それが呼び出すすべてのサブフロー(LoadSubflow
arguments.flowId)の両方について、Descope コンソールからエクスポートしたもの。認証後の昇格サブフロー(例:add-passkeys)も含みます。ファイルパスまたは貼り付けた JSON のどちらでも構いません。 - プロジェクト ID と ベース URL(未設定の場合)—
AuthProvider/ SDK の初期化に必要です。 - React アプリのマウントポイント — BYOS のエントリーポイントをレンダリングするファイルまたはコンポーネントです。
- 既存の BYOS コード(変更する場合)— 現在の画面コンポーネントと画面マップへのパスです。
ユーザーがメインフローの JSON しか提供しない場合は、コンポーネントを生成する前に作業を止めて、サブフローを依頼してください。サブフローの JSON が不足していることが、実行時に [byos] no handler for screen "..." エラーが発生する最大の原因です。
ワークフロー
-
ユーザーからフロー JSON を集めます。 エージェントは Descope コンソールにアクセスできないため、ユーザーがエクスポートして貼り付けるか、パスを指定する必要があります。メインフローと、それが呼び出すすべてのサブフロー(
arguments.flowIdを持つLoadSubflowアクション)を用意してください。これには、logged-inアクションの後に実行される認証後の権限昇格サブフロー(例:add-passkeys)も含まれます。実行時にはすでにユーザーが認証済みのため、見落としやすいので注意してください。メインフローしか提供されていない場合は、フロー内のLoadSubflowアクションをスキャンし、先に各サブフローをエクスポートするようユーザーに依頼してください。サブフローの JSON が不足していると、画面を検出できず、実行時に[byos] no handler for screen …が発生します。 -
このディレクトリにある
parse-flow.mjsで解析します。node parse-flow.mjs <path/to/flow.json>を実行すると、すべての画面タスクについて、screenName、allInputKeys、contextKeys、次のルール(interactionId → taskId)、UI ノードの概要(入力のname属性、ボタンのラベル)、サブフローの呼び出しが出力されます。 -
画面名のマップを作成します。 すべてのフローを通じて、一意な画面名ごとに React コンポーネントを 1 つ作成します。名前の衝突に注意してください。複数のタスクで同じ名前が使われることがよくあります(例:「Welcome Screen」がメール入力とパスワード入力の両方に使われる)。名前が衝突する場合は、
state.context.form.*のヒューリスティックに基づいて振り分けるルーターコンポーネントを作成してください(「注意点」→「画面名の衝突」を参照)。 -
各コンポーネントを作成します。 すべての BYOS 画面で行うことは、次の 4 つです。
- 表示用にコンテキストを読み取る:
state.context.sentTo.maskedEmail、state.context.form.emailなど - 入力を
formに集める(setForm({ ...form, key: value })を使用) - ボタンのクリック時に
state.next(interactionId, payload)を呼び出す - Descope がエラーを報告したときに
state.error?.textを表示する
- 表示用にコンテキストを読み取る:
-
フローを接続します。 Descope React SDK は、BYOS のエントリポイントとして
@descope/react-sdkの<Descope flowId="..." onScreenUpdate={handler} />を提供します。onScreenUpdateは(screenName, state, next)を受け取り、対応する画面コンポーネントに処理を振り分けます。よく使われるパターンは、byosScreensマップを受け取り、内部で振り分けを行う薄いFlowOrByosラッパーを作成することですが、FlowOrByosは SDK のエクスポートではなく、自分で作成する必要があります。エントリポイントにインスタンスを 1 つマウントしてください。onSuccessでは、認証状態のキャッシュを無効化してください(「注意点」→「セッションの固定化」を参照)。 -
エンドツーエンドで検証します。 フローがサポートするすべてのユーザージャーニーを確認してください。コンソールで
[byos] no handler for screen "..."が表示される画面は、マップに登録されていません。
重要なルール
注記: 以下のルールのうち、いくつか(ネストされた
<form>タグの動作、WebAuthn のセレモニーの所有者、ctxKeyによる事前入力、componentsConditionsのフィールド名、E.164 形式以外の番号のサイレントな拒否)は、実際の BYOS セッションから経験的に導き出したもので、Descope の公式ドキュメントには明記されていません。ただし、本番環境で検証され、フローの JSON 構造との照合も済んでいます。
- フォームのキーは入力ノードの
nameプロパティと一致します。allInputKeysやinputsMetadata.keyではありません。Task 40 の Set Password 入力はname="newPassword"ですが、allInputKeys: ["newPassword_noPolicyOverrides"]です。{ newPassword: "..." }を送信してください。 state.nextのペイロードには、現在の画面の出力だけを送信してください。 サブフローの境界を越えて累積したフォーム全体を展開すると、コンテキストが汚染され、気づかないまま失敗することがあります。<form>タグをネストしないでください。 Descope のウェブコンポーネントは子要素を<form>でラップします。<div>とonClick、onKeyDown={makeEnterHandler(submit)}を使用してください。- OAuth ボタンでは、インタラクション ID だけでなく、ペイロードに
providerを設定してください({ provider: 'google' })。 - 電話番号は E.164 形式にする必要があります。 E.164 形式でない番号は SMS コネクターによってサイレントに拒否されます。フローは完了しますが、セッションは作成されません。
onSuccessで認証キャッシュを無効化してください。 セッションのアップグレード(匿名 → 検証済み)では同じsubが再利用されることがあり、userId をキーとするキャッシュは自動更新されません。- 画面 JSON から
componentsConditionsを読み取り、表示/非表示のルールを反映してください(例:unauthUser.verifiedPhoneが false の場合、「コードでサインイン」を非表示にします)。 - WebAuthn フロー:セレモニーは SDK が実行します。 インタラクションが WebAuthn アクションタスク(
webauthn-update-user-start/finish、Sign Up or In / Passkeys)にルーティングされたら、state.next(interactionId)を呼び出すだけにしてください。BYOS からnavigator.credentials.create/getを呼び出してはいけません。@simplewebauthn/browser、startAuthentication、startRegistration、または WebAuthn ヘルパーライブラリをインポートしてはいけません。Descope SDK がセレモニーの両方の段階をすでに実行します。SDK はonScreenUpdateでアクションを検知し、セレモニーを自ら実行します。キャンセルやエラーはstate.error.textに表示されます。 - 事前入力には入力の
ctxKeyを読み取ってください。 入力ノードにprops.ctxKey="someKey"がある場合は、useEffectでコンテキスト値をキーにして初回レンダリング時にstate.context[someKey]をform[name]に設定してください(ローカルのフィールドが空の場合のみ設定します)。 Device Not Supportedの分岐を反映してください。 WebAuthn フローでは、deviceInfo.webAuthnSupportに基づいて分岐することがよくあります。非対応の分岐は実際の画面タスクです。BYOS コンポーネントを用意しないと、プロモーションの途中でホスト型ウィジェットが表示されます。
共有画面名のヒューリスティック
2 つのタスクで同じ画面名を使用している場合、曖昧さを解消する手がかりを推測してはいけません。それぞれの経路をたどり、「フォームフィールドの蓄積」テーブルを作成する必要があります。
手順(必須 — 省略すると、気づきにくいバグが発生します):
- 重複する画面名を使用する各タスクについて、親フローのエントリーポイントに到達するまで
next.rulesをさかのぼります。 - 各経路について、途中にあるすべての画面タスクと、その
allInputKeysを列挙します。これらが、その経路でユーザーが入力するフォームフィールドです。 - ちょうど一方の経路だけで値が設定されるフォームフィールドを見つけます。
- ヒューリスティックとして
Boolean(state?.context?.form?.<that field>)を使用します。
**よくある間違い:**両方の経路を最後までたどらなかったため、両方の経路で値が設定されるフィールドを選んでしまうことです。
以下の例は特定のフローセットに基づいています。タスク ID、画面名、サブフロー名は異なります。これらは手法を示すものであり、必ず自分のフローに対して
parse-flow.mjsを実行してください。例:「Verify OTP」が
sign-in-sms-otpとprogressive-profile-smsで共有されている場合、form.phoneがよさそうに見えます。しかし、どちらのフローでも、「Verify OTP」に到達する前に電話番号入力画面で電話番号を収集しています。代わりにform.passwordを選んでください。sign-in-sms-otpの経路は、form.passwordに値を書き込む親フローのパスワード画面から開始されます。一方、progressive-profile-smsの経路はマジックリンクの後から始まり、パスワードは入力されていません。
参考用のヒューリスティック(このスキルの元になったフローセットの例であり、すべてのケースに当てはまるわけではありません):
| 重複 | この画面の前に経路 A で収集する項目 | この画面の前に経路 B で収集する項目 | 手がかり |
|---|---|---|---|
| "Welcome Screen"(メールアドレス入力 vs パスワード入力) | — | email | state.context.form.email |
| "Verify OTP"(sign-in-sms-otp vs progressive-profile-sms) | email、password、phone | email、phone | state.context.form.password(phone ではありません。両方の経路で収集されます!) |
3 つの経路が重複する例(メインフローと 2 つのサブフローで共有される Magic Link Sent):
| 経路 | この画面の前に収集する項目 | |
|---|---|---|
| メインフロー | email | パスワードなし |
| 新規ユーザーのサブフロー | email、password、fullName | パスワードと fullName |
| 既存ユーザーのサブフロー | email、password | パスワードあり、fullName なし |
述語:supportsChooseOther = !hasPassword || hasFullName。
選択したヒューリスティックと完全なトレースを、ルーターコンポーネントの先頭に記録してください。Descope コンソールで画面名を変更できる場合は、変更を依頼してください。ヒューリスティックよりも、一意の名前を使うほうが常に優れています。
**実際に成功したケース:**2 つの User Information タスク(verify-email-magic-link の既存ユーザー用と新規ユーザー用の分岐)は、コンソールで User Information - Unverified - Email Only と User Information - Unverified - Email and Name に変更されました(ヒューリスティックでは fullName の出力有無を判定できました)。名前の変更はより簡単で、わかりやすく、フローを編集しても影響を受けません。基本的には名前を変更してください。
注意点のリファレンス
失敗事例の完全な一覧(問題 → 症状 → 修正方法):references/gotchas.md を参照してください。気づかない失敗の原因を BYOS コードのせいにする前に、このファイルを確認してください。「動かない」という症状の多くは、既知の注意点に該当します。
検証
フローがサポートするユーザージャーニーを、それぞれエンドツーエンドでテストしてください。
- フロー JSON では、メインフローが
task Nから開始し、action=logged-inのtask Mで終了していますか? - フロー内のすべての画面タスクに BYOS コンポーネントがあるか、ホスト型にフォールバックする判断が明示されていますか?
onSuccessが呼び出され、アプリは想定される認証済み状態に到達していますか(未認証のまま止まっていませんか)?onErrorが呼び出され、Descope のエラーメッセージが表示されますか(何も表示されずに失敗していませんか)?- 認証後のプロモーション用サブフロー(パスキーへの移行など)を、
Device Not Supported分岐を含めてエンドツーエンドで確認しましたか? - WebAuthn 画面では
state.next(interaction)のみが呼び出されていますか。BYOS コードでnavigator.credentials.*を呼び出していませんか?
一連のユーザージャーニー全体を実行した際に、コンソールに [byos] no handler for screen "..." の警告が表示されないことを確認してください。
パーサー
parse-flow.mjs(このディレクトリ内)— Node スクリプトです。入力:フロー JSON のパス(複数可)。出力:各フローについて、画面タスク、その入力、終了時のインタラクション、UI ノード名、サブフローローダーを示す概要テーブルです。実行して出力を画面マップのコメントヘッダーに貼り付ければ、BYOS コンポーネントに必要なすべての定数の信頼できる情報源になります。
注意すべき点
次のような状況に気づいたら:
- フロー JSON で確認していないインタラクション ID をハードコーディングしている — 中断して、まず解析してください
- サブフロー境界をまたいで
{ ...form }をstate.nextに展開している — 出力だけを送信してください - 各フローのルールを確認せず、すべての「OTP を検証」バリエーションに同じボタンを追加している — コンテキストに応じて制御してください
- BYOS コンポーネント内に
<form>タグを書いている —<div>を使ってください onSuccessで認証キャッシュを無効化せずにnavigate()を呼び出している — 先に無効化してください- BYOS のクリックハンドラーから
navigator.credentials.create/getを呼び出している — SDK が実行するため、インタラクションを呼び出すだけで十分です @simplewebauthn/browserや WebAuthn ヘルパーをインポートしている — 同じ落とし穴です。サードパーティライブラリ版であり、SDK が実行します- 「その時点ではユーザーはすでにログインしているから」とサブフローのエクスポートを省略している — 認証後の昇格サブフローは実際の画面をレンダリングします
いずれの場合も、作業を止めて references/gotchas.md を読み直し、フロー JSON と照合してください。
参考資料
references/byos-component-patterns.md— 推奨されるコードパターン:コアの接続(onScreenUpdate→ByosState)、画面ルーター、画面のひな形、メール、OTP、電話、OAuth、衝突ルーター、ctxKey の事前入力に関する完全な例です。セットアップを始めるときは、まずこちらを確認してください。references/gotchas.md— 問題の一覧:実際に発生した BYOS の失敗モードを 19 件、それぞれ症状 → 根本原因 → 修正方法の順で掲載しています。リリース前チェックリストも含まれています。parse-flow.mjs— Node パーサー:node parse-flow.mjs <flow.json> [more.json ...]。画面タスク、実際のフォームキーのnameプロパティ、インタラクション ID、サブフローローダー、衝突に関する警告を出力します。コンポーネントを書く前に実行してください。