@bearei/nest 0.0.0-stage → 2.0.0-dev.1

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.
Files changed (145) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/LICENSE +21 -0
  3. package/README.md +100 -2
  4. package/README.zh-CN.md +103 -0
  5. package/dist/action-log/action-log.interface.d.ts +12 -0
  6. package/dist/action-log/action-log.interface.js +2 -0
  7. package/dist/action-log/action-log.module.d.ts +2 -0
  8. package/dist/action-log/action-log.module.js +22 -0
  9. package/dist/action-log/action-log.service.d.ts +10 -0
  10. package/dist/action-log/action-log.service.js +42 -0
  11. package/dist/action-log/index.d.ts +3 -0
  12. package/dist/action-log/index.js +19 -0
  13. package/dist/authentication/authentication-context.interceptor.d.ts +7 -0
  14. package/dist/authentication/authentication-context.interceptor.js +70 -0
  15. package/dist/authentication/authentication.enum.d.ts +6 -0
  16. package/dist/authentication/authentication.enum.js +9 -0
  17. package/dist/authentication/authentication.guard.d.ts +13 -0
  18. package/dist/authentication/authentication.guard.js +92 -0
  19. package/dist/authentication/authentication.interface.d.ts +2 -0
  20. package/dist/authentication/authentication.interface.js +2 -0
  21. package/dist/authentication/index.d.ts +4 -0
  22. package/dist/authentication/index.js +20 -0
  23. package/dist/authorization/authorization.interface.d.ts +3 -0
  24. package/dist/authorization/authorization.interface.js +2 -0
  25. package/dist/authorization/authorization.module.d.ts +2 -0
  26. package/dist/authorization/authorization.module.js +22 -0
  27. package/dist/authorization/authorization.service.d.ts +10 -0
  28. package/dist/authorization/authorization.service.js +56 -0
  29. package/dist/authorization/index.d.ts +3 -0
  30. package/dist/authorization/index.js +19 -0
  31. package/dist/base-service/base-service.interface.d.ts +84 -0
  32. package/dist/base-service/base-service.interface.js +2 -0
  33. package/dist/base-service/base-service.module.d.ts +2 -0
  34. package/dist/base-service/base-service.module.js +22 -0
  35. package/dist/base-service/base-service.service.d.ts +27 -0
  36. package/dist/base-service/base-service.service.js +517 -0
  37. package/dist/base-service/index.d.ts +3 -0
  38. package/dist/base-service/index.js +19 -0
  39. package/dist/common.dto.d.ts +19 -0
  40. package/dist/common.dto.js +111 -0
  41. package/dist/common.entity.d.ts +11 -0
  42. package/dist/common.entity.js +76 -0
  43. package/dist/common.enum.d.ts +19 -0
  44. package/dist/common.enum.js +24 -0
  45. package/dist/common.interface.d.ts +19 -0
  46. package/dist/common.interface.js +2 -0
  47. package/dist/database/database.module.d.ts +5 -0
  48. package/dist/database/database.module.js +58 -0
  49. package/dist/database/index.d.ts +1 -0
  50. package/dist/database/index.js +17 -0
  51. package/dist/decorators/allow-access.decorator.d.ts +3 -0
  52. package/dist/decorators/allow-access.decorator.js +7 -0
  53. package/dist/decorators/index.d.ts +8 -0
  54. package/dist/decorators/index.js +24 -0
  55. package/dist/decorators/is-captcha.decorator.d.ts +2 -0
  56. package/dist/decorators/is-captcha.decorator.js +23 -0
  57. package/dist/decorators/is-nano-id.decorator.d.ts +2 -0
  58. package/dist/decorators/is-nano-id.decorator.js +22 -0
  59. package/dist/decorators/is-password.decorator.d.ts +2 -0
  60. package/dist/decorators/is-password.decorator.js +23 -0
  61. package/dist/decorators/is-version.decorator.d.ts +2 -0
  62. package/dist/decorators/is-version.decorator.js +22 -0
  63. package/dist/decorators/omit-entity-keys.decorator.d.ts +2 -0
  64. package/dist/decorators/omit-entity-keys.decorator.js +7 -0
  65. package/dist/decorators/require-permissions.decorator.d.ts +3 -0
  66. package/dist/decorators/require-permissions.decorator.js +7 -0
  67. package/dist/decorators/require-subscription-features.decorator.d.ts +3 -0
  68. package/dist/decorators/require-subscription-features.decorator.js +7 -0
  69. package/dist/exception/exception.enum.d.ts +12 -0
  70. package/dist/exception/exception.enum.js +16 -0
  71. package/dist/exception/exception.interface.d.ts +9 -0
  72. package/dist/exception/exception.interface.js +2 -0
  73. package/dist/exception/http.exception.d.ts +4 -0
  74. package/dist/exception/http.exception.js +47 -0
  75. package/dist/exception/index.d.ts +3 -0
  76. package/dist/exception/index.js +19 -0
  77. package/dist/external/external.interface.d.ts +58 -0
  78. package/dist/external/external.interface.js +2 -0
  79. package/dist/external/external.module.d.ts +2 -0
  80. package/dist/external/external.module.js +24 -0
  81. package/dist/external/external.service.d.ts +15 -0
  82. package/dist/external/external.service.js +160 -0
  83. package/dist/external/index.d.ts +3 -0
  84. package/dist/external/index.js +19 -0
  85. package/dist/index.d.ts +17 -0
  86. package/dist/index.js +39 -0
  87. package/dist/jwt/index.d.ts +3 -0
  88. package/dist/jwt/index.js +19 -0
  89. package/dist/jwt/jwt.interface.d.ts +15 -0
  90. package/dist/jwt/jwt.interface.js +2 -0
  91. package/dist/jwt/jwt.module.d.ts +2 -0
  92. package/dist/jwt/jwt.module.js +43 -0
  93. package/dist/jwt/jwt.service.d.ts +10 -0
  94. package/dist/jwt/jwt.service.js +52 -0
  95. package/dist/permission/index.d.ts +3 -0
  96. package/dist/permission/index.js +19 -0
  97. package/dist/permission/permission.enum.d.ts +70 -0
  98. package/dist/permission/permission.enum.js +73 -0
  99. package/dist/permission/permission.guard.d.ts +9 -0
  100. package/dist/permission/permission.guard.js +55 -0
  101. package/dist/permission/permission.interface.d.ts +2 -0
  102. package/dist/permission/permission.interface.js +2 -0
  103. package/dist/redis/index.d.ts +3 -0
  104. package/dist/redis/index.js +19 -0
  105. package/dist/redis/redis.interface.d.ts +4 -0
  106. package/dist/redis/redis.interface.js +2 -0
  107. package/dist/redis/redis.module.d.ts +2 -0
  108. package/dist/redis/redis.module.js +22 -0
  109. package/dist/redis/redis.service.d.ts +14 -0
  110. package/dist/redis/redis.service.js +51 -0
  111. package/dist/subscription-feature/index.d.ts +3 -0
  112. package/dist/subscription-feature/index.js +19 -0
  113. package/dist/subscription-feature/subscription-feature.enum.d.ts +3 -0
  114. package/dist/subscription-feature/subscription-feature.enum.js +6 -0
  115. package/dist/subscription-feature/subscription-feature.interceptor.d.ts +9 -0
  116. package/dist/subscription-feature/subscription-feature.interceptor.js +68 -0
  117. package/dist/subscription-feature/subscription-feature.interface.d.ts +2 -0
  118. package/dist/subscription-feature/subscription-feature.interface.js +2 -0
  119. package/dist/utils/create-response-sanitizer/create-response-sanitizer.utils.d.ts +1 -0
  120. package/dist/utils/create-response-sanitizer/create-response-sanitizer.utils.js +23 -0
  121. package/dist/utils/create-response-sanitizer/index.d.ts +1 -0
  122. package/dist/utils/create-response-sanitizer/index.js +17 -0
  123. package/dist/utils/extract-bearer-token/extract-bearer-token.utils.d.ts +1 -0
  124. package/dist/utils/extract-bearer-token/extract-bearer-token.utils.js +13 -0
  125. package/dist/utils/extract-bearer-token/index.d.ts +1 -0
  126. package/dist/utils/extract-bearer-token/index.js +17 -0
  127. package/dist/utils/format-mobile-number/format-mobile-number.utils.d.ts +1 -0
  128. package/dist/utils/format-mobile-number/format-mobile-number.utils.js +5 -0
  129. package/dist/utils/format-mobile-number/index.d.ts +1 -0
  130. package/dist/utils/format-mobile-number/index.js +17 -0
  131. package/dist/utils/generate-random-number-string/generate-random-number-string.utils.d.ts +1 -0
  132. package/dist/utils/generate-random-number-string/generate-random-number-string.utils.js +6 -0
  133. package/dist/utils/generate-random-number-string/index.d.ts +1 -0
  134. package/dist/utils/generate-random-number-string/index.js +17 -0
  135. package/dist/utils/index.d.ts +6 -0
  136. package/dist/utils/index.js +22 -0
  137. package/dist/utils/run-with-database-retry/index.d.ts +1 -0
  138. package/dist/utils/run-with-database-retry/index.js +17 -0
  139. package/dist/utils/run-with-database-retry/run-with-database-retry.utils.d.ts +1 -0
  140. package/dist/utils/run-with-database-retry/run-with-database-retry.utils.js +23 -0
  141. package/dist/utils/sign/index.d.ts +1 -0
  142. package/dist/utils/sign/index.js +17 -0
  143. package/dist/utils/sign/sign.utils.d.ts +5 -0
  144. package/dist/utils/sign/sign.utils.js +82 -0
  145. package/package.json +129 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,76 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 2.0.0-dev.1 — 2026-10-26
8
+
9
+ Development snapshot published under the `next` dist-tag for early integration.
10
+ Its contents match the 2.0.0 scope documented below; this entry is replaced by the
11
+ final 2.0.0 release notes.
12
+
13
+ Install it with `yarn add @bearei/nest@next` or pin `@bearei/nest@2.0.0-dev.1`.
14
+
15
+ ## 2.0.0 — unreleased
16
+
17
+ First release under `@bearei/nest`. It supersedes `@bearei/service-common`, whose last release was `1.7.9`;
18
+ that package replaced `@bearei/server-common` at `1.4.0`. The major version matches the studio-wide 2.0.0
19
+ baseline; because the npm identity is new, the Breaking list below is also the migration guide from `1.7.9`.
20
+
21
+ ### Breaking
22
+
23
+ - Renamed the package to `@bearei/nest` (previously `@bearei/service-common`, originally `@bearei/server-common`). Update imports; the old names will not be published again.
24
+ - Raised the runtime baseline to Node.js >= 20, NestJS 11, TypeORM 1.x and `@nestjs/axios` 4. NestJS, TypeORM, validation, Axios, RxJS and `reflect-metadata` are peer dependencies, so the application owns exactly one copy of each.
25
+ - Removed `CommonEntity.entityStatus`, `EntityStatus` and `ENTITY_STATUS`. Soft deletion is expressed only by `deletedAt`; recovery clears `deletedAt`. The `@BeforeSoftRemove` / `@BeforeRecover` hooks are gone.
26
+ - Renamed `CommonEntity.primaryId` to `internalId` (column `internal_id`). `createdAt` / `updatedAt` / `deletedAt` are typed `Date | string` (`deletedAt` also `null`) so imported JSON values are preserved.
27
+ - `CommonEntity.beforeInsert` now keeps a caller-provided `id` and `createdAt` instead of overwriting them, which makes imports idempotent.
28
+ - Split `BaseService` options into `BaseServiceCreateOptions`, `BaseServiceDeleteOptions`, `BaseServiceUpdateOptions`, `BaseServiceFindOptions`, `BaseServiceCountOptions`, `BaseServiceFindOneOptions` and `BaseServiceUpdateAssociationOptions`. `BaseServiceOptions<T>` is no longer generic. The old `CreateOptions` / `DelOptions` / `UpdateOptions` / `FindOptions` / `CountOptions` / `FindOneOptions` / `UpdateAssociationOptions` types are removed.
29
+ - Renamed `BaseService.del` to `delete` and `BaseService.checkProhibited` to `assertNotProhibited`. `BaseService.throwError` now takes `{httpExceptionKind, ...}`.
30
+ - `BaseService.createSuccessActionLog` now takes the affected entity (or entities) plus `ActionLogOptions`. `ActionLogService.success` / `.failure` take the payload as the first argument.
31
+ - Renamed `ActionLogOptions.enabledLog` to `isLogEnabled`. `headers` is optional, and logging requires both `isLogEnabled` and `headers`.
32
+ - Replaced `throwHTTPException(options)(exception)` with `createHttpException(kind, options)` and `throwHttpException(kind, options)`. `HTTPException` / `HTTPExceptionCode` are renamed to `HttpExceptionKind` / `HttpExceptionCode`, and `ThrowHTTPExceptionOptions` to `ThrowHttpExceptionOptions`.
33
+ - `HTTP_EXCEPTION.GENERIC` now returns `{code, message, statusCode}` instead of `{code, message, status}`; every exception kind uses the same `statusCode` field.
34
+ - `VALIDATION_METHOD` values are now `AND` / `OR` instead of `and` / `or`. Update persisted or hard-coded values.
35
+ - Renamed the DTOs: `IndexDTO` → `ListQueryDto` (`size` → `pageSize`, `count` → `withTotal`, `order` → `sortOrder`, `createdAtRanges` → `createdAtRange`), `ByIdDTO` → `ByIdDto`, `ByEmailDTO` → `ByEmailDto`, `NotIdDTO` → `ExcludeIdDto` (`notId` → `excludeId`).
36
+ - Replaced the pagination result `FindResult` (string `limit` / `offset` / `total`) with `PaginatedResult` (`page`, `pageSize`, optional numeric `total`, `results`).
37
+ - Renamed the decorators: `PublicAccesses` → `AllowAccess`, `AuthenticationPermissions` → `RequirePermissions`, `AuthenticationSubscriptionFeatures` → `RequireSubscriptionFeatures`, `IsNanoid` → `IsNanoId`, `OmitEntityKey` → `OmitEntityKeys`. Their metadata keys changed as well.
38
+ - Removed `AccessGuard`, `AccessInterceptor`, `AuthenticationService`, `AuthenticationModule` and the `AppModule` / controller / service / `main.ts` bootstrap. This package is a library and no longer ships a runnable Nest application.
39
+ - Removed the `buildMobileNumber`, `generateRandomNumber` and `createSanitizeResponseEntity` utilities; the replacements are listed under Added.
40
+ - Changed permission values from `DOMAIN_DOMAIN_ACTION` to `resource:action` (for example `USER_USER_VIEW` → `user:read`). Persisted permission rows must be migrated.
41
+ - Reduced `SUBSCRIPTION_FEATURE` to `BEAREI_BOOKMARK_CLOUD_SYNC`; the previous bookmark/carton/tag flags are removed.
42
+ - Renamed JWT types `JWTPayloadOptions` → `JwtSignPayloadOptions` and `JWTPayload` → `JwtPayload`. `jti` and `nbf` are optional in the payload. Tokens are signed with RS256 from the JWK in `jwt.privateKey`.
43
+ - Renamed `RedisSetOptions.expireTime` to `ttlMilliseconds`.
44
+ - Renamed `ExternalService.authenticationUserPermissions` / `authenticationUserSubscriptionFeatures` to `findUserPermissions` / `findUserSubscriptionFeatures`.
45
+
46
+ ### Added
47
+
48
+ - `AuthenticationGuard`: verifies bearer tokens, resolves the request identity and accepts internal HMAC-signed calls.
49
+ - `AuthenticationContextInterceptor`: rejects client-supplied `x-bearei-*` identity headers, forwards trusted identity headers and sanitizes response bodies.
50
+ - `AuthorizationModule` / `AuthorizationService`: permission resolution behind `@RequirePermissions`.
51
+ - `BaseService.createMany`: batch insert that applies the same server-managed-key rules as `create`.
52
+ - Query options on `BaseServiceFindOptions`: `innerJoin`, `queryBuilderWhere`, `totalFindOptions` and `sortBy` for joined, filtered and counted lists.
53
+ - `CommonEntity`: nanoid public `id`, internal numeric id, optimistic-locking `version`, UTC timestamps and idempotent inserts.
54
+ - `ListQueryDto`, `ByIdDto`, `ByEmailDto`, `ExcludeIdDto`, plus `PaginatedResult`, `ReservedRequestHeaders`, `AuthenticatedRequest` and string-literal union types for the shared enums.
55
+ - `ApiErrorBody` and `ReservedRequestHeaderName`, re-exported from `@bearei/contracts`.
56
+ - `createResponseSanitizer` (replaces `createSanitizeResponseEntity`), `formatMobileNumber` (replaces `buildMobileNumber`) and `generateRandomNumberString` (replaces `generateRandomNumber`).
57
+ - `runWithDatabaseRetry` for transient database connection failures.
58
+ - `AllowAccess`, `RequirePermissions` and `RequireSubscriptionFeatures` decorators, alongside the existing validators.
59
+ - Unit tests for every module, root-export smoke test, `test/setup.ts`, and CI workflows for verification and tag-driven releases.
60
+
61
+ ### Changed
62
+
63
+ - `ActionLogService` is best-effort: unless logging is explicitly enabled and headers are forwarded, it skips the write and never fails the business operation.
64
+ - `JwtService.verifyAsync` normalizes verification failures to `UNAUTHORIZED` and logs the underlying reason.
65
+ - `extractBearerToken` now matches the `Bearer` scheme case-insensitively, accepts a configurable header name and returns `undefined` for malformed or non-string headers.
66
+ - `ExternalService.createActionLog` takes the request headers plus `{action, serviceName, data, ...}` instead of a single options object.
67
+ - `ListQueryDto` transforms accept empty query values, numeric strings and date arrays without producing `NaN` or invalid dates.
68
+ - `@bearei/contracts` now owns `HTTP_EXCEPTION_CODE`, `LANGUAGE`, `ORDER`, `ApiErrorBody`, `ReservedRequestHeaderName` and `PaginatedResult`. `@bearei/nest` re-exports the same names, and `PaginatedResult<T>` is no longer constrained to `CommonEntity`.
69
+
70
+ ### Removed
71
+
72
+ - Application scaffold: `AppModule`, `AppController`, `AppService`, `main.ts` and the e2e test harness.
73
+ - Fastify and Helmet platform dependencies; the library no longer chooses an HTTP adapter.
74
+ - The `crypto` runtime dependency; signing uses Node's built-in `crypto`.
75
+ - `ENTITY_STATUS`, `EntityStatus`, `entityStatus`, `beforeSoftRemove` and `beforeRecover`.
76
+ - The old `access/`, `app.*` and `authentication.service` modules.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Bearei
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,101 @@
1
- # Temporary Holding Version
1
+ # @bearei/nest
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Shared foundation modules for Bearei NestJS services: base CRUD service, authentication and authorization guards, permission and subscription-feature checks, JWT, Redis, external HTTP calls, action logging, and common DTOs/entities.
4
+
5
+ English | [中文](./README.zh-CN.md)
6
+
7
+ This package was previously published as `@bearei/service-common` (last release `1.7.9`). `@bearei/nest` starts at `2.0.0`, matching the studio-wide baseline; because the package name changed, it is a new install rather than an in-place upgrade. Migration notes are in the [CHANGELOG](./CHANGELOG.md).
8
+
9
+ ## Requirements
10
+
11
+ - Node.js >= 20
12
+ - NestJS 11
13
+ - TypeORM 1.x
14
+
15
+ ## Installation
16
+
17
+ The package declares its NestJS, TypeORM, and validation dependencies as peer dependencies so your application owns exactly one copy of each.
18
+
19
+ ```bash
20
+ yarn add @bearei/nest
21
+ yarn add @nestjs/common @nestjs/core @nestjs/config @nestjs/axios @nestjs/jwt @nestjs/typeorm
22
+ yarn add axios class-transformer class-validator reflect-metadata rxjs typeorm
23
+ ```
24
+
25
+ Runtime dependencies installed automatically: `@bearei/contracts`, `bcrypt`, `dayjs`, `ioredis`, `nanoid`.
26
+
27
+ Wire contracts shared with clients — error codes, the error body shape, pagination limits, language and sort values, and reserved request headers — live in `@bearei/contracts` and are re-exported from this package.
28
+
29
+ ## Modules
30
+
31
+ | Module | Responsibility |
32
+ | -------------------------------- | ----------------------------------------------------------------------------------- |
33
+ | `BaseServiceModule` | Generic TypeORM CRUD service with pagination, relation handling, and action logging |
34
+ | `DatabaseModule` | TypeORM connection helpers and retry support |
35
+ | `JwtModule` | RS256 signing and verification from a JWK configured under `jwt.*` |
36
+ | `RedisModule` | Shared ioredis client and cache helpers |
37
+ | `ExternalModule` | Outbound HTTP requests through `@nestjs/axios` |
38
+ | `ActionLogModule` | Pluggable action-log writer |
39
+ | `AuthorizationModule` | Permission resolution and guards |
40
+ | `AuthenticationGuard` | Bearer token resolution and authentication context |
41
+ | `SubscriptionFeatureInterceptor` | Subscription-feature enforcement |
42
+ | `Common DTOs / entities / enums` | Pagination, response envelopes, shared entity columns, and shared enums |
43
+
44
+ Decorators include `@AllowAccess`, `@RequirePermissions`, `@RequireSubscriptionFeatures`, `@IsNanoId`, `@IsPassword`, `@IsVersion`, `@IsCaptcha`, and `@OmitEntityKeys`.
45
+
46
+ ## Quick start
47
+
48
+ ```ts
49
+ import {Module} from '@nestjs/common'
50
+ import {ConfigModule} from '@nestjs/config'
51
+ import {DatabaseModule, JwtModule, RedisModule} from '@bearei/nest'
52
+
53
+ @Module({
54
+ imports: [ConfigModule.forRoot({isGlobal: true}), DatabaseModule, JwtModule, RedisModule]
55
+ })
56
+ export class AppModule {}
57
+ ```
58
+
59
+ Extend the base service for CRUD endpoints:
60
+
61
+ ```ts
62
+ import {Injectable} from '@nestjs/common'
63
+ import {InjectRepository} from '@nestjs/typeorm'
64
+ import {BaseService} from '@bearei/nest'
65
+ import {Repository} from 'typeorm'
66
+
67
+ import {User} from './user.entity'
68
+
69
+ @Injectable()
70
+ export class UserService extends BaseService<User> {
71
+ constructor(@InjectRepository(User) repository: Repository<User>) {
72
+ super(repository)
73
+ }
74
+ }
75
+ ```
76
+
77
+ JWT configuration is read from `jwt.privateKey` (a JWK object or JSON string), `jwt.expiresIn`, and an optional `jwt.publicKey`.
78
+
79
+ ## Development
80
+
81
+ ```bash
82
+ yarn install
83
+ yarn lint # ESLint, no fixes
84
+ yarn typecheck # tsc --noEmit
85
+ yarn test # Jest with coverage thresholds
86
+ yarn build # compile to dist/
87
+ yarn verify # lint + typecheck + test + build
88
+ ```
89
+
90
+ Packaging:
91
+
92
+ ```bash
93
+ yarn pack --dry-run # inspect the published file list
94
+ yarn pack # produce the tarball
95
+ ```
96
+
97
+ Releases are published from CI on version tags. Update `version` in `package.json`, push the matching `v<version>` tag, and the release workflow publishes the package.
98
+
99
+ ## License
100
+
101
+ MIT
@@ -0,0 +1,103 @@
1
+ # @bearei/nest
2
+
3
+ Bearei NestJS 服务的共享基础模块:通用 CRUD 基础服务、认证与授权守卫、权限与订阅特性校验、JWT、Redis、外部 HTTP 调用、操作日志,以及通用 DTO 与实体。
4
+
5
+ [English](./README.md) | 中文
6
+
7
+ 本包此前以 `@bearei/service-common` 发布(最后版本 `1.7.9`)。`@bearei/nest` 从 `2.0.0` 开始,与工作室整体版本基线对齐;包名已变更,属于重新安装而非原地升级。迁移说明见 [CHANGELOG](./CHANGELOG.md)。
8
+
9
+ ## 环境要求
10
+
11
+ - Node.js >= 20
12
+ - NestJS 11
13
+ - TypeORM 1.x
14
+
15
+ ## 安装
16
+
17
+ 本包将 NestJS、TypeORM 及校验相关依赖声明为 peer dependencies,确保应用中每个依赖只存在一个实例。
18
+
19
+ ```bash
20
+ yarn add @bearei/nest
21
+ yarn add @nestjs/common @nestjs/core @nestjs/config @nestjs/axios @nestjs/jwt @nestjs/typeorm
22
+ yarn add axios class-transformer class-validator reflect-metadata rxjs typeorm
23
+ ```
24
+
25
+ 随包自动安装的运行时依赖:`@bearei/contracts`、`bcrypt`、`dayjs`、`ioredis`、`nanoid`。
26
+
27
+ 与客户端共享的传输契约——错误码、错误体结构、分页上限、语言与排序值、保留请求头——定义在 `@bearei/contracts`,并由本包转出。
28
+
29
+ ## 模块
30
+
31
+ | 模块 | 职责 |
32
+ | -------------------------------- | ---------------------------------------------------- |
33
+ | `BaseServiceModule` | 通用 TypeORM CRUD 服务,包含分页、关联处理与操作日志 |
34
+ | `DatabaseModule` | TypeORM 连接辅助与重试支持 |
35
+ | `JwtModule` | 基于 `jwt.*` 配置中的 JWK 进行 RS256 签名与验签 |
36
+ | `RedisModule` | 共享 ioredis 客户端与缓存辅助方法 |
37
+ | `ExternalModule` | 通过 `@nestjs/axios` 发起外部 HTTP 请求 |
38
+ | `ActionLogModule` | 可插拔的操作日志写入能力 |
39
+ | `AuthorizationModule` | 权限解析与守卫 |
40
+ | `AuthenticationGuard` | Bearer Token 解析与认证上下文 |
41
+ | `SubscriptionFeatureInterceptor` | 订阅特性校验 |
42
+ | `通用 DTO / 实体 / 枚举` | 分页、响应包装、共享实体列与通用枚举 |
43
+
44
+ 装饰器包括 `@AllowAccess`、`@RequirePermissions`、`@RequireSubscriptionFeatures`、`@IsNanoId`、`@IsPassword`、`@IsVersion`、`@IsCaptcha` 与 `@OmitEntityKeys`。
45
+
46
+ ## 快速开始
47
+
48
+ ```ts
49
+ import {Module} from '@nestjs/common'
50
+ import {ConfigModule} from '@nestjs/config'
51
+ import {DatabaseModule, JwtModule, RedisModule} from '@bearei/nest'
52
+
53
+ @Module({
54
+ imports: [ConfigModule.forRoot({isGlobal: true}), DatabaseModule, JwtModule, RedisModule]
55
+ })
56
+ export class AppModule {}
57
+ ```
58
+
59
+ 通过继承基础服务实现 CRUD 接口:
60
+
61
+ ```ts
62
+ import {Injectable} from '@nestjs/common'
63
+ import {InjectRepository} from '@nestjs/typeorm'
64
+ import {BaseService} from '@bearei/nest'
65
+ import {Repository} from 'typeorm'
66
+
67
+ import {User} from './user.entity'
68
+
69
+ @Injectable()
70
+ export class UserService extends BaseService<User> {
71
+ constructor(@InjectRepository(User) repository: Repository<User>) {
72
+ super(repository)
73
+ }
74
+ }
75
+ ```
76
+
77
+ JWT 相关配置读取自 `jwt.privateKey`(JWK 对象或 JSON 字符串)、`jwt.expiresIn`,以及可选的 `jwt.publicKey`。
78
+
79
+ ## 开发
80
+
81
+ ```bash
82
+ yarn install
83
+ yarn lint # ESLint,不自动修复
84
+ yarn typecheck # tsc --noEmit
85
+ yarn test # Jest,含覆盖率门槛
86
+ yarn build # 编译到 dist/
87
+ yarn verify # lint + typecheck + test + build
88
+ ```
89
+
90
+ 打包:
91
+
92
+ ```bash
93
+ yarn pack --dry-run # 查看将要发布的文件清单
94
+ yarn pack # 生成 tarball
95
+ ```
96
+
97
+ ## 发布
98
+
99
+ 发布由 CI 在版本 tag 上执行。修改 `package.json` 中的 `version`,推送同版本的 `v<version>` tag,发布工作流会自动校验 tag 与版本一致并发布到 npm。
100
+
101
+ ## 许可证
102
+
103
+ MIT
@@ -0,0 +1,12 @@
1
+ import type { Action, ActionResult, RequestHeaders } from '../common.interface';
2
+ export interface ActionLogOptions {
3
+ action: Action;
4
+ headers?: RequestHeaders;
5
+ isLogEnabled?: boolean;
6
+ message?: string;
7
+ raw: Record<string, unknown>;
8
+ serviceName: string;
9
+ }
10
+ export interface CreateActionLogOptions extends ActionLogOptions {
11
+ result: ActionResult;
12
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ export declare class ActionLogModule {
2
+ }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.ActionLogModule = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const external_1 = require("../external");
12
+ const action_log_service_1 = require("./action-log.service");
13
+ let ActionLogModule = class ActionLogModule {
14
+ };
15
+ exports.ActionLogModule = ActionLogModule;
16
+ exports.ActionLogModule = ActionLogModule = __decorate([
17
+ (0, common_1.Module)({
18
+ exports: [action_log_service_1.ActionLogService],
19
+ imports: [external_1.ExternalModule],
20
+ providers: [action_log_service_1.ActionLogService]
21
+ })
22
+ ], ActionLogModule);
@@ -0,0 +1,10 @@
1
+ import type { ActionLogOptions, CreateActionLogOptions } from './action-log.interface';
2
+ import { ExternalService } from '../external';
3
+ export declare class ActionLogService {
4
+ private readonly externalService;
5
+ private readonly logger;
6
+ constructor(externalService: ExternalService);
7
+ success(data: Record<string, unknown>, options: ActionLogOptions): Promise<void>;
8
+ failure(data: Record<string, unknown>, options: ActionLogOptions): Promise<void>;
9
+ create(data: Record<string, unknown>, { headers, isLogEnabled, ...restOptions }: CreateActionLogOptions): Promise<void>;
10
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var ActionLogService_1;
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.ActionLogService = void 0;
14
+ const common_1 = require("@nestjs/common");
15
+ const common_enum_1 = require("../common.enum");
16
+ const external_1 = require("../external");
17
+ let ActionLogService = ActionLogService_1 = class ActionLogService {
18
+ externalService;
19
+ logger = new common_1.Logger(ActionLogService_1.name);
20
+ constructor(externalService) {
21
+ this.externalService = externalService;
22
+ }
23
+ success(data, options) {
24
+ return this.create(data, { ...options, result: common_enum_1.ACTION_RESULT.SUCCESS });
25
+ }
26
+ failure(data, options) {
27
+ return this.create(data, { ...options, result: common_enum_1.ACTION_RESULT.FAILURE });
28
+ }
29
+ async create(data, { headers, isLogEnabled = false, ...restOptions }) {
30
+ if (!isLogEnabled || !headers) {
31
+ return;
32
+ }
33
+ await this.externalService
34
+ .createActionLog(headers, { ...restOptions, data })
35
+ .catch(error => this.logger.warn('Operation log enqueue failed.', String(error)));
36
+ }
37
+ };
38
+ exports.ActionLogService = ActionLogService;
39
+ exports.ActionLogService = ActionLogService = ActionLogService_1 = __decorate([
40
+ (0, common_1.Injectable)(),
41
+ __metadata("design:paramtypes", [external_1.ExternalService])
42
+ ], ActionLogService);
@@ -0,0 +1,3 @@
1
+ export * from './action-log.interface';
2
+ export * from './action-log.module';
3
+ export * from './action-log.service';
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./action-log.interface"), exports);
18
+ __exportStar(require("./action-log.module"), exports);
19
+ __exportStar(require("./action-log.service"), exports);
@@ -0,0 +1,7 @@
1
+ import type { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
2
+ import { Reflector } from '@nestjs/core';
3
+ export declare class AuthenticationContextInterceptor implements NestInterceptor {
4
+ private readonly reflector;
5
+ constructor(reflector: Reflector);
6
+ intercept(context: ExecutionContext, next: CallHandler): import("rxjs").Observable<any>;
7
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.AuthenticationContextInterceptor = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const core_1 = require("@nestjs/core");
15
+ const rxjs_1 = require("rxjs");
16
+ const decorators_1 = require("../decorators");
17
+ const exception_1 = require("../exception");
18
+ const utils_1 = require("../utils");
19
+ const DEFAULT_OMIT_ENTITY_KEYS = ['deletedAt', 'internalId', 'version'];
20
+ let AuthenticationContextInterceptor = class AuthenticationContextInterceptor {
21
+ reflector;
22
+ constructor(reflector) {
23
+ this.reflector = reflector;
24
+ }
25
+ intercept(context, next) {
26
+ const requiredOmitEntityKeys = this.reflector.getAllAndOverride(decorators_1.OMIT_ENTITY_KEYS, [
27
+ context.getClass(),
28
+ context.getHandler()
29
+ ]);
30
+ const request = context.switchToHttp().getRequest();
31
+ const sanitizeResponseData = (0, utils_1.createResponseSanitizer)([
32
+ ...(requiredOmitEntityKeys ?? []),
33
+ ...DEFAULT_OMIT_ENTITY_KEYS
34
+ ]);
35
+ if (request.isInternalAccess) {
36
+ return next
37
+ .handle()
38
+ .pipe((0, rxjs_1.map)(data => (data && typeof data === 'object' ? sanitizeResponseData(data) : data)));
39
+ }
40
+ const reservedHeaderValues = [
41
+ request.headers['x-bearei-client'],
42
+ request.headers['x-bearei-domain-id'],
43
+ request.headers['x-bearei-ip'],
44
+ request.headers['x-bearei-subject']
45
+ ];
46
+ if (!request.isSubscriptionEvaluated) {
47
+ reservedHeaderValues.push(request.headers['x-bearei-subscription']);
48
+ }
49
+ if (reservedHeaderValues.some(Boolean)) {
50
+ (0, exception_1.throwHttpException)(exception_1.HTTP_EXCEPTION.BAD_REQUEST, {
51
+ code: exception_1.HTTP_EXCEPTION_CODE.CONTAINS_INVALID_REQUEST_HEADERS,
52
+ message: 'Contains invalid request headers.'
53
+ });
54
+ }
55
+ const forwardedFor = request.headers['x-forwarded-for'];
56
+ const clientIp = typeof forwardedFor === 'string' ? forwardedFor.split(',')[0].trim()
57
+ : Array.isArray(forwardedFor) ? forwardedFor[0]
58
+ : undefined;
59
+ request.headers['x-bearei-client'] = request.audience;
60
+ request.headers['x-bearei-domain-id'] = request.issuer;
61
+ request.headers['x-bearei-ip'] = clientIp ?? request.headers['x-real-ip'];
62
+ request.headers['x-bearei-subject'] = request.subject;
63
+ return next.handle().pipe((0, rxjs_1.map)(data => (data && typeof data === 'object' ? sanitizeResponseData(data) : data)));
64
+ }
65
+ };
66
+ exports.AuthenticationContextInterceptor = AuthenticationContextInterceptor;
67
+ exports.AuthenticationContextInterceptor = AuthenticationContextInterceptor = __decorate([
68
+ (0, common_1.Injectable)(),
69
+ __metadata("design:paramtypes", [core_1.Reflector])
70
+ ], AuthenticationContextInterceptor);
@@ -0,0 +1,6 @@
1
+ export declare const ACCESS: {
2
+ readonly ALI_FC: "ALI_FC";
3
+ readonly APPLE_IAP: "APPLE_IAP";
4
+ readonly GUEST: "GUEST";
5
+ readonly USER: "USER";
6
+ };
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ACCESS = void 0;
4
+ exports.ACCESS = {
5
+ ALI_FC: 'ALI_FC',
6
+ APPLE_IAP: 'APPLE_IAP',
7
+ GUEST: 'GUEST',
8
+ USER: 'USER'
9
+ };
@@ -0,0 +1,13 @@
1
+ import type { CanActivate, ExecutionContext } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { Reflector } from '@nestjs/core';
4
+ import { JwtService } from '../jwt';
5
+ export declare class AuthenticationGuard implements CanActivate {
6
+ private readonly configService;
7
+ private readonly jwtService;
8
+ private readonly reflector;
9
+ constructor(configService: ConfigService, jwtService: JwtService, reflector: Reflector);
10
+ canActivate(context: ExecutionContext): Promise<boolean>;
11
+ private validatePayload;
12
+ private getHeader;
13
+ }
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.AuthenticationGuard = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const config_1 = require("@nestjs/config");
15
+ const core_1 = require("@nestjs/core");
16
+ const decorators_1 = require("../decorators");
17
+ const exception_1 = require("../exception");
18
+ const jwt_1 = require("../jwt");
19
+ const utils_1 = require("../utils");
20
+ const authentication_enum_1 = require("./authentication.enum");
21
+ let AuthenticationGuard = class AuthenticationGuard {
22
+ configService;
23
+ jwtService;
24
+ reflector;
25
+ constructor(configService, jwtService, reflector) {
26
+ this.configService = configService;
27
+ this.jwtService = jwtService;
28
+ this.reflector = reflector;
29
+ }
30
+ async canActivate(context) {
31
+ const allowedAccessTypes = this.reflector.getAllAndOverride(decorators_1.ALLOWED_ACCESS_TYPES, [
32
+ context.getClass(),
33
+ context.getHandler()
34
+ ]) ?? [];
35
+ const isPublic = [authentication_enum_1.ACCESS.ALI_FC, authentication_enum_1.ACCESS.APPLE_IAP].some(accessType => allowedAccessTypes.includes(accessType));
36
+ if (isPublic) {
37
+ return true;
38
+ }
39
+ const request = context.switchToHttp().getRequest();
40
+ const headers = request.headers;
41
+ const authorizationHeader = headers['x-bearei-internal-authorization'] ? 'x-bearei-internal-authorization' : 'authorization';
42
+ const token = (0, utils_1.extractBearerToken)(headers, authorizationHeader);
43
+ if (!token) {
44
+ return (0, exception_1.throwHttpException)(exception_1.HTTP_EXCEPTION.UNAUTHORIZED, {
45
+ code: exception_1.HTTP_EXCEPTION_CODE.TOKEN_IS_NOT_FOUND,
46
+ message: 'Authentication token is not found.'
47
+ });
48
+ }
49
+ const payload = await this.jwtService.verifyAsync(token);
50
+ return this.validatePayload(request, allowedAccessTypes, payload);
51
+ }
52
+ validatePayload(request, accessTypes, payload) {
53
+ const { aud: audience, iss: issuer, sub: subject } = payload;
54
+ if (request.headers['x-bearei-internal-authorization']) {
55
+ const secret = `${this.configService.get('service.accessKey')}:${this.configService.get('service.secretKey')}`;
56
+ const isSignatureValid = (0, utils_1.verifyHmacSignature)(secret, { serviceName: this.configService.get('service.name') }, subject);
57
+ if (!isSignatureValid) {
58
+ (0, exception_1.throwHttpException)(exception_1.HTTP_EXCEPTION.UNAUTHORIZED, {
59
+ code: exception_1.HTTP_EXCEPTION_CODE.INTERNAL_SERVER_SIGNATURE_VERIFICATION_FAILED,
60
+ message: 'Internal service signature verification failed.'
61
+ });
62
+ }
63
+ request.isInternalAccess = true;
64
+ request.audience = this.getHeader(request, 'x-bearei-client');
65
+ request.issuer = this.getHeader(request, 'x-bearei-domain-id');
66
+ request.subject = this.getHeader(request, 'x-bearei-subject');
67
+ return true;
68
+ }
69
+ const isGuestAccessAllowed = accessTypes.includes(authentication_enum_1.ACCESS.GUEST);
70
+ if (subject === authentication_enum_1.ACCESS.GUEST && !isGuestAccessAllowed) {
71
+ (0, exception_1.throwHttpException)(exception_1.HTTP_EXCEPTION.UNAUTHORIZED, {
72
+ code: exception_1.HTTP_EXCEPTION_CODE.ACCESS_DENIED_FOR_GUEST,
73
+ message: 'Access denied for guest users.'
74
+ });
75
+ }
76
+ request.audience = audience;
77
+ request.issuer = issuer;
78
+ request.subject = subject;
79
+ return Boolean(subject);
80
+ }
81
+ getHeader(request, key) {
82
+ const value = request.headers[key];
83
+ return Array.isArray(value) ? value[0] : value;
84
+ }
85
+ };
86
+ exports.AuthenticationGuard = AuthenticationGuard;
87
+ exports.AuthenticationGuard = AuthenticationGuard = __decorate([
88
+ (0, common_1.Injectable)(),
89
+ __metadata("design:paramtypes", [config_1.ConfigService,
90
+ jwt_1.JwtService,
91
+ core_1.Reflector])
92
+ ], AuthenticationGuard);
@@ -0,0 +1,2 @@
1
+ import type { ACCESS } from './authentication.enum';
2
+ export type AccessType = (typeof ACCESS)[keyof typeof ACCESS];
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });