@deplois/api 0.0.0-stage → 0.0.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 (56) hide show
  1. package/README.md +176 -2
  2. package/dist/api.module.d.ts +10 -0
  3. package/dist/api.module.js +58 -0
  4. package/dist/controller/session.controller.d.ts +30 -0
  5. package/dist/controller/session.controller.js +115 -0
  6. package/dist/create-api.d.ts +16 -0
  7. package/dist/create-api.js +113 -0
  8. package/dist/database/client.d.ts +6 -0
  9. package/dist/database/client.js +29 -0
  10. package/dist/database/identity.store.d.ts +12 -0
  11. package/dist/database/identity.store.js +91 -0
  12. package/dist/database/index.d.ts +5 -0
  13. package/dist/database/index.js +21 -0
  14. package/dist/database/metadata.d.ts +1 -0
  15. package/dist/database/metadata.js +21 -0
  16. package/dist/database/session.entity.d.ts +11 -0
  17. package/dist/database/session.entity.js +62 -0
  18. package/dist/database/user.entity.d.ts +10 -0
  19. package/dist/database/user.entity.js +54 -0
  20. package/dist/identity/identity.plugin.d.ts +31 -0
  21. package/dist/identity/identity.plugin.js +161 -0
  22. package/dist/identity/identity.service.d.ts +33 -0
  23. package/dist/identity/identity.service.js +36 -0
  24. package/dist/identity/interface.d.ts +49 -0
  25. package/dist/identity/interface.js +2 -0
  26. package/dist/index.d.ts +19 -0
  27. package/dist/index.js +40 -0
  28. package/dist/interface.d.ts +20 -0
  29. package/dist/interface.js +2 -0
  30. package/dist/options.d.ts +23 -0
  31. package/dist/options.js +4 -0
  32. package/dist/session.module.d.ts +2 -0
  33. package/dist/session.module.js +18 -0
  34. package/dist/system/decorator/raw-response.decorator.d.ts +2 -0
  35. package/dist/system/decorator/raw-response.decorator.js +7 -0
  36. package/dist/system/error/app.exception.d.ts +6 -0
  37. package/dist/system/error/app.exception.js +12 -0
  38. package/dist/system/error/error-code.enum.d.ts +11 -0
  39. package/dist/system/error/error-code.enum.js +15 -0
  40. package/dist/system/error/error.plugin.d.ts +14 -0
  41. package/dist/system/error/error.plugin.js +108 -0
  42. package/dist/system/filter/http-exception.filter.d.ts +9 -0
  43. package/dist/system/filter/http-exception.filter.js +49 -0
  44. package/dist/system/guard/session.guard.d.ts +13 -0
  45. package/dist/system/guard/session.guard.js +64 -0
  46. package/dist/system/guard/user.guard.d.ts +13 -0
  47. package/dist/system/guard/user.guard.js +46 -0
  48. package/dist/system/interceptor/response.interceptor.d.ts +9 -0
  49. package/dist/system/interceptor/response.interceptor.js +44 -0
  50. package/dist/system/logger/file.logger.d.ts +38 -0
  51. package/dist/system/logger/file.logger.js +116 -0
  52. package/dist/system/logger/request.logger.d.ts +25 -0
  53. package/dist/system/logger/request.logger.js +99 -0
  54. package/dist/system/pipe/body-validation.pipe.d.ts +7 -0
  55. package/dist/system/pipe/body-validation.pipe.js +71 -0
  56. package/package.json +45 -4
package/README.md CHANGED
@@ -1,3 +1,177 @@
1
- # Temporary Holding Version
1
+ # @deplois/api
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
+ NestJS 11 공통 API 모듈. 사용자와 세션을 관리하며 실제 로그인 인증 및 OAuth는 외부 모듈에서 처리합니다. Node.js 22 이상을 사용합니다.
4
+
5
+ ## 구성
6
+
7
+ - `APIModule`: 응답 포맷, DTO Body 검증, 예외 처리, 요청 로그.
8
+ - `SessionModule`: 선택적으로 등록하는 `/session` API.
9
+ - `IdentityService`: `userPlugin`, `sessionPlugin` 접근.
10
+ - `SessionGuard`: 유효한 세션을 확인하고 `request.session`에 저장.
11
+ - `UserGuard`: 세션 검증 후 활성 사용자가 최소 1명인지 확인하고 `request.users`에 저장.
12
+ - `@deplois/api/database`: `UserEntity`, `SessionEntity`, `TypeORMIdentityStore`, `createDatabaseClient`, `mergeMetadata`.
13
+
14
+ 별도 AccessToken은 사용하지 않습니다. 세션 하나에 여러 사용자 ID를 연결할 수 있습니다. 실제 사용자 정보는 사용자 테이블에서 조회하므로 세션 metadata에 개인정보를 복제하지 않습니다.
15
+
16
+ ## DB 연결
17
+
18
+ 기존 Nest 앱에 패키지와 필요한 peer dependency를 설치합니다. PostgreSQL 저장소를 쓸 경우 `typeorm`, `pg`도 설치합니다. 현재 TypeORM 지원 버전은 `^1.1.0`입니다.
19
+
20
+ ```typescript
21
+ import { Module } from '@nestjs/common'
22
+ import { APIModule, SessionModule, createIdentityPlugins } from '@deplois/api'
23
+ import { createDatabaseClient, TypeORMIdentityStore, UserEntity, SessionEntity } from '@deplois/api/database'
24
+
25
+ export const databaseClient = createDatabaseClient({
26
+ type: 'postgres',
27
+ url: process.env.DATABASE_URL,
28
+ entities: [UserEntity, SessionEntity],
29
+ synchronize: false
30
+ })
31
+
32
+ @Module({
33
+ imports: [
34
+ APIModule.forRootAsync({
35
+ useFactory: async () => ({
36
+ identity: createIdentityPlugins(new TypeORMIdentityStore(await databaseClient.connect()), {
37
+ sessionDuration: 60 * 60 * 24
38
+ })
39
+ })
40
+ }),
41
+ SessionModule
42
+ ]
43
+ })
44
+ export class AppModule {}
45
+ ```
46
+
47
+ 기존 DataSource를 `new TypeORMIdentityStore(dataSource)`에 직접 전달해도 됩니다. DataSource의 entities에는 두 Entity를 모두 등록합니다. 테이블 이름은 `api_user`, `api_session`, `api_session_users`입니다. 스키마 생성/변경은 앱의 TypeORM migration으로 적용하세요. 모듈은 기존 DB에 자동으로 synchronize하지 않습니다.
48
+
49
+ `databaseClient.get()`으로 연결된 DataSource와 repository/manager를 사용할 수 있습니다. 앱 종료 시 `databaseClient.disconnect()`를 호출하세요. Repository를 직접 수정하면 플러그인의 metadata 병합·비활성화 규칙은 적용되지 않습니다.
50
+
51
+ ## 사용자 등록 및 세션 로그인
52
+
53
+ ```typescript
54
+ constructor(private readonly identity: IdentityService) {}
55
+
56
+ // 서버에서 확인한 사용자 정보로 등록
57
+ const userResult = await this.identity.userPlugin.create({
58
+ profile: { fullName: '민재', emailAddress: 'minjae@example.com' },
59
+ metadata: {}
60
+ })
61
+ if (!userResult.success) throw userResult.error
62
+
63
+ // 별도 인증 모듈에서 로그인 검증을 마친 뒤 실행
64
+ const result = await this.identity.sessionPlugin.addUser(sessionToken, userResult.data.id)
65
+ if (!result.success) throw result.error
66
+
67
+ return { token: result.data.token }
68
+ ```
69
+
70
+ `addUser`는 로그인 시 세션 토큰을 교체합니다. 응답의 새 토큰을 클라이언트에 저장해야 하며 이전 토큰은 더 이상 사용할 수 없습니다. 클라이언트가 보낸 userId만으로 호출하는 공개 로그인 API를 만들지 마세요. 인증된 userId를 별도 OAuth 모듈에서 전달합니다.
71
+
72
+ - `userPlugin.create({ profile?, metadata? })`
73
+ - `userPlugin.get(userId)`
74
+ - `userPlugin.update(userId, { profile?, metadata? })`
75
+ - `userPlugin.disable(userId)`: `is_active=false`, `deleted_date` 설정, 이후 로그인 사용자에서 제외.
76
+ - `sessionPlugin.create({ userId?, metadata? })`: 익명 또는 인증 완료된 사용자 세션 생성.
77
+ - `sessionPlugin.get(token)`: 검증 및 자동 만료 연장. 이미 만료된 세션은 연장하지 않음.
78
+ - `sessionPlugin.addUser(token, userId)`: 사용자 연결 및 토큰 교체.
79
+ - `sessionPlugin.removeUser(token, userId)`: 해당 계정만 로그아웃.
80
+ - `sessionPlugin.revoke(token)`: 세션 전체 폐기.
81
+
82
+ 플러그인 결과는 `{ success: true, data }` 또는 `{ success: false, error }`입니다. 토큰은 128자 base64url 난수이며 DB에는 SHA-256 해시만 저장합니다.
83
+
84
+ `UserGuard`는 복수 사용자 목록을 제공합니다. 특정 사용자 작업에서는 대상 userId가 `request.users`에 속하는지 추가로 확인해야 합니다. 첫 번째 사용자를 임의로 현재 사용자로 취급하지 않습니다.
85
+
86
+ ## 세션 API
87
+
88
+ `SessionModule`을 import한 경우에만 생성됩니다. 앱의 global prefix가 있으면 그대로 적용됩니다.
89
+
90
+ | 메서드 | 경로 | data |
91
+ | --- | --- | --- |
92
+ | PUT | `/session` | `{ id, token, expires_in }` (201) |
93
+ | GET | `/session` | `{ expires_in }` |
94
+ | GET | `/session/users` | `{ users: [{ id, profile, created_at }] }` |
95
+ | DELETE | `/session/user/:userId` | `null` |
96
+ | DELETE | `/session` | `null` |
97
+
98
+ PUT 외 요청에는 `Authorization: Bearer <session token>`을 전달합니다. `profile` 응답은 `full_name`, `email_address`, `phone_number`, `image_url`입니다. 기존 API의 사용자 응답 전체를 그대로 복제한 것은 아닙니다.
99
+
100
+ ## 응답 및 오류
101
+
102
+ ```typescript
103
+ { success: true, data: {}, error: null }
104
+ { success: false, data: null, error: { code: 'WRONG_PARAMETER', message: 'Invalid parameter.' } }
105
+ ```
106
+
107
+ Body DTO는 class-validator로 검증합니다. 필수 필드 누락은 `A required parameter is missing.`, 잘못된 값/미등록 필드는 `Invalid parameter.`입니다. Query/Param 검증은 각 컨트롤러의 pipe로 지정합니다.
108
+
109
+ ```typescript
110
+ throw errorPlugin.createException('WRONG_SESSION', 'Wrong session.', 400, { cause: originalError })
111
+ ```
112
+
113
+ cause는 응답에 노출되지 않습니다. `APIModule`의 `onError(error, context)`에서 서버 오류 추적 시스템으로 전달할 수 있습니다. 알 수 없는 오류 내용도 기본적으로 숨깁니다.
114
+
115
+ `@RawResponse()`는 성공 응답의 래핑만 해제합니다. OAuth 표준 오류 응답이 필요한 컨트롤러는 별도 예외 필터를 사용하세요.
116
+
117
+ ## 로그 및 metadata
118
+
119
+ 요청 로그는 기본적으로 경로·상태·시간만 기록하며 헤더/쿼리/본문을 기록하지 않습니다. `logging: false`로 끄거나 `logging: { bodies: true, write: entry => ... }`로 설정합니다. 본문을 켜면 자격증명 관련 키는 필터링합니다. 필터만 믿고 민감한 본문을 기록하지 마세요.
120
+
121
+ `userPlugin.update`의 metadata/profile은 기존 키를 유지하고 전달한 키만 병합합니다. `undefined`는 해당 키 삭제, `null`은 null 저장, 배열은 교체입니다. 세션 사용자 연결과 만료/폐기 변경은 TypeORM transaction에서 처리합니다.
122
+
123
+ `IdentityStore` 인터페이스를 구현하면 다른 저장소도 쓸 수 있습니다. 세션 update는 현재 토큰 해시와 만료/폐기 상태를 원자적으로 확인해야 합니다.
124
+
125
+ ## 개발
126
+
127
+ ```sh
128
+ npm run check
129
+ npm test
130
+ npm pack
131
+ ```
132
+
133
+ 테스트는 메모리 저장소를 이용한 Nest HTTP 통합 테스트와 세션 수명주기 테스트입니다. 실제 PostgreSQL 연결/동시성 테스트는 별도로 수행해야 합니다. 패키지는 아직 게시하지 않았습니다.
134
+
135
+ ## 파일 로그와 CLI
136
+
137
+ ```typescript
138
+ import { createAPI } from '@deplois/api'
139
+ import { AppModule } from './app.module'
140
+
141
+ async function bootstrap() {
142
+ const api = await createAPI(AppModule, {
143
+ port: 4000,
144
+ host: '127.0.0.1',
145
+ name: 'deplois API',
146
+ logs: { directory: 'logs', maxFileSize: 10 * 1024 * 1024, retentionDays: 30 }
147
+ })
148
+ api.app.setGlobalPrefix('v1')
149
+ await api.listen()
150
+ }
151
+
152
+ void bootstrap().catch(() => { process.exitCode = 1 })
153
+ ```
154
+
155
+ `APIModule`과 함께 사용하면 Nest 로그·HTTP 요청 로그·처리된 요청 오류를 콘솔 대신 `logs/api-<pid>-YYYY-MM-DD-<part>.jsonl`에 기록합니다. 날짜는 UTC 기준입니다. 각 행은 JSON이고, 용량 기준으로 분할됩니다. 보관 기간 정리는 시작 후 첫 기록과 날짜 변경 시 해당 이름의 로그 파일에만 적용합니다. `logs/`는 Git에서 제외하세요.
156
+
157
+ TTY에서는 요청 수, 최근 1,000건의 응답 시간 중앙값, 오류/경고 로그 수, 실행 시간과 주소를 표시합니다. `s` 요약, `l` 최근 로그, `n` 최근 요청, `r` 통계 초기화, `?` 도움말, `q` 정상 종료입니다. `r`은 서버 재시작이 아닙니다. 로그/요청 보기는 최근 기록 일부이며 전체 기록은 파일에 있습니다.
158
+
159
+ `cli: false` 또는 터미널이 없는 환경에서는 CLI를 표시하지 않습니다. SIGINT/SIGTERM 및 `api.close()`는 Nest 종료 훅을 실행하고 대기 중인 파일 기록을 마칩니다. DB 정리는 앱의 종료 훅에서 처리하세요. `console.log`나 별도 라이브러리의 stdout 출력은 가로채지 않습니다. Nest `Logger` 또는 `api.logger`를 사용하세요.
160
+
161
+ 기본 본문 기록 정책은 그대로 유지합니다. `APIModule`에 `logging: { bodies: true }`를 설정해야 본문도 기록합니다. 사용자 지정 `logging.write`는 기본 HTTP logger를 대체하므로 파일 기록/CLI 요청 통계도 직접 연결해야 합니다. 문자열로 출력한 비밀번호나 오류 메시지 속 비밀 값까지 자동 판별하지는 않습니다.
162
+
163
+ 디스크 기록 실패/쓰기 대기열 초과는 `failedWrites`와 CLI에 표시합니다. 대기열은 메모리 보호를 위해 최대 10,000건으로 제한하며 초과 로그는 버립니다. 비대화형 환경의 모니터링에서는 `api.logger.failedWrites`를 확인할 수 있습니다.
164
+
165
+
166
+ ## 로컬 배포 스크립트
167
+
168
+ ```sh
169
+ npm run publish -- fetch "수정 내용" --deploy
170
+ npm run publish -- minor "기능 추가" --deploy
171
+ npm run publish -- major "호환성 변경" --deploy
172
+ npm run publish -- 1.2.3 "릴리스 내용" --deploy
173
+ ```
174
+
175
+ `fetch`는 세 번째 버전을 1 올립니다. minor/major를 올리면 하위 버전은 0으로 초기화합니다. 테스트·빌드 후 버전을 변경하고 변경 파일 전체를 commit → push → npmjs 공개 배포합니다. `--deploy`를 빼면 commit·push까지만 진행합니다. `npm run deploy`는 현재 버전을 테스트·빌드해서 npm에만 배포합니다.
176
+
177
+ 각 패키지 폴더에 별도 Git 저장소와 origin이 설정되어 있어야 합니다. 상위 저장소를 대신 commit하지 않습니다. npm 배포 시에는 npmjs 로그인 및 배포 권한이 필요합니다. 스크립트 자체는 template과 동일하게 Git 및 npm 패키지에서 제외하는 로컬 파일입니다.
@@ -0,0 +1,10 @@
1
+ import { DynamicModule, MiddlewareConsumer, NestModule } from '@nestjs/common';
2
+ import { APIAsyncOptions, APIOptions } from './options';
3
+ import { RequestLogger } from './system/logger/request.logger';
4
+ export declare class APIModule implements NestModule {
5
+ private readonly logger;
6
+ static forRoot(options?: APIOptions): DynamicModule;
7
+ static forRootAsync(options: APIAsyncOptions): DynamicModule;
8
+ constructor(logger: RequestLogger);
9
+ configure(consumer: MiddlewareConsumer): void;
10
+ }
@@ -0,0 +1,58 @@
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 APIModule_1;
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.APIModule = void 0;
14
+ const common_1 = require("@nestjs/common");
15
+ const core_1 = require("@nestjs/core");
16
+ const options_1 = require("./options");
17
+ const http_exception_filter_1 = require("./system/filter/http-exception.filter");
18
+ const response_interceptor_1 = require("./system/interceptor/response.interceptor");
19
+ const body_validation_pipe_1 = require("./system/pipe/body-validation.pipe");
20
+ const request_logger_1 = require("./system/logger/request.logger");
21
+ const identity_service_1 = require("./identity/identity.service");
22
+ const user_guard_1 = require("./system/guard/user.guard");
23
+ const session_guard_1 = require("./system/guard/session.guard");
24
+ let APIModule = APIModule_1 = class APIModule {
25
+ logger;
26
+ static forRoot(options = {}) {
27
+ return this.forRootAsync({ useFactory: () => options });
28
+ }
29
+ static forRootAsync(options) {
30
+ return {
31
+ module: APIModule_1,
32
+ global: true,
33
+ imports: options.imports ?? [],
34
+ providers: [
35
+ { provide: options_1.API_OPTIONS, useFactory: options.useFactory, inject: options.inject ?? [] },
36
+ request_logger_1.RequestLogger,
37
+ session_guard_1.SessionGuard,
38
+ user_guard_1.UserGuard,
39
+ identity_service_1.IdentityService,
40
+ { provide: core_1.APP_PIPE, useClass: body_validation_pipe_1.BodyValidationPipe },
41
+ { provide: core_1.APP_FILTER, useClass: http_exception_filter_1.HTTPExceptionFilter },
42
+ { provide: core_1.APP_INTERCEPTOR, useClass: response_interceptor_1.ResponseInterceptor }
43
+ ],
44
+ exports: [options_1.API_OPTIONS, session_guard_1.SessionGuard, user_guard_1.UserGuard, identity_service_1.IdentityService]
45
+ };
46
+ }
47
+ constructor(logger) {
48
+ this.logger = logger;
49
+ }
50
+ configure(consumer) {
51
+ consumer.apply(this.logger.use.bind(this.logger)).forRoutes('{*path}');
52
+ }
53
+ };
54
+ exports.APIModule = APIModule;
55
+ exports.APIModule = APIModule = APIModule_1 = __decorate([
56
+ (0, common_1.Module)({}),
57
+ __metadata("design:paramtypes", [request_logger_1.RequestLogger])
58
+ ], APIModule);
@@ -0,0 +1,30 @@
1
+ import type { Request as ExpressRequest, Response } from 'express';
2
+ import { IdentityService } from '../identity/identity.service';
3
+ import { SessionRequest } from '../system/guard/session.guard';
4
+ import type { Session } from '../identity/interface';
5
+ export declare class SessionController {
6
+ private readonly identity;
7
+ constructor(identity: IdentityService);
8
+ create(request: ExpressRequest, response: Response): Promise<{
9
+ id: string;
10
+ token: string;
11
+ expires_in: number;
12
+ }>;
13
+ get(request: SessionRequest<Session>, response: Response): {
14
+ expires_in: number;
15
+ };
16
+ users(request: SessionRequest<Session>, response: Response): Promise<{
17
+ users: {
18
+ id: string;
19
+ profile: {
20
+ full_name: string | null;
21
+ email_address: string | null;
22
+ phone_number: string | null;
23
+ image_url: string | null;
24
+ };
25
+ created_at: string;
26
+ }[];
27
+ }>;
28
+ removeUser(request: ExpressRequest, userId: string): Promise<null>;
29
+ revoke(request: ExpressRequest): Promise<null>;
30
+ }
@@ -0,0 +1,115 @@
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 __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SessionController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const app_exception_1 = require("../system/error/app.exception");
18
+ const identity_service_1 = require("../identity/identity.service");
19
+ const session_guard_1 = require("../system/guard/session.guard");
20
+ let SessionController = class SessionController {
21
+ identity;
22
+ constructor(identity) {
23
+ this.identity = identity;
24
+ }
25
+ async create(request, response) {
26
+ const result = await this.identity.sessionPlugin.create({ metadata: { ip_address: request.ip, user_agent: request.headers['user-agent'] ?? null } });
27
+ if (!result.success)
28
+ throw result.error;
29
+ response.setHeader('Cache-Control', 'no-store');
30
+ return { id: result.data.session.id, token: result.data.token, expires_in: Math.max(0, Math.floor((result.data.session.expiresDate.getTime() - Date.now()) / 1000)) };
31
+ }
32
+ get(request, response) {
33
+ response.setHeader('Cache-Control', 'no-store');
34
+ return { expires_in: Math.max(0, Math.floor((request.session.expiresDate.getTime() - Date.now()) / 1000)) };
35
+ }
36
+ async users(request, response) {
37
+ response.setHeader('Cache-Control', 'no-store');
38
+ const results = await Promise.all(request.session.userIds.map(id => this.identity.userPlugin.get(id)));
39
+ for (const result of results) {
40
+ if (!result.success && !(result.error instanceof app_exception_1.AppException && result.error.error.code === 'USER_NOT_FOUND'))
41
+ throw result.error;
42
+ }
43
+ return { users: results.flatMap(result => result.success ? [{
44
+ id: result.data.id,
45
+ profile: {
46
+ full_name: result.data.profile.fullName ?? null,
47
+ email_address: result.data.profile.emailAddress ?? null,
48
+ phone_number: result.data.profile.phoneNumber ?? null,
49
+ image_url: result.data.profile.imageUrl ?? null
50
+ },
51
+ created_at: result.data.createdDate.toISOString()
52
+ }] : []) };
53
+ }
54
+ async removeUser(request, userId) {
55
+ const result = await this.identity.sessionPlugin.removeUser((0, session_guard_1.getBearerToken)(request.headers.authorization), userId);
56
+ if (!result.success)
57
+ throw result.error;
58
+ return null;
59
+ }
60
+ async revoke(request) {
61
+ const result = await this.identity.sessionPlugin.revoke((0, session_guard_1.getBearerToken)(request.headers.authorization));
62
+ if (!result.success)
63
+ throw result.error;
64
+ return null;
65
+ }
66
+ };
67
+ exports.SessionController = SessionController;
68
+ __decorate([
69
+ (0, common_1.Put)(),
70
+ (0, common_1.HttpCode)(201),
71
+ __param(0, (0, common_1.Request)()),
72
+ __param(1, (0, common_1.Res)({ passthrough: true })),
73
+ __metadata("design:type", Function),
74
+ __metadata("design:paramtypes", [Object, Object]),
75
+ __metadata("design:returntype", Promise)
76
+ ], SessionController.prototype, "create", null);
77
+ __decorate([
78
+ (0, common_1.Get)(),
79
+ (0, common_1.UseGuards)(session_guard_1.SessionGuard),
80
+ __param(0, (0, common_1.Request)()),
81
+ __param(1, (0, common_1.Res)({ passthrough: true })),
82
+ __metadata("design:type", Function),
83
+ __metadata("design:paramtypes", [Object, Object]),
84
+ __metadata("design:returntype", void 0)
85
+ ], SessionController.prototype, "get", null);
86
+ __decorate([
87
+ (0, common_1.Get)('users'),
88
+ (0, common_1.UseGuards)(session_guard_1.SessionGuard),
89
+ __param(0, (0, common_1.Request)()),
90
+ __param(1, (0, common_1.Res)({ passthrough: true })),
91
+ __metadata("design:type", Function),
92
+ __metadata("design:paramtypes", [Object, Object]),
93
+ __metadata("design:returntype", Promise)
94
+ ], SessionController.prototype, "users", null);
95
+ __decorate([
96
+ (0, common_1.Delete)('user/:userId'),
97
+ (0, common_1.UseGuards)(session_guard_1.SessionGuard),
98
+ __param(0, (0, common_1.Request)()),
99
+ __param(1, (0, common_1.Param)('userId', new common_1.ParseUUIDPipe())),
100
+ __metadata("design:type", Function),
101
+ __metadata("design:paramtypes", [Object, String]),
102
+ __metadata("design:returntype", Promise)
103
+ ], SessionController.prototype, "removeUser", null);
104
+ __decorate([
105
+ (0, common_1.Delete)(),
106
+ (0, common_1.UseGuards)(session_guard_1.SessionGuard),
107
+ __param(0, (0, common_1.Request)()),
108
+ __metadata("design:type", Function),
109
+ __metadata("design:paramtypes", [Object]),
110
+ __metadata("design:returntype", Promise)
111
+ ], SessionController.prototype, "revoke", null);
112
+ exports.SessionController = SessionController = __decorate([
113
+ (0, common_1.Controller)('session'),
114
+ __metadata("design:paramtypes", [identity_service_1.IdentityService])
115
+ ], SessionController);
@@ -0,0 +1,16 @@
1
+ import type { NestApplicationOptions, Type } from '@nestjs/common';
2
+ import { FileLogger, FileLoggerOptions } from './system/logger/file.logger';
3
+ export interface CreateAPIOptions {
4
+ port?: number;
5
+ host?: string;
6
+ name?: string;
7
+ cli?: boolean;
8
+ logs?: FileLoggerOptions;
9
+ nest?: Omit<NestApplicationOptions, 'logger' | 'abortOnError'>;
10
+ }
11
+ export declare function createAPI(module: Type<unknown>, options?: CreateAPIOptions): Promise<{
12
+ app: import("@nestjs/common").INestApplication<any>;
13
+ logger: FileLogger;
14
+ close: () => Promise<void>;
15
+ listen(): Promise<void>;
16
+ }>;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createAPI = createAPI;
4
+ const core_1 = require("@nestjs/core");
5
+ const node_readline_1 = require("node:readline");
6
+ const file_logger_1 = require("./system/logger/file.logger");
7
+ async function createAPI(module, options = {}) {
8
+ const logger = new file_logger_1.FileLogger(options.logs);
9
+ const started = Date.now();
10
+ let app;
11
+ try {
12
+ app = await core_1.NestFactory.create(module, { ...options.nest, logger, abortOnError: false });
13
+ }
14
+ catch (error) {
15
+ logger.error(error);
16
+ await logger.close();
17
+ throw error;
18
+ }
19
+ let timer;
20
+ let view = 'summary';
21
+ let screenActive = false;
22
+ let address = '';
23
+ let closing;
24
+ const interactive = options.cli !== false && !!process.stdout.isTTY && !!process.stdin.isTTY;
25
+ const wasRaw = process.stdin.isRaw;
26
+ const wasPaused = process.stdin.isPaused();
27
+ const clean = (text) => text.replace(/[\x00-\x1f\x7f-\x9f]/g, ' ');
28
+ const render = () => {
29
+ const width = Math.max(20, (process.stdout.columns || 80) - 1);
30
+ const rows = [
31
+ `${options.name ?? 'deplois API'} · ${Math.floor((Date.now() - started) / 1000)}s`,
32
+ `Local ${address}`,
33
+ `${logger.requests} requests · ${logger.median}ms median (last 1000) · ${logger.errors} errors · ${logger.warnings} warnings`,
34
+ `Logs ${logger.directory}${logger.failedWrites ? ` · ${logger.failedWrites} failed/dropped writes` : ''}`,
35
+ ''
36
+ ];
37
+ if (view === 'logs')
38
+ rows.push(...logger.recent.slice(-8));
39
+ if (view === 'network')
40
+ rows.push(...logger.network.slice(-8).map(entry => `${entry.request.method} ${entry.request.path} ${entry.response.status_code} ${entry.response.duration_ms}ms`));
41
+ if (view === 'help')
42
+ rows.push('s summary · l recent logs · n recent requests · r reset statistics · q graceful shutdown');
43
+ rows.push('', 's summary · l logs · n network · r reset · ? help · q quit');
44
+ process.stdout.write('\x1b[H\x1b[2J' + rows.slice(0, Math.max(5, (process.stdout.rows || 24) - 1)).map(row => clean(row).slice(0, width)).join('\n'));
45
+ };
46
+ const close = () => {
47
+ if (closing)
48
+ return closing;
49
+ closing = (async () => {
50
+ if (timer)
51
+ clearInterval(timer);
52
+ if (screenActive) {
53
+ process.stdin.off('keypress', keypress);
54
+ process.stdin.setRawMode(wasRaw ?? false);
55
+ if (wasPaused)
56
+ process.stdin.pause();
57
+ process.stdout.write('\x1b[?25h\x1b[?1049l');
58
+ }
59
+ process.off('SIGINT', shutdown);
60
+ process.off('SIGTERM', shutdown);
61
+ try {
62
+ await app.close();
63
+ }
64
+ finally {
65
+ await logger.close();
66
+ }
67
+ })();
68
+ return closing;
69
+ };
70
+ const shutdown = () => { void close().catch(() => { process.exitCode = 1; }); };
71
+ const keypress = (text, key) => {
72
+ if (text === 'q' || (key.ctrl && key.name === 'c'))
73
+ return shutdown();
74
+ if (text === 'r')
75
+ logger.reset();
76
+ if (text === 'l')
77
+ view = 'logs';
78
+ if (text === 'n')
79
+ view = 'network';
80
+ if (text === 's')
81
+ view = 'summary';
82
+ if (text === '?')
83
+ view = 'help';
84
+ render();
85
+ };
86
+ return {
87
+ app, logger, close,
88
+ async listen() {
89
+ try {
90
+ await app.listen(options.port ?? 4000, options.host ?? '127.0.0.1');
91
+ address = await app.getUrl();
92
+ process.once('SIGINT', shutdown);
93
+ process.once('SIGTERM', shutdown);
94
+ if (interactive) {
95
+ screenActive = true;
96
+ process.stdout.write('\x1b[?1049h\x1b[?25l');
97
+ (0, node_readline_1.emitKeypressEvents)(process.stdin);
98
+ process.stdin.setRawMode(true);
99
+ process.stdin.resume();
100
+ process.stdin.on('keypress', keypress);
101
+ timer = setInterval(render, 1000);
102
+ timer.unref();
103
+ render();
104
+ }
105
+ }
106
+ catch (error) {
107
+ logger.error(error);
108
+ await close();
109
+ throw error;
110
+ }
111
+ }
112
+ };
113
+ }
@@ -0,0 +1,6 @@
1
+ import { DataSource, DataSourceOptions } from 'typeorm';
2
+ export declare function createDatabaseClient(options: DataSourceOptions): {
3
+ get(): DataSource;
4
+ connect(): Promise<DataSource>;
5
+ disconnect(): Promise<void>;
6
+ };
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDatabaseClient = createDatabaseClient;
4
+ const typeorm_1 = require("typeorm");
5
+ // One explicit client per application; no import-time connections or hidden global singleton.
6
+ function createDatabaseClient(options) {
7
+ const database = new typeorm_1.DataSource(options);
8
+ let connecting = null;
9
+ return {
10
+ get() {
11
+ if (!database.isInitialized)
12
+ throw new Error('Database client is not initialized.');
13
+ return database;
14
+ },
15
+ async connect() {
16
+ if (database.isInitialized)
17
+ return database;
18
+ if (!connecting)
19
+ connecting = database.initialize().finally(() => { connecting = null; });
20
+ return connecting;
21
+ },
22
+ async disconnect() {
23
+ if (connecting)
24
+ await connecting;
25
+ if (database.isInitialized)
26
+ await database.destroy();
27
+ }
28
+ };
29
+ }
@@ -0,0 +1,12 @@
1
+ import { DataSource } from 'typeorm';
2
+ import type { IdentityStore, Session, SessionUpdate, User } from '../identity/interface';
3
+ export declare class TypeORMIdentityStore implements IdentityStore {
4
+ private readonly database;
5
+ constructor(database: DataSource);
6
+ createUser(user: User): Promise<void>;
7
+ getUser(id: string): Promise<User | null>;
8
+ updateUser(id: string, patch: Pick<Partial<User>, 'profile' | 'metadata' | 'isActive'>, now: Date): Promise<User | null>;
9
+ createSession(session: Session): Promise<void>;
10
+ getSession(tokenHash: string): Promise<Session | null>;
11
+ updateSession(id: string, update: SessionUpdate, now: Date, tokenHash?: string): Promise<Session | null>;
12
+ }