@h-ai/iam 0.1.0-alpha.61 → 0.1.0-alpha.62
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 +268 -113
- package/README.ja.md +268 -113
- package/README.md +268 -113
- package/dist/index.d.ts +23 -6
- package/dist/index.js +139 -44
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -193,6 +193,7 @@ var IamConfigSchema = z.object({
|
|
|
193
193
|
|
|
194
194
|
// messages/en-US.json
|
|
195
195
|
var en_US_default = {
|
|
196
|
+
iam_batchUserIdsInvalid: "Provide at most 1000 non-empty user IDs",
|
|
196
197
|
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
197
198
|
iam_securityStateFailed: "Unable to update authentication security state. Please try again later.",
|
|
198
199
|
iam_securityBusy: "Too many authentication requests. Please try again later.",
|
|
@@ -380,6 +381,7 @@ var en_US_default = {
|
|
|
380
381
|
|
|
381
382
|
// messages/zh-CN.json
|
|
382
383
|
var zh_CN_default = {
|
|
384
|
+
iam_batchUserIdsInvalid: "\u8BF7\u63D0\u4F9B\u6700\u591A 1000 \u4E2A\u975E\u7A7A\u7528\u6237 ID",
|
|
383
385
|
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
384
386
|
iam_securityStateFailed: "\u65E0\u6CD5\u66F4\u65B0\u8BA4\u8BC1\u5B89\u5168\u72B6\u6001\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
385
387
|
iam_securityBusy: "\u8BA4\u8BC1\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
@@ -1103,6 +1105,23 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
|
|
|
1103
1105
|
}
|
|
1104
1106
|
};
|
|
1105
1107
|
|
|
1108
|
+
// src/authn/apikey/iam-authn-apikey-scope.ts
|
|
1109
|
+
function covers(grant, permission) {
|
|
1110
|
+
return grant === "*" || grant === permission || grant.endsWith(":*") && permission.startsWith(grant.slice(0, -1));
|
|
1111
|
+
}
|
|
1112
|
+
function applyApiKeyScopes(access, scopes) {
|
|
1113
|
+
const permissions = /* @__PURE__ */ new Set();
|
|
1114
|
+
for (const permission of access.permissions) {
|
|
1115
|
+
for (const scope of scopes) {
|
|
1116
|
+
if (covers(permission, scope))
|
|
1117
|
+
permissions.add(scope);
|
|
1118
|
+
else if (covers(scope, permission))
|
|
1119
|
+
permissions.add(permission);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return { ...access, roles: scopes.includes("*") ? access.roles : [], permissions: [...permissions] };
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1106
1125
|
// src/user/iam-user-utils.ts
|
|
1107
1126
|
function toUser(storedUser) {
|
|
1108
1127
|
return {
|
|
@@ -1219,7 +1238,7 @@ function createApiKeyStrategy(config) {
|
|
|
1219
1238
|
return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
|
|
1220
1239
|
}
|
|
1221
1240
|
logger.info("API Key authentication succeeded", { userId: userResult.data.id, keyId: apiKey.id });
|
|
1222
|
-
return ok(toUser(userResult.data));
|
|
1241
|
+
return ok({ user: toUser(userResult.data), apiKey });
|
|
1223
1242
|
}
|
|
1224
1243
|
};
|
|
1225
1244
|
const apiKeyFunctions = {
|
|
@@ -1663,6 +1682,8 @@ function generateToken() {
|
|
|
1663
1682
|
}
|
|
1664
1683
|
function buildSession(options, now, sessionTtl, accessToken) {
|
|
1665
1684
|
return {
|
|
1685
|
+
apiKeyId: options.apiKeyId,
|
|
1686
|
+
apiKeyScopes: options.apiKeyScopes,
|
|
1666
1687
|
userId: options.userId,
|
|
1667
1688
|
username: options.username,
|
|
1668
1689
|
displayName: options.displayName,
|
|
@@ -2160,6 +2181,7 @@ async function createAuthnOperations(deps) {
|
|
|
2160
2181
|
apiKeyStrategy,
|
|
2161
2182
|
sessionFunctions,
|
|
2162
2183
|
authzFunctions,
|
|
2184
|
+
userRepository,
|
|
2163
2185
|
config
|
|
2164
2186
|
});
|
|
2165
2187
|
logger5.info("Authn sub-feature initialized");
|
|
@@ -2182,6 +2204,7 @@ function buildAuthnOperations(deps) {
|
|
|
2182
2204
|
apiKeyStrategy,
|
|
2183
2205
|
sessionFunctions,
|
|
2184
2206
|
authzFunctions,
|
|
2207
|
+
userRepository,
|
|
2185
2208
|
config
|
|
2186
2209
|
} = deps;
|
|
2187
2210
|
const loginConfig = LoginConfigSchema.parse(config.login ?? {});
|
|
@@ -2195,20 +2218,6 @@ function buildAuthnOperations(deps) {
|
|
|
2195
2218
|
}
|
|
2196
2219
|
return null;
|
|
2197
2220
|
}
|
|
2198
|
-
async function resolveUserRoleCodes(userId) {
|
|
2199
|
-
const rolesResult = await authzFunctions.getUserRoles(userId);
|
|
2200
|
-
if (!rolesResult.success) {
|
|
2201
|
-
return rolesResult;
|
|
2202
|
-
}
|
|
2203
|
-
return ok(rolesResult.data.map((role) => role.code));
|
|
2204
|
-
}
|
|
2205
|
-
async function resolveUserPermissionCodes(userId) {
|
|
2206
|
-
const permissionsResult = await authzFunctions.getUserPermissions(userId);
|
|
2207
|
-
if (!permissionsResult.success) {
|
|
2208
|
-
return permissionsResult;
|
|
2209
|
-
}
|
|
2210
|
-
return ok(permissionsResult.data.map((p) => p.code));
|
|
2211
|
-
}
|
|
2212
2221
|
function resolveStrategy(type, strategy) {
|
|
2213
2222
|
if (strategy) {
|
|
2214
2223
|
return ok(strategy);
|
|
@@ -2230,26 +2239,22 @@ function buildAuthnOperations(deps) {
|
|
|
2230
2239
|
iamM("iam_featureNotImplemented")
|
|
2231
2240
|
);
|
|
2232
2241
|
}
|
|
2233
|
-
async function buildAuthResult(user) {
|
|
2242
|
+
async function buildAuthResult(user, apiKey) {
|
|
2234
2243
|
if (!user.enabled)
|
|
2235
2244
|
return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
|
|
2236
|
-
const
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
return roleCodesResult;
|
|
2242
|
-
}
|
|
2243
|
-
if (!permCodesResult.success) {
|
|
2244
|
-
return permCodesResult;
|
|
2245
|
-
}
|
|
2245
|
+
const authorization = await authzFunctions.getUserAuthorization(user.id);
|
|
2246
|
+
if (!authorization.success)
|
|
2247
|
+
return authorization;
|
|
2248
|
+
const access = authorization.data;
|
|
2249
|
+
const effective = apiKey ? applyApiKeyScopes(access, apiKey.scopes) : access;
|
|
2246
2250
|
const sessionResult = await sessionFunctions.create({
|
|
2247
2251
|
userId: user.id,
|
|
2248
2252
|
username: user.username,
|
|
2249
2253
|
displayName: user.displayName,
|
|
2250
2254
|
avatarUrl: user.avatarUrl,
|
|
2251
|
-
|
|
2252
|
-
|
|
2255
|
+
...effective,
|
|
2256
|
+
apiKeyId: apiKey?.id,
|
|
2257
|
+
apiKeyScopes: apiKey?.scopes
|
|
2253
2258
|
});
|
|
2254
2259
|
if (!sessionResult.success) {
|
|
2255
2260
|
return sessionResult;
|
|
@@ -2262,8 +2267,7 @@ function buildAuthnOperations(deps) {
|
|
|
2262
2267
|
return ok({
|
|
2263
2268
|
user,
|
|
2264
2269
|
tokens: tokenPair,
|
|
2265
|
-
|
|
2266
|
-
permissions: permCodesResult.data,
|
|
2270
|
+
...effective,
|
|
2267
2271
|
agreements: buildAgreementDisplay(agreementConfig, agreementConfig.showOnLogin)
|
|
2268
2272
|
});
|
|
2269
2273
|
}
|
|
@@ -2299,7 +2303,15 @@ function buildAuthnOperations(deps) {
|
|
|
2299
2303
|
return loginWithStrategy("ldap", credentials, ldapStrategy);
|
|
2300
2304
|
},
|
|
2301
2305
|
async loginWithApiKey(credentials) {
|
|
2302
|
-
|
|
2306
|
+
const disabled = loginDisabled("apikey");
|
|
2307
|
+
if (disabled)
|
|
2308
|
+
return disabled;
|
|
2309
|
+
if (!apiKeyStrategy)
|
|
2310
|
+
return err(HaiIamError.STRATEGY_NOT_SUPPORTED, iamM("iam_featureNotImplemented"));
|
|
2311
|
+
const authenticated = await apiKeyStrategy.authenticate({ type: "apikey", ...credentials });
|
|
2312
|
+
if (!authenticated.success)
|
|
2313
|
+
return authenticated;
|
|
2314
|
+
return buildAuthResult(authenticated.data.user, authenticated.data.apiKey);
|
|
2303
2315
|
},
|
|
2304
2316
|
async logout(accessToken) {
|
|
2305
2317
|
const sessionResult = await sessionFunctions.get(accessToken);
|
|
@@ -2314,7 +2326,21 @@ function buildAuthnOperations(deps) {
|
|
|
2314
2326
|
return ok(void 0);
|
|
2315
2327
|
},
|
|
2316
2328
|
async verifyToken(accessToken) {
|
|
2317
|
-
|
|
2329
|
+
const session = await sessionFunctions.verifyToken(accessToken);
|
|
2330
|
+
if (!session.success)
|
|
2331
|
+
return session;
|
|
2332
|
+
const [user, authorization] = await Promise.all([
|
|
2333
|
+
userRepository.findById(session.data.userId),
|
|
2334
|
+
authzFunctions.getUserAuthorization(session.data.userId)
|
|
2335
|
+
]);
|
|
2336
|
+
if (!user.success)
|
|
2337
|
+
return user;
|
|
2338
|
+
if (!user.data?.enabled)
|
|
2339
|
+
return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
|
|
2340
|
+
if (!authorization.success)
|
|
2341
|
+
return authorization;
|
|
2342
|
+
const current = { ...session.data, username: user.data.username, displayName: user.data.displayName, avatarUrl: user.data.avatarUrl, ...authorization.data };
|
|
2343
|
+
return ok(current.apiKeyId ? applyApiKeyScopes(current, current.apiKeyScopes ?? []) : current);
|
|
2318
2344
|
},
|
|
2319
2345
|
async sendOtp(identifier) {
|
|
2320
2346
|
const disabled = loginDisabled("otp");
|
|
@@ -2666,6 +2692,7 @@ async function createDbRolePermissionRepository() {
|
|
|
2666
2692
|
});
|
|
2667
2693
|
}
|
|
2668
2694
|
var USER_ROLE_TABLE = "hai_iam_user_roles";
|
|
2695
|
+
var ROLE_TABLE = "hai_iam_roles";
|
|
2669
2696
|
var USER_ROLE_SCHEMA = {
|
|
2670
2697
|
user_id: { type: "TEXT", notNull: true },
|
|
2671
2698
|
role_id: { type: "TEXT", notNull: true }
|
|
@@ -2752,6 +2779,12 @@ async function createDbUserRoleRepository(roleRepository) {
|
|
|
2752
2779
|
async getRoleIds(userId, tx) {
|
|
2753
2780
|
return getRoleIdsInternal(userId, tx);
|
|
2754
2781
|
},
|
|
2782
|
+
async getRoleSummaries(userId) {
|
|
2783
|
+
return reldb.sql.query(
|
|
2784
|
+
`SELECT r.id, r.code FROM ${USER_ROLE_TABLE} ur JOIN ${ROLE_TABLE} r ON r.id = ur.role_id WHERE ur.user_id = ?`,
|
|
2785
|
+
[userId]
|
|
2786
|
+
);
|
|
2787
|
+
},
|
|
2755
2788
|
async getRoles(userId, tx) {
|
|
2756
2789
|
const idsResult = await getRoleIdsInternal(userId, tx);
|
|
2757
2790
|
if (!idsResult.success)
|
|
@@ -3186,6 +3219,15 @@ function createRbacManager(config) {
|
|
|
3186
3219
|
async getUserPermissions(userId) {
|
|
3187
3220
|
return getUserPermissionsInternal(userId);
|
|
3188
3221
|
},
|
|
3222
|
+
async getUserAuthorization(userId) {
|
|
3223
|
+
const roles = await userRoleRepository.getRoleSummaries(userId);
|
|
3224
|
+
if (!roles.success)
|
|
3225
|
+
return roles;
|
|
3226
|
+
const permissions = roles.data.length ? await rolePermissionRepository.getPermissionCodesForRoles(roles.data.map((role) => role.id)) : ok([]);
|
|
3227
|
+
if (!permissions.success)
|
|
3228
|
+
return permissions;
|
|
3229
|
+
return ok({ roles: roles.data.map((role) => role.code), permissions: permissions.data });
|
|
3230
|
+
},
|
|
3189
3231
|
async getUserRoles(userId) {
|
|
3190
3232
|
return userRoleRepository.getRoles(userId);
|
|
3191
3233
|
},
|
|
@@ -4201,7 +4243,8 @@ async function createSessionOperations(deps) {
|
|
|
4201
4243
|
maxAge: sessionConfig.maxAge,
|
|
4202
4244
|
sliding: sessionConfig.sliding,
|
|
4203
4245
|
singleDevice: sessionConfig.singleDevice,
|
|
4204
|
-
sessionRepository
|
|
4246
|
+
sessionRepository,
|
|
4247
|
+
apiKeyRepository: config.login.apikey ? await createDbApiKeyRepository() : void 0
|
|
4205
4248
|
});
|
|
4206
4249
|
logger8.info("Session sub-feature initialized");
|
|
4207
4250
|
return ok(functions);
|
|
@@ -4219,6 +4262,23 @@ function buildSessionFunctions(config) {
|
|
|
4219
4262
|
const sliding = config.sliding ?? true;
|
|
4220
4263
|
const singleDevice = config.singleDevice ?? false;
|
|
4221
4264
|
const repo = config.sessionRepository;
|
|
4265
|
+
async function validateApiKey(session) {
|
|
4266
|
+
if (!session.apiKeyId)
|
|
4267
|
+
return ok(session);
|
|
4268
|
+
if (!config.apiKeyRepository)
|
|
4269
|
+
return err(HaiIamError.APIKEY_INVALID, iamM("iam_apikeyInvalid"));
|
|
4270
|
+
const found = await config.apiKeyRepository.findOneById(session.apiKeyId);
|
|
4271
|
+
if (!found.success)
|
|
4272
|
+
return found;
|
|
4273
|
+
const key = found.data;
|
|
4274
|
+
if (!key || key.userId !== session.userId)
|
|
4275
|
+
return err(HaiIamError.APIKEY_INVALID, iamM("iam_apikeyInvalid"));
|
|
4276
|
+
if (!key.enabled)
|
|
4277
|
+
return err(HaiIamError.APIKEY_DISABLED, iamM("iam_apikeyDisabled"));
|
|
4278
|
+
if (key.expiresAt && /* @__PURE__ */ new Date() > key.expiresAt)
|
|
4279
|
+
return err(HaiIamError.APIKEY_EXPIRED, iamM("iam_apikeyExpired"));
|
|
4280
|
+
return ok(applyApiKeyScopes({ ...session, apiKeyScopes: key.scopes }, key.scopes));
|
|
4281
|
+
}
|
|
4222
4282
|
return {
|
|
4223
4283
|
async create(options) {
|
|
4224
4284
|
try {
|
|
@@ -4269,6 +4329,9 @@ function buildSessionFunctions(config) {
|
|
|
4269
4329
|
}
|
|
4270
4330
|
return ok(null);
|
|
4271
4331
|
}
|
|
4332
|
+
const validated = await validateApiKey(session);
|
|
4333
|
+
if (!validated.success)
|
|
4334
|
+
return validated;
|
|
4272
4335
|
if (sliding) {
|
|
4273
4336
|
const now = /* @__PURE__ */ new Date();
|
|
4274
4337
|
const updateResult = await repo.updateByAccessToken(accessToken, {
|
|
@@ -4279,7 +4342,7 @@ function buildSessionFunctions(config) {
|
|
|
4279
4342
|
return updateResult;
|
|
4280
4343
|
}
|
|
4281
4344
|
}
|
|
4282
|
-
return ok(stripInternalSessionFields(
|
|
4345
|
+
return ok(stripInternalSessionFields(validated.data));
|
|
4283
4346
|
},
|
|
4284
4347
|
async verifyToken(accessToken) {
|
|
4285
4348
|
const sessionResult = await this.get(accessToken);
|
|
@@ -4321,6 +4384,10 @@ function buildSessionFunctions(config) {
|
|
|
4321
4384
|
iamM("iam_refreshTokenExpired")
|
|
4322
4385
|
);
|
|
4323
4386
|
}
|
|
4387
|
+
const validated = await validateApiKey(oldSession);
|
|
4388
|
+
if (!validated.success)
|
|
4389
|
+
return validated;
|
|
4390
|
+
const effectiveSession = validated.data;
|
|
4324
4391
|
const removeOldSessionResult = await repo.removeByAccessToken(oldSession.accessToken);
|
|
4325
4392
|
if (!removeOldSessionResult.success) {
|
|
4326
4393
|
return removeOldSessionResult;
|
|
@@ -4330,8 +4397,10 @@ function buildSessionFunctions(config) {
|
|
|
4330
4397
|
username: oldSession.username,
|
|
4331
4398
|
displayName: oldSession.displayName,
|
|
4332
4399
|
avatarUrl: oldSession.avatarUrl,
|
|
4333
|
-
roles:
|
|
4334
|
-
permissions:
|
|
4400
|
+
roles: effectiveSession.roles,
|
|
4401
|
+
permissions: effectiveSession.permissions,
|
|
4402
|
+
apiKeyId: effectiveSession.apiKeyId,
|
|
4403
|
+
apiKeyScopes: effectiveSession.apiKeyScopes,
|
|
4335
4404
|
source: oldSession.source,
|
|
4336
4405
|
data: oldSession.data ? { ...oldSession.data, _tokenPair: void 0 } : void 0
|
|
4337
4406
|
});
|
|
@@ -4827,6 +4896,29 @@ function buildRegistrationOps(ctx) {
|
|
|
4827
4896
|
function buildUserQueryOps(ctx) {
|
|
4828
4897
|
const { userRepository, sessionFunctions, authzFunctions } = ctx;
|
|
4829
4898
|
return {
|
|
4899
|
+
async getUsers(userIds, options) {
|
|
4900
|
+
if (userIds.length > 1e3 || userIds.some((id) => typeof id !== "string" || !id.trim()))
|
|
4901
|
+
return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_batchUserIdsInvalid"));
|
|
4902
|
+
const ids = [...new Set(userIds)];
|
|
4903
|
+
const found = /* @__PURE__ */ new Map();
|
|
4904
|
+
for (let offset = 0; offset < ids.length; offset += 500) {
|
|
4905
|
+
const batch = ids.slice(offset, offset + 500);
|
|
4906
|
+
const users = await userRepository.findAll({ where: `id IN (${batch.map(() => "?").join(", ")})`, params: batch });
|
|
4907
|
+
if (!users.success)
|
|
4908
|
+
return users;
|
|
4909
|
+
for (const user of users.data)
|
|
4910
|
+
found.set(user.id, toUser(user));
|
|
4911
|
+
}
|
|
4912
|
+
const items = ids.flatMap((id) => found.has(id) ? [found.get(id)] : []);
|
|
4913
|
+
if (options?.include?.includes("roles") && items.length) {
|
|
4914
|
+
const roles = await authzFunctions.getUserRolesForMany(items.map((user) => user.id));
|
|
4915
|
+
if (!roles.success)
|
|
4916
|
+
return roles;
|
|
4917
|
+
for (const user of items)
|
|
4918
|
+
user.roles = roles.data.get(user.id) ?? [];
|
|
4919
|
+
}
|
|
4920
|
+
return ok(items);
|
|
4921
|
+
},
|
|
4830
4922
|
async getCurrentUser(accessToken) {
|
|
4831
4923
|
const verifyResult = await sessionFunctions.verifyToken(accessToken);
|
|
4832
4924
|
if (!verifyResult.success) {
|
|
@@ -4855,9 +4947,9 @@ function buildUserQueryOps(ctx) {
|
|
|
4855
4947
|
const user = toUser(userResult.data);
|
|
4856
4948
|
if (options?.include?.includes("roles")) {
|
|
4857
4949
|
const rolesResult = await authzFunctions.getUserRoles(userId);
|
|
4858
|
-
if (rolesResult.success)
|
|
4859
|
-
|
|
4860
|
-
|
|
4950
|
+
if (!rolesResult.success)
|
|
4951
|
+
return rolesResult;
|
|
4952
|
+
user.roles = rolesResult.data;
|
|
4861
4953
|
}
|
|
4862
4954
|
return ok(user);
|
|
4863
4955
|
},
|
|
@@ -4874,6 +4966,10 @@ function buildUserQueryOps(ctx) {
|
|
|
4874
4966
|
conditions.push("enabled = ?");
|
|
4875
4967
|
params.push(options.enabled ? 1 : 0);
|
|
4876
4968
|
}
|
|
4969
|
+
if (options?.roleCode) {
|
|
4970
|
+
conditions.push("EXISTS (SELECT 1 FROM hai_iam_user_roles ur JOIN hai_iam_roles r ON r.id = ur.role_id WHERE ur.user_id = hai_iam_users.id AND r.code = ?)");
|
|
4971
|
+
params.push(options.roleCode);
|
|
4972
|
+
}
|
|
4877
4973
|
const where = conditions.length > 0 ? conditions.join(" AND ") : void 0;
|
|
4878
4974
|
const sortColumn = options?.sortBy ? USER_SORT_COLUMNS[options.sortBy] : USER_SORT_COLUMNS.createdAt;
|
|
4879
4975
|
const sortDirection = options?.sortBy && options.sortDirection === "asc" ? "ASC" : "DESC";
|
|
@@ -4890,11 +4986,10 @@ function buildUserQueryOps(ctx) {
|
|
|
4890
4986
|
if (options?.include?.includes("roles") && items.length > 0) {
|
|
4891
4987
|
const userIds = items.map((u) => u.id);
|
|
4892
4988
|
const rolesMapResult = await authzFunctions.getUserRolesForMany(userIds);
|
|
4893
|
-
if (rolesMapResult.success)
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
}
|
|
4989
|
+
if (!rolesMapResult.success)
|
|
4990
|
+
return rolesMapResult;
|
|
4991
|
+
for (const user of items)
|
|
4992
|
+
user.roles = rolesMapResult.data.get(user.id) ?? [];
|
|
4898
4993
|
}
|
|
4899
4994
|
return ok({
|
|
4900
4995
|
items,
|