From 73fffee2f77d1ead969e71073e08acce3528ff79 Mon Sep 17 00:00:00 2001 From: George Date: Tue, 13 Oct 2026 18:47:51 -0500 Subject: [PATCH] =?UTF-8?q?feat(api):=20permisos=20de=20men=C3=BA=20en=20S?= =?UTF-8?q?QLite=20con=20API=20de=20administraci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SQLite (better-sqlite3 + TypeORM, WAL) con tabla menu_permissions (usuario web, patrón de ruta); archivo fuera del repo. - GET /api/user/menu para el frontend; GET/PUT/DELETE /api/permissions solo para administradores (acceso "*"). - ADMIN_WEB_USERS garantiza el acceso total al arrancar; no se puede quitar el "*" al último administrador (409, dentro de una transacción). - Patrones validados (`*`, `/modulo/ruta`, `/modulo/*`). A diferencia del frontend anterior, `/inventario/*` ya no habilita `/inventario-x`. - Helper de e2e compartido (SAP simulado, SQLite en memoria). Co-Authored-By: Claude Sonnet 5.5 --- .gitignore | 3 + apps/api/.env.example | 5 + apps/api/README.md | 22 + apps/api/package.json | 4 + apps/api/src/app.module.ts | 4 +- apps/api/src/config/env.schema.spec.ts | 6 + apps/api/src/config/env.schema.ts | 13 + apps/api/src/database/database.module.ts | 29 + apps/api/src/permissions/admin.guard.ts | 17 + .../src/permissions/menu-permission.entity.ts | 15 + .../src/permissions/permissions.controller.ts | 57 ++ .../api/src/permissions/permissions.module.ts | 14 + .../permissions/permissions.service.spec.ts | 60 ++ .../src/permissions/permissions.service.ts | 83 +++ apps/api/test/auth.e2e-spec.ts | 30 +- apps/api/test/health.e2e-spec.ts | 14 +- apps/api/test/permissions.e2e-spec.ts | 86 +++ apps/api/test/support/create-test-app.ts | 51 ++ package-lock.json | 646 +++++++++++++++++- 19 files changed, 1104 insertions(+), 55 deletions(-) create mode 100644 apps/api/src/database/database.module.ts create mode 100644 apps/api/src/permissions/admin.guard.ts create mode 100644 apps/api/src/permissions/menu-permission.entity.ts create mode 100644 apps/api/src/permissions/permissions.controller.ts create mode 100644 apps/api/src/permissions/permissions.module.ts create mode 100644 apps/api/src/permissions/permissions.service.spec.ts create mode 100644 apps/api/src/permissions/permissions.service.ts create mode 100644 apps/api/test/permissions.e2e-spec.ts create mode 100644 apps/api/test/support/create-test-app.ts diff --git a/.gitignore b/.gitignore index 859d7f6..125fa11 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ logs/ apps/api/certs/*.pem apps/api/certs/*.cer apps/api/certs/*.crt +*.sqlite-wal +*.sqlite-shm +apps/api/data/ diff --git a/apps/api/.env.example b/apps/api/.env.example index bd94c30..832b524 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -7,6 +7,11 @@ LOG_LEVEL=log # Inactividad (minutos) tras la cual caduca la sesión web SESSION_TIMEOUT_MINUTES=30 +# Base SQLite local (permisos de menú) +SQLITE_PATH=./data/app.sqlite +# Usuarios web (U_WEB_USER) con acceso total "*", garantizado al arrancar (separar con comas) +ADMIN_WEB_USERS= + # --- SAP Business One Service Layer --- SAPB1_SL_URL=https://servidor-sap:50000/b1s/v1 SAPB1_SL_COMPANYDB= diff --git a/apps/api/README.md b/apps/api/README.md index 144215b..fce2edd 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -32,3 +32,25 @@ Guard global: todo `/api/*` exige `Authorization: Bearer ` salvo rutas `@ ```bash curl -s -H 'Content-Type: application/json' -d '{"username":"USUARIO","password":"CLAVE"}' localhost:3000/api/auth/login ``` + +## Permisos de menú + +Se guardan en SQLite (`SQLITE_PATH`, por defecto `./data/app.sqlite`, fuera del repo). Un usuario web (`U_WEB_USER`) tiene una lista de patrones de ruta: + +| Patrón | Permite | +|---|---| +| `*` | Todo (administrador) | +| `/ventas/*` | `/ventas` y todo lo que cuelga de `/ventas/` (no `/ventas-x`) | +| `/ventas/ordenes` | Solo esa ruta | + +| Endpoint | Quién | Descripción | +|---|---|---| +| `GET /api/user/menu` | Cualquier sesión | `{allowedPaths}` del usuario actual | +| `GET /api/permissions`, `GET /api/permissions/:webUser` | Administrador | Consulta | +| `PUT /api/permissions/:webUser` `{paths}` | Administrador | Reemplaza los permisos | +| `DELETE /api/permissions/:webUser` | Administrador | Elimina los permisos | + +- `ADMIN_WEB_USERS=george,ana` garantiza `*` a esos usuarios en cada arranque (nunca se queda el sistema sin acceso). +- El sistema no deja quitar el acceso total al último administrador (409). +- El esquema se sincroniza automáticamente (`synchronize`); pasar a migraciones cuando se estabilice por cliente. +- Si `better-sqlite3` falla al instalar en una máquina nueva: `npm rebuild better-sqlite3`. diff --git a/apps/api/package.json b/apps/api/package.json index fc203e7..c13afee 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -26,8 +26,11 @@ "@nestjs/config": "^12.0.1", "@nestjs/core": "^12.0.1", "@nestjs/platform-express": "^12.0.1", + "@nestjs/typeorm": "^12.0.2", + "better-sqlite3": "^12.11.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "typeorm": "^1.1.2", "undici": "^6.20.1", "zod": "^4.6.7" }, @@ -36,6 +39,7 @@ "@nestjs/mau": "^0.2.6", "@nestjs/schematics": "^12.0.0", "@nestjs/testing": "^12.0.1", + "@types/better-sqlite3": "^9.6.0", "@types/express": "^5.0.0", "@types/node": "^24.0.0", "@types/supertest": "^7.0.0", diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 168e702..ff01a20 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -1,11 +1,13 @@ import { Module } from '@nestjs/common'; import { AuthModule } from './auth/auth.module.js'; import { AppConfigModule } from './config/config.module.js'; +import { DatabaseModule } from './database/database.module.js'; import { HealthController } from './health/health.controller.js'; +import { PermissionsModule } from './permissions/permissions.module.js'; import { SapModule } from './sap/sap.module.js'; @Module({ - imports: [AppConfigModule, SapModule, AuthModule], + imports: [AppConfigModule, DatabaseModule, SapModule, AuthModule, PermissionsModule], controllers: [HealthController], }) export class AppModule {} diff --git a/apps/api/src/config/env.schema.spec.ts b/apps/api/src/config/env.schema.spec.ts index f55399d..3faf36d 100644 --- a/apps/api/src/config/env.schema.spec.ts +++ b/apps/api/src/config/env.schema.spec.ts @@ -16,6 +16,12 @@ describe('validateEnv', () => { expect(env.SAPB1_SL_VERIFY_SSL).toBe(true); expect(env.SAPB1_SL_CA_FILE).toBeUndefined(); expect(env.SAPB1_SL_MAX_CONCURRENCY).toBe(1); + expect(env.SQLITE_PATH).toBe('./data/app.sqlite'); + expect(env.ADMIN_WEB_USERS).toEqual([]); + }); + + it('separa ADMIN_WEB_USERS', () => { + expect(validateEnv({ ...sap, ADMIN_WEB_USERS: 'george, ana ,' }).ADMIN_WEB_USERS).toEqual(['george', 'ana']); }); it('quita la barra final de la URL de SAP', () => { diff --git a/apps/api/src/config/env.schema.ts b/apps/api/src/config/env.schema.ts index a0b5b3e..50b44bd 100644 --- a/apps/api/src/config/env.schema.ts +++ b/apps/api/src/config/env.schema.ts @@ -19,6 +19,19 @@ export const envSchema = z .filter(Boolean), ), LOG_LEVEL: z.enum(['error', 'warn', 'log', 'debug', 'verbose']).default('log'), + // Base SQLite local (permisos de menú). `:memory:` para pruebas. + SQLITE_PATH: z.string().min(1).default('./data/app.sqlite'), + // Usuarios web (U_WEB_USER) que siempre tienen acceso total ("*"); se + // garantiza al arrancar para no quedar fuera por un error de permisos. + ADMIN_WEB_USERS: z + .string() + .default('') + .transform((v) => + v + .split(',') + .map((s) => s.trim()) + .filter(Boolean), + ), // Inactividad tras la cual caduca la sesión web (deslizante). SESSION_TIMEOUT_MINUTES: z.coerce.number().int().positive().default(30), diff --git a/apps/api/src/database/database.module.ts b/apps/api/src/database/database.module.ts new file mode 100644 index 0000000..d1db36b --- /dev/null +++ b/apps/api/src/database/database.module.ts @@ -0,0 +1,29 @@ +import { mkdirSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AppConfigService } from '../config/app-config.service.js'; + +/** Base SQLite local de la aplicación (hoy: permisos de menú). */ +@Module({ + imports: [ + TypeOrmModule.forRootAsync({ + inject: [AppConfigService], + useFactory: (config: AppConfigService) => { + const database = config.get('SQLITE_PATH'); + const inMemory = database === ':memory:'; + if (!inMemory) mkdirSync(dirname(resolve(database)), { recursive: true }); + return { + type: 'better-sqlite3' as const, + database, + enableWAL: !inMemory, + autoLoadEntities: true, + // Una sola tabla pequeña: el esquema se sincroniza solo. Cuando el + // esquema se estabilice por cliente, pasar a migraciones. + synchronize: true, + }; + }, + }), + ], +}) +export class DatabaseModule {} diff --git a/apps/api/src/permissions/admin.guard.ts b/apps/api/src/permissions/admin.guard.ts new file mode 100644 index 0000000..7727054 --- /dev/null +++ b/apps/api/src/permissions/admin.guard.ts @@ -0,0 +1,17 @@ +import { type CanActivate, type ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common'; +import type { AuthenticatedRequest } from '../auth/auth.types.js'; +import { PermissionsService } from './permissions.service.js'; + +/** Solo usuarios con acceso total ("*"). Se ejecuta después del guard de sesión global. */ +@Injectable() +export class AdminGuard implements CanActivate { + constructor(private readonly permissions: PermissionsService) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + if (!(await this.permissions.isAdmin(req.user.webUser))) { + throw new ForbiddenException('Requiere permisos de administrador'); + } + return true; + } +} diff --git a/apps/api/src/permissions/menu-permission.entity.ts b/apps/api/src/permissions/menu-permission.entity.ts new file mode 100644 index 0000000..7667837 --- /dev/null +++ b/apps/api/src/permissions/menu-permission.entity.ts @@ -0,0 +1,15 @@ +import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm'; + +/** Un patrón de ruta permitido para un usuario web (`*`, `/ventas/*`, `/ventas/ordenes`). */ +@Entity('menu_permissions') +@Unique(['webUser', 'pattern']) +export class MenuPermission { + @PrimaryGeneratedColumn() + id: number; + + @Column() + webUser: string; + + @Column() + pattern: string; +} diff --git a/apps/api/src/permissions/permissions.controller.ts b/apps/api/src/permissions/permissions.controller.ts new file mode 100644 index 0000000..278829b --- /dev/null +++ b/apps/api/src/permissions/permissions.controller.ts @@ -0,0 +1,57 @@ +import { Body, Controller, Delete, Get, HttpCode, Param, Put, Req, UseGuards } from '@nestjs/common'; +import { z } from 'zod'; +import type { AuthenticatedRequest } from '../auth/auth.types.js'; +import { ZodValidationPipe } from '../common/zod-validation.pipe.js'; +import { AdminGuard } from './admin.guard.js'; +import { PermissionsService } from './permissions.service.js'; + +// `*`, `/ventas/ordenes` o `/ventas/*` +const pathPattern = z + .string() + .max(100) + .regex(/^(\*|(\/[A-Za-z0-9_-]+)+(\/\*)?)$/, 'Patrón inválido: use "*", "/modulo/ruta" o "/modulo/*"'); +const setPathsSchema = z.object({ paths: z.array(pathPattern).max(200) }); +const webUserSchema = z.string().trim().min(1).max(100); +type SetPathsDto = z.infer; + +/** Menú permitido para el usuario de la sesión (lo consume el frontend). */ +@Controller('user') +export class UserMenuController { + constructor(private readonly permissions: PermissionsService) {} + + @Get('menu') + async menu(@Req() req: AuthenticatedRequest) { + return { allowedPaths: await this.permissions.getPaths(req.user.webUser) }; + } +} + +/** Administración de permisos (solo acceso total). */ +@Controller('permissions') +@UseGuards(AdminGuard) +export class PermissionsController { + constructor(private readonly permissions: PermissionsService) {} + + @Get() + list() { + return this.permissions.listAll(); + } + + @Get(':webUser') + async get(@Param('webUser', new ZodValidationPipe(webUserSchema)) webUser: string) { + return { webUser, paths: await this.permissions.getPaths(webUser) }; + } + + @Put(':webUser') + set( + @Param('webUser', new ZodValidationPipe(webUserSchema)) webUser: string, + @Body(new ZodValidationPipe(setPathsSchema)) body: SetPathsDto, + ) { + return this.permissions.setPaths(webUser, body.paths); + } + + @Delete(':webUser') + @HttpCode(204) + remove(@Param('webUser', new ZodValidationPipe(webUserSchema)) webUser: string) { + return this.permissions.remove(webUser); + } +} diff --git a/apps/api/src/permissions/permissions.module.ts b/apps/api/src/permissions/permissions.module.ts new file mode 100644 index 0000000..6bba02e --- /dev/null +++ b/apps/api/src/permissions/permissions.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AdminGuard } from './admin.guard.js'; +import { MenuPermission } from './menu-permission.entity.js'; +import { PermissionsController, UserMenuController } from './permissions.controller.js'; +import { PermissionsService } from './permissions.service.js'; + +@Module({ + imports: [TypeOrmModule.forFeature([MenuPermission])], + controllers: [UserMenuController, PermissionsController], + providers: [PermissionsService, AdminGuard], + exports: [PermissionsService], +}) +export class PermissionsModule {} diff --git a/apps/api/src/permissions/permissions.service.spec.ts b/apps/api/src/permissions/permissions.service.spec.ts new file mode 100644 index 0000000..5b45c2f --- /dev/null +++ b/apps/api/src/permissions/permissions.service.spec.ts @@ -0,0 +1,60 @@ +import { ConflictException } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import type { AppConfigService } from '../config/app-config.service.js'; +import { MenuPermission } from './menu-permission.entity.js'; +import { PermissionsService } from './permissions.service.js'; + +describe('PermissionsService (SQLite en memoria)', () => { + let ds: DataSource; + let service: PermissionsService; + + beforeEach(async () => { + ds = new DataSource({ type: 'better-sqlite3', database: ':memory:', entities: [MenuPermission], synchronize: true }); + await ds.initialize(); + service = new PermissionsService(ds.getRepository(MenuPermission), { get: () => [] } as unknown as AppConfigService); + }); + + afterEach(async () => { + await ds.destroy(); + }); + + it('setPaths reemplaza y quita duplicados', async () => { + await service.setPaths('luis', ['/ventas/*', '/compras/*']); + await service.setPaths('luis', ['/inventario/*', '/inventario/*']); + expect(await service.getPaths('luis')).toEqual(['/inventario/*']); + }); + + it('listAll agrupa por usuario', async () => { + await service.setPaths('b', ['/x']); + await service.setPaths('a', ['/z', '/y']); + expect(await service.listAll()).toEqual([ + { webUser: 'a', paths: ['/y', '/z'] }, + { webUser: 'b', paths: ['/x'] }, + ]); + }); + + it('ensureAdmins es idempotente y conserva otros permisos', async () => { + await service.setPaths('ana', ['/ventas/*']); + await service.ensureAdmins(['ana', 'ana', 'luis']); + await service.ensureAdmins(['ana']); + expect(await service.getPaths('ana')).toEqual(['*', '/ventas/*']); + expect(await service.isAdmin('luis')).toBe(true); + }); + + it('protege al último administrador (setPaths y remove)', async () => { + await service.ensureAdmins(['ana']); + await expect(service.setPaths('ana', ['/ventas/*'])).rejects.toBeInstanceOf(ConflictException); + await expect(service.remove('ana')).rejects.toBeInstanceOf(ConflictException); + expect(await service.getPaths('ana')).toEqual(['*']); // la transacción no dejó cambios + + await service.ensureAdmins(['luis']); + await service.remove('ana'); + expect(await service.getPaths('ana')).toEqual([]); + }); + + it('no permite duplicar (usuario, patrón) a nivel de base de datos', async () => { + const repo = ds.getRepository(MenuPermission); + await repo.insert({ webUser: 'a', pattern: '/x' }); + await expect(repo.insert({ webUser: 'a', pattern: '/x' })).rejects.toThrow(); + }); +}); diff --git a/apps/api/src/permissions/permissions.service.ts b/apps/api/src/permissions/permissions.service.ts new file mode 100644 index 0000000..2cfc992 --- /dev/null +++ b/apps/api/src/permissions/permissions.service.ts @@ -0,0 +1,83 @@ +import { ConflictException, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Not, Repository } from 'typeorm'; +import { AppConfigService } from '../config/app-config.service.js'; +import { MenuPermission } from './menu-permission.entity.js'; + +export const ALL_ACCESS = '*'; + +export interface UserPermissions { + webUser: string; + paths: string[]; +} + +@Injectable() +export class PermissionsService implements OnApplicationBootstrap { + private readonly logger = new Logger(PermissionsService.name); + + constructor( + @InjectRepository(MenuPermission) private readonly repo: Repository, + private readonly config: AppConfigService, + ) {} + + async onApplicationBootstrap() { + await this.ensureAdmins(this.config.get('ADMIN_WEB_USERS')); + } + + async getPaths(webUser: string): Promise { + const rows = await this.repo.find({ where: { webUser }, order: { pattern: 'ASC' } }); + return rows.map((r) => r.pattern); + } + + async listAll(): Promise { + const rows = await this.repo.find({ order: { webUser: 'ASC', pattern: 'ASC' } }); + const byUser = new Map(); + for (const r of rows) byUser.set(r.webUser, [...(byUser.get(r.webUser) ?? []), r.pattern]); + return [...byUser].map(([webUser, paths]) => ({ webUser, paths })); + } + + async isAdmin(webUser: string): Promise { + return this.repo.existsBy({ webUser, pattern: ALL_ACCESS }); + } + + /** Reemplaza todos los permisos del usuario. */ + async setPaths(webUser: string, paths: string[]): Promise { + const unique = [...new Set(paths)].sort(); + await this.repo.manager.transaction(async (manager) => { + const repo = manager.getRepository(MenuPermission); + if (!unique.includes(ALL_ACCESS)) await this.assertNotLastAdmin(repo, webUser); + await repo.delete({ webUser }); + if (unique.length) await repo.insert(unique.map((pattern) => ({ webUser, pattern }))); + }); + this.logger.log(`Permisos de ${webUser} actualizados (${unique.length} patrones)`); + return { webUser, paths: unique }; + } + + async remove(webUser: string): Promise { + await this.repo.manager.transaction(async (manager) => { + const repo = manager.getRepository(MenuPermission); + await this.assertNotLastAdmin(repo, webUser); + await repo.delete({ webUser }); + }); + this.logger.log(`Permisos de ${webUser} eliminados`); + } + + /** Garantiza el acceso total de los administradores configurados en el .env. */ + async ensureAdmins(webUsers: string[]): Promise { + for (const webUser of webUsers) { + if (await this.isAdmin(webUser)) continue; + await this.repo.insert({ webUser, pattern: ALL_ACCESS }); + this.logger.log(`Administrador "${webUser}" garantizado desde ADMIN_WEB_USERS`); + } + } + + /** Evita dejar el sistema sin ningún usuario con acceso total. */ + private async assertNotLastAdmin(repo: Repository, webUser: string) { + const isAdminNow = await repo.existsBy({ webUser, pattern: ALL_ACCESS }); + if (!isAdminNow) return; + const others = await repo.countBy({ pattern: ALL_ACCESS, webUser: Not(webUser) }); + if (others === 0) { + throw new ConflictException('No se puede quitar el acceso total al último administrador'); + } + } +} diff --git a/apps/api/test/auth.e2e-spec.ts b/apps/api/test/auth.e2e-spec.ts index ef81921..bbaeffb 100644 --- a/apps/api/test/auth.e2e-spec.ts +++ b/apps/api/test/auth.e2e-spec.ts @@ -1,36 +1,12 @@ -import { Test } from '@nestjs/testing'; import type { INestApplication } from '@nestjs/common'; import request from 'supertest'; - -process.env.SAPB1_SL_URL ??= 'https://sap.invalid:50000/b1s/v1'; -process.env.SAPB1_SL_COMPANYDB ??= 'DB'; -process.env.SAPB1_SL_USER ??= 'u'; -process.env.SAPB1_SL_PASSWORD ??= 'p'; - -const EMPLOYEES = [ - { EmployeeID: 7, FirstName: 'Ana', LastName: 'Pérez', Active: 'tYES', U_WEB_USER: 'ana', U_WEB_PASS: 'secreto' }, -]; +import { createTestApp, employee } from './support/create-test-app.js'; describe('Auth (e2e, SAP simulado)', () => { let app: INestApplication; beforeEach(async () => { - const { AppModule } = await import('../src/app.module.js'); - const { SapClientService } = await import('../src/sap/sap-client.service.js'); - const { SapSessionService } = await import('../src/sap/sap-session.service.js'); - const moduleRef = await Test.createTestingModule({ imports: [AppModule] }) - .overrideProvider(SapClientService) - .useValue({ - get: async (_path: string, q: { $filter: string }) => ({ - data: { value: q.$filter === "U_WEB_USER eq 'ana'" ? EMPLOYEES : [] }, - }), - }) - .overrideProvider(SapSessionService) - .useValue({ getSession: async () => ({}), status: () => ({ connected: true }), onModuleDestroy: () => undefined }) - .compile(); - app = moduleRef.createNestApplication(); - app.setGlobalPrefix('api'); - await app.init(); + app = await createTestApp([employee('ana', 7)]); }); afterEach(async () => { @@ -47,7 +23,7 @@ describe('Auth (e2e, SAP simulado)', () => { it('flujo completo: login -> me -> ruta protegida -> logout -> 401', async () => { const res = await login('ana', 'secreto').expect(200); - expect(res.body.user).toMatchObject({ webUser: 'ana', empId: 7, firstName: 'Ana' }); + expect(res.body.user).toMatchObject({ webUser: 'ana', empId: 7, firstName: 'ana' }); expect(JSON.stringify(res.body)).not.toContain('secreto'); const auth = { Authorization: `Bearer ${res.body.token}` }; diff --git a/apps/api/test/health.e2e-spec.ts b/apps/api/test/health.e2e-spec.ts index eaac46b..3393468 100644 --- a/apps/api/test/health.e2e-spec.ts +++ b/apps/api/test/health.e2e-spec.ts @@ -1,22 +1,12 @@ -import { Test } from '@nestjs/testing'; import type { INestApplication } from '@nestjs/common'; import request from 'supertest'; - -// Valores ficticios: este e2e no se conecta a SAP (el login es perezoso). -process.env.SAPB1_SL_URL ??= 'https://sap.invalid:50000/b1s/v1'; -process.env.SAPB1_SL_COMPANYDB ??= 'DB'; -process.env.SAPB1_SL_USER ??= 'u'; -process.env.SAPB1_SL_PASSWORD ??= 'p'; +import { createTestApp } from './support/create-test-app.js'; describe('Health (e2e)', () => { let app: INestApplication; beforeAll(async () => { - const { AppModule } = await import('../src/app.module.js'); - const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile(); - app = moduleRef.createNestApplication(); - app.setGlobalPrefix('api'); - await app.init(); + app = await createTestApp([]); }); afterAll(async () => { diff --git a/apps/api/test/permissions.e2e-spec.ts b/apps/api/test/permissions.e2e-spec.ts new file mode 100644 index 0000000..6ae6023 --- /dev/null +++ b/apps/api/test/permissions.e2e-spec.ts @@ -0,0 +1,86 @@ +import type { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { createTestApp, employee } from './support/create-test-app.js'; + +describe('Permisos de menú (e2e)', () => { + let app: INestApplication; + let admin: { Authorization: string }; + let luis: { Authorization: string }; + + const loginAs = async (username: string) => { + const res = await request(app.getHttpServer()).post('/api/auth/login').send({ username, password: 'secreto' }).expect(200); + return { Authorization: `Bearer ${res.body.token}` }; + }; + + beforeEach(async () => { + app = await createTestApp([employee('ana', 1), employee('luis', 2)], 'ana'); + admin = await loginAs('ana'); + }); + + afterEach(async () => { + await app.close(); + }); + + it('el administrador del .env obtiene "*" al arrancar', async () => { + const res = await request(app.getHttpServer()).get('/api/user/menu').set(admin).expect(200); + expect(res.body).toEqual({ allowedPaths: ['*'] }); + }); + + it('un usuario sin permisos recibe menú vacío', async () => { + luis = await loginAs('luis'); + const res = await request(app.getHttpServer()).get('/api/user/menu').set(luis).expect(200); + expect(res.body.allowedPaths).toEqual([]); + }); + + it('el admin asigna permisos y el usuario los ve; un no-admin no puede administrar', async () => { + await request(app.getHttpServer()) + .put('/api/permissions/luis') + .set(admin) + .send({ paths: ['/ventas/*', '/inventario/articulos', '/ventas/*'] }) + .expect(200) + .expect({ webUser: 'luis', paths: ['/inventario/articulos', '/ventas/*'] }); + + luis = await loginAs('luis'); + const menu = await request(app.getHttpServer()).get('/api/user/menu').set(luis).expect(200); + expect(menu.body.allowedPaths).toEqual(['/inventario/articulos', '/ventas/*']); + + await request(app.getHttpServer()).get('/api/permissions').set(luis).expect(403); + await request(app.getHttpServer()).put('/api/permissions/luis').set(luis).send({ paths: ['*'] }).expect(403); + }); + + it('PUT reemplaza, GET lista y DELETE elimina', async () => { + await request(app.getHttpServer()).put('/api/permissions/luis').set(admin).send({ paths: ['/ventas/*'] }).expect(200); + await request(app.getHttpServer()).put('/api/permissions/luis').set(admin).send({ paths: ['/compras/*'] }).expect(200); + + const list = await request(app.getHttpServer()).get('/api/permissions').set(admin).expect(200); + expect(list.body).toEqual([ + { webUser: 'ana', paths: ['*'] }, + { webUser: 'luis', paths: ['/compras/*'] }, + ]); + + await request(app.getHttpServer()).delete('/api/permissions/luis').set(admin).expect(204); + const one = await request(app.getHttpServer()).get('/api/permissions/luis').set(admin).expect(200); + expect(one.body.paths).toEqual([]); + }); + + it('rechaza patrones inválidos', async () => { + for (const bad of ['ventas', '/ventas/', '/ventas/*/x', '/a b', '**', '']) { + await request(app.getHttpServer()).put('/api/permissions/luis').set(admin).send({ paths: [bad] }).expect(400); + } + await request(app.getHttpServer()).put('/api/permissions/luis').set(admin).send({}).expect(400); + }); + + it('no permite quitar el acceso total al último administrador', async () => { + await request(app.getHttpServer()).put('/api/permissions/ana').set(admin).send({ paths: ['/ventas/*'] }).expect(409); + await request(app.getHttpServer()).delete('/api/permissions/ana').set(admin).expect(409); + + // con un segundo administrador, ana sí puede dejar de serlo + await request(app.getHttpServer()).put('/api/permissions/luis').set(admin).send({ paths: ['*'] }).expect(200); + await request(app.getHttpServer()).put('/api/permissions/ana').set(admin).send({ paths: ['/ventas/*'] }).expect(200); + }); + + it('sin sesión no se accede al menú ni a la administración', async () => { + await request(app.getHttpServer()).get('/api/user/menu').expect(401); + await request(app.getHttpServer()).get('/api/permissions').expect(401); + }); +}); diff --git a/apps/api/test/support/create-test-app.ts b/apps/api/test/support/create-test-app.ts new file mode 100644 index 0000000..f0f04ae --- /dev/null +++ b/apps/api/test/support/create-test-app.ts @@ -0,0 +1,51 @@ +import { Test } from '@nestjs/testing'; +import type { INestApplication } from '@nestjs/common'; + +// Valores ficticios: los e2e no se conectan a SAP ni a un archivo SQLite. +process.env.SAPB1_SL_URL ??= 'https://sap.invalid:50000/b1s/v1'; +process.env.SAPB1_SL_COMPANYDB ??= 'DB'; +process.env.SAPB1_SL_USER ??= 'u'; +process.env.SAPB1_SL_PASSWORD ??= 'p'; +process.env.SQLITE_PATH = ':memory:'; + +export interface FakeEmployee { + EmployeeID: number; + FirstName: string; + LastName: string; + Active: string; + U_WEB_USER: string; + U_WEB_PASS: string; +} + +export const employee = (webUser: string, id = 1, pass = 'secreto'): FakeEmployee => ({ + EmployeeID: id, + FirstName: webUser, + LastName: 'Test', + Active: 'tYES', + U_WEB_USER: webUser, + U_WEB_PASS: pass, +}); + +/** App completa con EmployeesInfo y la sesión SAP simulados. */ +export async function createTestApp(employees: FakeEmployee[], adminWebUsers = ''): Promise { + process.env.ADMIN_WEB_USERS = adminWebUsers; + const { AppModule } = await import('../../src/app.module.js'); + const { SapClientService } = await import('../../src/sap/sap-client.service.js'); + const { SapSessionService } = await import('../../src/sap/sap-session.service.js'); + + const moduleRef = await Test.createTestingModule({ imports: [AppModule] }) + .overrideProvider(SapClientService) + .useValue({ + get: async (_path: string, q: { $filter: string }) => ({ + data: { value: employees.filter((e) => q.$filter === `U_WEB_USER eq '${e.U_WEB_USER}'`) }, + }), + }) + .overrideProvider(SapSessionService) + .useValue({ getSession: async () => ({}), status: () => ({ connected: true }), onModuleDestroy: () => undefined }) + .compile(); + + const app = moduleRef.createNestApplication(); + app.setGlobalPrefix('api'); + await app.init(); + return app; +} diff --git a/package-lock.json b/package-lock.json index 2e603ea..30c1f43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,8 +24,11 @@ "@nestjs/config": "^12.0.1", "@nestjs/core": "^12.0.1", "@nestjs/platform-express": "^12.0.1", + "@nestjs/typeorm": "^12.0.2", + "better-sqlite3": "^12.11.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "typeorm": "^1.1.2", "undici": "^6.20.1", "zod": "^4.6.7" }, @@ -34,6 +37,7 @@ "@nestjs/mau": "^0.2.6", "@nestjs/schematics": "^12.0.0", "@nestjs/testing": "^12.0.1", + "@types/better-sqlite3": "^9.6.0", "@types/express": "^5.0.0", "@types/node": "^24.0.0", "@types/supertest": "^7.0.0", @@ -977,6 +981,22 @@ } } }, + "node_modules/@nestjs/typeorm": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-12.0.2.tgz", + "integrity": "sha512-16b8s8mGkQdTH4i+dhhCwYkfuhWpLMdtuK82vOMjwa5ZpKYjFohU2jvbqkEJmx03fVO4HEpb0aw4VQXo+lxdKw==", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0 || ^12.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0 || ^12.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", @@ -1734,6 +1754,12 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1763,6 +1789,16 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, + "node_modules/@types/better-sqlite3": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-9.6.0.tgz", + "integrity": "sha512-ZEEwBSgMu7GYJOynoagg5X9JbxfL6dTJDsgViJIqh67jV44kyOr9RXfmFjLK5rzC4MWssP06t9hu/JwGDnUbCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -2182,7 +2218,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.4.0.tgz", "integrity": "sha512-9k3v7xcHwgdO/DruxGIg4HtjvlAZlcnsX/mzqUb1t3NkYnl9kK2UJ+Gq0io+vQf7iT//BD/HB/NBkUR1LWxoeA==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -2255,7 +2290,6 @@ "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", @@ -2272,11 +2306,33 @@ ], "license": "MIT" }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -2338,7 +2394,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -2475,6 +2530,12 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -2540,6 +2601,99 @@ "node": ">= 12" } }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -2688,6 +2842,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2705,6 +2865,44 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -2741,7 +2939,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -2843,6 +3040,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2908,6 +3114,15 @@ "tests/browser-compat" ] }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2957,6 +3172,15 @@ "node": ">= 0.6" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -3106,7 +3330,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -3154,6 +3377,12 @@ "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -3251,6 +3480,12 @@ "node": ">= 0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3275,11 +3510,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -3325,6 +3568,12 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", @@ -3463,6 +3712,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/inquirer": { "version": "8.2.6", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", @@ -4176,6 +4431,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -4196,12 +4463,17 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4298,6 +4570,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", @@ -4327,6 +4605,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/node-abi": { + "version": "3.98.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.98.0.tgz", + "integrity": "sha512-Llh8noNw7s15m+rmIrZnZQbe4gLyasL9loT74rC92PkmwWKeEedKvc7o6pjp8B3MhrYT/Cl846NOYZbz1wmddw==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-emoji": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", @@ -4660,7 +4950,6 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -4708,6 +4997,33 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prettier": { "version": "3.9.10", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", @@ -4741,6 +5057,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", @@ -4785,11 +5111,25 @@ "node": ">= 0.10" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -4924,7 +5264,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -4951,7 +5290,6 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5103,6 +5441,51 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/skin-tone": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", @@ -5157,6 +5540,22 @@ "node": ">=0.10.0" } }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -5205,7 +5604,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -5249,6 +5647,15 @@ "node": ">=4" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strtok3": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.5.0.tgz", @@ -5314,6 +5721,34 @@ "node": ">=8" } }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -5342,7 +5777,6 @@ "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -5426,6 +5860,18 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", @@ -5470,6 +5916,99 @@ "url": "https://opencollective.com/express" } }, + "node_modules/typeorm": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-1.1.2.tgz", + "integrity": "sha512-UDQ8++2oFCK4fETiRKEC0eCES/i+/1SB4NBqXWeJNosI1i2SVFCDPhqIsXHWpFzFHTBOA553upQZj0NKVbNdUQ==", + "license": "MIT", + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "ansis": "^4.3.1", + "dayjs": "^1.11.23", + "debug": "^4.4.3", + "dedent": "^1.7.2", + "reflect-metadata": "^0.2.2", + "sql-highlight": "^6.1.0", + "tinyglobby": "^0.2.17", + "tslib": "^2.8.1", + "yargs": "^18.1.0" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.11.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^8.0.0", + "@sap/hana-client": "^2.14.22", + "better-sqlite3": "^12.0.0", + "ioredis": "^5.0.4", + "mongodb": "^7.0.0", + "mssql": "^12.0.0", + "mysql2": "^3.15.3", + "oracledb": "^6.3.0 || ^7.0.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^5.0.0 || ^6.0.0", + "sql.js": "^1.4.0", + "ts-node": "^10.9.2", + "typeorm-aurora-data-api-driver": "^3.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -5547,7 +6086,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/vary": { @@ -5827,6 +6365,84 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/yoctocolors": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz",