diff --git a/app/backend/src/auth/decorators/roles.decorator.ts b/app/backend/src/auth/decorators/roles.decorator.ts new file mode 100644 index 000000000..07aebb9c6 --- /dev/null +++ b/app/backend/src/auth/decorators/roles.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from "@nestjs/common"; +import { UserRole } from "../enums/user-role.enum"; + +export const ROLES_KEY = "roles"; +export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles); diff --git a/app/backend/src/auth/enums/user-role.enum.ts b/app/backend/src/auth/enums/user-role.enum.ts new file mode 100644 index 000000000..72509bccd --- /dev/null +++ b/app/backend/src/auth/enums/user-role.enum.ts @@ -0,0 +1,4 @@ +export enum UserRole { + Admin = "admin", + User = "user", +} diff --git a/app/backend/src/auth/guards/roles.guard.ts b/app/backend/src/auth/guards/roles.guard.ts new file mode 100644 index 000000000..74c8baa8f --- /dev/null +++ b/app/backend/src/auth/guards/roles.guard.ts @@ -0,0 +1,21 @@ +import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import { ROLES_KEY } from "./roles.decorator"; +import { UserRole } from "../enums/user-role.enum"; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.getAllAndOverride( + ROLES_KEY, + [context.getHandler(), context.getClass()], + ); + if (!requiredRoles) { + return true; + } + const { user } = context.switchToHttp().getRequest(); + return requiredRoles.some((role) => user.role?.includes(role)); + } +} diff --git a/app/backend/src/common/errors/contract-adapter.error.ts b/app/backend/src/common/errors/contract-adapter.error.ts new file mode 100644 index 000000000..4a34a4006 --- /dev/null +++ b/app/backend/src/common/errors/contract-adapter.error.ts @@ -0,0 +1,14 @@ +export enum ContractAdapterErrorCode { + StreamError = "STREAM_ERROR", + ParseError = "PARSE_ERROR", + UnknownError = "UNKNOWN_ERROR", +} + +export class ContractAdapterError extends Error { + constructor( + public readonly code: ContractAdapterErrorCode, + public readonly message: string, + ) { + super(message); + } +} diff --git a/app/backend/src/health/health.module.ts b/app/backend/src/health/health.module.ts index 3a30c144d..dae3dd6a6 100644 --- a/app/backend/src/health/health.module.ts +++ b/app/backend/src/health/health.module.ts @@ -1,14 +1,20 @@ import { Module } from "@nestjs/common"; +import { HealthController } from "./health.controller"; +import { HealthService } from "./health.service"; import { SupabaseModule } from "../supabase/supabase.module"; import { StellarModule } from "../stellar/stellar.module"; import { JobQueueModule } from "../job-queue/job-queue.module"; import { IngestionModule } from "../ingestion/ingestion.module"; import { TransactionsModule } from "../transactions/transactions.module"; -import { HealthController } from "./health.controller"; -import { HealthService } from "./health.service"; @Module({ - imports: [SupabaseModule, StellarModule, JobQueueModule, IngestionModule, TransactionsModule], + imports: [ + SupabaseModule, + StellarModule, + JobQueueModule, + IngestionModule, + TransactionsModule, + ], controllers: [HealthController], providers: [HealthService], }) diff --git a/app/backend/src/health/health.service.ts b/app/backend/src/health/health.service.ts index 1a8de39a5..724efc2ba 100644 --- a/app/backend/src/health/health.service.ts +++ b/app/backend/src/health/health.service.ts @@ -7,6 +7,7 @@ import { JobQueueService } from "../job-queue/job-queue.service"; import { JobRepository } from "../job-queue/job.repository"; import { CursorRepository } from "../ingestion/cursor.repository"; import { SorobanRpcService } from "../transactions/soroban-rpc.service"; +import { StellarIngestionService } from "../ingestion/stellar-ingestion.service"; @Injectable() export class HealthService { @@ -22,6 +23,7 @@ export class HealthService { private readonly jobRepository: JobRepository, private readonly cursorRepository: CursorRepository, private readonly sorobanRpcService: SorobanRpcService, + private readonly stellarIngestionService: StellarIngestionService, ) {} /** @@ -280,6 +282,33 @@ export class HealthService { } } + async checkStellarIngestion(): Promise<{ + status: "up" | "down"; + details?: string; + }> { + const { isRunning, contractId } = this.stellarIngestionService.getStatus(); + + if (!isRunning) { + return { + status: "down", + details: "Stellar ingestion service is not running", + }; + } + + if (!contractId) { + return { + status: "down", + details: + "Stellar ingestion service is not configured with a contract ID", + }; + } + + return { + status: "up", + details: `Stellar ingestion service is running for contract ${contractId}`, + }; + } + /** * Checks if database migrations are applied by querying the schema_migrations table. * This is a Supabase/PostgreSQL specific check. @@ -348,19 +377,34 @@ export class HealthService { * Performs deep dependency checks for /ready. */ async getReadinessStatus() { - const [supabase, env, migrations, queue, horizon, sorobanRpc, ingestion] = - await Promise.all([ - this.checkSupabase(), - Promise.resolve(this.checkEnvironment()), - this.checkMigrations(), - this.checkQueue(), - this.checkHorizon(), - this.checkSorobanRpc(), - this.checkIngestionLag(), - ]); + const [ + supabase, + env, + migrations, + queue, + horizon, + sorobanRpc, + ingestion, + stellarIngestion, + ] = await Promise.all([ + this.checkSupabase(), + Promise.resolve(this.checkEnvironment()), + this.checkMigrations(), + this.checkQueue(), + this.checkHorizon(), + this.checkSorobanRpc(), + this.checkIngestionLag(), + this.checkStellarIngestion(), + ]); // Critical dependencies: database, migrations, queue, horizon - const criticalChecks = [supabase, migrations, queue, horizon]; + const criticalChecks = [ + supabase, + migrations, + queue, + horizon, + stellarIngestion, + ]; const ready = criticalChecks.every((check) => check.status === "up"); return { @@ -415,6 +459,15 @@ export class HealthService { lastSuccess: ingestion.lastSuccess, error: ingestion.status === "down" ? ingestion.details : undefined, }, + { + name: "stellar_ingestion", + status: stellarIngestion.status, + details: stellarIngestion.details, + error: + stellarIngestion.status === "down" + ? stellarIngestion.details + : undefined, + }, ], }; } diff --git a/app/backend/src/ingestion/stellar-ingestion.service.ts b/app/backend/src/ingestion/stellar-ingestion.service.ts index b7a1c0add..15d914dc1 100644 --- a/app/backend/src/ingestion/stellar-ingestion.service.ts +++ b/app/backend/src/ingestion/stellar-ingestion.service.ts @@ -13,6 +13,10 @@ import { SorobanEventParser, RawHorizonContractEvent, } from "./soroban-event.parser"; +import { + ContractAdapterError, + ContractAdapterErrorCode, +} from "../common/errors/contract-adapter.error"; import { CursorRepository } from "./cursor.repository"; import { EscrowEventRepository } from "./escrow-event.repository"; import { JobQueueService } from "../job-queue/job-queue.service"; @@ -172,10 +176,15 @@ export class StellarIngestionService implements OnModuleInit, OnModuleDestroy { onmessage: (record: unknown) => { void this.handleRecord(record as RawHorizonContractEvent, streamId); }, + onerror: (err: unknown) => { this.logger.error(`Stream error for ${streamId}: ${String(err)}`); this.stopCurrentStream(); this.scheduleReconnect(contractId); + throw new ContractAdapterError( + ContractAdapterErrorCode.StreamError, + String(err), + ); }, }) as () => void; @@ -217,6 +226,10 @@ export class StellarIngestionService implements OnModuleInit, OnModuleDestroy { this.logger.error(`SSE error for ${streamId}: ${String(err)}`); es.close(); this.scheduleReconnect(contractId); + throw new ContractAdapterError( + ContractAdapterErrorCode.StreamError, + String(err), + ); }; return () => es.close(); @@ -283,6 +296,10 @@ export class StellarIngestionService implements OnModuleInit, OnModuleDestroy { this.logger.error(`Stream error for ${streamId}: ${String(err)}`); this.stopCurrentStream(); this.scheduleReconnect(contractId); + throw new ContractAdapterError( + ContractAdapterErrorCode.StreamError, + String(err), + ); }, }) as () => void; @@ -365,6 +382,17 @@ export class StellarIngestionService implements OnModuleInit, OnModuleDestroy { } } + // --------------------------------------------------------------------------- + // Public status + // --------------------------------------------------------------------------- + + getStatus(): { isRunning: boolean; contractId: string | null } { + return { + isRunning: !!this.stopStream, + contractId: this.currentContractId, + }; + } + // --------------------------------------------------------------------------- // Event processing // --------------------------------------------------------------------------- @@ -373,23 +401,30 @@ export class StellarIngestionService implements OnModuleInit, OnModuleDestroy { raw: RawHorizonContractEvent, streamId: string, ): Promise { - const event = this.parser.parse(raw); + try { + const event = this.parser.parse(raw); - if (!event) { - // Unrecognised or non- RustAcademy event; still advance cursor. - await this.safeUpdateCursor(streamId, raw.paging_token, raw.ledger); - return; - } + if (!event) { + // Unrecognised or non- RustAcademy event; still advance cursor. + await this.safeUpdateCursor(streamId, raw.paging_token, raw.ledger); + return; + } - this.logger.debug( - `Processing ${event.eventType} paging_token=${event.pagingToken}`, - ); + this.logger.debug( + `Processing ${event.eventType} paging_token=${event.pagingToken}`, + ); - await this.persistEvent(event); - await this.safeUpdateCursor(streamId, raw.paging_token, raw.ledger); + await this.persistEvent(event); + await this.safeUpdateCursor(streamId, raw.paging_token, raw.ledger); - // Emit for other services / notification layer - this.eventEmitter.emit(`stellar.${event.eventType}`, event); + // Emit for other services / notification layer + this.eventEmitter.emit(`stellar.${event.eventType}`, event); + } catch (err) { + throw new ContractAdapterError( + ContractAdapterErrorCode.ParseError, + `Failed to parse event: ${String(err)}`, + ); + } } private async persistEvent(event: RustAcademyContractEvent): Promise { diff --git a/app/backend/src/payments/dto/payout.dto.ts b/app/backend/src/payments/dto/payout.dto.ts new file mode 100644 index 000000000..3b41ad80b --- /dev/null +++ b/app/backend/src/payments/dto/payout.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsNumber, IsString } from "class-validator"; + +export class PayoutDto { + @ApiProperty() + @IsNotEmpty() + @IsString() + destinationAddress: string; + + @ApiProperty() + @IsNotEmpty() + @IsNumber() + amount: number; +} diff --git a/app/backend/src/payments/entities/payout.entity.ts b/app/backend/src/payments/entities/payout.entity.ts new file mode 100644 index 000000000..29edf4bc3 --- /dev/null +++ b/app/backend/src/payments/entities/payout.entity.ts @@ -0,0 +1,38 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from "typeorm"; + +export enum PayoutStatus { + Pending = "pending", + Released = "released", + Failed = "failed", +} + +@Entity() +export class Payout { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column() + destinationAddress: string; + + @Column() + amount: number; + + @Column({ + type: "enum", + enum: PayoutStatus, + default: PayoutStatus.Pending, + }) + status: PayoutStatus; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/app/backend/src/payments/payments.controller.ts b/app/backend/src/payments/payments.controller.ts index bdf18ed17..b40ddf561 100644 --- a/app/backend/src/payments/payments.controller.ts +++ b/app/backend/src/payments/payments.controller.ts @@ -1,8 +1,21 @@ -import { Controller, Get, Query } from "@nestjs/common"; +import { + Controller, + Get, + Query, + Post, + Body, + Param, + UseGuards, +} from "@nestjs/common"; import { ApiTags, ApiOperation, ApiResponse } from "@nestjs/swagger"; import { HorizonService } from "../transactions/horizon.service"; import { SensitiveMutation } from "../auth/decorators/sensitive-mutation.decorator"; +import { PaymentsService } from "./payments.service"; +import { PayoutDto } from "./dto/payout.dto"; +import { Roles } from "../auth/decorators/roles.decorator"; +import { UserRole } from "../auth/enums/user-role.enum"; +import { RolesGuard } from "../auth/guards/roles.guard"; type RecentPaymentsQuery = { address: string; @@ -13,7 +26,30 @@ type RecentPaymentsQuery = { @ApiTags("payments") @Controller("payments") export class PaymentsController { - constructor(private readonly horizonService: HorizonService) {} + constructor( + private readonly horizonService: HorizonService, + private readonly paymentsService: PaymentsService, + ) {} + + @Post("payout") + @Roles(UserRole.Admin) + @UseGuards(RolesGuard) + @SensitiveMutation("payments.payout.create") + @ApiOperation({ summary: "Create a payout" }) + @ApiResponse({ status: 201, description: "Payout created" }) + async createPayout(@Body() payoutDto: PayoutDto) { + return this.paymentsService.createPayout(payoutDto); + } + + @Post("payout/:id/release") + @Roles(UserRole.Admin) + @UseGuards(RolesGuard) + @SensitiveMutation("payments.payout.release") + @ApiOperation({ summary: "Release a payout" }) + @ApiResponse({ status: 200, description: "Payout released" }) + async releasePayout(@Param("id") id: string) { + return this.paymentsService.releasePayout(id); + } // Read-only, but payment-sensitive: exposes an address's payment history, // which is a reconnaissance target for scraping/enumeration. Tagged diff --git a/app/backend/src/payments/payments.module.ts b/app/backend/src/payments/payments.module.ts index 2126fa9d5..979713bea 100644 --- a/app/backend/src/payments/payments.module.ts +++ b/app/backend/src/payments/payments.module.ts @@ -1,14 +1,16 @@ import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; import { HorizonService } from "../transactions/horizon.service"; import { PaymentsController } from "./payments.controller"; import { AuditModule } from "../audit/audit.module"; +import { PaymentsService } from "./payments.service"; +import { Payout } from "./entities/payout.entity"; +import { PayoutRepository } from "./payout.repository"; @Module({ - // AuditModule is imported so the @SensitiveMutation-tagged route above - // can resolve AuditInterceptor's dependencies (Issue #551). - imports: [AuditModule], + imports: [AuditModule, TypeOrmModule.forFeature([Payout, PayoutRepository])], controllers: [PaymentsController], - providers: [HorizonService], + providers: [HorizonService, PaymentsService], exports: [], }) export class PaymentsModule {} diff --git a/app/backend/src/payments/payments.service.ts b/app/backend/src/payments/payments.service.ts new file mode 100644 index 000000000..9dd39e621 --- /dev/null +++ b/app/backend/src/payments/payments.service.ts @@ -0,0 +1,52 @@ +import { + Injectable, + NotFoundException, + ConflictException, + InternalServerErrorException, +} from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { PayoutRepository } from "./payout.repository"; +import { PayoutDto } from "./dto/payout.dto"; +import { Payout, PayoutStatus } from "./entities/payout.entity"; + +@Injectable() +export class PaymentsService { + constructor( + @InjectRepository(PayoutRepository) + private payoutRepository: PayoutRepository, + ) {} + + async createPayout(payoutDto: PayoutDto): Promise { + // TODO: Add balance check + // TODO: Add duplicate check + + try { + const payout = this.payoutRepository.create(payoutDto); + await this.payoutRepository.save(payout); + return payout; + } catch (error) { + throw new InternalServerErrorException("Error creating payout"); + } + } + + async releasePayout(payoutId: string): Promise { + const payout = await this.payoutRepository.findOne(payoutId); + + if (!payout) { + throw new NotFoundException(`Payout with ID "${payoutId}" not found`); + } + + if (payout.status !== PayoutStatus.Pending) { + throw new ConflictException( + `Payout with ID "${payoutId}" is not pending`, + ); + } + + // TODO: Implement payout release logic + + payout.status = PayoutStatus.Released; + await this.payoutRepository.save(payout); + + return payout; + } +} diff --git a/app/backend/src/payments/payout.repository.ts b/app/backend/src/payments/payout.repository.ts new file mode 100644 index 000000000..b083f6e1b --- /dev/null +++ b/app/backend/src/payments/payout.repository.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm"; +import { Payout } from "./entities/payout.entity"; + +@EntityRepository(Payout) +export class PayoutRepository extends Repository {} diff --git a/package-lock.json b/package-lock.json index e1bd7dc05..10e7a9523 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@types/express": "^5.0.6", "nodemon": "^3.1.14", "ts-node": "^10.9.2", - "turbo": "^2.3.3", + "turbo": "^2.10.12", "typescript": "^5.3.3" } }, @@ -119,6 +119,92 @@ "dev": true, "license": "MIT" }, + "node_modules/@turbo/darwin-64": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/@turbo/darwin-64/-/darwin-64-2.10.12.tgz", + "integrity": "sha512-9nKgKoF6ZOUsM+or0OtNf+TTJSfGvDNP7ZFv/ZGWVwOSCkumyctQiTeHwB4UNljHTnC41AqylgbunLDHoccNrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@turbo/darwin-arm64": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/@turbo/darwin-arm64/-/darwin-arm64-2.10.12.tgz", + "integrity": "sha512-H4Elb1jqTZVeIC9bbcNwjSzemZ6RegoTOVHeuV5Osirt2Z8UguTyisMEkvZjPVZgMeN9J4ERZBFad40tFnkb7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@turbo/linux-64": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/@turbo/linux-64/-/linux-64-2.10.12.tgz", + "integrity": "sha512-lr7KIotukvjZwEXiFSYAeOH3BWzjFVBbSzTbv0fuGFsNukYyH0+g1hB5ecqnJkgkYU+KHEMG1edOhnjiKON1wQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android", + "linux" + ] + }, + "node_modules/@turbo/linux-arm64": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/@turbo/linux-arm64/-/linux-arm64-2.10.12.tgz", + "integrity": "sha512-f0pZDTtvzB5SuNwuXBaKbZHUCMCukgc8nMlHEuvLmj91Fzec+MEbr3cAvGNor5htEDqZnO6Lxt9N/GPI/77oGA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android", + "linux" + ] + }, + "node_modules/@turbo/windows-64": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/@turbo/windows-64/-/windows-64-2.10.12.tgz", + "integrity": "sha512-SDOueJRjS/QcykWf2KCRtTLmIl5YMKsLbXkXQGhDwcTXvKXZiS5ih5lBl/gkwZIpYFjqA/rAlfMzlAFcVHNe0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@turbo/windows-arm64": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/@turbo/windows-arm64/-/windows-arm64-2.10.12.tgz", + "integrity": "sha512-0i0mVUa4kKk+/B3RwEwPMf9CB+T7ul56hn5FFHNA4VUNTOoLBEd6aNf3FaKfCatDNZ6cicCEf6if9QUTVyzzcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -2052,99 +2138,22 @@ } }, "node_modules/turbo": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.8.10.tgz", - "integrity": "sha512-OxbzDES66+x7nnKGg2MwBA1ypVsZoDTLHpeaP4giyiHSixbsiTaMyeJqbEyvBdp5Cm28fc+8GG6RdQtic0ijwQ==", + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.10.12.tgz", + "integrity": "sha512-AswgMPnpOoaVZHrrSBejETzEbuIA69OVGwfkHwfrY0A23VjWXBANzgq9+OymWOHAIArB7D1+1z498WY8fGg1Jw==", "dev": true, + "license": "MIT", "bin": { "turbo": "bin/turbo" }, "optionalDependencies": { - "turbo-darwin-64": "2.8.10", - "turbo-darwin-arm64": "2.8.10", - "turbo-linux-64": "2.8.10", - "turbo-linux-arm64": "2.8.10", - "turbo-windows-64": "2.8.10", - "turbo-windows-arm64": "2.8.10" - } - }, - "node_modules/turbo-darwin-64": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.8.10.tgz", - "integrity": "sha512-A03fXh+B7S8mL3PbdhTd+0UsaGrhfyPkODvzBDpKRY7bbeac4MDFpJ7I+Slf2oSkCEeSvHKR7Z4U71uKRUfX7g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/turbo-darwin-arm64": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-2.8.10.tgz", - "integrity": "sha512-sidzowgWL3s5xCHLeqwC9M3s9M0i16W1nuQF3Mc7fPHpZ+YPohvcbVFBB2uoRRHYZg6yBnwD4gyUHKTeXfwtXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/turbo-linux-64": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.8.10.tgz", - "integrity": "sha512-YK9vcpL3TVtqonB021XwgaQhY9hJJbKKUhLv16osxV0HkcQASQWUqR56yMge7puh6nxU67rQlTq1b7ksR1T3KA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/turbo-linux-arm64": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.8.10.tgz", - "integrity": "sha512-3+j2tL0sG95iBJTm+6J8/45JsETQABPqtFyYjVjBbi6eVGdtNTiBmHNKrbvXRlQ3ZbUG75bKLaSSDHSEEN+btQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/turbo-windows-64": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.8.10.tgz", - "integrity": "sha512-hdeF5qmVY/NFgiucf8FW0CWJWtyT2QPm5mIsX0W1DXAVzqKVXGq+Zf+dg4EUngAFKjDzoBeN6ec2Fhajwfztkw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/turbo-windows-arm64": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.8.10.tgz", - "integrity": "sha512-QGdr/Q8LWmj+ITMkSvfiz2glf0d7JG0oXVzGL3jxkGqiBI1zXFj20oqVY0qWi+112LO9SVrYdpHS0E/oGFrMbQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] + "@turbo/darwin-64": "2.10.12", + "@turbo/darwin-arm64": "2.10.12", + "@turbo/linux-64": "2.10.12", + "@turbo/linux-arm64": "2.10.12", + "@turbo/windows-64": "2.10.12", + "@turbo/windows-arm64": "2.10.12" + } }, "node_modules/tweetnacl": { "version": "1.0.3", diff --git a/package.json b/package.json index 3467913fd..4d4d9442e 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@types/express": "^5.0.6", "nodemon": "^3.1.14", "ts-node": "^10.9.2", - "turbo": "^2.3.3", + "turbo": "^2.10.12", "typescript": "^5.3.3" }, "pnpm": {