From 43304c882b3fb32abac2473c9bc1bd41756945ea Mon Sep 17 00:00:00 2001 From: AI Bot Date: Sat, 30 May 2026 16:51:49 +0100 Subject: [PATCH 1/3] feat: emit SessionApproved event with required signature (#547) --- contract/src/lib.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/contract/src/lib.rs b/contract/src/lib.rs index c6aa30f2f..b28c05924 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -474,8 +474,16 @@ impl SkillSyncEscrow { ); session.status = Status::Approved; Self::save_session_internal(&env, &session_id, &session); - env.events() - .publish((Symbol::new(&env, "SessionApproved"), session_id), session.amount); + env.events().publish( + (Symbol::new(&env, "SessionApproved"), session_id), + ( + session.buyer, + session.seller, + session.amount, + 0_i128, // fee is currently 0 in SkillSyncEscrow + env.ledger().timestamp(), + ), + ); } // ── #526: refund_session ────────────────────────────────────────────────── From c954159f5fdf1003c7e4f67db691c1639aff3cce Mon Sep 17 00:00:00 2001 From: AI Bot Date: Sat, 30 May 2026 17:20:23 +0100 Subject: [PATCH 2/3] feat: common API response formatter, featured mentor flag, and DisputeOpened event (#495, #492, #550) --- backend/package-lock.json | 13 +++ backend/src/admin/admin.controller.ts | 33 ++++++- backend/src/admin/admin.module.ts | 3 +- backend/src/app.module.ts | 2 + backend/src/auth/entities/audit-log.entity.ts | 2 + .../common/filters/http-exception.filter.ts | 1 + .../interceptors/response.interceptor.ts | 67 +++++++++++++++ backend/src/main.ts | 3 + .../src/users/cron/featured-mentor.cron.ts | 53 ++++++++++++ backend/src/users/entities/user.entity.ts | 11 +++ backend/src/users/mentors.controller.ts | 17 ++++ backend/src/users/users.module.ts | 6 +- backend/src/users/users.service.ts | 77 +++++++++++++++++ contract/src/lib.rs | 29 +++++++ contract/src/test.rs | 85 +++++++++++++++++++ 15 files changed, 398 insertions(+), 4 deletions(-) create mode 100644 backend/src/common/interceptors/response.interceptor.ts create mode 100644 backend/src/users/cron/featured-mentor.cron.ts create mode 100644 backend/src/users/mentors.controller.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 63a73879c..1c5c4eeba 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -3058,6 +3058,19 @@ } } }, + "node_modules/@nestjs/typeorm": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.1.tgz", + "integrity": "sha512-8rw/nKT0S+L+MkzgE9F2/mox7mAgsPlwfzmW9gsESN1lmQtIrVEfiiBwC2O8+guS1jBfQehJIdcdUj2OAp4VUQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0", + "typeorm": "^0.3.0 || ^1.0.0-dev" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", diff --git a/backend/src/admin/admin.controller.ts b/backend/src/admin/admin.controller.ts index 4f0dfbeef..ef5308f36 100644 --- a/backend/src/admin/admin.controller.ts +++ b/backend/src/admin/admin.controller.ts @@ -21,11 +21,42 @@ import { VerifyMentorBodyDto } from './dto/admin.dto'; import { ProfileHistoryQueryDto } from '../availability/dto/availability-query.dto'; import { SuspendUserDto } from './dto/suspend-user.dto'; +import { UsersService } from '../users/users.service'; + @Controller('admin') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(AuthRole.ADMIN) export class AdminController { - constructor(private readonly adminService: AdminService) {} + constructor( + private readonly adminService: AdminService, + private readonly usersService: UsersService, + ) {} + + @Post('mentors/:id/feature') + featureMentor( + @Param('id') id: string, + @Req() req: Request & { user?: JwtPayload }, + ) { + const audit = { + ipAddress: (req.ip || req.socket?.remoteAddress || null) as string | null, + userAgent: req.headers['user-agent'] || null, + deviceFingerprint: null, + }; + return this.usersService.featureMentor(id, audit); + } + + @Delete('mentors/:id/unfeature') + unfeatureMentor( + @Param('id') id: string, + @Req() req: Request & { user?: JwtPayload }, + ) { + const audit = { + ipAddress: (req.ip || req.socket?.remoteAddress || null) as string | null, + userAgent: req.headers['user-agent'] || null, + deviceFingerprint: null, + }; + return this.usersService.unfeatureMentor(id, audit); + } @Post('mentors/:id/verify') verifyMentor( diff --git a/backend/src/admin/admin.module.ts b/backend/src/admin/admin.module.ts index f01345bbe..a34af7cd3 100644 --- a/backend/src/admin/admin.module.ts +++ b/backend/src/admin/admin.module.ts @@ -6,9 +6,10 @@ import { AdminController } from './admin.controller'; import { AdminService } from './admin.service'; import { ProfileHistorySubscriber } from './profile-history.subscriber'; import { AuthModule } from '../auth/auth.module'; +import { UsersModule } from '../users/users.module'; @Module({ - imports: [TypeOrmModule.forFeature([User, ProfileHistory]), AuthModule], + imports: [TypeOrmModule.forFeature([User, ProfileHistory]), AuthModule, UsersModule], controllers: [AdminController], providers: [AdminService, ProfileHistorySubscriber], }) diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 5652e5d31..7c6828ca5 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,4 +1,5 @@ import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TypeOrmModule } from '@nestjs/typeorm'; @@ -16,6 +17,7 @@ import { AdminModule } from './admin/admin.module'; @Module({ imports: [ + ScheduleModule.forRoot(), ConfigModule.forRoot({ isGlobal: true }), TypeOrmModule.forRoot({ ...AppDataSource.options, diff --git a/backend/src/auth/entities/audit-log.entity.ts b/backend/src/auth/entities/audit-log.entity.ts index 597473721..e490531c0 100644 --- a/backend/src/auth/entities/audit-log.entity.ts +++ b/backend/src/auth/entities/audit-log.entity.ts @@ -13,6 +13,8 @@ export enum AuditEventType { UNSUSPEND_USER = 'UNSUSPEND_USER', SUSPICIOUS_ACTIVITY = 'SUSPICIOUS_ACTIVITY', USERNAME_CHANGED = 'USERNAME_CHANGED', + MENTOR_FEATURED = 'MENTOR_FEATURED', + MENTOR_UNFEATURED = 'MENTOR_UNFEATURED', } @Entity({ name: 'audit_logs' }) diff --git a/backend/src/common/filters/http-exception.filter.ts b/backend/src/common/filters/http-exception.filter.ts index 574d3d644..1c13b4f3f 100644 --- a/backend/src/common/filters/http-exception.filter.ts +++ b/backend/src/common/filters/http-exception.filter.ts @@ -91,6 +91,7 @@ export class HttpExceptionFilter implements ExceptionFilter { responseBody.code ?? mapHttpStatusToErrorCode(status, error.toString()); const payload: Record = { + success: false, statusCode: status, message, error, diff --git a/backend/src/common/interceptors/response.interceptor.ts b/backend/src/common/interceptors/response.interceptor.ts new file mode 100644 index 000000000..0fa6fe63b --- /dev/null +++ b/backend/src/common/interceptors/response.interceptor.ts @@ -0,0 +1,67 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Request, Response } from 'express'; + +export interface ResponseEnvelope { + success: boolean; + statusCode: number; + message: string; + data: T; + timestamp: string; + path: string; +} + +@Injectable() +export class ResponseInterceptor + implements NestInterceptor> +{ + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable> { + const ctx = context.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + const statusCode = response.statusCode; + const path = request.originalUrl || request.url || ''; + const timestamp = new Date().toISOString(); + + return next.handle().pipe( + map((data) => { + // If data is already an envelope, return it as is + if ( + data && + typeof data === 'object' && + 'success' in data && + 'statusCode' in data + ) { + return data; + } + + // Determine message + let message = 'Operation successful'; + if (data && typeof data === 'object' && 'message' in data && typeof data.message === 'string') { + message = data.message; + // Optionally remove message from data if you don't want it duplicated + // delete data.message; + } + + return { + success: true, + statusCode, + message, + data: data !== undefined ? data : null, + timestamp, + path, + }; + }), + ); + } +} diff --git a/backend/src/main.ts b/backend/src/main.ts index 1f735df9d..295a789cc 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -7,6 +7,8 @@ import { DataSource } from 'typeorm'; import { ApiValidationException } from './common/exceptions/api-exceptions'; import { HttpExceptionFilter } from './common/filters/http-exception.filter'; +import { ResponseInterceptor } from './common/interceptors/response.interceptor'; + async function bootstrap() { const app = await NestFactory.create(AppModule, { // Disable NestJS built-in logger noise; our middleware handles request logs @@ -28,6 +30,7 @@ async function bootstrap() { }), ); app.useGlobalFilters(new HttpExceptionFilter()); + app.useGlobalInterceptors(new ResponseInterceptor()); // Verify database connection before starting server const dataSource = app.get(DataSource); diff --git a/backend/src/users/cron/featured-mentor.cron.ts b/backend/src/users/cron/featured-mentor.cron.ts new file mode 100644 index 000000000..24decf6d4 --- /dev/null +++ b/backend/src/users/cron/featured-mentor.cron.ts @@ -0,0 +1,53 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, LessThan } from 'typeorm'; +import { User } from '../entities/user.entity'; +import { AuditLogService } from '../../auth/audit-log.service'; +import { AuditEventType } from '../../auth/entities/audit-log.entity'; + +@Injectable() +export class FeaturedMentorCron { + private readonly logger = new Logger(FeaturedMentorCron.name); + + constructor( + @InjectRepository(User) + private readonly userRepo: Repository, + private readonly auditLogService: AuditLogService, + ) {} + + @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) + async handleCron() { + this.logger.debug('Running expired featured mentors cleanup job'); + + // 30 days ago + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + + const expiredMentors = await this.userRepo.find({ + where: { + isFeatured: true, + featuredAt: LessThan(thirtyDaysAgo), + }, + }); + + if (expiredMentors.length === 0) { + return; + } + + for (const mentor of expiredMentors) { + mentor.isFeatured = false; + mentor.featuredAt = null; + mentor.featuredOrder = null; + await this.userRepo.save(mentor); + + await this.auditLogService.logEvent({ + userId: mentor.id, + eventType: 'MENTOR_UNFEATURED' as AuditEventType, + details: { reason: 'Expired after 30 days' }, + }); + + this.logger.log(`Unfeatured mentor ${mentor.id} due to expiry`); + } + } +} diff --git a/backend/src/users/entities/user.entity.ts b/backend/src/users/entities/user.entity.ts index 9d415037e..1c37d444e 100644 --- a/backend/src/users/entities/user.entity.ts +++ b/backend/src/users/entities/user.entity.ts @@ -63,6 +63,17 @@ export class User { @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) updatedAt!: Date; + @Index() + @Column({ name: 'is_featured', type: 'boolean', default: false }) + isFeatured!: boolean; + + @Column({ name: 'featured_at', type: 'timestamptz', nullable: true }) + featuredAt!: Date | null; + + @Index() + @Column({ name: 'featured_order', type: 'int', nullable: true }) + featuredOrder!: number | null; + @ManyToMany(() => Role, (role) => role.users, { eager: true }) @JoinTable({ name: 'user_roles', diff --git a/backend/src/users/mentors.controller.ts b/backend/src/users/mentors.controller.ts new file mode 100644 index 000000000..61213b794 --- /dev/null +++ b/backend/src/users/mentors.controller.ts @@ -0,0 +1,17 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { UsersService } from './users.service'; + +@Controller('mentors') +export class MentorsController { + constructor(private readonly usersService: UsersService) {} + + @Get('featured') + async getFeaturedMentors( + @Query('page') page?: string, + @Query('limit') limit?: string, + ) { + const pageNum = page ? parseInt(page, 10) || 1 : 1; + const limitNum = limit ? Math.min(parseInt(limit, 10) || 10, 50) : 10; + return this.usersService.getFeaturedMentors(pageNum, limitNum); + } +} diff --git a/backend/src/users/users.module.ts b/backend/src/users/users.module.ts index ce6651940..bbfdf323f 100644 --- a/backend/src/users/users.module.ts +++ b/backend/src/users/users.module.ts @@ -3,14 +3,16 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Role } from './entities/role.entity'; import { User } from './entities/user.entity'; import { UsersController } from './users.controller'; +import { MentorsController } from './mentors.controller'; import { UsersService } from './users.service'; import { AuthModule } from '../auth/auth.module'; import { AuditLogService } from '../auth/audit-log.service'; +import { FeaturedMentorCron } from './cron/featured-mentor.cron'; @Module({ imports: [TypeOrmModule.forFeature([User, Role]), AuthModule], - controllers: [UsersController], - providers: [UsersService, AuditLogService], + controllers: [UsersController, MentorsController], + providers: [UsersService, AuditLogService, FeaturedMentorCron], exports: [UsersService], }) export class UsersModule {} diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index 3356a66f5..6f25cc28e 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -156,4 +156,81 @@ export class UsersService implements OnModuleInit { async findByUsername(username: string): Promise { return this.userRepo.findOne({ where: { username } }); } + + async featureMentor(userId: string, audit?: RequestAudit): Promise { + const user = await this.userRepo.findOne({ where: { id: userId } }); + if (!user) throw new NotFoundException('Mentor not found'); + + // Enforce max featured mentors + const maxFeatured = 10; + const currentFeaturedCount = await this.userRepo.count({ where: { isFeatured: true } }); + + if (!user.isFeatured && currentFeaturedCount >= maxFeatured) { + throw new BadRequestException(`Maximum of ${maxFeatured} featured mentors reached`); + } + + user.isFeatured = true; + user.featuredAt = new Date(); + // Sort logic (just set to count if not set) + if (user.featuredOrder === null || user.featuredOrder === undefined) { + const maxOrderUser = await this.userRepo.findOne({ + where: { isFeatured: true }, + order: { featuredOrder: 'DESC' } + }); + user.featuredOrder = maxOrderUser?.featuredOrder != null ? maxOrderUser.featuredOrder + 1 : 1; + } + + const saved = await this.userRepo.save(user); + + if (audit) { + await this.auditLogService.logEvent({ + userId, + eventType: 'MENTOR_FEATURED' as AuditEventType, + audit, + details: { featuredOrder: user.featuredOrder }, + }); + } + + return saved; + } + + async unfeatureMentor(userId: string, audit?: RequestAudit): Promise { + const user = await this.userRepo.findOne({ where: { id: userId } }); + if (!user) throw new NotFoundException('Mentor not found'); + + user.isFeatured = false; + user.featuredAt = null; + user.featuredOrder = null; + + const saved = await this.userRepo.save(user); + + if (audit) { + await this.auditLogService.logEvent({ + userId, + eventType: 'MENTOR_UNFEATURED' as AuditEventType, + audit, + details: {}, + }); + } + + return saved; + } + + async getFeaturedMentors(page: number = 1, limit: number = 10): Promise<{ items: User[], meta: { total: number, page: number, lastPage: number } }> { + const [items, total] = await this.userRepo.findAndCount({ + where: { isFeatured: true }, + order: { featuredOrder: 'ASC' }, + skip: (page - 1) * limit, + take: limit, + }); + + return { + items, + meta: { + total, + page, + lastPage: Math.ceil(total / limit), + }, + }; + } } diff --git a/contract/src/lib.rs b/contract/src/lib.rs index b28c05924..bb2a3f5ec 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -456,6 +456,35 @@ impl SkillSyncEscrow { ); } + // ── #550: dispute_session ────────────────────────────────────────────────── + + /// Opens a dispute for a session. + /// - Caller can be buyer or seller. + /// - Session must be in Locked or Completed state. + /// - Emits DisputeOpened event. + pub fn dispute_session(env: Env, session_id: Bytes32, opened_by: Address, reason: soroban_sdk::String) { + let mut session = Self::get_session_internal(&env, &session_id); + + opened_by.require_auth(); + assert!( + opened_by == session.buyer || opened_by == session.seller, + "Unauthorized: must be buyer or seller" + ); + + assert!( + session.status == Status::Locked || session.status == Status::Completed, + "InvalidState: session must be Locked or Completed" + ); + + session.status = Status::Disputed; + Self::save_session_internal(&env, &session_id, &session); + + env.events().publish( + (Symbol::new(&env, "DisputeOpened"), session_id.clone()), + (opened_by, reason, env.ledger().timestamp()), + ); + } + // ── #526: approve_session ───────────────────────────────────────────────── /// Buyer approves completed session, releasing funds to seller. diff --git a/contract/src/test.rs b/contract/src/test.rs index f700c7193..3723f9798 100644 --- a/contract/src/test.rs +++ b/contract/src/test.rs @@ -777,4 +777,89 @@ mod test_skillsync_escrow { assert_eq!(event_data.old_wasm_hash, new_wasm_hash); assert_eq!(event_data.new_wasm_hash, newer_wasm_hash); } + + // ── #550: dispute_session ──────────────────────────────────────────────── + + #[test] + fn test_dispute_session_by_buyer_succeeds() { + let (env, admin, buyer, seller, token_id, cid) = setup(); + let client = SkillSyncEscrowClient::new(&env, &cid); + client.initialize(&admin); + let id = make_id(&env, 100); + client.lock_funds(&id, &buyer, &seller, &100, &token_id); + + let reason = soroban_sdk::String::from_str(&env, "Service not delivered"); + client.dispute_session(&id, &buyer, &reason); + + let s = client.get_session(&id); + assert!(matches!(s.status, Status::Disputed)); + + let events = env.events().all(); + let last = events.last().unwrap(); + assert_eq!(last.0, cid); + assert_eq!( + last.1, + (Symbol::new(&env, "DisputeOpened"), id.clone()).into_val(&env) + ); + assert_eq!( + last.2, + (buyer, reason, env.ledger().timestamp()).into_val(&env) + ); + } + + #[test] + fn test_dispute_session_by_seller_succeeds() { + let (env, admin, buyer, seller, token_id, cid) = setup(); + let client = SkillSyncEscrowClient::new(&env, &cid); + client.initialize(&admin); + let id = make_id(&env, 101); + client.lock_funds(&id, &buyer, &seller, &100, &token_id); + client.complete_session(&id); // Seller completes it + + let reason = soroban_sdk::String::from_str(&env, "Buyer unresponsive"); + client.dispute_session(&id, &seller, &reason); + + let s = client.get_session(&id); + assert!(matches!(s.status, Status::Disputed)); + + let events = env.events().all(); + let last = events.last().unwrap(); + assert_eq!(last.0, cid); + assert_eq!( + last.1, + (Symbol::new(&env, "DisputeOpened"), id.clone()).into_val(&env) + ); + assert_eq!( + last.2, + (seller, reason, env.ledger().timestamp()).into_val(&env) + ); + } + + #[test] + #[should_panic(expected = "Unauthorized: must be buyer or seller")] + fn test_dispute_session_by_unauthorized_reverts() { + let (env, admin, buyer, seller, token_id, cid) = setup(); + let client = SkillSyncEscrowClient::new(&env, &cid); + client.initialize(&admin); + let id = make_id(&env, 102); + client.lock_funds(&id, &buyer, &seller, &100, &token_id); + + let random_user = Address::generate(&env); + let reason = soroban_sdk::String::from_str(&env, "Random reason"); + client.dispute_session(&id, &random_user, &reason); + } + + #[test] + #[should_panic(expected = "InvalidState: session must be Locked or Completed")] + fn test_dispute_session_invalid_state_reverts() { + let (env, admin, buyer, seller, token_id, cid) = setup(); + let client = SkillSyncEscrowClient::new(&env, &cid); + client.initialize(&admin); + let id = make_id(&env, 103); + client.lock_funds(&id, &buyer, &seller, &100, &token_id); + client.refund_session(&id, &token_id); // State becomes Refunded + + let reason = soroban_sdk::String::from_str(&env, "Want to dispute after refund"); + client.dispute_session(&id, &buyer, &reason); // Should panic + } } From 74f64c229327bb4ea52c2a6bc39600aad3917325 Mon Sep 17 00:00:00 2001 From: AI Bot Date: Sat, 30 May 2026 17:29:27 +0100 Subject: [PATCH 3/3] fix: resolve failing test suite and missing stellar-sdk dependency --- backend/package-lock.json | 493 +++++++++++++++++++- backend/package.json | 1 + backend/src/admin/admin.service.ts | 4 +- backend/src/auth/auth.service.spec.ts | 30 +- backend/src/auth/suspension.service.spec.ts | 10 +- backend/src/users/users.service.spec.ts | 30 +- backend/tsconfig.json | 1 - 7 files changed, 526 insertions(+), 43 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 1c5c4eeba..bd9c0cd82 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -23,6 +23,7 @@ "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "sharp": "^0.33.5", + "stellar-sdk": "^13.3.0", "typeorm": "^1.0.0" }, "devDependencies": { @@ -3174,6 +3175,56 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", "license": "MIT" }, + "node_modules/@stellar/js-xdr": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.2.tgz", + "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==", + "license": "Apache-2.0" + }, + "node_modules/@stellar/stellar-base": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-13.1.0.tgz", + "integrity": "sha512-90EArG+eCCEzDGj3OJNoCtwpWDwxjv+rs/RNPhvg4bulpjN/CSRj+Ys/SalRcfM4/WRC5/qAfjzmJBAuquWhkA==", + "license": "Apache-2.0", + "dependencies": { + "@stellar/js-xdr": "^3.1.2", + "base32.js": "^0.1.0", + "bignumber.js": "^9.1.2", + "buffer": "^6.0.3", + "sha.js": "^2.3.6", + "tweetnacl": "^1.0.3" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "sodium-native": "^4.3.3" + } + }, + "node_modules/@stellar/stellar-base/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/@tokenizer/inflate": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", @@ -4380,6 +4431,18 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -4581,9 +4644,35 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/babel-jest": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", @@ -4690,11 +4779,63 @@ "dev": true, "license": "MIT" }, + "node_modules/bare-addon-resolve": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz", + "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-module-resolve": "^1.10.0", + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-module-resolve": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.2.tgz", + "integrity": "sha512-j+hiD5k99qec4KjJvYsI67q5AOBifmy9JG3oeMVxTmvrhn2sIdp8StrUvZu4YNgwTpO+NhniQG16N1ETDe1k5w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-semver": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.3.tgz", + "integrity": "sha512-HS/A30bi2+PiRJfU6R4+Kp+6KeLSCSByjYM2iiobOKzLAvtu1CT+S8xWfiU7wz0erknjkUoC+yXy108tzIuP5Q==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/base32.js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", + "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -4724,6 +4865,15 @@ "node": ">=6.0.0" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -4955,6 +5105,24 @@ "node": ">= 0.8" } }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -5312,7 +5480,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -5609,11 +5776,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -5843,7 +6026,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6117,6 +6299,15 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -6297,6 +6488,15 @@ } } }, + "node_modules/feaxios": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/feaxios/-/feaxios-0.0.23.tgz", + "integrity": "sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==", + "license": "MIT", + "dependencies": { + "is-retry-allowed": "^3.0.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -6400,6 +6600,41 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -6449,7 +6684,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -6466,7 +6700,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6476,7 +6709,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -6815,6 +7047,18 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -6831,7 +7075,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -6882,6 +7125,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -7041,6 +7297,18 @@ "dev": true, "license": "MIT" }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -7109,6 +7377,18 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-retry-allowed": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-3.0.0.tgz", + "integrity": "sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -7122,6 +7402,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -8997,6 +9292,15 @@ "node": ">=4" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -9084,6 +9388,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -9126,6 +9439,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -9221,6 +9543,19 @@ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "license": "Apache-2.0" }, + "node_modules/require-addon": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", + "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-addon-resolve": "^1.3.0" + }, + "engines": { + "bare": ">=1.10.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -9422,12 +9757,49 @@ "url": "https://opencollective.com/express" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/sharp": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", @@ -9600,6 +9972,16 @@ "node": ">=8" } }, + "node_modules/sodium-native": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz", + "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==", + "license": "MIT", + "optional": true, + "dependencies": { + "require-addon": "^1.1.0" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -9692,6 +10074,26 @@ "node": ">= 0.8" } }, + "node_modules/stellar-sdk": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/stellar-sdk/-/stellar-sdk-13.3.0.tgz", + "integrity": "sha512-jAA3+U7oAUueldoS4kuEhcym+DigElWq9isPxt7tjMrE7kTJ2vvY29waavUb2FSfQIWwGbuwAJTYddy2BeyJsw==", + "deprecated": "⚠️ This package has moved to @stellar/stellar-sdk! 🚚", + "license": "Apache-2.0", + "dependencies": { + "@stellar/stellar-base": "^13.1.0", + "axios": "^1.8.4", + "bignumber.js": "^9.3.0", + "eventsource": "^2.0.2", + "feaxios": "^0.0.23", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -10178,6 +10580,26 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10218,6 +10640,12 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -10409,6 +10837,12 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -10476,6 +10910,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -10875,6 +11323,12 @@ "punycode": "^2.1.0" } }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -10991,6 +11445,27 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.21.tgz", + "integrity": "sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", diff --git a/backend/package.json b/backend/package.json index d7a98e1a9..4480bc524 100644 --- a/backend/package.json +++ b/backend/package.json @@ -38,6 +38,7 @@ "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "sharp": "^0.33.5", + "stellar-sdk": "^13.3.0", "typeorm": "^1.0.0" }, "devDependencies": { diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts index ceebede49..e25e9a27c 100644 --- a/backend/src/admin/admin.service.ts +++ b/backend/src/admin/admin.service.ts @@ -50,6 +50,8 @@ export class AdminService { take: limit, skip: offset, }); + return { items, total }; + } async suspendUser( userId: string, @@ -67,6 +69,4 @@ export class AdminService { async listSuspendedUsers(limit = 50, offset = 0) { return this.suspensionService.listActiveSuspensions(limit, offset); } - return { items, total }; - } } diff --git a/backend/src/auth/auth.service.spec.ts b/backend/src/auth/auth.service.spec.ts index 9bd2b8b9c..b8de19fd7 100644 --- a/backend/src/auth/auth.service.spec.ts +++ b/backend/src/auth/auth.service.spec.ts @@ -105,7 +105,7 @@ describe('AuthService', () => { beforeEach(async () => { jest.useFakeTimers().setSystemTime(new Date('2026-01-01T00:00:00.000Z')); repository = new InMemoryRefreshTokenRepository(); - const userStore = { + userStore = { users: [] as User[], findOne: jest.fn(async (options: { where: { walletAddress?: string; id?: string } }) => { if (options.where.walletAddress) { @@ -132,6 +132,10 @@ describe('AuthService', () => { logRefreshTokenUsage: jest.fn().mockResolvedValue(undefined), logLoginSuccess: jest.fn().mockResolvedValue(undefined), logLoginFailure: jest.fn().mockResolvedValue(undefined), + logLogout: jest.fn().mockResolvedValue(undefined), + logPasswordEquivalentChange: jest.fn().mockResolvedValue(undefined), + logRoleAssignment: jest.fn().mockResolvedValue(undefined), + }; suspensionServiceMock = { getActiveSuspension: jest.fn().mockResolvedValue(null) }; const module: TestingModule = await Test.createTestingModule({ @@ -152,9 +156,6 @@ describe('AuthService', () => { transaction: ( handler: (entityManager: InMemoryEntityManager) => Promise, ) => handler(manager), - transaction: ( - handler: (entityManager: InMemoryEntityManager) => Promise, - ) => handler(manager), }, }, { @@ -178,6 +179,7 @@ describe('AuthService', () => { }); it('rotates refresh tokens and invalidates the previous token', async () => { + userStore.users.push({ id: 'user-1', walletAddress: 'GABC', roles: [] } as any); const issued = await service.issueTokenPair( { sub: 'user-1', walletAddress: 'GABC' }, audit(), @@ -200,6 +202,7 @@ describe('AuthService', () => { }); it('returns 401 when the refresh token is expired', async () => { + userStore.users.push({ id: 'user-1', walletAddress: 'GABC', roles: [] } as any); const issued = await service.issueTokenPair({ sub: 'user-1' }, audit()); jest.setSystemTime(new Date('2026-02-01T00:00:01.000Z')); @@ -207,7 +210,11 @@ describe('AuthService', () => { UnauthorizedException, ); expect(auditLogService.logRefreshTokenUsage).toHaveBeenCalledWith( - blocks login for suspended users with suspension reason', async () => { + expect.objectContaining({ success: false, userId: null }), + ); + }); + + it('blocks login for suspended users with suspension reason', async () => { userStore.users.push({ id: 'user-1', walletAddress: 'GABC', @@ -237,13 +244,15 @@ describe('AuthService', () => { await expect(service.login('GABC', audit())).rejects.toBeInstanceOf(ForbiddenException); expect(auditLogService.logLoginFailure).toHaveBeenCalledWith( - 'GABC', - expect.anything(), - 'Account suspended', + expect.objectContaining({ + attemptedWalletAddress: 'GABC', + reason: 'Account suspended', + }) ); }); it('blocks refresh for suspended users with suspension reason', async () => { + userStore.users.push({ id: 'user-1', walletAddress: 'GABC', roles: [] } as any); const issued = await service.issueTokenPair({ sub: 'user-1', walletAddress: 'GABC' }, audit()); const suspended = { id: 'suspension-1', @@ -264,11 +273,8 @@ describe('AuthService', () => { ); }); - it('expect.objectContaining({ success: false, userId: 'user-1' }), - ); - }); - it('detects refresh token reuse and revokes the token family', async () => { + userStore.users.push({ id: 'user-1', walletAddress: 'GABC', roles: [] } as any); const issued = await service.issueTokenPair({ sub: 'user-1' }, audit()); await service.refresh(issued.refreshToken, audit()); diff --git a/backend/src/auth/suspension.service.spec.ts b/backend/src/auth/suspension.service.spec.ts index 4f0416944..6ef3c0331 100644 --- a/backend/src/auth/suspension.service.spec.ts +++ b/backend/src/auth/suspension.service.spec.ts @@ -50,6 +50,8 @@ describe('SuspensionService', () => { }; const auditLogService = { logEvent: jest.fn(), + logUserSuspension: jest.fn(), + logUserUnsuspension: jest.fn(), }; const manager = { create: jest.fn(), @@ -129,9 +131,9 @@ describe('SuspensionService', () => { suspendedBy: 'admin-1', })); expect(refreshUpdateBuilder.execute).toHaveBeenCalled(); - expect(auditLogService.logEvent).toHaveBeenCalledWith(expect.objectContaining({ + expect(auditLogService.logUserSuspension).toHaveBeenCalledWith(expect.objectContaining({ userId: 'user-1', - eventType: 'SUSPEND_USER', + suspendedBy: 'admin-1', })); }); }); @@ -164,9 +166,9 @@ describe('SuspensionService', () => { expect(result.liftedAt).toBeInstanceOf(Date); expect(result.liftedBy).toBe('admin-1'); - expect(auditLogService.logEvent).toHaveBeenCalledWith(expect.objectContaining({ + expect(auditLogService.logUserUnsuspension).toHaveBeenCalledWith(expect.objectContaining({ userId: 'user-1', - eventType: 'UNSUSPEND_USER', + liftedBy: 'admin-1', })); }); }); diff --git a/backend/src/users/users.service.spec.ts b/backend/src/users/users.service.spec.ts index b3a626f3f..3b05fd363 100644 --- a/backend/src/users/users.service.spec.ts +++ b/backend/src/users/users.service.spec.ts @@ -118,7 +118,7 @@ describe('UsersService - Username Functionality', () => { }); describe('updateUsername', () => { - const mockUser: User = { + const getMockUser = (): User => ({ id: 'user-1', walletAddress: 'GABC123...', username: null, @@ -135,7 +135,7 @@ describe('UsersService - Username Functionality', () => { createdAt: new Date(), updatedAt: new Date(), roles: [], - }; + }); const mockAudit: RequestAudit = { ipAddress: '127.0.0.1', @@ -144,8 +144,8 @@ describe('UsersService - Username Functionality', () => { }; it('should successfully update username when valid and available', async () => { - mockUserRepo.findOne.mockResolvedValueOnce(mockUser).mockResolvedValueOnce(null); - mockUserRepo.save.mockResolvedValue({ ...mockUser, username: 'john_doe', usernameChangedAt: new Date() }); + mockUserRepo.findOne.mockResolvedValueOnce(getMockUser()).mockResolvedValueOnce(null); + mockUserRepo.save.mockResolvedValue({ ...getMockUser(), username: 'john_doe', usernameChangedAt: new Date() }); const result = await service.updateUsername('user-1', 'john_doe', mockAudit); @@ -163,9 +163,9 @@ describe('UsersService - Username Functionality', () => { }); it('should set default display name based on wallet address when not set', async () => { - mockUserRepo.findOne.mockResolvedValueOnce(mockUser).mockResolvedValueOnce(null); + mockUserRepo.findOne.mockResolvedValueOnce(getMockUser()).mockResolvedValueOnce(null); mockUserRepo.save.mockResolvedValue({ - ...mockUser, + ...getMockUser(), username: 'john_doe', usernameChangedAt: new Date(), displayName: 'GABC12...3...' @@ -191,8 +191,8 @@ describe('UsersService - Username Functionality', () => { }); it('should throw ConflictException when username is already taken by another user', async () => { - const otherUser = { ...mockUser, id: 'user-2', username: 'john_doe' }; - mockUserRepo.findOne.mockResolvedValueOnce(mockUser).mockResolvedValueOnce(otherUser); + const otherUser = { ...getMockUser(), id: 'user-2', username: 'john_doe' }; + mockUserRepo.findOne.mockResolvedValueOnce(getMockUser()).mockResolvedValueOnce(otherUser); await expect(service.updateUsername('user-1', 'john_doe', mockAudit)).rejects.toThrow( ConflictException, @@ -200,7 +200,7 @@ describe('UsersService - Username Functionality', () => { }); it('should allow updating to same username (no conflict with self)', async () => { - const userWithUsername = { ...mockUser, username: 'john_doe' }; + const userWithUsername = { ...getMockUser(), username: 'john_doe' }; mockUserRepo.findOne.mockResolvedValueOnce(userWithUsername).mockResolvedValueOnce(userWithUsername); mockUserRepo.save.mockResolvedValue({ ...userWithUsername, usernameChangedAt: new Date() }); @@ -211,7 +211,7 @@ describe('UsersService - Username Functionality', () => { it('should throw BadRequestException when username changed within cooldown period', async () => { const userWithRecentChange = { - ...mockUser, + ...getMockUser(), username: 'old_username', usernameChangedAt: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000), // 15 days ago }; @@ -225,7 +225,7 @@ describe('UsersService - Username Functionality', () => { it('should allow username change after 30 day cooldown period', async () => { const userWithExpiredCooldown = { - ...mockUser, + ...getMockUser(), username: 'old_username', usernameChangedAt: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000), // 31 days ago }; @@ -242,8 +242,8 @@ describe('UsersService - Username Functionality', () => { }); it('should allow first username change (no previous change)', async () => { - mockUserRepo.findOne.mockResolvedValueOnce(mockUser).mockResolvedValueOnce(null); - mockUserRepo.save.mockResolvedValue({ ...mockUser, username: 'john_doe', usernameChangedAt: new Date() }); + mockUserRepo.findOne.mockResolvedValueOnce(getMockUser()).mockResolvedValueOnce(null); + mockUserRepo.save.mockResolvedValue({ ...getMockUser(), username: 'john_doe', usernameChangedAt: new Date() }); const result = await service.updateUsername('user-1', 'john_doe', mockAudit); @@ -251,8 +251,8 @@ describe('UsersService - Username Functionality', () => { }); it('should not log audit event when audit parameter is not provided', async () => { - mockUserRepo.findOne.mockResolvedValueOnce(mockUser).mockResolvedValueOnce(null); - mockUserRepo.save.mockResolvedValue({ ...mockUser, username: 'john_doe', usernameChangedAt: new Date() }); + mockUserRepo.findOne.mockResolvedValueOnce(getMockUser()).mockResolvedValueOnce(null); + mockUserRepo.save.mockResolvedValue({ ...getMockUser(), username: 'john_doe', usernameChangedAt: new Date() }); await service.updateUsername('user-1', 'john_doe'); diff --git a/backend/tsconfig.json b/backend/tsconfig.json index ed40b1928..8ddbfd146 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -14,7 +14,6 @@ "sourceMap": true, "outDir": "./dist", "baseUrl": "./", - "ignoreDeprecations": "6.0", "incremental": true, "skipLibCheck": true, "strictNullChecks": true,