@h-ai/iam 0.1.0-alpha.59 → 0.1.0-alpha.61
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.en.md +212 -55
- package/README.ja.md +212 -57
- package/README.md +225 -57
- package/dist/index.d.ts +104 -9
- package/dist/index.js +665 -339
- package/dist/index.js.map +1 -1
- package/package.json +20 -6
package/README.ja.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
4
4
|
|
|
5
|
-
[フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/audit](../audit/README.ja.md) · [@h-ai/cache](../cache/README.ja.md) · [@h-ai/core](../core/README.ja.md) · [@h-ai/crypto](../crypto/README.ja.md) · [@h-ai/reldb](../reldb/README.ja.md)
|
|
5
|
+
[フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/audit](../audit/README.ja.md) · [@h-ai/cache](../cache/README.ja.md) · [@h-ai/core](../core/README.ja.md) · [@h-ai/crypto](../crypto/README.ja.md) · [@h-ai/reldb](../reldb/README.ja.md) · [@h-ai/reach](../reach/README.ja.md)
|
|
6
6
|
|
|
7
7
|
共通の `iam` オブジェクトで認証、セッション、認可、ユーザー管理を提供する Identity and Access Management モジュールです。
|
|
8
8
|
|
|
@@ -31,66 +31,182 @@ pnpm add @h-ai/iam
|
|
|
31
31
|
- `@h-ai/crypto`:内部で使用するパスワードハッシュ。自動初期化。
|
|
32
32
|
- `@h-ai/audit`:主要な RBAC 操作の監査記録。
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
- `@h-ai/reach`:**任意**。アプリがインストール、初期化、注入して通知テンプレートを使用します。独自送信コールバックでは不要です。
|
|
35
|
+
- `ldapts`:**任意**。LDAP を明示的に有効化し、`ldapClientFactory` がない場合だけ読み込みます。
|
|
36
|
+
|
|
37
|
+
## 任意の LDAP と通知連携
|
|
38
|
+
|
|
39
|
+
パスワード認証、ユーザー管理、RBAC は LDAP/Reach なしで動作します。両方とも任意の peer dependency で、必要な連携だけをインストールします。以下は reldb/cache 初期化後の起動・認証関数内の例で、`username/password` は検証済み入力です。
|
|
40
|
+
|
|
41
|
+
### LDAP / Active Directory
|
|
42
|
+
|
|
43
|
+
既定クライアントは `pnpm add ldapts` で追加し、`login.ldap: true` と接続設定を明示します。接続設定だけでは LDAP は有効になりません。SDK なしで既定クライアントを有効にすると初期化が設定エラーを返します。
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
# config/_iam.yml(启动前设置 LDAP_BIND_PASSWORD)
|
|
47
|
+
login:
|
|
48
|
+
ldap: true
|
|
49
|
+
ldap:
|
|
50
|
+
url: ldaps://ldap.example.com:636
|
|
51
|
+
bindDn: cn=service,dc=example,dc=com
|
|
52
|
+
bindPassword: '${LDAP_BIND_PASSWORD}'
|
|
53
|
+
searchBase: ou=users,dc=example,dc=com
|
|
54
|
+
searchFilter: '(uid={{username}})'
|
|
55
|
+
usernameAttribute: uid
|
|
56
|
+
emailAttribute: mail
|
|
57
|
+
displayNameAttribute: cn
|
|
58
|
+
connectTimeout: 5000
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`ldaps://` は TLS、`ldap://` と `useTls: true` は StartTLS を使用します。Active Directory では `searchFilter: "(sAMAccountName={{username}})"` と `usernameAttribute: sAMAccountName` を指定できます。検索前に入力をエスケープします。初回ログインはローカルユーザーを同期して既定ロールを割り当て、無効化とロックも適用します。`ldapSyncUser: false` または `bind/search/unbind` を HaiResult で返す `ldapClientFactory` を注入できます。独自ファクトリでは ldapts は不要です。
|
|
35
62
|
|
|
36
63
|
```ts
|
|
37
|
-
import {
|
|
64
|
+
import type { IamConfigSettingsInput } from '@h-ai/iam'
|
|
65
|
+
import { core } from '@h-ai/core'
|
|
38
66
|
import { iam } from '@h-ai/iam'
|
|
39
|
-
import { reldb } from '@h-ai/reldb'
|
|
40
67
|
|
|
41
|
-
//
|
|
42
|
-
await
|
|
43
|
-
|
|
68
|
+
// core 已加载 config/,reldb 与 cache 已成功初始化。
|
|
69
|
+
const initialized = await iam.init(core.config.getOrThrow<IamConfigSettingsInput>('iam'))
|
|
70
|
+
if (!initialized.success)
|
|
71
|
+
return initialized
|
|
72
|
+
const result = await iam.auth.loginWithLdap({ username, password })
|
|
73
|
+
if (!result.success)
|
|
74
|
+
return result
|
|
75
|
+
return result
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 任意の Reach による自動送信
|
|
79
|
+
|
|
80
|
+
`pnpm add @h-ai/reach` で追加します。設定、初期化、終了はアプリが管理し、IAM は注入されたインスタンスだけを使用します。SMTP/HTTP は例で、SMS ゲートウェイは [Reach 通知仕様](../reach/README.ja.md) を受け取ります。Console はメタデータのみを記録し、実際の送信は行いません。
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
# config/_reach.yml;环境变量仅在服务端设置。
|
|
84
|
+
providers:
|
|
85
|
+
- name: email
|
|
86
|
+
type: smtp
|
|
87
|
+
host: '${SMTP_HOST}'
|
|
88
|
+
port: 465
|
|
89
|
+
secure: true
|
|
90
|
+
user: '${SMTP_USER}'
|
|
91
|
+
pass: '${SMTP_PASSWORD}'
|
|
92
|
+
from: '${MAIL_FROM}'
|
|
93
|
+
- name: sms
|
|
94
|
+
type: api
|
|
95
|
+
url: '${SMS_GATEWAY_URL}'
|
|
96
|
+
templates:
|
|
97
|
+
- name: otp_email
|
|
98
|
+
provider: email
|
|
99
|
+
subject: Verification code
|
|
100
|
+
body: 'Your code is {code}.'
|
|
101
|
+
- name: otp_sms
|
|
102
|
+
provider: sms
|
|
103
|
+
body: 'Your code is {code}.'
|
|
104
|
+
- name: password_reset
|
|
105
|
+
provider: email
|
|
106
|
+
subject: Password reset
|
|
107
|
+
body: 'Reset: {resetUrl}. Expires: {expiresAt}.'
|
|
108
|
+
- name: password_reset_sms
|
|
109
|
+
provider: sms
|
|
110
|
+
body: 'Reset: {resetUrl}. Expires: {expiresAt}.'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`otp_email / otp_sms` は `{code}`、`password_reset / password_reset_sms` は `{token}`、`{expiresAt}`、`{resetUrl}` を使用します。`passwordReset.mode: phone` で SMS リセットを選択します。テンプレートは `iam.init()` より前に設定し、利用できないチャネルは公開設定で非表示になります。
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import type { ReachConfigInput } from '@h-ai/reach'
|
|
117
|
+
import { core } from '@h-ai/core'
|
|
118
|
+
import { iam } from '@h-ai/iam'
|
|
119
|
+
import { reach } from '@h-ai/reach'
|
|
120
|
+
|
|
121
|
+
// core、reldb、cache 已初始化;按应用生命周期反序关闭 IAM 与 Reach。
|
|
122
|
+
const notification = await reach.init(core.config.getOrThrow<ReachConfigInput>('reach'))
|
|
123
|
+
if (!notification.success)
|
|
124
|
+
return notification
|
|
125
|
+
const initialized = await iam.init({
|
|
126
|
+
reach,
|
|
127
|
+
login: { otp: true },
|
|
128
|
+
otp: { email: true, sms: true },
|
|
129
|
+
passwordReset: { mode: 'email', resetUrl: 'https://app.example.com/auth/reset-password' },
|
|
130
|
+
})
|
|
131
|
+
if (!initialized.success)
|
|
132
|
+
return initialized
|
|
133
|
+
return iam.auth.sendOtp('user@example.com')
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 独自メール・SMS 送信
|
|
44
137
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
138
|
+
`onOtpSendEmail`、`onOtpSendSms`、`onPasswordResetRequest` を注入して既存の送信サービスを使用します。必要なものだけ実装でき、Reach と併用する場合は明示コールバックを優先します。例のサービスはアプリ実装で、送信成功時のみ Promise を解決します。HaiResult を返すサービスは `success` を検査し、失敗時はコールバック内で例外にします。IAM は例外を捕捉して業務エラーを返し、コードや Token、送信内部情報をユーザーに表示しません。
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { iam } from '@h-ai/iam'
|
|
142
|
+
import { mailService, smsService } from './notifications.js'
|
|
143
|
+
|
|
144
|
+
// mailService/smsService 为应用已有的发送服务;不需要安装或初始化 Reach。
|
|
145
|
+
// 调用方须检查服务投递结果,失败时拒绝 Promise,不能吞掉错误。
|
|
146
|
+
const initialized = await iam.init({
|
|
147
|
+
otp: { email: true, sms: true },
|
|
49
148
|
onOtpSendEmail: async (email, code) => {
|
|
50
|
-
await
|
|
149
|
+
await mailService.send({ to: email, subject: 'Verification code', text: code })
|
|
51
150
|
},
|
|
52
|
-
|
|
151
|
+
onOtpSendSms: async (phone, code) => {
|
|
152
|
+
await smsService.send({ to: phone, code })
|
|
153
|
+
},
|
|
154
|
+
passwordReset: { mode: 'email' },
|
|
53
155
|
onPasswordResetRequest: async (user, token, expiresAt) => {
|
|
54
|
-
|
|
156
|
+
if (!user.email)
|
|
157
|
+
throw new Error('Reset recipient unavailable')
|
|
158
|
+
const url = new URL('https://app.example.com/auth/reset-password')
|
|
159
|
+
url.searchParams.set('token', token)
|
|
160
|
+
await mailService.send({ to: user.email, subject: 'Password reset', text: url.toString(), expiresAt })
|
|
55
161
|
},
|
|
56
162
|
})
|
|
163
|
+
if (!initialized.success)
|
|
164
|
+
return initialized
|
|
165
|
+
```
|
|
57
166
|
|
|
58
|
-
|
|
59
|
-
const userResult = await iam.user.register({
|
|
60
|
-
username: 'admin',
|
|
61
|
-
email: 'admin@example.com',
|
|
62
|
-
password: 'Password123',
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
// 3. 登录
|
|
66
|
-
const loginResult = await iam.auth.login({
|
|
67
|
-
identifier: 'admin',
|
|
68
|
-
password: 'Password123',
|
|
69
|
-
})
|
|
70
|
-
if (loginResult.success) {
|
|
71
|
-
const { user, tokens } = loginResult.data
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// 4. 验证令牌
|
|
75
|
-
const session = await iam.auth.verifyToken(loginResult.data.tokens.accessToken)
|
|
167
|
+
## クイックスタート
|
|
76
168
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
})
|
|
169
|
+
```ts
|
|
170
|
+
import { cache } from '@h-ai/cache'
|
|
171
|
+
import { core } from '@h-ai/core'
|
|
172
|
+
import { iam } from '@h-ai/iam'
|
|
173
|
+
import { reldb } from '@h-ai/reldb'
|
|
83
174
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
175
|
+
// 基础密码认证不需要安装 Reach 或 ldapts。
|
|
176
|
+
async function example() {
|
|
177
|
+
try {
|
|
178
|
+
const db = await reldb.init({ type: 'sqlite', database: './data.db' })
|
|
179
|
+
if (!db.success)
|
|
180
|
+
return db
|
|
181
|
+
const cached = await cache.init({ type: 'memory' })
|
|
182
|
+
if (!cached.success)
|
|
183
|
+
return cached
|
|
184
|
+
const initialized = await iam.init({})
|
|
185
|
+
if (!initialized.success)
|
|
186
|
+
return initialized
|
|
187
|
+
const registered = await iam.user.register({ username: 'demo', password: 'Password123' })
|
|
188
|
+
if (!registered.success)
|
|
189
|
+
return registered
|
|
190
|
+
const login = await iam.auth.login({ identifier: 'demo', password: 'Password123' })
|
|
191
|
+
if (!login.success)
|
|
192
|
+
return login
|
|
193
|
+
return await iam.auth.verifyToken(login.data.tokens.accessToken)
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
// 应用关闭时按依赖的反向顺序释放。
|
|
197
|
+
await iam.close()
|
|
198
|
+
await cache.close()
|
|
199
|
+
await reldb.close()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
89
202
|
|
|
90
|
-
|
|
91
|
-
|
|
203
|
+
const result = await example()
|
|
204
|
+
if (!result.success)
|
|
205
|
+
core.logger.error('Authentication example failed', { code: result.error.code })
|
|
92
206
|
```
|
|
93
207
|
|
|
208
|
+
LDAP は既定で無効です。公開 OTP は `otp` 設定と送信コールバック、パスワードリセットは通知コールバックが必要です。
|
|
209
|
+
|
|
94
210
|
`iam.config` は**機密情報をマスクした設定スナップショット**を返します。LDAP の `url` / `bindPassword` などは自動的に非表示になります。状態確認用で、元の設定の代わりにはなりません。
|
|
95
211
|
|
|
96
212
|
## 設計
|
|
@@ -114,14 +230,14 @@ iam.init()
|
|
|
114
230
|
|
|
115
231
|
### データの保存先
|
|
116
232
|
|
|
117
|
-
| データ種別 | 保存先
|
|
118
|
-
| ------------------------------------------------- |
|
|
119
|
-
| ユーザー、ロール、権限、関連付け、API Key | DB
|
|
120
|
-
| セッション(accessToken → Session) | キャッシュ
|
|
121
|
-
| Token 対応(refreshToken → userId + accessToken) | キャッシュ
|
|
122
|
-
| OTP コード | キャッシュ
|
|
123
|
-
| パスワードリセット Token | キャッシュ
|
|
124
|
-
| API Key | DB
|
|
233
|
+
| データ種別 | 保存先 | 説明 |
|
|
234
|
+
| ------------------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
235
|
+
| ユーザー、ロール、権限、関連付け、API Key | DB(基本 5、任意の API Key 1 テーブル) | `hai_iam_users`、`hai_iam_roles`、`hai_iam_permissions`、`hai_iam_role_permissions`、`hai_iam_user_roles`、`hai_iam_api_keys` |
|
|
236
|
+
| セッション(accessToken → Session) | キャッシュ | TTL 付き、任意の自動延長 |
|
|
237
|
+
| Token 対応(refreshToken → userId + accessToken) | キャッシュ | 独立 key、更新後に旧 Token を即時無効化 |
|
|
238
|
+
| OTP コード | キャッシュ | TTL + 試行回数制限 |
|
|
239
|
+
| パスワードリセット Token | キャッシュ | TTL + 最大検証回数 |
|
|
240
|
+
| API Key | DB | `hai_iam_api_keys`。平文は保存せず hash のみ。接頭辞で候補検索 |
|
|
125
241
|
|
|
126
242
|
---
|
|
127
243
|
|
|
@@ -174,7 +290,7 @@ const result = await iam.auth.login({ identifier: 'admin', password: 'Password12
|
|
|
174
290
|
await iam.auth.sendOtp('user@example.com')
|
|
175
291
|
const result = await iam.auth.loginWithOtp({ identifier: 'user@example.com', code: '123456' })
|
|
176
292
|
|
|
177
|
-
// LDAP
|
|
293
|
+
// LDAP 登录(内置客户端,可选注入 ldapClientFactory)
|
|
178
294
|
const result = await iam.auth.loginWithLdap({ username: 'jdoe', password: 'pass' })
|
|
179
295
|
|
|
180
296
|
// API Key 登录(需启用 login.apikey: true)
|
|
@@ -250,7 +366,7 @@ await iam.session.deleteByUserId(userId)
|
|
|
250
366
|
|
|
251
367
|
#### 注意点
|
|
252
368
|
|
|
253
|
-
- **キャッシュのみの保存**:
|
|
369
|
+
- **キャッシュのみの保存**:Memory は再起動時に消失します。共有 Redis のセッションは IAM サービス再起動後も TTL または失効まで保持します。
|
|
254
370
|
- **自動延長**:`sliding: true` なら各 `get()` で TTL を延長。
|
|
255
371
|
- **Refresh Rotation**:旧 refreshToken は一度の使用で無効化。再使用は漏洩の兆候。
|
|
256
372
|
- **Token 強度**:256-bit の暗号学的乱数(`crypto.getRandomValues(new Uint8Array(32))`)、base64url 形式。
|
|
@@ -259,6 +375,8 @@ await iam.session.deleteByUserId(userId)
|
|
|
259
375
|
|
|
260
376
|
### 認可(authz)— `iam.authz`
|
|
261
377
|
|
|
378
|
+
ロール一覧は `iam.authz.getRoleUserCounts(roleIds)` の 1 回の集計で現在のページの実際の所属人数を取得し、`HaiResult<Map<string, number>>` を返します。関連がなければ 0、検索失敗は読み込みエラーを表示し、0 件の統計を作りません。
|
|
379
|
+
|
|
262
380
|
#### 主要な型
|
|
263
381
|
|
|
264
382
|
- **`AuthzOperations`**:ロール・権限 CRUD、ロールと権限・ユーザーとロールの割り当て、権限確認。
|
|
@@ -528,6 +646,45 @@ if (consumed.success) {
|
|
|
528
646
|
|
|
529
647
|
特徴:暗号学的に安全な乱数、TTL、アトミックな単回消費(並行消費では 1 件のみ成功)、用途の固定による別用途の再使用防止。
|
|
530
648
|
|
|
649
|
+
## 設定による認証画面
|
|
650
|
+
|
|
651
|
+
`iam.publicConfig` とクライアントの HTTP `client.iam.auth.publicConfig()` は秘密情報を含まない設定だけを公開し、実際に利用可能な認証方式を表示します。`login.password/otp/ldap`、`otp.email/sms`、`register.enabled/defaultEnabled`、`passwordReset.enabled/mode/resetUrl` と規約表示を個別に設定できます。`ui` はカード/分割レイアウト、タイトル、説明、ブランド、Logo、画像を指定します。LDAP は任意で既定は無効です。既定クライアントを選択する時だけ ldapts を追加するか、独自ファクトリを注入します。
|
|
652
|
+
|
|
653
|
+
任意の Reach を選択した場合、reldb/cache/Reach の初期化後、`iam.init({ ...settings, reach })` を呼び出します。`otp_email`、`otp_sms`、`password_reset`、`password_reset_sms` テンプレートを対応する送信 Provider に接続します。明示的なコールバックは既定動作を上書きします。Console はメタデータだけを記録するため、実際の配送には SMTP、SMS または HTTP サービスが必要です。送信失敗はエラーになります。
|
|
654
|
+
|
|
655
|
+
SvelteKit は `kit.auth.createApi(iam)` と匿名用の `kit.auth.publicPaths` を使用し、Cookie 認証のフォームには `kit.client.createAuthActions()` を使用します。HTTP クライアントは `apiClient.createAuthActions(client)` により安全な設定済みストレージに Token を保存します。`AuthPanel` に設定と actions を渡すと、ログイン、登録、パスワード復旧を利用できます。`LoginForm` は複数方式と再送間隔を、パスワードフォームは完全な `passwordPolicy` を受け取ります。管理者は権限を検証した上で `iam.user.createUser()` を使用できます。
|
|
656
|
+
|
|
657
|
+
規約は入力を保持したまま Modal で開きます。URL のページは iframe 埋め込みを許可する必要があり、スクリプトを無効にし別ウィンドウ表示も用意します。LoginForm/RegisterForm は `header/extraFields/footer`、AuthPanel は `loginHeader/loginFields/loginFooter` と `registerHeader/registerFields/registerFooter` を提供します。追加フィールドはアプリの状態と actions で処理します。[UI の使い方](../ui/README.ja.md) を参照してください。
|
|
658
|
+
|
|
659
|
+
複数ノードは同じ DB と Redis を共有する必要があります。Memory は単一プロセス専用です。ログイン失敗は DB の原子的更新で累積し、OTP の試行回数、再送間隔、単回使用は共有状態で管理します。コードの使用や試行上限によって再送制限を回避できません。リセットは共有 TTL とユーザー別ロックを使用し、ポリシー違反も試行に含め、最新 Token の成功使用は一回だけです。リセット後は旧セッションとログインロックを解除します。無効なユーザーは認証できません。
|
|
660
|
+
|
|
661
|
+
標準 HTTP 入口は `GET /auth/config`、`POST /auth/login/ldap`、`POST /auth/password-reset/request`、`POST /auth/password-reset/confirm` です。セキュリティエラーは `SECURITY_BUSY (hai:iam:023)` と `PASSWORD_RESET_DISABLED (hai:iam:024)` です。`ldap` などの任意項目は YAML に宣言してから環境変数で上書きするか、`HAI_IAM` の JSON 値でモジュール設定全体を置き換えます。既存項目には `HAI_IAM_LOGIN_OTP` などを使用できます。
|
|
662
|
+
|
|
663
|
+
`passwordReset.mode` は `email`(既定、`password_reset`)または `phone`(`password_reset_sms`)を選び、公開設定に応じて復旧フォームが入力欄を切り替えます。
|
|
664
|
+
|
|
665
|
+
### Markdown の規約
|
|
666
|
+
|
|
667
|
+
`agreements.userAgreementMarkdown` と `privacyPolicyMarkdown` で設定から直接本文を読み込みます。YAML の `|` は段落と改行を保持します。Modal は安全な MarkdownRenderer を再利用し、生 HTML、スクリプト、コードツールを無効にします。同じ規約に本文と URL を指定した場合は本文を優先し、URL は任意の外部表示リンクとして使用します。URL のみの場合は隔離 iframe で表示します。
|
|
668
|
+
|
|
669
|
+
```yaml
|
|
670
|
+
agreements:
|
|
671
|
+
showOnRegister: true
|
|
672
|
+
showOnLogin: false
|
|
673
|
+
userAgreementMarkdown: |
|
|
674
|
+
# 用户协议
|
|
675
|
+
|
|
676
|
+
**账户使用条款**
|
|
677
|
+
|
|
678
|
+
- 请妥善保管账户凭据。
|
|
679
|
+
privacyPolicyMarkdown: |
|
|
680
|
+
# 隐私政策
|
|
681
|
+
|
|
682
|
+
注册资料用于账户管理,会话信息用于身份认证。
|
|
683
|
+
# 可选 URL;同主题同时配置 Markdown 和 URL 时,弹窗优先显示 Markdown。
|
|
684
|
+
# userAgreementUrl: https://example.com/terms
|
|
685
|
+
# privacyPolicyUrl: https://example.com/privacy
|
|
686
|
+
```
|
|
687
|
+
|
|
531
688
|
## エラー処理
|
|
532
689
|
|
|
533
690
|
すべての操作は `HaiResult<T>` を返し、`HaiIamError` で分岐します:
|
|
@@ -552,5 +709,3 @@ pnpm test
|
|
|
552
709
|
## ライセンス
|
|
553
710
|
|
|
554
711
|
Apache-2.0
|
|
555
|
-
|
|
556
|
-
ロール一覧は `iam.authz.getRoleUserCounts(roleIds)` の 1 回の集計で現在のページの実際の所属人数を取得し、`HaiResult<Map<string, number>>` を返します。関連がなければ 0、検索失敗は読み込みエラーを表示し、0 件の統計を作りません。
|