@aiquants/auth-react-router 0.13.1 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -90,6 +90,62 @@ export default () => <AuthUserAdminAppView />
90
90
  - Store errors (e.g. "cannot remove the last administrator") are returned as `{ ok: false, error }` and rendered by the views; guard rejections propagate untouched.
91
91
  - Labels default to **English** (`defaultUserAdminLabels`); inject `jaUserAdminLabels` or a partial override via `resolveUserAdminLabels`.
92
92
 
93
+ ### Upstream group catalog (optional port)
94
+
95
+ The groups and directory tabs let the operator **pick** an upstream group instead of spelling its address. Wire the optional
96
+ `directoryCatalog` port to enable it; omit it and both surfaces fall back to typing the address by hand.
97
+
98
+ ```ts
99
+ createUserAdminApp({
100
+ store: myUserAdminStore,
101
+ guards: { ... },
102
+ // `observedAt` is required. It is `null` only when the upstream has NEVER been observed —
103
+ // which is not the same as "observed, and the answer was empty".
104
+ directoryCatalog: {
105
+ listGroups: async (request) => {
106
+ const { groups, observedAt } = await myCatalogReader.read(request)
107
+ return { groups, observedAt }
108
+ },
109
+ },
110
+ })
111
+ ```
112
+
113
+ The list is usually a **copy** written by a separate process that holds the upstream credential, not a live read.
114
+ That is why the port must report when the upstream was observed: a list with no timestamp reads as "the directory right
115
+ now", and the operator picks a group that no longer exists. `observedAt: null` is rendered as `unavailable /
116
+ neverObserved` rather than as an empty list, so a deployment whose synchronization has never run is never mistaken for a
117
+ domain with no groups.
118
+
119
+ - **The catalog is a convenience read, never a gate.** A failure is reported to the view as
120
+ `{ kind: "unavailable", reason }` — the tab still opens, and manual entry stays available. It is fetched only for the two
121
+ segments that render a picker, and only for the intents that create a link.
122
+ - **`reason` is a classification, not the upstream's text** (`denied` / `rateLimited` / `upstreamError` /
123
+ `neverObserved` / `unknown`; the first three are derived from a numeric `status` when the thrown value carries one,
124
+ `neverObserved` from a `null` observation time). The original error goes to `console.error` only: an upstream message
125
+ can carry the service-account address, the impersonated subject, internal URLs, or a credential fragment, and holding
126
+ read access to this page is not a reason to be shown any of them.
127
+ - **The same rule governs store errors.** An exception's own text reaches the operator only when the store declares it
128
+ operator-facing — by throwing `UserAdminOperatorError`, or by setting `operatorFacing: true` on the error (for hosts
129
+ that cannot extend this package's class). Everything else is reported as a generic sentence and logged: a driver
130
+ exception carries the schema, table, constraint and the offending values.
131
+ - **The provider is taken from `getDirectoryStatus()`, never from the form**, and the link intents are refused outright
132
+ when the directory is not enabled. The absence of a control in the UI is not a constraint.
133
+ - **`observedAt` and `isStale` are decided on the server.** Judging staleness in the browser lets a client whose clock
134
+ runs behind suppress the warning, and desynchronizes server and client rendering.
135
+ - **A host that overrides `labels` wholesale must supply `groupsView.catalogFailures`** (five keys) alongside the rest;
136
+ `resolveUserAdminLabels` fills anything omitted from a partial override.
137
+ - **Listing the catalog requires the same verbs as creating a link** (`update` + `create` + `delete`), not `read`. It
138
+ enumerates another organization's department structure and headcounts; a principal who cannot link has no use for it.
139
+ A denial is turned into "no picker", never into a failed page.
140
+ - **The action re-checks what the picker offered.** A key the catalog does not list is refused server-side (case and
141
+ surrounding whitespace are treated as equal), because the picker's `disabled` rows are a UI promise, not a constraint.
142
+ When no catalog is wired — or the upstream is unreachable — the check is skipped rather than turned into a refusal:
143
+ refusing there would make linking impossible in exactly the deployments that must type the address by hand.
144
+ - **An upstream group already linked to another local group is refused**, so its membership cannot be projected onto two
145
+ groups (double-counted members, both sets of roles granted).
146
+ - `createLinkedGroup` creates the group and links it in **one** intent, and therefore requires the union of the verbs both
147
+ halves need (`create` + `update` + `delete`). Splitting it in two would leave a state where only one half succeeded.
148
+
93
149
  Styling: this package has **no hand-written component CSS** (the `GoogleForm` classes are plain Tailwind utilities), so it ships **no components-only artifact** — only a standalone build. Two consumption modes, never mixed:
94
150
 
95
151
  - **Tailwind v4 host** — add `@source "../node_modules/@aiquants/auth-react-router/src/**/*.{ts,tsx}";` (monorepo: `../../../../packages/auth-react-router/src/**/*.{ts,tsx}`) so the `GoogleForm` classes are generated in the host's own canonical build; `src` ships in the published package.
@@ -101,6 +157,23 @@ Styling: this package has **no hand-written component CSS** (the `GoogleForm` cl
101
157
 
102
158
  Never mix the two: importing the standalone utility CSS next to a host Tailwind build duplicates same-named utilities, and base/variant cascade winners flip versus the single-build canonical order.
103
159
 
160
+ The admin surface renders `@aiquants/select-box`, whose own stylesheet is **not** part of either artifact above. Import it in
161
+ the host, or the group picker's dropdown renders unstyled:
162
+
163
+ ```css
164
+ @import "@aiquants/select-box/styles/select-box.css";
165
+ ```
166
+
167
+ Its transitive peers (`@aiquants/fuzzy-search`, `@aiquants/virtualscroll`) are declared here as well, so a host that
168
+ installs this package is told what the picker needs rather than discovering it as a runtime resolution failure.
169
+
170
+ All three are **required** peers, not optional. `src/admin/views.tsx` imports `@aiquants/select-box` at the top level and
171
+ `./admin` re-exports it, so the specifier survives into `dist/admin.mjs`: a host without it does not get a degraded
172
+ picker, it gets `ERR_MODULE_NOT_FOUND` for the whole admin surface — including the groups and allowlist tabs, which have
173
+ nothing to do with the picker. Marking them optional would suppress the install warning that is the only thing standing
174
+ between a consumer and that failure. Note also that `@aiquants/select-box` pins React 19, which is stricter than this
175
+ package's own `react: ">=18"`; a host that mounts `/admin` must satisfy the stricter one.
176
+
104
177
  ## Behavior Contract
105
178
 
106
179
  - **Session cookie byte compatibility**: `__session` (httpOnly/lax/30d), key `"user"`, value = `{ id, displayName, name, emails, accessToken, refreshToken?, expirationDateMs?, provider, role? }` — never `photo`/`_json`/`photos`. Changing this invalidates all live 30-day sessions.
package/dist/admin.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_router from 'react-router';
2
- import { AuthUser, AuthGroup, AuthGroupMember, AuthAllowlistEntry, AuthDirectoryMembershipMode, AuthDirectoryStatus } from '@aiquants/auth-core';
2
+ import { AuthUser, AuthGroup, AuthGroupMember, AuthAllowlistEntry, AuthDirectoryMembershipMode, AuthDirectoryStatus, AuthDirectoryCatalogSnapshot, AuthDirectoryCatalogGroup } from '@aiquants/auth-core';
3
3
  import React__default from 'react';
4
4
 
5
5
  /**
@@ -48,6 +48,32 @@ type UserAdminLabels = {
48
48
  };
49
49
  /** アクションが拒否・失敗したときの文言。 */
50
50
  errors: {
51
+ /** 一覧に無い上流キーを指定した。`{key}` に受け取った値。 */
52
+ unknownUpstream: string;
53
+ /** その上流グループは既に別のローカルグループへ連携している。`{key}` / `{group}`。 */
54
+ alreadyLinked: string;
55
+ /** ディレクトリ未設定の配備で連携を求められたときの説明。 */
56
+ directoryDisabled: string;
57
+ /** 必須欄が欠けている、または読めない値だったことの説明。 */
58
+ invalidField: string;
59
+ /**
60
+ * Shown for an intent this screen does not perform. この画面が行わない intent への文言。
61
+ *
62
+ * ⚠️ 受け取った値を含めない。認可より手前で返るため、認証を通っていない相手にも届く。
63
+ */
64
+ unknownIntent: string;
65
+ /**
66
+ * Operator-facing names for the form fields. 欄の、操作者に見える名前。
67
+ *
68
+ * ⚠️ `invalidField` の `{field}` に **通信上のキー** を差し込まない。日本語の文の中に
69
+ * `membershipMode` のような識別子が現れ、画面のどの欄を指すのか操作者には判らない。
70
+ * 表に無い欄はキーのまま出す (出さないより、判りにくくても出すほうがよい)。
71
+ */
72
+ fieldNames: Record<string, string>;
73
+ /** 許可リストの種別が既定の 2 値でなかったことの説明。 */
74
+ invalidAllowlistType: string;
75
+ /** 想定外の失敗 (原文はサーバーのログにだけ残す)。 */
76
+ unexpected: string;
51
77
  /**
52
78
  * Shown when the actor lacks permission for the attempted operation.
53
79
  * 操作の権限が無いときに出す文言。
@@ -56,6 +82,13 @@ type UserAdminLabels = {
56
82
  * 拒否理由といった内部の語彙が、権限を持たない相手にそのまま渡る。
57
83
  */
58
84
  forbidden: string;
85
+ /**
86
+ * Shown when the request body exceeds the ceiling. 本文が上限を越えたときの文言。
87
+ *
88
+ * この画面が送るのは短い文字列だけなので、越えるのは誤操作か攻撃である。どちらにも
89
+ * 同じ文言を返す (どこで弾かれたかを細かく教えない)。
90
+ */
91
+ requestTooLarge: string;
59
92
  };
60
93
  /** シェル (タブ枠) の見出し。 */
61
94
  heading: string;
@@ -64,6 +97,8 @@ type UserAdminLabels = {
64
97
  label: string;
65
98
  canWrite: string;
66
99
  canRead: string;
100
+ /** 削除だけを持つ主体の表示 (読み書きが無いことを「権限なし」と描かない)。 */
101
+ canDelete: string;
67
102
  none: string;
68
103
  /** 未結線・取得失敗・付与ゼロのいずれも「不明」であり、許可ではない。 */
69
104
  unknown: string;
@@ -77,6 +112,8 @@ type UserAdminLabels = {
77
112
  };
78
113
  usersView: {
79
114
  title: string;
115
+ /** 並べ替えの読み上げ名。`{column}` は列名。 */
116
+ sortBy: string;
80
117
  searchPlaceholder: string;
81
118
  /** 検索欄の読み上げ名。プレースホルダは入力すると消えるため名前にならない。 */
82
119
  searchLabel: string;
@@ -93,12 +130,65 @@ type UserAdminLabels = {
93
130
  activateAria: string;
94
131
  deactivateAria: string;
95
132
  /** 無効化はサインインを止める。確認を取る。 */
133
+ /** 検索で 0 件になったときの文言 (「登録が無い」と区別する)。 */
134
+ emptyFiltered: string;
135
+ /** 一覧の絞り込み結果を読み上げるための文言。 */
136
+ listSummary: string;
137
+ /** 昇順で並べ替え中の列の読み上げ名。 */
138
+ sortedAscending: string;
139
+ /** 降順で並べ替え中の列の読み上げ名。 */
140
+ sortedDescending: string;
96
141
  confirmDeactivate: string;
97
142
  empty: string;
98
143
  };
99
144
  groupsView: {
100
145
  /** 検索語に一致しないときの説明。空状態と混ぜると「消えた」と誤読される。 */
101
146
  emptyFiltered: string;
147
+ /** 作成方法の切替。ローカルに作るか、上流のグループから作るか。 */
148
+ /** 作成方法を選ぶ操作群の読み上げ名。 */
149
+ createModeLabel: string;
150
+ createModeLocal: string;
151
+ createModeUpstream: string;
152
+ /** 上流グループを選ぶ欄。 */
153
+ pickUpstream: string;
154
+ pickUpstreamPlaceholder: string;
155
+ /** 選択肢 1 件の副題。`{count}` はメンバー数。 */
156
+ upstreamOptionMembers: string;
157
+ /** すでに連携済みの選択肢に付ける表示。 */
158
+ upstreamAlreadyLinked: string;
159
+ /** 候補一覧が引けなかったときの説明。`{reason}` に理由。 */
160
+ /** 候補は引けたが、この配備の連携プロバイダ名が決まっていないことの説明。 */
161
+ providerUnset: string;
162
+ /** 候補を選ぶまで作成できないことの説明 (押せないボタンの理由)。 */
163
+ pickUpstreamFirst: string;
164
+ /**
165
+ * Shown when every upstream group is already linked. 上流のすべてが連携済みのときの文言。
166
+ *
167
+ * ⚠️ この場面で「先に選べ」と言ってはならない。従いようのない指示になる。
168
+ */
169
+ allUpstreamLinked: string;
170
+ /** 上流を読めたが 1 件も無かったことの説明。 */
171
+ catalogEmpty: string;
172
+ catalogUnavailable: string;
173
+ /** 上流へ届かないときの案内 (作成対話向け — この対話にアドレス欄は無い)。 */
174
+ catalogUnavailableHere: string;
175
+ /** 候補一覧をいつ観測したか。 */
176
+ catalogObservedAt: string;
177
+ /** 観測が配備の許す鮮度を越えている旨。 */
178
+ catalogStale: string;
179
+ /** 候補一覧を引けなかった理由の文言 (上流の例外文は使わない)。 */
180
+ catalogFailures: {
181
+ denied: string;
182
+ rateLimited: string;
183
+ upstreamError: string;
184
+ neverObserved: string;
185
+ unknown: string;
186
+ };
187
+ /** 上流から作る場合の説明 (何が起きるかを先に伝える)。 */
188
+ createUpstreamNotice: string;
189
+ /** グループのメンバー面へ移動する操作。 */
190
+ openMembers: string;
191
+ openMembersAria: string;
102
192
  title: string;
103
193
  searchPlaceholder: string;
104
194
  searchLabel: string;
@@ -110,6 +200,8 @@ type UserAdminLabels = {
110
200
  /** 所属人数。`{count}` を人数へ置換する。 */
111
201
  memberCountTemplate: string;
112
202
  confirmDelete: string;
203
+ /** 連携付きで作る前の確認。`{group}` / `{upstream}`。委譲は元に戻しづらい。 */
204
+ confirmCreateLinked: string;
113
205
  /** 行ごとの読み上げ名。`{group}` をグループ名へ置換する。 */
114
206
  editGroupAria: string;
115
207
  deleteGroupAria: string;
@@ -120,8 +212,25 @@ type UserAdminLabels = {
120
212
  };
121
213
  groupMembersView: {
122
214
  title: string;
215
+ /** 利用者の絞り込み。数百人の一覧を目で追わせない。 */
216
+ searchLabel: string;
217
+ searchPlaceholder: string;
218
+ /** 所属している人だけに絞る切替。`{count}` は該当数。 */
219
+ onlyMembers: string;
220
+ /** 絞り込み結果が空のときの説明。 */
221
+ emptyFiltered: string;
222
+ /** 一覧の要約。`{shown}` / `{total}` / `{members}`。 */
223
+ summary: string;
123
224
  selectGroup: string;
124
225
  selectPrompt: string;
226
+ /** グループが 1 つも無い配備での説明 (「選べ」は従えない指示になる)。 */
227
+ noGroups: string;
228
+ /** 所属だけに絞った結果 0 件になったときの説明。 */
229
+ emptyNoMembers: string;
230
+ /** 利用者が 1 人も登録されていない配備での説明。 */
231
+ noUsers: string;
232
+ /** 深いリンクが指すグループが見つからず、代替を出していることの説明。 */
233
+ deepLinkMissed: string;
125
234
  actionAdd: string;
126
235
  actionRemove: string;
127
236
  /** 行ごとの読み上げ名。`{user}` を利用者名へ置換する。 */
@@ -169,6 +278,8 @@ type UserAdminLabels = {
169
278
  syncEnabled: string;
170
279
  syncNow: string;
171
280
  syncQueued: string;
281
+ /** 同じ範囲の要求が既に待っていて、積まれなかったことの説明。 */
282
+ syncAlreadyPending: string;
172
283
  /** 同期が滞っていることの警告。停止はセキュリティ事象である。 */
173
284
  staleWarning: string;
174
285
  disabledWarning: string;
@@ -186,6 +297,8 @@ type UserAdminLabels = {
186
297
  pendingTemplate: string;
187
298
  /** 連携先が 1 つも残っていないときの説明。 */
188
299
  nothingToLink: string;
300
+ /** グループが 1 つも無い配備での説明 (「全部が連携済み」と区別する)。 */
301
+ noGroupsToLink: string;
189
302
  /** 未結線の配備で操作を出さない理由。 */
190
303
  notWiredNotice: string;
191
304
  /** 行ごとの読み上げ名。`{group}` をグループ名へ置換する。 */
@@ -204,9 +317,21 @@ type UserAdminLabels = {
204
317
  empty: string;
205
318
  /** 連携するグループを選ぶ欄。 */
206
319
  selectGroup: string;
320
+ /** 選択欄が自前で持つ文言。渡さないと英語のまま出る。 */
321
+ /** 選択欄の入力の手引き。 */
322
+ pickUpstreamPlaceholder: string;
323
+ pickerNoOptions: string;
324
+ pickerClear: string;
325
+ pickerToggle: string;
326
+ /** 選択中の値の読み上げ。`{value}`。 */
327
+ pickerSelected: string;
328
+ /** 候補件数の読み上げ。`{count}`。 */
329
+ pickerAvailable: string;
207
330
  /** 上流グループのアドレス入力。 */
208
331
  externalKeyPlaceholder: string;
209
332
  /** 連携解除の確認。台帳と許可設定が消える。 */
333
+ /** 連携を作る前の確認 (元へ戻すには連携解除という別の操作が要る)。 */
334
+ confirmLink: string;
210
335
  confirmUnlink: string;
211
336
  /** 同期の一時停止・再開。 */
212
337
  pause: string;
@@ -303,21 +428,43 @@ type UserAdminStore = {
303
428
  */
304
429
  linkDirectoryGroup: (groupId: number, data: {
305
430
  provider: string;
431
+ externalId: string;
306
432
  externalKey: string;
307
433
  membershipMode: AuthDirectoryMembershipMode;
308
434
  }) => Promise<void>;
435
+ /**
436
+ * Creates a local group and links it to an upstream group as one indivisible step.
437
+ * ローカルグループの作成と上流への連携を、分割不可能な 1 手として行う処理。
438
+ *
439
+ * ⚠️ **途中で失敗したら何も残してはならない。** 作成だけが成功すると、運用者には「連携に
440
+ * 失敗した」ではなく「知らないグループが増えた」と見える。実装は 1 つのトランザクションで
441
+ * 行うこと (作成 → 連携の順に別々の書込を並べると、この不可分性は成立しない)。
442
+ */
443
+ createLinkedGroup: (data: {
444
+ groupKey: string;
445
+ name: string;
446
+ description?: string;
447
+ provider: string;
448
+ externalId: string;
449
+ externalKey: string;
450
+ membershipMode: AuthDirectoryMembershipMode;
451
+ }) => Promise<AuthGroup>;
309
452
  /** Stops following the upstream, keeping the memberships already projected. 連携の解除。 */
310
453
  unlinkDirectoryGroup: (groupId: number) => Promise<void>;
311
454
  /** Pauses or resumes synchronization for one group. 1 グループの同期を停止・再開する処理。 */
312
455
  setDirectorySyncEnabled: (groupId: number, isSyncEnabled: boolean) => Promise<void>;
313
456
  /**
314
- * Queues a synchronization request. 同期要求をキューへ積む処理。
457
+ * Queues a "sync now" request. 「今すぐ同期」の要求を積む処理。
315
458
  *
316
- * 上流へ到達できるのは同期ジョブだけなので、web からは要求を残すことしかできない。
459
+ * ⚠️ 戻り値を `void` にしてはならない。同じ範囲の要求が既に待っているとき、積み増さないのは
460
+ * 正しい (押した数だけ上流への全件走査が並ぶ) が、それを成功と区別せずに返すと、押しても
461
+ * 反応しない画面ができる — しかも詰まった 1 行が以後の押下すべてを飲み込む。
317
462
  *
318
- * @param groupId `null` requests every linked group of the tenant. `null` はテナント全件。
463
+ * @param groupId Target group, or `null` for the whole tenant. 対象グループ (`null` でテナント全件)。
464
+ * @param actor Who asked, for the audit record. 要求した主体 (監査記録用)。
465
+ * @returns Whether it was queued or already waiting. 積んだか、既に待っていたか。
319
466
  */
320
- requestDirectorySync: (groupId: number | null) => Promise<void>;
467
+ requestDirectorySync: (groupId: number | null, actor: string) => Promise<UserAdminSyncRequestOutcome>;
321
468
  /** Health of the synchronization, for the header warning. 同期の健全性 (警告表示用)。 */
322
469
  getDirectoryStatus: () => Promise<AuthDirectoryStatus>;
323
470
  };
@@ -332,10 +479,44 @@ declare function createInMemoryUserAdminStore(initial?: {
332
479
  allowlist?: AuthAllowlistEntry[];
333
480
  }): UserAdminStore;
334
481
  /**
335
- * The CRUD verb an operation performs. Deliberately a plain string union so this package stays
336
- * independent of any particular authorization library.
337
- * 操作が行う CRUD 種別。特定の認可ライブラリに依存しないよう素の文字列 union で表現する。
482
+ * Normalizes an upstream key for comparison. 上流キーを比較用に正規化する処理。
483
+ *
484
+ * 前後の空白と大文字小文字を同一視する。**画面と実装で正規化がずれると**、選択欄は「未連携」と
485
+ * 見なすキーをサーバーが「連携済み」と見なす、といった食い違いが生まれる。1 か所に置く。
486
+ *
487
+ * @param key Raw upstream key. 生の上流キー。
488
+ * @returns The comparable form. 比較可能な形。
338
489
  */
490
+ declare function normalizeUpstreamKey(key: string): string;
491
+ /**
492
+ * An error whose own text is meant for the operator to read.
493
+ * 文面を **運用者に読ませるつもりで** 投げるエラー。
494
+ *
495
+ * ⚠️ ストアの例外文をそのまま画面へ返してはならない。ドライバの例外はスキーマ名・表名・制約名・
496
+ * 衝突した値・接続先をそのまま含み、それが閲覧権限しか持たない主体の画面に出る。一方で
497
+ * 「最後の管理者は外せません」のような業務上の拒否理由は、読めなければ操作そのものが行き詰まる。
498
+ * 両者は文面からは区別できないので、**投げる側が宣言する**。宣言の無い例外は一般的な文言になる。
499
+ *
500
+ * ホストのストアは、この class を継承しても `operatorFacing: true` を持たせてもよい
501
+ * (別パッケージのエラー型を継承できない事情に配慮した二経路である)。
502
+ */
503
+ declare class UserAdminOperatorError extends Error {
504
+ /** 運用者向けであることの印。継承できない場合はこの属性だけでもよい。 */
505
+ readonly operatorFacing = true;
506
+ constructor(message: string);
507
+ }
508
+ /**
509
+ * An input problem the operator can fix, carrying a label key instead of a sentence.
510
+ * 運用者が直せる入力の問題。文ではなく **ラベルのキー** を運ぶ。
511
+ *
512
+ * ⚠️ メッセージを実装側で組むと、既定 (英語) の配備に日本語が出る。文言はラベルに集約し、
513
+ * ここでは「どの文言か」と差し替え値だけを運ぶ。
514
+ */
515
+ declare class UserAdminInputError extends Error {
516
+ readonly labelKey: keyof UserAdminLabels["errors"];
517
+ readonly values: Record<string, string | number>;
518
+ constructor(labelKey: keyof UserAdminLabels["errors"], values?: Record<string, string | number>);
519
+ }
339
520
  /**
340
521
  * The only route-args field this module reads. React Router's full `LoaderFunctionArgs` satisfies it, so
341
522
  * route wiring is unchanged — but declaring the minimum keeps callers (and tests) from fabricating fields
@@ -345,6 +526,11 @@ declare function createInMemoryUserAdminStore(initial?: {
345
526
  type RouteArgs = {
346
527
  request: Request;
347
528
  };
529
+ /**
530
+ * The CRUD verb an operation performs. Deliberately a plain string union so this package stays
531
+ * independent of any particular authorization library.
532
+ * 操作が行う CRUD 種別。特定の認可ライブラリに依存しないよう素の文字列 union で表現する。
533
+ */
348
534
  type UserAdminAccess = "read" | "create" | "update" | "delete";
349
535
  /**
350
536
  * Access guard the user-admin surface is mounted behind. Deny by throwing
@@ -377,6 +563,83 @@ type UserAdminGuards = {
377
563
  error: unknown;
378
564
  }) => void;
379
565
  };
566
+ /**
567
+ * Every intent the permission table knows. 権限表が知っている intent すべて。
568
+ *
569
+ * ⚠️ 消費側の網羅性検査のために公開する。検査側が intent を手書きで並べる形しか無いと、
570
+ * 実装に intent が増えても表は勝手には伸びず、**最も権限の重い新しい intent** だけが
571
+ * 無検査で通る状態が静かに生まれる。突き合わせる相手を実装側から提供する。
572
+ *
573
+ * @returns Intent names, in declaration order. 宣言順の intent 名。
574
+ */
575
+ declare function userAdminIntents(): string[];
576
+ /**
577
+ * The CRUD verbs one intent requires. 1 つの intent が要求する CRUD 種別。
578
+ *
579
+ * ⚠️ 消費側が権限表を検査できるようにするために公開する。表そのものは公開しない — 書き換え
580
+ * 可能な参照を渡すと、検査の対象が実行時に差し替えられる形になる。
581
+ *
582
+ * @param intent - Intent name. intent 名。
583
+ * @returns The required verbs, or an empty list for an unknown intent. 要求する種別 (未知なら空)。
584
+ */
585
+ declare function userAdminAccessFor(intent: string): readonly UserAdminAccess[];
586
+ /**
587
+ * One upstream group as the picker shows it. 選択欄に出す上流グループ 1 件。
588
+ *
589
+ * ⚠️ **別名ではなく `@aiquants/auth-core` の型そのものである。** この形は上流アダプタ・永続化層・
590
+ * 管理 UI の 3 層を通る。層ごとに「同じ意味の別の型」を宣言すると、構造的部分型のせいでどの境界も
591
+ * 通ってしまい、上流へ足した欄が途中で黙って捨てられる (実測: 別々に宣言していた間は、必須欄を
592
+ * 足しても読み手側の境界で `tsc --strict` が 0 を返した)。欄の集合は `auth-core` の型テストが固定
593
+ * しているので、足すときは 3 層すべての受け渡しを見直すことになる。
594
+ *
595
+ * 特定のディレクトリ実装 (Google 等) の型は依然として import しない — `auth-core` は上流に依存
596
+ * しない語彙のパッケージであり、`AuthDirectoryStatus` などを既にここから受け取っている。
597
+ */
598
+ type UserAdminCatalogGroup = AuthDirectoryCatalogGroup;
599
+ /**
600
+ * The upstream catalog as the loader hands it to the views. loader がビューへ渡す上流の候補一覧。
601
+ *
602
+ * ⚠️ **これは利便性のための読み取りである。** 上流へ届かなくても画面は開かなければならない
603
+ * (グループ一覧や許可リストの閲覧は、選択欄の可否とは無関係である)。したがって失敗は
604
+ * `reason` として **描画可能な状態** に変換し、例外にしない。認可やサインイン判定のように
605
+ * 「判らないなら拒否」であるべき経路とは、意図的に扱いを変えている。
606
+ */
607
+ type UserAdminCatalog = {
608
+ kind: "ready";
609
+ groups: UserAdminCatalogGroup[];
610
+ observedAt: string;
611
+ isStale: boolean;
612
+ } | {
613
+ kind: "unavailable";
614
+ reason: UserAdminCatalogFailure;
615
+ } | {
616
+ kind: "not-wired";
617
+ };
618
+ /**
619
+ * What the catalog port answers: the groups, and when the upstream was actually observed.
620
+ * 候補一覧ポートの答え。グループと、上流を実際に観測した時刻。
621
+ *
622
+ * ⚠️ `observedAt` は省略できない。この一覧は上流を直に叩いた結果とは限らず、資格情報を持つ別の
623
+ * プロセスが書いた写しであることが多い。時刻を伴わない一覧は「上流の現在」と区別できず、運用者は
624
+ * 既に消えたグループを選ぶ。`null` は **一度も観測していない** ことを意味する (空の答えではない)。
625
+ */
626
+ type UserAdminCatalogSnapshot = AuthDirectoryCatalogSnapshot;
627
+ /**
628
+ * What a "sync now" request did. 「今すぐ同期」の要求が行ったこと。
629
+ *
630
+ * `already-pending` は失敗ではない — 同じ範囲の要求が既に待っている。ただし **成功でもない**:
631
+ * 何も積まれていないので、待っている行が誰にも拾われなければ何も起きない。
632
+ */
633
+ type UserAdminSyncRequestOutcome = "queued" | "already-pending";
634
+ /**
635
+ * Why the upstream catalog could not be listed, classified for display.
636
+ * 上流の候補一覧を引けなかった理由を、表示のために分類したもの。
637
+ *
638
+ * ⚠️ **上流の例外文をそのまま画面へ出してはならない。** サービスアカウントのアドレス、借用先、
639
+ * 内部 URL、資格情報の断片が混じり得る。管理面を開けるだけの主体に、それらを読ませる理由は無い。
640
+ * 原文は `console.error` へ出し、画面にはこの分類に対応する自前の文言を出す。
641
+ */
642
+ type UserAdminCatalogFailure = "denied" | "rateLimited" | "upstreamError" | "neverObserved" | "unknown";
380
643
  /**
381
644
  * Neutral per-resource permission summary the admin shell renders in its header badge.
382
645
  * 管理シェルのヘッダーバッジが描画する、リソース単位の中立な権限サマリ。
@@ -399,6 +662,26 @@ type UserAdminAppConfig = {
399
662
  * 必須。省略を許すとユーザー名簿と全更新操作が無防備に露出するため、型で強制する。
400
663
  */
401
664
  guards: UserAdminGuards;
665
+ /**
666
+ * Names who is acting, for audit columns. 監査列に残す「誰が」を解決する処理。
667
+ *
668
+ * ⚠️ 省略すると、要求を積んだ主体が記録されない。同期要求の表は「誰がいつ何を要求し、結果が
669
+ * どうだったか」を残すために在り、その `requested_by` が定数だと、乱発を疑ったときに誰の
670
+ * ものか判らない — 乱発こそがこの intent に `update` を要求した理由である。
671
+ */
672
+ resolveActor?: (request: Request) => Promise<string> | string;
673
+ /**
674
+ * Lists the upstream groups the operator may link to. 連携先の候補を列挙するポート。
675
+ *
676
+ * 任意である。注入しない配備では選択欄を出さず、上流キーの手入力に留まる — 上流へ問い合わせる
677
+ * 資格情報を web 側へ置かない構成を選べるようにするためである (同期ジョブだけが鍵を持つ形)。
678
+ *
679
+ * ⚠️ **失敗しても画面を落としてはならない。** 例外は loader が捕まえて
680
+ * `{ kind: "unavailable" }` へ写す。ここが落ちると、グループ一覧すら開けなくなる。
681
+ */
682
+ directoryCatalog?: {
683
+ listGroups: (request: Request) => Promise<UserAdminCatalogSnapshot>;
684
+ };
402
685
  /**
403
686
  * Per-request self-permission lookup for the header annotation. Resolving to a map of
404
687
  * {@link UserAdminPermission} shows the badge; rejecting shows an explicit "unknown" badge — never a
@@ -431,6 +714,7 @@ declare function createUserAdminApp(config: UserAdminAppConfig): {
431
714
  allowlist: AuthAllowlistEntry[];
432
715
  directoryStatus: AuthDirectoryStatus;
433
716
  myPermissions: Record<string, UserAdminPermission | undefined> | null;
717
+ catalog: UserAdminCatalog;
434
718
  labels: UserAdminLabels;
435
719
  }>;
436
720
  action: ({ request }: RouteArgs) => Promise<react_router.UNSAFE_DataWithResponseInit<{
@@ -438,7 +722,10 @@ declare function createUserAdminApp(config: UserAdminAppConfig): {
438
722
  error: string;
439
723
  }> | {
440
724
  ok: boolean;
441
- error?: undefined;
725
+ syncRequest?: undefined;
726
+ } | {
727
+ ok: boolean;
728
+ syncRequest: UserAdminSyncRequestOutcome;
442
729
  } | {
443
730
  ok: boolean;
444
731
  error: string;
@@ -458,6 +745,15 @@ type UserAdminShellProps = {
458
745
  */
459
746
  declare function UserAdminShell({ children, renderHeader }: UserAdminShellProps): React__default.JSX.Element;
460
747
 
748
+ /**
749
+ * @module admin/views
750
+ * @description The five tab views of the user administration screen.
751
+ * ユーザー管理画面を構成する 5 つのタブビュー。
752
+ *
753
+ * 一覧・検索・所属・許可リスト・ディレクトリ連携を描く。データはすべて loader から渡され、
754
+ * 書き込みはビュー唯一の fetcher (`useSubmitThen`) を通す。
755
+ */
756
+
461
757
  type UserAdminLoaderData = {
462
758
  segment: string;
463
759
  users: AuthUser[];
@@ -465,6 +761,8 @@ type UserAdminLoaderData = {
465
761
  groupMembers: AuthGroupMember[];
466
762
  allowlist: AuthAllowlistEntry[];
467
763
  directoryStatus: AuthDirectoryStatus;
764
+ /** 上流の候補一覧。選択欄を出す面でのみ `ready` になる。 */
765
+ catalog: UserAdminCatalog;
468
766
  labels?: UserAdminLabels;
469
767
  };
470
768
  declare function UsersView(): React__default.JSX.Element;
@@ -499,4 +797,4 @@ type AuthUserAdminAppViewProps = UserAdminShellProps;
499
797
  */
500
798
  declare function AuthUserAdminAppView({ renderHeader }?: AuthUserAdminAppViewProps): React__default.JSX.Element;
501
799
 
502
- export { AllowlistView, AuthUserAdminAppView, type AuthUserAdminAppViewProps, DirectoryView, GroupMembersView, GroupsView, type PartialUserAdminLabels, type UserAdminAccess, type UserAdminAppConfig, type UserAdminGuards, type UserAdminLabels, type UserAdminLoaderData, type UserAdminPermission, UserAdminShell, type UserAdminShellProps, type UserAdminStore, UsersView, createInMemoryUserAdminStore, createUserAdminApp, defaultUserAdminLabels, jaUserAdminLabels, resolveUserAdminLabels };
800
+ export { AllowlistView, AuthUserAdminAppView, type AuthUserAdminAppViewProps, DirectoryView, GroupMembersView, GroupsView, type PartialUserAdminLabels, type UserAdminAccess, type UserAdminAppConfig, type UserAdminCatalog, type UserAdminCatalogFailure, type UserAdminCatalogGroup, type UserAdminCatalogSnapshot, type UserAdminGuards, UserAdminInputError, type UserAdminLabels, type UserAdminLoaderData, UserAdminOperatorError, type UserAdminPermission, UserAdminShell, type UserAdminShellProps, type UserAdminStore, type UserAdminSyncRequestOutcome, UsersView, createInMemoryUserAdminStore, createUserAdminApp, defaultUserAdminLabels, jaUserAdminLabels, normalizeUpstreamKey, resolveUserAdminLabels, userAdminAccessFor, userAdminIntents };