@h-ai/iam 0.1.0-alpha.69 → 0.1.0-alpha.71
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 +4 -0
- package/README.ja.md +4 -0
- package/README.md +4 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.js +39 -22
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/README.en.md
CHANGED
|
@@ -882,6 +882,10 @@ Sessions and refresh consumption share one versioned `hai:iam:sessions:{SHA256(u
|
|
|
882
882
|
|
|
883
883
|
`iam.session.update(token, patch, expectedVersion)` requires the Session.version from the read. Stale versions return CONFLICT; revoked tokens return SESSION_NOT_FOUND. Timestamp-only sliding updates keep the content version, allowing normal parallel requests. Batch permission updates bind the initial token/version set and never apply delayed snapshots to later sign-ins; newer content versions require re-reading authorization before retry.
|
|
884
884
|
|
|
885
|
+
## Schema and credential notifications
|
|
886
|
+
|
|
887
|
+
After RelDB initialization, `migrateIam({ dryRun: true })` previews changes without writes. iam.init upgrades supported credential/reset columns, retaining users/passwords and permissions. Back up and stop old writers first. Legacy sessions without credential versions must sign in again; never roll back credential generations to restore authorization. Default Reach notifications use sensitive/expiresAt/idempotencyKey without plaintext persistence or delayed replay. Custom callbacks must enforce the same expiry and credential protection.
|
|
888
|
+
|
|
885
889
|
## Development and testing
|
|
886
890
|
|
|
887
891
|
Public examples import from `@h-ai/iam` and distinguish defaultable `IamConfigInput` from parsed `IamConfig`. Initialize server dependencies, check `HaiResult.success`, update sessions with their observed `version`, and reuse persistent notification idempotency keys. Strategy/repository examples marked as internal context do not define application deep imports. The checker verifies types only; Redis/database, LDAP and delivery behavior still require their integration tests. See the [JSDoc conventions (Chinese)](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证).
|
package/README.ja.md
CHANGED
|
@@ -878,6 +878,10 @@ if (!result.success) {
|
|
|
878
878
|
|
|
879
879
|
`iam.session.update(token, patch, expectedVersion)` は読み取った Session.version が必須です。旧版は CONFLICT、失効 Token は SESSION_NOT_FOUND を返し、新しい状態を上書きしません。自動延長の時刻だけの更新は内容版を変更せず、通常の並行リクエストを許容します。権限一括更新は最初の Token/版集合に限定し、後続ログインに古い値を適用しません。
|
|
880
880
|
|
|
881
|
+
## スキーマと認証情報の通知
|
|
882
|
+
|
|
883
|
+
RelDB 初期化後の `migrateIam({ dryRun: true })` は読み取り専用です。iam.init は対応する認証・リセット列を補い、ユーザー、パスワードと権限を保持します。事前に旧書込を停止しバックアップしてください。安全世代のない旧セッションは再ログインが必要で、認可復元のため世代を戻してはいけません。既定 Reach 通知は sensitive/expiresAt/idempotencyKey で平文保存や遅延再送を防ぎます。独自コールバックも同じ期限と保護を守ってください。
|
|
884
|
+
|
|
881
885
|
## 開発とテスト
|
|
882
886
|
|
|
883
887
|
公開例は `@h-ai/iam` を使用し、既定値を省略できる `IamConfigInput` と解析済み `IamConfig` を区別します。依存を先に初期化し、`HaiResult.success` を確認し、会話更新には読取時の `version`、通知には永続的な冪等キーを使います。「模块内部上下文」の戦略・倉庫例はアプリ向け深い import の契約ではありません。チェッカーは型だけを確認し、Redis/DB・LDAP・配送の動作には対応する統合テストが必要です。[JSDoc 規約](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)も参照してください。
|
package/README.md
CHANGED
|
@@ -891,6 +891,10 @@ if (!result.success) {
|
|
|
891
891
|
|
|
892
892
|
`iam.session.update(token, patch, expectedVersion)` 必须传入读取到的 Session.version。旧版本返回 CONFLICT,已撤销 token 返回 SESSION_NOT_FOUND;不会将旧内容重新写回或覆盖新会话。仅滑动时间戳更新不改变内容版本,允许正常并行请求。批量权限更新绑定首次读取的 token/版本集合,不将迟到数据应用到后续登录;遇到较新内容版本失败,调用方重新查询授权后再提交。
|
|
893
893
|
|
|
894
|
+
## 持久结构与凭据通知
|
|
895
|
+
|
|
896
|
+
`migrateIam({ dryRun: true })` 在 RelDB 初始化后只读预检;iam.init 自动补齐支持版本的凭据/重置字段,保留用户与密码,不修改业务权限。升级前备份停旧写入;没有安全代次的旧会话需重新登录,不能回退数据库安全代次来恢复授权。默认 Reach 通知使用 sensitive/expiresAt/idempotencyKey,不落明文、不延迟重投;自定义回调同样须遵守有效期与凭据保护。
|
|
897
|
+
|
|
894
898
|
## 开发测试
|
|
895
899
|
|
|
896
900
|
公开示例从 `@h-ai/iam` 导入,区分可省略默认值的 `IamConfigInput` 与 Schema 解析后的 `IamConfig`。服务端依赖先初始化;`HaiResult` 先检查 `success`,会话修改使用读取到的 `version`,通知复用持久幂等键。内部策略/仓储示例标记为“模块内部上下文”,不作为应用的深层导入入口。检查器只验证示例类型;真实 Redis/数据库、LDAP 和通知投递仍需对应集成测试。见[模块 JSDoc 约定](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)。
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { IamFunctions } from './browser.js';
|
|
2
2
|
export { AgreementConfig, AgreementConfigSchema, AgreementDisplay, ApiKey, ApiKeyConfig, ApiKeyConfigSchema, ApiKeyCredentials, ApiKeyOperations, ApiKeySortDirection, ApiKeySortField, AuthResult, AuthStrategy, AuthStrategyType, AuthStrategyTypeSchema, AuthUiConfig, AuthUiConfigSchema, AuthnOperations, AuthzOperations, ConsumeTicketOptions, ConsumedTicket, CreateApiKeyOptions, CreateApiKeyResult, CreateSessionOptions, Credentials, HaiIamError, IamConfig, IamConfigInput, IamConfigSchema, IamConfigSettingsInput, IamNotificationContext, IamPublicConfig, IssueTicketOptions, IssuedTicket, LdapClientFactory, LdapConfig, LdapConfigSchema, LdapCredentials, ListApiKeysOptions, ListUsersOptions, LoginConfig, LoginConfigSchema, OtpConfig, OtpConfigSchema, OtpCredentials, PasswordConfig, PasswordConfigSchema, PasswordCredentials, PasswordResetConfig, PasswordResetConfigSchema, Permission, PermissionQueryOptions, PermissionType, RbacConfig, RbacConfigSchema, RegisterConfig, RegisterConfigSchema, RegisterOptions, RegisterResult, Role, SecurityConfig, SecurityConfigSchema, Session, SessionConfig, SessionConfigSchema, SessionData, SessionFieldUpdates, SessionOperations, StoredUser, TicketGrant, TicketOperations, TokenPair, UpdateCurrentUserInput, User, UserOperations, UserSortDirection, UserSortField } from './browser.js';
|
|
3
|
+
import * as _h_ai_core from '@h-ai/core';
|
|
4
|
+
import * as _h_ai_reldb from '@h-ai/reldb';
|
|
3
5
|
import 'zod';
|
|
4
|
-
import '@h-ai/core';
|
|
5
6
|
import '@h-ai/reach';
|
|
6
|
-
import '@h-ai/reldb';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @h-ai/iam — IAM 服务主入口
|
|
@@ -29,4 +29,16 @@ import '@h-ai/reldb';
|
|
|
29
29
|
*/
|
|
30
30
|
declare const iam: IamFunctions;
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* 预检或迁移 IAM 表。升级保留用户和密码;旧版无安全代次的会话需重新登录。
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { migrateIam } from '@h-ai/iam'
|
|
37
|
+
* const plan = await migrateIam({ dryRun: true }) // RelDB 已初始化;只读。
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
declare function migrateIam(options?: {
|
|
41
|
+
dryRun?: boolean;
|
|
42
|
+
}): Promise<_h_ai_core.HaiResult<_h_ai_reldb.SchemaMigrationPlan>>;
|
|
43
|
+
|
|
44
|
+
export { IamFunctions, iam, migrateIam };
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { AgreementConfigSchema, ApiKeyConfigSchema, AuthStrategyTypeSchema, Auth
|
|
|
3
3
|
import { cache } from '@h-ai/cache';
|
|
4
4
|
import { core, err, ok } from '@h-ai/core';
|
|
5
5
|
import { crypto as crypto$1 } from '@h-ai/crypto';
|
|
6
|
-
import { reldb, BaseReldbCrudRepository } from '@h-ai/reldb';
|
|
6
|
+
import { migrateSchema, reldb, schemaColumns, BaseReldbCrudRepository, HaiReldbError } from '@h-ai/reldb';
|
|
7
7
|
import { createHash, randomBytes } from 'crypto';
|
|
8
8
|
import { audit } from '@h-ai/audit';
|
|
9
9
|
|
|
@@ -3168,8 +3168,7 @@ function createRbacManager(config) {
|
|
|
3168
3168
|
if (!createResult.success) {
|
|
3169
3169
|
if (ownTx)
|
|
3170
3170
|
await tx.rollback();
|
|
3171
|
-
|
|
3172
|
-
if (msg.includes("unique") || msg.includes("duplicate")) {
|
|
3171
|
+
if (createResult.error.code === HaiReldbError.DUPLICATE_ENTRY.code) {
|
|
3173
3172
|
return err(HaiIamError.ROLE_ALREADY_EXISTS, iamM("iam_roleAlreadyExist"));
|
|
3174
3173
|
}
|
|
3175
3174
|
return mapRepositoryError2("iam_createRoleFailed", createResult.error.message);
|
|
@@ -3208,10 +3207,6 @@ function createRbacManager(config) {
|
|
|
3208
3207
|
} catch (error) {
|
|
3209
3208
|
if (ownTx)
|
|
3210
3209
|
await tx.rollback();
|
|
3211
|
-
const msg = String(error).toLowerCase();
|
|
3212
|
-
if (msg.includes("unique") || msg.includes("duplicate")) {
|
|
3213
|
-
return err(HaiIamError.ROLE_ALREADY_EXISTS, iamM("iam_roleAlreadyExist"));
|
|
3214
|
-
}
|
|
3215
3210
|
return err(
|
|
3216
3211
|
HaiIamError.REPOSITORY_ERROR,
|
|
3217
3212
|
iamM("iam_createRoleFailed", { params: { message: String(error) } }),
|
|
@@ -3396,8 +3391,7 @@ function createRbacManager(config) {
|
|
|
3396
3391
|
if (!createResult.success) {
|
|
3397
3392
|
if (ownTx)
|
|
3398
3393
|
await tx.rollback();
|
|
3399
|
-
|
|
3400
|
-
if (msg.includes("unique") || msg.includes("duplicate")) {
|
|
3394
|
+
if (createResult.error.code === HaiReldbError.DUPLICATE_ENTRY.code) {
|
|
3401
3395
|
return err(HaiIamError.PERMISSION_ALREADY_EXISTS, iamM("iam_permissionAlreadyExist"));
|
|
3402
3396
|
}
|
|
3403
3397
|
return mapRepositoryError2("iam_createPermissionFailed", createResult.error.message);
|
|
@@ -3425,10 +3419,6 @@ function createRbacManager(config) {
|
|
|
3425
3419
|
} catch (error) {
|
|
3426
3420
|
if (ownTx)
|
|
3427
3421
|
await tx.rollback();
|
|
3428
|
-
const msg = String(error).toLowerCase();
|
|
3429
|
-
if (msg.includes("unique") || msg.includes("duplicate")) {
|
|
3430
|
-
return err(HaiIamError.PERMISSION_ALREADY_EXISTS, iamM("iam_permissionAlreadyExist"));
|
|
3431
|
-
}
|
|
3432
3422
|
return err(
|
|
3433
3423
|
HaiIamError.REPOSITORY_ERROR,
|
|
3434
3424
|
iamM("iam_createPermissionFailed", { params: { message: String(error) } }),
|
|
@@ -3600,6 +3590,31 @@ function createRbacManager(config) {
|
|
|
3600
3590
|
}
|
|
3601
3591
|
};
|
|
3602
3592
|
}
|
|
3593
|
+
var columns = { credential_version: "VARCHAR(255) NOT NULL DEFAULT ''", reset_version: "VARCHAR(255) NOT NULL DEFAULT ''", reset_hash: "TEXT", reset_expires_at: "BIGINT", reset_attempts: "INTEGER NOT NULL DEFAULT 0" };
|
|
3594
|
+
var steps = [{
|
|
3595
|
+
version: 1,
|
|
3596
|
+
name: "credential-and-reset-versions",
|
|
3597
|
+
async needed(sql) {
|
|
3598
|
+
const found = await schemaColumns(reldb, "hai_iam_users", sql);
|
|
3599
|
+
return found.success ? ok(found.data.length > 0 && Object.keys(columns).some((name) => !found.data.includes(name))) : found;
|
|
3600
|
+
},
|
|
3601
|
+
async apply(sql) {
|
|
3602
|
+
const found = await schemaColumns(reldb, "hai_iam_users", sql);
|
|
3603
|
+
if (!found.success)
|
|
3604
|
+
return found;
|
|
3605
|
+
for (const [name, definition] of Object.entries(columns)) {
|
|
3606
|
+
if (found.data.includes(name))
|
|
3607
|
+
continue;
|
|
3608
|
+
const added = await sql.execute(`ALTER TABLE hai_iam_users ADD COLUMN ${name} ${definition}`);
|
|
3609
|
+
if (!added.success)
|
|
3610
|
+
return added;
|
|
3611
|
+
}
|
|
3612
|
+
return ok(void 0);
|
|
3613
|
+
}
|
|
3614
|
+
}];
|
|
3615
|
+
function migrateIam(options = {}) {
|
|
3616
|
+
return migrateSchema(reldb, "iam", steps, options);
|
|
3617
|
+
}
|
|
3603
3618
|
|
|
3604
3619
|
// src/iam-notifications.ts
|
|
3605
3620
|
async function resolveIamNotifications(input) {
|
|
@@ -3620,7 +3635,7 @@ async function resolveIamNotifications(input) {
|
|
|
3620
3635
|
const provider = configured.get(template);
|
|
3621
3636
|
if (!reach?.isInitialized || !provider || !to)
|
|
3622
3637
|
throw new Error(iamM("iam_passwordResetDeliveryFailed"));
|
|
3623
|
-
const result = await reach.send({ provider, to, template, vars, idempotencyKey: context.idempotencyKey });
|
|
3638
|
+
const result = await reach.send({ provider, to, template, vars, idempotencyKey: context.idempotencyKey, sensitive: true, expiresAt: context.expiresAt.getTime() });
|
|
3624
3639
|
if (!result.success || !result.data.success)
|
|
3625
3640
|
throw new Error(iamM("iam_passwordResetDeliveryFailed"));
|
|
3626
3641
|
}
|
|
@@ -4359,15 +4374,14 @@ function mapRepositoryError(messageKey, message) {
|
|
|
4359
4374
|
iamM(messageKey, { params: { message } })
|
|
4360
4375
|
);
|
|
4361
4376
|
}
|
|
4362
|
-
function mapUpdateErrorAsDomainError(
|
|
4363
|
-
|
|
4364
|
-
if (loweredMessage.includes("unique") || loweredMessage.includes("duplicate")) {
|
|
4377
|
+
function mapUpdateErrorAsDomainError(error) {
|
|
4378
|
+
if (error.code === HaiReldbError.DUPLICATE_ENTRY.code) {
|
|
4365
4379
|
return err(
|
|
4366
4380
|
HaiIamError.USER_ALREADY_EXISTS,
|
|
4367
4381
|
iamM("iam_userAlreadyExist")
|
|
4368
4382
|
);
|
|
4369
4383
|
}
|
|
4370
|
-
return mapRepositoryError("iam_updateUserFailed", message);
|
|
4384
|
+
return mapRepositoryError("iam_updateUserFailed", error.message);
|
|
4371
4385
|
}
|
|
4372
4386
|
async function validateUniqueFieldsForUpdate(userRepository, userId, data) {
|
|
4373
4387
|
const currentResult = await userRepository.findById(userId);
|
|
@@ -4471,7 +4485,7 @@ function buildRegistrationOps(ctx) {
|
|
|
4471
4485
|
}, tx);
|
|
4472
4486
|
if (!createResult.success) {
|
|
4473
4487
|
await tx.rollback();
|
|
4474
|
-
return mapUpdateErrorAsDomainError(createResult.error
|
|
4488
|
+
return mapUpdateErrorAsDomainError(createResult.error);
|
|
4475
4489
|
}
|
|
4476
4490
|
const createdUserResult = await userRepository.findByUsername(options.username, tx);
|
|
4477
4491
|
if (!createdUserResult.success || !createdUserResult.data) {
|
|
@@ -4658,7 +4672,7 @@ function buildUserMutationOps(ctx) {
|
|
|
4658
4672
|
}
|
|
4659
4673
|
const updateResult = await userRepository.updateById(userId, safeData);
|
|
4660
4674
|
if (!updateResult.success) {
|
|
4661
|
-
return mapUpdateErrorAsDomainError(updateResult.error
|
|
4675
|
+
return mapUpdateErrorAsDomainError(updateResult.error);
|
|
4662
4676
|
}
|
|
4663
4677
|
if (updateResult.data.changes === 0) {
|
|
4664
4678
|
return err(
|
|
@@ -4698,7 +4712,7 @@ function buildUserMutationOps(ctx) {
|
|
|
4698
4712
|
}
|
|
4699
4713
|
const updateResult = await userRepository.updateById(userId, data);
|
|
4700
4714
|
if (!updateResult.success) {
|
|
4701
|
-
return mapUpdateErrorAsDomainError(updateResult.error
|
|
4715
|
+
return mapUpdateErrorAsDomainError(updateResult.error);
|
|
4702
4716
|
}
|
|
4703
4717
|
if (updateResult.data.changes === 0) {
|
|
4704
4718
|
return err(
|
|
@@ -5042,6 +5056,9 @@ var iam = {
|
|
|
5042
5056
|
const parsed = parseResult.data;
|
|
5043
5057
|
const { onPasswordResetRequest, onOtpSendEmail, onOtpSendSms } = await resolveIamNotifications(config);
|
|
5044
5058
|
logger11.info("IAM notification channels resolved", { email: Boolean(onOtpSendEmail), sms: Boolean(onOtpSendSms), passwordReset: Boolean(onPasswordResetRequest) });
|
|
5059
|
+
const migration = await migrateIam();
|
|
5060
|
+
if (!migration.success)
|
|
5061
|
+
return migration;
|
|
5045
5062
|
const credentialUsers = await createDbUserRepository();
|
|
5046
5063
|
const sessionResult = await createSessionOperations({
|
|
5047
5064
|
config: parsed,
|
|
@@ -5171,6 +5188,6 @@ var iam = {
|
|
|
5171
5188
|
}
|
|
5172
5189
|
};
|
|
5173
5190
|
|
|
5174
|
-
export { iam };
|
|
5191
|
+
export { iam, migrateIam };
|
|
5175
5192
|
//# sourceMappingURL=index.js.map
|
|
5176
5193
|
//# sourceMappingURL=index.js.map
|