@h-ai/iam 0.1.0-alpha.61 → 0.1.0-alpha.63

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/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 [roleCodesResult, permCodesResult] = await Promise.all([
2237
- resolveUserRoleCodes(user.id),
2238
- resolveUserPermissionCodes(user.id)
2239
- ]);
2240
- if (!roleCodesResult.success) {
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
- roles: roleCodesResult.data,
2252
- permissions: permCodesResult.data
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
- roles: roleCodesResult.data,
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
- return loginWithStrategy("apikey", credentials, apiKeyStrategy);
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
- return sessionFunctions.verifyToken(accessToken);
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(session));
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: oldSession.roles,
4334
- permissions: oldSession.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
- user.roles = rolesResult.data;
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
- for (const user of items) {
4895
- user.roles = rolesMapResult.data.get(user.id) ?? [];
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,