@deplois/api 0.0.0-stage → 0.0.2

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 +63 -4
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileLogger = void 0;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_path_1 = require("node:path");
6
+ const request_logger_1 = require("./request.logger");
7
+ class FileLogger {
8
+ directory;
9
+ recent = [];
10
+ network = [];
11
+ requests = 0;
12
+ errors = 0;
13
+ warnings = 0;
14
+ failedWrites = 0;
15
+ durations = [];
16
+ queue = Promise.resolve();
17
+ pending = 0;
18
+ day = '';
19
+ part = 0;
20
+ size = 0;
21
+ closed = false;
22
+ maxFileSize;
23
+ retentionDays;
24
+ constructor(options = {}) {
25
+ this.directory = (0, node_path_1.resolve)(options.directory ?? 'logs');
26
+ this.maxFileSize = options.maxFileSize ?? 10 * 1024 * 1024;
27
+ this.retentionDays = options.retentionDays ?? 30;
28
+ if (!Number.isSafeInteger(this.maxFileSize) || this.maxFileSize < 1)
29
+ throw new Error('Invalid log file size.');
30
+ if (!Number.isSafeInteger(this.retentionDays) || this.retentionDays < 1)
31
+ throw new Error('Invalid log retention.');
32
+ }
33
+ get median() {
34
+ const durations = [...this.durations].sort((a, b) => a - b);
35
+ const middle = Math.floor(durations.length / 2);
36
+ return durations.length ? (durations.length % 2 ? durations[middle] : (durations[middle - 1] + durations[middle]) / 2) : 0;
37
+ }
38
+ reset() {
39
+ this.requests = this.errors = this.warnings = 0;
40
+ this.durations = [];
41
+ this.network.length = 0;
42
+ }
43
+ log(message, ...details) {
44
+ if (message && typeof message === 'object' && 'request' in message && 'response' in message) {
45
+ const entry = message;
46
+ if (typeof entry.response?.status_code === 'number' && typeof entry.response.duration_ms === 'number') {
47
+ this.requests++;
48
+ this.durations.push(entry.response.duration_ms);
49
+ if (this.durations.length > 1000)
50
+ this.durations.shift();
51
+ this.network.push(entry);
52
+ if (this.network.length > 20)
53
+ this.network.shift();
54
+ }
55
+ }
56
+ this.write('info', message, details);
57
+ }
58
+ error(message, ...details) { this.errors++; this.write('error', message, details); }
59
+ warn(message, ...details) { this.warnings++; this.write('warn', message, details); }
60
+ debug(message, ...details) { this.write('debug', message, details); }
61
+ verbose(message, ...details) { this.write('verbose', message, details); }
62
+ fatal(message, ...details) { this.errors++; this.write('fatal', message, details); }
63
+ write(level, message, details) {
64
+ if (this.closed)
65
+ return;
66
+ const timestamp = new Date().toISOString();
67
+ const normalize = (value) => value instanceof Error
68
+ ? { name: value.name, message: value.message, stack: value.stack }
69
+ : value;
70
+ let line;
71
+ try {
72
+ line = JSON.stringify({ timestamp, level, message: (0, request_logger_1.sanitize)(normalize(message)), details: (0, request_logger_1.sanitize)(details.map(normalize)) }, (_, value) => typeof value === 'bigint' ? String(value) : value) + '\n';
73
+ }
74
+ catch {
75
+ line = JSON.stringify({ timestamp, level, message: '[Unserializable log]' }) + '\n';
76
+ }
77
+ this.recent.push(line.trim());
78
+ if (this.recent.length > 20)
79
+ this.recent.shift();
80
+ // Bound memory if storage cannot keep up. The CLI exposes dropped/failed writes.
81
+ if (this.pending >= 10000) {
82
+ this.failedWrites++;
83
+ return;
84
+ }
85
+ this.pending++;
86
+ this.queue = this.queue.then(async () => {
87
+ const day = timestamp.slice(0, 10);
88
+ if (day !== this.day) {
89
+ await (0, promises_1.mkdir)(this.directory, { recursive: true, mode: 0o700 });
90
+ this.day = day;
91
+ this.part = 0;
92
+ const files = await (0, promises_1.readdir)(this.directory);
93
+ const prefix = `api-${process.pid}-${day}-`;
94
+ for (const file of files) {
95
+ if (file.startsWith(prefix) && /^\d+\.jsonl$/.test(file.slice(prefix.length)))
96
+ this.part = Math.max(this.part, Number(file.slice(prefix.length, -6)));
97
+ const match = /^api-\d+-(\d{4}-\d{2}-\d{2})-\d+\.jsonl$/.exec(file);
98
+ if (match && Date.parse(match[1]) < Date.parse(day) - this.retentionDays * 86400000)
99
+ await (0, promises_1.unlink)((0, node_path_1.join)(this.directory, file)).catch(() => { });
100
+ }
101
+ this.size = await (0, promises_1.stat)(this.path()).then(file => file.size, () => 0);
102
+ }
103
+ const size = Buffer.byteLength(line);
104
+ if (this.size && this.size + size > this.maxFileSize) {
105
+ this.part++;
106
+ this.size = 0;
107
+ }
108
+ await (0, promises_1.appendFile)(this.path(), line, { mode: 0o600 });
109
+ this.size += size;
110
+ }).catch(() => { this.failedWrites++; this.day = ''; }).finally(() => { this.pending--; });
111
+ }
112
+ path() { return (0, node_path_1.join)(this.directory, `api-${process.pid}-${this.day}-${this.part}.jsonl`); }
113
+ async flush() { await this.queue; }
114
+ async close() { this.closed = true; await this.flush(); }
115
+ }
116
+ exports.FileLogger = FileLogger;
@@ -0,0 +1,25 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ import { APIOptions } from '../../options';
3
+ export interface HTTPLog {
4
+ request: {
5
+ id: string;
6
+ method: string;
7
+ path: string;
8
+ body?: unknown;
9
+ };
10
+ response: {
11
+ status_code: number;
12
+ duration_ms: number;
13
+ body?: unknown;
14
+ };
15
+ }
16
+ export declare function sanitize(value: unknown, keys?: string[], seen?: WeakSet<object>, depth?: number): unknown;
17
+ export declare class RequestLogger {
18
+ private readonly options;
19
+ private readonly requests;
20
+ private readonly logger;
21
+ constructor(options: APIOptions);
22
+ use(request: Request, response: Response, next: NextFunction): void;
23
+ response(request: Request, body: unknown): void;
24
+ error(request: Request, error: unknown): void;
25
+ }
@@ -0,0 +1,99 @@
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.RequestLogger = void 0;
16
+ exports.sanitize = sanitize;
17
+ const node_crypto_1 = require("node:crypto");
18
+ const common_1 = require("@nestjs/common");
19
+ const options_1 = require("../../options");
20
+ function sanitize(value, keys = [], seen = new WeakSet(), depth = 0) {
21
+ if (depth > 12)
22
+ return '[Truncated]';
23
+ if (typeof value === 'string')
24
+ return value.length > 4000 ? `${value.slice(0, 4000)}…` : value;
25
+ if (value == null || typeof value !== 'object')
26
+ return value;
27
+ if (seen.has(value))
28
+ return '[Circular]';
29
+ seen.add(value);
30
+ if (Array.isArray(value))
31
+ return value.slice(0, 100).map(item => sanitize(item, keys, seen, depth + 1));
32
+ const result = {};
33
+ for (const [key, content] of Object.entries(value).slice(0, 100)) {
34
+ const normalized = key.replace(/[^a-z0-9]/gi, '').toLowerCase();
35
+ const sensitive = ['password', 'token', 'secret', 'authorization', 'cookie', 'privatekey', 'salt', 'hash', 'cardnumber', 'cvv', 'cvc', 'codeverifier', 'codechallenge', 'code', ...keys.map(item => item.replace(/[^a-z0-9]/gi, '').toLowerCase())];
36
+ Object.defineProperty(result, key, {
37
+ value: sensitive.some(item => normalized.includes(item)) ? '[Filtered]' : sanitize(content, keys, seen, depth + 1),
38
+ enumerable: true
39
+ });
40
+ }
41
+ return result;
42
+ }
43
+ let RequestLogger = class RequestLogger {
44
+ options;
45
+ requests = new WeakMap();
46
+ logger = new common_1.Logger('HTTP');
47
+ constructor(options) {
48
+ this.options = options;
49
+ }
50
+ use(request, response, next) {
51
+ const context = { id: (0, node_crypto_1.randomUUID)(), start: performance.now(), body: undefined };
52
+ this.requests.set(request, context);
53
+ response.setHeader('X-Request-ID', context.id);
54
+ response.once('finish', () => {
55
+ if (this.options.logging === false)
56
+ return;
57
+ const settings = this.options.logging || {};
58
+ const entry = {
59
+ request: { id: context.id, method: request.method, path: request.path },
60
+ response: { status_code: response.statusCode, duration_ms: Math.round(performance.now() - context.start) }
61
+ };
62
+ if (settings.bodies) {
63
+ entry.request.body = sanitize(request.body, settings.redact);
64
+ entry.response.body = sanitize(context.body, settings.redact);
65
+ }
66
+ try {
67
+ if (settings.write)
68
+ Promise.resolve(settings.write(entry)).catch(() => this.logger.warn('HTTP log delivery failed.'));
69
+ else
70
+ this.logger.log(entry);
71
+ }
72
+ catch {
73
+ this.logger.warn('HTTP log delivery failed.');
74
+ }
75
+ });
76
+ next();
77
+ }
78
+ response(request, body) {
79
+ const context = this.requests.get(request);
80
+ if (context)
81
+ context.body = body;
82
+ }
83
+ error(request, error) {
84
+ if (this.options.logging !== false)
85
+ this.logger.error(error instanceof Error ? { name: error.name, message: error.message, stack: error.stack, requestId: this.requests.get(request)?.id } : sanitize(error));
86
+ try {
87
+ Promise.resolve(this.options.onError?.(error, this.requests.get(request)?.id ?? '')).catch(() => this.logger.warn('Error reporting failed.'));
88
+ }
89
+ catch {
90
+ this.logger.warn('Error reporting failed.');
91
+ }
92
+ }
93
+ };
94
+ exports.RequestLogger = RequestLogger;
95
+ exports.RequestLogger = RequestLogger = __decorate([
96
+ (0, common_1.Injectable)(),
97
+ __param(0, (0, common_1.Inject)(options_1.API_OPTIONS)),
98
+ __metadata("design:paramtypes", [Object])
99
+ ], RequestLogger);
@@ -0,0 +1,7 @@
1
+ import { ArgumentMetadata, ValidationPipe } from '@nestjs/common';
2
+ import { APIOptions } from '../../options';
3
+ export declare class BodyValidationPipe extends ValidationPipe {
4
+ private readonly options;
5
+ constructor(options: APIOptions);
6
+ transform(value: unknown, metadata: ArgumentMetadata): Promise<any>;
7
+ }
@@ -0,0 +1,71 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.BodyValidationPipe = void 0;
19
+ const common_1 = require("@nestjs/common");
20
+ const error_plugin_1 = __importDefault(require("../error/error.plugin"));
21
+ const error_code_enum_1 = require("../error/error-code.enum");
22
+ const options_1 = require("../../options");
23
+ const MISSING_PARAMETER = 'A required parameter is missing.';
24
+ const INVALID_PARAMETER = 'Invalid parameter.';
25
+ let BodyValidationPipe = class BodyValidationPipe extends common_1.ValidationPipe {
26
+ options;
27
+ constructor(options) {
28
+ super({
29
+ transform: true,
30
+ transformOptions: { enableImplicitConversion: false },
31
+ whitelist: true,
32
+ forbidNonWhitelisted: true,
33
+ forbidUnknownValues: true,
34
+ skipMissingProperties: false,
35
+ validationError: { target: false, value: true },
36
+ exceptionFactory: (errors) => {
37
+ const issues = collectIssues(errors);
38
+ return error_plugin_1.default.createException(error_code_enum_1.ErrorCode.wrong_parameter, issues.some(issue => issue.missing) ? MISSING_PARAMETER : INVALID_PARAMETER, common_1.HttpStatus.BAD_REQUEST, { cause: { validation: issues } });
39
+ }
40
+ });
41
+ this.options = options;
42
+ }
43
+ async transform(value, metadata) {
44
+ if (this.options.validation === false || metadata.type !== 'body' || !this.toValidate(metadata))
45
+ return value;
46
+ if (value == null || typeof value !== 'object' || Array.isArray(value)) {
47
+ throw error_plugin_1.default.createException(error_code_enum_1.ErrorCode.wrong_parameter, value == null ? MISSING_PARAMETER : INVALID_PARAMETER, common_1.HttpStatus.BAD_REQUEST, { cause: { validation: [{ field: metadata.data ?? 'body', missing: value == null }] } });
48
+ }
49
+ return super.transform(value, metadata);
50
+ }
51
+ };
52
+ exports.BodyValidationPipe = BodyValidationPipe;
53
+ exports.BodyValidationPipe = BodyValidationPipe = __decorate([
54
+ (0, common_1.Injectable)(),
55
+ __param(0, (0, common_1.Inject)(options_1.API_OPTIONS)),
56
+ __metadata("design:paramtypes", [Object])
57
+ ], BodyValidationPipe);
58
+ function collectIssues(errors, parent = '') {
59
+ return errors.flatMap(error => {
60
+ const field = parent ? `${parent}.${error.property}` : error.property;
61
+ const rules = Object.keys(error.constraints ?? {});
62
+ return [
63
+ ...(rules.length ? [{
64
+ field,
65
+ rules,
66
+ missing: error.value == null && !rules.includes('whitelistValidation')
67
+ }] : []),
68
+ ...collectIssues(error.children ?? [], field)
69
+ ];
70
+ });
71
+ }
package/package.json CHANGED
@@ -1,6 +1,65 @@
1
1
  {
2
2
  "name": "@deplois/api",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.2",
4
+ "description": "Shared NestJS API infrastructure for deplois",
5
+ "type": "commonjs",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./database": {
14
+ "types": "./dist/database/index.d.ts",
15
+ "default": "./dist/database/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "check": "tsc -p tsconfig.json --noEmit",
25
+ "test": "npm run build && node --test test/*.test.cjs",
26
+ "prepack": "npm test",
27
+ "publish": "node scripts/publish.mjs",
28
+ "deploy": "node scripts/publish.mjs"
29
+ },
30
+ "engines": {
31
+ "node": ">=22"
32
+ },
33
+ "peerDependencies": {
34
+ "@nestjs/common": "^11.0.0 || ^12.0.0",
35
+ "@nestjs/core": "^11.0.0 || ^12.0.0",
36
+ "class-transformer": "^0.5.1",
37
+ "class-validator": "^0.15.0",
38
+ "reflect-metadata": "^0.2.2",
39
+ "rxjs": "^7.8.1",
40
+ "typeorm": "^1.1.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "typeorm": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "devDependencies": {
48
+ "@nestjs/common": "^11.0.1",
49
+ "@nestjs/core": "^11.0.1",
50
+ "@nestjs/platform-express": "^11.0.1",
51
+ "@types/express": "^5.0.0",
52
+ "@types/node": "^22.10.7",
53
+ "class-transformer": "^0.5.1",
54
+ "class-validator": "^0.15.1",
55
+ "reflect-metadata": "^0.2.2",
56
+ "rxjs": "^7.8.1",
57
+ "typeorm": "^1.1.0",
58
+ "typescript": "^5.7.3"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public",
62
+ "registry": "https://registry.npmjs.org/"
63
+ },
64
+ "license": "UNLICENSED"
65
+ }