{"version":3,"sources":["../src/index.ts","../src/authnpaas.module.ts","../src/const.ts","../src/guards/authnpaas.guard.ts","../src/services/authnpaas.service.ts","../src/controllers/authnpaas.controller.ts","../src/decorators/public.decorator.ts","../src/decorators/need-login.decorator.ts"],"sourcesContent":["/**\n * @lark-apaas/nestjs-authnpaas\n *\n * 基于 CASL 的 NestJS 权限管理模块\n * 提供角色、权限点位、环境等多维度的鉴权能力\n */\n\n// 主模块\nexport { AuthNPaasModule } from './authnpaas.module';\n\n// 服务\nexport * from './services';\n\n// 控制器\nexport * from './controllers';\n\n// 装饰器\nexport * from './decorators';\n\n// 守卫\nexport * from './guards';\n\n// 类型定义\nexport * from './types';\n\n// 常量\nexport * from './const';\n","import { DynamicModule, Module } from '@nestjs/common';\nimport { APP_GUARD, Reflector } from '@nestjs/core';\nimport { AuthNPaasModuleOptions } from './types';\nimport { AUTHNPAAS_MODULE_OPTIONS } from './const';\nimport { AuthNPaasGuard } from './guards/authnpaas.guard';\nimport { AuthNPaasService } from './services/authnpaas.service';\nimport { AuthNPaasController } from './controllers/authnpaas.controller';\n\n@Module({})\nexport class AuthNPaasModule {\n  static forRoot(options?: AuthNPaasModuleOptions): DynamicModule {\n    return {\n      module: AuthNPaasModule,\n      global: true,\n      controllers: [AuthNPaasController],\n      providers: [\n        // 配置提供者\n        {\n          provide: AUTHNPAAS_MODULE_OPTIONS,\n          useValue: {\n            ...(options || {}),\n          },\n        },\n        // 核心服务\n        Reflector,\n        // 服务提供者\n        AuthNPaasGuard,\n        AuthNPaasService,\n        // 守卫提供者\n        {\n          provide: APP_GUARD,\n          useClass: AuthNPaasGuard,\n        },\n      ],\n      exports: [AuthNPaasService],\n    };\n  }\n}\n","/**\n * 常量\n */\n\n/** AuthNPaas 模块选项 Token */\nexport const AUTHNPAAS_MODULE_OPTIONS = Symbol('AUTHNPAAS_MODULE_OPTIONS');\n\n/**\n * 元数据键\n */\n\n/** 需要登录元数据键 */\nexport const NEED_LOGIN_KEY = 'authnpaas:needLogin';\n","import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';\nimport { Response } from 'express';\nimport { Reflector } from '@nestjs/core';\nimport { NEED_LOGIN_KEY } from '../const';\n\n/**\n * AuthNPaas 守卫\n * 负责协调所有鉴权检查，具体检查逻辑委托给 PermissionService\n */\n@Injectable()\nexport class AuthNPaasGuard implements CanActivate {\n  constructor(private reflector: Reflector) {}\n\n  async canActivate(context: ExecutionContext): Promise<boolean> {\n    const http = context.switchToHttp();\n    const request = http.getRequest();\n    const response = http.getResponse<Response>();\n\n    const { userId, loginUrl } = request.userContext || {};\n\n    // 读取 NeedLogin 元数据并标记到请求对象，供异常过滤器使用\n    const needLoginMeta = this.reflector.getAllAndOverride<{\n      loginPath?: string;\n    }>(NEED_LOGIN_KEY, [context.getHandler(), context.getClass()]);\n\n    // NeedLogin 且无 userId -> 一律 401；拿得到 loginUrl 时顺带透出跳转 url\n    if (needLoginMeta && !userId) {\n      if (loginUrl) {\n        response.setHeader('x-login-url', loginUrl);\n      }\n      throw new UnauthorizedException('未登录');\n    }\n\n    return true;\n  }\n}\n","import { Injectable, Inject, Logger, HttpException, HttpStatus } from '@nestjs/common';\nimport {\n  PLATFORM_HTTP_CLIENT,\n  OBSERVABLE_SERVICE,\n  type PlatformHttpClient,\n  type ObservableService,\n  RequestContextService,\n  createTimer,\n} from '@lark-apaas/nestjs-common';\n\nimport type { MiaodaUserInfo } from '../types';\n\nconst SDK_MODULE = 'authn';\n\n/**\n * 平台 InnerAPI「按 ID 批量查人」路径。\n *\n * 服务端路由为 `/innerapi/v1/app/:appID/account/list_users`（另有\n * `/force_innerapi/...` 别名），`/innerapi` 前缀在 PlatformHttpClient 的\n * baseURL 里（env `FORCE_AUTHN_INNERAPI_DOMAIN`），故此处只写相对部分，\n * 与同包 `account/user/convert` 的写法一致。\n *\n * 刻意不做成 `AuthNPaasModuleOptions` 的可覆盖字段——module options 是公开\n * API，加了就删不掉，而这里一旦要改只需发一个补丁版。\n */\nconst LIST_USERS_PATH = (appId: string): string => `/v1/app/${appId}/account/list_users`;\n\n/** 单次批量查询的 ID 上限，与 getBatchLarkUserIds / getBatchMiaodaUserIds 一致 */\nconst MAX_BATCH_SIZE = 100;\n\n/** RPC 层的 BaseResp。注意下游异常时 StatusCode 仍是 0，真正的错误在 KStatusCode 上 */\ninterface PlatformBaseResp {\n  StatusCode?: number;\n  StatusMessage?: string;\n  KStatusCode?: string;\n  KStatusMessage?: string;\n}\n\n/** 业务负载。网关可能把它整个包进 data，也可能摊在顶层，两种都要吃 */\ninterface ListUsersPayload {\n  items?: MiaodaUserInfo[];\n  status_code?: string | number;\n  error_msg?: string;\n  /** 部分服务端用 err_msg 而非 error_msg，两者都读，否则错误文案会被整段丢弃 */\n  err_msg?: string;\n  BaseResp?: PlatformBaseResp;\n}\n\n/** 平台响应体（宽松形态） */\ninterface ListUsersResponse extends ListUsersPayload {\n  data?: ListUsersPayload;\n}\n\nfunction throwPlatformError(\n  message: string,\n  httpStatus: number,\n  platformCode?: string | number,\n  logId?: string,\n): never {\n  throw new HttpException(\n    {\n      statusCode: httpStatus,\n      code: 'PLATFORM_API_ERROR',\n      message,\n      platformCode,\n      'x-tt-logid': logId ?? '',\n    },\n    httpStatus,\n  );\n}\n\n/**\n * 取平台错误文案，兼容 error_msg / err_msg 两种字段名、顶层与 data 两层位置。\n *\n * 与 nestjs-authzpaas 的 `pickErrorMessage` 同形：部分服务端用 err_msg 而非\n * error_msg，只读一个会把错误文案整段丢掉、退化成兜底文案，排查只剩 logid。\n */\nfunction pickPlatformMessage(body: ListUsersResponse | undefined, fallback: string): string {\n  return (\n    body?.error_msg\n    || body?.err_msg\n    || body?.data?.error_msg\n    || body?.data?.err_msg\n    || fallback\n  );\n}\n\n/** 只透出契约内的三个字段，防止平台将来多返回字段时被动扩大对外契约 */\nfunction pickUserInfo(raw: MiaodaUserInfo): MiaodaUserInfo {\n  return {\n    miaodaUserID: String(raw.miaodaUserID),\n    name: raw.name,\n    avatar: raw.avatar,\n  };\n}\n\n/**\n * 妙搭通讯录能力（`AuthNPaasService.listUsersByIds`，服务端 / OpenAPI 态可用）\n *\n * 与本类其他方法的区别：查人**不依赖调用者身份**，只用应用凭据（AK/SK 自签\n * JWT + x-api-key）+ 上下文里的 `appId`。因此在 OpenAPI 请求（没有 cookie、\n * 网关注入不了登录态、`RequestContext.userId` 为空）下同样可用。\n *\n * 服务端**没有**搜人 / 搜部门 / 搜群能力——搜索结果取决于「谁在搜」，\n * OpenAPI 态拿不到调用者视角。需要搜索请改走飞书开放平台通讯录 API。\n */\n\n/**\n * 妙搭用户ID → 飞书用户ID 的转换类型\n */\nconst CONVERT_TYPE_FORCE_TO_FEISHU = 30;\n\n/**\n * 飞书用户ID → 妙搭用户ID 的转换类型\n */\nconst CONVERT_TYPE_FEISHU_TO_FORCE = 31;\n\n/**\n * ID 转换结果项\n */\nexport interface UserIDMappingItem {\n  sourceUserID: string;\n  targetUserID: string;\n}\n\n/**\n * 批量转换响应\n */\ninterface BatchConvertResponse {\n  data?: {\n    items?: UserIDMappingItem[];\n  };\n  status_code?: string;\n  status_message?: string;\n  error_msg?: string;\n}\n\n@Injectable()\nexport class AuthNPaasService {\n  private readonly logger = new Logger(AuthNPaasService.name);\n\n  constructor(\n    @Inject(PLATFORM_HTTP_CLIENT) private readonly httpClient: PlatformHttpClient,\n    @Inject(OBSERVABLE_SERVICE) private readonly obs: ObservableService,\n    private readonly requestContext: RequestContextService,\n  ) {}\n\n  /**\n   * 获取当前登录用户的飞书 user_id\n   *\n   * 从请求上下文获取当前妙搭 userId，调用转换接口获取对应的飞书 user_id\n   *\n   * @returns 飞书 user_id，如果当前用户未登录或转换失败返回 null\n   */\n  async getCurrentUserLarkUserId(): Promise<string | null> {\n    const userId = this.requestContext.get('userId');\n    if (!userId) {\n      this.logger.warn('getCurrentUserLarkUserId: no userId in request context');\n      return null;\n    }\n\n    const items = await this.batchConvertUserIds('getCurrentUserLarkUserId', [userId], CONVERT_TYPE_FORCE_TO_FEISHU);\n    if (!items || items.length === 0) {\n      return null;\n    }\n\n    return items[0].targetUserID || null;\n  }\n\n  /**\n   * 批量将妙搭 userId 转换为飞书 user_id\n   *\n   * @param userIds - 妙搭用户 ID 列表，最多 100 个\n   * @returns 飞书 user_id 数组，顺序与输入对应，转换失败的项为 null\n   */\n  async getBatchLarkUserIds(userIds: string[]): Promise<(string | null)[]> {\n    if (!userIds || userIds.length === 0) {\n      return [];\n    }\n\n    if (userIds.length > 100) {\n      throw new Error('userIds length must not exceed 100');\n    }\n\n    const items = await this.batchConvertUserIds('getBatchLarkUserIds', userIds, CONVERT_TYPE_FORCE_TO_FEISHU);\n    const mapping = new Map(items.map(item => [item.sourceUserID, item.targetUserID]));\n    return userIds.map(id => mapping.get(id) ?? null);\n  }\n\n  /**\n   * 批量将飞书 user_id(employee_id)转换为妙搭 userId\n   *\n   * @param employeeIds - 飞书 user_id(employee_id)列表,最多 100 个\n   * @returns 妙搭 userId 数组,顺序与输入对应,转换失败的项为 null\n   */\n  async getBatchMiaodaUserIds(employeeIds: string[]): Promise<(string | null)[]> {\n    if (!employeeIds || employeeIds.length === 0) {\n      return [];\n    }\n\n    if (employeeIds.length > 100) {\n      throw new Error('employeeIds length must not exceed 100');\n    }\n\n    const items = await this.batchConvertUserIds('getBatchMiaodaUserIds', employeeIds, CONVERT_TYPE_FEISHU_TO_FORCE);\n    const mapping = new Map(items.map(item => [item.sourceUserID, item.targetUserID]));\n    return employeeIds.map(id => mapping.get(id) ?? null);\n  }\n\n  /**\n   * 调用批量转换接口（带可观测性）\n   */\n  private async batchConvertUserIds(method: string, userIds: string[], convertType: number): Promise<UserIDMappingItem[]> {\n    const appId = this.requestContext.get('appId');\n    if (!appId) {\n      this.logger.warn(`${method}: no appId in request context`);\n      return [];\n    }\n\n    const url = `/v1/app/${appId}/account/user/convert`;\n    const body = {\n      convertType,\n      userIDList: userIds,\n    };\n\n    const spanName = `${SDK_MODULE}.${method}`;\n    const spanAttrs: Record<string, unknown> = {\n      module: SDK_MODULE,\n      method,\n      source_type: 'platform',\n      paas_attributes_module: SDK_MODULE,\n      http_method: 'POST',\n      path: url,\n    };\n    const logAttrs: Record<string, unknown> = {\n      source_type: 'platform',\n      paas_attributes_module: SDK_MODULE,\n    };\n\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    return this.obs.trace(spanName, async (span: any) => {\n      span.setAttributes(spanAttrs);\n      const elapsed = createTimer();\n\n      try {\n        this.logger.log(`[batchConvertUserIds] request: ${url} ${JSON.stringify(body)}`);\n        const response = await this.httpClient.post(url, body);\n        const logId = response.headers?.get?.('x-tt-logid') ?? '';\n        const raw = await response.text();\n        const durationMs = elapsed();\n\n        this.logger.log(`[batchConvertUserIds] response: status=${response.status} ok=${response.ok} logId=${logId} raw=${raw}`);\n\n        if (!response.ok) {\n          span.setAttributes({ duration_ms: durationMs, success: false, error: `HTTP ${response.status}`, 'x-tt-logid': logId });\n          this.logger.warn(JSON.stringify({\n            method, path: url, duration_ms: durationMs, success: false,\n            error: `HTTP ${response.status} ${response.statusText}`,\n            'x-tt-logid': logId, params: { convertType, count: userIds.length },\n          }), logAttrs);\n          return [];\n        }\n\n        const result: BatchConvertResponse = raw ? JSON.parse(raw) : {};\n\n        if (String(result.status_code) !== '0' && result.status_code !== undefined) {\n          const errMsg = `[${result.status_code}] ${result.status_message || result.error_msg}`;\n          span.setAttributes({ duration_ms: durationMs, success: false, error: errMsg, 'x-tt-logid': logId });\n          this.logger.error(JSON.stringify({\n            method, path: url, duration_ms: durationMs, success: false,\n            error: errMsg, 'x-tt-logid': logId,\n            params: { convertType, count: userIds.length },\n          }), logAttrs);\n          return [];\n        }\n\n        const items = (result.data?.items ?? []).filter(\n          (item): item is UserIDMappingItem => item != null && item.sourceUserID != null,\n        );\n\n        span.setAttributes({ duration_ms: durationMs, success: true, 'x-tt-logid': logId, result_count: items.length });\n        this.logger.log(JSON.stringify({\n          method, path: url, duration_ms: durationMs, success: true,\n          'x-tt-logid': logId,\n          params: { convertType, count: userIds.length },\n          result_count: items.length,\n          response: raw.length > 1024 ? raw.slice(0, 1024) + '...' : raw,\n        }), logAttrs);\n\n        return items;\n      } catch (error: unknown) {\n        const durationMs = elapsed();\n        const errMsg = error instanceof Error ? error.message : String(error);\n\n        const err = error as Record<string, unknown> | undefined;\n        const logId =\n          (err?.headers as { get?: (k: string) => string })?.get?.('x-tt-logid')\n          ?? (err?.response as { headers?: Record<string, string> })?.headers?.['x-tt-logid']\n          ?? err?.logId\n          ?? '';\n        this.logger.error(`[batchConvertUserIds] CATCH: ${errMsg} logId=${logId}`);\n\n        span.setAttributes({ duration_ms: durationMs, success: false, error: errMsg });\n        this.logger.warn(JSON.stringify({\n          method, path: url, duration_ms: durationMs, success: false,\n          error: errMsg,\n          params: { convertType, count: userIds.length },\n        }), logAttrs);\n        return [];\n      }\n    });\n  }\n\n  /**\n   * 按妙搭 userID 批量查用户信息（姓名 + 头像）\n   *\n   * @param userIds - 妙搭用户 ID 列表，最多 100 个\n   * @returns 与入参**等长且同序**的数组；未命中的位置为 `null`\n   *\n   * @throws {Error} 入参超过 100 个，或请求上下文中缺少 appId\n   * @throws {HttpException} 平台调用失败（502），错误信息含平台 code 与 x-tt-logid\n   *\n   * @example\n   * ```ts\n   * const [alice, bob] = await contacts.listUsersByIds([aliceId, bobId]);\n   * const displayName = alice?.name?.zh_cn ?? alice?.name?.en_us ?? '';\n   * const avatarUrl = alice?.avatar?.image?.large;\n   * ```\n   */\n  async listUsersByIds(userIds: string[]): Promise<(MiaodaUserInfo | null)[]> {\n    if (!userIds || userIds.length === 0) {\n      return [];\n    }\n\n    if (userIds.length > MAX_BATCH_SIZE) {\n      throw new Error(`userIds length must not exceed ${MAX_BATCH_SIZE}`);\n    }\n\n    const appId = this.requestContext.get('appId');\n    if (!appId) {\n      // 刻意不静默返回空——OpenAPI 态查不到人和「上下文没配好」是两回事\n      throw new Error('listUsersByIds: no appId in request context');\n    }\n\n    // 去重 + 去空串；请求体只带有效 ID，返回仍按原始入参对齐\n    const queryIds = [...new Set(userIds.filter(id => !!id))];\n    if (queryIds.length === 0) {\n      return userIds.map(() => null);\n    }\n\n    const url = LIST_USERS_PATH(appId);\n    const body = { userIDList: queryIds };\n\n    const items = await this.requestListUsers(url, body);\n    const found = new Map<string, MiaodaUserInfo>(\n      items\n        .filter(item => item != null && item.miaodaUserID != null)\n        .map(item => [String(item.miaodaUserID), pickUserInfo(item)]),\n    );\n\n    // 每个位置给一份独立拷贝：入参含重复 ID 时若共享引用，调用方改一处会连带改\n    // 另一处。必须是结构化拷贝而非 `{ ...hit }`——浅拷贝只隔离顶层，name / avatar\n    // 仍是同一份引用，改 `first.avatar.image.large` 照样连带改 second。\n    return userIds.map(id => {\n      const hit = id ? found.get(id) : undefined;\n      return hit ? structuredClone(hit) : null;\n    });\n  }\n\n  /** 发起平台调用并解析出 items（带可观测性） */\n  private async requestListUsers(\n    url: string,\n    body: { userIDList: string[] },\n  ): Promise<MiaodaUserInfo[]> {\n    const method = 'listUsersByIds';\n    const spanAttrs: Record<string, unknown> = {\n      module: SDK_MODULE,\n      method,\n      source_type: 'platform',\n      paas_attributes_module: SDK_MODULE,\n      http_method: 'POST',\n      path: url,\n    };\n    const logAttrs: Record<string, unknown> = {\n      source_type: 'platform',\n      paas_attributes_module: SDK_MODULE,\n    };\n\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    return this.obs.trace(`${SDK_MODULE}.${method}`, async (span: any) => {\n      span.setAttributes(spanAttrs);\n      const elapsed = createTimer();\n\n      try {\n        const response = await this.httpClient.post(url, body);\n        const logId = response.headers?.get?.('x-tt-logid') ?? '';\n        const raw = await response.text();\n        const items = parseListUsersResponse(response, raw, logId);\n        const durationMs = elapsed();\n\n        span.setAttributes({\n          duration_ms: durationMs,\n          success: true,\n          'x-tt-logid': logId,\n          result_count: items.length,\n        });\n        this.logger.log(\n          JSON.stringify({\n            method,\n            path: url,\n            duration_ms: durationMs,\n            success: true,\n            'x-tt-logid': logId,\n            params: { count: body.userIDList.length },\n            result_count: items.length,\n          }),\n          logAttrs,\n        );\n\n        return items;\n      } catch (error: unknown) {\n        const durationMs = elapsed();\n        const errMsg = error instanceof Error ? error.message : String(error);\n        const logId =\n          error instanceof HttpException\n            ? ((error.getResponse() as Record<string, unknown>)?.['x-tt-logid'] as string) ?? ''\n            : '';\n\n        span.setAttributes({\n          duration_ms: durationMs,\n          success: false,\n          error: errMsg,\n          ...(logId ? { 'x-tt-logid': logId } : {}),\n        });\n        this.logger.warn(\n          JSON.stringify({\n            method,\n            path: url,\n            duration_ms: durationMs,\n            success: false,\n            error: errMsg,\n            'x-tt-logid': logId,\n            params: { count: body.userIDList.length },\n          }),\n          logAttrs,\n        );\n\n        throw error;\n      }\n    });\n  }\n}\n\n/**\n * 解析平台响应。\n *\n * 三个判据并联，任一命中即视为失败：\n * 1. HTTP 非 2xx —— 传输 / 网关层\n * 2. `status_code` 非 '0' —— HTTP InnerAPI 业务层\n * 3. `BaseResp.KStatusCode` 非空 —— RPC 层。**关键**：平台在下游异常时\n *    仍会返回 `StatusCode: 0`，只判 status_code 会把 `tenant not found`\n *    静默降级成「查无此人」。\n *\n * 判据 2/3 与 items 一样，顶层和 `data` 下都查——网关是否包一层未定，\n * 只查一处会让被包裹的错误漏网，等于把要消灭的失效模式又放进来一遍。\n */\nfunction parseListUsersResponse(\n  response: { ok: boolean; status: number },\n  raw: string,\n  logId: string,\n): MiaodaUserInfo[] {\n  if (!response.ok) {\n    let err: ListUsersResponse | undefined;\n    try {\n      err = JSON.parse(raw);\n    } catch {\n      /* 错误体不是 JSON 时忽略，用兜底文案 */\n    }\n    const message = pickPlatformMessage(err, `平台 API 请求失败 (${response.status})`);\n    const httpStatus =\n      response.status >= 500 ? HttpStatus.BAD_GATEWAY : (response.status as number);\n    throwPlatformError(message, httpStatus, err?.status_code, logId);\n  }\n\n  if (!raw) {\n    return [];\n  }\n\n  let json: ListUsersResponse;\n  try {\n    json = JSON.parse(raw);\n  } catch {\n    throwPlatformError('平台 API 响应不是合法 JSON', HttpStatus.BAD_GATEWAY, undefined, logId);\n  }\n\n  // body 是字面量 \"null\" 时 JSON.parse 得到 null，后续取字段会崩\n  if (json == null) {\n    return [];\n  }\n\n  // 错误标记与 items 一样，顶层和 data 下都要查——只查一处会让被包裹的\n  // 下游异常漏网，正是本服务要消灭的那种「静默降级成查无此人」\n  const statusCode = json.status_code ?? json.data?.status_code;\n  if (statusCode != null && String(statusCode) !== '0') {\n    throwPlatformError(\n      pickPlatformMessage(json, `平台业务错误 (${statusCode})`),\n      HttpStatus.BAD_GATEWAY,\n      statusCode,\n      logId,\n    );\n  }\n\n  const baseResp = json.BaseResp ?? json.data?.BaseResp;\n  const kStatusCode = baseResp?.KStatusCode;\n  if (kStatusCode) {\n    throwPlatformError(\n      baseResp?.KStatusMessage || baseResp?.StatusMessage || `平台业务错误 (${kStatusCode})`,\n      HttpStatus.BAD_GATEWAY,\n      kStatusCode,\n      logId,\n    );\n  }\n\n  // 跨租户 / 全部未命中 / 空入参时平台不返回 items 键（不是空数组）\n  return json.data?.items ?? json.items ?? [];\n}\n","import { Controller, Get } from '@nestjs/common';\nimport { ApiExcludeController } from '@nestjs/swagger';\nimport { AuthNPaasService } from '../services/authnpaas.service';\n\n@ApiExcludeController()\n@Controller('api/authnpaas')\nexport class AuthNPaasController {\n  constructor(private readonly authnService: AuthNPaasService) {}\n\n  /**\n   * 获取当前登录用户的飞书 user_id\n   *\n   * GET /api/authnpaas/lark-user-id\n   */\n  @Get('lark-user-id')\n  async getCurrentUserLarkUserId() {\n    const larkUserId = await this.authnService.getCurrentUserLarkUserId();\n    return { lark_user_id: larkUserId };\n  }\n}\n","import { SetMetadata } from '@nestjs/common';\n\n/**\n * Public 装饰器的元数据键\n */\nexport const IS_PUBLIC_KEY = 'isPublic';\n\n/**\n * 标记接口为公开接口，跳过 AuthNPaasGuard 的鉴权检查\n * \n * @example\n * ```typescript\n * @Controller('mock-api/users')\n * @Public()  // 控制器级别\n * export class MockPermissionController {\n *   @Get(':userId/permissions')\n *   getUserPermissions() {}\n * }\n * \n * // 或者在方法级别\n * @Controller('api')\n * export class ApiController {\n *   @Get('public-info')\n *   @Public()  // 方法级别\n *   getPublicInfo() {}\n * }\n * ```\n */\nexport const Public = () => SetMetadata(IS_PUBLIC_KEY, true);\n\n","import { SetMetadata } from '@nestjs/common';\nimport { NEED_LOGIN_KEY } from '../const';\n\n/**\n * NeedLogin 装饰器\n * 标记接口需要登录。如果鉴权失败（如 401/403），异常过滤器会将请求重定向到登录页。\n * 可选地传入登录页路径，默认 '/login'。\n */\nexport const NeedLogin = (loginPath?: string): MethodDecorator & ClassDecorator => {\n  return SetMetadata(NEED_LOGIN_KEY, { loginPath });\n};\n\n\n"],"mappings":"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","names":["import_common","import_core","AUTHNPAAS_MODULE_OPTIONS","Symbol","NEED_LOGIN_KEY","AuthNPaasGuard","reflector","canActivate","context","http","switchToHttp","request","getRequest","response","getResponse","userId","loginUrl","userContext","needLoginMeta","getAllAndOverride","NEED_LOGIN_KEY","getHandler","getClass","setHeader","UnauthorizedException","import_common","SDK_MODULE","LIST_USERS_PATH","appId","MAX_BATCH_SIZE","throwPlatformError","message","httpStatus","platformCode","logId","HttpException","statusCode","code","pickPlatformMessage","body","fallback","error_msg","err_msg","data","pickUserInfo","raw","miaodaUserID","String","name","avatar","CONVERT_TYPE_FORCE_TO_FEISHU","CONVERT_TYPE_FEISHU_TO_FORCE","AuthNPaasService","logger","Logger","httpClient","obs","requestContext","getCurrentUserLarkUserId","userId","get","warn","items","batchConvertUserIds","length","targetUserID","getBatchLarkUserIds","userIds","Error","mapping","Map","map","item","sourceUserID","id","getBatchMiaodaUserIds","employeeIds","method","convertType","url","userIDList","spanName","spanAttrs","module","source_type","paas_attributes_module","http_method","path","logAttrs","trace","span","setAttributes","elapsed","createTimer","log","JSON","stringify","response","post","headers","text","durationMs","status","ok","duration_ms","success","error","statusText","params","count","result","parse","status_code","undefined","errMsg","status_message","filter","result_count","slice","err","listUsersByIds","queryIds","Set","requestListUsers","found","hit","structuredClone","parseListUsersResponse","getResponse","HttpStatus","BAD_GATEWAY","json","baseResp","BaseResp","kStatusCode","KStatusCode","KStatusMessage","StatusMessage","import_common","AuthNPaasController","authnService","getCurrentUserLarkUserId","larkUserId","lark_user_id","AuthNPaasModule","forRoot","options","module","global","controllers","AuthNPaasController","providers","provide","AUTHNPAAS_MODULE_OPTIONS","useValue","Reflector","AuthNPaasGuard","AuthNPaasService","APP_GUARD","useClass","exports","import_common","IS_PUBLIC_KEY","Public","SetMetadata","import_common","NeedLogin","loginPath","SetMetadata","NEED_LOGIN_KEY"]}