From 4f98599efef371b5cdd823dfd06ba218fc8dfae9 Mon Sep 17 00:00:00 2001 From: ustaxs Date: Tue, 21 Jul 2026 15:39:42 +0100 Subject: [PATCH 1/2] Add OpenAPI (Swagger) generation, API Explorer and generated TypeScript client --- .env.example | 5 ++ .github/workflows/build-check.yml | 58 ++++++++++++++- .gitignore | 10 +++ package.json | 5 +- scripts/export-openapi.ts | 70 ++++++++++++++++++ .../oracle/dto/payload-response.dto.ts | 38 ++++++++++ src/blockchain/oracle/dto/sign-payload.dto.ts | 10 +++ .../oracle/dto/verify-signature.dto.ts | 16 +++++ src/blockchain/oracle/oracle.controller.ts | 72 +++++++++++++++++++ src/config/swagger.config.ts | 17 ++++- 10 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 scripts/export-openapi.ts diff --git a/.env.example b/.env.example index 28eda4c..6bbf3de 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,11 @@ NODE_ENV=development PORT=3000 API_PREFIX=/api/v1 +# OpenAPI / Swagger +# Swagger UI (/api/docs) and JSON export are always active in development. +# In production they are disabled by default — set to "true" to opt in. +SWAGGER_ENABLED=false + # Database DATABASE_URL=postgresql://user:password@localhost:5432/alianStructure.db diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml index f06e31a..5fabf09 100644 --- a/.github/workflows/build-check.yml +++ b/.github/workflows/build-check.yml @@ -24,4 +24,60 @@ jobs: run: npx tsc --noEmit - name: Build project - run: npm run build \ No newline at end of file + run: npm run build + + openapi: + name: Generate OpenAPI Spec & TypeScript Client + runs-on: ubuntu-latest + # Only run on pushes to main/master (not PRs) to avoid redundant artifact uploads. + if: github.event_name == 'push' + needs: build + env: + NODE_ENV: development + # Provide minimal env vars so the app can bootstrap without a real DB. + # The export script boots with abortOnError:false so it tolerates DB absence. + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/openapi_gen + JWT_SECRET: ci-openapi-export-secret + PORT: 3001 + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Export OpenAPI JSON + run: npm run openapi:export + + - name: Set up Java (required by OpenAPI Generator CLI) + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - name: Generate TypeScript fetch client + run: | + npx @openapitools/openapi-generator-cli@2.13.4 generate \ + -i docs/openapi.json \ + -g typescript-fetch \ + -o client \ + --additional-properties=supportsES6=true,typescriptThreePlus=true + + - name: Upload OpenAPI JSON artifact + uses: actions/upload-artifact@v4 + with: + name: openapi-spec + path: docs/openapi.json + retention-days: 30 + + - name: Upload TypeScript client artifact + uses: actions/upload-artifact@v4 + with: + name: typescript-client + path: client/ + retention-days: 30 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 22e1f81..0067a24 100644 --- a/.gitignore +++ b/.gitignore @@ -151,3 +151,13 @@ dist # Vite logs files vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# Generated OpenAPI artifacts — produced by `npm run openapi:export` +docs/openapi.json + +# Generated TypeScript client — produced by `npm run openapi:client` +client/ + +# OpenAPI Generator CLI cache +.openapi-generator/ +openapitools.json diff --git a/package.json b/package.json index a4d4743..bf0acc9 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,9 @@ "seed:audit": "ts-node src/seeds/seed-audit-data.ts", "docs:generate": "nest start --watch", "docs:serve": "nest start", - "docs:build": "nest build && node dist/main.js" + "docs:build": "nest build && node dist/main.js", + "openapi:export": "cross-env NODE_ENV=development ts-node -r tsconfig-paths/register scripts/export-openapi.ts", + "openapi:client": "npm run openapi:export && npx @openapitools/openapi-generator-cli generate -i docs/openapi.json -g typescript-fetch -o client --additional-properties=supportsES6=true,typescriptThreePlus=true" }, "dependencies": { "@nestjs/axios": "^4.0.1", @@ -127,6 +129,7 @@ "eslint-plugin-import": "^2.32.0", "eslint-plugin-prettier": "^5.1.2", "jest": "^29.7.0", + "cross-env": "^7.0.3", "nodemon": "^3.1.11", "pg": "^8.22.0", "pino-pretty": "^13.1.3", diff --git a/scripts/export-openapi.ts b/scripts/export-openapi.ts new file mode 100644 index 0000000..9b4a523 --- /dev/null +++ b/scripts/export-openapi.ts @@ -0,0 +1,70 @@ +/** + * Standalone script that boots the NestJS app just long enough to generate + * and write the OpenAPI JSON document to docs/openapi.json, then exits. + * + * Usage: + * npx ts-node -r tsconfig-paths/register scripts/export-openapi.ts + * npm run openapi:export + */ + +import { NestFactory } from "@nestjs/core"; +import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; +import { writeFileSync, mkdirSync } from "fs"; +import { join } from "path"; +import { AppModule } from "../src/app.module"; + +async function exportOpenApi() { + // Silence NestJS bootstrap logs — we only want the artefact output + const app = await NestFactory.create(AppModule, { logger: false, abortOnError: false }); + + const config = new DocumentBuilder() + .setTitle("alian-structure Backend API") + .setDescription( + "Comprehensive API documentation for alian-structure backend services including " + + "agent management, oracle submissions, compute operations, and audit trails.", + ) + .setVersion("1.0.0") + .setContact("alian-structure Team", "https://alian-structure.com", "api@alian-structure.com") + .setLicense("Apache 2.0", "https://www.apache.org/licenses/LICENSE-2.0") + .addServer("http://localhost:3001", "Development Server") + .addServer("https://api.alian-structure.com", "Production Server") + .addBearerAuth( + { type: "http", scheme: "bearer", bearerFormat: "JWT", name: "JWT", description: "Enter JWT token", in: "header" }, + "JWT-auth", + ) + .addApiKey( + { type: "apiKey", name: "X-API-Key", in: "header", description: "API key for service-to-service communication" }, + "api-key", + ) + .addTag("Health", "Liveness, readiness, and startup probes for Kubernetes orchestration") + .addTag("Authentication", "User authentication and authorization") + .addTag("Enhanced Authentication & KYC", "Enhanced auth with 2FA and KYC flows") + .addTag("Users", "User management operations") + .addTag("Oracle", "Oracle data submissions and payload management") + .addTag("Price Feed", "Aggregated on-chain price data") + .addTag("Audit", "Audit trail and logging") + .addTag("Profile", "User profile management") + .addTag("Info", "API health and meta-information") + .build(); + + const document = SwaggerModule.createDocument(app, config, { + deepScanRoutes: true, + operationIdFactory: (_controllerKey: string, methodKey: string) => methodKey, + }); + + // Write JSON + const outDir = join(__dirname, "..", "docs"); + mkdirSync(outDir, { recursive: true }); + + const jsonPath = join(outDir, "openapi.json"); + writeFileSync(jsonPath, JSON.stringify(document, null, 2), "utf8"); + console.log(`✅ OpenAPI JSON written to ${jsonPath}`); + + await app.close(); + process.exit(0); +} + +exportOpenApi().catch((err) => { + console.error("Failed to export OpenAPI spec:", err); + process.exit(1); +}); diff --git a/src/blockchain/oracle/dto/payload-response.dto.ts b/src/blockchain/oracle/dto/payload-response.dto.ts index 8c8186e..c6e6978 100644 --- a/src/blockchain/oracle/dto/payload-response.dto.ts +++ b/src/blockchain/oracle/dto/payload-response.dto.ts @@ -1,27 +1,65 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { PayloadStatus, PayloadType } from "../entities/signed-payload.entity"; /** * Response DTO for payload operations */ export class PayloadResponseDto { + @ApiProperty({ description: "Unique payload UUID", example: "a1b2c3d4-1234-5678-90ef-ghijklmnopqr" }) id: string; + + @ApiProperty({ description: "Type of payload", enum: PayloadType, example: PayloadType.PRICE_FEED }) payloadType: PayloadType; + + @ApiProperty({ description: "Ethereum address that signed this payload", example: "0xAbCd1234567890abcdef1234567890abcdef1234" }) signerAddress: string; + + @ApiProperty({ description: "Submission nonce", example: "42" }) nonce: string; + + @ApiProperty({ description: "Raw payload data", type: "object", example: { token: "ETH", price: 3200.5 } }) payload: Record; + + @ApiProperty({ description: "Keccak256 hash of the payload", example: "0xabc123..." }) payloadHash: string; + + @ApiProperty({ description: "EIP-712 structured data hash", example: "0xdef456..." }) structuredDataHash: string; + + @ApiPropertyOptional({ description: "ECDSA signature (0x-prefixed, 132 chars)", nullable: true, example: "0x..." }) signature: string | null; + + @ApiProperty({ description: "Payload expiry timestamp" }) expiresAt: Date; + + @ApiProperty({ description: "Current submission status", enum: PayloadStatus, example: PayloadStatus.PENDING }) status: PayloadStatus; + + @ApiPropertyOptional({ description: "On-chain transaction hash after submission", nullable: true, example: "0x..." }) transactionHash: string | null; + + @ApiPropertyOptional({ description: "Block number when confirmed on-chain", nullable: true, example: "18500000" }) blockNumber: string | null; + + @ApiProperty({ description: "Total number of submission attempts", example: 1 }) submissionAttempts: number; + + @ApiPropertyOptional({ description: "Error message if submission failed", nullable: true }) errorMessage: string | null; + + @ApiPropertyOptional({ description: "Optional metadata", nullable: true, type: "object" }) metadata: Record | null; + + @ApiProperty({ description: "Record creation timestamp" }) createdAt: Date; + + @ApiProperty({ description: "Record last-updated timestamp" }) updatedAt: Date; + + @ApiPropertyOptional({ description: "When submitted to blockchain", nullable: true }) submittedAt: Date | null; + + @ApiPropertyOptional({ description: "When confirmed on-chain", nullable: true }) confirmedAt: Date | null; } diff --git a/src/blockchain/oracle/dto/sign-payload.dto.ts b/src/blockchain/oracle/dto/sign-payload.dto.ts index e7a8dad..2f504da 100644 --- a/src/blockchain/oracle/dto/sign-payload.dto.ts +++ b/src/blockchain/oracle/dto/sign-payload.dto.ts @@ -1,13 +1,23 @@ import { IsString, IsNotEmpty, Matches } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; /** * DTO for signing a payload with a private key */ export class SignPayloadDto { + @ApiProperty({ + description: "UUID of the payload to sign", + example: "a1b2c3d4-1234-5678-90ef-ghijklmnopqr", + }) @IsString() @IsNotEmpty() payloadId: string; + @ApiProperty({ + description: "Ethereum private key (0x-prefixed, 64 hex chars). NOTE: use client-side signing in production.", + example: "0x4c0883a69102937d6231471b5dbb6e538eba2ef68e5fd63f36fe1ef7e9bb4d7f", + pattern: "^0x[a-fA-F0-9]{64}$", + }) @IsString() @IsNotEmpty() @Matches(/^0x[a-fA-F0-9]{64}$/, { diff --git a/src/blockchain/oracle/dto/verify-signature.dto.ts b/src/blockchain/oracle/dto/verify-signature.dto.ts index d3cb15d..876a5e3 100644 --- a/src/blockchain/oracle/dto/verify-signature.dto.ts +++ b/src/blockchain/oracle/dto/verify-signature.dto.ts @@ -1,13 +1,24 @@ import { IsString, IsNotEmpty, IsObject, Matches } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; /** * DTO for verifying a signature off-chain */ export class VerifySignatureDto { + @ApiProperty({ + description: "Payload data that was originally signed", + type: "object", + example: { token: "ETH", price: 3200.5, timestamp: 1620000000000 }, + }) @IsObject() @IsNotEmpty() payload: Record; + @ApiProperty({ + description: "ECDSA signature (0x-prefixed, 132 chars)", + example: "0x1234567890abcdef....", + pattern: "^0x[a-fA-F0-9]{130}$", + }) @IsString() @IsNotEmpty() @Matches(/^0x[a-fA-F0-9]{130}$/, { @@ -15,6 +26,11 @@ export class VerifySignatureDto { }) signature: string; + @ApiProperty({ + description: "Expected signer Ethereum address (0x-prefixed, 40 hex chars)", + example: "0xAbCd1234567890abcdef1234567890abcdef1234", + pattern: "^0x[a-fA-F0-9]{40}$", + }) @IsString() @IsNotEmpty() @Matches(/^0x[a-fA-F0-9]{40}$/, { diff --git a/src/blockchain/oracle/oracle.controller.ts b/src/blockchain/oracle/oracle.controller.ts index aa1af2a..4dbfa4e 100644 --- a/src/blockchain/oracle/oracle.controller.ts +++ b/src/blockchain/oracle/oracle.controller.ts @@ -11,6 +11,14 @@ import { HttpStatus, Logger, } from "@nestjs/common"; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiParam, + ApiQuery, +} from "@nestjs/swagger"; import { OracleService } from "./services/oracle.service"; import { JwtAuthGuard } from "src/core/auth/jwt.guard"; import { CreatePayloadDto } from "./dto/create-payload.dto"; @@ -24,6 +32,7 @@ import { PayloadStatus } from "./entities/signed-payload.entity"; * Controller for Oracle service endpoints * Handles payload creation, signing, and submission */ +@ApiTags("Oracle") @Controller("oracle") export class OracleController { private readonly logger = new Logger(OracleController.name); @@ -36,7 +45,12 @@ export class OracleController { */ @Post("payloads") @UseGuards(JwtAuthGuard) + @ApiBearerAuth("JWT-auth") @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: "Create a new payload", description: "Create a new payload ready for signing. Requires JWT authentication." }) + @ApiResponse({ status: 201, description: "Payload created", type: PayloadResponseDto }) + @ApiResponse({ status: 400, description: "Invalid payload data" }) + @ApiResponse({ status: 401, description: "Unauthorized" }) async createPayload( @Request() req, @Body() createPayloadDto: CreatePayloadDto, @@ -56,7 +70,13 @@ export class OracleController { */ @Post("payloads/:id/sign") @UseGuards(JwtAuthGuard) + @ApiBearerAuth("JWT-auth") @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: "Sign a payload", description: "Sign a payload with a private key. **Use client-side signing in production.**" }) + @ApiParam({ name: "id", description: "Payload UUID" }) + @ApiResponse({ status: 200, description: "Payload signed", type: PayloadResponseDto }) + @ApiResponse({ status: 401, description: "Unauthorized" }) + @ApiResponse({ status: 404, description: "Payload not found" }) async signPayload( @Param("id") id: string, @Body() signPayloadDto: SignPayloadDto, @@ -72,7 +92,13 @@ export class OracleController { */ @Post("payloads/:id/submit") @UseGuards(JwtAuthGuard) + @ApiBearerAuth("JWT-auth") @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: "Submit payload on-chain", description: "Submit a fully signed payload to the blockchain." }) + @ApiParam({ name: "id", description: "Payload UUID" }) + @ApiResponse({ status: 200, description: "Payload submitted", schema: { type: "object", properties: { transactionHash: { type: "string" }, payload: { $ref: "#/components/schemas/PayloadResponseDto" } } } }) + @ApiResponse({ status: 401, description: "Unauthorized" }) + @ApiResponse({ status: 404, description: "Payload not found" }) async submitPayload( @Param("id") id: string, ): Promise<{ transactionHash: string; payload: PayloadResponseDto }> { @@ -86,7 +112,13 @@ export class OracleController { */ @Post("payloads/:id/retry") @UseGuards(JwtAuthGuard) + @ApiBearerAuth("JWT-auth") @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: "Retry failed submission", description: "Retry submitting a payload that previously failed." }) + @ApiParam({ name: "id", description: "Payload UUID" }) + @ApiResponse({ status: 200, description: "Retry initiated", schema: { type: "object", properties: { transactionHash: { type: "string" }, payload: { $ref: "#/components/schemas/PayloadResponseDto" } } } }) + @ApiResponse({ status: 401, description: "Unauthorized" }) + @ApiResponse({ status: 404, description: "Payload not found" }) async retrySubmission( @Param("id") id: string, ): Promise<{ transactionHash: string; payload: PayloadResponseDto }> { @@ -100,6 +132,8 @@ export class OracleController { */ @Post("verify-signature") @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: "Verify signature off-chain", description: "Verify an ECDSA signature against a payload and expected signer address." }) + @ApiResponse({ status: 200, description: "Verification result", schema: { type: "object", properties: { valid: { type: "boolean" }, message: { type: "string" } } } }) async verifySignature( @Body() verifySignatureDto: VerifySignatureDto, ): Promise<{ valid: boolean; message: string }> { @@ -116,6 +150,11 @@ export class OracleController { */ @Get("payloads/:id/verify") @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: "Verify payload signature", description: "Check whether the stored signature on a payload is valid for a given signer." }) + @ApiParam({ name: "id", description: "Payload UUID" }) + @ApiQuery({ name: "expectedSigner", description: "Ethereum address of the expected signer", required: false }) + @ApiResponse({ status: 200, description: "Verification result", schema: { type: "object", properties: { valid: { type: "boolean" }, payloadId: { type: "string" } } } }) + @ApiResponse({ status: 404, description: "Payload not found" }) async verifyPayloadSignature( @Param("id") id: string, @Query("expectedSigner") expectedSigner: string, @@ -135,6 +174,12 @@ export class OracleController { */ @Get("payloads/:id") @UseGuards(JwtAuthGuard) + @ApiBearerAuth("JWT-auth") + @ApiOperation({ summary: "Get payload by ID" }) + @ApiParam({ name: "id", description: "Payload UUID" }) + @ApiResponse({ status: 200, description: "Payload found", type: PayloadResponseDto }) + @ApiResponse({ status: 401, description: "Unauthorized" }) + @ApiResponse({ status: 404, description: "Payload not found" }) async getPayload(@Param("id") id: string): Promise { return this.oracleService.getPayload(id); } @@ -144,6 +189,12 @@ export class OracleController { */ @Get("my-payloads") @UseGuards(JwtAuthGuard) + @ApiBearerAuth("JWT-auth") + @ApiOperation({ summary: "Get my payloads", description: "Retrieve all payloads belonging to the authenticated wallet address." }) + @ApiQuery({ name: "status", enum: PayloadStatus, required: false, description: "Filter by submission status" }) + @ApiQuery({ name: "limit", required: false, description: "Max results to return (default 50)", type: Number }) + @ApiResponse({ status: 200, description: "List of payloads", type: [PayloadResponseDto] }) + @ApiResponse({ status: 401, description: "Unauthorized" }) async getMyPayloads( @Request() req, @Query("status") status?: PayloadStatus, @@ -167,6 +218,11 @@ export class OracleController { * Get payloads for a specific address (public endpoint) */ @Get("payloads/address/:address") + @ApiOperation({ summary: "Get payloads by address", description: "Retrieve payloads submitted by a specific Ethereum address (public)." }) + @ApiParam({ name: "address", description: "Ethereum wallet address" }) + @ApiQuery({ name: "status", enum: PayloadStatus, required: false }) + @ApiQuery({ name: "limit", required: false, type: Number }) + @ApiResponse({ status: 200, description: "List of payloads", type: [PayloadResponseDto] }) async getPayloadsForAddress( @Param("address") address: string, @Query("status") status?: PayloadStatus, @@ -186,6 +242,11 @@ export class OracleController { */ @Get("payloads/pending/ready") @UseGuards(JwtAuthGuard) + @ApiBearerAuth("JWT-auth") + @ApiOperation({ summary: "Get pending payloads", description: "Retrieve signed payloads that are ready for on-chain submission." }) + @ApiQuery({ name: "limit", required: false, type: Number, description: "Max results (default 100)" }) + @ApiResponse({ status: 200, description: "List of pending payloads", type: [PayloadResponseDto] }) + @ApiResponse({ status: 401, description: "Unauthorized" }) async getPendingPayloads( @Query("limit") limit?: number, ): Promise { @@ -198,6 +259,9 @@ export class OracleController { * Get current nonce for an address */ @Get("nonce/:address") + @ApiOperation({ summary: "Get nonce for address", description: "Retrieve the current submission nonce for an Ethereum address." }) + @ApiParam({ name: "address", description: "Ethereum wallet address" }) + @ApiResponse({ status: 200, description: "Current nonce", schema: { type: "object", properties: { address: { type: "string" }, nonce: { type: "string" } } } }) async getCurrentNonce(@Param("address") address: string): Promise<{ address: string; nonce: string; @@ -215,6 +279,10 @@ export class OracleController { */ @Get("my-nonce") @UseGuards(JwtAuthGuard) + @ApiBearerAuth("JWT-auth") + @ApiOperation({ summary: "Get my nonce", description: "Retrieve the current submission nonce for the authenticated wallet address." }) + @ApiResponse({ status: 200, description: "Current nonce", schema: { type: "object", properties: { address: { type: "string" }, nonce: { type: "string" } } } }) + @ApiResponse({ status: 401, description: "Unauthorized" }) async getMyNonce(@Request() req): Promise<{ address: string; nonce: string; @@ -232,6 +300,8 @@ export class OracleController { * Get Oracle service statistics */ @Get("stats") + @ApiOperation({ summary: "Get Oracle statistics", description: "Retrieve aggregate statistics about oracle submissions and payload statuses." }) + @ApiResponse({ status: 200, description: "Oracle statistics" }) async getStatistics(): Promise { return this.oracleService.getStatistics(); } @@ -240,6 +310,8 @@ export class OracleController { * Health check endpoint */ @Get("health") + @ApiOperation({ summary: "Oracle health check" }) + @ApiResponse({ status: 200, description: "Service is healthy", schema: { type: "object", properties: { status: { type: "string" }, timestamp: { type: "string" }, service: { type: "string" } } } }) async healthCheck(): Promise<{ status: string; timestamp: string; diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts index 37177b7..7b3588a 100644 --- a/src/config/swagger.config.ts +++ b/src/config/swagger.config.ts @@ -1,9 +1,20 @@ import { INestApplication } from "@nestjs/common"; -import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; +import { DocumentBuilder, SwaggerModule, OpenAPIObject } from "@nestjs/swagger"; import { ConfigService } from "@nestjs/config"; -export function setupSwagger(app: INestApplication): void { +export function setupSwagger(app: INestApplication): OpenAPIObject | null { const configService = app.get(ConfigService); + const nodeEnv = configService.get("NODE_ENV", "development"); + + // Swagger is opt-in in production — set SWAGGER_ENABLED=true to expose it. + const swaggerEnabled = + nodeEnv !== "production" || + configService.get("SWAGGER_ENABLED") === "true"; + + if (!swaggerEnabled) { + return null; + } + const port = Number(process.env.PORT || configService.get("PORT", 3001)); const config = new DocumentBuilder() @@ -76,6 +87,8 @@ export function setupSwagger(app: INestApplication): void { tryItOutEnabled: true, }, }); + + return document; } From 0af6aac5bbd35e1542215ced84eed3e91c6fd5e0 Mon Sep 17 00:00:00 2001 From: ustaxs Date: Tue, 21 Jul 2026 15:46:13 +0100 Subject: [PATCH 2/2] fixed workflow --- package-lock.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/package-lock.json b/package-lock.json index 89f3ab4..0841d68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,6 +94,7 @@ "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.17.0", "@typescript-eslint/parser": "^6.17.0", + "cross-env": "^7.0.3", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.32.0", @@ -9149,6 +9150,25 @@ "node": ">=12.0.0" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",