@aiquants/auth-react-router 0.15.0 → 0.16.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 +100 -7
- package/dist/admin.d.mts +13 -436
- package/dist/admin.d.ts +13 -436
- package/dist/admin.js +1 -2
- package/dist/admin.mjs +1 -2
- package/dist/client.d.mts +1 -10
- package/dist/client.d.ts +1 -10
- package/dist/client.js +0 -1
- package/dist/client.mjs +0 -1
- package/dist/server.d.mts +6 -146
- package/dist/server.d.ts +6 -146
- package/dist/server.js +0 -1
- package/dist/server.mjs +0 -1
- package/package.json +12 -16
- package/dist/admin.js.map +0 -1
- package/dist/admin.mjs.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/client.mjs.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/server.mjs.map +0 -1
- package/src/admin/app.ts +0 -9
- package/src/admin/format.ts +0 -58
- package/src/admin/labels.ts +0 -838
- package/src/admin/server.ts +0 -1143
- package/src/admin/ui.tsx +0 -276
- package/src/admin/views.tsx +0 -2120
- package/src/admin.ts +0 -5
- package/src/client/GoogleForm.tsx +0 -47
- package/src/client.ts +0 -6
- package/src/server/auth-server.ts +0 -69
- package/src/server/authenticate-in-loader.ts +0 -243
- package/src/server/authenticate.ts +0 -224
- package/src/server/google-strategy-impl.ts +0 -154
- package/src/server/google-strategy.ts +0 -183
- package/src/server/handlers.ts +0 -139
- package/src/server/logger.ts +0 -14
- package/src/server/ports.ts +0 -113
- package/src/server/session.ts +0 -132
- package/src/server.ts +0 -14
- package/src/styles/standalone.entry.css +0 -6
package/dist/admin.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/admin/labels.ts","../src/admin/server.ts","../src/admin/format.ts","../src/admin/ui.tsx","../src/admin/views.tsx"],"sourcesContent":["/**\n * Labels and i18n strings for the user administration UI.\n * ユーザー管理 UI で使用するラベルと i18n 文字列の定義。\n *\n * パッケージ既定は **英語** ([defaultUserAdminLabels])。日本語で使う場合はアプリ側が\n * [jaUserAdminLabels] を明示的に注入する (認可管理 `@aiquants/authz-react-router` と同じ規約)。\n * UI 文字列はすべて本モジュールに集約し、コンポーネント側へ直書きしないこと — 直書きすると\n * 日本語以外の利用者がその箇所を差し替えられなくなる。\n */\n\nexport type UserAdminLabels = {\n title: string\n /**\n * BCP 47 locale used to format dates. 日付整形に使う BCP 47 ロケール。\n *\n * 実行環境任せにするとサーバーとブラウザで別の文字列になり、水和が壊れる。\n */\n locale: string\n /** IANA time zone used to format dates. 日付整形に使う IANA タイムゾーン。 */\n timeZone: string\n /**\n * Strings shared by more than one view. 複数のビューが共有する文言。\n *\n * ビューごとに同じ語を持たせると、ある画面の「削除」だけを言い換えることが **できなく\n * なる** (実装がどのビューのキーを読んでいるかは呼び出し側から見えない)。共有する語は\n * ここ 1 か所に置く。\n */\n common: {\n create: string\n edit: string\n delete: string\n save: string\n cancel: string\n actions: string\n status: string\n id: string\n /** 変更不能な供給元 (環境変数など) 由来の行に出す表示。 */\n readOnly: string\n /** 上流所有のグループに出す編集不可の表示。 */\n managedUpstream: string\n /** `#{id}` の表示。記号の位置は言語で変わる。 */\n idTemplate: string\n /** 値が無いことの表示 (説明欄など)。 */\n none: string\n }\n /** アクションが拒否・失敗したときの文言。 */\n errors: {\n /** 一覧に無い上流キーを指定した。`{key}` に受け取った値。 */\n unknownUpstream: string\n /** その上流グループは既に別のローカルグループへ連携している。`{key}` / `{group}`。 */\n alreadyLinked: string\n /** ディレクトリ未設定の配備で連携を求められたときの説明。 */\n directoryDisabled: string\n /** 必須欄が欠けている、または読めない値だったことの説明。 */\n invalidField: string\n /**\n * Shown for an intent this screen does not perform. この画面が行わない intent への文言。\n *\n * ⚠️ 受け取った値を含めない。認可より手前で返るため、認証を通っていない相手にも届く。\n */\n unknownIntent: string\n /**\n * Operator-facing names for the form fields. 欄の、操作者に見える名前。\n *\n * ⚠️ `invalidField` の `{field}` に **通信上のキー** を差し込まない。日本語の文の中に\n * `membershipMode` のような識別子が現れ、画面のどの欄を指すのか操作者には判らない。\n * 表に無い欄はキーのまま出す (出さないより、判りにくくても出すほうがよい)。\n */\n fieldNames: Record<string, string>\n /** 許可リストの種別が既定の 2 値でなかったことの説明。 */\n invalidAllowlistType: string\n /** 想定外の失敗 (原文はサーバーのログにだけ残す)。 */\n unexpected: string\n /**\n * Shown when the actor lacks permission for the attempted operation.\n * 操作の権限が無いときに出す文言。\n *\n * ガードの例外メッセージをそのまま返してはならない。テナント ID・アプリキー・\n * 拒否理由といった内部の語彙が、権限を持たない相手にそのまま渡る。\n */\n forbidden: string\n /**\n * Shown when the request body exceeds the ceiling. 本文が上限を越えたときの文言。\n *\n * この画面が送るのは短い文字列だけなので、越えるのは誤操作か攻撃である。どちらにも\n * 同じ文言を返す (どこで弾かれたかを細かく教えない)。\n */\n requestTooLarge: string\n }\n /** シェル (タブ枠) の見出し。 */\n heading: string\n /** シェルが描く自己権限バッジの文言。 */\n permissions: {\n label: string\n canWrite: string\n canRead: string\n /** 削除だけを持つ主体の表示 (読み書きが無いことを「権限なし」と描かない)。 */\n canDelete: string\n none: string\n /** 未結線・取得失敗・付与ゼロのいずれも「不明」であり、許可ではない。 */\n unknown: string\n }\n tabs: {\n users: string\n groups: string\n groupMembers: string\n allowlist: string\n directory: string\n }\n usersView: {\n title: string\n /** 並べ替えの読み上げ名。`{column}` は列名。 */\n sortBy: string\n searchPlaceholder: string\n /** 検索欄の読み上げ名。プレースホルダは入力すると消えるため名前にならない。 */\n searchLabel: string\n addUser: string\n editUser: string\n displayName: string\n email: string\n active: string\n inactive: string\n activate: string\n deactivate: string\n /** 行ごとの読み上げ名。`{user}` を利用者名へ置換する。 */\n editUserAria: string\n activateAria: string\n deactivateAria: string\n /** 無効化はサインインを止める。確認を取る。 */\n /** 検索で 0 件になったときの文言 (「登録が無い」と区別する)。 */\n emptyFiltered: string\n /** 一覧の絞り込み結果を読み上げるための文言。 */\n listSummary: string\n /** 昇順で並べ替え中の列の読み上げ名。 */\n sortedAscending: string\n /** 降順で並べ替え中の列の読み上げ名。 */\n sortedDescending: string\n confirmDeactivate: string\n empty: string\n }\n groupsView: {\n /** 検索語に一致しないときの説明。空状態と混ぜると「消えた」と誤読される。 */\n emptyFiltered: string\n /** 作成方法の切替。ローカルに作るか、上流のグループから作るか。 */\n /** 作成方法を選ぶ操作群の読み上げ名。 */\n createModeLabel: string\n createModeLocal: string\n createModeUpstream: string\n /** 上流グループを選ぶ欄。 */\n pickUpstream: string\n pickUpstreamPlaceholder: string\n /** 選択肢 1 件の副題。`{count}` はメンバー数。 */\n upstreamOptionMembers: string\n /** すでに連携済みの選択肢に付ける表示。 */\n upstreamAlreadyLinked: string\n /** 候補一覧が引けなかったときの説明。`{reason}` に理由。 */\n /** 候補は引けたが、この配備の連携プロバイダ名が決まっていないことの説明。 */\n providerUnset: string\n /** 候補を選ぶまで作成できないことの説明 (押せないボタンの理由)。 */\n pickUpstreamFirst: string\n /**\n * Shown when every upstream group is already linked. 上流のすべてが連携済みのときの文言。\n *\n * ⚠️ この場面で「先に選べ」と言ってはならない。従いようのない指示になる。\n */\n allUpstreamLinked: string\n /** 上流を読めたが 1 件も無かったことの説明。 */\n catalogEmpty: string\n catalogUnavailable: string\n /** 上流へ届かないときの案内 (作成対話向け — この対話にアドレス欄は無い)。 */\n catalogUnavailableHere: string\n /** 候補一覧をいつ観測したか。 */\n catalogObservedAt: string\n /** 観測が配備の許す鮮度を越えている旨。 */\n catalogStale: string\n /** 候補一覧を引けなかった理由の文言 (上流の例外文は使わない)。 */\n catalogFailures: {\n denied: string\n rateLimited: string\n upstreamError: string\n neverObserved: string\n unknown: string\n }\n /** 上流から作る場合の説明 (何が起きるかを先に伝える)。 */\n createUpstreamNotice: string\n /** グループのメンバー面へ移動する操作。 */\n openMembers: string\n openMembersAria: string\n title: string\n searchPlaceholder: string\n searchLabel: string\n addGroup: string\n editGroup: string\n groupKey: string\n name: string\n description: string\n /** 所属人数。`{count}` を人数へ置換する。 */\n memberCountTemplate: string\n confirmDelete: string\n /** 連携付きで作る前の確認。`{group}` / `{upstream}`。委譲は元に戻しづらい。 */\n confirmCreateLinked: string\n /** 行ごとの読み上げ名。`{group}` をグループ名へ置換する。 */\n editGroupAria: string\n deleteGroupAria: string\n noDescription: string\n groupKeyPlaceholder: string\n namePlaceholder: string\n empty: string\n }\n groupMembersView: {\n title: string\n /** 利用者の絞り込み。数百人の一覧を目で追わせない。 */\n searchLabel: string\n searchPlaceholder: string\n /** 所属している人だけに絞る切替。`{count}` は該当数。 */\n onlyMembers: string\n /** 絞り込み結果が空のときの説明。 */\n emptyFiltered: string\n /** 一覧の要約。`{shown}` / `{total}` / `{members}`。 */\n summary: string\n selectGroup: string\n selectPrompt: string\n /** グループが 1 つも無い配備での説明 (「選べ」は従えない指示になる)。 */\n noGroups: string\n /** 所属だけに絞った結果 0 件になったときの説明。 */\n emptyNoMembers: string\n /** 利用者が 1 人も登録されていない配備での説明。 */\n noUsers: string\n /** 深いリンクが指すグループが見つからず、代替を出していることの説明。 */\n deepLinkMissed: string\n actionAdd: string\n actionRemove: string\n /** 行ごとの読み上げ名。`{user}` を利用者名へ置換する。 */\n actionAddAria: string\n actionRemoveAria: string\n /** 上流所有グループで編集を出さない理由。押せない理由を書かないと故障に見える。 */\n upstreamNotice: string\n /** 見出し。`{group}` をグループ名へ置換する。語順が言語で変わるため位置指定にする。 */\n membersOfTemplate: string\n groupKeyLabel: string\n }\n allowlistView: {\n title: string\n addEntry: string\n pattern: string\n type: string\n emailType: string\n domainType: string\n /** 一覧のバッジ表示 (短縮形)。 */\n emailTypeShort: string\n domainTypeShort: string\n /** グループ単位の許可。上流ディレクトリの所属で判定される。 */\n groupTypeShort: string\n /** 対象列の見出し。パターンとグループが混在するため「パターン」ではない。 */\n target: string\n description: string\n /** 配備全体が所有する行 (環境変数由来) の所有者表示。 */\n deploymentOwner: string\n /** 許可の取り消しはサインインを止める。確認を取る。 */\n confirmDelete: string\n /** 行ごとの読み上げ名。`{target}` を対象へ置換する。 */\n deleteAria: string\n empty: string\n }\n directoryView: {\n title: string\n membershipMode: string\n directMode: string\n transitiveMode: string\n lastSynced: string\n neverSynced: string\n /** 上流に居るがローカルユーザーへ解決できていない人数。`{count}` を人数へ置換する。 */\n unresolvedTemplate: string\n syncPaused: string\n syncEnabled: string\n syncNow: string\n syncQueued: string\n /** 同じ範囲の要求が既に待っていて、積まれなかったことの説明。 */\n syncAlreadyPending: string\n /** 連携は作れたが、その直後の同期要求を積めなかったときの告知。 */\n linkSyncNotQueued: string\n /** 同期が滞っていることの警告。停止はセキュリティ事象である。 */\n staleWarning: string\n disabledWarning: string\n /** 停止件数。`{paused}` / `{total}` を件数へ置換する。 */\n pausedTemplate: string\n /** 失敗したグループの列挙。`{groups}` を名前の並びへ置換する。 */\n erroredTemplate: string\n /** 失敗理由付きの 1 件。`{group}` / `{reason}`。 */\n erroredItemTemplate: string\n /** 列挙の区切り。言語で変わる。 */\n listSeparator: string\n /** 滞留警告に添える最終成功時刻。`{at}`。 */\n oldestSyncedTemplate: string\n /** 未処理の同期要求。`{count}`。 */\n pendingTemplate: string\n /** 連携先が 1 つも残っていないときの説明。 */\n nothingToLink: string\n /** グループが 1 つも無い配備での説明 (「全部が連携済み」と区別する)。 */\n noGroupsToLink: string\n /** 未結線の配備で操作を出さない理由。 */\n notWiredNotice: string\n /** 行ごとの読み上げ名。`{group}` をグループ名へ置換する。 */\n syncNowAria: string\n syncQueuedAria: string\n pauseAria: string\n resumeAria: string\n allowGroupAria: string\n disallowGroupAria: string\n unlinkAria: string\n /** グループ単位のサインイン許可を外す確認。 */\n confirmDisallow: string\n link: string\n unlink: string\n externalKey: string\n empty: string\n /** 連携するグループを選ぶ欄。 */\n selectGroup: string\n /** 選択欄が自前で持つ文言。渡さないと英語のまま出る。 */\n /** 選択欄の入力の手引き。 */\n pickUpstreamPlaceholder: string\n pickerNoOptions: string\n pickerClear: string\n pickerToggle: string\n /** 選択中の値の読み上げ。`{value}`。 */\n pickerSelected: string\n /** 候補件数の読み上げ。`{count}`。 */\n pickerAvailable: string\n /** 上流グループのアドレス入力。 */\n externalKeyPlaceholder: string\n /** 連携解除の確認。台帳と許可設定が消える。 */\n /** 連携を作る前の確認 (元へ戻すには連携解除という別の操作が要る)。 */\n confirmLink: string\n confirmUnlink: string\n /** 同期の一時停止・再開。 */\n pause: string\n resume: string\n /** 許可グループに加える / 外す。 */\n allowGroup: string\n disallowGroup: string\n /** グループ名と鍵の併記。`{name}` / `{key}`。 */\n groupOptionTemplate: string\n }\n}\n\n/**\n * Package default labels (English).\n * パッケージ既定ラベル (英語)。\n */\nexport const defaultUserAdminLabels: UserAdminLabels = {\n title: \"Users & Groups\",\n locale: \"en-US\",\n timeZone: \"UTC\",\n heading: \"\\u{1F464} User administration\",\n common: {\n create: \"Create\",\n edit: \"Edit\",\n delete: \"Delete\",\n save: \"Save\",\n cancel: \"Cancel\",\n actions: \"Actions\",\n status: \"Status\",\n id: \"ID\",\n readOnly: \"Read-only\",\n managedUpstream: \"Managed upstream\",\n idTemplate: \"#{id}\",\n none: \"-\",\n },\n errors: {\n unknownUpstream: \"No group named {key} exists upstream. Check the spelling, or pick one from the list.\",\n directoryDisabled: \"This deployment has no directory configured, so groups cannot be linked.\",\n invalidField: \"“{field}” is missing or not a value this operation accepts.\",\n unknownIntent: \"This screen does not perform that operation.\",\n fieldNames: {\n groupId: \"Group\",\n userId: \"User\",\n entryId: \"Entry\",\n displayName: \"Display name\",\n email: \"Email\",\n groupKey: \"Group key\",\n name: \"Name\",\n provider: \"Directory provider\",\n externalKey: \"Directory group address\",\n membershipMode: \"Membership expansion\",\n isActive: \"Active\",\n isSyncEnabled: \"Synchronization\",\n pattern: \"Pattern\",\n type: \"Type\",\n },\n invalidAllowlistType: \"The allowlist type must be either an address or a domain.\",\n unexpected: \"The operation failed. The reason is in the server log.\",\n alreadyLinked: \"{key} is already linked to {group}. One upstream group can back only one local group.\",\n forbidden: \"You do not have permission for this operation. The badge in the header shows what you currently hold; ask an administrator for the rest.\",\n requestTooLarge: \"The request was too large to process.\",\n },\n permissions: {\n label: \"Your permissions:\",\n canWrite: \"Editable\",\n canRead: \"Read only\",\n canDelete: \"Delete only\",\n none: \"No access\",\n unknown: \"Unknown\",\n },\n tabs: {\n users: \"\\u{1F464} Users\",\n groups: \"\\u{1F465} Groups\",\n groupMembers: \"\\u{1F517} Members\",\n allowlist: \"\\u2709\\uFE0F Allowlist\",\n directory: \"\\u{1F504} Directory\",\n },\n usersView: {\n title: \"Users\",\n sortBy: \"Sort by {column}\",\n searchPlaceholder: \"Search by name or email...\",\n searchLabel: \"Search users\",\n addUser: \"\\uFF0B Add user\",\n editUser: \"Edit user\",\n displayName: \"Name\",\n email: \"Email\",\n active: \"Active\",\n inactive: \"Inactive\",\n activate: \"Activate\",\n deactivate: \"Deactivate\",\n editUserAria: \"Edit {user}\",\n activateAria: \"Activate {user}\",\n deactivateAria: \"Deactivate {user}\",\n emptyFiltered: \"No user matches this search.\",\n listSummary: \"Showing {shown} of {total} users.\",\n sortedAscending: \"{column}, sorted ascending. Activate to sort descending.\",\n sortedDescending: \"{column}, sorted descending. Activate to sort ascending.\",\n confirmDeactivate: \"Deactivate {user}? They will no longer be able to sign in.\",\n empty: \"No user is registered yet.\",\n },\n groupsView: {\n emptyFiltered: \"No group matches this search\",\n createModeLabel: \"How to create the group\",\n createModeLocal: \"Create locally\",\n createModeUpstream: \"Create from a directory group\",\n pickUpstream: \"Directory group\",\n pickUpstreamPlaceholder: \"Search by name or address...\",\n upstreamOptionMembers: \"{count} members\",\n upstreamAlreadyLinked: \"already linked to {group}\",\n providerUnset: \"This deployment has no directory provider configured, so a directory group cannot be linked yet. Ask an administrator to set it.\",\n pickUpstreamFirst: \"Pick a directory group first.\",\n allUpstreamLinked: \"Every directory group is already linked to a local group. One upstream group can back only one local group, so unlink one first.\",\n catalogEmpty: \"The directory reported no groups as of {at}. Creating from a directory group is unavailable until one exists upstream.\",\n catalogUnavailable: \"The directory could not be reached, so the group list is unavailable. Enter the address by hand instead. ({reason})\",\n catalogUnavailableHere: \"The directory could not be reached, so creating from a directory group is unavailable. Create the group locally, then link it on the Directory tab. ({reason})\",\n catalogObservedAt: \"This list is the directory as of {at}.\",\n catalogStale: \"⚠ This list is the directory as of {at} and is overdue for a refresh — a group created since then will not appear.\",\n catalogFailures: {\n denied: \"the directory refused this deployment's credential\",\n rateLimited: \"the directory is rate-limiting this deployment\",\n upstreamError: \"the directory reported an internal error\",\n neverObserved: \"the directory has not been read yet — the synchronization job may not be running\",\n unknown: \"the reason is in the server log\",\n },\n createUpstreamNotice:\n \"Creates the group and links it in one step. The directory then owns its membership — and this screen can no longer rename, describe or delete the group. Unlinking is the only way back, and it removes the member ledger and the group-based sign-in permission.\",\n openMembers: \"Members\",\n openMembersAria: \"Show the members of {group}\",\n title: \"Groups\",\n searchPlaceholder: \"Search by group name or key...\",\n searchLabel: \"Search groups\",\n addGroup: \"\\uFF0B Add group\",\n editGroup: \"Edit group\",\n groupKey: \"Group key\",\n name: \"Name\",\n description: \"Description\",\n memberCountTemplate: \"{count} members\",\n confirmDelete: \"Delete {group}? Its {count} membership record(s) go with it, and anyone who could sign in only through this group will be locked out.\",\n confirmCreateLinked: \"Create {group} and hand its membership to {upstream}? Renaming and deleting it here becomes impossible.\",\n editGroupAria: \"Edit {group}\",\n deleteGroupAria: \"Delete {group}\",\n noDescription: \"No description\",\n groupKeyPlaceholder: \"e.g. dev-team\",\n namePlaceholder: \"e.g. Development team\",\n empty: \"No groups registered\",\n },\n groupMembersView: {\n title: \"Group members\",\n searchLabel: \"Search users\",\n searchPlaceholder: \"Search by name or email...\",\n onlyMembers: \"Members only ({count})\",\n emptyFiltered: \"No user matches this search\",\n summary: \"Showing {shown} of {total} users. This group has {members} member(s).\",\n selectGroup: \"Select a group\",\n selectPrompt: \"Select a group to manage its members\",\n noGroups: \"No group exists yet. Create one on the Groups tab first.\",\n emptyNoMembers: \"This group has no members yet. Untick “Members only” to add someone.\",\n noUsers: \"No user is registered yet. Add one on the Users tab first.\",\n deepLinkMissed: \"Group {id} was not found — it may have been deleted. Showing {group} instead.\",\n actionAdd: \"Add\",\n actionRemove: \"Remove\",\n actionAddAria: \"Add {user} to this group\",\n actionRemoveAria: \"Remove {user} from this group\",\n upstreamNotice: \"Membership of this group is owned by the upstream directory. Edit it there \\u2014 a local change would be reverted at the next synchronization.\",\n membersOfTemplate: \"Members of {group}\",\n groupKeyLabel: \"Group key\",\n },\n allowlistView: {\n title: \"Sign-in allowlist\",\n addEntry: \"\\uFF0B Add pattern\",\n pattern: \"Pattern (e.g. @example.com or user@example.com)\",\n type: \"Type\",\n emailType: \"Exact email match\",\n domainType: \"Domain match\",\n emailTypeShort: \"Email\",\n domainTypeShort: \"Domain\",\n groupTypeShort: \"Group\",\n target: \"Target\",\n description: \"Description\",\n deploymentOwner: \"Deployment-wide\",\n confirmDelete: \"Remove {target} from the sign-in allowlist? Anyone who could sign in only through it will be locked out.\",\n deleteAria: \"Remove the allowlist entry for {target}\",\n empty: \"No allowlist entries registered\",\n },\n directoryView: {\n title: \"Directory synchronization\",\n membershipMode: \"Membership\",\n directMode: \"Direct only\",\n transitiveMode: \"Nested expanded\",\n lastSynced: \"Last synced\",\n neverSynced: \"Never\",\n unresolvedTemplate: \"{count} unresolved\",\n syncPaused: \"Paused\",\n syncEnabled: \"Active\",\n syncNow: \"Sync now\",\n syncQueued: \"Queued\",\n syncAlreadyPending: \"A synchronization request for this scope is already waiting, so nothing was queued. If it never completes, the synchronization job may not be running.\",\n linkSyncNotQueued: 'The link was created, but a synchronization request could not be queued. Use \"Sync now\" on the Directory tab.',\n staleWarning: \"Synchronization is overdue. Until it runs, people removed upstream keep their access.\",\n disabledWarning: \"Directory synchronization is not wired for this deployment. Existing memberships remain in force and are no longer being refreshed.\",\n pausedTemplate: \"{paused} of {total} links are paused\",\n erroredTemplate: \"Last synchronization failed for {groups}\",\n erroredItemTemplate: \"{group}: {reason}\",\n listSeparator: \", \",\n oldestSyncedTemplate: \"oldest successful sync: {at}\",\n pendingTemplate: \"{count} sync requests are queued\",\n nothingToLink: \"Every group is already linked to a directory.\",\n noGroupsToLink: \"No group exists yet. Create one on the Groups tab first.\",\n notWiredNotice: \"Directory synchronization is not wired for this deployment, so links cannot be created or changed here.\",\n syncNowAria: \"Synchronize {group} now\",\n syncQueuedAria: \"A synchronization of {group} is queued\",\n pauseAria: \"Pause synchronization of {group}\",\n resumeAria: \"Resume synchronization of {group}\",\n allowGroupAria: \"Allow members of {group} to sign in\",\n disallowGroupAria: \"Stop allowing members of {group} to sign in\",\n unlinkAria: \"Unlink {group} from the directory\",\n confirmDisallow: \"Stop allowing {group} to sign in? Anyone who could sign in only through it will be locked out.\",\n link: \"Link to a directory group\",\n unlink: \"Unlink\",\n externalKey: \"Upstream group\",\n empty: \"No groups are linked to a directory\",\n selectGroup: \"Group to link\",\n pickUpstreamPlaceholder: \"Search by name or address...\",\n pickerNoOptions: \"No matching group\",\n pickerClear: \"Clear the selection\",\n pickerToggle: \"Show the group list\",\n pickerSelected: \"Selected: {value}\",\n pickerAvailable: \"{count} groups available\",\n externalKeyPlaceholder: \"e.g. dev-team@example.com\",\n confirmLink: \"Hand {group}'s membership to {upstream}? Renaming, deleting, and editing its members here becomes impossible.\",\n confirmUnlink: \"Unlink {group} from {upstream}? Its member ledger and group-based sign-in permission are removed.\",\n pause: \"Pause\",\n resume: \"Resume\",\n allowGroup: \"Allow sign-in\",\n disallowGroup: \"Stop allowing sign-in\",\n groupOptionTemplate: \"{name} ({key})\",\n },\n}\n\n/**\n * Standard Japanese labels for user administration (opt-in).\n * ユーザー管理の標準日本語ラベル (アプリ側で明示注入して使う)。\n */\nexport const jaUserAdminLabels: UserAdminLabels = {\n title: \"ユーザー・グループ管理\",\n locale: \"ja-JP\",\n timeZone: \"Asia/Tokyo\",\n heading: \"\\u{1F464} ユーザー管理\",\n common: {\n create: \"作成\",\n edit: \"編集\",\n delete: \"削除\",\n save: \"保存\",\n cancel: \"キャンセル\",\n actions: \"操作\",\n status: \"状態\",\n id: \"ID\",\n readOnly: \"変更不可\",\n managedUpstream: \"上流が所有\",\n idTemplate: \"#{id}\",\n none: \"-\",\n },\n errors: {\n unknownUpstream: \"上流に {key} というグループが見つかりません。綴りを確認するか、一覧から選んでください。\",\n directoryDisabled: \"この配備にはディレクトリが設定されていないため、グループを連携できません。\",\n invalidField: \"「{field}」が未指定か、この操作が受け付けられない値です。\",\n unknownIntent: \"この画面では、その操作を行いません。\",\n fieldNames: {\n groupId: \"グループ\",\n userId: \"ユーザー\",\n entryId: \"項目\",\n displayName: \"表示名\",\n email: \"メールアドレス\",\n groupKey: \"グループキー\",\n name: \"名前\",\n provider: \"連携プロバイダ\",\n externalKey: \"ディレクトリのグループアドレス\",\n membershipMode: \"所属の展開\",\n isActive: \"有効\",\n isSyncEnabled: \"同期\",\n pattern: \"パターン\",\n type: \"種別\",\n },\n invalidAllowlistType: \"許可リストの種別は、アドレスかドメインのいずれかで指定してください。\",\n unexpected: \"操作に失敗しました。理由はサーバーのログに出ています。\",\n alreadyLinked: \"{key} は既に {group} と連携しています。1 つの上流グループを 2 つのローカルグループへ繋ぐことはできません。\",\n forbidden: \"この操作を行う権限がありません。今の権限はヘッダーのバッジに出ています。必要であれば管理者に付与を依頼してください。\",\n requestTooLarge: \"送信された内容が大きすぎるため処理できませんでした。\",\n },\n permissions: {\n label: \"あなたの権限:\",\n canWrite: \"編集可\",\n canRead: \"閲覧のみ\",\n canDelete: \"削除のみ\",\n none: \"権限なし\",\n unknown: \"権限不明\",\n },\n tabs: {\n users: \"\\u{1F464} ユーザー一覧\",\n groups: \"\\u{1F465} ユーザーグループ\",\n groupMembers: \"\\u{1F517} グループメンバー\",\n allowlist: \"\\u2709\\uFE0F アクセス許可リスト\",\n directory: \"\\u{1F504} ディレクトリ連携\",\n },\n usersView: {\n title: \"ユーザー管理\",\n sortBy: \"{column} で並べ替え\",\n searchPlaceholder: \"表示名や Email で検索...\",\n searchLabel: \"ユーザーを検索\",\n addUser: \"\\uFF0B ユーザー追加\",\n editUser: \"ユーザー編集\",\n displayName: \"表示名\",\n email: \"Email\",\n active: \"有効\",\n inactive: \"無効\",\n activate: \"有効化\",\n deactivate: \"無効化\",\n editUserAria: \"{user} を編集\",\n activateAria: \"{user} を有効化\",\n deactivateAria: \"{user} を無効化\",\n emptyFiltered: \"検索条件に一致する利用者はいません。\",\n listSummary: \"{total} 人中 {shown} 人を表示しています。\",\n sortedAscending: \"{column} (昇順で並べ替え中)。押すと降順になります。\",\n sortedDescending: \"{column} (降順で並べ替え中)。押すと昇順になります。\",\n confirmDeactivate: \"{user} を無効化しますか? 以後サインインできなくなります。\",\n empty: \"ユーザーが登録されていません。\",\n },\n groupsView: {\n emptyFiltered: \"検索条件に一致するグループはありません\",\n createModeLabel: \"作成方法\",\n createModeLocal: \"ローカルに作成\",\n createModeUpstream: \"ディレクトリのグループから作成\",\n pickUpstream: \"ディレクトリのグループ\",\n pickUpstreamPlaceholder: \"名前かアドレスで検索...\",\n upstreamOptionMembers: \"{count} 名\",\n upstreamAlreadyLinked: \"{group} と連携済み\",\n providerUnset: \"この配備には連携プロバイダ名が設定されていないため、ディレクトリのグループを連携できません。管理者に設定を依頼してください。\",\n pickUpstreamFirst: \"先にディレクトリのグループを選んでください。\",\n allUpstreamLinked: \"ディレクトリのグループはすべて、いずれかのローカルグループと連携済みです。1 つの上流グループを繋げる先は 1 つだけなので、繋ぎ替えるにはどれかの連携を解除してください。\",\n catalogEmpty: \"{at} 時点で、上流にグループが 1 つもありませんでした。上流にグループができるまで、ディレクトリからの作成はできません。\",\n catalogUnavailable: \"上流へ到達できないため、グループ一覧を出せません。アドレスを手入力してください。({reason})\",\n catalogUnavailableHere: \"上流へ到達できないため、ディレクトリからの作成はできません。ローカルに作成したうえで、連携タブから連携してください。({reason})\",\n catalogObservedAt: \"この一覧は {at} 時点のディレクトリです。\",\n catalogStale: \"⚠ この一覧は {at} 時点のもので、更新が滞っています。それ以降に作られたグループは出てきません。\",\n catalogFailures: {\n denied: \"この配備の資格情報が上流に拒否されました\",\n rateLimited: \"上流がこの配備の呼び出しを制限しています\",\n upstreamError: \"上流が内部エラーを返しました\",\n neverObserved: \"上流をまだ一度も読めていません (同期ジョブが動いていない可能性があります)\",\n unknown: \"理由はサーバーのログに出ています\",\n },\n createUpstreamNotice: \"グループの作成と連携を 1 手で行います。以後、所属は上流が所有し — この画面からは名称・説明の変更も削除もできなくなります。戻す手段は連携解除だけで、それはメンバー台帳とグループ単位のサインイン許可を消します。\",\n openMembers: \"メンバー\",\n openMembersAria: \"{group} のメンバーを表示\",\n title: \"ユーザーグループ管理\",\n searchPlaceholder: \"グループ名やキーで検索...\",\n searchLabel: \"グループを検索\",\n addGroup: \"\\uFF0B グループ追加\",\n editGroup: \"グループ編集\",\n groupKey: \"グループキー\",\n name: \"名称\",\n description: \"説明\",\n memberCountTemplate: \"所属 {count} 名\",\n confirmDelete: \"{group} を削除しますか? 所属 {count} 件も一緒に消え、このグループでしかサインインできない人はサインインできなくなります。\",\n confirmCreateLinked: \"{group} を作成し、その所属を {upstream} に委ねますか? 以後この画面から名称変更と削除ができなくなります。\",\n editGroupAria: \"{group} を編集\",\n deleteGroupAria: \"{group} を削除\",\n noDescription: \"説明なし\",\n groupKeyPlaceholder: \"例: dev-team\",\n namePlaceholder: \"例: 開発チーム\",\n empty: \"ユーザーグループが登録されていません\",\n },\n groupMembersView: {\n title: \"グループメンバー管理\",\n searchLabel: \"利用者を検索\",\n searchPlaceholder: \"表示名や Email で検索...\",\n onlyMembers: \"所属のみ表示 ({count} 名)\",\n emptyFiltered: \"検索条件に一致する利用者はいません\",\n summary: \"{total} 名のうち {shown} 名を表示中。このグループの所属は {members} 名です。\",\n selectGroup: \"グループ選択\",\n selectPrompt: \"グループを選択してください\",\n noGroups: \"グループがまだ 1 つもありません。先にグループタブで作成してください。\",\n emptyNoMembers: \"このグループにはまだメンバーがいません。「所属のみ表示」のチェックを外すと追加できます。\",\n noUsers: \"利用者がまだ 1 人も登録されていません。先にユーザータブで追加してください。\",\n deepLinkMissed: \"グループ {id} が見つかりませんでした (削除された可能性があります)。代わりに {group} を表示しています。\",\n actionAdd: \"追加\",\n actionRemove: \"除外\",\n actionAddAria: \"{user} をこのグループへ追加\",\n actionRemoveAria: \"{user} をこのグループから除外\",\n upstreamNotice: \"このグループの所属は上流ディレクトリが所有しています。変更は上流で行ってください (ここで変えても次の同期で元に戻ります)。\",\n membersOfTemplate: \"{group} の所属メンバー\",\n groupKeyLabel: \"グループキー\",\n },\n allowlistView: {\n title: \"サインイン許可リスト\",\n addEntry: \"\\uFF0B パターン追加\",\n pattern: \"パターン (例: @example.com または user@example.com)\",\n type: \"種別\",\n emailType: \"メールアドレス完全一致\",\n domainType: \"ドメイン一致\",\n emailTypeShort: \"メール\",\n domainTypeShort: \"ドメイン\",\n groupTypeShort: \"グループ\",\n target: \"対象\",\n description: \"説明 / 備考\",\n deploymentOwner: \"配備共通\",\n confirmDelete: \"{target} を許可リストから削除しますか? これでしかサインインできない人はログインできなくなります。\",\n deleteAria: \"{target} の許可を削除\",\n empty: \"サインイン許可リストに登録がありません。\",\n },\n directoryView: {\n title: \"ディレクトリ同期\",\n membershipMode: \"所属の展開\",\n directMode: \"直接所属のみ\",\n transitiveMode: \"入れ子を展開\",\n lastSynced: \"最終同期\",\n neverSynced: \"未実行\",\n unresolvedTemplate: \"{count} 件が未解決\",\n syncPaused: \"停止中\",\n syncEnabled: \"有効\",\n syncNow: \"今すぐ同期\",\n syncQueued: \"同期を要求しました\",\n syncAlreadyPending: \"この範囲の同期要求は既に待機中のため、新しくは積みませんでした。いつまでも完了しない場合、同期ジョブが動いていない可能性があります。\",\n linkSyncNotQueued: \"連携は作成しましたが、同期要求を積めませんでした。ディレクトリ連携タブの「今すぐ同期」で要求してください。\",\n staleWarning: \"同期が滞っています。実行されるまで、上流から外れた人のアクセスは残り続けます。\",\n disabledWarning: \"この配備ではディレクトリ同期が結線されていません。既存の所属はそのまま有効で、更新だけが止まっています。\",\n pausedTemplate: \"連携 {total} 件のうち {paused} 件が停止中\",\n erroredTemplate: \"直近の同期に失敗したグループ: {groups}\",\n erroredItemTemplate: \"{group}: {reason}\",\n listSeparator: \" / \",\n oldestSyncedTemplate: \"最も古い同期成功: {at}\",\n pendingTemplate: \"{count} 件の同期要求が未処理\",\n nothingToLink: \"すべてのグループが既にディレクトリと連携しています。\",\n noGroupsToLink: \"グループがまだ 1 つもありません。先にグループタブで作成してください。\",\n notWiredNotice: \"この配備ではディレクトリ同期が結線されていないため、ここから連携の作成・変更はできません。\",\n syncNowAria: \"{group} を今すぐ同期\",\n syncQueuedAria: \"{group} の同期を要求済み\",\n pauseAria: \"{group} の同期を一時停止\",\n resumeAria: \"{group} の同期を再開\",\n allowGroupAria: \"{group} のメンバーのサインインを許可\",\n disallowGroupAria: \"{group} のメンバーのサインイン許可を外す\",\n unlinkAria: \"{group} のディレクトリ連携を解除\",\n confirmDisallow: \"{group} のサインイン許可を外しますか? これでしかサインインできない人はログインできなくなります。\",\n link: \"ディレクトリのグループと連携する\",\n unlink: \"連携を解除\",\n externalKey: \"上流グループ\",\n empty: \"ディレクトリと連携しているグループはありません\",\n selectGroup: \"連携するグループ\",\n pickUpstreamPlaceholder: \"名前かアドレスで検索...\",\n pickerNoOptions: \"一致するグループがありません\",\n pickerClear: \"選択を解除\",\n pickerToggle: \"グループ一覧を開く\",\n pickerSelected: \"選択中: {value}\",\n pickerAvailable: \"候補 {count} 件\",\n externalKeyPlaceholder: \"例: dev-team@example.com\",\n confirmLink: \"{group} の所属を {upstream} に委ねますか? 以後この画面から名称変更・削除・メンバー編集ができなくなります。\",\n confirmUnlink: \"{group} と {upstream} の連携を解除しますか? メンバー台帳とグループ単位のサインイン許可が失われます。\",\n pause: \"一時停止\",\n resume: \"再開\",\n allowGroup: \"サインインを許可\",\n disallowGroup: \"サインイン許可を外す\",\n groupOptionTemplate: \"{name} ({key})\",\n },\n}\n\n/** セクション単位の部分上書き。 */\nexport type PartialUserAdminLabels = {\n title?: string\n locale?: string\n timeZone?: string\n common?: Partial<UserAdminLabels[\"common\"]>\n errors?: Partial<UserAdminLabels[\"errors\"]>\n heading?: string\n permissions?: Partial<UserAdminLabels[\"permissions\"]>\n tabs?: Partial<UserAdminLabels[\"tabs\"]>\n usersView?: Partial<UserAdminLabels[\"usersView\"]>\n groupsView?: Partial<UserAdminLabels[\"groupsView\"]>\n groupMembersView?: Partial<UserAdminLabels[\"groupMembersView\"]>\n allowlistView?: Partial<UserAdminLabels[\"allowlistView\"]>\n directoryView?: Partial<UserAdminLabels[\"directoryView\"]>\n}\n\n/**\n * Shallow per-section merge over the English defaults.\n * 英語既定に対してセクション単位で浅くマージする処理。\n */\nexport function resolveUserAdminLabels(over?: PartialUserAdminLabels): UserAdminLabels {\n const d = defaultUserAdminLabels\n if (!over) return d\n return {\n title: over.title ?? d.title,\n locale: over.locale ?? d.locale,\n timeZone: over.timeZone ?? d.timeZone,\n heading: over.heading ?? d.heading,\n common: { ...d.common, ...over.common },\n errors: { ...d.errors, ...over.errors },\n permissions: { ...d.permissions, ...over.permissions },\n tabs: { ...d.tabs, ...over.tabs },\n usersView: { ...d.usersView, ...over.usersView },\n groupsView: { ...d.groupsView, ...over.groupsView },\n groupMembersView: { ...d.groupMembersView, ...over.groupMembersView },\n allowlistView: { ...d.allowlistView, ...over.allowlistView },\n directoryView: { ...d.directoryView, ...over.directoryView },\n }\n}\n","/**\n * @module admin/server\n * @description The loader, action and store contract of the user administration route.\n * ユーザー管理ルートの loader・action と、その記憶域の契約。\n *\n * 権限は intent ごとに要求する CRUD 種別へ写し (`INTENT_ACCESS`)、拒否は画面を落とさず\n * その操作だけの失敗として返す。上流の候補一覧はこのプロセスから引かず、同期ジョブが書いた\n * ものを読むだけである。\n */\nimport type { AuthAllowlistEntry, AuthDirectoryCatalogGroup, AuthDirectoryCatalogSnapshot, AuthDirectoryMembershipMode, AuthDirectoryStatus, AuthGroup, AuthGroupMember, AuthUser } from \"@aiquants/auth-core\"\nimport { data } from \"react-router\"\nimport { fill as fillLabel } from \"./format\"\nimport { type PartialUserAdminLabels, resolveUserAdminLabels, type UserAdminLabels } from \"./labels\"\n\n/**\n * Server store interface for managing users, groups, memberships, and allowlists.\n * ユーザー・グループ・所属・アロワリストを管理するサーバー層ストアのインターフェース。\n */\nexport type UserAdminStore = {\n listUsers: () => Promise<AuthUser[]>\n createUser: (data: { displayName: string; email: string }) => Promise<AuthUser>\n updateUser: (id: number, data: { displayName: string; email: string }) => Promise<AuthUser>\n toggleUserActive: (id: number, isActive: boolean) => Promise<AuthUser>\n\n listGroups: () => Promise<AuthGroup[]>\n createGroup: (data: { groupKey: string; name: string; description?: string }) => Promise<AuthGroup>\n updateGroup: (id: number, data: { name: string; description?: string }) => Promise<AuthGroup>\n deleteGroup: (id: number) => Promise<void>\n\n listGroupMembers: () => Promise<AuthGroupMember[]>\n addGroupMember: (groupId: number, userId: number) => Promise<AuthGroupMember>\n removeGroupMember: (groupId: number, userId: number) => Promise<void>\n\n listAllowlist: () => Promise<AuthAllowlistEntry[]>\n addAllowlistEntry: (data: { pattern: string; type: \"email\" | \"domain\"; description?: string }) => Promise<AuthAllowlistEntry>\n removeAllowlistEntry: (id: number) => Promise<void>\n\n /**\n * Allows every member of an externally-sourced group to sign in.\n * 外部供給グループのメンバー全員にサインインを許可する処理。\n *\n * 判定は上流メンバー台帳を読むため、ローカル管理のグループを渡しても誰にも何も与えない。\n * ストアはそれを拒否すること (無言の no-op を作らない)。\n */\n addAllowlistGroup: (groupId: number, description?: string) => Promise<void>\n removeAllowlistGroup: (id: number) => Promise<void>\n\n /**\n * Links a local group to a group in the upstream directory.\n * ローカルグループを上流ディレクトリのグループへ結び付ける処理。\n *\n * リンク後、そのグループの名称と所属は上流が所有する。ロール割当はローカルのままである。\n */\n linkDirectoryGroup: (groupId: number, data: { provider: string; externalId: string; externalKey: string; membershipMode: AuthDirectoryMembershipMode }) => Promise<void>\n /**\n * Creates a local group and links it to an upstream group as one indivisible step.\n * ローカルグループの作成と上流への連携を、分割不可能な 1 手として行う処理。\n *\n * ⚠️ **途中で失敗したら何も残してはならない。** 作成だけが成功すると、運用者には「連携に\n * 失敗した」ではなく「知らないグループが増えた」と見える。実装は 1 つのトランザクションで\n * 行うこと (作成 → 連携の順に別々の書込を並べると、この不可分性は成立しない)。\n */\n createLinkedGroup: (data: { groupKey: string; name: string; description?: string; provider: string; externalId: string; externalKey: string; membershipMode: AuthDirectoryMembershipMode }) => Promise<AuthGroup>\n /** Stops following the upstream, keeping the memberships already projected. 連携の解除。 */\n unlinkDirectoryGroup: (groupId: number) => Promise<void>\n /** Pauses or resumes synchronization for one group. 1 グループの同期を停止・再開する処理。 */\n setDirectorySyncEnabled: (groupId: number, isSyncEnabled: boolean) => Promise<void>\n /**\n * Queues a \"sync now\" request. 「今すぐ同期」の要求を積む処理。\n *\n * ⚠️ 戻り値を `void` にしてはならない。同じ範囲の要求が既に待っているとき、積み増さないのは\n * 正しい (押した数だけ上流への全件走査が並ぶ) が、それを成功と区別せずに返すと、押しても\n * 反応しない画面ができる — しかも詰まった 1 行が以後の押下すべてを飲み込む。\n *\n * @param groupId Target group, or `null` for the whole tenant. 対象グループ (`null` でテナント全件)。\n * @param actor Who asked, for the audit record. 要求した主体 (監査記録用)。\n * @returns Whether it was queued or already waiting. 積んだか、既に待っていたか。\n */\n requestDirectorySync: (groupId: number | null, actor: string) => Promise<UserAdminSyncRequestOutcome>\n /** Health of the synchronization, for the header warning. 同期の健全性 (警告表示用)。 */\n getDirectoryStatus: () => Promise<AuthDirectoryStatus>\n}\n\n/**\n * Creates an in-memory UserAdminStore initialized with optional seed data.\n * シードデータで初期化されるメモリ上の UserAdminStore を作成する。\n */\nexport function createInMemoryUserAdminStore(initial?: { users?: AuthUser[]; groups?: AuthGroup[]; groupMembers?: AuthGroupMember[]; allowlist?: AuthAllowlistEntry[] }): UserAdminStore {\n const users: AuthUser[] = initial?.users ?? [\n { id: 1, email: \"admin@example.com\", displayName: \"システム管理者\", isActive: true },\n { id: 2, email: \"user1@example.com\", displayName: \"開発 太郎\", isActive: true },\n { id: 3, email: \"user2@example.com\", displayName: \"営業 花子\", isActive: true },\n ]\n let groups: AuthGroup[] = initial?.groups ?? [\n { id: 1, groupKey: \"dev-team\", name: \"開発チーム\", description: \"システム開発担当グループ\", isActive: true, readOnly: false },\n { id: 2, groupKey: \"sales-dept\", name: \"営業部\", description: \"営業ソリューション担当\", isActive: true, readOnly: false },\n ]\n let groupMembers: AuthGroupMember[] = initial?.groupMembers ?? [\n { id: 1, groupId: 1, userId: 2 },\n { id: 2, groupId: 2, userId: 3 },\n ]\n let allowlist: AuthAllowlistEntry[] = initial?.allowlist ?? [\n { id: 1, pattern: \"admin@example.com\", type: \"email\", description: \"初期管理者\", owner: { kind: \"tenant\", tenantId: \"default\" }, readOnly: false },\n { id: 2, pattern: \"@example.com\", type: \"domain\", description: \"社内ドメイン\", owner: { kind: \"tenant\", tenantId: \"default\" }, readOnly: false },\n ]\n\n return {\n async listUsers() {\n return [...users]\n },\n async createUser(data) {\n const newUser: AuthUser = {\n id: Math.max(...users.map((u) => u.id), 0) + 1,\n email: data.email,\n displayName: data.displayName,\n isActive: true,\n }\n users.push(newUser)\n return newUser\n },\n async updateUser(id, data) {\n const idx = users.findIndex((u) => u.id === id)\n if (idx === -1) throw new UserAdminOperatorError(`User not found: ${id}`)\n users[idx] = { ...users[idx], displayName: data.displayName, email: data.email }\n return users[idx]\n },\n async toggleUserActive(id, isActive) {\n const idx = users.findIndex((u) => u.id === id)\n if (idx === -1) throw new UserAdminOperatorError(`User not found: ${id}`)\n users[idx] = { ...users[idx], isActive }\n return users[idx]\n },\n\n async listGroups() {\n return [...groups]\n },\n async createGroup(data) {\n const newGroup: AuthGroup = {\n id: Math.max(...groups.map((g) => g.id), 0) + 1,\n groupKey: data.groupKey,\n name: data.name,\n description: data.description,\n isActive: true,\n readOnly: false,\n }\n groups.push(newGroup)\n return newGroup\n },\n async updateGroup(id, data) {\n const idx = groups.findIndex((g) => g.id === id)\n if (idx === -1) throw new UserAdminOperatorError(`Group not found: ${id}`)\n groups[idx] = { ...groups[idx], name: data.name, description: data.description }\n return groups[idx]\n },\n async deleteGroup(id) {\n groups = groups.filter((g) => g.id !== id)\n groupMembers = groupMembers.filter((m) => m.groupId !== id)\n },\n\n async listGroupMembers() {\n return [...groupMembers]\n },\n async addGroupMember(groupId, userId) {\n const existing = groupMembers.find((m) => m.groupId === groupId && m.userId === userId)\n if (existing) return existing\n const newMember: AuthGroupMember = {\n id: Math.max(...groupMembers.map((m) => m.id), 0) + 1,\n groupId,\n userId,\n }\n groupMembers.push(newMember)\n return newMember\n },\n async removeGroupMember(groupId, userId) {\n groupMembers = groupMembers.filter((m) => !(m.groupId === groupId && m.userId === userId))\n },\n\n async listAllowlist() {\n return [...allowlist]\n },\n async addAllowlistEntry(data) {\n const newEntry: AuthAllowlistEntry = {\n id: Math.max(...allowlist.map((a) => a.id), 0) + 1,\n pattern: data.pattern,\n type: data.type,\n description: data.description,\n owner: { kind: \"tenant\", tenantId: \"default\" },\n readOnly: false,\n }\n allowlist.push(newEntry)\n return newEntry\n },\n async removeAllowlistEntry(id) {\n allowlist = allowlist.filter((a) => a.id !== id)\n },\n\n // --- ディレクトリ連携 ------------------------------------------------------------\n // このメモリ実装はパッケージ自身の spec 用のテストシームであり、上流を持たない。\n // 連携面は「結線されていない配備」を表現する形で応答する\n async addAllowlistGroup(groupId, description) {\n const group = groups.find((g) => g.id === groupId)\n if (!group) throw new UserAdminOperatorError(`Group not found: ${groupId}`)\n allowlist = [...allowlist, { id: Math.max(...allowlist.map((a) => a.id), 0) + 1, type: \"group\", groupId, groupKey: group.groupKey, groupName: group.name, description, owner: { kind: \"tenant\", tenantId: \"default\" }, readOnly: false }]\n },\n async removeAllowlistGroup(id) {\n allowlist = allowlist.filter((a) => !(a.type === \"group\" && a.id === id))\n },\n async linkDirectoryGroup() {\n throw new UserAdminOperatorError(\"This in-memory store has no upstream directory; inject a real store to link groups.\")\n },\n async createLinkedGroup() {\n throw new UserAdminOperatorError(\"This in-memory store has no upstream directory; inject a real store to create a linked group.\")\n },\n async unlinkDirectoryGroup() {\n throw new UserAdminOperatorError(\"This in-memory store has no upstream directory; inject a real store to unlink groups.\")\n },\n async setDirectorySyncEnabled() {\n throw new UserAdminOperatorError(\"This in-memory store has no upstream directory; inject a real store to pause synchronization.\")\n },\n async requestDirectorySync() {\n throw new UserAdminOperatorError(\"This in-memory store has no upstream directory; inject a real store to request a sync.\")\n },\n async getDirectoryStatus() {\n return { isEnabled: false, provider: \"\", linkedGroupCount: 0, isStale: false, pausedGroupCount: 0, stalenessThresholdSec: 0, erroredGroupIds: [], pendingRequestCount: 0 }\n },\n }\n}\n\n/**\n * Decides whether an observation is overdue. 観測が期限超過かどうかを決める処理。\n *\n * 上限が正の数でなければ **判定しない** のではなく、既定の上限で判定する。0 や `NaN` で\n * 「決して古くならない」に倒すと、環境変数の打ち間違いが警告そのものを黙って無効化する。\n *\n * @param observedAt When the upstream was observed. 上流を観測した時刻。\n * @param thresholdSec Deployment's staleness bound in seconds. 配備の陳腐化上限 (秒)。\n * @returns True when the observation is overdue. 期限超過なら true。\n */\nfunction isStaleObservation(observedAt: Date, thresholdSec: number): boolean {\n const bound = Number.isFinite(thresholdSec) && thresholdSec > 0 ? thresholdSec : DEFAULT_STALENESS_SEC\n return (Date.now() - observedAt.getTime()) / 1000 > bound\n}\n\n/** 配備が上限を示さなかったときに使う既定 (秒)。判定を無効化するより、既定で判定する。 */\n/**\n * Largest action body this screen ever needs. この画面の操作が要する本文の上限。\n *\n * ⚠️ intent は本文の中にあるため、ガードより先に本文を読む以外の選択肢が無い。この画面が送るのは\n * 短い文字列だけなので、64 KiB は通常の操作すべてに対して桁で余裕がある。上限そのものより、\n * **認証を通っていない要求が積み上げられる量を定数で抑える** ことが目的である。\n */\nconst MAX_ACTION_BODY_BYTES = 64 * 1024\n\nconst DEFAULT_STALENESS_SEC = 5400\n\n/**\n * Loads the upstream catalog, turning any failure into a renderable state.\n * 上流の候補一覧を読み、あらゆる失敗を **描画可能な状態** へ変える処理。\n *\n * ⚠️ ここで例外を通すと、上流が落ちているだけでユーザー管理面が丸ごと開かなくなる。候補一覧は\n * 選択欄を出すための利便であり、グループの閲覧や許可リストの管理はそれと無関係に成立する。\n * 「判らないなら拒否」を貫くべき認可・サインイン判定とは、意図的に扱いを分けている。\n *\n * @param port The injected catalog port, if any. 注入された候補一覧ポート (無い場合あり)。\n * @param needed False for segments that never show a picker. 選択欄を出さない面では false。\n * @param request The incoming request, for per-request credentials. リクエスト (資格情報の解決用)。\n * @param thresholdSec Deployment's staleness bound in seconds. 配備の陳腐化上限 (秒)。\n * @returns A state the views can render without further checks. ビューがそのまま描ける状態。\n */\nasync function loadCatalog(port: { listGroups: (request: Request) => Promise<UserAdminCatalogSnapshot> } | undefined, needed: boolean, request: Request, thresholdSec: number): Promise<UserAdminCatalog> {\n if (!port) return { kind: \"not-wired\" }\n if (!needed) return { kind: \"not-wired\" }\n try {\n const { groups, observedAt } = await port.listGroups(request)\n // ⚠️ 一度も観測していない一覧を `ready` で返してはならない。0 件の答えとして描かれ、\n // 運用者には「上流にグループが無い」と読める — 実際には同期が一度も走っていないだけである\n if (observedAt === null || !Number.isFinite(observedAt.getTime())) return { kind: \"unavailable\", reason: \"neverObserved\" }\n // ⚠️ 陳腐化の判定は **サーバーで** 行う。ブラウザの時計で判定すると、遅れている端末では\n // 経過が負になり「古い」という警告が出なくなる — 安全側の警告が、利用者側の入力で\n // 消える形になる。水和のずれも同時に無くなる\n return { kind: \"ready\", groups, observedAt: observedAt.toISOString(), isStale: isStaleObservation(observedAt, thresholdSec) }\n } catch (error) {\n // 原文はサーバー側のログにだけ残す。画面へ運ぶのは分類だけである\n console.error(\"[@aiquants/auth-react-router] directoryCatalog.listGroups failed; the picker falls back to manual entry:\", error)\n return { kind: \"unavailable\", reason: classifyCatalogFailure(error) }\n }\n}\n\n/**\n * Normalizes an upstream key for comparison. 上流キーを比較用に正規化する処理。\n *\n * 前後の空白と大文字小文字を同一視する。**画面と実装で正規化がずれると**、選択欄は「未連携」と\n * 見なすキーをサーバーが「連携済み」と見なす、といった食い違いが生まれる。1 か所に置く。\n *\n * @param key Raw upstream key. 生の上流キー。\n * @returns The comparable form. 比較可能な形。\n */\nexport function normalizeUpstreamKey(key: string): string {\n return key.trim().toLowerCase()\n}\n\n/**\n * An error whose own text is meant for the operator to read.\n * 文面を **運用者に読ませるつもりで** 投げるエラー。\n *\n * ⚠️ ストアの例外文をそのまま画面へ返してはならない。ドライバの例外はスキーマ名・表名・制約名・\n * 衝突した値・接続先をそのまま含み、それが閲覧権限しか持たない主体の画面に出る。一方で\n * 「最後の管理者は外せません」のような業務上の拒否理由は、読めなければ操作そのものが行き詰まる。\n * 両者は文面からは区別できないので、**投げる側が宣言する**。宣言の無い例外は一般的な文言になる。\n *\n * ホストのストアは、この class を継承しても `operatorFacing: true` を持たせてもよい\n * (別パッケージのエラー型を継承できない事情に配慮した二経路である)。\n */\nexport class UserAdminOperatorError extends Error {\n /** 運用者向けであることの印。継承できない場合はこの属性だけでもよい。 */\n readonly operatorFacing = true\n constructor(message: string) {\n super(message)\n this.name = \"UserAdminOperatorError\"\n }\n}\n\n/**\n * Decides whether an error's own text may reach the operator. 例外文を運用者へ見せてよいか判定する処理。\n *\n * @param error The thrown value. 投げられた値。\n * @returns True when the store declared it operator-facing. ストアが運用者向けと宣言していれば true。\n */\nfunction isOperatorFacing(error: unknown): error is Error {\n return error instanceof UserAdminOperatorError || (typeof error === \"object\" && error !== null && (error as { operatorFacing?: unknown }).operatorFacing === true && error instanceof Error)\n}\n\n/**\n * An input problem the operator can fix, carrying a label key instead of a sentence.\n * 運用者が直せる入力の問題。文ではなく **ラベルのキー** を運ぶ。\n *\n * ⚠️ メッセージを実装側で組むと、既定 (英語) の配備に日本語が出る。文言はラベルに集約し、\n * ここでは「どの文言か」と差し替え値だけを運ぶ。\n */\nexport class UserAdminInputError extends Error {\n constructor(\n readonly labelKey: keyof UserAdminLabels[\"errors\"],\n readonly values: Record<string, string | number> = {},\n ) {\n super(labelKey)\n this.name = \"UserAdminInputError\"\n }\n}\n\n/**\n * Tells a per-operation authorization denial apart from any other failure, without importing an authz library.\n * 認可ライブラリを import せずに、操作単位の認可拒否を他の失敗と見分ける判定。\n *\n * 特定の実装の例外クラスへ `instanceof` を張ると、そのライブラリに縛られる (`AGENTS.md` の\n * クロスパッケージ・ハードコード禁止)。`status === 403` は HTTP の共通語彙なので、これだけを見る。\n *\n * @param error The thrown value. 送出された値。\n * @returns True when it carries HTTP 403. HTTP 403 を名乗る場合に true。\n */\nfunction isForbidden(error: unknown): boolean {\n // Response でも素の例外でも `status` は共通語彙である。特定の実装の例外クラスへ\n // `instanceof` を張ると、そのライブラリに縛られる\n return typeof error === \"object\" && error !== null && (error as { status?: unknown }).status === 403\n}\n\n/**\n * Reads the allowlist kind, refusing anything but the two declared values.\n * 許可の種別を読み取る処理 (宣言された 2 値以外を拒否する)。\n *\n * 指定外を既定値へ倒すと、綴り違い (`Domain`) や欠落が **完全一致の許可** として登録される。\n * 「ドメイン全体を許可したつもりが 1 アドレスだけだった」は、逆向きより静かに壊れる。\n *\n * @param formData Submitted form. 送信されたフォーム。\n * @returns The declared kind. 宣言された種別。\n * @throws {Error} When the value is absent or unknown. 未指定・未知の値の場合。\n */\nfunction requireAllowlistType(formData: FormData): \"email\" | \"domain\" {\n const raw = String(formData.get(\"type\") ?? \"\")\n if (raw === \"email\" || raw === \"domain\") return raw\n // 受け取った値は反射しない。運用者が直せるのは「どの欄が不正か」までである\n throw new UserAdminInputError(\"invalidAllowlistType\")\n}\n\n/** 表示するタブ。未知の値は先頭のタブへ寄せる。 */\nconst SEGMENTS = [\"users\", \"groups\", \"group_members\", \"allowlist\", \"directory\"] as const\n\n/**\n * Derives the segment from a request path, tolerating the router's single-fetch suffix.\n * リクエストパスからセグメントを決める処理 (ルーターの単一フェッチ用の接尾辞を許容する)。\n *\n * ⚠️ クライアント側の遷移では、ルーターは画面の URL ではなく **`<path>.data`** を取りに来る\n * (単一フェッチ)。素朴に末尾セグメントを見ると `\"groups.data\"` になり、どのタブにも一致せず\n * 既定へ落ちる。結果、**URL は変わるのに中身が変わらない** — 全画面再読込でだけ正しく描かれる\n * ため、動作確認では気付きにくい。接尾辞を落としてから判定する。\n *\n * @param pathname Request pathname. リクエストのパス。\n * @returns One of the known segments. 既知のセグメントのいずれか。\n */\nfunction segmentFromPathname(pathname: string): (typeof SEGMENTS)[number] {\n const parts = pathname\n .replace(/\\.data$/, \"\")\n .split(\"/\")\n .filter(Boolean)\n const last = parts[parts.length - 1] ?? \"\"\n return (SEGMENTS as readonly string[]).includes(last) ? (last as (typeof SEGMENTS)[number]) : \"users\"\n}\n\n/**\n * The only route-args field this module reads. React Router's full `LoaderFunctionArgs` satisfies it, so\n * route wiring is unchanged — but declaring the minimum keeps callers (and tests) from fabricating fields\n * that are never looked at.\n * このモジュールが実際に読むルート引数の全て。React Router の完全な引数はこれを満たすため結線は不変。\n */\ntype RouteArgs = { request: Request }\n\n/**\n * The CRUD verb an operation performs. Deliberately a plain string union so this package stays\n * independent of any particular authorization library.\n * 操作が行う CRUD 種別。特定の認可ライブラリに依存しないよう素の文字列 union で表現する。\n */\nexport type UserAdminAccess = \"read\" | \"create\" | \"update\" | \"delete\"\n\n/**\n * Access guard the user-admin surface is mounted behind. Deny by throwing\n * (a `Response`/`redirect` or an `Error`); returning normally means \"allow\".\n *\n * ユーザー管理面をマウントする際の必須アクセスガード。拒否は throw で表現する\n * (`Response` / `redirect` / `Error`)。正常 return は「許可」を意味する。\n *\n * 操作ごとの CRUD 種別を受け取るため、呼び出し側は「削除だけ別権限」といった細分化ができる。\n * `intent` は監査ログ用の補助情報(認可判断は `action` で行うこと)。\n */\nexport type UserAdminGuards = {\n /**\n * 許可は「正常 return」、拒否は「throw」で表す。返り値は判定に使わない (`void` 契約)。\n * 述語のつもりで `false` を返す実装を書くと素通りするため、返り値型で明示的に禁じる。\n */\n requireAccess: (request: Request, context: { action: UserAdminAccess; intent: string }) => Promise<void> | void\n /**\n * Observes a denial that was converted into an in-page error. 画面内エラーへ変換した拒否の観測点。\n *\n * 拒否を画面内に返すと、既定のエラー経路が拾っていたログが消える。管理面への総当たりが\n * 200 番台の列に紛れて見えなくなるため、宿主が記録できる口を開けておく。\n */\n onDenied?: (context: { intent: string; actions: readonly UserAdminAccess[]; error: unknown }) => void\n}\n\n/**\n * Maps each action intent to the CRUD verb it actually performs.\n * 各 intent が実際に行う操作の CRUD 種別への対応表。\n *\n * 割当の付与・解除は行の追加・削除であるため、認可管理 UI (`/authz`) の\n * `assignUserRole` = create / `removeUserRole` = delete と同じ粒度に揃える。\n */\nconst INTENT_ACCESS = new Map<string, readonly UserAdminAccess[]>([\n [\"createUser\", [\"create\"]],\n [\"updateUser\", [\"update\"]],\n [\"toggleUserActive\", [\"update\"]],\n [\"createGroup\", [\"create\"]],\n [\"updateGroup\", [\"update\"]],\n [\"deleteGroup\", [\"delete\"]],\n [\"addGroupMember\", [\"create\"]],\n [\"removeGroupMember\", [\"delete\"]],\n [\"addAllowlistEntry\", [\"create\"]],\n [\"removeAllowlistEntry\", [\"delete\"]],\n [\"addAllowlistGroup\", [\"create\"]],\n [\"removeAllowlistGroup\", [\"delete\"]],\n // 連携の開始は、以後の所属の **追加と削除の両方** を上流へ委ねる行為である。所属を足せる\n // だけの主体が「足すことも消すこともできる仕掛け」を据えられてはならない。属性変更でもある\n // ため update も要る\n // 作成と連携を 1 手で行う。要求する権限は両者の合併である (どちらか一方で通してはならない)\n [\"createLinkedGroup\", [\"create\", \"update\", \"delete\"]],\n [\"linkDirectoryGroup\", [\"update\", \"create\", \"delete\"]],\n // 解除は台帳とグループ単位のサインイン許可を **消す**。属性を戻すだけではないので、\n // 削除権限を持たない主体には許さない\n [\"unlinkDirectoryGroup\", [\"update\", \"delete\"]],\n [\"setDirectorySyncEnabled\", [\"update\"]],\n // 書き込みを伴わないが update を要求する。読取権限だけの主体が上流問い合わせを誘発できると、\n // レート制限を外部から枯渇させられる\n [\"requestDirectorySync\", [\"update\"]],\n // 上流の候補一覧は書込みではないが、**外部組織の部門構成と規模** を列挙する。読取権限だけの\n // 主体に見せる理由は無く、連携を作れない主体には選択欄そのものが無意味である。要求する権限は\n // 連携を作る intent と同じにする (どれか 1 つで通せば、その 1 つで一覧が読める)\n [\"catalog\", [\"update\", \"create\", \"delete\"]],\n])\n\n/**\n * Every intent the permission table knows. 権限表が知っている intent すべて。\n *\n * ⚠️ 消費側の網羅性検査のために公開する。検査側が intent を手書きで並べる形しか無いと、\n * 実装に intent が増えても表は勝手には伸びず、**最も権限の重い新しい intent** だけが\n * 無検査で通る状態が静かに生まれる。突き合わせる相手を実装側から提供する。\n *\n * @returns Intent names, in declaration order. 宣言順の intent 名。\n */\nexport function userAdminIntents(): string[] {\n return [...INTENT_ACCESS.keys()]\n}\n\n/**\n * The CRUD verbs one intent requires. 1 つの intent が要求する CRUD 種別。\n *\n * ⚠️ 消費側が権限表を検査できるようにするために公開する。表そのものは公開しない — 書き換え\n * 可能な参照を渡すと、検査の対象が実行時に差し替えられる形になる。\n *\n * @param intent - Intent name. intent 名。\n * @returns The required verbs, or an empty list for an unknown intent. 要求する種別 (未知なら空)。\n */\nexport function userAdminAccessFor(intent: string): readonly UserAdminAccess[] {\n return INTENT_ACCESS.get(intent) ?? []\n}\n/**\n * One upstream group as the picker shows it. 選択欄に出す上流グループ 1 件。\n *\n * ⚠️ **別名ではなく `@aiquants/auth-core` の型そのものである。** この形は上流アダプタ・永続化層・\n * 管理 UI の 3 層を通る。層ごとに「同じ意味の別の型」を宣言すると、構造的部分型のせいでどの境界も\n * 通ってしまい、上流へ足した欄が途中で黙って捨てられる (実測: 別々に宣言していた間は、必須欄を\n * 足しても読み手側の境界で `tsc --strict` が 0 を返した)。欄の集合は `auth-core` の型テストが固定\n * しているので、足すときは 3 層すべての受け渡しを見直すことになる。\n *\n * 特定のディレクトリ実装 (Google 等) の型は依然として import しない — `auth-core` は上流に依存\n * しない語彙のパッケージであり、`AuthDirectoryStatus` などを既にここから受け取っている。\n */\nexport type UserAdminCatalogGroup = AuthDirectoryCatalogGroup\n\n/**\n * The upstream catalog as the loader hands it to the views. loader がビューへ渡す上流の候補一覧。\n *\n * ⚠️ **これは利便性のための読み取りである。** 上流へ届かなくても画面は開かなければならない\n * (グループ一覧や許可リストの閲覧は、選択欄の可否とは無関係である)。したがって失敗は\n * `reason` として **描画可能な状態** に変換し、例外にしない。認可やサインイン判定のように\n * 「判らないなら拒否」であるべき経路とは、意図的に扱いを変えている。\n */\nexport type UserAdminCatalog = { kind: \"ready\"; groups: UserAdminCatalogGroup[]; observedAt: string; isStale: boolean } | { kind: \"unavailable\"; reason: UserAdminCatalogFailure } | { kind: \"not-wired\" }\n\n/**\n * What the catalog port answers: the groups, and when the upstream was actually observed.\n * 候補一覧ポートの答え。グループと、上流を実際に観測した時刻。\n *\n * ⚠️ `observedAt` は省略できない。この一覧は上流を直に叩いた結果とは限らず、資格情報を持つ別の\n * プロセスが書いた写しであることが多い。時刻を伴わない一覧は「上流の現在」と区別できず、運用者は\n * 既に消えたグループを選ぶ。`null` は **一度も観測していない** ことを意味する (空の答えではない)。\n */\nexport type UserAdminCatalogSnapshot = AuthDirectoryCatalogSnapshot\n\n/**\n * What a \"sync now\" request did. 「今すぐ同期」の要求が行ったこと。\n *\n * `already-pending` は失敗ではない — 同じ範囲の要求が既に待っている。ただし **成功でもない**:\n * 何も積まれていないので、待っている行が誰にも拾われなければ何も起きない。\n */\nexport type UserAdminSyncRequestOutcome = \"queued\" | \"already-pending\"\n\n/**\n * What the sync request made right after a link was created did.\n * 連携の直後に積む同期要求が行ったこと。\n *\n * `not-queued` は **連携は成功したが要求は積めなかった** ことを表す。要求の失敗を連携の失敗として\n * 投げると、運用者には「連携に失敗した」と伝わる一方で上流所有のグループは既に増えている。\n * 連携の結果と要求の結果は別の事実なので、別の値で運ぶ。\n */\nexport type UserAdminLinkSyncOutcome = UserAdminSyncRequestOutcome | \"not-queued\"\n\n/**\n * Why the upstream catalog could not be listed, classified for display.\n * 上流の候補一覧を引けなかった理由を、表示のために分類したもの。\n *\n * ⚠️ **上流の例外文をそのまま画面へ出してはならない。** サービスアカウントのアドレス、借用先、\n * 内部 URL、資格情報の断片が混じり得る。管理面を開けるだけの主体に、それらを読ませる理由は無い。\n * 原文は `console.error` へ出し、画面にはこの分類に対応する自前の文言を出す。\n */\nexport type UserAdminCatalogFailure = \"denied\" | \"rateLimited\" | \"upstreamError\" | \"neverObserved\" | \"unknown\"\n\n/**\n * Classifies a catalog failure without quoting the upstream. 上流を引用せずに失敗を分類する処理。\n *\n * 上流アダプタの型に依存しないよう、`status` という数値プロパティの有無だけで判定する。\n * 判別できないものは `unknown` へ倒す (推測した分類を出すより、判らないと言う方が正しい)。\n *\n * @param error The thrown value. 投げられた値。\n * @returns The display classification. 表示用の分類。\n */\nfunction classifyCatalogFailure(error: unknown): UserAdminCatalogFailure {\n const status = typeof error === \"object\" && error !== null && \"status\" in error ? (error as { status: unknown }).status : undefined\n if (typeof status !== \"number\") return \"unknown\"\n if (status === 401 || status === 403) return \"denied\"\n if (status === 429) return \"rateLimited\"\n if (status >= 500) return \"upstreamError\"\n return \"unknown\"\n}\n\n/**\n * Neutral per-resource permission summary the admin shell renders in its header badge.\n * 管理シェルのヘッダーバッジが描画する、リソース単位の中立な権限サマリ。\n *\n * この形はこのパッケージ自身の契約であり、特定の認可ライブラリの型を import しない\n * (`AGENTS.md` のクロスパッケージ・ハードコード禁止)。`canRead` / `canWrite` / `canDelete` を持つ\n * ビューであれば構造的にそのまま渡せる。\n */\nexport type UserAdminPermission = {\n canRead: boolean\n canWrite: boolean\n canDelete: boolean\n}\n\nexport type UserAdminAppConfig = {\n store: UserAdminStore\n /** セクション単位の部分上書き。既定は英語 (`defaultUserAdminLabels`)。 */\n labels?: PartialUserAdminLabels\n /**\n * Required — omitting a guard would expose the whole user directory and its write actions.\n * 必須。省略を許すとユーザー名簿と全更新操作が無防備に露出するため、型で強制する。\n */\n guards: UserAdminGuards\n /**\n * Names who is acting, for audit columns. 監査列に残す「誰が」を解決する処理。\n *\n * ⚠️ 省略すると、要求を積んだ主体が記録されない。同期要求の表は「誰がいつ何を要求し、結果が\n * どうだったか」を残すために在り、その `requested_by` が定数だと、乱発を疑ったときに誰の\n * ものか判らない — 乱発こそがこの intent に `update` を要求した理由である。\n */\n resolveActor?: (request: Request) => Promise<string> | string\n /**\n * Lists the upstream groups the operator may link to. 連携先の候補を列挙するポート。\n *\n * 任意である。注入しない配備では選択欄を出さず、上流キーの手入力に留まる — 上流へ問い合わせる\n * 資格情報を web 側へ置かない構成を選べるようにするためである (同期ジョブだけが鍵を持つ形)。\n *\n * ⚠️ **失敗しても画面を落としてはならない。** 例外は loader が捕まえて\n * `{ kind: \"unavailable\" }` へ写す。ここが落ちると、グループ一覧すら開けなくなる。\n */\n directoryCatalog?: {\n listGroups: (request: Request) => Promise<UserAdminCatalogSnapshot>\n }\n /**\n * Per-request self-permission lookup for the header annotation. Resolving to a map of\n * {@link UserAdminPermission} shows the badge; rejecting shows an explicit \"unknown\" badge — never a\n * permissive one. Any authz library whose view carries `canRead` / `canWrite` / `canDelete` satisfies it.\n * ヘッダー注釈に自分の実効権限を表示するためのリクエスト別権限取得関数。失敗時は「権限不明」表示。\n */\n getMyPermissions?: (request: Request) => Promise<Record<string, UserAdminPermission | undefined>>\n /**\n * Display names for the resource keys the badge shows, e.g. `{ authz_admin: \"ユーザー管理 UI\" }`.\n * バッジに出すリソースキーの表示名。未指定のキーはキー文字列をそのまま表示。\n *\n * 表示名をこのパッケージ側に埋め込むと、別パッケージのリソースキーを名指しすることになる\n * (`AGENTS.md` のクロスパッケージ・ハードコード禁止)。中立な差し込み口として受け取る。\n */\n resourceLabels?: Record<string, string>\n}\n\n/**\n * Reads a required positive integer id from submitted form data.\n * 送信フォームから必須の正整数 ID を読み取る処理。\n *\n * @throws when the field is missing, non-numeric, or not a positive integer.\n * 欠落・非数値・正整数でない場合は throw。\n */\nfunction requireId(formData: FormData, field: string): number {\n const raw = formData.get(field)\n const n = Number(raw)\n // NaN / 小数 / 0 以下は不正な ID として弾く (Number(null) === 0 の取り違えも防ぐ)\n if (raw === null || String(raw).trim() === \"\" || !Number.isInteger(n) || n <= 0) {\n throw new UserAdminInputError(\"invalidField\", { field })\n }\n return n\n}\n\n/**\n * Reads a required non-empty trimmed string from submitted form data.\n * 送信フォームから必須の非空文字列 (トリム済み) を読み取る処理。\n *\n * @throws when the field is missing or blank. 欠落・空白のみの場合は throw。\n */\nfunction requireText(formData: FormData, field: string): string {\n const value = String(formData.get(field) ?? \"\").trim()\n if (value === \"\") throw new UserAdminInputError(\"invalidField\", { field })\n return value\n}\n\n/**\n * Reads an explicit boolean. 明示的な真偽値の読み取り。\n *\n * `\"true\"` / `\"false\"` のみを受け付ける。欠落を既定へ倒すと、破壊側 (停止) が既定方向になる。\n *\n * @throws when the field is absent or not one of the two literals. 明示指定でない場合。\n */\nfunction requireBoolean(formData: FormData, field: string): boolean {\n const raw = String(formData.get(field) ?? \"\")\n if (raw !== \"true\" && raw !== \"false\") throw new UserAdminInputError(\"invalidField\", { field })\n return raw === \"true\"\n}\n\n/**\n * Reads the membership expansion mode. 所属展開モードの読み取り。\n *\n * 既定へ倒さない。入れ子展開が黙って有効になると権限が静かに広がり、黙って無効になると\n * 入れ子側の全員が削除候補になる。どちらの向きにも倒さず、明示させる。\n *\n * @throws when the value is not one of the two modes. 2 値でない場合。\n */\nfunction requireMembershipMode(formData: FormData): AuthDirectoryMembershipMode {\n const raw = String(formData.get(\"membershipMode\") ?? \"\")\n if (raw !== \"direct\" && raw !== \"transitive\") throw new UserAdminInputError(\"invalidField\", { field: \"membershipMode\" })\n return raw\n}\n\n/** Reads an optional trimmed string; blank becomes `undefined`. 任意文字列の読み取り (空は undefined)。 */\nfunction optionalText(formData: FormData, field: string): string | undefined {\n const value = String(formData.get(field) ?? \"\").trim()\n return value === \"\" ? undefined : value\n}\n\n/**\n * Creates React Router loader & action handler for user administration.\n * ユーザー管理用 React Router の loader および action ハンドラーを作成する。\n *\n * `config.guards` は必須。loader / action の双方が、処理を始める前にガードを通過させる。\n */\nexport function createUserAdminApp(config: UserAdminAppConfig) {\n const labels = resolveUserAdminLabels(config.labels)\n\n /**\n * Names who is acting. 「誰が」を解決する処理。\n *\n * 解決できない配備では固定値を残す (記録そのものを落とすより、判らないと記録する方がよい)。\n *\n * @param request The incoming request. 受け取ったリクエスト。\n * @returns The actor label. 監査に残す主体。\n */\n async function actorOf(request: Request): Promise<string> {\n if (!config.resolveActor) return \"user-admin\"\n try {\n return (await config.resolveActor(request)) || \"user-admin\"\n } catch (error) {\n console.error(\"[@aiquants/auth-react-router] resolveActor failed; recording the request as an unnamed actor:\", error)\n return \"user-admin\"\n }\n }\n\n /**\n * Queues a sync for a group that was linked a moment ago, without letting the queue fail the link.\n * たった今連携したグループの同期を積む処理 (要求の失敗で連携の結果を覆さない)。\n *\n * ⚠️ 連携を作った時点で、そのグループの所属は次の全件走査 (既定 10 分) まで空のままである。\n * 運用者が別途「今すぐ同期」を押さなければならないことは、画面のどこにも書かれていない。\n * ここで積めば、常駐しているジョブが数秒で拾う。\n *\n * ⚠️ 例外を伝播させない。連携は **既に成功している** ので、ここで投げると `{ ok: false }` が\n * 返り「連携に失敗した」と伝わる一方で、上流所有のグループは増えている (作成対話が守った\n * 不可分性が、結果の報告で崩れる)。積めなかった事実は `not-queued` として画面に言わせる。\n *\n * @param groupId The group that was just linked. 連携したばかりのグループ。\n * @param request The incoming request, for the audit actor. 監査用の主体を解決するリクエスト。\n * @returns What the request did. 要求が行ったこと。\n */\n async function requestSyncAfterLink(groupId: number, request: Request): Promise<UserAdminLinkSyncOutcome> {\n try {\n return await config.store.requestDirectorySync(groupId, await actorOf(request))\n } catch (error) {\n console.error(`[@aiquants/auth-react-router] the link for group ${groupId} was created, but its sync request could not be queued:`, error)\n return \"not-queued\"\n }\n }\n\n /**\n * Decides whether this request may list the upstream catalog. 候補一覧を引いてよいかを決める処理。\n *\n * ⚠️ 候補一覧は外部組織の部門構成を列挙する。`read` で通すと、閲覧権限だけの主体がそれを\n * 読めてしまう。ここでは **連携を作る intent と同じ権限** を要求し、拒否は「見せない」へ写す。\n * 認可以外の理由 (ガードの内部エラー) でも見せない側へ倒す — 利便のための読み取りであり、\n * 判らないときに見せる理由が無い。\n *\n * @param request - The incoming request. 受け取ったリクエスト。\n * @returns True when the catalog may be fetched. 候補一覧を引いてよいなら true。\n */\n async function mayReadCatalog(request: Request): Promise<boolean> {\n if (!config.directoryCatalog) return false\n const actions = INTENT_ACCESS.get(\"catalog\")\n // ⚠️ 要求すべき権限が引けなければ **見せない**。空配列で回すと 0 回の検査で `true` を\n // 返すことになり、認可の補助が fail-open な方向へ倒れる\n if (actions === undefined || actions.length === 0) return false\n try {\n for (const action of actions) await config.guards.requireAccess(request, { action, intent: \"catalog\" })\n return true\n } catch (error) {\n // 拒否は握り潰さず監査へ流す。ここだけ記録が抜けると、「誰が何を見られなかったか」\n // という問いに答えられない範囲が生まれる\n try {\n config.guards.onDenied?.({ intent: \"catalog\", actions, error })\n } catch (hookError) {\n console.error(\"[@aiquants/auth-react-router] onDenied threw; the denial itself still stands:\", hookError)\n }\n return false\n }\n }\n\n async function loader({ request }: RouteArgs) {\n // 読取ガード: 拒否は throw されるためここで処理が打ち切られる\n await config.guards.requireAccess(request, { action: \"read\", intent: \"loader\" })\n const segment = segmentFromPathname(new URL(request.url).pathname)\n\n // 候補一覧は **必要な面だけ** で引く。全画面で引くと、上流への往復が毎回の描画に乗る\n const needsCatalog = (segment === \"groups\" || segment === \"directory\") && (await mayReadCatalog(request))\n const [users, groups, groupMembers, allowlist, directoryStatus, myPermissions] = await Promise.all([\n config.store.listUsers(),\n config.store.listGroups(),\n config.store.listGroupMembers(),\n config.store.listAllowlist(),\n config.store.getDirectoryStatus(),\n // 取得できない場合は null (= 権限不明)。空マップに倒すと「付与ゼロ」と区別できなくなる\n config.getMyPermissions\n ? config.getMyPermissions(request).catch((e) => {\n console.error(\"[@aiquants/auth-react-router] getMyPermissions failed; rendering the permission badge as unknown:\", e)\n return null\n })\n : Promise.resolve(null),\n ])\n // 陳腐化の上限は配備の状態から取る。候補一覧の取得はこの後 (上限を知ってから) 行う\n const catalog = await loadCatalog(config.directoryCatalog, needsCatalog, request, directoryStatus.stalenessThresholdSec)\n\n return {\n resourceLabels: config.resourceLabels,\n segment,\n users,\n groups,\n groupMembers,\n allowlist,\n directoryStatus,\n myPermissions,\n catalog,\n labels,\n }\n }\n\n async function action({ request }: RouteArgs) {\n // ⚠️ 本文を読む前に大きさで断る。intent は本文の中にあるので、ガードより先に読む以外の\n // 選択肢が無い — つまり **認証を通っていない要求でも本文が積まれる**。この画面が送るのは\n // 短い文字列だけなので、上限を先に見るだけで、その積み上がりを定数で抑えられる。\n // 送出側が長さを申告しない場合 (chunked) はここでは分からない。前段のリバースプロキシの\n // 本文上限が最終的な砦であり、この検査はその代わりではなく **手前の安い一段** である\n const declaredLength = Number(request.headers.get(\"content-length\") ?? Number.NaN)\n if (Number.isFinite(declaredLength) && declaredLength > MAX_ACTION_BODY_BYTES) {\n return data({ ok: false, error: labels.errors.requestTooLarge }, { status: 413 })\n }\n // intent の判定に FormData が要るため、ここでだけ本文を先に読む。\n // 未知 intent は「何も実行しない」ため、ガード前に読んでも権限判断には影響しない。\n const formData = await request.formData()\n const intent = String(formData.get(\"intent\") ?? \"\")\n\n // 未知 intent はストアへ到達しない (ガードも走らせず 400 相当を返す)。\n // Map 参照のためプロトタイプ継承キー (\"constructor\" 等) を拾わない。\n const access = INTENT_ACCESS.get(intent)\n // ⚠️ 受け取った値を応答へ echo しない。ここは認可より **手前** なので、認証を通っていない\n // POST でも到達する — echo すると、攻撃者が選んだ文字列を 200 で返す原始的な道具になり、\n // 管理面への探りが 2xx の列に紛れる。状態も 400 にして、成功の集計から外す\n if (!access) return data({ ok: false, error: labels.errors.unknownIntent }, { status: 400 })\n\n /** 403 かどうか。Response でも素の例外でも `status` は共通語彙である。 */\n const isDenied = (error: unknown) => isForbidden(error)\n /**\n * Turns a denial into an in-page error without leaking the guard's own message.\n * 拒否を、ガードの内部文言を漏らさずに画面内エラーへ変える処理。\n */\n const denied = (error: unknown) => {\n try {\n config.guards.onDenied?.({ intent, actions: access, error })\n } catch {\n // 記録に失敗しても拒否の応答は返す。ログの都合で操作の結果が変わってはならない\n }\n // 監視から消えないよう、状態は 403 のまま返す。画面は data() なので生き残る\n return data({ ok: false, error: labels.errors.forbidden }, { status: 403 })\n }\n\n // 操作ごとの CRUD 種別でガードする。複数を要求する intent は **すべて** 通ること\n try {\n for (const action of access) await config.guards.requireAccess(request, { action, intent })\n } catch (error) {\n // 403 は「この操作だけができない」であり、画面ごと落とすと **他の操作まで巻き添え** に\n // なる (閲覧と作成はできるのに削除だけ拒否された、で一覧が消える)。401 や再認証の\n // リダイレクト、それ以外の障害はそのまま伝播させる — 握り潰すとログインし直せない\n if (!isDenied(error)) throw error\n return denied(error)\n }\n\n try {\n return await dispatch(intent, formData, request)\n } catch (error) {\n // ガードの後 (ストアの中) で投げられた拒否も同じ扱いにする。ここを素通りさせると、\n // 拒否理由がそのまま利用者へ渡り、しかも HTTP は 200 になって監視から消える\n if (isDenied(error)) return denied(error)\n // ガード拒否 (Response/redirect) はそのまま伝播させる。ストア由来の業務エラーだけ\n // {ok:false,error} に写して UI へ届ける (ロックアウト理由・重複パターン等)\n if (error instanceof Response) throw error\n // 実装側で文を組まず、ラベルから引く (既定は英語の配備に日本語が出るのを避ける)\n if (error instanceof UserAdminInputError) {\n // ⚠️ `{field}` には **操作者が画面で見ている名前** を差し込む。通信上のキーを\n // そのまま出すと、日本語の文の中に `membershipMode` のような識別子が現れ、\n // どの欄のことなのか読み手には判らない。表に無い欄はキーのまま出す\n const raw = error.values.field\n const values = typeof raw === \"string\" ? { ...error.values, field: labels.errors.fieldNames[raw] ?? raw } : error.values\n const template = labels.errors[error.labelKey]\n return { ok: false, error: fillLabel(typeof template === \"string\" ? template : String(error.labelKey), values) }\n }\n // ⚠️ 例外文をそのまま返してよいのは、ストアが **運用者向けと宣言した** ものだけである。\n // ドライバの例外は一意制約名・表名・衝突した値・接続先をそのまま含み、それが閲覧権限\n // しか持たない主体の画面へ出る。原文は必ずサーバーのログへ残す\n console.error(`[@aiquants/auth-react-router] intent \"${intent}\" failed:`, error)\n return { ok: false, error: isOperatorFacing(error) ? error.message : labels.errors.unexpected }\n }\n }\n\n /**\n * Resolves the stable upstream id for a chosen key. 選ばれたキーに対応する安定 ID を決める処理。\n *\n * ⚠️ 正準キーは安定 ID である。アドレスは上流で改名され得るため、それだけを保存すると、改名\n * された瞬間にリンクが上流を見失う。一覧が引けているならそこから引く (画面の送信値を信用\n * しない)。引けていない配備 (手入力しか手段が無い) では送信値を使い、無ければアドレスを\n * そのまま置く — 同期が最初に成功した時点で、上流の本当の ID へ自動的に書き戻される。\n *\n * @param formData The submitted form. 送信されたフォーム。\n * @param externalKey The chosen upstream key. 選ばれた上流キー。\n * @param catalog The catalog resolved for this request. この要求で解決済みの候補一覧。\n * @returns The upstream id to persist. 保存する上流 ID。\n */\n function resolveExternalId(formData: FormData, externalKey: string, catalog: UserAdminCatalog): string {\n const submitted = String(formData.get(\"externalId\") ?? \"\").trim()\n if (catalog.kind === \"ready\") {\n // ⚠️ 送られてきた安定 ID が候補一覧に在るなら、それを優先する。アドレスは一意とは\n // 限らない (別名行、改名の途中、別ドメインの同名) ため、アドレスで引き当てると\n // 選択欄で 2 件目を選んだ利用者に 1 件目を結び付けることになる。取り消せない委譲を\n // 取り違えたまま確定させないため、識別子が在るときは識別子で決める\n if (submitted !== \"\" && catalog.groups.some((g) => g.externalId === submitted)) return submitted\n const wanted = normalizeUpstreamKey(externalKey)\n const match = catalog.groups.find((g) => normalizeUpstreamKey(g.externalKey) === wanted)\n if (match) return match.externalId\n }\n return submitted === \"\" ? externalKey : submitted\n }\n\n /**\n * Refuses the link intents when this deployment has no directory. ディレクトリ未設定の配備で連携を拒否する処理。\n *\n * ⚠️ 画面は連携の欄そのものを出さないが、それは **UI だけの約束** である。組み立てた POST は\n * 通ってしまい、`provider` が空、あるいは配備と食い違う行が残る。そういう行は同期側が\n * 「別プロバイダ」として恒久的に無視するため、**連携済みに見えて永久に同期されない** グループが\n * できる。加えて供給元は送られてきた値ではなく配備の状態から取る — 唯一の源はここである。\n *\n * @returns The deployment's provider key. この配備の供給元キー。\n * @throws {UserAdminInputError} When the directory is not configured. ディレクトリ未設定の場合。\n */\n function requireDirectoryEnabled(status: AuthDirectoryStatus): string {\n const provider = status.provider.trim()\n // ⚠️ 前後の空白を落として返す。落とさずに保存すると、同期側の `!==` 比較が恒久的に\n // 「別プロバイダ」と判定し、連携済みに見えて永久に同期されないグループができる\n if (!status.isEnabled || provider === \"\") throw new UserAdminInputError(\"directoryDisabled\")\n return provider\n }\n\n /**\n * Refuses an upstream group that is already linked to another local group.\n * 既に別のローカルグループへ連携している上流グループを拒否する処理。\n *\n * 画面は連携済みの選択肢を選べなくしているが、それは **UI だけの約束** である。二重に連携すると\n * 同じ上流の所属が 2 つのグループへ投影され、メンバーが二重に数えられ、両方のロールが配られる。\n *\n * @param externalKey The upstream key about to be linked. 連携しようとしている上流キー。\n * @throws {UserAdminInputError} When it is already linked. 既に連携している場合。\n */\n async function assertNotAlreadyLinked(externalId: string, externalKey: string): Promise<void> {\n // ⚠️ 突合の第一キーは **安定 ID** である。アドレスで比べると、上流でグループが改名された\n // 直後 (旧アドレスは別名として残る) に、まだ再同期していないリンクとは一致せず、同じ上流\n // グループが 2 つのローカルグループへ投影される。DB の一意制約は ID 側に張ってあるため、\n // アドレスだけで検査すると制約も後ろ盾にならない\n const wantedId = normalizeUpstreamKey(externalId)\n const wantedKey = normalizeUpstreamKey(externalKey)\n const groups = await config.store.listGroups()\n const clash = groups.find((g) => g.externalLink !== undefined && (normalizeUpstreamKey(g.externalLink.externalId) === wantedId || normalizeUpstreamKey(g.externalLink.externalKey) === wantedKey))\n if (clash) throw new UserAdminInputError(\"alreadyLinked\", { key: externalKey, group: clash.name })\n }\n\n /**\n * Refuses an upstream key the catalog does not list. 候補一覧に無い上流キーを拒否する処理。\n *\n * 候補一覧を注入していない配備では検査できない。**検査できないことを黙って通す** のは\n * 望ましくないが、ここで拒否すると手入力しか手段が無い配備で連携が一切作れなくなる。\n * 「一覧が在るなら一覧に従う」という条件付きの検査であることを、意図として明示しておく。\n *\n * @param externalKey The key the operator chose or typed. 選択または入力された上流キー。\n * @param catalog The catalog already resolved for this request. この要求で解決済みの候補一覧。\n * @throws {UserAdminInputError} When a catalog exists and does not list the key. 一覧が在り、そこに無い場合。\n */\n function assertKnownUpstream(externalId: string, externalKey: string, catalog: UserAdminCatalog): void {\n // 一覧が引けていないときは検査しない。**ここで拒否すると、上流が落ちている間は連携が\n // 一切作れなくなる** — しかも画面は「アドレスを手入力してください」と案内している。\n // 検査は綴り違いを早く知らせるためのものであり、可用性を落としてよい理由にはならない\n if (catalog.kind !== \"ready\") return\n const wantedId = normalizeUpstreamKey(externalId)\n const wantedKey = normalizeUpstreamKey(externalKey)\n // ⚠️ 2 欄が **同じ 1 件を指していること** まで確かめる。片方だけ照合すると、一覧に在る\n // アドレスと、別のグループの安定 ID を組み合わせた送信が通ってしまう\n if (!catalog.groups.some((g) => normalizeUpstreamKey(g.externalId) === wantedId && normalizeUpstreamKey(g.externalKey) === wantedKey)) {\n throw new UserAdminInputError(\"unknownUpstream\", { key: externalKey })\n }\n }\n\n /** intent ごとの実処理。認可・入力検証を通過した後にのみ呼ばれる。 */\n async function dispatch(intent: string, formData: FormData, request: Request) {\n // 候補一覧は **必要な intent に入ってから** 読む。先に読むと、上流と無関係な操作\n // (利用者の作成、許可リストの編集) にまで上流への往復が乗る\n // 配備の状態はこの送信で 1 度だけ読む。`requireDirectoryEnabled` が読んだものを使い回す —\n // 読み直すと 1 回の連携作成で 4 往復になり、しかも `provider` と鮮度上限が別の瞬間の\n // スナップショットから来ることになる\n let cachedStatus: AuthDirectoryStatus | null = null\n const status = async () => {\n cachedStatus ??= await config.store.getDirectoryStatus()\n return cachedStatus\n }\n const catalog = async () => loadCatalog(config.directoryCatalog, true, request, (await status()).stalenessThresholdSec)\n if (intent === \"createUser\") {\n await config.store.createUser({ displayName: requireText(formData, \"displayName\"), email: requireText(formData, \"email\") })\n return { ok: true }\n }\n\n if (intent === \"updateUser\") {\n await config.store.updateUser(requireId(formData, \"userId\"), { displayName: requireText(formData, \"displayName\"), email: requireText(formData, \"email\") })\n return { ok: true }\n }\n\n if (intent === \"toggleUserActive\") {\n // 破壊側 (無効化) を既定にしない。明示的な \"true\"/\"false\" 以外は不正入力として弾く。\n const rawActive = String(formData.get(\"isActive\") ?? \"\")\n if (rawActive !== \"true\" && rawActive !== \"false\") throw new UserAdminInputError(\"invalidField\", { field: \"isActive\" })\n await config.store.toggleUserActive(requireId(formData, \"userId\"), rawActive === \"true\")\n return { ok: true }\n }\n\n if (intent === \"createGroup\") {\n await config.store.createGroup({ groupKey: requireText(formData, \"groupKey\"), name: requireText(formData, \"name\"), description: optionalText(formData, \"description\") })\n return { ok: true }\n }\n\n if (intent === \"updateGroup\") {\n await config.store.updateGroup(requireId(formData, \"groupId\"), { name: requireText(formData, \"name\"), description: optionalText(formData, \"description\") })\n return { ok: true }\n }\n\n if (intent === \"deleteGroup\") {\n await config.store.deleteGroup(requireId(formData, \"groupId\"))\n return { ok: true }\n }\n\n if (intent === \"addGroupMember\") {\n await config.store.addGroupMember(requireId(formData, \"groupId\"), requireId(formData, \"userId\"))\n return { ok: true }\n }\n\n if (intent === \"removeGroupMember\") {\n await config.store.removeGroupMember(requireId(formData, \"groupId\"), requireId(formData, \"userId\"))\n return { ok: true }\n }\n\n if (intent === \"addAllowlistEntry\") {\n const type = requireAllowlistType(formData)\n await config.store.addAllowlistEntry({ pattern: requireText(formData, \"pattern\"), type, description: optionalText(formData, \"description\") })\n return { ok: true }\n }\n\n if (intent === \"addAllowlistGroup\") {\n await config.store.addAllowlistGroup(requireId(formData, \"groupId\"), optionalText(formData, \"description\"))\n return { ok: true }\n }\n if (intent === \"removeAllowlistGroup\") {\n await config.store.removeAllowlistGroup(requireId(formData, \"entryId\"))\n return { ok: true }\n }\n if (intent === \"createLinkedGroup\") {\n const externalKey = requireText(formData, \"externalKey\")\n const provider = requireDirectoryEnabled(await status())\n // 一覧はこの送信で 1 度だけ読む。安定 ID の解決と存在検査で 2 度読むと、その分だけ\n // 上流への往復が増える\n const resolved = await catalog()\n const externalId = resolveExternalId(formData, externalKey, resolved)\n await assertNotAlreadyLinked(externalId, externalKey)\n // 候補一覧が在るなら、選択肢に無いキーは受け付けない。手入力の綴り違いは「10 分後に\n // 赤いバナー」ではなく、その場で分かるべきである\n assertKnownUpstream(externalId, externalKey, resolved)\n const group = await config.store.createLinkedGroup({\n groupKey: requireText(formData, \"groupKey\"),\n name: requireText(formData, \"name\"),\n description: optionalText(formData, \"description\"),\n provider,\n externalId,\n externalKey,\n membershipMode: requireMembershipMode(formData),\n })\n return { ok: true, syncRequest: await requestSyncAfterLink(group.id, request) }\n }\n if (intent === \"linkDirectoryGroup\") {\n const externalKey = requireText(formData, \"externalKey\")\n const provider = requireDirectoryEnabled(await status())\n const resolved = await catalog()\n const externalId = resolveExternalId(formData, externalKey, resolved)\n await assertNotAlreadyLinked(externalId, externalKey)\n assertKnownUpstream(externalId, externalKey, resolved)\n const groupId = requireId(formData, \"groupId\")\n await config.store.linkDirectoryGroup(groupId, {\n provider,\n externalId,\n externalKey,\n membershipMode: requireMembershipMode(formData),\n })\n return { ok: true, syncRequest: await requestSyncAfterLink(groupId, request) }\n }\n if (intent === \"unlinkDirectoryGroup\") {\n await config.store.unlinkDirectoryGroup(requireId(formData, \"groupId\"))\n return { ok: true }\n }\n if (intent === \"setDirectorySyncEnabled\") {\n await config.store.setDirectorySyncEnabled(requireId(formData, \"groupId\"), requireBoolean(formData, \"isSyncEnabled\"))\n return { ok: true }\n }\n if (intent === \"requestDirectorySync\") {\n const raw = String(formData.get(\"groupId\") ?? \"\").trim()\n const outcome = await config.store.requestDirectorySync(raw === \"\" ? null : requireId(formData, \"groupId\"), await actorOf(request))\n // ⚠️ 何が起きたかを画面へ返す。`{ ok: true }` だけを返すと、積まれなかったことが\n // 成功と見分けられず、押しても反応しない画面になる\n return { ok: true, syncRequest: outcome }\n }\n if (intent === \"removeAllowlistEntry\") {\n await config.store.removeAllowlistEntry(requireId(formData, \"entryId\"))\n return { ok: true }\n }\n\n // INTENT_ACCESS を通過した intent は上のいずれかで処理済み (到達しない安全網)\n // 権限表を通ったのに分岐が無い intent は、表と実装の食い違いである (`catalog` のような\n // 読み取り専用キーが POST で来た場合を含む)。利用者へ値は返さず、ログにだけ残す\n console.error(`[@aiquants/auth-react-router] intent \"${intent}\" is in the permission table but has no handler`)\n return data({ ok: false, error: labels.errors.unknownIntent }, { status: 400 })\n }\n\n return { loader, action }\n}\n","/**\n * @module admin/format\n * @description Text helpers shared by the shell and the views.\n * シェルとビューが共有する文言整形の補助。\n *\n * 数や名前を JSX で連結すると語順が英語に固定され、日付を実行環境任せで整形すると\n * サーバーとブラウザで別の文字列になる (水和が壊れる)。どちらもラベル側の宣言に従わせる。\n */\nimport type { UserAdminLabels } from \"./labels\"\n\n/**\n * Substitutes `{key}` placeholders in a label template.\n * ラベルの雛形に含まれる `{key}` を差し替える処理。\n *\n * ⚠️ **1 回の走査で置き換え、差し替え値は文字列として扱う。** キーごとに `replaceAll` を重ねると\n * 2 つの穴が開く。(1) 先に入れた値の中の `{key}` が後の回で **もう一度** 置き換えられる。\n * (2) `replaceAll` の第 2 引数は `$&` `` $` `` `$1` を置換パターンとして解釈するため、値に `$`\n * が含まれると周囲の文言が複製されたり消えたりする。この関数は取り消せない操作の確認文\n * (「{group} の所属を {upstream} に委ねますか?」) を組み立てるため、上流のグループ名を操れる\n * 相手が **もっともらしい別の確認文** を作れてしまう。置換関数を使えばどちらも起きない。\n *\n * 未知のキーは雛形のまま残す。空文字へ倒すと、差し替え忘れが文の欠落として静かに通る。\n *\n * @param template Label text containing `{key}` placeholders. `{key}` を含むラベル文言。\n * @param values Replacement values by key. キーごとの差し替え値。\n * @returns The filled text. 差し替え済みの文言。\n */\nexport function fill(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{(\\w+)\\}/g, (match, key: string) => (Object.hasOwn(values, key) ? String(values[key]) : match))\n}\n\n/**\n * Formats an ISO timestamp with the locale the labels declare, degrading instead of throwing.\n * ISO 時刻を **ラベルが宣言したロケール** で整形する処理 (例外を投げず、読める形へ退避する)。\n *\n * `Intl.DateTimeFormat().format()` は不正な日時で `RangeError` を投げる (`toLocaleString()` が\n * 「Invalid Date」を返すのとは違う)。この関数を呼ぶのはシェルの同期バナーであり、バナーは\n * **全タブで描かれる**。上流の 1 行が壊れた時刻を持っていただけで、ユーザー管理面が丸ごと\n * エラー境界へ落ちる。ロケール文字列が空でも同じく投げるため、そこも退避する。\n *\n * オフセットの無い ISO 文字列 (`2026-03-01T09:00:00`) は仕様上 **実行環境の時間帯** で解釈される。\n * サーバーとブラウザで別の時刻になり水和が壊れるため、UTC として読む。\n *\n * @param iso ISO 8601 timestamp. ISO 8601 の時刻 (オフセット無しは UTC として解釈)。\n * @param labels Resolved label set carrying locale and time zone. ロケールと時間帯を持つラベル。\n * @returns Formatted text, or the input itself when it cannot be formatted. 整形済みの文言 (整形できない場合は入力そのもの)。\n */\nexport function formatDateTime(iso: string, labels: UserAdminLabels): string {\n // オフセットが無ければ UTC を補う (実行環境の時間帯で解釈させない)\n const normalized = /(?:Z|[+-]\\d{2}:?\\d{2})$/.test(iso) ? iso : `${iso}Z`\n try {\n return new Intl.DateTimeFormat(labels.locale, { timeZone: labels.timeZone, dateStyle: \"medium\", timeStyle: \"short\" }).format(new Date(normalized))\n } catch {\n // 不正な日時 (`RangeError: Invalid time value`) も、不正なロケール・時間帯も、ここで\n // 受け止める。画面を落とすより生の値を見せる方がまだ役に立つ\n return iso\n }\n}\n","/**\n * @module admin/ui\n * @description The shell around the five tab views: heading, tabs, badges and banners.\n * 5 つのタブビューを囲む外枠 (見出し・タブ・バッジ・告知)。\n */\nimport type { AuthDirectoryStatus, AuthGroup } from \"@aiquants/auth-core\"\nimport type React from \"react\"\nimport { useEffect, useRef } from \"react\"\nimport { Link, useLoaderData, useRevalidator } from \"react-router\"\nimport { fill, formatDateTime } from \"./format\"\nimport { defaultUserAdminLabels, type UserAdminLabels } from \"./labels\"\nimport type { UserAdminPermission } from \"./server\"\n\n/**\n * How the console keeps itself current while \"sync now\" requests are waiting for the job.\n * 「今すぐ同期」の要求がジョブを待っている間、画面が自分を最新に保つ間隔と上限。\n *\n * The loader only re-runs after the console's own submissions, so a request the job finishes a few\n * seconds later would stay invisible until a reload. While at least one request is pending the\n * console re-validates every `intervalMs`, and gives up after `maxMs` so a deployment with no job\n * consuming the queue does not poll forever from every open tab.\n * ローダーは自分の送信のあとにしか再実行されないため、ジョブが数秒後に終えた要求はリロードまで\n * 見えない。未処理の要求が 1 件でもある間は `intervalMs` ごとに再検証し、`maxMs` で諦める\n * (キューを消化するジョブが居ない配備で、開いている全タブが永久に叩き続けないため)。\n */\nexport type PendingSyncPolling = {\n /** Milliseconds between re-validations. 再検証の間隔 (ミリ秒)。 */\n intervalMs: number\n /** Milliseconds after which polling stops for an unchanged pending count. 件数が変わらないまま諦めるまでの時間 (ミリ秒)。 */\n maxMs: number\n}\n\n/** Defaults: every 3 seconds, for up to 5 minutes. 既定値 (3 秒ごと、最長 5 分)。 */\nexport const DEFAULT_PENDING_SYNC_POLLING: PendingSyncPolling = { intervalMs: 3_000, maxMs: 300_000 }\n\nexport type UserAdminShellProps = {\n children?: React.ReactNode\n renderHeader?: (props: { title: string; annotation: React.ReactNode }) => React.ReactNode\n /** Polling while sync requests are pending; defaults to {@link DEFAULT_PENDING_SYNC_POLLING}. 未処理中のポーリング設定。 */\n pendingSyncPolling?: PendingSyncPolling\n}\n\n/**\n * Re-validates the route data periodically while sync requests are waiting for the job.\n * 同期要求がジョブを待っている間、ルートのデータを定期的に再検証する処理。\n *\n * @param pendingRequestCount Requests not yet finished, from the loader. ローダーが返した未処理件数。\n * @param polling Interval and cap. 間隔と上限。\n * @returns Nothing. 戻り値なし。\n */\nfunction usePendingSyncRevalidation(pendingRequestCount: number, polling: PendingSyncPolling): void {\n const revalidator = useRevalidator()\n // 進行中の再検証に重ねない。重ねると応答順が入れ替わり、古い結果が新しい結果を上書きし得る。\n // 効果のクロージャは古い state を掴むので、毎描画で更新する参照から読む\n const stateRef = useRef(revalidator.state)\n stateRef.current = revalidator.state\n const { revalidate } = revalidator\n const { intervalMs, maxMs } = polling\n\n useEffect(() => {\n if (pendingRequestCount <= 0) return\n // 件数が変わるたびに効果が張り直され、上限も測り直される。上限が効くのは件数が\n // 変わらないまま (誰にも拾われないまま) 時間だけが過ぎる場合である\n const startedAt = Date.now()\n const timer = setInterval(() => {\n if (Date.now() - startedAt >= maxMs) {\n clearInterval(timer)\n return\n }\n if (stateRef.current === \"idle\") void revalidate()\n }, intervalMs)\n return () => clearInterval(timer)\n }, [pendingRequestCount, intervalMs, maxMs, revalidate])\n}\n\n/**\n * Render one resource's permission summary as a compact pill.\n * 1 リソース分の権限サマリをコンパクトなタグとして描画するコンポーネント。\n */\nfunction GroupPill({ label, summary }: { label: string; summary: { text: string; color: string; bgColor: string; borderColor: string } }) {\n return (\n <span\n style={{\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: \"0.18rem\",\n fontSize: \"0.65rem\",\n padding: \"0.08rem 0.35rem\",\n borderRadius: \"9999px\",\n backgroundColor: summary.bgColor,\n color: summary.color,\n border: `1px solid ${summary.borderColor}`,\n fontWeight: 600,\n lineHeight: \"1.2\",\n }}>\n <span>{label}:</span>\n <span>{summary.text}</span>\n </span>\n )\n}\n\n/**\n * Warns when synchronization is not actually keeping the mirror fresh.\n * 同期が実際には鏡を新しく保てていないことを警告する部品。\n *\n * 同期の停止は「上流から外れた人がテナントに居続ける」というセキュリティ事象である。件数と\n * エラー数だけを見ると何も起きていないように読めるため、**滞留・全停止・未結線の 3 つを\n * それぞれ名指しで** 出す。出さなければ、状態を計算している意味がない。\n *\n * どのタブに居ても見えるようにシェル側で描く。ディレクトリの停止はグループ一覧を見ている\n * ときだけ起きる事象ではない。\n */\nfunction DirectorySyncBanner({ status, labels, groups }: { status?: AuthDirectoryStatus; labels: UserAdminLabels; groups: AuthGroup[] }) {\n if (!status || status.linkedGroupCount === 0) return null\n const L = labels.directoryView\n const warnings: Array<{ id: string; text: string }> = []\n if (!status.isEnabled) warnings.push({ id: \"disabled\", text: L.disabledWarning })\n if (status.pausedGroupCount > 0) warnings.push({ id: \"paused\", text: fill(L.pausedTemplate, { paused: status.pausedGroupCount, total: status.linkedGroupCount }) })\n if (status.isStale) {\n const at = status.oldestSyncedAt ? formatDateTime(status.oldestSyncedAt, labels) : L.neverSynced\n warnings.push({ id: \"stale\", text: `${L.staleWarning} ${fill(L.oldestSyncedTemplate, { at })}` })\n }\n if (status.erroredGroupIds.length > 0) {\n // ID を並べても運用者には何のグループか判らない。名前と、あれば実際の失敗理由を出す\n const described = status.erroredGroupIds.map((id) => {\n const group = groups.find((g) => g.id === id)\n if (!group) return fill(labels.common.idTemplate, { id })\n const reason = group.externalLink?.syncError\n return reason ? fill(L.erroredItemTemplate, { group: group.name, reason }) : group.name\n })\n warnings.push({ id: \"errored\", text: fill(L.erroredTemplate, { groups: described.join(L.listSeparator) }) })\n }\n if (status.pendingRequestCount > 0) warnings.push({ id: \"pending\", text: fill(L.pendingTemplate, { count: status.pendingRequestCount }) })\n if (warnings.length === 0) return null\n return (\n // ライブリージョンにはしない。初期描画から在る内容は読み上げられず、見出しも読み順も\n // 失われる。**読める本文** として置き、見出しから辿れるようにする\n <section\n aria-labelledby=\"directory-sync-heading\"\n data-testid=\"directory-sync-banner\"\n style={{ display: \"flex\", flexDirection: \"column\", gap: \"0.35rem\", padding: \"0.7rem 1rem\", marginBottom: \"1rem\", borderRadius: \"6px\", border: \"1px solid #fde68a\", backgroundColor: \"#fffbeb\", color: \"#92400e\", fontSize: \"0.85rem\" }}>\n <h2 id=\"directory-sync-heading\" style={{ margin: 0, fontSize: \"0.9rem\", fontWeight: 700 }}>\n {L.title}\n </h2>\n {warnings.map((w) => (\n <span key={w.id} data-directory-warning={w.id}>\n {w.text}\n </span>\n ))}\n </section>\n )\n}\n\n/**\n * Shell layout for user administration pages with header annotation delegation.\n * ヘッダー注釈委譲を備えたユーザー管理ページのシェルレイアウト。\n */\nexport function UserAdminShell({ children, renderHeader, pendingSyncPolling }: UserAdminShellProps) {\n // ⚠️ ここは `UserAdminLoaderData` と違い **すべて任意** で受ける。シェルは loader を持たない\n // 呼び出し (エラー境界の外側、テスト、部分描画) からも描かれるため、欠落を落ちる理由にしない\n const data = useLoaderData() as { segment?: string; labels?: UserAdminLabels; myPermissions?: Record<string, UserAdminPermission | undefined> | null; resourceLabels?: Record<string, string>; directoryStatus?: AuthDirectoryStatus; groups?: AuthGroup[] } | undefined\n // 同期状態が無い呼び出し (ディレクトリ未結線・部分描画) に未処理の要求は存在しない\n usePendingSyncRevalidation(data?.directoryStatus?.pendingRequestCount ?? 0, pendingSyncPolling ?? DEFAULT_PENDING_SYNC_POLLING)\n const segment = data?.segment ?? \"users\"\n const labels = data?.labels ?? defaultUserAdminLabels\n // null = 未結線または取得失敗。空 {} も「分からない」扱いで、いずれも許可として描画しない\n const myPermissions = data?.myPermissions ?? null\n // リソースキーの表示名は呼び出し側が差し込む (キーをこのパッケージに埋め込まない)\n const resourceLabels = data?.resourceLabels ?? {}\n const labelFor = (resourceKey: string) => (Object.hasOwn(resourceLabels, resourceKey) ? resourceLabels[resourceKey] : resourceKey)\n\n const tabs = [\n { key: \"users\", label: labels.tabs.users, href: \"/users\" },\n { key: \"groups\", label: labels.tabs.groups, href: \"/users/groups\" },\n { key: \"group_members\", label: labels.tabs.groupMembers, href: \"/users/group_members\" },\n { key: \"allowlist\", label: labels.tabs.allowlist, href: \"/users/allowlist\" },\n ]\n // 未結線でも **連携が残っていれば** 出す。停止を警告した直後に、解除や再開ができる唯一の\n // 面だけが消えるのは最悪の挙動である。連携も結線も無い配備でだけ隠す。\n // 判定は状態の件数と一覧の実体の **両方** を見る — 片方だけだと、件数が 0 なのに\n // 「上流が所有」と描かれるグループが取り残される\n const hasLinkedGroup = (data?.groups ?? []).some((g) => g.externalLink)\n if (data?.directoryStatus?.isEnabled || (data?.directoryStatus?.linkedGroupCount ?? 0) > 0 || hasLinkedGroup) {\n tabs.push({ key: \"directory\", label: labels.tabs.directory, href: \"/users/directory\" })\n }\n\n const permEntries = Object.entries(myPermissions ?? {})\n\n /**\n * Pick the badge text and colours for one resource, never rendering \"unknown\" as \"allowed\".\n * 1 リソース分のバッジ文言と配色を決める処理。**未知を許可として描かない** ことが要件。\n *\n * 以前は権限マップが空のとき「編集可」の固定値を描いていたため、ポート未結線・取得失敗・\n * 付与ゼロのいずれでも「全操作できます」と表示されていた (実際には操作するとガードで拒否される)。\n */\n const unavailableSummary = { text: labels.permissions.unknown, color: \"#92400e\", bgColor: \"#fffbeb\", borderColor: \"#fde68a\" }\n const getSummary = (perm: UserAdminPermission | undefined) => {\n // ⚠️ **鍵は在るが値が無い** のは「権限なし」ではなく「判らない」である。取得側の型が\n // `undefined` を許す以上、その差を潰すと、拒否理由が「バッジを見てください」と案内する\n // 相手に嘘のバッジを見せることになる\n if (perm === undefined) return unavailableSummary\n if (perm.canWrite) {\n return { text: labels.permissions.canWrite, color: \"#047857\", bgColor: \"#ecfdf5\", borderColor: \"#a7f3d0\" }\n }\n if (perm.canRead) {\n return { text: labels.permissions.canRead, color: \"#1d4ed8\", bgColor: \"#eff6ff\", borderColor: \"#bfdbfe\" }\n }\n // ⚠️ 削除だけを持つ主体は実在する。読み書きが無いことを「権限なし」と描くと、\n // 実際には削除できる相手に「何もできない」と言うことになる\n if (perm.canDelete) {\n return { text: labels.permissions.canDelete, color: \"#b91c1c\", bgColor: \"#fef2f2\", borderColor: \"#fecaca\" }\n }\n return { text: labels.permissions.none, color: \"#475569\", bgColor: \"#f1f5f9\", borderColor: \"#cbd5e1\" }\n }\n const annotation = (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"0.4rem\" }} data-testid=\"user-admin-header-annotation\">\n <span style={{ fontSize: \"0.75rem\", backgroundColor: \"#e0f2fe\", color: \"#0369a1\", padding: \"2px 8px\", borderRadius: \"12px\", fontWeight: 600 }}>{labels.heading}</span>\n\n <div style={{ display: \"inline-flex\", alignItems: \"center\", gap: \"0.25rem\", fontSize: \"0.68rem\", color: \"#475569\" }} data-testid=\"user-admin-permissions-badge\">\n <span style={{ fontWeight: 600, color: \"#334155\" }}>{labels.permissions.label}</span>\n <span style={{ display: \"inline-flex\", alignItems: \"center\", gap: \"0.18rem\" }}>\n {permEntries.length === 0 ? (\n <span data-testid=\"user-admin-permission-unavailable\">\n <GroupPill label={labels.heading} summary={unavailableSummary} />\n </span>\n ) : (\n permEntries.map(([resKey, perm]) => <GroupPill key={resKey} label={labelFor(resKey)} summary={getSummary(perm)} />)\n )}\n </span>\n </div>\n </div>\n )\n\n return (\n <div data-testid=\"user-admin-shell\" style={{ width: \"100%\", minHeight: \"100vh\", backgroundColor: \"#f8fafc\", padding: \"1.5rem\", boxSizing: \"border-box\" }}>\n {renderHeader ? (\n renderHeader({ title: labels.title, annotation })\n ) : (\n <div style={{ marginBottom: \"1.5rem\", display: \"flex\", justifyContent: \"space-between\", alignItems: \"center\" }}>\n <h1 style={{ margin: 0, fontSize: \"1.5rem\", fontWeight: 700, color: \"#0f172a\" }}>{labels.title}</h1>\n {annotation}\n </div>\n )}\n\n <nav aria-label={labels.title} style={{ display: \"flex\", gap: \"0.5rem\", marginBottom: \"1.2rem\", borderBottom: \"2px solid #e2e8f0\", paddingBottom: \"0.5rem\" }}>\n {tabs.map((tab) => {\n const isActive = segment === tab.key\n return (\n <Link\n key={tab.key}\n to={tab.href}\n data-testid={`user-admin-tab-${tab.key}`}\n aria-current={isActive ? \"page\" : undefined}\n style={{\n padding: \"0.5rem 1rem\",\n borderRadius: \"6px 6px 0 0\",\n fontSize: \"0.88rem\",\n fontWeight: isActive ? 700 : 500,\n color: isActive ? \"#0284c7\" : \"#64748b\",\n backgroundColor: isActive ? \"#ffffff\" : \"transparent\",\n textDecoration: \"none\",\n borderBottom: isActive ? \"3px solid #0284c7\" : \"3px solid transparent\",\n transition: \"all 0.15s ease\",\n }}>\n {tab.label}\n </Link>\n )\n })}\n </nav>\n\n <DirectorySyncBanner status={data?.directoryStatus} labels={labels} groups={data?.groups ?? []} />\n\n <main>{children}</main>\n </div>\n )\n}\n","/**\n * @module admin/views\n * @description The five tab views of the user administration screen.\n * ユーザー管理画面を構成する 5 つのタブビュー。\n *\n * 一覧・検索・所属・許可リスト・ディレクトリ連携を描く。データはすべて loader から渡され、\n * 書き込みはビュー唯一の fetcher (`useSubmitThen`) を通す。\n */\nimport type { AuthAllowlistEntry, AuthDirectoryMembershipMode, AuthDirectoryStatus, AuthGroup, AuthGroupMember, AuthUser } from \"@aiquants/auth-core\"\nimport { getHighlightSegments, type Option, type RenderOptionContext, SelectBox, type SelectBoxTexts } from \"@aiquants/select-box\"\nimport type React from \"react\"\nimport { useEffect, useMemo, useRef, useState } from \"react\"\nimport { Link, useFetcher, useLoaderData, useSearchParams } from \"react-router\"\nimport { fill, formatDateTime } from \"./format\"\nimport { defaultUserAdminLabels, type UserAdminLabels } from \"./labels\"\nimport { normalizeUpstreamKey, type UserAdminCatalog, type UserAdminCatalogGroup } from \"./server\"\nimport { UserAdminShell, type UserAdminShellProps } from \"./ui\"\n\nexport type UserAdminLoaderData = {\n segment: string\n users: AuthUser[]\n groups: AuthGroup[]\n groupMembers: AuthGroupMember[]\n allowlist: AuthAllowlistEntry[]\n directoryStatus: AuthDirectoryStatus\n /** 上流の候補一覧。選択欄を出す面でのみ `ready` になる。 */\n catalog: UserAdminCatalog\n labels?: UserAdminLabels\n}\n\n/**\n * Renders the error returned by the last action submission, if any.\n * 直近の action 送信が返したエラーを表示する部品。\n *\n * ロックアウト理由・重複パターン・入力不正などの文言は action が `{ok:false,error}` で返す。\n * 表示しなければ操作が黙って失敗したように見えるため、各ビューの先頭に必ず置く。\n */\nfunction ActionError({ result: raw, id }: { result: unknown; id?: string }) {\n const result = raw as { ok?: boolean; error?: string } | undefined\n if (result?.ok !== false || !result.error) return null\n return (\n // グリッド配下でも行を跨ぐ。空のときは描かないので、余分な行も生まれない\n <div id={id} data-testid=\"user-admin-action-error\" role=\"alert\" style={{ gridColumn: \"1 / -1\", padding: \"0.7rem 1rem\", borderRadius: \"6px\", border: \"1px solid #fecaca\", backgroundColor: \"#fef2f2\", color: \"#b91c1c\", fontSize: \"0.85rem\" }}>\n {result.error}\n </div>\n )\n}\n\n/**\n * Submits through the view's single fetcher and defers \"close and clear\" until the server accepts.\n * ビュー唯一の fetcher で送信し、**サーバーが受理してから** 「閉じて消す」を行うためのフック。\n *\n * 送信直後に閉じて入力を消すと、認可拒否や検証エラーで戻ったときに打ち込んだ内容が丸ごと\n * 失われ、利用者は全部打ち直すことになる。成功応答 (`ok === true`) を待って初めて破棄する。\n *\n * ⚠️ **このビューの送信はすべてここを通すこと。** fetcher は 1 つを共有しており、後の送信は\n * 前の送信を打ち切る。素の `fetcher.submit` を混ぜると、打ち切られた送信の後始末が **後の\n * 送信の成功応答で** 実行される (行の操作を挟んだだけで、フォームの入力が消える)。ここを\n * 通せば後の送信が `pending` を必ず上書きするため、後始末は常に最後の送信のものになる。\n */\nfunction useSubmitThen(fetcher: ReturnType<typeof useFetcher>) {\n const pending = useRef<((data: unknown) => void) | null>(null)\n // 破棄した結果の世代。`fetcher.data` は宣言的に消せないため、こちらで無効化する\n const [discarded, setDiscarded] = useState<unknown>(undefined)\n const result = fetcher.data === discarded ? undefined : fetcher.data\n\n useEffect(() => {\n if (fetcher.state !== \"idle\" || fetcher.data === undefined) return\n const onSuccess = pending.current\n if (!onSuccess) return\n // 同じ応答で 2 度実行しないよう、消費した時点で捨てる。ここは **冗長な防御** である —\n // すべての送信がこのフックを通る限り、後の送信が必ず `pending` を上書きするため。\n // 素の `fetcher.submit` が 1 つでも混ざった瞬間に効き始める\n pending.current = null\n // ⚠️ 後始末には応答そのものを渡す。`ok === true` は「受理した」であって「要求どおりに\n // なった」ではない — 積まれなかった要求も受理として返る。区別できない形にすると、\n // 押した控えだけが成功の見た目で残る\n if ((fetcher.data as { ok?: boolean }).ok === true) onSuccess(fetcher.data)\n }, [fetcher.state, fetcher.data])\n\n const submit = (payload: Record<string, string>, onSuccess?: (data: unknown) => void) => {\n pending.current = onSuccess ?? null\n setDiscarded(undefined)\n fetcher.submit(payload, { method: \"post\" })\n }\n /**\n * Forgets the last result so an unrelated failure cannot describe a freshly opened dialog.\n * 直前の結果を忘れる処理 (無関係な失敗が、開いたばかりのダイアログの説明文にならないように)。\n */\n const resetResult = () => setDiscarded(fetcher.data)\n\n return { submit, resetResult, result }\n}\n\n/**\n * Gives a dialog the behavior its ARIA attributes promise: focus, trap, Escape, restore.\n * ダイアログに、ARIA 属性が約束している挙動 (初期焦点・封じ込め・Escape・復帰) を与えるフック。\n *\n * 役割属性だけを付けても、焦点は背後に残り、Tab は外へ出て行き、Escape は効かない。\n * キーボード利用者は「閉じる」に到達できず、支援技術は背後を読み続ける。\n *\n * @param isOpen Whether the dialog is mounted and visible. ダイアログが開いているかどうか。\n * @param onClose Invoked when Escape is pressed. Escape が押されたときに呼ばれる処理。\n * @returns Ref to attach to the dialog panel. ダイアログ本体へ付ける ref。\n */\nfunction useModalBehavior(isOpen: boolean, onClose: () => void, openerRef?: React.RefObject<HTMLElement | null>) {\n const ref = useRef<HTMLFormElement>(null)\n // 再描画のたびに実行されると入力中に焦点が先頭へ飛ぶため、依存は開閉だけにする\n const closeRef = useRef(onClose)\n closeRef.current = onClose\n\n useEffect(() => {\n if (!isOpen) return\n const panel = ref.current\n // `activeElement` を見るだけでは戻し先にならない。押下で焦点が乗らないブラウザ\n // (Safari / macOS の Firefox) では body へ戻ってしまう。開いた要素を明示的に覚える\n const opener = openerRef?.current ?? (document.activeElement as HTMLElement | null)\n /**\n * Tab が実際に止まる要素だけを、文書順で返す。\n *\n * ⚠️ 同じ `name` のラジオ群は、ブラウザにとって **停止点 1 つ** である (選択中の 1 つ)。\n * 両方を輪に入れると 2 つずれる — 先頭で Shift+Tab したときに罠が働かず `aria-modal` の\n * 対話の外へ抜け、末尾から Tab したときは選択されていない 1px のラジオへ焦点が飛ぶ。\n * 初期焦点だけがこの配慮をしていて、輪はしていなかった\n */\n const focusables = () => {\n // ⚠️ 最後の `[tabindex]` 節は、前の節が除いた **無効な要素を連れ戻す**。無効かつ\n // `tabindex=\"0\"` の要素は実在する (仮想スクロールの矢印ボタンがそれで、popup の中に\n // 常に 2 つ出る)。輪の端がそういう要素になると、`preventDefault` の後に `focus()` が\n // 黙って何もしないため、**そこから Tab で出られない** 本物の罠になる。\n // `inert` と非表示の部分木も同様に、到達できない停止点を輪へ持ち込む\n const all = Array.from(panel?.querySelectorAll<HTMLElement>('a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex=\"-1\"])') ?? []).filter(\n (element) => !((element as HTMLElement & { disabled?: boolean }).disabled || element.hasAttribute(\"inert\")) && element.closest(\"[inert]\") === null && element.getAttribute(\"aria-hidden\") !== \"true\",\n )\n const seenRadioGroups = new Set<string>()\n return all.filter((element) => {\n const radio = element instanceof HTMLInputElement && element.type === \"radio\" ? element : null\n if (!radio || radio.name === \"\") return true\n // 群の代表は選択中の 1 つ。どれも選択されていなければ先頭が停止点になる\n if (!radio.checked && all.some((other) => other instanceof HTMLInputElement && other.type === \"radio\" && other.name === radio.name && other.checked)) return false\n if (seenRadioGroups.has(radio.name)) return false\n seenRadioGroups.add(radio.name)\n return true\n })\n }\n // ⚠️ 初期焦点は **見える** 要素へ置く。1px に切り詰めた入力 (ラジオ群の実体) へ置くと、\n // 焦点環はその 1px の箱に描かれ、操作者からは「どこにも焦点が無い」ようにしか見えない。\n // Tab の輪からは外さない — 見えないだけで、到達できる必要はある。\n // 寸法では判定しない (jsdom は常に 0 を答え、検査が空砲になる)。意図を属性で示す\n const items = focusables()\n ;(items.find((element) => !element.hasAttribute(\"data-aqar-visually-hidden\")) ?? items[0])?.focus()\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") {\n // 変換中の Escape は「候補を取り消す」であって「閉じる」ではない。ここで\n // 閉じると、編集中の下書きは状態ごと捨てられ、打ち直しになる\n if (event.isComposing || event.keyCode === 229) return\n // ⚠️ 内側の部品が既に処理した Escape で対話ごと閉じない。選択欄は「一覧を閉じる」\n // ために Escape を消費し、`preventDefault` で消費済みだと示した上で、対話が\n // 反応できるよう伝播は止めない。ここで既定動作の有無を見ないと、**一覧を畳む\n // つもりの 1 打鍵で対話全体と入力内容が消える**\n if (event.defaultPrevented) return\n event.preventDefault()\n closeRef.current()\n return\n }\n if (event.key !== \"Tab\") return\n const items = focusables()\n if (items.length === 0) return\n const first = items[0]\n const last = items[items.length - 1]\n const active = document.activeElement\n const outside = !panel?.contains(active)\n if (event.shiftKey && (active === first || outside)) {\n event.preventDefault()\n last.focus()\n } else if (!event.shiftKey && (active === last || outside)) {\n event.preventDefault()\n first.focus()\n }\n }\n document.addEventListener(\"keydown\", onKeyDown)\n return () => {\n document.removeEventListener(\"keydown\", onKeyDown)\n opener?.focus?.()\n }\n }, [isOpen, openerRef])\n\n return ref\n}\n\n/**\n * Turns catalog entries into picker options, marking the ones that are already linked.\n * 候補一覧を選択肢へ変える処理 (すでに連携済みのものに印を付ける)。\n *\n * 連携済みを **隠さず、選べなくする** のは、運用者が探しているグループが「無い」のか\n * 「既に繋がっている」のかを区別できるようにするためである。隠すと、見つからない理由が判らない。\n *\n * @param catalog The loader's catalog state. loader が渡した候補一覧の状態。\n * @param linkedTo Upstream key to the local group already holding it. 上流キー → それを押さえているローカルグループ名。\n * @param labels Resolved labels. 解決済みラベル。\n * @returns Options for the picker. 選択欄に渡す選択肢。\n */\nfunction upstreamOptions(catalog: UserAdminCatalog, linkedTo: Map<string, string>, labels: UserAdminLabels): UpstreamOption[] {\n if (catalog.kind !== \"ready\") return []\n return catalog.groups.map((entry) => {\n // ⚠️ 「連携済み」だけでは、どのローカルグループが押さえているのか判らず、探しようも\n // 解除しようも無い。名指しできる情報は手元にあるので、名指しする。\n // 突合は **安定 ID を第一キー、アドレスを第二キー** とする (サーバー側と同じ規則)。\n // アドレスだけで見ると、上流で改名された直後は選べるように見えて、押すと拒否される\n const holder = linkedTo.get(normalizeUpstreamKey(entry.externalId)) ?? linkedTo.get(normalizeUpstreamKey(entry.externalKey))\n const linked = holder !== undefined\n const members = entry.memberCount === undefined ? \"\" : fill(labels.groupsView.upstreamOptionMembers, { count: entry.memberCount })\n return {\n // ⚠️ 名前とアドレスの **両方を label に入れる**。選択欄が描くのも探すのも label だけで\n // あり、別の項目へ入れた値は表示も検索もされない。同名のグループ (別部門の「営業部」等)\n // を見分けられないまま選ばせるのが、この機能で最も避けたい取り違えである\n label: fill(labels.directoryView.groupOptionTemplate, { name: entry.name, key: entry.externalKey }),\n // ⚠️ 選択肢を区別するのは **安定 ID** である。アドレスは一意とは限らない (別名行、\n // 改名の途中、別ドメインの同名) し、選んだ後に控えを引き当てるのもこの値である。\n // アドレスを鍵にすると、2 件目を選んだのに 1 件目の名前が出る — 取り違えたまま\n // 取り消せない委譲 (作成と連携) を押すことになる\n value: entry.externalId,\n note: linked ? fill(labels.groupsView.upstreamAlreadyLinked, { group: holder }) : members,\n ...(linked ? { disabled: true } : {}),\n entry,\n }\n })\n}\n\n/**\n * States when the catalog was observed, loudly when it is older than the deployment allows.\n * 候補一覧をいつ観測したかを示す表示。配備が許す鮮度を越えていれば強く出す。\n *\n * ⚠️ この一覧は上流を直に叩いた結果ではなく、資格情報を持つ同期ジョブが書いた **写し** である。\n * 時刻を出さないと「上流の現在」と読まれ、既に消えたグループを選ばせる。逆に、作ったばかりの\n * グループが出てこない理由も、時刻を見て初めて分かる。\n *\n * ⚠️ 陳腐化の判定は **loader が済ませている**。ここで `Date.now()` を読むと、時計が遅れている\n * 端末では経過が負になり警告が消える (安全側の警告が利用者側の入力で無効化される)。加えて、\n * 描画時に読むと開いたままの画面では判定が更新されず、水和のずれも生む。\n */\nfunction CatalogFreshness({ observedAt, isStale, labels }: { observedAt: string; isStale: boolean; labels: UserAdminLabels }): React.ReactNode {\n return (\n <p data-testid=\"catalog-freshness\" data-stale={isStale ? \"\" : undefined} style={{ margin: \"0.35rem 0 0 0\", fontSize: \"0.72rem\", color: isStale ? \"#92400e\" : \"#64748b\" }}>\n {fill(isStale ? labels.groupsView.catalogStale : labels.groupsView.catalogObservedAt, { at: formatDateTime(observedAt, labels) })}\n </p>\n )\n}\n\n/**\n * Whether an action result carries a message worth describing the dialog with.\n * 操作結果が、対話の説明として指す価値のある文言を持っているか。\n *\n * ⚠️ `aria-describedby` を常に張ると、誤りが無い間 **解決できない IDREF** を指し続ける。支援技術は\n * 黙って無視するため実害は小さいが、「説明が在るのに読めない」状態を常設することになる。\n *\n * @param result The action result, if any. 操作結果 (無い場合あり)。\n * @returns True when an error message exists. エラー文言が在れば true。\n */\nfunction hasError(result: unknown): boolean {\n return typeof result === \"object\" && result !== null && (result as { ok?: unknown }).ok === false && typeof (result as { error?: unknown }).error === \"string\"\n}\n\n/**\n * 一致箇所の装飾。\n *\n * ⚠️ **選択欄パッケージの内部クラス名を書かない。** `sb-highlight--*` はそのパッケージの公開 API\n * ではなく、向こうが patch リリースで改名した瞬間に、こちらは何のエラーも出さずに装飾を失う。\n * 自前の値で描き、印は自パッケージの `data-*` に持つ。\n */\nconst matchStyle: Record<\"exact\" | \"fuzzy\", React.CSSProperties> = {\n exact: { backgroundColor: \"#d1fae5\", color: \"#064e3b\", borderRadius: \"2px\", fontWeight: 600 },\n fuzzy: { backgroundColor: \"#fef3c7\", color: \"#78350f\", borderRadius: \"2px\", fontWeight: 600 },\n}\n\n/**\n * Turns the sync-request part of an action result into a notice, or nothing when there is nothing to say.\n * 操作結果のうち同期要求の部分を告知文へ写す処理 (言うことが無ければ `null`)。\n *\n * ⚠️ `queued` は告げない。積めたことの結果は、数秒後の再検証で `最終同期` が進むことそのもの\n * である。告げるのは **押しても何も積まれなかった** 2 つの場合だけで、黙ると押しても反応しない\n * 画面になる。\n *\n * @param actionResult The fetcher's last result. 直前の操作結果。\n * @param labels Active labels. 使用中のラベル。\n * @returns The notice text, or `null`. 告知文、または `null`。\n */\nfunction syncRequestNoticeOf(actionResult: unknown, labels: UserAdminLabels): string | null {\n if (typeof actionResult !== \"object\" || actionResult === null) return null\n const outcome = (actionResult as { syncRequest?: unknown }).syncRequest\n if (outcome === \"already-pending\") return labels.directoryView.syncAlreadyPending\n if (outcome === \"not-queued\") return labels.directoryView.linkSyncNotQueued\n return null\n}\n\n/**\n * Indexes linked groups by both upstream keys. 連携済みグループを 2 つの上流キーで索引する処理。\n *\n * 安定 ID とアドレスの両方を引けるようにする。上流が改名すると、リンク行のアドレスは次の同期まで\n * 古いままであり、アドレスだけの索引では「連携済み」を見落とす。\n *\n * @param groups Local groups, linked or not. ローカルグループ (連携の有無を問わない)。\n * @returns Upstream key to the local group name holding it. 上流キー → 押さえているローカル名。\n */\nfunction upstreamIndex(groups: readonly AuthGroup[]): Map<string, string> {\n const index = new Map<string, string>()\n for (const group of groups) {\n const link = group.externalLink\n if (!link) continue\n index.set(normalizeUpstreamKey(link.externalId), group.name)\n index.set(normalizeUpstreamKey(link.externalKey), group.name)\n }\n return index\n}\n\n/** 選択欄 1 行分。`note` は選べない理由か規模を添える。 */\ntype UpstreamOption = { label: string; value: string; note: string; disabled?: boolean; entry: UserAdminCatalogGroup }\n\n/**\n * Renders one upstream option: the searchable label, plus why it cannot be picked.\n * 上流の選択肢 1 行を描く処理 (検索対象のラベルと、選べない理由)。\n *\n * 既定の描画は `label` しか出さないため、`disabled` の理由が伝わらない。薄く見えるだけの行は、\n * 読み込み中や壊れている行と区別できない。\n */\nfunction renderUpstreamOption(option: Option, context: RenderOptionContext): React.ReactNode {\n const note = typeof option.note === \"string\" ? option.note : \"\"\n return (\n // ⚠️ 行は幅で切り詰められる。切り詰められるのは末尾、つまりアドレス — 同名のグループを\n // 見分ける唯一の手がかりである。全文を `title` に持たせて、少なくとも確かめられるようにする\n <span data-aqar-upstream-option title={note === \"\" ? option.label : `${option.label} — ${note}`} style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"baseline\", gap: \"0.75rem\", width: \"100%\", minWidth: 0 }}>\n <span data-aqar-upstream-option-label style={{ overflow: \"hidden\", textOverflow: \"ellipsis\", whiteSpace: \"nowrap\" }}>\n {/* 独自描画は既定の一致ハイライトを失う。同じ関数と同じ装飾クラスを使って復元する\n (打った語がどこに当たったのか判らないと、絞り込みの手応えが消える) */}\n {getHighlightSegments(option.label, context.query).map((segment, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: 分割位置そのものが同一性であり、他に安定した鍵が無い\n <span key={index} data-aqar-match={segment.matchType === \"none\" ? undefined : segment.matchType} style={segment.matchType === \"none\" ? undefined : matchStyle[segment.matchType]}>\n {segment.text}\n </span>\n ))}\n </span>\n {note === \"\" ? null : (\n <span data-aqar-upstream-option-note style={{ flexShrink: 0, fontSize: \"0.72rem\", opacity: 0.75 }}>\n {note}\n </span>\n )}\n </span>\n )\n}\n\n/**\n * The texts `@aiquants/select-box` renders itself, taken from this package's labels.\n * 選択欄が自前で描く文言を、このパッケージのラベルから与える処理。\n *\n * 渡さないと件数・空状態・クリア・開閉の読み上げが英語のまま出る — 和文の配備でそこだけ英語になる。\n */\nfunction pickerTexts(labels: UserAdminLabels): SelectBoxTexts {\n const L = labels.directoryView\n return {\n noOptions: L.pickerNoOptions,\n clear: L.pickerClear,\n toggle: L.pickerToggle,\n selection: (selected) => fill(L.pickerSelected, { value: selected.map((o) => o.label).join(\", \") }),\n status: (info) => fill(L.pickerAvailable, { count: info.count }),\n }\n}\n\n/**\n * Builds a collator that cannot crash the view. 画面を落とさない照合器を作る処理。\n *\n * `Intl.Collator` は BCP 47 として読めないタグに `RangeError` を投げる。並べ替えは表示だけの\n * 関心事なのに、投げられると一覧そのものが描けなくなる。読めないタグは環境既定に倒す。\n *\n * @param locale Requested locale tag. 要求されたロケールタグ。\n * @returns A comparison function for display strings. 表示文字列用の比較関数。\n */\nfunction collatorFor(locale: string): (a: string, b: string) => number {\n try {\n return new Intl.Collator(locale).compare\n } catch {\n return new Intl.Collator().compare\n }\n}\n\n/**\n * Derives a local group key from an upstream address. 上流アドレスからローカルのグループキーを導く処理。\n *\n * アドレスのローカル部だけを使い、鍵に使えない文字を落とす。運用者が手で打ち直す手間を省くための\n * **初期値** であり、確定値ではない (画面上で編集できる)。\n *\n * @param externalKey Upstream address. 上流のアドレス。\n * @returns A usable group key. グループキーとして使える文字列。\n */\nfunction groupKeyFromUpstream(externalKey: string): string {\n const local = externalKey.split(\"@\")[0] ?? externalKey\n return local\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n}\n\nexport function UsersView() {\n const data = useLoaderData() as UserAdminLoaderData\n const fetcher = useFetcher()\n const { submit: submitThen, resetResult, result: actionResult } = useSubmitThen(fetcher)\n const labels = data.labels ?? defaultUserAdminLabels\n\n const [searchQuery, setSearchQuery] = useState(\"\")\n // 2 つの独立した状態にすると「両方開いている」状態が表現でき、エラー欄が 2 つ描かれる。\n // 排他であることを型で持つ\n const [modal, setModal] = useState<{ kind: \"create\" } | { kind: \"edit\"; user: AuthUser } | null>(null)\n const editingUser = modal?.kind === \"edit\" ? modal.user : null\n const isCreateOpen = modal?.kind === \"create\"\n const openerRef = useRef<HTMLElement | null>(null)\n /** ダイアログを開く。直前の操作の結果を持ち越さない (別の操作の拒否が説明文になる)。 */\n const openModal = (next: { kind: \"create\" } | { kind: \"edit\"; user: AuthUser }, event: React.MouseEvent<HTMLElement>) => {\n openerRef.current = event.currentTarget\n resetResult()\n // ⚠️ 前回の下書きを残すと、別人を追加するつもりで開いた欄に前の人の Email が入っている。\n // 見えている表示名だけ書き換えて送ると、**別人のサインイン identity を持つ利用者** ができる\n if (next.kind === \"create\") {\n setNewDisplayName(\"\")\n setNewEmail(\"\")\n }\n setModal(next)\n }\n\n const [newDisplayName, setNewDisplayName] = useState(\"\")\n const [newEmail, setNewEmail] = useState(\"\")\n const createUserRef = useModalBehavior(isCreateOpen, () => setModal(null), openerRef)\n const editUserRef = useModalBehavior(editingUser !== null, () => setModal(null), openerRef)\n\n const busy = fetcher.state !== \"idle\"\n // 並べ替えは表示だけの関心事なので状態は画面に持つ。URL へ載せると共有できるが、\n // 「同じ URL を開いたのに並びが違う」という取り違えを防ぐには loader 側の合意が要る\n const [sort, setSort] = useState<{ column: \"id\" | \"displayName\" | \"email\" | \"isActive\"; ascending: boolean }>({ column: \"displayName\", ascending: true })\n const compareText = collatorFor(labels.locale)\n // メールクライアントやチャットから貼った値はほぼ必ず末尾に空白を伴い、IME は全角空白を作る。\n // trim しないと、存在する利用者が「登録が無い」ように見える\n const userNeedle = searchQuery.trim().toLowerCase()\n const filteredUsers = data.users\n .filter((u) => u.displayName.toLowerCase().includes(userNeedle) || u.email.toLowerCase().includes(userNeedle))\n .slice()\n .sort((a, b) => {\n const direction = sort.ascending ? 1 : -1\n if (sort.column === \"id\") return (a.id - b.id) * direction\n if (sort.column === \"isActive\") return (Number(a.isActive) - Number(b.isActive)) * direction\n // 表示名と Email は言語に依存する。ロケールを固定して、環境ごとに並びが変わるのを防ぐ\n return compareText(a[sort.column], b[sort.column]) * direction\n })\n\n /** 同じ列を押したら向きだけ変える。別の列なら昇順から始める。 */\n const toggleSort = (column: typeof sort.column) => {\n setSort((current) => (current.column === column ? { column, ascending: !current.ascending } : { column, ascending: true }))\n }\n\n const handleToggleActive = (user: AuthUser) => {\n // ⚠️ `aria-disabled` は焦点を残すために `disabled` の代わりに使っている。押せてしまう\n // ぶん、実際に断るのはここである\n if (busy) return\n // 無効化はサインインを止める。有効化は元に戻すだけなので確認しない\n if (user.isActive && !window.confirm(fill(labels.usersView.confirmDeactivate, { user: user.displayName }))) return\n submitThen({ intent: \"toggleUserActive\", userId: String(user.id), isActive: String(!user.isActive) })\n }\n\n const handleCreateUser = (e: React.FormEvent) => {\n e.preventDefault()\n if (!(newDisplayName && newEmail)) return\n // 送信した値と今の値が違うなら、往復の間に打ち込まれたものである。消してはならない\n const sentName = newDisplayName\n const sentEmail = newEmail\n submitThen({ intent: \"createUser\", displayName: sentName, email: sentEmail }, () => {\n setNewDisplayName((current) => (current === sentName ? \"\" : current))\n setNewEmail((current) => (current === sentEmail ? \"\" : current))\n setModal(null)\n })\n }\n\n const handleUpdateUser = (e: React.FormEvent) => {\n e.preventDefault()\n if (!editingUser) return\n submitThen({ intent: \"updateUser\", userId: String(editingUser.id), displayName: editingUser.displayName, email: editingUser.email }, () => setModal(null))\n }\n\n return (\n <section data-testid=\"users-view-section\" style={{ display: \"flex\", flexDirection: \"column\", gap: \"1rem\" }}>\n <h2 style={heading}>{labels.usersView.title}</h2>\n {/* モーダルが開いている間はモーダル内に描く。両方描くと同じ hook が 2 つになる */}\n {!modal && <ActionError result={actionResult} />}\n <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"center\", backgroundColor: \"#ffffff\", padding: \"1rem\", borderRadius: \"8px\", border: \"1px solid #e2e8f0\" }}>\n <input\n type=\"text\"\n aria-label={labels.usersView.searchLabel}\n placeholder={labels.usersView.searchPlaceholder}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n data-testid=\"users-search-input\"\n style={{ padding: \"6px 12px\", borderRadius: \"6px\", border: \"1px solid #cbd5e1\", fontSize: \"0.85rem\", width: \"260px\" }}\n />\n <button\n type=\"button\"\n onClick={(event) => openModal({ kind: \"create\" }, event)}\n data-testid=\"users-add-button\"\n style={{ padding: \"6px 14px\", borderRadius: \"6px\", backgroundColor: \"#0284c7\", color: \"#ffffff\", border: \"none\", fontSize: \"0.85rem\", fontWeight: 600, cursor: \"pointer\" }}>\n {labels.usersView.addUser}\n </button>\n </div>\n\n {/* 絞り込みの結果は視覚的にしか変わらない。読み上げへ届けるには live region が要る —\n 無いと、打ち込むたびに何件残ったのか判らないまま表全体を辿ることになる */}\n <p data-testid=\"users-summary\" role=\"status\" aria-live=\"polite\" style={{ margin: 0, fontSize: \"0.78rem\", color: \"#475569\" }}>\n {fill(labels.usersView.listSummary, { shown: filteredUsers.length, total: data.users.length })}\n </p>\n\n <div style={{ backgroundColor: \"#ffffff\", borderRadius: \"8px\", border: \"1px solid #e2e8f0\", overflow: \"hidden\" }}>\n <table style={{ width: \"100%\", borderCollapse: \"collapse\", fontSize: \"0.85rem\", textAlign: \"left\" }}>\n <thead>\n <tr style={{ backgroundColor: \"#f8fafc\", borderBottom: \"1px solid #e2e8f0\", color: \"#475569\" }}>\n {(\n [\n [\"id\", labels.common.id],\n [\"displayName\", labels.usersView.displayName],\n [\"email\", labels.usersView.email],\n [\"isActive\", labels.common.status],\n ] as const\n ).map(([column, heading]) => (\n <th key={column} scope=\"col\" style={{ padding: 0 }} aria-sort={sort.column === column ? (sort.ascending ? \"ascending\" : \"descending\") : \"none\"}>\n <button\n type=\"button\"\n data-testid={`users-sort-${column}`}\n onClick={() => toggleSort(column)}\n // 向きを名前に含める。含めないと、押しても押し返しても同じ\n // 「Sort by 名前」としか読まれず、今どちらなのか判らない\n aria-label={fill(sort.column === column ? (sort.ascending ? labels.usersView.sortedAscending : labels.usersView.sortedDescending) : labels.usersView.sortBy, { column: heading })}\n style={{ width: \"100%\", padding: \"10px 14px\", textAlign: \"left\", background: \"transparent\", border: \"none\", cursor: \"pointer\", font: \"inherit\", color: \"inherit\", fontWeight: sort.column === column ? 700 : 500 }}>\n {heading}\n {/* 向きは記号だけに頼らない。読み上げは aria-sort が担う */}\n {/* 使っていない列に ▲ を薄く出すと「昇順で並んでいる」と読めてしまう。\n 並べ替えていない列は向きを持たない記号を出す */}\n <span aria-hidden=\"true\" style={{ marginLeft: \"4px\", opacity: sort.column === column ? 1 : 0.35 }}>\n {sort.column === column ? (sort.ascending ? \"▲\" : \"▼\") : \"⇅\"}\n </span>\n </button>\n </th>\n ))}\n <th scope=\"col\" style={{ padding: \"10px 14px\", textAlign: \"right\" }}>\n {labels.common.actions}\n </th>\n </tr>\n </thead>\n <tbody>\n {filteredUsers.map((u) => (\n <tr key={u.id} style={{ borderBottom: \"1px solid #f1f5f9\" }} data-testid={`user-row-${u.id}`}>\n <td style={{ padding: \"10px 14px\", color: \"#475569\" }}>{fill(labels.common.idTemplate, { id: u.id })}</td>\n <td style={{ padding: \"10px 14px\", fontWeight: 600, color: \"#0f172a\" }}>{u.displayName}</td>\n <td style={{ padding: \"10px 14px\", color: \"#475569\" }}>{u.email}</td>\n <td style={{ padding: \"10px 14px\" }}>\n <span style={{ fontSize: \"0.75rem\", padding: \"2px 8px\", borderRadius: \"10px\", backgroundColor: u.isActive ? \"#dcfce7\" : \"#fee2e2\", color: u.isActive ? \"#15803d\" : \"#b91c1c\", fontWeight: 600 }}>\n {u.isActive ? labels.usersView.active : labels.usersView.inactive}\n </span>\n </td>\n <td style={{ padding: \"10px 14px\", textAlign: \"right\" }}>\n <button\n type=\"button\"\n onClick={(event) => openModal({ kind: \"edit\", user: u }, event)}\n aria-label={fill(labels.usersView.editUserAria, { user: u.displayName })}\n data-testid={`edit-user-btn-${u.id}`}\n style={{ padding: \"3px 8px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", backgroundColor: \"#ffffff\", fontSize: \"0.78rem\", cursor: \"pointer\", marginRight: \"6px\" }}>\n {labels.common.edit}\n </button>\n <button\n type=\"button\"\n onClick={() => handleToggleActive(u)}\n {...rowActionProps(busy)}\n aria-label={fill(u.isActive ? labels.usersView.deactivateAria : labels.usersView.activateAria, { user: u.displayName })}\n data-testid={`toggle-user-btn-${u.id}`}\n style={{ padding: \"3px 8px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", backgroundColor: \"#ffffff\", fontSize: \"0.78rem\", cursor: busy ? \"default\" : \"pointer\", color: u.isActive ? \"#b91c1c\" : \"#15803d\" }}>\n {u.isActive ? labels.usersView.deactivate : labels.usersView.activate}\n </button>\n </td>\n </tr>\n ))}\n {filteredUsers.length === 0 && (\n <tr>\n <td colSpan={5} style={{ padding: \"2rem\", textAlign: \"center\", color: \"#475569\" }}>\n {/* 「検索で 0 件」と「そもそも登録が無い」は別の事態である。同じ文言に\n すると、新規テナントを開いた運用者は一覧が壊れていると読む */}\n {userNeedle === \"\" ? labels.usersView.empty : labels.usersView.emptyFiltered}\n </td>\n </tr>\n )}\n </tbody>\n </table>\n </div>\n\n {/* Create Modal */}\n {isCreateOpen && (\n <div style={{ position: \"fixed\", inset: 0, backgroundColor: \"rgba(0,0,0,0.4)\", display: \"flex\", justifyContent: \"center\", alignItems: \"center\", zIndex: 100 }}>\n <form\n ref={createUserRef}\n onSubmit={handleCreateUser}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"create-user-title\"\n {...(hasError(actionResult) ? { \"aria-describedby\": \"create-user-error\" } : {})}\n data-testid=\"user-create-modal\"\n style={{ backgroundColor: \"#ffffff\", padding: \"1.5rem\", borderRadius: \"8px\", width: \"360px\", boxShadow: \"0 10px 25px rgba(0,0,0,0.15)\" }}>\n <h3 id=\"create-user-title\" style={{ margin: \"0 0 1rem 0\", fontSize: \"1.1rem\" }}>\n {labels.usersView.addUser}\n </h3>\n <ActionError result={actionResult} id=\"create-user-error\" />\n <div style={{ marginBottom: \"1rem\" }}>\n <label htmlFor=\"create-user-display-name-input\" style={{ display: \"block\", fontSize: \"0.8rem\", color: \"#475569\", marginBottom: \"4px\" }}>\n {labels.usersView.displayName}\n </label>\n <input\n id=\"create-user-display-name-input\"\n type=\"text\"\n value={newDisplayName}\n onChange={(e) => setNewDisplayName(e.target.value)}\n required\n data-testid=\"create-user-display-name\"\n style={{ width: \"100%\", padding: \"6px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", boxSizing: \"border-box\" }}\n />\n </div>\n <div style={{ marginBottom: \"1.2rem\" }}>\n <label htmlFor=\"create-user-email-input\" style={{ display: \"block\", fontSize: \"0.8rem\", color: \"#475569\", marginBottom: \"4px\" }}>\n {labels.usersView.email}\n </label>\n <input\n id=\"create-user-email-input\"\n type=\"email\"\n value={newEmail}\n onChange={(e) => setNewEmail(e.target.value)}\n required\n data-testid=\"create-user-email\"\n style={{ width: \"100%\", padding: \"6px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", boxSizing: \"border-box\" }}\n />\n </div>\n <div style={{ display: \"flex\", justifyContent: \"flex-end\", gap: \"0.5rem\" }}>\n <button type=\"button\" onClick={() => setModal(null)} data-testid=\"cancel-create-user\" style={{ padding: \"6px 12px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", backgroundColor: \"#ffffff\", cursor: \"pointer\" }}>\n {labels.common.cancel}\n </button>\n <button type=\"submit\" data-testid=\"submit-create-user\" disabled={busy} aria-busy={busy} style={{ padding: \"6px 12px\", borderRadius: \"4px\", backgroundColor: \"#0284c7\", color: \"#ffffff\", border: \"none\", cursor: \"pointer\" }}>\n {labels.common.create}\n </button>\n </div>\n </form>\n </div>\n )}\n\n {/* Edit Modal */}\n {editingUser && (\n <div style={{ position: \"fixed\", inset: 0, backgroundColor: \"rgba(0,0,0,0.4)\", display: \"flex\", justifyContent: \"center\", alignItems: \"center\", zIndex: 100 }}>\n <form\n ref={editUserRef}\n onSubmit={handleUpdateUser}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"edit-user-title\"\n {...(hasError(actionResult) ? { \"aria-describedby\": \"edit-user-error\" } : {})}\n data-testid=\"user-edit-modal\"\n style={{ backgroundColor: \"#ffffff\", padding: \"1.5rem\", borderRadius: \"8px\", width: \"360px\", boxShadow: \"0 10px 25px rgba(0,0,0,0.15)\" }}>\n <h3 id=\"edit-user-title\" style={{ margin: \"0 0 1rem 0\", fontSize: \"1.1rem\" }}>\n {labels.usersView.editUser}\n </h3>\n <ActionError result={actionResult} id=\"edit-user-error\" />\n <div style={{ marginBottom: \"1rem\" }}>\n <label htmlFor=\"edit-user-display-name-input\" style={{ display: \"block\", fontSize: \"0.8rem\", color: \"#475569\", marginBottom: \"4px\" }}>\n {labels.usersView.displayName}\n </label>\n <input\n id=\"edit-user-display-name-input\"\n type=\"text\"\n value={editingUser.displayName}\n onChange={(e) => setModal({ kind: \"edit\", user: { ...editingUser, displayName: e.target.value } })}\n required\n data-testid=\"edit-user-display-name\"\n style={{ width: \"100%\", padding: \"6px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", boxSizing: \"border-box\" }}\n />\n </div>\n <div style={{ marginBottom: \"1.2rem\" }}>\n <label htmlFor=\"edit-user-email-input\" style={{ display: \"block\", fontSize: \"0.8rem\", color: \"#475569\", marginBottom: \"4px\" }}>\n {labels.usersView.email}\n </label>\n <input\n id=\"edit-user-email-input\"\n type=\"email\"\n value={editingUser.email}\n onChange={(e) => setModal({ kind: \"edit\", user: { ...editingUser, email: e.target.value } })}\n required\n data-testid=\"edit-user-email\"\n style={{ width: \"100%\", padding: \"6px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", boxSizing: \"border-box\" }}\n />\n </div>\n <div style={{ display: \"flex\", justifyContent: \"flex-end\", gap: \"0.5rem\" }}>\n <button type=\"button\" onClick={() => setModal(null)} style={{ padding: \"6px 12px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", backgroundColor: \"#ffffff\", cursor: \"pointer\" }}>\n {labels.common.cancel}\n </button>\n <button type=\"submit\" data-testid=\"submit-edit-user\" disabled={busy} aria-busy={busy} style={{ padding: \"6px 12px\", borderRadius: \"4px\", backgroundColor: \"#0284c7\", color: \"#ffffff\", border: \"none\", cursor: \"pointer\" }}>\n {labels.common.save}\n </button>\n </div>\n </form>\n </div>\n )}\n </section>\n )\n}\n\n/**\n * View for managing user groups (create, edit, delete).\n * ユーザーグループ管理ビュー (作成・編集・削除)。\n */\nexport function GroupsView() {\n const data = useLoaderData() as UserAdminLoaderData\n const fetcher = useFetcher()\n const { submit: submitThen, resetResult, result: actionResult } = useSubmitThen(fetcher)\n const labels = data.labels ?? defaultUserAdminLabels\n const syncRequestNotice = syncRequestNoticeOf(actionResult, labels)\n\n const [searchQuery, setSearchQuery] = useState(\"\")\n const [modal, setModal] = useState<{ kind: \"create\" } | { kind: \"edit\"; group: AuthGroup } | null>(null)\n const editingGroup = modal?.kind === \"edit\" ? modal.group : null\n const isCreateOpen = modal?.kind === \"create\"\n const openerRef = useRef<HTMLElement | null>(null)\n /** ダイアログを開く。直前の操作の結果と入力を持ち越さない。 */\n const openModal = (next: { kind: \"create\" } | { kind: \"edit\"; group: AuthGroup }, event: React.MouseEvent<HTMLElement>) => {\n openerRef.current = event.currentTarget\n resetResult()\n // 前回の入力を残すと、上流から作った直後に開いた作成欄が別グループの名前で埋まっている。\n // 「連携済みなのに、もう 1 つ作ろうとしている」ように見え、取り違えたまま送られる\n if (next.kind === \"create\") resetCreateFields()\n setModal(next)\n }\n\n const busy = fetcher.state !== \"idle\"\n const [newGroupKey, setNewGroupKey] = useState(\"\")\n const [newName, setNewName] = useState(\"\")\n const [newDescription, setNewDescription] = useState(\"\")\n // 上流から作る経路は、候補一覧が引けているときだけ選べる\n const [createMode, setCreateMode] = useState<\"local\" | \"upstream\">(\"local\")\n /**\n * 焦点が当たっている作成経路。\n *\n * ⚠️ ラジオ群の実体は視覚的に隠してある (装飾はラベル側の押しボタン風の見た目)。隠した入力へ\n * 焦点が来ると、ブラウザは 1px の箱に焦点環を描き、操作者からは「どこにも焦点が無い」ように\n * 見える。インラインの style では `:focus-within` を書けないため、焦点の在処を状態で持って\n * ラベルへ環を描く\n */\n const [focusedMode, setFocusedMode] = useState<\"local\" | \"upstream\" | null>(null)\n const [upstream, setUpstream] = useState<UserAdminCatalogGroup | null>(null)\n const [membershipMode, setMembershipMode] = useState<AuthDirectoryMembershipMode>(\"direct\")\n /**\n * 上流から埋めた 3 欄の控え。ローカルへ戻すときに「こちらが埋めた分だけ」を消すために持つ。\n * 一律に消すと、先に手で打った内容まで失う\n */\n const prefilled = useRef<{ key: string; name: string; description: string } | null>(null)\n // ⚠️ 候補の作り直しは描画ごとに行わない。新しい配列を毎回渡すと、選択欄は同一性で\n // 記憶しているため、**無関係な欄の 1 打鍵ごとに候補全件を再正規化し、あいまい検索の\n // 索引まで組み直す**。候補 4000 件で 1 打鍵 22ms を実測した (実ブラウザではさらに重い)\n // ⚠️ 所属件数はグループごとに数え直さない。カード 1 枚ごとに所属配列を全走査すると\n // O(グループ数 × 所属数) になり、検索欄の 1 打鍵ごとにそれを繰り返す。1 回だけ畳んでおく\n const memberCountByGroup = useMemo(() => {\n const counts = new Map<number, number>()\n for (const m of data.groupMembers) counts.set(m.groupId, (counts.get(m.groupId) ?? 0) + 1)\n return counts\n }, [data.groupMembers])\n const linkedTo = useMemo(() => upstreamIndex(data.groups), [data.groups])\n const options = useMemo(() => upstreamOptions(data.catalog, linkedTo, labels), [data.catalog, linkedTo, labels])\n // ⚠️ 判定に使うのは **選べる候補の数** である。行数で判定すると、全件が連携済みの配備で\n // 「すべて選べない選択欄」と「押せない作成ボタン」を出し、hint は「まず候補を選べ」と\n // 言う — 従いようのない指示になる\n const selectableUpstreamCount = useMemo(() => options.filter((o) => o.disabled !== true).length, [options])\n // 候補が引けても、この配備がどのプロバイダとして連携するかが決まっていなければ連携は作れない。\n // 送れば `provider: \"\"` の行が残り、同期ジョブは「別プロバイダ」として恒久的に無視する\n // ⚠️ 候補が 0 件の一覧で経路を出すと、選べない選択欄と押せない作成ボタンという終端へ導く。\n // 「読めたが 1 件も無い」は上流側の話であり、この面で解決できることは何も無い\n const upstreamPickable = data.catalog.kind === \"ready\" && data.catalog.groups.length > 0 && data.directoryStatus.provider !== \"\"\n const createGroupRef = useModalBehavior(isCreateOpen, () => setModal(null), openerRef)\n const editGroupRef = useModalBehavior(editingGroup !== null, () => setModal(null), openerRef)\n\n const groupNeedle = searchQuery.trim().toLowerCase()\n const filteredGroups = data.groups.filter((g) => g.name.toLowerCase().includes(groupNeedle) || g.groupKey.toLowerCase().includes(groupNeedle))\n\n /** 作成欄をすべて初期状態へ戻す。 */\n const resetCreateFields = () => {\n setNewGroupKey(\"\")\n setNewName(\"\")\n setNewDescription(\"\")\n setCreateMode(\"local\")\n setUpstream(null)\n setMembershipMode(\"direct\")\n prefilled.current = null\n }\n\n const handleCreateGroup = (e: React.FormEvent) => {\n e.preventDefault()\n if (!(newGroupKey && newName)) return\n if (createMode === \"upstream\" && upstream === null) return\n // 連携付きで作ると、名前・説明・メンバー・削除のすべてが上流の管理下へ移る。取り消すには\n // 連携解除という別の操作が要るため、押した先が戻れない操作であることを先に伝える\n if (createMode === \"upstream\" && upstream !== null && !window.confirm(fill(labels.groupsView.confirmCreateLinked, { group: newName, upstream: upstream.externalKey }))) return\n // 送信した値と今の値が違うなら、往復の間に打ち込まれたものである。消してはならない\n const sent = { key: newGroupKey, name: newName, description: newDescription }\n const clear = () => {\n setNewGroupKey((current) => (current === sent.key ? \"\" : current))\n setNewName((current) => (current === sent.name ? \"\" : current))\n setNewDescription((current) => (current === sent.description ? \"\" : current))\n setUpstream(null)\n setModal(null)\n }\n if (createMode === \"upstream\" && upstream !== null) {\n // 作成と連携は 1 つの intent で行う。2 回に分けると、片方だけ成功した状態が生まれる\n submitThen({ intent: \"createLinkedGroup\", groupKey: sent.key, name: sent.name, description: sent.description, provider: data.directoryStatus.provider, externalKey: upstream.externalKey, membershipMode }, clear)\n return\n }\n submitThen({ intent: \"createGroup\", groupKey: sent.key, name: sent.name, description: sent.description }, clear)\n }\n\n /** 上流グループを選んだら、名前と鍵の初期値を埋める (打ち直しの手間を省く)。 */\n const chooseUpstream = (entry: UserAdminCatalogGroup | null) => {\n setUpstream(entry)\n if (!entry) return\n const filled = { key: groupKeyFromUpstream(entry.externalKey), name: entry.name, description: entry.description ?? \"\" }\n setNewName(filled.name)\n setNewGroupKey(filled.key)\n setNewDescription(filled.description)\n prefilled.current = filled\n }\n\n /**\n * 作成の経路を切り替える。ローカルへ戻すときは、上流から埋めた欄だけを空へ戻す。\n *\n * 残すと、ローカル作成のはずが上流グループの名前と説明で作られる。一律に消すと、先に\n * 手で打った内容まで失う\n */\n const switchCreateMode = (mode: \"local\" | \"upstream\") => {\n setCreateMode(mode)\n if (mode === \"upstream\") return\n setUpstream(null)\n const filled = prefilled.current\n if (!filled) return\n setNewGroupKey((current) => (current === filled.key ? \"\" : current))\n setNewName((current) => (current === filled.name ? \"\" : current))\n setNewDescription((current) => (current === filled.description ? \"\" : current))\n prefilled.current = null\n }\n\n const handleUpdateGroup = (e: React.FormEvent) => {\n e.preventDefault()\n if (!editingGroup) return\n submitThen({ intent: \"updateGroup\", groupId: String(editingGroup.id), name: editingGroup.name, description: editingGroup.description ?? \"\" }, () => setModal(null))\n }\n\n const handleDeleteGroup = (group: AuthGroup) => {\n // ⚠️ `aria-disabled` は焦点を残すために `disabled` の代わりに使っている。押せてしまう\n // ぶん、実際に断るのはここである\n if (busy) return\n // ⚠️ 名前も失うものも述べる。この面で最も破壊的な操作でありながら、確認が最も薄かった。\n // 一覧の中から誤って隣のカードを押しても、対話の中に気付ける手がかりが無かった\n const memberCount = data.groupMembers.filter((m) => m.groupId === group.id).length\n if (!window.confirm(fill(labels.groupsView.confirmDelete, { group: group.name, count: memberCount }))) return\n submitThen({ intent: \"deleteGroup\", groupId: String(group.id) })\n }\n\n return (\n <section data-testid=\"groups-view-section\" style={{ display: \"flex\", flexDirection: \"column\", gap: \"1rem\" }}>\n <h2 style={heading}>{labels.groupsView.title}</h2>\n {!modal && <ActionError result={actionResult} />}\n {/* ⚠️ ライブリージョンは **常に置いておき、中身だけ入れ替える** (連携面と同じ理由)。\n 上流から作った直後に要求が積めなかったことを、閉じた対話の外で伝える唯一の場所 */}\n <p\n data-testid=\"groups-sync-request-notice\"\n role=\"status\"\n aria-live=\"polite\"\n hidden={syncRequestNotice === null}\n style={syncRequestNotice === null ? undefined : { margin: 0, padding: \"0.5rem 0.7rem\", borderRadius: \"6px\", backgroundColor: \"#fffbeb\", border: \"1px solid #fde68a\", color: \"#92400e\", fontSize: \"0.8rem\" }}>\n {syncRequestNotice}\n </p>\n <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"center\", backgroundColor: \"#ffffff\", padding: \"1rem\", borderRadius: \"8px\", border: \"1px solid #e2e8f0\" }}>\n <input\n type=\"text\"\n aria-label={labels.groupsView.searchLabel}\n placeholder={labels.groupsView.searchPlaceholder}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n data-testid=\"groups-search-input\"\n style={{ padding: \"6px 12px\", borderRadius: \"6px\", border: \"1px solid #cbd5e1\", fontSize: \"0.85rem\", width: \"260px\" }}\n />\n <button\n type=\"button\"\n onClick={(event) => openModal({ kind: \"create\" }, event)}\n data-testid=\"groups-add-button\"\n style={{ padding: \"6px 14px\", borderRadius: \"6px\", backgroundColor: \"#4338ca\", color: \"#ffffff\", border: \"none\", fontSize: \"0.85rem\", fontWeight: 600, cursor: \"pointer\" }}>\n {labels.groupsView.addGroup}\n </button>\n </div>\n\n <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(auto-fill, minmax(280px, 1fr))\", gap: \"1rem\" }}>\n {filteredGroups.map((g) => {\n const memberCount = memberCountByGroup.get(g.id) ?? 0\n return (\n <div\n key={g.id}\n style={{ backgroundColor: \"#ffffff\", borderRadius: \"8px\", border: \"1px solid #e2e8f0\", padding: \"1rem\", display: \"flex\", flexDirection: \"column\", justifyContent: \"space-between\" }}\n data-testid={`group-card-${g.id}`}\n data-group-readonly={g.readOnly ? \"true\" : \"false\"}>\n <div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"center\", marginBottom: \"0.5rem\" }}>\n <h3 style={{ margin: 0, fontSize: \"1rem\", color: \"#0f172a\" }}>{g.name}</h3>\n <span style={{ fontSize: \"0.72rem\", backgroundColor: \"#e0e7ff\", color: \"#4338ca\", padding: \"2px 6px\", borderRadius: \"4px\", fontFamily: \"monospace\" }}>{g.groupKey}</span>\n </div>\n <p style={{ margin: \"0 0 0.8rem 0\", fontSize: \"0.82rem\", color: \"#64748b\", minHeight: \"2.4em\" }}>{g.description || labels.groupsView.noDescription}</p>\n {/* 所属も名称も上流が所有していることを、操作を試す前に伝える */}\n {g.externalLink && (\n <div data-testid={`group-source-${g.id}`} style={{ display: \"flex\", alignItems: \"center\", gap: \"6px\", flexWrap: \"wrap\", marginBottom: \"0.6rem\" }}>\n <span style={{ fontSize: \"0.72rem\", padding: \"2px 8px\", borderRadius: \"10px\", backgroundColor: \"#ede9fe\", color: \"#6d28d9\", fontWeight: 600 }}>{labels.common.managedUpstream}</span>\n <span style={{ fontSize: \"0.72rem\", fontFamily: \"monospace\", color: \"#64748b\" }}>{g.externalLink.externalKey}</span>\n {g.externalLink.unresolvedCount > 0 && (\n <span data-testid={`group-unresolved-${g.id}`} style={{ fontSize: \"0.72rem\", color: \"#b45309\" }}>\n {fill(labels.directoryView.unresolvedTemplate, { count: g.externalLink.unresolvedCount })}\n </span>\n )}\n </div>\n )}\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"center\", borderTop: \"1px solid #f1f5f9\", paddingTop: \"0.6rem\" }}>\n <Link to={`/users/group_members?group=${g.id}`} data-testid={`group-open-members-${g.id}`} aria-label={fill(labels.groupsView.openMembersAria, { group: g.name })} style={{ fontSize: \"0.78rem\", color: \"#1d4ed8\", textDecoration: \"none\" }}>\n {fill(labels.groupsView.memberCountTemplate, { count: memberCount })}\n </Link>\n {/* 上流が所有する行では操作を **描かない**。無効化したボタンを出すと、\n 押せる操作と見分けがつかず、押せない理由も伝わらない */}\n {g.readOnly ? (\n <span data-testid={`readonly-group-${g.id}`} style={{ fontSize: \"0.75rem\", color: \"#475569\" }}>\n {labels.common.readOnly}\n </span>\n ) : (\n <div>\n <button\n type=\"button\"\n onClick={(event) => openModal({ kind: \"edit\", group: g }, event)}\n {...rowActionProps(busy)}\n aria-label={fill(labels.groupsView.editGroupAria, { group: g.name })}\n data-testid={`edit-group-btn-${g.id}`}\n style={{ padding: \"3px 8px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", backgroundColor: \"#ffffff\", fontSize: \"0.75rem\", cursor: \"pointer\", marginRight: \"4px\" }}>\n {labels.common.edit}\n </button>\n <button\n type=\"button\"\n onClick={() => handleDeleteGroup(g)}\n {...rowActionProps(busy)}\n aria-label={fill(labels.groupsView.deleteGroupAria, { group: g.name })}\n data-testid={`delete-group-btn-${g.id}`}\n style={{ padding: \"3px 8px\", borderRadius: \"4px\", border: \"1px solid #fee2e2\", backgroundColor: \"#fff5f5\", color: \"#b91c1c\", fontSize: \"0.75rem\", cursor: \"pointer\" }}>\n {labels.common.delete}\n </button>\n </div>\n )}\n </div>\n </div>\n )\n })}\n </div>\n {filteredGroups.length === 0 && (\n <div data-testid=\"groups-empty\" style={{ padding: \"2rem\", textAlign: \"center\", color: \"#475569\", backgroundColor: \"#ffffff\", borderRadius: \"8px\", border: \"1px solid #e2e8f0\" }}>\n {data.groups.length === 0 ? labels.groupsView.empty : labels.groupsView.emptyFiltered}\n </div>\n )}\n\n {/* Create Group Modal */}\n {isCreateOpen && (\n <div style={{ position: \"fixed\", inset: 0, backgroundColor: \"rgba(0,0,0,0.4)\", display: \"flex\", justifyContent: \"center\", alignItems: \"center\", zIndex: 100 }}>\n <form\n ref={createGroupRef}\n onSubmit={handleCreateGroup}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"create-group-title\"\n {...(hasError(actionResult) ? { \"aria-describedby\": \"create-group-error\" } : {})}\n data-testid=\"group-create-modal\"\n style={{ backgroundColor: \"#ffffff\", padding: \"1.5rem\", borderRadius: \"8px\", width: \"360px\", boxShadow: \"0 10px 25px rgba(0,0,0,0.15)\" }}>\n <h3 id=\"create-group-title\" style={{ margin: \"0 0 1rem 0\", fontSize: \"1.1rem\" }}>\n {labels.groupsView.addGroup}\n </h3>\n <ActionError result={actionResult} id=\"create-group-error\" />\n\n {/* 上流から作る経路は候補一覧が在るときだけ提示する。出しておいて押せない\n 切替は、機能が壊れているのと見分けが付かない */}\n {upstreamPickable && (\n <fieldset data-testid=\"create-group-mode\" style={{ display: \"flex\", gap: \"0.25rem\", marginBottom: \"1rem\", border: \"none\", padding: 0, margin: \"0 0 1rem 0\" }}>\n {/* ⚠️ 本物のラジオ群である。同じ `name` を持つ入力にはブラウザが矢印キー移動と\n 「選択中の 1 つだけが Tab の停止点」を無償で与える。押しボタンに\n `role=\"radio\"` を被せると、その両方を手で書き直すことになる */}\n <legend style={visuallyHidden}>{labels.groupsView.createModeLabel}</legend>\n {([\"local\", \"upstream\"] as const).map((mode) => (\n <label\n key={mode}\n data-aqar-create-mode={mode}\n data-selected={createMode === mode ? \"\" : undefined}\n data-focused={focusedMode === mode ? \"\" : undefined}\n style={{\n outline: focusedMode === mode ? \"2px solid #4338ca\" : undefined,\n outlineOffset: \"2px\",\n flex: 1,\n padding: \"0.4rem 0.6rem\",\n fontSize: \"0.8rem\",\n borderRadius: \"6px\",\n cursor: \"pointer\",\n textAlign: \"center\",\n border: createMode === mode ? \"1px solid #4338ca\" : \"1px solid #cbd5e1\",\n backgroundColor: createMode === mode ? \"#eef2ff\" : \"#ffffff\",\n color: createMode === mode ? \"#3730a3\" : \"#475569\",\n fontWeight: createMode === mode ? 700 : 500,\n }}>\n <input\n type=\"radio\"\n name=\"create-group-mode\"\n value={mode}\n checked={createMode === mode}\n onChange={() => switchCreateMode(mode)}\n data-testid={`create-group-mode-${mode}`}\n data-aqar-visually-hidden\n onFocus={() => setFocusedMode(mode)}\n onBlur={() => setFocusedMode((current) => (current === mode ? null : current))}\n style={visuallyHidden}\n />\n {mode === \"local\" ? labels.groupsView.createModeLocal : labels.groupsView.createModeUpstream}\n </label>\n ))}\n </fieldset>\n )}\n\n {data.catalog.kind === \"ready\" && data.catalog.groups.length === 0 && (\n <p data-testid=\"create-group-catalog-empty\" role=\"note\" style={{ margin: \"0 0 1rem 0\", padding: \"0.5rem 0.7rem\", borderRadius: \"6px\", backgroundColor: \"#f8fafc\", border: \"1px solid #e2e8f0\", color: \"#475569\", fontSize: \"0.78rem\" }}>\n {fill(labels.groupsView.catalogEmpty, { at: formatDateTime(data.catalog.observedAt ?? \"\", labels) })}\n </p>\n )}\n\n {data.catalog.kind === \"ready\" && data.catalog.groups.length > 0 && data.directoryStatus.provider === \"\" && (\n <p data-testid=\"create-group-provider-unset\" role=\"note\" style={{ margin: \"0 0 1rem 0\", padding: \"0.5rem 0.7rem\", borderRadius: \"6px\", backgroundColor: \"#fffbeb\", border: \"1px solid #fde68a\", color: \"#92400e\", fontSize: \"0.78rem\" }}>\n {labels.groupsView.providerUnset}\n </p>\n )}\n\n {data.catalog.kind === \"unavailable\" && (\n <p data-testid=\"create-group-catalog-unavailable\" role=\"note\" style={{ margin: \"0 0 1rem 0\", padding: \"0.5rem 0.7rem\", borderRadius: \"6px\", backgroundColor: \"#fffbeb\", border: \"1px solid #fde68a\", color: \"#92400e\", fontSize: \"0.78rem\" }}>\n {/* ⚠️ この対話には上流アドレスの欄が無い (一覧が引けないと経路ごと消える)。\n 「手入力してください」と案内すると、存在しない欄を探させることになる */}\n {fill(labels.groupsView.catalogUnavailableHere, { reason: labels.groupsView.catalogFailures[data.catalog.reason] })}\n </p>\n )}\n\n {createMode === \"upstream\" && upstreamPickable && data.catalog.kind === \"ready\" && (\n <div style={{ marginBottom: \"1rem\" }}>\n <p data-testid=\"create-group-upstream-notice\" role=\"note\" style={{ margin: \"0 0 0.5rem 0\", fontSize: \"0.78rem\", color: \"#5b21b6\" }}>\n {labels.groupsView.createUpstreamNotice}\n </p>\n <div id=\"create-group-upstream-label\" style={fieldLabel}>\n {labels.groupsView.pickUpstream}\n </div>\n {/* ⚠️ 選択欄は行を選んでいない Enter を「フォームへ渡す」設計であり、\n 絞り込みを打った直後の Enter が **前回選んだ相手のまま** 送信になる。\n 送信先は取り消せない委譲であるため、ここで暗黙送信だけを止める */}\n {/* biome-ignore lint/a11y/noStaticElementInteractions: 子の暗黙送信を止めるためだけの捕捉であり、対話要素ではない */}\n <div\n data-testid=\"create-group-upstream-picker\"\n onKeyDown={(event) => {\n // ⚠️ 変換中の Enter には触れない。日本語 IME の確定は\n // `keydown{key:\"Enter\", keyCode:229, isComposing:true}` として届き、\n // ここで既定動作を止めると変換の確定そのものを潰し得る。\n // 変換中はそもそも暗黙の送信は起きないので、止める意味も無い\n if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return\n if (event.key === \"Enter\") event.preventDefault()\n }}>\n <SelectBox\n options={options}\n value={upstream === null ? null : (options.find((o) => o.value === upstream.externalId) ?? null)}\n onChange={(picked: Option | Option[] | null) => chooseUpstream(picked === null || Array.isArray(picked) ? null : ((picked as { entry?: UserAdminCatalogGroup }).entry ?? null))}\n placeholder={labels.groupsView.pickUpstreamPlaceholder}\n ariaLabelledBy=\"create-group-upstream-label\"\n renderOption={renderUpstreamOption}\n texts={pickerTexts(labels)}\n clearable\n />\n </div>\n <CatalogFreshness observedAt={data.catalog.observedAt} isStale={data.catalog.isStale} labels={labels} />\n <div style={{ marginTop: \"0.6rem\" }}>\n <label htmlFor=\"create-group-membership-mode\" style={fieldLabel}>\n {labels.directoryView.membershipMode}\n </label>\n <select\n id=\"create-group-membership-mode\"\n data-testid=\"create-group-membership-mode\"\n value={membershipMode}\n onChange={(e) => setMembershipMode(e.target.value as AuthDirectoryMembershipMode)}\n style={{ width: \"100%\", padding: \"6px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", boxSizing: \"border-box\" }}>\n <option value=\"direct\">{labels.directoryView.directMode}</option>\n <option value=\"transitive\">{labels.directoryView.transitiveMode}</option>\n </select>\n </div>\n </div>\n )}\n\n <div style={{ marginBottom: \"1rem\" }}>\n <label htmlFor=\"create-group-key-input\" style={{ display: \"block\", fontSize: \"0.8rem\", color: \"#475569\", marginBottom: \"4px\" }}>\n {labels.groupsView.groupKey}\n </label>\n <input\n id=\"create-group-key-input\"\n type=\"text\"\n value={newGroupKey}\n onChange={(e) => setNewGroupKey(e.target.value)}\n placeholder={labels.groupsView.groupKeyPlaceholder}\n required\n data-testid=\"create-group-key\"\n style={{ width: \"100%\", padding: \"6px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", boxSizing: \"border-box\" }}\n />\n </div>\n <div style={{ marginBottom: \"1rem\" }}>\n <label htmlFor=\"create-group-name-input\" style={{ display: \"block\", fontSize: \"0.8rem\", color: \"#475569\", marginBottom: \"4px\" }}>\n {labels.groupsView.name}\n </label>\n <input\n id=\"create-group-name-input\"\n type=\"text\"\n value={newName}\n onChange={(e) => setNewName(e.target.value)}\n placeholder={labels.groupsView.namePlaceholder}\n required\n data-testid=\"create-group-name\"\n style={{ width: \"100%\", padding: \"6px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", boxSizing: \"border-box\" }}\n />\n </div>\n <div style={{ marginBottom: \"1.2rem\" }}>\n <label htmlFor=\"create-group-desc-input\" style={{ display: \"block\", fontSize: \"0.8rem\", color: \"#475569\", marginBottom: \"4px\" }}>\n {labels.groupsView.description}\n </label>\n <textarea\n id=\"create-group-desc-input\"\n value={newDescription}\n onChange={(e) => setNewDescription(e.target.value)}\n data-testid=\"create-group-description\"\n style={{ width: \"100%\", padding: \"6px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", boxSizing: \"border-box\", height: \"60px\" }}\n />\n </div>\n {/* ⚠️ 押せない理由を `title` だけに置かない。無効化されたボタンは焦点を持てず、\n 読み上げの要素移動でも飛ばされ、Safari では tooltip も出ない — つまり\n キーボード・読み上げ・多くのマウス操作者のいずれにも届かない */}\n {createMode === \"upstream\" && upstream === null && (\n <p id=\"create-group-submit-hint\" data-testid=\"create-group-submit-hint\" style={{ margin: \"0 0 0.6rem 0\", fontSize: \"0.78rem\", color: \"#475569\", textAlign: \"right\" }}>\n {/* ⚠️ 選べる候補が 1 件も無いときに「先に選べ」と言ってはならない。\n 従いようのない指示であり、操作者は自分の見落としを探し続ける。\n 候補は隠さない (どれが既に繋がっているのかを見せる価値は残る) */}\n {selectableUpstreamCount > 0 ? labels.groupsView.pickUpstreamFirst : labels.groupsView.allUpstreamLinked}\n </p>\n )}\n <div style={{ display: \"flex\", justifyContent: \"flex-end\", gap: \"0.5rem\" }}>\n <button type=\"button\" onClick={() => setModal(null)} data-testid=\"cancel-create-group\" style={{ padding: \"6px 12px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", backgroundColor: \"#ffffff\", cursor: \"pointer\" }}>\n {labels.common.cancel}\n </button>\n <button\n type=\"submit\"\n data-testid=\"submit-create-group\"\n // 上流モードで候補が空のまま押せると、送信は黙って捨てられる。押せない理由を\n // 見せる方が、無反応なボタンより短く伝わる\n disabled={busy || (createMode === \"upstream\" && upstream === null)}\n aria-busy={busy}\n {...(createMode === \"upstream\" && upstream === null ? { \"aria-describedby\": \"create-group-submit-hint\" } : {})}\n style={{\n padding: \"6px 12px\",\n borderRadius: \"4px\",\n backgroundColor: busy || (createMode === \"upstream\" && upstream === null) ? \"#a5b4fc\" : \"#4338ca\",\n color: \"#ffffff\",\n border: \"none\",\n cursor: busy || (createMode === \"upstream\" && upstream === null) ? \"not-allowed\" : \"pointer\",\n }}>\n {labels.common.create}\n </button>\n </div>\n </form>\n </div>\n )}\n\n {/* Edit Group Modal */}\n {editingGroup && (\n <div style={{ position: \"fixed\", inset: 0, backgroundColor: \"rgba(0,0,0,0.4)\", display: \"flex\", justifyContent: \"center\", alignItems: \"center\", zIndex: 100 }}>\n <form\n ref={editGroupRef}\n onSubmit={handleUpdateGroup}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"edit-group-title\"\n {...(hasError(actionResult) ? { \"aria-describedby\": \"edit-group-error\" } : {})}\n data-testid=\"group-edit-modal\"\n style={{ backgroundColor: \"#ffffff\", padding: \"1.5rem\", borderRadius: \"8px\", width: \"360px\", boxShadow: \"0 10px 25px rgba(0,0,0,0.15)\" }}>\n <h3 id=\"edit-group-title\" style={{ margin: \"0 0 1rem 0\", fontSize: \"1.1rem\" }}>\n {labels.groupsView.editGroup}\n </h3>\n <ActionError result={actionResult} id=\"edit-group-error\" />\n <div style={{ marginBottom: \"1rem\" }}>\n <label htmlFor=\"edit-group-name-input\" style={{ display: \"block\", fontSize: \"0.8rem\", color: \"#475569\", marginBottom: \"4px\" }}>\n {labels.groupsView.name}\n </label>\n <input\n id=\"edit-group-name-input\"\n type=\"text\"\n value={editingGroup.name}\n onChange={(e) => setModal({ kind: \"edit\", group: { ...editingGroup, name: e.target.value } })}\n required\n data-testid=\"edit-group-name\"\n style={{ width: \"100%\", padding: \"6px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", boxSizing: \"border-box\" }}\n />\n </div>\n <div style={{ marginBottom: \"1.2rem\" }}>\n <label htmlFor=\"edit-group-desc-input\" style={{ display: \"block\", fontSize: \"0.8rem\", color: \"#475569\", marginBottom: \"4px\" }}>\n {labels.groupsView.description}\n </label>\n <textarea\n id=\"edit-group-desc-input\"\n value={editingGroup.description ?? \"\"}\n onChange={(e) => setModal({ kind: \"edit\", group: { ...editingGroup, description: e.target.value } })}\n data-testid=\"edit-group-description\"\n style={{ width: \"100%\", padding: \"6px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", boxSizing: \"border-box\", height: \"60px\" }}\n />\n </div>\n <div style={{ display: \"flex\", justifyContent: \"flex-end\", gap: \"0.5rem\" }}>\n <button type=\"button\" onClick={() => setModal(null)} style={{ padding: \"6px 12px\", borderRadius: \"4px\", border: \"1px solid #cbd5e1\", backgroundColor: \"#ffffff\", cursor: \"pointer\" }}>\n {labels.common.cancel}\n </button>\n <button type=\"submit\" data-testid=\"submit-edit-group\" disabled={busy} aria-busy={busy} style={{ padding: \"6px 12px\", borderRadius: \"4px\", backgroundColor: \"#4338ca\", color: \"#ffffff\", border: \"none\", cursor: \"pointer\" }}>\n {labels.common.save}\n </button>\n </div>\n </form>\n </div>\n )}\n </section>\n )\n}\n\n/**\n * View for managing group member assignments.\n * グループメンバー割り当て管理ビュー。\n */\nexport function GroupMembersView() {\n const data = useLoaderData() as UserAdminLoaderData\n const fetcher = useFetcher()\n const { submit: submitThen, result: actionResult } = useSubmitThen(fetcher)\n const busy = fetcher.state !== \"idle\"\n const labels = data.labels ?? defaultUserAdminLabels\n\n // 遷移元がグループを指定していればそれを選ぶ (グループ一覧から 1 クリックで来られるように)\n const [searchParams] = useSearchParams()\n const requested = Number(searchParams.get(\"group\") ?? \"\")\n const deepLinked = Number.isInteger(requested) && requested > 0 ? requested : null\n const [picked, setPicked] = useState<number | null>(null)\n /**\n * 「所属のみ」表示中に、この画面で外した人。\n *\n * ⚠️ 外した行をその場で消すと、下の行が **カーソルの下へせり上がる**。上から順に外していく\n * のは所属を整理する自然な手順であり、往復のたびに次の「外す」ボタンが今いる位置へ来る。\n * 確認も無いため、普通の速さで押している運用者が意図しない人を外す\n */\n const [justRemoved, setJustRemoved] = useState<ReadonlySet<number>>(new Set())\n /**\n * 深いリンクが指す先が変わったら、その指定に従う。\n *\n * ⚠️ 初期値としてだけ読むと、同じ面に居るまま `?group=` が変わっても (グループ面の各カードから\n * 続けて飛んだ場合) 選択が動かず、開いている面と URL が食い違う\n */\n const lastDeepLink = useRef<number | null>(deepLinked)\n if (lastDeepLink.current !== deepLinked) {\n lastDeepLink.current = deepLinked\n if (deepLinked !== null) {\n setPicked(deepLinked)\n // 別のグループの「外したばかり」を持ち越さない (押しボタン経由と同じ扱い)\n setJustRemoved(new Set())\n }\n }\n\n const [memberQuery, setMemberQuery] = useState(\"\")\n const [onlyMembers, setOnlyMembers] = useState(false)\n\n // 再検証で対象が消えることがある。消えた ID を握り続けると空の面になる\n const requestedGroupId = picked ?? deepLinked ?? data.groups[0]?.id ?? null\n const currentGroup = data.groups.find((g) => g.id === requestedGroupId) ?? data.groups[0]\n /**\n * 頼んだ相手と、実際に出ている相手が違うときの、頼んだ側の ID。\n *\n * ⚠️ 判定は「**頼んだ相手が出ているか**」で行う。「深いリンクで来て、まだ一度も選んでいない」\n * ことを条件にすると、一度でも一覧から選んだ後は永久に黙る — そして代替が出る場面の大半は\n * そちらである。**別の運用者が今開いているグループを消した** ときがそれで、画面の見出しだけが\n * 入れ替わり、続けて押した「除外」は選んだ覚えのないグループへ書かれる。書込先を画面に\n * 合わせてある (下の `handleToggleMember`) ことが、この沈黙を余計に危うくする\n */\n const missedTarget = requestedGroupId !== null && currentGroup !== undefined && currentGroup.id !== requestedGroupId ? requestedGroupId : null\n const currentMemberUserIds = new Set(data.groupMembers.filter((m) => m.groupId === currentGroup?.id).map((m) => m.userId))\n // 数百人の一覧を目で追わせない。名前と Email の両方を対象にする\n const needle = memberQuery.trim().toLowerCase()\n const visibleUsers = data.users.filter((u) => {\n // この画面で外した人は残す (行が消えると、下の行がカーソルの下へせり上がる)\n if (onlyMembers && !currentMemberUserIds.has(u.id) && !justRemoved.has(u.id)) return false\n if (needle === \"\") return true\n return u.displayName.toLowerCase().includes(needle) || u.email.toLowerCase().includes(needle)\n })\n\n const handleToggleMember = (userId: number, isMember: boolean) => {\n // ⚠️ `aria-disabled` は焦点を残すために `disabled` の代わりに使っている。押せてしまう\n // ぶん、実際に断るのはここである\n if (busy) return\n // ⚠️ 書き込む先は **画面に出ているグループ** でなければならない。選択 ID をそのまま送ると、\n // 再検証で対象が消えて代替が表示されている間に、見えていない別グループを書き換える\n if (!currentGroup) return\n const groupId = String(currentGroup.id)\n if (isMember) {\n setJustRemoved((current) => new Set(current).add(userId))\n submitThen({ intent: \"removeGroupMember\", groupId, userId: String(userId) })\n } else {\n setJustRemoved((current) => {\n if (!current.has(userId)) return current\n const next = new Set(current)\n next.delete(userId)\n return next\n })\n submitThen({ intent: \"addGroupMember\", groupId, userId: String(userId) })\n }\n }\n\n return (\n <section data-testid=\"group-members-view-section\" style={{ display: \"grid\", gridTemplateColumns: \"240px 1fr\", gap: \"1.2rem\" }}>\n <h2 style={{ ...heading, gridColumn: \"1 / -1\" }}>{labels.groupMembersView.title}</h2>\n <ActionError result={actionResult} />\n {missedTarget !== null && (\n <p data-testid=\"member-deep-link-missed\" role=\"note\" style={{ gridColumn: \"1 / -1\", margin: 0, padding: \"0.5rem 0.7rem\", borderRadius: \"6px\", backgroundColor: \"#fffbeb\", border: \"1px solid #fde68a\", color: \"#92400e\", fontSize: \"0.8rem\" }}>\n {fill(labels.groupMembersView.deepLinkMissed, { id: missedTarget, group: currentGroup?.name ?? \"\" })}\n </p>\n )}\n <div style={{ backgroundColor: \"#ffffff\", padding: \"1rem\", borderRadius: \"8px\", border: \"1px solid #e2e8f0\" }}>\n <h3 style={{ margin: \"0 0 0.8rem 0\", fontSize: \"0.9rem\", color: \"#475569\" }}>{labels.groupMembersView.selectGroup}</h3>\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: \"0.4rem\" }}>\n {data.groups.map((g) => {\n const isSelected = g.id === currentGroup?.id\n return (\n <button\n type=\"button\"\n key={g.id}\n onClick={() => {\n setPicked(g.id)\n // 別のグループの「外したばかり」を持ち越さない\n setJustRemoved(new Set())\n }}\n data-testid={`select-group-btn-${g.id}`}\n style={{\n textAlign: \"left\",\n padding: \"8px 10px\",\n borderRadius: \"6px\",\n border: isSelected ? \"1px solid #6366f1\" : \"1px solid #e2e8f0\",\n backgroundColor: isSelected ? \"#e0e7ff\" : \"#ffffff\",\n color: isSelected ? \"#3730a3\" : \"#0f172a\",\n cursor: \"pointer\",\n fontWeight: isSelected ? 600 : 400,\n fontSize: \"0.85rem\",\n }}>\n {g.name}\n {g.readOnly ? (\n <span data-testid={`group-upstream-marker-${g.id}`} style={{ marginLeft: \"6px\", fontSize: \"0.7rem\", color: \"#7c3aed\" }}>\n {labels.common.managedUpstream}\n </span>\n ) : null}\n </button>\n )\n })}\n </div>\n </div>\n\n <div style={{ backgroundColor: \"#ffffff\", padding: \"1.2rem\", borderRadius: \"8px\", border: \"1px solid #e2e8f0\" }}>\n {currentGroup ? (\n <>\n <div style={{ marginBottom: \"1rem\", borderBottom: \"1px solid #e2e8f0\", paddingBottom: \"0.8rem\" }}>\n <h3 style={{ margin: 0, fontSize: \"1.1rem\", color: \"#0f172a\" }}>{fill(labels.groupMembersView.membersOfTemplate, { group: currentGroup.name })}</h3>\n <span style={{ fontSize: \"0.8rem\", color: \"#64748b\" }}>\n {labels.groupMembersView.groupKeyLabel}: {currentGroup.groupKey}\n </span>\n {currentGroup.readOnly ? (\n <div data-testid=\"group-members-upstream-notice\" role=\"note\" style={{ marginTop: \"0.5rem\", padding: \"0.5rem 0.7rem\", borderRadius: \"6px\", backgroundColor: \"#f5f3ff\", border: \"1px solid #ddd6fe\", color: \"#5b21b6\", fontSize: \"0.8rem\" }}>\n {labels.groupMembersView.upstreamNotice}\n </div>\n ) : null}\n </div>\n <div style={{ display: \"flex\", gap: \"0.75rem\", alignItems: \"center\", flexWrap: \"wrap\", marginBottom: \"0.8rem\" }}>\n <input\n type=\"search\"\n aria-label={labels.groupMembersView.searchLabel}\n placeholder={labels.groupMembersView.searchPlaceholder}\n value={memberQuery}\n onChange={(e) => setMemberQuery(e.target.value)}\n data-testid=\"member-search-input\"\n style={{ flex: 1, minWidth: \"220px\", padding: \"6px 12px\", borderRadius: \"6px\", border: \"1px solid #cbd5e1\", fontSize: \"0.85rem\" }}\n />\n <label style={{ display: \"inline-flex\", alignItems: \"center\", gap: \"0.35rem\", fontSize: \"0.8rem\", color: \"#475569\" }}>\n <input\n type=\"checkbox\"\n data-testid=\"member-only-toggle\"\n checked={onlyMembers}\n onChange={(e) => {\n setOnlyMembers(e.target.checked)\n // 絞り込みを掛け直したら、その時点の所属で数え直す。\n // 残すと「外したはずの人が所属のみ表示に居る」状態が続く\n setJustRemoved(new Set())\n }}\n />\n {fill(labels.groupMembersView.onlyMembers, { count: currentMemberUserIds.size })}\n </label>\n </div>\n {/* 絞り込みの結果は視覚的にしか変わらない。読み上げへ届けるには live region が要る */}\n <p data-testid=\"member-summary\" role=\"status\" aria-live=\"polite\" style={{ margin: \"0 0 0.6rem 0\", fontSize: \"0.78rem\", color: \"#475569\" }}>\n {fill(labels.groupMembersView.summary, { shown: visibleUsers.length, total: data.users.length, members: currentMemberUserIds.size })}\n </p>\n {visibleUsers.length === 0 && (\n <p data-testid=\"member-empty-filtered\" style={{ margin: \"0 0 0.6rem 0\", fontSize: \"0.85rem\", color: \"#475569\" }}>\n {/* 効いている絞り込みを名指しする。打っていない検索のせいにしない。\n そもそも利用者が 1 人も居ない配備を「検索で 0 件」と言わない */}\n {data.users.length === 0 ? labels.groupMembersView.noUsers : needle === \"\" && onlyMembers ? labels.groupMembersView.emptyNoMembers : labels.groupMembersView.emptyFiltered}\n </p>\n )}\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: \"0.5rem\" }}>\n {visibleUsers.map((u) => {\n const isMember = currentMemberUserIds.has(u.id)\n return (\n <div\n key={u.id}\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n padding: \"8px 12px\",\n borderRadius: \"6px\",\n border: \"1px solid #f1f5f9\",\n backgroundColor: isMember ? \"#f0fdf4\" : \"#ffffff\",\n }}\n data-testid={`member-row-${u.id}`}>\n <div>\n <span style={{ fontWeight: 600, fontSize: \"0.88rem\", color: \"#0f172a\", marginRight: \"8px\" }}>{u.displayName}</span>\n <span style={{ fontSize: \"0.78rem\", color: \"#64748b\" }}>({u.email})</span>\n </div>\n {currentGroup.readOnly ? (\n isMember ? (\n <span data-testid={`member-readonly-${u.id}`} style={{ fontSize: \"0.75rem\", color: \"#475569\" }}>\n {labels.common.managedUpstream}\n </span>\n ) : null\n ) : (\n <button\n type=\"button\"\n onClick={() => handleToggleMember(u.id, isMember)}\n {...rowActionProps(busy)}\n aria-label={fill(isMember ? labels.groupMembersView.actionRemoveAria : labels.groupMembersView.actionAddAria, { user: u.displayName })}\n data-testid={`toggle-member-btn-${u.id}`}\n style={{\n padding: \"4px 12px\",\n borderRadius: \"4px\",\n border: isMember ? \"1px solid #b91c1c\" : \"1px solid #0284c7\",\n backgroundColor: isMember ? \"#ffffff\" : \"#0284c7\",\n color: isMember ? \"#b91c1c\" : \"#ffffff\",\n fontSize: \"0.78rem\",\n fontWeight: 600,\n cursor: \"pointer\",\n }}>\n {isMember ? labels.groupMembersView.actionRemove : labels.groupMembersView.actionAdd}\n </button>\n )}\n </div>\n )\n })}\n </div>\n </>\n ) : (\n <div data-testid=\"member-no-groups\" style={{ color: \"#475569\", textAlign: \"center\", padding: \"2rem\" }}>\n {/* 左の一覧が空なら「選べ」は従えない指示である。何が無いのかを言う */}\n {data.groups.length === 0 ? labels.groupMembersView.noGroups : labels.groupMembersView.selectPrompt}\n </div>\n )}\n </div>\n </section>\n )\n}\n\n/** Badge colours per allowlist kind. 許可種別ごとのバッジ配色。 */\nfunction allowlistBadgeStyle(type: AuthAllowlistEntry[\"type\"]): { backgroundColor: string; color: string } {\n if (type === \"email\") return { backgroundColor: \"#e0f2fe\", color: \"#0369a1\" }\n if (type === \"domain\") return { backgroundColor: \"#fef3c7\", color: \"#b45309\" }\n return { backgroundColor: \"#ede9fe\", color: \"#6d28d9\" }\n}\n\n/** Badge text per allowlist kind. 許可種別ごとのバッジ文言。 */\nfunction allowlistTypeLabel(type: AuthAllowlistEntry[\"type\"], labels: UserAdminLabels): string {\n if (type === \"email\") return labels.allowlistView.emailTypeShort\n if (type === \"domain\") return labels.allowlistView.domainTypeShort\n return labels.allowlistView.groupTypeShort\n}\n\n/**\n * View for managing authentication allowlist entries.\n * アクセス許可リスト (Allowlist) 管理ビュー。\n */\nexport function AllowlistView() {\n const data = useLoaderData() as UserAdminLoaderData\n const fetcher = useFetcher()\n const labels = data.labels ?? defaultUserAdminLabels\n\n const { submit: submitThen, result: actionResult } = useSubmitThen(fetcher)\n const busy = fetcher.state !== \"idle\"\n const [pattern, setPattern] = useState(\"\")\n const [type, setType] = useState<\"email\" | \"domain\">(\"email\")\n const [description, setDescription] = useState(\"\")\n\n const handleAddEntry = (e: React.FormEvent) => {\n e.preventDefault()\n if (!pattern) return\n const sent = { pattern, description }\n submitThen({ intent: \"addAllowlistEntry\", pattern: sent.pattern, type, description: sent.description }, () => {\n setPattern((current) => (current === sent.pattern ? \"\" : current))\n setDescription((current) => (current === sent.description ? \"\" : current))\n })\n }\n\n const handleRemoveEntry = (id: number) => {\n submitThen({ intent: \"removeAllowlistEntry\", entryId: String(id) })\n }\n\n /** グループ許可は別の表に住むため、別の intent で消す。 */\n const handleRemoveGroupEntry = (id: number) => {\n submitThen({ intent: \"removeAllowlistGroup\", entryId: String(id) })\n }\n\n return (\n <section data-testid=\"allowlist-view-section\" style={{ display: \"flex\", flexDirection: \"column\", gap: \"1.2rem\" }}>\n <h2 style={heading}>{labels.allowlistView.title}</h2>\n <ActionError result={actionResult} />\n <form onSubmit={handleAddEntry} style={{ backgroundColor: \"#ffffff\", padding: \"1.2rem\", borderRadius: \"8px\", border: \"1px solid #e2e8f0\", display: \"flex\", flexWrap: \"wrap\", gap: \"0.8rem\", alignItems: \"flex-end\" }}>\n <div style={{ flex: 2, minWidth: \"220px\" }}>\n <label htmlFor=\"allowlist-pattern-input-field\" style={{ display: \"block\", fontSize: \"0.78rem\", color: \"#475569\", marginBottom: \"4px\" }}>\n {labels.allowlistView.pattern}\n </label>\n <input\n id=\"allowlist-pattern-input-field\"\n type=\"text\"\n value={pattern}\n onChange={(e) => setPattern(e.target.value)}\n required\n data-testid=\"allowlist-pattern-input\"\n style={{ width: \"100%\", padding: \"6px 10px\", borderRadius: \"6px\", border: \"1px solid #cbd5e1\", fontSize: \"0.85rem\", boxSizing: \"border-box\" }}\n />\n </div>\n <div style={{ flex: 1, minWidth: \"140px\" }}>\n <label htmlFor=\"allowlist-type-select-field\" style={{ display: \"block\", fontSize: \"0.78rem\", color: \"#475569\", marginBottom: \"4px\" }}>\n {labels.allowlistView.type}\n </label>\n <select\n id=\"allowlist-type-select-field\"\n value={type}\n onChange={(e) => setType(e.target.value as \"email\" | \"domain\")}\n data-testid=\"allowlist-type-select\"\n style={{ width: \"100%\", padding: \"6px 10px\", borderRadius: \"6px\", border: \"1px solid #cbd5e1\", fontSize: \"0.85rem\", boxSizing: \"border-box\" }}>\n <option value=\"email\">{labels.allowlistView.emailType}</option>\n <option value=\"domain\">{labels.allowlistView.domainType}</option>\n </select>\n </div>\n <div style={{ flex: 2, minWidth: \"200px\" }}>\n <label htmlFor=\"allowlist-desc-input-field\" style={{ display: \"block\", fontSize: \"0.78rem\", color: \"#475569\", marginBottom: \"4px\" }}>\n {labels.allowlistView.description}\n </label>\n <input\n id=\"allowlist-desc-input-field\"\n type=\"text\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n data-testid=\"allowlist-desc-input\"\n style={{ width: \"100%\", padding: \"6px 10px\", borderRadius: \"6px\", border: \"1px solid #cbd5e1\", fontSize: \"0.85rem\", boxSizing: \"border-box\" }}\n />\n </div>\n <button type=\"submit\" data-testid=\"submit-allowlist-add\" disabled={busy} aria-busy={busy} style={{ padding: \"7px 16px\", borderRadius: \"6px\", backgroundColor: \"#047857\", color: \"#ffffff\", border: \"none\", fontSize: \"0.85rem\", fontWeight: 600, cursor: \"pointer\" }}>\n {labels.allowlistView.addEntry}\n </button>\n </form>\n\n <div style={{ backgroundColor: \"#ffffff\", borderRadius: \"8px\", border: \"1px solid #e2e8f0\", overflow: \"hidden\" }}>\n <table style={{ width: \"100%\", borderCollapse: \"collapse\", fontSize: \"0.85rem\", textAlign: \"left\" }}>\n <thead>\n <tr style={{ backgroundColor: \"#f8fafc\", borderBottom: \"1px solid #e2e8f0\", color: \"#475569\" }}>\n <th scope=\"col\" style={{ padding: \"10px 14px\" }}>\n {labels.common.id}\n </th>\n <th scope=\"col\" style={{ padding: \"10px 14px\" }}>\n {labels.allowlistView.target}\n </th>\n <th scope=\"col\" style={{ padding: \"10px 14px\" }}>\n {labels.allowlistView.type}\n </th>\n <th scope=\"col\" style={{ padding: \"10px 14px\" }}>\n {labels.allowlistView.description}\n </th>\n <th scope=\"col\" style={{ padding: \"10px 14px\", textAlign: \"right\" }}>\n {labels.common.actions}\n </th>\n </tr>\n </thead>\n <tbody>\n {data.allowlist.map((item) => (\n <tr key={`${item.type}-${item.id}`} style={{ borderBottom: \"1px solid #f1f5f9\" }} data-testid={`allowlist-row-${item.type}-${item.id}`} data-allowlist-type={item.type}>\n <td style={{ padding: \"10px 14px\", color: \"#475569\" }}>{fill(labels.common.idTemplate, { id: item.id })}</td>\n <td style={{ padding: \"10px 14px\", fontWeight: 600, color: \"#0f172a\" }}>\n {/* パターン許可とグループ許可では正準値が違う。グループはアドレスではなく\n グループそのものが対象なので、名前とキーを見せる */}\n {item.type === \"group\" ? (\n <span data-testid={`allowlist-group-${item.type}-${item.id}`}>\n {item.groupName}\n <span style={{ marginLeft: \"6px\", fontFamily: \"monospace\", fontSize: \"0.78rem\", color: \"#64748b\" }}>{item.groupKey}</span>\n </span>\n ) : (\n <span style={{ fontFamily: \"monospace\" }}>{item.pattern}</span>\n )}\n </td>\n <td style={{ padding: \"10px 14px\" }}>\n <span style={{ fontSize: \"0.75rem\", padding: \"2px 8px\", borderRadius: \"10px\", ...allowlistBadgeStyle(item.type), fontWeight: 600 }}>{allowlistTypeLabel(item.type, labels)}</span>\n </td>\n <td style={{ padding: \"10px 14px\", color: \"#475569\" }}>\n {item.description || labels.common.none}\n {/* 配備全体が所有する行は、どのテナントの管理者でも編集できない */}\n {item.owner.kind === \"deployment\" && (\n <span data-testid={`allowlist-owner-${item.type}-${item.id}`} style={{ marginLeft: \"8px\", fontSize: \"0.72rem\", color: \"#475569\" }}>\n {labels.allowlistView.deploymentOwner}\n </span>\n )}\n </td>\n <td style={{ padding: \"10px 14px\", textAlign: \"right\" }}>\n {/* 変更不能な供給元 (環境変数など) は削除ボタンを出さない。無効化ではなく\n 非描画にするのは、押せないボタンが押せる操作と見分けられないため */}\n {item.readOnly ? (\n <span data-testid={`readonly-allowlist-${item.type}-${item.id}`} style={{ fontSize: \"0.75rem\", color: \"#475569\" }}>\n {labels.common.readOnly}\n </span>\n ) : (\n <button\n type=\"button\"\n onClick={() => {\n // これでしかサインインできない人はログインできなくなる。取り消せない\n if (!window.confirm(fill(labels.allowlistView.confirmDelete, { target: item.type === \"group\" ? item.groupName : item.pattern }))) return\n if (item.type === \"group\") handleRemoveGroupEntry(item.id)\n else handleRemoveEntry(item.id)\n }}\n disabled={busy}\n aria-busy={busy}\n aria-label={fill(labels.allowlistView.deleteAria, { target: item.type === \"group\" ? item.groupName : item.pattern })}\n data-testid={`remove-allowlist-btn-${item.type}-${item.id}`}\n style={{ padding: \"3px 8px\", borderRadius: \"4px\", border: \"1px solid #fee2e2\", backgroundColor: \"#fff5f5\", color: \"#b91c1c\", fontSize: \"0.78rem\", cursor: \"pointer\" }}>\n {labels.common.delete}\n </button>\n )}\n </td>\n </tr>\n ))}\n {data.allowlist.length === 0 && (\n <tr>\n <td colSpan={5} style={{ padding: \"2rem\", textAlign: \"center\", color: \"#475569\" }}>\n {labels.allowlistView.empty}\n </td>\n </tr>\n )}\n </tbody>\n </table>\n </div>\n </section>\n )\n}\n\n/**\n * View for the upstream directory: what is linked, how fresh it is, and the controls that change it.\n * 上流ディレクトリのビュー。何が連携され、どれだけ新しく、それを変える操作を提供する。\n *\n * このタブが無いと、連携の作成・解除・一時停止・即時同期・グループ単位のサインイン許可という\n * 5 つの操作にどこからも到達できない。実装されているのに押せる場所が無い状態は、実装が無いのと\n * 変わらないうえ、「許可を消せるが作れない」という片肺の運用を強いる。\n */\nexport function DirectoryView() {\n const data = useLoaderData() as UserAdminLoaderData\n const fetcher = useFetcher()\n const { submit: submitThen, result: actionResult } = useSubmitThen(fetcher)\n const labels = data.labels ?? defaultUserAdminLabels\n const L = labels.directoryView\n\n const linkable = data.groups.filter((g) => !g.externalLink)\n const [groupId, setGroupId] = useState(\"\")\n const [externalKey, setExternalKey] = useState(\"\")\n // ⚠️ 選んだ候補の **安定 ID** を別に持つ。アドレスだけで引き当てると、同じアドレスの行が\n // 2 つある配備 (別名行、改名の途中) で、選んだのとは違う行が控えに出る。手入力に切り替えた\n // ときは空に戻す — 手入力は識別子を証明できないので、サーバー側もアドレスで解決する\n const [pickedExternalId, setPickedExternalId] = useState(\"\")\n const [membershipMode, setMembershipMode] = useState<AuthDirectoryMembershipMode>(\"direct\")\n\n const linked = data.groups.filter((g) => g.externalLink)\n const allowedGroupIds = new Set(data.allowlist.filter((e) => e.type === \"group\").map((e) => e.groupId))\n const allowlistEntryOf = (id: number) => data.allowlist.find((e) => e.type === \"group\" && e.groupId === id)\n const busy = fetcher.state !== \"idle\"\n // 「要求しました」を出す対象。押しても何も変わらないと、何度も押される\n const [queued, setQueued] = useState<{ groupId: number; syncedAtAtPress: string | null } | null>(null)\n /**\n * 直前の「今すぐ同期」が積まれなかったことの通知。\n *\n * ⚠️ 積まれなかったことを成功と同じ見た目にしてはならない。同じ範囲の要求が既に待っている\n * とき積み増さないのは正しいが、黙って成功に見せると、押しても何も起きない画面になる —\n * しかもその待っている行が誰にも拾われなければ、以後の押下すべてが同じように消える\n */\n const syncRequestNotice = syncRequestNoticeOf(actionResult, labels)\n /**\n * 「要求しました」を出し続けてよいグループ。\n *\n * ⚠️ **押した時刻より後に同期が成功していたら、もう出さない。** 出し続けると、運用者は\n * 新しく押した要求と古い表示を見分けられず、同期が終わったのかどうかも判らない。\n * 同期の完了は `syncedAt` が進んだことで判る — それが唯一の観測点である\n */\n const queuedGroupId = (() => {\n if (queued === null) return null\n const target = data.groups.find((g) => g.id === queued.groupId)\n const syncedAt = target?.externalLink?.syncedAt ?? null\n // ⚠️ 端末の時計を判定に使わない。押した時刻 (ブラウザ) と同期時刻 (サーバー) を比べると、\n // 進んだ時計では控えが永久に消えず、遅れた時計では押した直後に消える。**押した瞬間に\n // 見えていた同期時刻から変わったか** だけを見る — どちらもサーバーが作った値である\n if (syncedAt !== queued.syncedAtAtPress) return null\n return queued.groupId\n })()\n // 供給元が判らないまま欄を出すと、送信は必ずサーバー側の内部文言で失敗する\n const canLink = data.directoryStatus.isEnabled && data.directoryStatus.provider !== \"\"\n const catalogOptions = useMemo(() => upstreamOptions(data.catalog, upstreamIndex(linked), labels), [data.catalog, linked, labels])\n // ⚠️ 候補は隠さない (どのローカルグループが押さえているかを見せる価値は残る) が、選べる候補が\n // 1 件も無いなら **その理由を言う**。言わないと、すべて選べない選択欄と押せないボタンだけが\n // 残り、操作者は自分の見落としを探し続ける\n const selectableCatalogCount = useMemo(() => catalogOptions.filter((o) => o.disabled !== true).length, [catalogOptions])\n\n /** 連携を作る。上流キーは運用者が知っている値 (グループアドレス) で受け取る。 */\n const handleLink = (e: React.FormEvent) => {\n e.preventDefault()\n if (!(groupId && externalKey)) return\n // 連携すると名前・説明・メンバー・削除のすべてが上流の管理下へ移る。作成側と同じく、\n // 戻せない操作であることを先に伝える\n const target = linkable.find((g) => String(g.id) === groupId)\n if (!window.confirm(fill(L.confirmLink, { group: target?.name ?? groupId, upstream: externalKey }))) return\n // 供給元は配備の設定から来る。ここに特定の値を書くと、別の上流を使う配備で作った\n // リンクが同期側で恒久的に拒否される\n const sentKey = externalKey\n submitThen({ intent: \"linkDirectoryGroup\", groupId, provider: data.directoryStatus.provider, externalKey: sentKey, externalId: pickedExternalId, membershipMode }, () => {\n setExternalKey((current) => (current === sentKey ? \"\" : current))\n setPickedExternalId((current) => (externalKey === sentKey ? \"\" : current))\n // 連携済みになった行は候補から消える。選択を残すと、表示は空欄なのに前の\n // グループへ送信される\n setGroupId(\"\")\n })\n }\n\n return (\n <section data-testid=\"directory-view-section\" style={{ display: \"flex\", flexDirection: \"column\", gap: \"1rem\" }}>\n <h2 style={heading}>{L.title}</h2>\n <ActionError result={actionResult} />\n\n {!canLink && (\n <p data-testid=\"directory-not-wired\" style={{ margin: 0, padding: \"0.7rem 1rem\", borderRadius: \"6px\", border: \"1px solid #e2e8f0\", backgroundColor: \"#f8fafc\", color: \"#475569\", fontSize: \"0.85rem\" }}>\n {L.notWiredNotice}\n </p>\n )}\n\n {/* ⚠️ 連携候補が無い理由は 2 つあり、向かわせる先が逆である。グループが 1 つも無いのに\n 「すべて連携済み」と言うと、運用者は存在しない連携済み一覧を探し、真の次の一手\n (グループタブで作る) に辿り着けない。空集合の意味を先に分ける */}\n {canLink && data.groups.length === 0 && (\n <p data-testid=\"directory-no-groups\" style={{ margin: 0, padding: \"0.7rem 1rem\", borderRadius: \"6px\", border: \"1px solid #e2e8f0\", backgroundColor: \"#f8fafc\", color: \"#475569\", fontSize: \"0.85rem\" }}>\n {L.noGroupsToLink}\n </p>\n )}\n\n {canLink && data.groups.length > 0 && linkable.length === 0 && (\n <p data-testid=\"directory-nothing-to-link\" style={{ margin: 0, padding: \"0.7rem 1rem\", borderRadius: \"6px\", border: \"1px solid #e2e8f0\", backgroundColor: \"#f8fafc\", color: \"#475569\", fontSize: \"0.85rem\" }}>\n {L.nothingToLink}\n </p>\n )}\n\n {/* ⚠️ ライブリージョンは **常に置いておき、中身だけ入れ替える**。内容と一緒に挿入した\n `role=\"status\"` は読み上げられない — 押しても何も起きなかったことを伝える唯一の\n 手段が、その伝えたい場面でだけ無言になる */}\n <p\n data-testid=\"directory-sync-already-pending\"\n role=\"status\"\n aria-live=\"polite\"\n hidden={syncRequestNotice === null}\n style={syncRequestNotice === null ? undefined : { margin: 0, padding: \"0.5rem 0.7rem\", borderRadius: \"6px\", backgroundColor: \"#fffbeb\", border: \"1px solid #fde68a\", color: \"#92400e\", fontSize: \"0.8rem\" }}>\n {syncRequestNotice}\n </p>\n\n {canLink && linkable.length > 0 && (\n <form onSubmit={handleLink} data-testid=\"directory-link-form\" style={{ display: \"flex\", gap: \"0.75rem\", flexWrap: \"wrap\", alignItems: \"flex-end\", backgroundColor: \"#ffffff\", padding: \"1rem\", borderRadius: \"8px\", border: \"1px solid #e2e8f0\" }}>\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: \"0.25rem\" }}>\n <label htmlFor=\"directory-group-select-input\" style={fieldLabel}>\n {L.selectGroup}\n </label>\n <select id=\"directory-group-select-input\" data-testid=\"directory-group-select\" value={groupId} onChange={(e) => setGroupId(e.target.value)} style={{ padding: \"0.5rem\", borderRadius: \"6px\", border: \"1px solid #cbd5e1\" }}>\n <option value=\"\">{L.selectGroup}</option>\n {linkable.map((g) => (\n <option key={g.id} value={String(g.id)}>\n {fill(L.groupOptionTemplate, { name: g.name, key: g.groupKey })}\n </option>\n ))}\n </select>\n </div>\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: \"0.25rem\", flex: 1, minWidth: \"260px\" }}>\n {/* 候補一覧が引けているなら選ばせる。手入力は綴り違いを「後で赤いバナー」に変える */}\n {/* ⚠️ 「読めたが 1 件も無い」なら手入力へ倒す (選択欄へ倒すと、選べない選択欄と\n 押せないボタンという終端になり、手入力欄まで消える)。行が在るなら選択欄を\n 出すが、そのうち選べるものが 0 件のときは理由を添える — 作成対話と同じ扱い */}\n {data.catalog.kind === \"ready\" && data.catalog.groups.length > 0 ? (\n <>\n <div id=\"directory-external-key-label\" style={fieldLabel}>\n {L.externalKey}\n </div>\n {/* biome-ignore lint/a11y/noStaticElementInteractions: 子の暗黙送信を止めるためだけの捕捉であり、対話要素ではない */}\n <div\n data-testid=\"directory-external-key-picker\"\n onKeyDown={(event) => {\n // 絞り込みを打った直後の Enter が、前回選んだ上流のまま連携を作ってしまう\n // ⚠️ 変換中の Enter には触れない。日本語 IME の確定は\n // `keydown{key:\"Enter\", keyCode:229, isComposing:true}` として届き、\n // ここで既定動作を止めると変換の確定そのものを潰し得る。\n // 変換中はそもそも暗黙の送信は起きないので、止める意味も無い\n if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return\n if (event.key === \"Enter\") event.preventDefault()\n }}>\n <SelectBox\n options={catalogOptions}\n // 選んだ後も **名前とアドレス** を見せる。生のアドレスだけに戻すと、\n // 名前で選んだ運用者に機械的なキーを突き返すことになる\n value={pickedExternalId === \"\" ? null : (catalogOptions.find((o) => o.value === pickedExternalId) ?? null)}\n onChange={(picked: Option | Option[] | null) => {\n if (picked === null || Array.isArray(picked)) {\n setPickedExternalId(\"\")\n setExternalKey(\"\")\n return\n }\n setPickedExternalId(String(picked.value))\n setExternalKey((picked as UpstreamOption).entry.externalKey)\n }}\n placeholder={L.pickUpstreamPlaceholder}\n ariaLabelledBy=\"directory-external-key-label\"\n renderOption={renderUpstreamOption}\n texts={pickerTexts(labels)}\n clearable\n />\n </div>\n {selectableCatalogCount === 0 && (\n <span data-testid=\"directory-all-upstream-linked\" style={{ fontSize: \"0.72rem\", color: \"#92400e\" }}>\n {labels.groupsView.allUpstreamLinked}\n </span>\n )}\n <CatalogFreshness observedAt={data.catalog.observedAt} isStale={data.catalog.isStale} labels={labels} />\n </>\n ) : (\n <>\n <label htmlFor=\"directory-external-key-input\" style={fieldLabel}>\n {L.externalKey}\n </label>\n <input\n id=\"directory-external-key-input\"\n type=\"email\"\n required\n data-testid=\"directory-external-key\"\n placeholder={L.externalKeyPlaceholder}\n value={externalKey}\n onChange={(e) => {\n // 手入力は識別子を証明できない。控えの安定 ID は捨て、\n // サーバー側にもアドレスで解決させる\n setPickedExternalId(\"\")\n setExternalKey(e.target.value)\n }}\n style={{ padding: \"0.5rem\", borderRadius: \"6px\", border: \"1px solid #cbd5e1\" }}\n />\n {data.catalog.kind === \"unavailable\" && (\n <span data-testid=\"directory-catalog-unavailable\" style={{ fontSize: \"0.72rem\", color: \"#92400e\" }}>\n {fill(labels.groupsView.catalogUnavailable, { reason: labels.groupsView.catalogFailures[data.catalog.reason] })}\n </span>\n )}\n {data.catalog.kind === \"ready\" && (\n <span data-testid=\"directory-catalog-empty\" style={{ fontSize: \"0.72rem\", color: \"#475569\" }}>\n {fill(labels.groupsView.catalogEmpty, { at: formatDateTime(data.catalog.observedAt, labels) })}\n </span>\n )}\n </>\n )}\n </div>\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: \"0.25rem\" }}>\n <label htmlFor=\"directory-membership-mode-input\" style={fieldLabel}>\n {L.membershipMode}\n </label>\n <select\n id=\"directory-membership-mode-input\"\n data-testid=\"directory-membership-mode\"\n value={membershipMode}\n onChange={(e) => setMembershipMode(e.target.value as AuthDirectoryMembershipMode)}\n style={{ padding: \"0.5rem\", borderRadius: \"6px\", border: \"1px solid #cbd5e1\" }}>\n <option value=\"direct\">{L.directMode}</option>\n <option value=\"transitive\">{L.transitiveMode}</option>\n </select>\n </div>\n <button\n type=\"submit\"\n data-testid=\"directory-link-submit\"\n disabled={!(groupId && externalKey) || busy}\n aria-busy={busy}\n style={{ padding: \"0.5rem 1rem\", borderRadius: \"6px\", border: \"none\", backgroundColor: groupId && externalKey && !busy ? \"#2563eb\" : \"#94a3b8\", color: \"#fff\", fontWeight: 600 }}>\n {L.link}\n </button>\n </form>\n )}\n\n {linked.length === 0 ? (\n <div data-testid=\"directory-empty\" style={{ padding: \"2rem\", textAlign: \"center\", color: \"#64748b\", backgroundColor: \"#ffffff\", borderRadius: \"8px\", border: \"1px solid #e2e8f0\" }}>\n {L.empty}\n </div>\n ) : (\n <table data-testid=\"directory-table\" style={{ width: \"100%\", borderCollapse: \"collapse\", backgroundColor: \"#ffffff\", borderRadius: \"8px\", overflow: \"hidden\", border: \"1px solid #e2e8f0\" }}>\n <thead>\n <tr style={{ backgroundColor: \"#f8fafc\", textAlign: \"left\" }}>\n <th scope=\"col\" style={cell}>\n {labels.groupsView.name}\n </th>\n <th scope=\"col\" style={cell}>\n {L.externalKey}\n </th>\n <th scope=\"col\" style={cell}>\n {L.membershipMode}\n </th>\n <th scope=\"col\" style={cell}>\n {L.lastSynced}\n </th>\n <th scope=\"col\" style={cell}>\n {labels.common.status}\n </th>\n <th scope=\"col\" style={cell}>\n {labels.common.actions}\n </th>\n </tr>\n </thead>\n <tbody>\n {linked.map((g) => {\n const link = g.externalLink as NonNullable<AuthGroup[\"externalLink\"]>\n const allowed = allowedGroupIds.has(g.id)\n return (\n <tr key={g.id} data-testid={`directory-row-${g.id}`} data-sync-enabled={String(link.isSyncEnabled)} style={{ borderTop: \"1px solid #e2e8f0\" }}>\n <td style={cell}>\n {g.name}\n <div style={{ fontSize: \"0.75rem\", color: \"#64748b\" }}>{g.groupKey}</div>\n </td>\n <td style={cell} data-testid={`directory-external-${g.id}`}>\n {link.externalKey}\n </td>\n <td style={cell}>{link.membershipMode === \"direct\" ? L.directMode : L.transitiveMode}</td>\n <td style={cell} data-testid={`directory-synced-${g.id}`}>\n {link.syncedAt ? formatDateTime(link.syncedAt, labels) : L.neverSynced}\n {link.unresolvedCount > 0 ? (\n <div style={{ fontSize: \"0.75rem\", color: \"#b45309\" }} data-testid={`directory-unresolved-${g.id}`}>\n {fill(L.unresolvedTemplate, { count: link.unresolvedCount })}\n </div>\n ) : null}\n </td>\n <td style={cell}>\n <span\n data-testid={`directory-status-${g.id}`}\n style={{ fontSize: \"0.75rem\", padding: \"2px 8px\", borderRadius: \"10px\", backgroundColor: link.isSyncEnabled ? \"#dcfce7\" : \"#fee2e2\", color: link.isSyncEnabled ? \"#166534\" : \"#b91c1c\", fontWeight: 600 }}>\n {link.isSyncEnabled ? L.syncEnabled : L.syncPaused}\n </span>\n {link.syncError ? (\n <div style={{ fontSize: \"0.75rem\", color: \"#b91c1c\", marginTop: \"0.25rem\" }} data-testid={`directory-error-${g.id}`}>\n {link.syncError}\n </div>\n ) : null}\n </td>\n {/* td 自体を flex にすると表セルの役割が失われ、列と見出しの対応が切れる */}\n <td style={cell}>\n <div style={{ display: \"flex\", gap: \"0.35rem\", flexWrap: \"wrap\" }}>\n <button\n type=\"button\"\n data-testid={`directory-sync-now-${g.id}`}\n {...rowActionProps(busy)}\n aria-label={fill(queuedGroupId === g.id ? L.syncQueuedAria : L.syncNowAria, { group: g.name })}\n onClick={() =>\n submitThen({ intent: \"requestDirectorySync\", groupId: String(g.id) }, (result) => {\n // ⚠️ 積まれたときだけ控えを出す。既に待っている要求があって積み増さなかった場合も\n // 受理として返るため、区別せずに控えを出すと、押した本人には「要求しました」と\n // 見えるのに、その下の説明文は「何も積まれていません」と言う画面になる\n if ((result as { syncRequest?: unknown } | undefined)?.syncRequest !== \"queued\") return\n setQueued({ groupId: g.id, syncedAtAtPress: g.externalLink?.syncedAt ?? null })\n })\n }\n style={smallButton}>\n {queuedGroupId === g.id ? L.syncQueued : L.syncNow}\n </button>\n <button\n type=\"button\"\n data-testid={`directory-toggle-sync-${g.id}`}\n {...rowActionProps(busy)}\n aria-label={fill(link.isSyncEnabled ? L.pauseAria : L.resumeAria, { group: g.name })}\n onClick={() => {\n setQueued(null)\n submitThen({ intent: \"setDirectorySyncEnabled\", groupId: String(g.id), isSyncEnabled: String(!link.isSyncEnabled) })\n }}\n style={smallButton}>\n {link.isSyncEnabled ? L.pause : L.resume}\n </button>\n <button\n type=\"button\"\n data-testid={`directory-allow-${g.id}`}\n {...rowActionProps(busy)}\n aria-label={fill(allowed ? L.disallowGroupAria : L.allowGroupAria, { group: g.name })}\n onClick={() => {\n const entry = allowlistEntryOf(g.id)\n if (allowed && entry) {\n // 取り消しはサインインを止める。追加は元に戻すだけなので確認しない\n if (!window.confirm(fill(L.confirmDisallow, { group: g.name }))) return\n submitThen({ intent: \"removeAllowlistGroup\", entryId: String(entry.id) })\n } else {\n submitThen({ intent: \"addAllowlistGroup\", groupId: String(g.id) })\n }\n }}\n style={smallButton}>\n {allowed ? L.disallowGroup : L.allowGroup}\n </button>\n <button\n type=\"button\"\n data-testid={`directory-unlink-${g.id}`}\n {...rowActionProps(busy)}\n aria-label={fill(L.unlinkAria, { group: g.name })}\n onClick={() => {\n // 台帳とグループ単位の許可が失われる。取り消せないので必ず確認する\n if (!window.confirm(fill(L.confirmUnlink, { group: g.name, upstream: g.externalLink?.externalKey ?? \"\" }))) return\n submitThen({ intent: \"unlinkDirectoryGroup\", groupId: String(g.id) })\n }}\n style={{ ...smallButton, color: \"#b91c1c\", borderColor: \"#fecaca\" }}>\n {L.unlink}\n </button>\n </div>\n </td>\n </tr>\n )\n })}\n </tbody>\n </table>\n )}\n </section>\n )\n}\n\n/** フォーム項目の見出し。プレースホルダは入力すると消えるため名前にならない。 */\nconst fieldLabel: React.CSSProperties = { fontSize: \"0.78rem\", color: \"#475569\", fontWeight: 600 }\n\n/**\n * 目には出さないが、読み上げと焦点には残す指定。\n *\n * `display: none` や `visibility: hidden` はアクセシビリティツリーからも消えるため使えない。\n * ここで隠すのはラジオ入力そのもので、隠しても **焦点は当たり続けなければならない** (矢印キーで\n * 移動できるのはブラウザが焦点を持てるからである)。\n */\nconst visuallyHidden: React.CSSProperties = { position: \"absolute\", width: \"1px\", height: \"1px\", padding: 0, margin: \"-1px\", overflow: \"hidden\", clipPath: \"inset(50%)\", whiteSpace: \"nowrap\", border: 0 }\n/** 各ビューの見出し。段階を飛ばさないよう h2 に固定する。 */\nconst heading: React.CSSProperties = { margin: 0, fontSize: \"1.05rem\", fontWeight: 700, color: \"#0f172a\" }\n/** 表セルの共通スタイル。 */\nconst cell: React.CSSProperties = { padding: \"0.6rem 0.75rem\", fontSize: \"0.85rem\", verticalAlign: \"top\" }\n/** 行内の小さな操作ボタン。 */\n/**\n * Marks a row action as unavailable without taking it out of the tab order.\n * 行の操作を「今は使えない」と示しつつ、タブ順からは外さない処理。\n *\n * ⚠️ `disabled` を使わない。HTML 上の無効化は **焦点を持てない要素** にするため、キーボードで\n * 押した瞬間にその要素が無効になり、ブラウザは焦点を `<body>` へ落とす。しかも busy は表全体で\n * 共有しているので、1 行を押しただけで表中の全ボタンが焦点を失う — 応答が返ったあと、操作者は\n * 文書の先頭から Tab で戻ることになる。`aria-disabled` は焦点を残したまま「使えない」を伝える。\n * 実際に押させないのは呼び出し側の早期 return である。\n *\n * @param busy - Whether a submission is in flight. 送信中かどうか。\n * @returns Props to spread onto the button. ボタンへ展開する属性。\n */\nfunction rowActionProps(busy: boolean): { \"aria-disabled\": boolean; \"aria-busy\": boolean; \"data-busy\": string | undefined } {\n return { \"aria-disabled\": busy, \"aria-busy\": busy, \"data-busy\": busy ? \"true\" : undefined }\n}\n\nconst smallButton: React.CSSProperties = { padding: \"0.25rem 0.6rem\", fontSize: \"0.75rem\", borderRadius: \"6px\", border: \"1px solid #cbd5e1\", backgroundColor: \"#fff\", cursor: \"pointer\" }\n\nconst SEGMENT_VIEWS: Record<string, () => React.ReactElement> = {\n users: UsersView,\n groups: GroupsView,\n group_members: GroupMembersView,\n allowlist: AllowlistView,\n directory: DirectoryView,\n}\n\nexport type AuthUserAdminAppViewProps = UserAdminShellProps\n\n/**\n * Top-level application view component for the user administration UI (`/users/*`).\n * ユーザー管理 UI 全体を描画するトップレベルコンポーネント。\n */\nexport function AuthUserAdminAppView({ renderHeader, pendingSyncPolling }: AuthUserAdminAppViewProps = {}) {\n const data = useLoaderData() as UserAdminLoaderData | undefined\n const segment = data?.segment ?? \"users\"\n // 素のオブジェクト参照だと `segment=\"constructor\"` が真値を返し、画面が描けなくなる\n const View = Object.hasOwn(SEGMENT_VIEWS, segment) ? SEGMENT_VIEWS[segment] : UsersView\n\n return (\n <UserAdminShell renderHeader={renderHeader} pendingSyncPolling={pendingSyncPolling}>\n <View />\n </UserAdminShell>\n )\n}\n"],"mappings":"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","names":["defaultUserAdminLabels","jaUserAdminLabels","resolveUserAdminLabels","over","d","data","fill","template","values","match","key","formatDateTime","iso","labels","normalized","createInMemoryUserAdminStore","initial","users","groups","groupMembers","allowlist","data","newUser","u","id","idx","UserAdminOperatorError","isActive","newGroup","g","m","groupId","userId","existing","newMember","newEntry","a","description","group","isStaleObservation","observedAt","thresholdSec","bound","DEFAULT_STALENESS_SEC","MAX_ACTION_BODY_BYTES","loadCatalog","port","needed","request","error","classifyCatalogFailure","normalizeUpstreamKey","key","message","isOperatorFacing","UserAdminInputError","labelKey","values","isForbidden","requireAllowlistType","formData","raw","SEGMENTS","segmentFromPathname","pathname","parts","last","INTENT_ACCESS","userAdminIntents","userAdminAccessFor","intent","status","requireId","field","n","requireText","value","requireBoolean","requireMembershipMode","optionalText","createUserAdminApp","config","labels","resolveUserAdminLabels","actorOf","requestSyncAfterLink","mayReadCatalog","actions","action","hookError","loader","segment","needsCatalog","directoryStatus","myPermissions","e","catalog","declaredLength","access","isDenied","denied","dispatch","template","fill","resolveExternalId","externalKey","submitted","wanted","match","requireDirectoryEnabled","provider","assertNotAlreadyLinked","externalId","wantedId","wantedKey","clash","assertKnownUpstream","cachedStatus","rawActive","type","resolved","useEffect","useRef","Link","useLoaderData","useRevalidator","jsx","jsxs","DEFAULT_PENDING_SYNC_POLLING","usePendingSyncRevalidation","pendingRequestCount","polling","revalidator","useRevalidator","stateRef","useRef","revalidate","intervalMs","maxMs","useEffect","startedAt","timer","GroupPill","label","summary","DirectorySyncBanner","status","labels","groups","L","warnings","fill","at","formatDateTime","described","id","group","g","reason","w","UserAdminShell","children","renderHeader","pendingSyncPolling","data","useLoaderData","segment","defaultUserAdminLabels","myPermissions","resourceLabels","labelFor","resourceKey","tabs","hasLinkedGroup","permEntries","unavailableSummary","getSummary","perm","annotation","resKey","tab","isActive","Link","getHighlightSegments","SelectBox","useEffect","useMemo","useRef","useState","Link","useFetcher","useLoaderData","useSearchParams","Fragment","jsx","jsxs","ActionError","raw","id","result","useSubmitThen","fetcher","pending","useRef","discarded","setDiscarded","useState","useEffect","onSuccess","payload","useModalBehavior","isOpen","onClose","openerRef","ref","closeRef","panel","opener","focusables","all","element","seenRadioGroups","radio","other","items","onKeyDown","event","first","last","active","outside","upstreamOptions","catalog","linkedTo","labels","entry","holder","normalizeUpstreamKey","linked","members","fill","CatalogFreshness","observedAt","isStale","formatDateTime","hasError","matchStyle","syncRequestNoticeOf","actionResult","outcome","upstreamIndex","groups","index","group","link","renderUpstreamOption","option","context","note","getHighlightSegments","segment","pickerTexts","L","selected","o","info","collatorFor","locale","groupKeyFromUpstream","externalKey","UsersView","data","useLoaderData","useFetcher","submitThen","resetResult","defaultUserAdminLabels","searchQuery","setSearchQuery","modal","setModal","editingUser","isCreateOpen","openModal","next","setNewDisplayName","setNewEmail","newDisplayName","newEmail","createUserRef","editUserRef","busy","sort","setSort","compareText","userNeedle","filteredUsers","u","a","b","direction","toggleSort","column","current","handleToggleActive","user","handleCreateUser","e","sentName","sentEmail","handleUpdateUser","heading","rowActionProps","GroupsView","syncRequestNotice","editingGroup","resetCreateFields","newGroupKey","setNewGroupKey","newName","setNewName","newDescription","setNewDescription","createMode","setCreateMode","focusedMode","setFocusedMode","upstream","setUpstream","membershipMode","setMembershipMode","prefilled","memberCountByGroup","useMemo","counts","m","options","selectableUpstreamCount","upstreamPickable","createGroupRef","editGroupRef","groupNeedle","filteredGroups","g","handleCreateGroup","sent","clear","chooseUpstream","filled","switchCreateMode","mode","handleUpdateGroup","handleDeleteGroup","memberCount","Link","visuallyHidden","fieldLabel","SelectBox","picked","GroupMembersView","searchParams","useSearchParams","requested","deepLinked","setPicked","justRemoved","setJustRemoved","lastDeepLink","memberQuery","setMemberQuery","onlyMembers","setOnlyMembers","requestedGroupId","currentGroup","missedTarget","currentMemberUserIds","needle","visibleUsers","handleToggleMember","userId","isMember","groupId","isSelected","allowlistBadgeStyle","type","allowlistTypeLabel","AllowlistView","pattern","setPattern","setType","description","setDescription","handleAddEntry","handleRemoveEntry","handleRemoveGroupEntry","item","DirectoryView","linkable","setGroupId","setExternalKey","pickedExternalId","setPickedExternalId","allowedGroupIds","allowlistEntryOf","queued","setQueued","queuedGroupId","canLink","catalogOptions","selectableCatalogCount","handleLink","target","sentKey","cell","allowed","smallButton","SEGMENT_VIEWS","AuthUserAdminAppView","renderHeader","pendingSyncPolling","View","UserAdminShell"]}
|