From b92a1e2566a9758cf0cd59cd3d2a4af7217c6c15 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:04:15 +0200 Subject: [PATCH 01/24] fix(security): remove seeded ADMIN with known password; gate dev seed behind flag [CRITICAL] T-1: the schema seeded test@local.com with a public bcrypt('123456') ADMIN via docker-entrypoint-initdb.d, and the password was documented in the READMEs. POSTGRESDB_PASSWORD also silently defaulted to 'password'. - Drop the hardcoded ADMIN INSERT/setval from db/10.user.sql (schema seeds no users). - Add fail-safe-OFF dev seed db/zz-dev-seed.sh: no-op unless DEV_SEED_ADMIN=true with an explicit BOOTSTRAP_ADMIN_EMAIL + externally-generated bcrypt hash. - Add scripts/gen-admin-hash.sh helper to produce the hash (cost 12). - Make POSTGRESDB_PASSWORD mandatory in docker-compose.db.yml (no :-password default). - Rotate documented credentials in README.md / docs/README_eng.md to seed instructions. - Document the no-secrets-in-schema rule in db/AGENTS.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- .env.example | 13 ++++++++++++- README.md | 20 ++++++++++++++++--- db/10.user.sql | 11 +++++------ db/AGENTS.md | 10 ++++++++-- db/zz-dev-seed.sh | 41 +++++++++++++++++++++++++++++++++++++++ docker-compose.db.yml | 7 ++++++- docs/README_eng.md | 20 ++++++++++++++++--- scripts/gen-admin-hash.sh | 16 +++++++++++++++ 8 files changed, 122 insertions(+), 16 deletions(-) create mode 100755 db/zz-dev-seed.sh create mode 100755 scripts/gen-admin-hash.sh diff --git a/.env.example b/.env.example index b4852f7..95bf97c 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,18 @@ POSTGRESDB_HOST=localhost POSTGRESDB_PORT=5432 POSTGRESDB_DATABASE=your_db_name POSTGRESDB_USER=postgres -POSTGRESDB_PASSWORD=password +# REQUIRED — there is no default. Set a strong value here AND before deploying. +# This dev placeholder is intentionally not a real-looking password; change it. +POSTGRESDB_PASSWORD=dev-only-change-me + +# Dev-only admin bootstrap. Off by default — the schema seeds NO users. +# To create a local admin, set DEV_SEED_ADMIN=true and provide a bcrypt hash: +# bash scripts/gen-admin-hash.sh 'your-strong-password' +# Then paste the output into BOOTSTRAP_ADMIN_PASSWORD_HASH below. +# NEVER enable this in production. +DEV_SEED_ADMIN=false +BOOTSTRAP_ADMIN_EMAIL= +BOOTSTRAP_ADMIN_PASSWORD_HASH= # Public JWT secrets used by the gateway. Access and refresh tokens are # signed with separate secrets so the compromise of one does not let an diff --git a/README.md b/README.md index 5a1d17a..0eb1842 100644 --- a/README.md +++ b/README.md @@ -118,10 +118,24 @@ npm run dev ### Usuario por Defecto +El esquema **no siembra ningún usuario**. Por seguridad no existen credenciales +por defecto. Para crear un administrador local en desarrollo, genera un hash y +habilita el seed (sólo dev): + +```bash +# 1. Genera el hash bcrypt de tu contraseña +bash scripts/gen-admin-hash.sh 'tu-contraseña-fuerte' + +# 2. En tu .env +DEV_SEED_ADMIN=true +BOOTSTRAP_ADMIN_EMAIL=admin@example.test +BOOTSTRAP_ADMIN_PASSWORD_HASH= + +# 3. Recrea la base de datos para que corra el seed +npm run dev:db:clean && npm run dev:db ``` -Email: test@local.com -Contraseña: 123456 -``` + +> Nunca habilites `DEV_SEED_ADMIN` en producción. ## 🛠️ Comandos de Desarrollo diff --git a/db/10.user.sql b/db/10.user.sql index de8ee06..9d58cdb 100644 --- a/db/10.user.sql +++ b/db/10.user.sql @@ -35,9 +35,8 @@ ALTER SEQUENCE public.user_id_seq OWNED BY public.user.id; ALTER TABLE ONLY public.user ALTER COLUMN id SET DEFAULT nextval('public.user_id_seq'::regclass); - -INSERT INTO public.user (id, email, name, lastname, password, permissions) VALUES (1, 'test@local.com', 'Test', 'T', '$2b$10$MFlYds40Bv5jd3BAOvuuk.XrgzeniY84r572RGfbr5/d7O8f5bouy', ARRAY['ADMIN']::permission_type[]); - - - -SELECT pg_catalog.setval('public.user_id_seq', 2); +-- NOTE: this schema file intentionally seeds NO users. A bootstrap admin is +-- created only by the gated, dev-only script `db/zz-dev-seed.sh`, which runs +-- after this file and does nothing unless DEV_SEED_ADMIN=true is set with an +-- explicit BOOTSTRAP_ADMIN_EMAIL / BOOTSTRAP_ADMIN_PASSWORD_HASH. Never ship a +-- known credential as part of the schema. diff --git a/db/AGENTS.md b/db/AGENTS.md index 1e9b4c4..733324b 100644 --- a/db/AGENTS.md +++ b/db/AGENTS.md @@ -36,8 +36,14 @@ next free unit after its base file. - Enums are real PG types (`CREATE TYPE … AS ENUM (...)`) and must match the enum in `libs/rest-dto` (e.g. `permission_type` ↔ `Permission`). - Add indexes for foreign keys and frequent lookup/filter columns. -- Seed/bootstrap rows go at the end of the file and must reset the sequence - (`SELECT pg_catalog.setval(...)`). +- **Never seed credentials or known passwords in schema files.** Schema `.sql` + files define structure only. Bootstrap data that includes secrets (e.g. an + admin user) must live in a **gated, dev-only** entrypoint script + (`db/zz-dev-seed.sh`) that is fail-safe OFF (does nothing unless + `DEV_SEED_ADMIN=true` with an explicit, externally-provided bcrypt hash). + Production compose never sets those vars, so it never seeds. +- Non-secret seed/bootstrap rows may go at the end of a schema file and must + reset the sequence (`SELECT pg_catalog.setval(...)`). ## Workflow when adding an entity diff --git a/db/zz-dev-seed.sh b/db/zz-dev-seed.sh new file mode 100755 index 0000000..b0190ca --- /dev/null +++ b/db/zz-dev-seed.sh @@ -0,0 +1,41 @@ +#!/bin/sh +# --------------------------------------------------------------------------- +# Gated, DEV-ONLY admin bootstrap. +# +# This file lives in the Postgres init pipeline (mounted at +# /docker-entrypoint-initdb.d) but is FAIL-SAFE OFF: it seeds nothing unless +# the operator explicitly opts in. Production (compose.yaml) never sets these +# variables, so the admin is never created there. +# +# To enable in dev, set in your .env: +# DEV_SEED_ADMIN=true +# BOOTSTRAP_ADMIN_EMAIL=admin@example.test +# BOOTSTRAP_ADMIN_PASSWORD_HASH= # generate, never hardcode +# +# Generate the hash with: +# bash scripts/gen-admin-hash.sh 'your-strong-password' +# --------------------------------------------------------------------------- +set -eu + +if [ "${DEV_SEED_ADMIN:-false}" != "true" ]; then + echo "[dev-seed] DEV_SEED_ADMIN is not 'true' — skipping admin seed." + exit 0 +fi + +if [ -z "${BOOTSTRAP_ADMIN_EMAIL:-}" ] || [ -z "${BOOTSTRAP_ADMIN_PASSWORD_HASH:-}" ]; then + echo "[dev-seed] DEV_SEED_ADMIN=true but BOOTSTRAP_ADMIN_EMAIL / BOOTSTRAP_ADMIN_PASSWORD_HASH are unset — skipping (no insecure default)." >&2 + exit 0 +fi + +echo "[dev-seed] seeding bootstrap ADMIN ${BOOTSTRAP_ADMIN_EMAIL}" +psql -v ON_ERROR_STOP=1 \ + --username "$POSTGRES_USER" \ + --dbname "$POSTGRES_DB" \ + -v admin_email="$BOOTSTRAP_ADMIN_EMAIL" \ + -v admin_hash="$BOOTSTRAP_ADMIN_PASSWORD_HASH" <<'EOSQL' +INSERT INTO public.user (email, name, lastname, password, permissions) +VALUES (:'admin_email', 'Admin', 'User', :'admin_hash', ARRAY['ADMIN']::permission_type[]) +ON CONFLICT (email) DO NOTHING; +SELECT pg_catalog.setval('public.user_id_seq', COALESCE((SELECT MAX(id) FROM public.user), 1), true); +EOSQL +echo "[dev-seed] done." diff --git a/docker-compose.db.yml b/docker-compose.db.yml index e6c9fc2..cab0bff 100644 --- a/docker-compose.db.yml +++ b/docker-compose.db.yml @@ -8,7 +8,12 @@ services: environment: POSTGRES_DB: ${POSTGRESDB_DATABASE:-your_db_name} POSTGRES_USER: ${POSTGRESDB_USER:-postgres} - POSTGRES_PASSWORD: ${POSTGRESDB_PASSWORD:-password} + # No insecure default: you MUST set POSTGRESDB_PASSWORD in your .env. + POSTGRES_PASSWORD: ${POSTGRESDB_PASSWORD:?POSTGRESDB_PASSWORD is required — set it in your .env (copy .env.example to .env)} + # Dev-only admin bootstrap (consumed by db/zz-dev-seed.sh). Off by default. + DEV_SEED_ADMIN: ${DEV_SEED_ADMIN:-false} + BOOTSTRAP_ADMIN_EMAIL: ${BOOTSTRAP_ADMIN_EMAIL:-} + BOOTSTRAP_ADMIN_PASSWORD_HASH: ${BOOTSTRAP_ADMIN_PASSWORD_HASH:-} ports: - "5432:5432" volumes: diff --git a/docs/README_eng.md b/docs/README_eng.md index 3c30dd7..07c8feb 100644 --- a/docs/README_eng.md +++ b/docs/README_eng.md @@ -117,10 +117,24 @@ npm run dev ### Default Credentials +The schema **seeds no users**. For security there are no default credentials. +To create a local admin in development, generate a hash and enable the +(dev-only) seed: + +```bash +# 1. Generate the bcrypt hash of your password +bash scripts/gen-admin-hash.sh 'your-strong-password' + +# 2. In your .env +DEV_SEED_ADMIN=true +BOOTSTRAP_ADMIN_EMAIL=admin@example.test +BOOTSTRAP_ADMIN_PASSWORD_HASH= + +# 3. Recreate the database so the seed runs +npm run dev:db:clean && npm run dev:db ``` -Email: test@local.com -Password: 123456 -``` + +> Never enable `DEV_SEED_ADMIN` in production. ## 🛠️ Development Commands diff --git a/scripts/gen-admin-hash.sh b/scripts/gen-admin-hash.sh new file mode 100755 index 0000000..877b7ce --- /dev/null +++ b/scripts/gen-admin-hash.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# Generates a bcrypt hash for the dev admin seed +# (BOOTSTRAP_ADMIN_PASSWORD_HASH, consumed by db/zz-dev-seed.sh). +# +# Usage: +# bash scripts/gen-admin-hash.sh 'your-strong-password' +# +# Cost defaults to HASH_SALT_ROUNDS (or 12), matching the API hashing config. +# --------------------------------------------------------------------------- +set -euo pipefail + +PASSWORD="${1:?Usage: gen-admin-hash.sh }" +ROUNDS="${HASH_SALT_ROUNDS:-12}" + +node -e "const b=require('bcrypt');console.log(b.hashSync(process.argv[1], parseInt(process.argv[2],10)))" "$PASSWORD" "$ROUNDS" From e0765c421d9921d39860f316e045c8892f3cd66d Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:05:32 +0200 Subject: [PATCH 02/24] fix(security): reject soft-deleted users at authentication [ALTO] T-2: validateCredentials/getUser queried User without deleted:false, so a soft-deleted account (offboarded admin, compromised user) could still log in and receive its original permissions. - validateCredentials: where { email, deleted: false }. - getUser: switch findByPk -> findOne with { id, deleted: false } so the filter is actually enforced (findByPk ignores extra where on the PK). - Update/extend auth.service.spec.ts. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/src/services/auth.service.spec.ts | 15 ++++++++++++++- apps/api/src/services/auth.service.ts | 9 +++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/api/src/services/auth.service.spec.ts b/apps/api/src/services/auth.service.spec.ts index 6595763..e09b560 100644 --- a/apps/api/src/services/auth.service.spec.ts +++ b/apps/api/src/services/auth.service.spec.ts @@ -35,7 +35,7 @@ describe('AuthService (internal)', () => { const result = await authService.validateCredentials('a@b.com', 'plain'); expect(User.findOne).toHaveBeenCalledWith({ - where: { email: 'a@b.com' }, + where: { email: 'a@b.com', deleted: false }, }); expect(bcrypt.compare).toHaveBeenCalledWith('plain', 'hashed'); expect(result).toEqual(mockUser); @@ -60,6 +60,19 @@ describe('AuthService (internal)', () => { }); }); + describe('getUser', () => { + it('only resolves non-deleted users (soft-delete filter)', async () => { + vi.mocked(User.findOne).mockResolvedValue(null); + + const result = await authService.getUser(1); + + expect(User.findOne).toHaveBeenCalledWith({ + where: { id: 1, deleted: false }, + }); + expect(result).toBeNull(); + }); + }); + describe('hashPassword', () => { it('delegates to bcrypt.hash with HASH_SALT_ROUNDS env', async () => { vi.mocked(bcrypt.hash).mockResolvedValue('hashed' as never); diff --git a/apps/api/src/services/auth.service.ts b/apps/api/src/services/auth.service.ts index 89f5435..930339b 100644 --- a/apps/api/src/services/auth.service.ts +++ b/apps/api/src/services/auth.service.ts @@ -10,14 +10,19 @@ class AuthService { email: string, password: string, ): Promise => { - const user: UserModel = await User.findOne({ where: { email } }); + const user: UserModel = await User.findOne({ + where: { email, deleted: false }, + }); if (!user) throw new Error('Email or password incorrect.'); const validPassword = await this.#comparePassword(password, user.password); if (!validPassword) throw new Error('Email or password incorrect.'); return user; }; - getUser = async (id: number): Promise => await User.findByPk(id); + // findOne (not findByPk) so the soft-delete filter is actually enforced: + // a soft-deleted account must never be resolvable for auth purposes. + getUser = async (id: number): Promise => + await User.findOne({ where: { id, deleted: false } }); hashPassword = async (password: string) => { return await hash(password, process.env.HASH_SALT_ROUNDS ?? 10); From a788a0e0cf66bbb0d48d858f5e3d2c0cb8795274 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:07:39 +0200 Subject: [PATCH 03/24] fix(security): allow-list writable fields to stop mass-assignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ALTO] T-3: AbstractCrudService passed req.body verbatim to model.create/update, letting a caller over-post permissions, deleted, deletedAt or id (privilege escalation / tampering). This is the base CRUD for every future entity, so the pattern would propagate. - AbstractCrudService: optional writableFields allow-list, applied as Sequelize's { fields } option on post/put so unlisted keys are dropped. - UserCrudService: declare USER_WRITABLE_FIELDS (email,name,lastName,permissions, password) — excludes id/deleted/deletedAt/audit columns even for admins. - Update specs to assert the fields allow-list is enforced. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../api/src/services/abstract-crud.service.ts | 25 ++++++++++++++++--- .../src/services/user-crud.service.spec.ts | 21 ++++++++++------ apps/api/src/services/user-crud.service.ts | 20 ++++++++++++--- 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/apps/api/src/services/abstract-crud.service.ts b/apps/api/src/services/abstract-crud.service.ts index 13da63c..871d918 100644 --- a/apps/api/src/services/abstract-crud.service.ts +++ b/apps/api/src/services/abstract-crud.service.ts @@ -8,9 +8,18 @@ import sequelize, { ModelStatic } from 'sequelize'; export abstract class AbstractCrudService { protected model: ModelStatic; + /** + * Allow-list of attributes a client may write through post/put. When set, it + * is passed as Sequelize's `fields` option so any extra key in the payload + * (e.g. `permissions`, `deleted`, `deletedAt`, `id`) is silently ignored, + * preventing mass-assignment / over-posting privilege escalation. Leave + * undefined only for internal entities with no client-facing write surface. + */ + protected readonly writableFields?: string[]; - constructor(model: ModelStatic) { + constructor(model: ModelStatic, writableFields?: string[]) { this.model = model; + this.writableFields = writableFields; } getAllPaged = async ( @@ -51,10 +60,20 @@ export abstract class AbstractCrudService { where, }); - post = async (model) => await this.model.create(model); + post = async (model) => + await this.model.create( + model, + this.writableFields ? { fields: this.writableFields } : undefined, + ); put = async (id: number, data) => - await this.model.update({ ...data }, { where: { id } }); + await this.model.update( + { ...data }, + { + where: { id }, + ...(this.writableFields ? { fields: this.writableFields } : {}), + }, + ); delete = async (id: number) => await this.model.update( diff --git a/apps/api/src/services/user-crud.service.spec.ts b/apps/api/src/services/user-crud.service.spec.ts index d0a98f1..77e534f 100644 --- a/apps/api/src/services/user-crud.service.spec.ts +++ b/apps/api/src/services/user-crud.service.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import userCrudService from './user-crud.service'; +import userCrudService, { USER_WRITABLE_FIELDS } from './user-crud.service'; import { User } from '@api/models'; import { Permission } from '@dto'; import authService from './auth.service'; @@ -53,10 +53,13 @@ describe('UserCrudService', () => { // Assert expect(authService.hashPassword).toHaveBeenCalledWith('plainPassword123'); - expect(User.create).toHaveBeenCalledWith({ - ...userData, - password: hashedPassword, - }); + expect(User.create).toHaveBeenCalledWith( + { + ...userData, + password: hashedPassword, + }, + { fields: USER_WRITABLE_FIELDS }, + ); expect(result).toEqual(createdUser); }); @@ -76,7 +79,9 @@ describe('UserCrudService', () => { // Assert expect(authService.hashPassword).not.toHaveBeenCalled(); - expect(User.create).toHaveBeenCalledWith(userData); + expect(User.create).toHaveBeenCalledWith(userData, { + fields: USER_WRITABLE_FIELDS, + }); expect(result).toEqual(createdUser); }); }); @@ -111,7 +116,7 @@ describe('UserCrudService', () => { expect(authService.hashPassword).toHaveBeenCalledWith('newPassword123'); expect(User.update).toHaveBeenCalledWith( { ...updateData, password: hashedPassword }, - { where: { id: userId } }, + { where: { id: userId }, fields: USER_WRITABLE_FIELDS }, ); expect(result).toEqual([1]); }); @@ -156,7 +161,7 @@ describe('UserCrudService', () => { email: 'updated@example.com', permissions: [Permission.READ_SOME_ENTITY], }, - { where: { id: userId } }, + { where: { id: userId }, fields: USER_WRITABLE_FIELDS }, ); }); diff --git a/apps/api/src/services/user-crud.service.ts b/apps/api/src/services/user-crud.service.ts index bd7a231..f0c0f3d 100644 --- a/apps/api/src/services/user-crud.service.ts +++ b/apps/api/src/services/user-crud.service.ts @@ -4,16 +4,27 @@ import { Op } from 'sequelize'; import { AbstractCrudService } from './abstract-crud.service'; import authService from './auth.service'; +// Fields an admin may legitimately set on a user. Notably excludes id, deleted, +// deletedAt, createdAt and updatedAt — those are never client-writable, even by +// an admin, so they cannot be tampered with via over-posting. +export const USER_WRITABLE_FIELDS = [ + 'email', + 'name', + 'lastName', + 'permissions', + 'password', +]; + class UserCrudService extends AbstractCrudService { constructor() { - super(User); + super(User, USER_WRITABLE_FIELDS); } post = async (userData) => { if (userData.password) { userData.password = await authService.hashPassword(userData.password); } - return await this.model.create(userData); + return await this.model.create(userData, { fields: this.writableFields }); }; put = async (id: number, userData) => { @@ -52,7 +63,10 @@ class UserCrudService extends AbstractCrudService { delete userData.password; } - return await this.model.update({ ...userData }, { where: { id } }); + return await this.model.update( + { ...userData }, + { where: { id }, fields: this.writableFields }, + ); }; delete = async (id: number) => { From efaafbb0eeaf2107c79dda814de6c9ee97938b92 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:10:34 +0200 Subject: [PATCH 04/24] feat(security): add Zod input-validation layer at the API edge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ALTO] T-4: the API had no input validation — req.body/query/params flowed untyped into Sequelize, enabling mass-assignment, NaN pagination and inconsistent errors. This establishes the validation pattern for every entity. - libs/rest-dto: add Zod schemas (userCreateSchema, userUpdateSchema, paginationQuerySchema, idParamSchema) as the single source of truth; input schemas are .strict() so unknown keys are rejected. - apps/api: add validate(schema, source) middleware (Express 5-safe: body/params replaced in place, query exposed on res.locals.query); wire body validation onto user POST/PUT. - Tests for the middleware; document the layer in api & rest-dto AGENTS.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/AGENTS.md | 9 + apps/api/src/middleware/index.ts | 1 + .../middleware/validate.middleware.spec.ts | 77 ++++ .../api/src/middleware/validate.middleware.ts | 41 +++ apps/api/src/routes/user-crud.routes.ts | 17 +- libs/rest-dto/AGENTS.md | 6 + libs/rest-dto/src/index.ts | 1 + libs/rest-dto/src/lib/validation.ts | 53 +++ package-lock.json | 347 ++++-------------- package.json | 1 + 10 files changed, 274 insertions(+), 279 deletions(-) create mode 100644 apps/api/src/middleware/validate.middleware.spec.ts create mode 100644 apps/api/src/middleware/validate.middleware.ts create mode 100644 libs/rest-dto/src/lib/validation.ts diff --git a/apps/api/AGENTS.md b/apps/api/AGENTS.md index b29e0f9..c1952a0 100644 --- a/apps/api/AGENTS.md +++ b/apps/api/AGENTS.md @@ -46,6 +46,15 @@ routes → controllers → services → models → (db via Sequelize) - **Never expose `password`.** `AbstractCrudService` excludes `password` from every read, and excludes `deleted`/`deletedAt` unless `where.deleted` is set. - Models import their shape from `@dto`; do not duplicate field types locally. +- **Validate every client input.** Never pass `req.body` / `req.query` / `req.params` + to a service unvalidated. Put a `validate(schema, source)` middleware + (`src/middleware/validate.middleware.ts`) in front of the controller, using a + Zod schema from `@dto` (`userCreateSchema`, `userUpdateSchema`, + `paginationQuerySchema`, `idParamSchema`, …). Input schemas are `.strict()`, so + unknown keys are rejected at the edge — this, plus the service-layer + `writableFields` allow-list, is the mass-assignment defense. Because `req.query` + is a read-only getter in Express 5, the parsed query is exposed on + `res.locals.query` (read it from there), while body/params are replaced in place. ## Internal auth diff --git a/apps/api/src/middleware/index.ts b/apps/api/src/middleware/index.ts index fb4fd44..e93a798 100644 --- a/apps/api/src/middleware/index.ts +++ b/apps/api/src/middleware/index.ts @@ -3,3 +3,4 @@ export { dbLoggingMiddleware, sequelizeErrorMiddleware, } from './db-error.middleware'; +export { validate } from './validate.middleware'; diff --git a/apps/api/src/middleware/validate.middleware.spec.ts b/apps/api/src/middleware/validate.middleware.spec.ts new file mode 100644 index 0000000..0ffac72 --- /dev/null +++ b/apps/api/src/middleware/validate.middleware.spec.ts @@ -0,0 +1,77 @@ +import { describe, expect, it, vi } from 'vitest'; +import { z } from 'zod'; +import { validate } from './validate.middleware'; + +const schema = z + .object({ email: z.string().email(), name: z.string().min(1) }) + .strict(); + +const makeRes = () => { + const res: Record = { locals: {} }; + res.status = vi.fn().mockReturnValue(res); + res.json = vi.fn().mockReturnValue(res); + res.send = vi.fn().mockReturnValue(res); + return res as never; +}; + +describe('validate middleware', () => { + it('calls next and exposes parsed body on success', () => { + const req = { body: { email: 'a@b.com', name: 'Ann' } } as never; + const res = makeRes(); + const next = vi.fn(); + + validate(schema)(req, res, next); + + expect(next).toHaveBeenCalledOnce(); + expect((req as { body: unknown }).body).toEqual({ + email: 'a@b.com', + name: 'Ann', + }); + }); + + it('rejects unknown keys (mass-assignment) with 400', () => { + const req = { + body: { email: 'a@b.com', name: 'Ann', permissions: ['ADMIN'] }, + } as never; + const res = makeRes(); + const next = vi.fn(); + + validate(schema)(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect( + (res as { status: ReturnType }).status, + ).toHaveBeenCalledWith(400); + }); + + it('rejects invalid payloads with 400 and does not call next', () => { + const req = { body: { email: 'not-an-email', name: '' } } as never; + const res = makeRes(); + const next = vi.fn(); + + validate(schema)(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect( + (res as { status: ReturnType }).status, + ).toHaveBeenCalledWith(400); + }); + + it('stores parsed query on res.locals when req.query is a getter', () => { + const querySchema = z.object({ page: z.coerce.number().int().min(1) }); + const req = {} as Record; + Object.defineProperty(req, 'query', { + get: () => ({ page: '3' }), + configurable: true, + }); + const res = makeRes(); + const next = vi.fn(); + + validate(querySchema, 'query')(req as never, res, next); + + expect(next).toHaveBeenCalledOnce(); + expect((res as { locals: { query: unknown } }).locals.query).toEqual({ + page: 3, + }); + }); +}); diff --git a/apps/api/src/middleware/validate.middleware.ts b/apps/api/src/middleware/validate.middleware.ts new file mode 100644 index 0000000..8c6192b --- /dev/null +++ b/apps/api/src/middleware/validate.middleware.ts @@ -0,0 +1,41 @@ +import HttpResponser from '@api/adapters/http/http.responser'; +import { NextFunction, Request, Response } from 'express'; +import { ZodError, ZodType } from 'zod'; + +type Source = 'body' | 'query' | 'params'; + +const formatIssues = (error: ZodError) => + error.issues + .map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`) + .join('; '); + +/** + * Validates `req[source]` against a Zod schema before the controller runs. + * On failure it short-circuits with a 400 (no stack/SQL leaked). On success it + * exposes the parsed, type-coerced, stripped value: + * - body/params are writable, so they are replaced in place. + * - query is a getter in Express 5, so the parsed value is stored on + * `res.locals.` (read it from there in the controller). + * + * Schemas are the single source of truth in `@dto`. + */ +export const validate = + (schema: ZodType, source: Source = 'body') => + (req: Request, res: Response, next: NextFunction) => { + const result = schema.safeParse(req[source]); + if (!result.success) { + return HttpResponser.errorJson( + res, + { message: `Validation failed — ${formatIssues(result.error)}` }, + 400, + ); + } + res.locals[source] = result.data; + try { + // body and params are writable; query is a read-only getter in Express 5. + (req as Record)[source] = result.data; + } catch { + /* query getter — consumers read res.locals.query instead */ + } + next(); + }; diff --git a/apps/api/src/routes/user-crud.routes.ts b/apps/api/src/routes/user-crud.routes.ts index 6c723d7..5ea26fb 100644 --- a/apps/api/src/routes/user-crud.routes.ts +++ b/apps/api/src/routes/user-crud.routes.ts @@ -1,5 +1,6 @@ import { userCrudController } from '@api/controllers'; -import { Permission } from '@dto'; +import { validate } from '@api/middleware'; +import { Permission, userCreateSchema, userUpdateSchema } from '@dto'; import { requireInternalAuth, InternalScope } from '@internal-auth'; import { Router } from 'express'; @@ -13,8 +14,18 @@ const requireAdmin = requireInternalAuth({ userCrudRouter.get('/', requireAdmin, userCrudController.getAll); userCrudRouter.get('/paged', requireAdmin, userCrudController.getAllPaged); userCrudRouter.get('/:id', requireAdmin, userCrudController.getById); -userCrudRouter.post('/', requireAdmin, userCrudController.post); -userCrudRouter.put('/:id', requireAdmin, userCrudController.put); +userCrudRouter.post( + '/', + requireAdmin, + validate(userCreateSchema), + userCrudController.post, +); +userCrudRouter.put( + '/:id', + requireAdmin, + validate(userUpdateSchema), + userCrudController.put, +); userCrudRouter.delete('/:id', requireAdmin, userCrudController.delete); export default userCrudRouter; diff --git a/libs/rest-dto/AGENTS.md b/libs/rest-dto/AGENTS.md index 3b27f29..a35541c 100644 --- a/libs/rest-dto/AGENTS.md +++ b/libs/rest-dto/AGENTS.md @@ -17,6 +17,12 @@ file wins and the duplicate is a bug. - `src/index.ts` — public barrel; everything consumers use is re-exported here. - `src/lib/rest-dto.ts` — entity DTOs (e.g. `UserDTO`), the `Permission` enum, and permission option metadata (`PERMISSION_OPTIONS`). +- `src/lib/validation.ts` — **runtime validation contracts** (Zod schemas: + `userCreateSchema`, `userUpdateSchema`, `paginationQuerySchema`, + `idParamSchema`). These are the single source of truth for request validation; + the API's `validate()` middleware parses every request through them. Input + schemas are `.strict()` (reject unknown keys). Add a schema here when you add an + entity — never inline validation in the apps. - `src/lib/common-types.ts` — shared primitives/helpers. ## Conventions diff --git a/libs/rest-dto/src/index.ts b/libs/rest-dto/src/index.ts index 6847190..624052d 100644 --- a/libs/rest-dto/src/index.ts +++ b/libs/rest-dto/src/index.ts @@ -1 +1,2 @@ export * from './lib/rest-dto'; +export * from './lib/validation'; diff --git a/libs/rest-dto/src/lib/validation.ts b/libs/rest-dto/src/lib/validation.ts new file mode 100644 index 0000000..7f58fcd --- /dev/null +++ b/libs/rest-dto/src/lib/validation.ts @@ -0,0 +1,53 @@ +import { z } from 'zod'; +import { Permission } from './rest-dto'; + +/** + * Runtime validation contracts for the API, kept here in `@dto` so the schema + * is a single source of truth shared by the backend (request validation) and, + * if needed, the frontend (form validation). The API never trusts req.body / + * query / params directly — it parses them through these schemas first. + * + * Input schemas are `.strict()`: unknown keys are rejected, which closes + * mass-assignment / over-posting at the edge (defense-in-depth on top of the + * service-layer field allow-list). + */ + +const passwordSchema = z + .string() + .min(8, 'Password must be at least 8 characters') + .max(128); + +export const userCreateSchema = z + .object({ + email: z.string().trim().toLowerCase().email().max(150), + name: z.string().trim().min(1).max(150), + lastName: z.string().trim().max(150).optional(), + permissions: z.array(z.nativeEnum(Permission)).nonempty().optional(), + password: passwordSchema, + }) + .strict(); + +export const userUpdateSchema = z + .object({ + email: z.string().trim().toLowerCase().email().max(150).optional(), + name: z.string().trim().min(1).max(150).optional(), + lastName: z.string().trim().max(150).optional(), + permissions: z.array(z.nativeEnum(Permission)).nonempty().optional(), + // Empty string => "leave password unchanged" (handled by the service). + password: z.union([passwordSchema, z.literal('')]).optional(), + }) + .strict(); + +/** Reusable query/param schemas for paged CRUD endpoints. */ +export const paginationQuerySchema = z.object({ + page: z.coerce.number().int().min(1).default(1), + limit: z.coerce.number().int().min(1).max(100).default(10), +}); + +export const idParamSchema = z.object({ + id: z.coerce.number().int().positive(), +}); + +export type UserCreateInput = z.infer; +export type UserUpdateInput = z.infer; +export type PaginationQuery = z.infer; diff --git a/package-lock.json b/package-lock.json index 9bdcb29..7f9b66e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "sequelize": "^6.37.8", "tslib": "^2.8.1", "uuid": "^14.0.0", + "zod": "^3.25.76", "zone.js": "^0.16.1" }, "devDependencies": { @@ -1968,6 +1969,16 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular/cli/node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@angular/common": { "version": "21.2.10", "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.10.tgz", @@ -2000,6 +2011,7 @@ "version": "21.2.10", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.10.tgz", "integrity": "sha512-FDcnj3ogRmnTca4m2GbKP2khFOCtoVvWDZyfw2ZCPAf+zsQlKTyscKvx4GpTFo+KHrYXpawUpDIWHORFpuqFEA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "7.29.0", @@ -2086,6 +2098,7 @@ "version": "21.2.10", "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-21.2.10.tgz", "integrity": "sha512-pQNL1LwI/6e8SbwiPxGJkXhV5AZxODhS/UJMEeYTx2fRvm4Ob3aMxWTl3ixhFZeYAx9bcU3QpfEJyGr7YDO24w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "7.29.0", @@ -2237,6 +2250,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2246,6 +2260,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -2276,12 +2291,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, "license": "MIT" }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2291,6 +2308,7 @@ "version": "7.29.1", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", @@ -2320,6 +2338,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", @@ -2336,6 +2355,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2444,6 +2464,7 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2467,6 +2488,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.6", @@ -2480,6 +2502,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.28.6", @@ -2583,6 +2606,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2601,6 +2625,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2625,6 +2650,7 @@ "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", @@ -2638,6 +2664,7 @@ "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -4223,6 +4250,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.28.6", @@ -4237,6 +4265,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -4255,6 +4284,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -6100,6 +6130,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -6110,6 +6141,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -6120,6 +6152,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -6140,12 +6173,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -11207,277 +11242,6 @@ "win32" ] }, - "node_modules/@rspack/binding": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.11.tgz", - "integrity": "sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q==", - "dev": true, - "license": "MIT", - "peer": true, - "optionalDependencies": { - "@rspack/binding-darwin-arm64": "1.7.11", - "@rspack/binding-darwin-x64": "1.7.11", - "@rspack/binding-linux-arm64-gnu": "1.7.11", - "@rspack/binding-linux-arm64-musl": "1.7.11", - "@rspack/binding-linux-x64-gnu": "1.7.11", - "@rspack/binding-linux-x64-musl": "1.7.11", - "@rspack/binding-wasm32-wasi": "1.7.11", - "@rspack/binding-win32-arm64-msvc": "1.7.11", - "@rspack/binding-win32-ia32-msvc": "1.7.11", - "@rspack/binding-win32-x64-msvc": "1.7.11" - } - }, - "node_modules/@rspack/binding-darwin-arm64": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.11.tgz", - "integrity": "sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@rspack/binding-darwin-x64": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.11.tgz", - "integrity": "sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-arm64-gnu": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.11.tgz", - "integrity": "sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-arm64-musl": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.11.tgz", - "integrity": "sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-x64-gnu": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.11.tgz", - "integrity": "sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-x64-musl": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.11.tgz", - "integrity": "sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-wasm32-wasi": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.11.tgz", - "integrity": "sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@napi-rs/wasm-runtime": "1.0.7" - } - }, - "node_modules/@rspack/binding-win32-arm64-msvc": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.11.tgz", - "integrity": "sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@rspack/binding-win32-ia32-msvc": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.11.tgz", - "integrity": "sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@rspack/binding-win32-x64-msvc": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.11.tgz", - "integrity": "sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@rspack/core": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.11.tgz", - "integrity": "sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@module-federation/runtime-tools": "0.22.0", - "@rspack/binding": "1.7.11", - "@rspack/lite-tapable": "1.1.0" - }, - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.1" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@rspack/core/node_modules/@module-federation/error-codes": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", - "integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@rspack/core/node_modules/@module-federation/runtime": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.22.0.tgz", - "integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@module-federation/error-codes": "0.22.0", - "@module-federation/runtime-core": "0.22.0", - "@module-federation/sdk": "0.22.0" - } - }, - "node_modules/@rspack/core/node_modules/@module-federation/runtime-core": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.22.0.tgz", - "integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@module-federation/error-codes": "0.22.0", - "@module-federation/sdk": "0.22.0" - } - }, - "node_modules/@rspack/core/node_modules/@module-federation/runtime-tools": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.22.0.tgz", - "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@module-federation/runtime": "0.22.0", - "@module-federation/webpack-bundler-runtime": "0.22.0" - } - }, - "node_modules/@rspack/core/node_modules/@module-federation/sdk": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz", - "integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@rspack/core/node_modules/@module-federation/webpack-bundler-runtime": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.22.0.tgz", - "integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@module-federation/runtime": "0.22.0", - "@module-federation/sdk": "0.22.0" - } - }, "node_modules/@rspack/dev-server": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@rspack/dev-server/-/dev-server-1.2.1.tgz", @@ -12570,6 +12334,7 @@ "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", @@ -12583,6 +12348,7 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -12592,6 +12358,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -12602,6 +12369,7 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.2" @@ -14713,6 +14481,7 @@ "version": "2.10.23", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz", "integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==", + "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -14917,6 +14686,7 @@ "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -15173,6 +14943,7 @@ "version": "1.0.30001791", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -15236,6 +15007,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, "license": "MIT", "dependencies": { "readdirp": "^5.0.0" @@ -15762,6 +15534,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -17180,6 +16953,7 @@ "version": "1.5.344", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "dev": true, "license": "ISC" }, "node_modules/emittery": { @@ -18117,6 +17891,7 @@ "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" @@ -18735,6 +18510,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -18753,6 +18529,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -20775,6 +20552,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -20825,6 +20603,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -21640,6 +21419,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -22482,6 +22262,7 @@ "version": "2.0.38", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, "license": "MIT" }, "node_modules/node-schedule": { @@ -24027,6 +23808,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -24794,6 +24576,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 20.19.0" @@ -24820,6 +24603,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, "license": "Apache-2.0" }, "node_modules/regenerate": { @@ -27536,6 +27320,7 @@ "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -28029,7 +27814,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -28185,6 +27970,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, "funding": [ { "type": "opencollective", @@ -29910,6 +29696,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, "license": "ISC" }, "node_modules/yaml": { @@ -29932,6 +29719,7 @@ "version": "18.0.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, "license": "MIT", "dependencies": { "cliui": "^9.0.1", @@ -29958,6 +29746,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -29970,6 +29759,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -29982,6 +29772,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^7.2.0", @@ -29996,12 +29787,14 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, "license": "MIT" }, "node_modules/yargs/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==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -30019,6 +29812,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" @@ -30034,6 +29828,7 @@ "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==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", @@ -30051,6 +29846,7 @@ "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==", + "dev": true, "license": "ISC", "engines": { "node": "^20.19.0 || ^22.12.0 || >=23" @@ -30106,10 +29902,9 @@ } }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 3643bb2..b25ad8c 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,7 @@ "sequelize": "^6.37.8", "tslib": "^2.8.1", "uuid": "^14.0.0", + "zod": "^3.25.76", "zone.js": "^0.16.1" }, "overrides": { From b8e54883e9a8cbc151cc32b16c69083c68455ec8 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:13:59 +0200 Subject: [PATCH 05/24] fix(security): re-read permissions on refresh rotation; bound remember [ALTO] T-5: refresh rotation copied permissions from the OLD refresh token, and remember=true minted a 365-day token, so a downgraded/revoked user kept elevated access for up to a year. remember was also read truthy from req.body. - API /internal/refresh/rotate re-reads the user (authService.getUser, which filters deleted:false) and returns current email+permissions; revokes the family and 401s if the user is gone/deactivated. - Gateway uses the API-returned claims on rotation, not the stale token claims. - Bound remember lifetime via JWT_REFRESH_REMEMBER_DAYS (default 30); JWT expiry and cookie maxAge share one source (token.service.rememberRefresh*). - Coerce remember to a strict boolean (=== true) in the login controller. - Update gateway specs + AGENTS.md; add env to .env.example/compose.yaml. Co-Authored-By: Claude Opus 4.8 (1M context) --- .env.example | 3 +++ .../refresh-lifecycle.controller.ts | 20 ++++++++++++++++-- apps/gateway/AGENTS.md | 21 +++++++++++++------ apps/gateway/src/clients/api.client.ts | 4 ++++ .../src/controllers/auth.controller.ts | 5 ++++- .../src/middleware/auth.middleware.spec.ts | 9 ++++++++ .../gateway/src/middleware/auth.middleware.ts | 11 ++++++---- .../src/services/token.service.spec.ts | 11 ++++++---- apps/gateway/src/services/token.service.ts | 21 ++++++++++++++++++- compose.yaml | 1 + 10 files changed, 88 insertions(+), 18 deletions(-) diff --git a/.env.example b/.env.example index 95bf97c..93988ae 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,9 @@ JWT_ACCESS_SECRET=dev-access-secret-replace-me JWT_REFRESH_SECRET=dev-refresh-secret-replace-me JWT_EXPIRES_IN=4h JWT_REFRESH_EXPIRES_IN=8h +# "remember me" refresh lifetime in days. Kept bounded (default 30) and backed +# by refresh-token rotation so revoked sessions die quickly. Avoid huge values. +JWT_REFRESH_REMEMBER_DAYS=30 # Internal JWT — Ed25519 asymmetric. The gateway holds the private key # and signs gateway → api tokens. The api only holds the public key, so diff --git a/apps/api/src/controllers/refresh-lifecycle.controller.ts b/apps/api/src/controllers/refresh-lifecycle.controller.ts index 138d209..9af1339 100644 --- a/apps/api/src/controllers/refresh-lifecycle.controller.ts +++ b/apps/api/src/controllers/refresh-lifecycle.controller.ts @@ -1,5 +1,5 @@ import HttpResponser from '@api/adapters/http/http.responser'; -import { refreshTokenFamilyService } from '@api/services'; +import { authService, refreshTokenFamilyService } from '@api/services'; import type { Request, Response } from 'express'; /** @@ -38,13 +38,29 @@ class RefreshLifecycleController { } const outcome = await refreshTokenFamilyService.rotate({ jti }); switch (outcome.status) { - case 'rotated': + case 'rotated': { + // Re-read the user from the authoritative source on every rotation so + // revoked/downgraded permissions (and soft-deleted accounts) take + // effect immediately, instead of carrying stale claims from the old + // refresh token forward (T-5). getUser already filters deleted:false. + const user = await authService.getUser(outcome.userId); + if (!user) { + await refreshTokenFamilyService.revokeFamily(outcome.familyId); + return HttpResponser.errorJson( + res, + { message: 'User no longer active' }, + 401, + ); + } return HttpResponser.successJson(res, { status: outcome.status, userId: outcome.userId, familyId: outcome.familyId, parentJti: outcome.parentJti, + email: user.email, + permissions: user.permissions, }); + } case 'reused-revoked': case 'family-revoked': return HttpResponser.errorJson( diff --git a/apps/gateway/AGENTS.md b/apps/gateway/AGENTS.md index bf89628..db78355 100644 --- a/apps/gateway/AGENTS.md +++ b/apps/gateway/AGENTS.md @@ -43,15 +43,24 @@ browser ──/api/*──▶ nginx ──proxy_pass──▶ gateway - CORS, `cookie-parser`, and `trust proxy` are configured in `src/main.ts`; allowed origins come from `CORS_ORIGIN` (comma-separated). - Responses use the gateway's own `HttpResponser` (`src/adapters/http/`). +- **On refresh rotation, trust the claims the API returns, never the old token.** + `/internal/refresh/rotate` re-reads the user and returns its current + `email`/`permissions`; the gateway must put those on `res.locals.user` so a + revoked/downgraded (or soft-deleted) account loses access on the next rotation + rather than carrying stale claims for the whole refresh lifetime. +- **`remember` is a strict boolean** (`=== true`) and the "remember me" refresh + lifetime is bounded by `JWT_REFRESH_REMEMBER_DAYS` (default 30) — never a + year-long token. The JWT expiry and cookie `maxAge` share that single source. ## Env vars -| Var | Purpose | -| ------------------------- | -------------------------------------------------- | -| `GATEWAY_PORT` | Listen port (default 3100) | -| `API_BASE_URL` | Upstream API base (default `http://api:3200`) | -| `INTERNAL_JWT_PRIVATE_KEY`| EdDSA private key for signing internal tokens (PEM)| -| `CORS_ORIGIN` | Comma-separated allowed origins | +| Var | Purpose | +| --------------------------- | --------------------------------------------------- | +| `GATEWAY_PORT` | Listen port (default 3100) | +| `API_BASE_URL` | Upstream API base (default `http://api:3200`) | +| `INTERNAL_JWT_PRIVATE_KEY` | EdDSA private key for signing internal tokens (PEM) | +| `CORS_ORIGIN` | Comma-separated allowed origins | +| `JWT_REFRESH_REMEMBER_DAYS` | "remember me" refresh lifetime in days (default 30) | ## Testing diff --git a/apps/gateway/src/clients/api.client.ts b/apps/gateway/src/clients/api.client.ts index c4feb54..97d98bf 100644 --- a/apps/gateway/src/clients/api.client.ts +++ b/apps/gateway/src/clients/api.client.ts @@ -17,6 +17,10 @@ interface RotateRefreshResponse { userId: number; familyId: string; parentJti: string; + // Authoritative, freshly-read claims so rotation cannot carry stale + // permissions forward (T-5). + email: string; + permissions: Permission[]; } const baseUrl = () => diff --git a/apps/gateway/src/controllers/auth.controller.ts b/apps/gateway/src/controllers/auth.controller.ts index 08f87dd..17fe150 100644 --- a/apps/gateway/src/controllers/auth.controller.ts +++ b/apps/gateway/src/controllers/auth.controller.ts @@ -12,7 +12,10 @@ class AuthController { login = async (req: Request, res: Response) => { const requestId = randomUUID(); try { - const { email, password, remember } = req.body ?? {}; + const { email, password } = req.body ?? {}; + // Strict boolean: any truthy non-true value (e.g. {} or "yes") must not + // silently opt the client into the long-lived "remember me" token (T-5). + const remember = req.body?.remember === true; if (!email || !password) { return HttpResponser.errorJson( res, diff --git a/apps/gateway/src/middleware/auth.middleware.spec.ts b/apps/gateway/src/middleware/auth.middleware.spec.ts index 5414565..a737097 100644 --- a/apps/gateway/src/middleware/auth.middleware.spec.ts +++ b/apps/gateway/src/middleware/auth.middleware.spec.ts @@ -142,6 +142,10 @@ describe('hasPermission', () => { userId: 7, familyId: FAMILY, parentJti: 'old-jti', + // api re-reads the user: the gateway must trust THESE claims, not the + // ones embedded in the old refresh token (T-5). + email: 'fresh@b.com', + permissions: [Permission.READ_SOME_ENTITY], }, }, { status: 201, body: { recorded: true } }, @@ -158,6 +162,11 @@ describe('hasPermission', () => { expect(next).toHaveBeenCalled(); expect(fetchMock).toHaveBeenCalledTimes(2); + expect(res.locals.user).toEqual({ + id: 7, + email: 'fresh@b.com', + permissions: [Permission.READ_SOME_ENTITY], + }); expect(setHeader).toHaveBeenCalledWith('Authorization', expect.any(String)); expect(cookie).toHaveBeenCalledWith( 'refreshToken', diff --git a/apps/gateway/src/middleware/auth.middleware.ts b/apps/gateway/src/middleware/auth.middleware.ts index 45f05c1..6a67440 100644 --- a/apps/gateway/src/middleware/auth.middleware.ts +++ b/apps/gateway/src/middleware/auth.middleware.ts @@ -1,6 +1,7 @@ import HttpResponser from '@gateway/adapters/http/http.responser'; import { ApiClient } from '@gateway/clients/api.client'; import { tokenService } from '@gateway/services'; +import { rememberRefreshMaxAgeMs } from '@gateway/services/token.service'; import { Permission } from '@dto'; import { randomUUID } from 'crypto'; import type { CookieOptions, NextFunction, Request, Response } from 'express'; @@ -71,7 +72,7 @@ const issueRefreshAndRecord = async ( options.requestId, ); const maxAge = options.user.remember - ? 365 * 24 * 60 * 60 * 1000 + ? rememberRefreshMaxAgeMs() : 8 * 60 * 60 * 1000; res.cookie(REFRESH_COOKIE, refreshToken, buildCookieOptions(maxAge)); }; @@ -205,10 +206,12 @@ const refreshAndContinue = async ( const requestId = randomUUID(); try { const rotation = await ApiClient.rotateRefresh(decoded.jti, requestId); + // Use the permissions/email the api just re-read from the user record, NOT + // the (possibly stale) claims embedded in the old refresh token (T-5). const user: UserContext = { - id: decoded.id, - email: decoded.email, - permissions: decoded.permissions ?? [], + id: rotation.userId, + email: rotation.email ?? decoded.email, + permissions: rotation.permissions ?? [], }; if ( diff --git a/apps/gateway/src/services/token.service.spec.ts b/apps/gateway/src/services/token.service.spec.ts index 15f681c..d602245 100644 --- a/apps/gateway/src/services/token.service.spec.ts +++ b/apps/gateway/src/services/token.service.spec.ts @@ -57,7 +57,7 @@ describe('TokenService', () => { expect(decoded.jti).toBeDefined(); }); - it('extends expiry when remember=true', async () => { + it('extends expiry to the bounded remember window when remember=true', async () => { const token = await tokenService.generateRefreshToken({ id: 1, email: 'a@b.com', @@ -65,9 +65,12 @@ describe('TokenService', () => { remember: true, }); const decoded = tokenService.verifyRefreshToken(token); - expect((decoded.exp ?? 0) - (decoded.iat ?? 0)).toBeGreaterThan( - 30 * 24 * 60 * 60, - ); + const lifetime = (decoded.exp ?? 0) - (decoded.iat ?? 0); + // Default 30d window — much longer than the 8h non-remember token, but no + // longer a year-long token (T-5). + expect(lifetime).toBeGreaterThan(8 * 60 * 60); + expect(lifetime).toBeLessThanOrEqual(31 * 24 * 60 * 60); + expect(lifetime).toBeGreaterThanOrEqual(29 * 24 * 60 * 60); }); it('rejects a refresh token verified with the access secret', async () => { diff --git a/apps/gateway/src/services/token.service.ts b/apps/gateway/src/services/token.service.ts index 7f6f270..75b0cc7 100644 --- a/apps/gateway/src/services/token.service.ts +++ b/apps/gateway/src/services/token.service.ts @@ -4,6 +4,20 @@ import jwt, { JwtPayload } from 'jsonwebtoken'; export type ClientTokenType = 'access' | 'refresh'; +const DEFAULT_REMEMBER_DAYS = 30; + +/** + * Lifetime (in days) of a "remember me" refresh token / cookie. Single source + * of truth shared by the JWT expiry and the cookie maxAge so they never drift. + */ +export const rememberRefreshDays = (): number => { + const parsed = Number(process.env.JWT_REFRESH_REMEMBER_DAYS); + return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REMEMBER_DAYS; +}; + +export const rememberRefreshMaxAgeMs = (): number => + rememberRefreshDays() * 24 * 60 * 60 * 1000; + export interface AccessTokenPayload extends JwtPayload { id: number; email: string; @@ -81,8 +95,13 @@ class TokenService { typ: 'refresh', jti: input.jti ?? randomUUID(), }; + // "remember me" leans on refresh rotation rather than a year-long token: a + // bounded window (default 30d) limits how long stale/revoked sessions live. + const rememberDays = rememberRefreshDays(); const expiresIn = ( - input.remember ? '365d' : (process.env.JWT_REFRESH_EXPIRES_IN ?? '8h') + input.remember + ? `${rememberDays}d` + : (process.env.JWT_REFRESH_EXPIRES_IN ?? '8h') ) as jwt.SignOptions['expiresIn']; return jwt.sign(payload, this.#refreshSecret(), { algorithm: 'HS256', diff --git a/compose.yaml b/compose.yaml index ac45cb3..7af20f0 100644 --- a/compose.yaml +++ b/compose.yaml @@ -61,6 +61,7 @@ services: - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET} - JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-4h} - JWT_REFRESH_EXPIRES_IN=${JWT_REFRESH_EXPIRES_IN:-8h} + - JWT_REFRESH_REMEMBER_DAYS=${JWT_REFRESH_REMEMBER_DAYS:-30} - INTERNAL_JWT_PRIVATE_KEY=${INTERNAL_JWT_PRIVATE_KEY} - CORS_ORIGIN=${CORS_ORIGIN} - SERVICE_FQDN_GATEWAY=${SERVICE_FQDN_GATEWAY} From 453d38270c8d833e079f89d054cc9c2b4dca0233 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:16:28 +0200 Subject: [PATCH 06/24] feat(security): rate-limit gateway auth endpoints [MEDIO] T-6: /login and the auth surface had no throttling/lockout, enabling credential stuffing and password brute force. - Add express-rate-limit limiters: loginRateLimiter keyed by IP+email counting only failed attempts (skipSuccessfulRequests), and a coarse per-IP authRateLimiter over the whole /v1/auth router. - Configurable via LOGIN_RATE_*/AUTH_RATE_* env (sane defaults); relies on the existing trust proxy:1 to see the real client IP behind Nginx. - Add supertest-based tests; document env in .env.example. Co-Authored-By: Claude Opus 4.8 (1M context) --- .env.example | 7 + apps/gateway/src/middleware/index.ts | 1 + .../middleware/rate-limit.middleware.spec.ts | 45 ++++ .../src/middleware/rate-limit.middleware.ts | 53 ++++ apps/gateway/src/routes/auth.routes.ts | 6 +- package-lock.json | 228 +++++++++++++++++- package.json | 3 + 7 files changed, 332 insertions(+), 11 deletions(-) create mode 100644 apps/gateway/src/middleware/rate-limit.middleware.spec.ts create mode 100644 apps/gateway/src/middleware/rate-limit.middleware.ts diff --git a/.env.example b/.env.example index 93988ae..f3e326d 100644 --- a/.env.example +++ b/.env.example @@ -46,6 +46,13 @@ INTERNAL_JWT_PUBLIC_KEY= GATEWAY_PORT=3100 API_BASE_URL=http://api:3200 +# Auth rate limiting (anti brute-force / credential stuffing). Login is keyed by +# IP+email and only counts failed attempts. +LOGIN_RATE_WINDOW_MS=900000 +LOGIN_RATE_MAX=10 +AUTH_RATE_WINDOW_MS=900000 +AUTH_RATE_MAX=100 + # API settings NODE_PORT=3200 NODE_ENV=development diff --git a/apps/gateway/src/middleware/index.ts b/apps/gateway/src/middleware/index.ts index 331f314..275a70f 100644 --- a/apps/gateway/src/middleware/index.ts +++ b/apps/gateway/src/middleware/index.ts @@ -1 +1,2 @@ export * from './auth.middleware'; +export * from './rate-limit.middleware'; diff --git a/apps/gateway/src/middleware/rate-limit.middleware.spec.ts b/apps/gateway/src/middleware/rate-limit.middleware.spec.ts new file mode 100644 index 0000000..a865777 --- /dev/null +++ b/apps/gateway/src/middleware/rate-limit.middleware.spec.ts @@ -0,0 +1,45 @@ +import express from 'express'; +import request from 'supertest'; +import { describe, expect, it } from 'vitest'; +import { loginRateLimiter } from './rate-limit.middleware'; + +const buildApp = () => { + const app = express(); + app.set('trust proxy', 1); + app.use(express.json()); + // Always "fail" so skipSuccessfulRequests does not exempt the attempts. + app.post('/login', loginRateLimiter, (_req, res) => { + res.status(401).json({ error: 'bad creds' }); + }); + return app; +}; + +describe('loginRateLimiter', () => { + it('429s after too many failed attempts for the same ip+email', async () => { + const app = buildApp(); + const agent = request(app); + const body = { email: 'victim@example.com', password: 'x' }; + + let last = 401; + for (let i = 0; i < 12; i++) { + const res = await agent.post('/login').send(body); + last = res.status; + } + expect(last).toBe(429); + }); + + it('keys by email: a different account is not throttled by another’s failures', async () => { + const app = buildApp(); + const agent = request(app); + + for (let i = 0; i < 12; i++) { + await agent + .post('/login') + .send({ email: 'a@example.com', password: 'x' }); + } + const other = await agent + .post('/login') + .send({ email: 'b@example.com', password: 'x' }); + expect(other.status).toBe(401); + }); +}); diff --git a/apps/gateway/src/middleware/rate-limit.middleware.ts b/apps/gateway/src/middleware/rate-limit.middleware.ts new file mode 100644 index 0000000..813970a --- /dev/null +++ b/apps/gateway/src/middleware/rate-limit.middleware.ts @@ -0,0 +1,53 @@ +import HttpResponser from '@gateway/adapters/http/http.responser'; +import type { Request, Response } from 'express'; +import rateLimit from 'express-rate-limit'; + +const num = (value: string | undefined, fallback: number): number => { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +}; + +const tooManyRequests = (_req: Request, res: Response) => + HttpResponser.errorJson( + res, + { message: 'Too many attempts, please try again later.' }, + 429, + ); + +/** + * Anti brute-force / credential-stuffing limiter for the login endpoint. + * Keyed by IP + submitted email so one attacker cannot lock every account by + * cycling addresses, nor exhaust one account from many IPs unnoticed. Only + * failed attempts count (skipSuccessfulRequests) so normal logins are never + * throttled. `trust proxy: 1` is set in main.ts, so req.ip is the real client. + */ +export const loginRateLimiter = rateLimit({ + windowMs: num(process.env.LOGIN_RATE_WINDOW_MS, 15 * 60 * 1000), + limit: num(process.env.LOGIN_RATE_MAX, 10), + standardHeaders: 'draft-7', + legacyHeaders: false, + skipSuccessfulRequests: true, + // IP comes from req.ip (trust proxy: 1 → real client behind Nginx). + validate: { keyGeneratorIpFallback: false }, + keyGenerator: (req) => { + const email = + typeof req.body?.email === 'string' + ? req.body.email.trim().toLowerCase() + : 'unknown'; + return `${req.ip ?? 'unknown-ip'}:${email}`; + }, + handler: tooManyRequests, +}); + +/** + * Coarser per-IP limiter for the rest of the auth surface (logout, refresh + * rotation that flows through the proxy) to blunt automated abuse without + * impacting interactive use. + */ +export const authRateLimiter = rateLimit({ + windowMs: num(process.env.AUTH_RATE_WINDOW_MS, 15 * 60 * 1000), + limit: num(process.env.AUTH_RATE_MAX, 100), + standardHeaders: 'draft-7', + legacyHeaders: false, + handler: tooManyRequests, +}); diff --git a/apps/gateway/src/routes/auth.routes.ts b/apps/gateway/src/routes/auth.routes.ts index 37f1653..a68bb44 100644 --- a/apps/gateway/src/routes/auth.routes.ts +++ b/apps/gateway/src/routes/auth.routes.ts @@ -1,9 +1,13 @@ import authController from '@gateway/controllers/auth.controller'; +import { authRateLimiter, loginRateLimiter } from '@gateway/middleware'; import { Router } from 'express'; const authRouter = Router(); -authRouter.post('/login', authController.login); +// Coarse per-IP cap on the whole auth surface, plus a strict per-IP+email cap +// on login (counts only failed attempts) to resist credential stuffing. +authRouter.use(authRateLimiter); +authRouter.post('/login', loginRateLimiter, authController.login); authRouter.post('/logout', authController.logout); export default authRouter; diff --git a/package-lock.json b/package-lock.json index 7f9b66e..095c687 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "express-rate-limit": "^7.5.1", "http-proxy-middleware": "^3.0.5", "jose": "^5.10.0", "jsonwebtoken": "^9.0.3", @@ -77,6 +78,7 @@ "@types/cookie-parser": "^1.4.10", "@types/express": "^5.0.6", "@types/node": "^22.13.0", + "@types/supertest": "^6.0.3", "@typescript-eslint/eslint-plugin": "^8.59.0", "@typescript-eslint/parser": "^8.59.0", "@vitest/coverage-v8": "^4.0.8", @@ -91,6 +93,7 @@ "nx": "22.7.0", "prettier": "^3.8.3", "react-refresh": "^0.10.0", + "supertest": "^7.2.2", "ts-node": "10.9.1", "typescript": "^5.8.2", "vitest": "^4.0.8", @@ -6865,6 +6868,25 @@ } } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/@modelcontextprotocol/sdk/node_modules/jose": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", @@ -7921,6 +7943,19 @@ "webpack": "^5.54.0" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -9970,6 +10005,16 @@ "win32" ] }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@parcel/watcher": { "version": "2.5.6", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", @@ -12438,6 +12483,13 @@ "@types/express": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -12599,6 +12651,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -12726,6 +12785,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/superagent": { + "version": "8.1.10", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.10.tgz", + "integrity": "sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/validator": { "version": "13.15.10", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", @@ -14096,6 +14179,13 @@ "integrity": "sha512-L0XlBwfx9QetHOsbLDrE/vh2t018w9462HM3iaFfxRiK83aJjAt/Ja3NMkOW7FICwWTlQBa3ZbL5FKhuQWkDrg==", "license": "MIT" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/asn1js": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", @@ -15345,6 +15435,16 @@ "dev": true, "license": "ISC" }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -15565,6 +15665,13 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/copy-anything": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", @@ -16747,6 +16854,17 @@ "node": ">= 4.0.0" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", @@ -17749,14 +17867,10 @@ } }, "node_modules/express-rate-limit": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz", - "integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==", - "dev": true, + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", "license": "MIT", - "dependencies": { - "ip-address": "10.1.0" - }, "engines": { "node": ">= 16" }, @@ -17827,6 +17941,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -18403,6 +18524,24 @@ "node": ">= 0.6" } }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -19444,9 +19583,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "dev": true, "license": "MIT", "engines": { @@ -21591,6 +21730,16 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -26850,6 +26999,65 @@ "webpack": "^5.0.0" } }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index b25ad8c..b48fe2c 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "@types/cookie-parser": "^1.4.10", "@types/express": "^5.0.6", "@types/node": "^22.13.0", + "@types/supertest": "^6.0.3", "@typescript-eslint/eslint-plugin": "^8.59.0", "@typescript-eslint/parser": "^8.59.0", "@vitest/coverage-v8": "^4.0.8", @@ -100,6 +101,7 @@ "nx": "22.7.0", "prettier": "^3.8.3", "react-refresh": "^0.10.0", + "supertest": "^7.2.2", "ts-node": "10.9.1", "typescript": "^5.8.2", "vitest": "^4.0.8", @@ -126,6 +128,7 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "express-rate-limit": "^7.5.1", "http-proxy-middleware": "^3.0.5", "jose": "^5.10.0", "jsonwebtoken": "^9.0.3", From 19055a39bca63463e58ce86ea3665fe313964b31 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:18:13 +0200 Subject: [PATCH 07/24] ci(security): make audit gate real and fix invalid job graph [MEDIO] T-7: the security job ran 'npm audit ... || true' with continue-on-error, so vulnerable deps never blocked a merge; and notify needs [lint,test,build,security,docker] referenced build/docker which are commented out, making the graph invalid. - Gate on production deps: npm audit --omit=dev --audit-level=high (blocking), full report still generated+uploaded for visibility. Dev/build-tool advisories churn and aren't shipped, so they're reported, not gated. - Apply non-breaking npm audit fix: clears the production axios high advisory (remaining prod issues are moderate, below the gate). - Fix notify needs -> [lint, test, security]. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 19 +- package-lock.json | 755 ++++++++++++++++++++++----------------- 2 files changed, 442 insertions(+), 332 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08078c3..5d6dc8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,13 +115,16 @@ jobs: - name: 📥 Install dependencies run: npm ci - - name: 🔒 Run Security Audit - run: npm audit --audit-level moderate || true - continue-on-error: true - - - name: 📊 Generate Audit Report + # Blocks the pipeline on high/critical advisories in PRODUCTION deps — + # the code that actually ships. Dev/build-tool advisories are reported but + # not gated (they churn constantly and can't be fixed without major + # toolchain bumps). Tighten with `npm audit fix` when prod issues appear. + - name: 🔒 Run Security Audit (production deps) + run: npm audit --omit=dev --audit-level=high + + - name: 📊 Generate Full Audit Report + if: always() run: npm audit --json > audit-report.json || true - continue-on-error: true - name: 📤 Upload Audit Report uses: actions/upload-artifact@v4 @@ -162,7 +165,9 @@ jobs: notify: name: 📢 Notify runs-on: ubuntu-latest - needs: [lint, test, build, security, docker] + # build & docker are currently disabled (commented out below); keep this + # list in sync with the jobs that actually run or the graph is invalid. + needs: [lint, test, security] if: failure() steps: - name: 📢 Notify Failure diff --git a/package-lock.json b/package-lock.json index 095c687..d34d86e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -405,17 +405,17 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "21.2.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-21.2.8.tgz", - "integrity": "sha512-NvTsu4+aDxj/mObw/tlFH7iyiDlFA7uVmk5jdicaV7TeY8QbMA0ona1mlSqehhyE0dgrROeYV6rXeJ4BcX7waw==", + "version": "21.2.14", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-21.2.14.tgz", + "integrity": "sha512-EgKDd5pYctnsj3yYAZt3vivodH+r61X0ivWbZljcwT7OO4CEDyTR7Vtu4TUSr+tlplz5x2PYsWNr1nqyF0hufw==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2102.8", - "@angular-devkit/build-webpack": "0.2102.8", - "@angular-devkit/core": "21.2.8", - "@angular/build": "21.2.8", + "@angular-devkit/architect": "0.2102.14", + "@angular-devkit/build-webpack": "0.2102.14", + "@angular-devkit/core": "21.2.14", + "@angular/build": "21.2.14", "@babel/core": "7.29.0", "@babel/generator": "7.29.1", "@babel/helper-annotate-as-pure": "7.27.3", @@ -426,7 +426,7 @@ "@babel/preset-env": "7.29.2", "@babel/runtime": "7.29.2", "@discoveryjs/json-ext": "0.6.3", - "@ngtools/webpack": "21.2.8", + "@ngtools/webpack": "21.2.14", "ansi-colors": "4.1.3", "autoprefixer": "10.4.27", "babel-loader": "10.0.0", @@ -447,7 +447,7 @@ "ora": "9.3.0", "picomatch": "4.0.4", "piscina": "5.1.4", - "postcss": "8.5.6", + "postcss": "8.5.12", "postcss-loader": "8.2.0", "resolve-url-loader": "5.0.0", "rxjs": "7.8.2", @@ -481,7 +481,7 @@ "@angular/platform-browser": "^21.0.0", "@angular/platform-server": "^21.0.0", "@angular/service-worker": "^21.0.0", - "@angular/ssr": "^21.2.8", + "@angular/ssr": "^21.2.14", "@web/test-runner": "^0.20.0", "browser-sync": "^3.0.2", "jest": "^30.2.0", @@ -537,6 +537,71 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/architect": { + "version": "0.2102.14", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.14.tgz", + "integrity": "sha512-0+vjVsCkMyJdVjz5XkPW+Bdf/9TI8V2voomx/+o0o+oOaqqiEhptQWFnaIlLr7HasjB0LxXK5P9L0oQ61vxj8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.14", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { + "version": "21.2.14", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.14.tgz", + "integrity": "sha512-RSOWXB9bFc2nwRWMxbIT0RbSNFUrwfBo4N5MNxbyQ69Ndc0gVm3h+3ArHv0qotH4d+pJYbm5ttXu8YqR2kc0CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -1181,13 +1246,13 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.2102.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.2102.8.tgz", - "integrity": "sha512-XBSfx302hMcnF7ABxObIInusYr8S0R9vqORKe48/bKhI5J15gyGXrlRvjXEPCY3s3w3ysLKcw5POBo5zEmmuGQ==", + "version": "0.2102.14", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.2102.14.tgz", + "integrity": "sha512-UjZzypzYLYaWMVplu9CYpx0gxKYu9+V3GiOrtlshInuGMZe9uQy7wRgiFUUKClRSMf8CCT1jfCof9gFwqYCRuQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2102.8", + "@angular-devkit/architect": "0.2102.14", "rxjs": "7.8.2" }, "engines": { @@ -1200,6 +1265,53 @@ "webpack-dev-server": "^5.0.2" } }, + "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/architect": { + "version": "0.2102.14", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.14.tgz", + "integrity": "sha512-0+vjVsCkMyJdVjz5XkPW+Bdf/9TI8V2voomx/+o0o+oOaqqiEhptQWFnaIlLr7HasjB0LxXK5P9L0oQ61vxj8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.14", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/core": { + "version": "21.2.14", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.14.tgz", + "integrity": "sha512-RSOWXB9bFc2nwRWMxbIT0RbSNFUrwfBo4N5MNxbyQ69Ndc0gVm3h+3ArHv0qotH4d+pJYbm5ttXu8YqR2kc0CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular-devkit/core": { "version": "21.2.8", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.8.tgz", @@ -1337,14 +1449,14 @@ } }, "node_modules/@angular/build": { - "version": "21.2.8", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.8.tgz", - "integrity": "sha512-t0PHT7ONDMLwcjC9GaClNF+gsUKN78ofBikw4huiu6np5Rwmxp8KKCrdoRx20lOiibSolXgjZ2Ny0xxjNdNdQA==", + "version": "21.2.14", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.14.tgz", + "integrity": "sha512-l8JB326iIwum2WmbopUUFdiuYsbHchix6MH8o6F6FA7LJr8QLTvipwwbw+Jx31/RE50WkGmzsZ1fBDw/cMbmUw==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2102.8", + "@angular-devkit/architect": "0.2102.14", "@babel/core": "7.29.0", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", @@ -1387,7 +1499,7 @@ "@angular/platform-browser": "^21.0.0", "@angular/platform-server": "^21.0.0", "@angular/service-worker": "^21.0.0", - "@angular/ssr": "^21.2.8", + "@angular/ssr": "^21.2.14", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^21.0.0", @@ -1436,6 +1548,53 @@ } } }, + "node_modules/@angular/build/node_modules/@angular-devkit/architect": { + "version": "0.2102.14", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.14.tgz", + "integrity": "sha512-0+vjVsCkMyJdVjz5XkPW+Bdf/9TI8V2voomx/+o0o+oOaqqiEhptQWFnaIlLr7HasjB0LxXK5P9L0oQ61vxj8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.14", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/build/node_modules/@angular-devkit/core": { + "version": "21.2.14", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.14.tgz", + "integrity": "sha512-RSOWXB9bFc2nwRWMxbIT0RbSNFUrwfBo4N5MNxbyQ69Ndc0gVm3h+3ArHv0qotH4d+pJYbm5ttXu8YqR2kc0CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular/build/node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -2236,12 +2395,12 @@ "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -2464,9 +2623,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -2488,29 +2647,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -2533,9 +2692,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", "engines": { @@ -2606,9 +2765,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -2616,9 +2775,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2664,13 +2823,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -3504,16 +3663,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.29.0" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -4250,48 +4409,65 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -6898,13 +7074,13 @@ } }, "node_modules/@module-federation/bridge-react-webpack-plugin": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-2.3.3.tgz", - "integrity": "sha512-W2jQ3Wuqree9Dq3UAx8jGbYtvHuuYgzrd2j9FP8Bt6NaynaNU1yYG86MBnAhZJPTltex0CguudR1dgFpYdvLUg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-2.5.1.tgz", + "integrity": "sha512-bMRKo1ac6dYUhvPWH6ElFRfR+yWjbCse2mwu9fEzjemeaAqqse0K7XBIrW+4HcU7k/A6fu4Dk8o137ol6ZFXvw==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/sdk": "2.3.3", + "@module-federation/sdk": "2.5.1", "@types/semver": "7.5.8", "semver": "7.6.3" } @@ -6923,14 +7099,14 @@ } }, "node_modules/@module-federation/cli": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/cli/-/cli-2.3.3.tgz", - "integrity": "sha512-g3f3aEruv07zK4VcUlAllswrp2ncA/jF0P0yoEWNRa9K7N+xNCfqcdzw2aVWOJ30qNMurhLWuyzYqfDIx0LpfQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/cli/-/cli-2.5.1.tgz", + "integrity": "sha512-Y5dQrcPoph91KAzg6XXQ40faqFQ4iPwq3JuvFbsawyKwhQ/+aHkaH7cfclNbNr9pHGiiozkdxb22zK3oJRI3rw==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/dts-plugin": "2.3.3", - "@module-federation/sdk": "2.3.3", + "@module-federation/dts-plugin": "2.5.1", + "@module-federation/sdk": "2.5.1", "commander": "11.1.0", "jiti": "2.4.2" }, @@ -6941,46 +7117,23 @@ "node": ">=16.0.0" } }, - "node_modules/@module-federation/data-prefetch": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/data-prefetch/-/data-prefetch-2.3.3.tgz", - "integrity": "sha512-ZM1QtyjbWYnhUizHFhwYjHGXlkZek3vzTpL35d5FkAhVrOU0u0Qv6zpZjdcCm0FJznqVsUQx1w0vagUyGWQf0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@module-federation/runtime": "2.3.3", - "@module-federation/sdk": "2.3.3" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "node_modules/@module-federation/dts-plugin": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/dts-plugin/-/dts-plugin-2.3.3.tgz", - "integrity": "sha512-VNtURt+hvieNKCBleAqHKffLAU4clKmuuqLQIbvDkFbGe4bo7hUaq5DruTnJBWWDOizZx0OQrdQYPijCnBK6UQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/dts-plugin/-/dts-plugin-2.5.1.tgz", + "integrity": "sha512-XylIL02As+VXk4DzFb8qYQs1YYoY0MZOpIqhf06LMkZBjPdOE0wJ1GOBvhFP9kM/cfuK7QV//mNdrWlZgvEkRA==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "2.3.3", - "@module-federation/managers": "2.3.3", - "@module-federation/sdk": "2.3.3", - "@module-federation/third-party-dts-extractor": "2.3.3", + "@module-federation/error-codes": "2.5.1", + "@module-federation/managers": "2.5.1", + "@module-federation/sdk": "2.5.1", + "@module-federation/third-party-dts-extractor": "2.5.1", "adm-zip": "0.5.10", "ansi-colors": "4.1.3", "isomorphic-ws": "5.0.0", "node-schedule": "2.1.1", "undici": "7.24.7", - "ws": "8.18.0" + "ws": "8.21.0" }, "peerDependencies": { "typescript": "^4.9.0 || ^5.0.0", @@ -7003,24 +7156,23 @@ } }, "node_modules/@module-federation/enhanced": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/enhanced/-/enhanced-2.3.3.tgz", - "integrity": "sha512-BJSs56lqO9NI9aC+hVhg2CU/UwG1TphVl1b7WBx6Jv6DYUyVQbgXeQpgqYVsxYVRKYOl7eDZmjXl2eA/n1IP/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@module-federation/bridge-react-webpack-plugin": "2.3.3", - "@module-federation/cli": "2.3.3", - "@module-federation/data-prefetch": "2.3.3", - "@module-federation/dts-plugin": "2.3.3", - "@module-federation/error-codes": "2.3.3", - "@module-federation/inject-external-runtime-core-plugin": "2.3.3", - "@module-federation/managers": "2.3.3", - "@module-federation/manifest": "2.3.3", - "@module-federation/rspack": "2.3.3", - "@module-federation/runtime-tools": "2.3.3", - "@module-federation/sdk": "2.3.3", - "@module-federation/webpack-bundler-runtime": "2.3.3", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/enhanced/-/enhanced-2.5.1.tgz", + "integrity": "sha512-uddrS58NxOW2lsLfXLHenzfX37QMuZ0g03nG9SVhqfo3tMdP9877SLaMKRpoOvy7YQRfrXVAGF//9U9QtEWr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@module-federation/bridge-react-webpack-plugin": "2.5.1", + "@module-federation/cli": "2.5.1", + "@module-federation/dts-plugin": "2.5.1", + "@module-federation/error-codes": "2.5.1", + "@module-federation/inject-external-runtime-core-plugin": "2.5.1", + "@module-federation/managers": "2.5.1", + "@module-federation/manifest": "2.5.1", + "@module-federation/rspack": "2.5.1", + "@module-federation/runtime-tools": "2.5.1", + "@module-federation/sdk": "2.5.1", + "@module-federation/webpack-bundler-runtime": "2.5.1", "schema-utils": "4.3.0", "tapable": "2.3.0", "upath": "2.0.1" @@ -7046,56 +7198,56 @@ } }, "node_modules/@module-federation/error-codes": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-2.3.3.tgz", - "integrity": "sha512-UVtKBoKnRDcHgByIDvPRZSxQqjqbNH7NvJm1KHLoce33+EDiIdZYs0HvvUQv43RgESpB9s7HjrqFlq3bEcAgfQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-2.5.1.tgz", + "integrity": "sha512-3KIR8XbEW0Y+Fn8IAnxzDWMvXQWiS40Z1TE/Fft9aTeXP9dDAM7AiVhjTh5yF2csAwHSt/1LJVZbiCmS13mE8A==", "dev": true, "license": "MIT" }, "node_modules/@module-federation/inject-external-runtime-core-plugin": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/inject-external-runtime-core-plugin/-/inject-external-runtime-core-plugin-2.3.3.tgz", - "integrity": "sha512-ImSft6hOkMdnpZX8O+RydwkYENxhAwT92n1OAT3Xf01DXMrEpSO0PqBlPGgontxuiaV9dM2/xWSLGuIWaOtupA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/inject-external-runtime-core-plugin/-/inject-external-runtime-core-plugin-2.5.1.tgz", + "integrity": "sha512-cCFCQ0j0xObXwVwBq/ZiU+MMdJrE2OVwdPUR7l6NHKKwKAOmkagM+AE5wNEkzVht6m+Dh6x6/5UTqqa/vbDrjQ==", "dev": true, "license": "MIT", "peerDependencies": { - "@module-federation/runtime-tools": "2.3.3" + "@module-federation/runtime-tools": "2.5.1" } }, "node_modules/@module-federation/managers": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/managers/-/managers-2.3.3.tgz", - "integrity": "sha512-sYL0t2guakJ+nDSQANH54uz5q1YxaNCn5C3lr+7BoRD49dX7Z6k7094yqOPEy8trzqdIoQVFpgewVA6IC/FeyQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/managers/-/managers-2.5.1.tgz", + "integrity": "sha512-AWbkH/U76FJDbdwBOodQ1An40WOdrDGAgqUMc8RD62+Ec/ocPfda0NhFcJbPxlUIvSFPTvBv6DwTnygmNC7GPA==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/sdk": "2.3.3", + "@module-federation/sdk": "2.5.1", "find-pkg": "2.0.0" } }, "node_modules/@module-federation/manifest": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/manifest/-/manifest-2.3.3.tgz", - "integrity": "sha512-mAEXuo5sGt8FUDzftU8f0ci0PbsZIDcLRYX9AkXwbXg0JRyVEvWyiBrEKF+zZuy7YM7eRdyp6JjLJPDzufhj5w==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/manifest/-/manifest-2.5.1.tgz", + "integrity": "sha512-z44Fita44rpLufoEW71tWw+eWGMVLVQzT2OjrzOHH8JLPOX3czT0INPwVbRp9v8puzWiU6Kz5W6PqelqutpJKA==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/dts-plugin": "2.3.3", - "@module-federation/managers": "2.3.3", - "@module-federation/sdk": "2.3.3", + "@module-federation/dts-plugin": "2.5.1", + "@module-federation/managers": "2.5.1", + "@module-federation/sdk": "2.5.1", "find-pkg": "2.0.0" } }, "node_modules/@module-federation/node": { - "version": "2.7.41", - "resolved": "https://registry.npmjs.org/@module-federation/node/-/node-2.7.41.tgz", - "integrity": "sha512-ZaNrfX+7ua8UvnRa6qgrDtViU9Oz3oBGpFprldU8ek0NB2QWNACu9RMU7fHdTD/FKlzGVLi/LgdKnNKkmJD2TA==", + "version": "2.7.44", + "resolved": "https://registry.npmjs.org/@module-federation/node/-/node-2.7.44.tgz", + "integrity": "sha512-XlwBWbSX2RMQM7dKQifUsMncDtvjeMFGH5LW6yG2cM/nqrM9uvN0QaG3AJIbYXKpOqH94x/XfP7jBV5JYyWpzQ==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/enhanced": "2.3.3", - "@module-federation/runtime": "2.3.3", - "@module-federation/sdk": "2.3.3", + "@module-federation/enhanced": "2.5.1", + "@module-federation/runtime": "2.5.1", + "@module-federation/sdk": "2.5.1", "encoding": "0.1.13", "node-fetch": "2.7.0", "tapable": "2.3.0" @@ -7156,19 +7308,19 @@ } }, "node_modules/@module-federation/rspack": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/rspack/-/rspack-2.3.3.tgz", - "integrity": "sha512-4s3G+wXZ6J3rKe0EeZnGLQUM7y+qpiI5NM3U6ylZuxD8q7mAwQVHThbH6ruDYUNDVEOc6N0j/+/LdfGRw+e5xw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/rspack/-/rspack-2.5.1.tgz", + "integrity": "sha512-9Qu4Q7P4VHUhMQLHKA95x3L85WKPtk2Lz0VPpUprIwlKhKgZbuy5ou2PjlWiaPIffWo1JsVfoBi+P9dtpOeEsw==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/bridge-react-webpack-plugin": "2.3.3", - "@module-federation/dts-plugin": "2.3.3", - "@module-federation/inject-external-runtime-core-plugin": "2.3.3", - "@module-federation/managers": "2.3.3", - "@module-federation/manifest": "2.3.3", - "@module-federation/runtime-tools": "2.3.3", - "@module-federation/sdk": "2.3.3" + "@module-federation/bridge-react-webpack-plugin": "2.5.1", + "@module-federation/dts-plugin": "2.5.1", + "@module-federation/inject-external-runtime-core-plugin": "2.5.1", + "@module-federation/managers": "2.5.1", + "@module-federation/manifest": "2.5.1", + "@module-federation/runtime-tools": "2.5.1", + "@module-federation/sdk": "2.5.1" }, "peerDependencies": { "@rspack/core": "^0.7.0 || ^1.0.0 || ^2.0.0-0", @@ -7185,47 +7337,47 @@ } }, "node_modules/@module-federation/runtime": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-2.3.3.tgz", - "integrity": "sha512-JYJ3qv9V85DtBtT/ppDuJNwBTUrYqqZDYcyiTzwY5+44dC5QPvgJ//F+BOhAhZ02WkZV0b4jsKTyLOC3vXKGqQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-2.5.1.tgz", + "integrity": "sha512-Tf33FIpnQMn8FjIUAQMtSTYQgGibfh5vEvJihFO3q/hG9LiWwLMErZvOz/+wcPsE81gzHjYPxQgMKGSP3BuG8g==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "2.3.3", - "@module-federation/runtime-core": "2.3.3", - "@module-federation/sdk": "2.3.3" + "@module-federation/error-codes": "2.5.1", + "@module-federation/runtime-core": "2.5.1", + "@module-federation/sdk": "2.5.1" } }, "node_modules/@module-federation/runtime-core": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-2.3.3.tgz", - "integrity": "sha512-B07LDH9KxhBO3GbULGW64mQFVQBtrEd3PoaCBm7XR1IbU8rMQUJQjDNVZgXYcyhRPBVP+3KWZuiaKFRiNb6PQw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-2.5.1.tgz", + "integrity": "sha512-UMuMsWHXeMrm8Isl8YD6/s1jmTVau3SQhp9RO4Ln+eD2lrjM4hQSwOX2xPtfT1C1I4/E6hgyZQV1K1Q/3Zpr0Q==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "2.3.3", - "@module-federation/sdk": "2.3.3" + "@module-federation/error-codes": "2.5.1", + "@module-federation/sdk": "2.5.1" } }, "node_modules/@module-federation/runtime-tools": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-2.3.3.tgz", - "integrity": "sha512-XODzyLbBYcy4wnYBXKIBqaHPVfBx1HshGdjZmSctDDnx9/VYgdx9DShb6UI+WuQBKJgPzTcx4xbvbCM4SdMilQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-2.5.1.tgz", + "integrity": "sha512-pYUNvaQQBEwP66TLrjmmfkDIrTmPnX0kK86HgClkWLQKkX/oCgnqDxEgNbjeCc75dwUvZP6fW2d0pZ5++XILTw==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/runtime": "2.3.3", - "@module-federation/webpack-bundler-runtime": "2.3.3" + "@module-federation/runtime": "2.5.1", + "@module-federation/webpack-bundler-runtime": "2.5.1" } }, "node_modules/@module-federation/sdk": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-2.3.3.tgz", - "integrity": "sha512-mwCS+LQdqiSc6fM5iz/S60ibaFNSH6kNqlZkCRIuS4yjdZ+jgnihz+6xp1QzppvfFgKLhEHBiXOmcYOdk3Ckew==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-2.5.1.tgz", + "integrity": "sha512-FDhCx81ZCxX1oT/fyt/bW+gpPt287GR156E/Thv1yhb9XyNHGNkqe8zqJOipOMfb07E22OMzSzOulCBvAOgn3g==", "dev": true, "license": "MIT", "peerDependencies": { - "node-fetch": "^3.3.2" + "node-fetch": "^2.7.0 || ^3.3.2" }, "peerDependenciesMeta": { "node-fetch": { @@ -7234,9 +7386,9 @@ } }, "node_modules/@module-federation/third-party-dts-extractor": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/third-party-dts-extractor/-/third-party-dts-extractor-2.3.3.tgz", - "integrity": "sha512-rR94TjC1QVQLQPTazI0waLc76hI8dnv6aHTl+PUEIY9s5hXp8TA85XS0QJQqIf2KTjlPgZbWAwyFjOAJluTjaw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/third-party-dts-extractor/-/third-party-dts-extractor-2.5.1.tgz", + "integrity": "sha512-/jD/o2IivDgg6jGUgdb9NZtlJWeoz1uKcblGL2BxYVzzlKT+1YS7Y2idTl1tqp4hNdFnDlPpQv6WLdKiLoOPNw==", "dev": true, "license": "MIT", "dependencies": { @@ -7245,21 +7397,21 @@ } }, "node_modules/@module-federation/webpack-bundler-runtime": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-2.3.3.tgz", - "integrity": "sha512-W+P6ZF9J3gwnQuoF07YV0OiR1D6sI/uErUu4+c3QXxka3orANUHujkddNSsDxL1obAGoJa7Da99crZKf7u2j/w==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-2.5.1.tgz", + "integrity": "sha512-0pUsP9aaWIUcfWUXqax/iSwozngORwf4RK0R1qTOYYC13qx+p4p1Ck28Rz6Tzj/6zpzJgcMQXR7nW4sL+ztaww==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "2.3.3", - "@module-federation/runtime": "2.3.3", - "@module-federation/sdk": "2.3.3" + "@module-federation/error-codes": "2.5.1", + "@module-federation/runtime": "2.5.1", + "@module-federation/sdk": "2.5.1" } }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", "cpu": [ "arm64" ], @@ -7271,9 +7423,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", "cpu": [ "x64" ], @@ -7285,9 +7437,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", "cpu": [ "arm" ], @@ -7299,9 +7451,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", "cpu": [ "arm64" ], @@ -7313,9 +7465,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", "cpu": [ "x64" ], @@ -7327,9 +7479,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", "cpu": [ "x64" ], @@ -7927,9 +8079,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "21.2.8", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-21.2.8.tgz", - "integrity": "sha512-e3A9NBuco6QdIqi4jKB8ateTGDEB2Nlby9Y5zG/uvbK0ggV6GPcEkqmVNX0LqyIQYv2Br9WmPLnJnegvl5lqEA==", + "version": "21.2.14", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-21.2.14.tgz", + "integrity": "sha512-HXt4pYLlWCJphO6TZoTsi2Z9Lyq/PqYpiKlqUmNoo/oIiSuXtoT/8+84Z/SfBdzeZpiVrAFz+/QGTVFoD8RSGg==", "dev": true, "license": "MIT", "engines": { @@ -14281,16 +14433,42 @@ } }, "node_modules/axios": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", - "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", + "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, + "node_modules/axios/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/axios/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/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -14748,9 +14926,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -16044,35 +16222,6 @@ "postcss": "^8.5.10" } }, - "node_modules/css-minimizer-webpack-plugin/node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-colormin": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.9.tgz", @@ -17949,9 +18098,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -18972,28 +19121,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/happy-dom/node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/harmony-reflect": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", @@ -19073,9 +19200,9 @@ } }, "node_modules/hono": { - "version": "4.12.15", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz", - "integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==", + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", "dev": true, "license": "MIT", "engines": { @@ -20665,28 +20792,6 @@ "node": ">=18" } }, - "node_modules/jsdom/node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -22065,9 +22170,9 @@ "license": "MIT" }, "node_modules/msgpackr": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz", - "integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz", + "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==", "dev": true, "license": "MIT", "optional": true, @@ -22076,9 +22181,9 @@ } }, "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -22090,12 +22195,12 @@ "download-msgpackr-prebuilds": "bin/download-prebuilds.js" }, "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" } }, "node_modules/multer": { @@ -24124,9 +24229,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", "dev": true, "funding": [ { @@ -24621,9 +24726,9 @@ } }, "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -29811,9 +29916,9 @@ } }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "license": "MIT", "engines": { From b5af66dfda243673f603be780ff97e0ce741eb44 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:19:15 +0200 Subject: [PATCH 08/24] ci(security): harden release workflow against supply-chain abuse [MEDIO] T-8: release.yml ran on every push to main with contents:write and pushed back to main using a token left in the local git config (persist-credentials default true). Any pre-push lifecycle script/dependency could reuse that credential to push to main / cut releases. - Trigger via workflow_dispatch with a bump choice (deliberate, maintainer-gated) instead of automatic push-to-main. - Top-level permissions: contents:read; elevate to write only in the job. - Checkout with persist-credentials:false; inject GITHUB_TOKEN only at the push step via an x-access-token remote URL. - npm version --ignore-scripts so no project/dep scripts run on the privileged runner before the push. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 70 +++++++++++++++++------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aba3be9..b2bb4ea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,31 @@ name: 🏷️ Release +# Releases are DELIBERATE, not automatic. Running on every push to main meant a +# privileged (contents:write) job with a push-capable token ran on each merge — +# a supply-chain footgun: any pre-push step (npm lifecycle script, dependency) +# could abuse the persisted credential to push to main. Now a maintainer +# triggers it manually and picks the bump type. on: - push: - branches: [main] + workflow_dispatch: + inputs: + bump: + description: Semver bump type + required: true + default: patch + type: choice + options: + - patch + - minor + - major + +# Least privilege by default; the release job elevates only what it needs. +permissions: + contents: read jobs: release: name: 🏷️ Version & Release runs-on: ubuntu-latest - # Skip version bump commits pushed by this same workflow - if: "!contains(github.event.head_commit.message, '[skip ci]') && github.actor != 'github-actions[bot]'" permissions: contents: write @@ -18,56 +34,40 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} + # Do NOT leave the auth token in the local git config where any later + # step / lifecycle script could reuse it. The push step injects it. + persist-credentials: false - name: 📦 Setup Node.js uses: actions/setup-node@v4 with: node-version: '22.12.0' - # ── Determine bump type from the commit/PR title ────────────────────────── - # feat!: or feat()!: → MAJOR - # feat: or feat(): → MINOR - # fix: or fix(): → PATCH - # anything else → no release - - name: 🔍 Determine bump type - id: bump - run: | - MSG=$(git log -1 --pretty=%s) - echo "Commit message: $MSG" - if echo "$MSG" | grep -qE '^feat(\(.+\))?!:'; then - echo "type=major" >> $GITHUB_OUTPUT - elif echo "$MSG" | grep -qE '^feat(\(.+\))?:'; then - echo "type=minor" >> $GITHUB_OUTPUT - elif echo "$MSG" | grep -qE '^fix(\(.+\))?:'; then - echo "type=patch" >> $GITHUB_OUTPUT - else - echo "type=none" >> $GITHUB_OUTPUT - fi - + # --ignore-scripts: never run project/dependency lifecycle scripts on the + # privileged runner before we push. - name: 🔢 Bump package version - if: steps.bump.outputs.type != 'none' - run: npm version ${{ steps.bump.outputs.type }} --no-git-tag-version - - - name: 📌 Read new version - if: steps.bump.outputs.type != 'none' id: version - run: echo "value=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + run: | + npm version ${{ inputs.bump }} --no-git-tag-version --ignore-scripts + echo "value=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT" - name: 📝 Commit, tag & push - if: steps.bump.outputs.type != 'none' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add package.json package-lock.json git commit -m "chore: release v${{ steps.version.outputs.value }} [skip ci]" - git tag v${{ steps.version.outputs.value }} - git push origin main --follow-tags + git tag "v${{ steps.version.outputs.value }}" + git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ + HEAD:main --follow-tags - name: 🚀 Create GitHub Release - if: steps.bump.outputs.type != 'none' uses: softprops/action-gh-release@v2 with: tag_name: v${{ steps.version.outputs.value }} name: v${{ steps.version.outputs.value }} generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 58bf2131478c4f716335b4073459b32b77cd16f3 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:19:34 +0200 Subject: [PATCH 09/24] fix(security): bind dev Postgres to loopback only [MEDIO] T-9: docker-compose.db.yml published 5432 on 0.0.0.0, exposing the dev database to the whole network. Combined with the (now-removed, see T-1) default password it was a direct DB compromise on an untrusted network. - Publish on 127.0.0.1:${POSTGRESDB_PORT:-5432}:5432 so only the local host can reach it. Password was already made mandatory in T-1. Co-Authored-By: Claude Opus 4.8 (1M context) --- docker-compose.db.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker-compose.db.yml b/docker-compose.db.yml index cab0bff..1ab6d59 100644 --- a/docker-compose.db.yml +++ b/docker-compose.db.yml @@ -15,7 +15,9 @@ services: BOOTSTRAP_ADMIN_EMAIL: ${BOOTSTRAP_ADMIN_EMAIL:-} BOOTSTRAP_ADMIN_PASSWORD_HASH: ${BOOTSTRAP_ADMIN_PASSWORD_HASH:-} ports: - - "5432:5432" + # Bind to loopback only: the dev DB must not be reachable from other hosts + # on the network. Override POSTGRESDB_PORT if 5432 is taken locally. + - "127.0.0.1:${POSTGRESDB_PORT:-5432}:5432" volumes: - postgres_data:/var/lib/postgresql/data - ./db:/docker-entrypoint-initdb.d From 11e14aeccf467837997ebc701ea5ad66cbfa492e Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:22:34 +0200 Subject: [PATCH 10/24] feat(security): wire auth guards to routes + add unauthorized page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [MEDIO] T-10: app.routes.ts declared no canActivate, so access control relied solely on @if(auth.isLoggedIn()) in templates, and the guards redirected to a non-existent 'unauthorized' route (404 inside the router). - Add a real 'unauthorized' route + page (the guards' redirect target). - Add a guarded example 'profile' route (canActivateFn) demonstrating the protected-route pattern; link it from the logged-in home card. - Add a '**' wildcard redirect to the landing page. - i18n (en/es/ca) for the new pages; document the guard convention in apps/front/AGENTS.md (guards are UX only — backend enforces authz). Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/front/AGENTS.md | 25 ++++++++++------ apps/front/src/app/app.routes.ts | 19 ++++++++++++ .../src/app/pages/home/home.component.html | 8 +++++ .../src/app/pages/home/home.component.ts | 4 +++ .../app/pages/profile/profile.component.html | 29 +++++++++++++++++++ .../app/pages/profile/profile.component.ts | 25 ++++++++++++++++ .../unauthorized/unauthorized.component.html | 25 ++++++++++++++++ .../unauthorized/unauthorized.component.ts | 12 ++++++++ apps/front/src/assets/i18n/ca.json | 10 +++++++ apps/front/src/assets/i18n/en.json | 10 +++++++ apps/front/src/assets/i18n/es.json | 10 +++++++ 11 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 apps/front/src/app/pages/profile/profile.component.html create mode 100644 apps/front/src/app/pages/profile/profile.component.ts create mode 100644 apps/front/src/app/pages/unauthorized/unauthorized.component.html create mode 100644 apps/front/src/app/pages/unauthorized/unauthorized.component.ts diff --git a/apps/front/AGENTS.md b/apps/front/AGENTS.md index 430483b..ab4e1fa 100644 --- a/apps/front/AGENTS.md +++ b/apps/front/AGENTS.md @@ -14,15 +14,15 @@ ## Folder layout (`src/app/`) -| Path | Contents | -| -------------- | ---------------------------------------------------------------- | -| `pages/` | Routed feature components (`home`, `login`, …) | -| `components/` | Reusable presentational components (`confirm`, `language-switcher`) | -| `services/` | App-wide services; `abstract-state.class.ts` is the state base | -| `libs/auth/` | Auth slice: `auth.provider`, guards, interceptors, services | -| `models/` | View models (`state.model.ts`) | -| `constants/` | App constants (`languages.constant.ts`) | -| `app.config.ts`, `app.routes.ts` | Root providers and routing | +| Path | Contents | +| -------------------------------- | ------------------------------------------------------------------- | +| `pages/` | Routed feature components (`home`, `login`, …) | +| `components/` | Reusable presentational components (`confirm`, `language-switcher`) | +| `services/` | App-wide services; `abstract-state.class.ts` is the state base | +| `libs/auth/` | Auth slice: `auth.provider`, guards, interceptors, services | +| `models/` | View models (`state.model.ts`) | +| `constants/` | App constants (`languages.constant.ts`) | +| `app.config.ts`, `app.routes.ts` | Root providers and routing | ## Auth (`src/app/libs/auth/`) @@ -31,6 +31,13 @@ - `interceptors/auth.interceptor.ts` — attach the access token / handle refresh. - `auth.provider.ts` — wire it all into `app.config.ts`. - Use these instead of inlining auth logic in components. +- **Protected routes MUST declare `canActivate`** in `app.routes.ts` + (`[canActivateFn]` for auth, `[canActivateWithPermission(Permission.X)]` for + permissioned routes) — never rely on `@if (auth.isLoggedIn())` in the template + alone. Permission guards redirect to `unauthorized` (a real route) on failure. + Guards are **UX only**; the backend is the real authorization boundary, so a + protected page must still call APIs that enforce the permission server-side. + See `pages/profile` (auth-guarded example) and `pages/unauthorized`. ## Conventions diff --git a/apps/front/src/app/app.routes.ts b/apps/front/src/app/app.routes.ts index 6b759fc..b96c6a3 100644 --- a/apps/front/src/app/app.routes.ts +++ b/apps/front/src/app/app.routes.ts @@ -1,4 +1,5 @@ import { Route } from '@angular/router'; +import { canActivateFn } from './libs/auth/guards/auth.guard'; export const appRoutes: Route[] = [ { @@ -9,4 +10,22 @@ export const appRoutes: Route[] = [ path: 'login', loadComponent: () => import('./pages/login/login.component'), }, + // Example PROTECTED route — the guard redirects anonymous users to /login. + // Use canActivateWithPermission(...) from auth-permission.guard.ts for routes + // that also require a specific permission. Guards are UX only; the backend is + // the real authorization boundary. + { + path: 'profile', + canActivate: [canActivateFn], + loadComponent: () => import('./pages/profile/profile.component'), + }, + { + path: 'unauthorized', + loadComponent: () => import('./pages/unauthorized/unauthorized.component'), + }, + // Unknown routes fall back to the landing page instead of a blank 404. + { + path: '**', + redirectTo: '', + }, ]; diff --git a/apps/front/src/app/pages/home/home.component.html b/apps/front/src/app/pages/home/home.component.html index 2eb427f..56d70a9 100644 --- a/apps/front/src/app/pages/home/home.component.html +++ b/apps/front/src/app/pages/home/home.component.html @@ -33,6 +33,14 @@

+
diff --git a/apps/front/src/app/pages/profile/profile.component.ts b/apps/front/src/app/pages/profile/profile.component.ts new file mode 100644 index 0000000..f83750f --- /dev/null +++ b/apps/front/src/app/pages/profile/profile.component.ts @@ -0,0 +1,25 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { AuthService } from '@front/app/libs/auth/services/auth.service'; +import { TranslocoModule } from '@jsverse/transloco'; + +/** + * Example PROTECTED route. It is guarded by `canActivateFn` in app.routes.ts, + * so an unauthenticated visit is redirected to /login. The decoded token data + * shown here is presentation-only — real authorization is always enforced by + * the backend, never by this guard. + */ +@Component({ + selector: 'app-profile', + standalone: true, + imports: [TranslocoModule, RouterLink], + templateUrl: './profile.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export default class ProfileComponent { + readonly auth = inject(AuthService); + + get permissions(): string[] { + return this.auth.tokenDecoded.permissions ?? []; + } +} diff --git a/apps/front/src/app/pages/unauthorized/unauthorized.component.html b/apps/front/src/app/pages/unauthorized/unauthorized.component.html new file mode 100644 index 0000000..b3e7ae9 --- /dev/null +++ b/apps/front/src/app/pages/unauthorized/unauthorized.component.html @@ -0,0 +1,25 @@ +
+
+
+
+
+ +
+

+ {{ 'unauthorized.title' | transloco }} +

+

+ {{ 'unauthorized.description' | transloco }} +

+ + {{ 'unauthorized.back' | transloco }} + +
+
+
+
diff --git a/apps/front/src/app/pages/unauthorized/unauthorized.component.ts b/apps/front/src/app/pages/unauthorized/unauthorized.component.ts new file mode 100644 index 0000000..69100a2 --- /dev/null +++ b/apps/front/src/app/pages/unauthorized/unauthorized.component.ts @@ -0,0 +1,12 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { TranslocoModule } from '@jsverse/transloco'; + +@Component({ + selector: 'app-unauthorized', + standalone: true, + imports: [TranslocoModule, RouterLink], + templateUrl: './unauthorized.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export default class UnauthorizedComponent {} diff --git a/apps/front/src/assets/i18n/ca.json b/apps/front/src/assets/i18n/ca.json index c2f0752..5d5e1e3 100644 --- a/apps/front/src/assets/i18n/ca.json +++ b/apps/front/src/assets/i18n/ca.json @@ -24,6 +24,16 @@ "invalid": "Usuari o contrasenya incorrectes" } }, + "unauthorized": { + "title": "Accés denegat", + "description": "No tens permisos per veure aquesta pàgina.", + "back": "Tornar a l'inici" + }, + "profile": { + "title": "El teu perfil", + "email": "Correu electrònic", + "permissions": "Permisos" + }, "common": { "loading": "Carregant...", "save": "Guardar", diff --git a/apps/front/src/assets/i18n/en.json b/apps/front/src/assets/i18n/en.json index c67a42b..bca4f1a 100644 --- a/apps/front/src/assets/i18n/en.json +++ b/apps/front/src/assets/i18n/en.json @@ -24,6 +24,16 @@ "invalid": "Invalid username or password" } }, + "unauthorized": { + "title": "Access denied", + "description": "You don't have permission to view this page.", + "back": "Back to home" + }, + "profile": { + "title": "Your profile", + "email": "Email", + "permissions": "Permissions" + }, "common": { "loading": "Loading...", "save": "Save", diff --git a/apps/front/src/assets/i18n/es.json b/apps/front/src/assets/i18n/es.json index a4ee958..df976a0 100644 --- a/apps/front/src/assets/i18n/es.json +++ b/apps/front/src/assets/i18n/es.json @@ -24,6 +24,16 @@ "invalid": "Usuario o contraseña incorrectos" } }, + "unauthorized": { + "title": "Acceso denegado", + "description": "No tienes permisos para ver esta página.", + "back": "Volver al inicio" + }, + "profile": { + "title": "Tu perfil", + "email": "Correo electrónico", + "permissions": "Permisos" + }, "common": { "loading": "Cargando...", "save": "Guardar", From 0caab96e90ac02a21bc2d5f3fe5b280e6b1506b9 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:23:25 +0200 Subject: [PATCH 11/24] fix(security): validate post-login redirect against open redirect [MEDIO] T-11: login.component took redirectUrl from navigation state and passed it to navigateByUrl without validation, allowing redirects to arbitrary internal routes and mishandled '//evil.com'-style values. - Add sanitizeRedirect() util: accepts only same-origin relative paths (single leading '/', no '//', no backslashes, no scheme); returns '' otherwise. - Use it when reading the redirect from navigation state. - Unit tests covering the open-redirect payloads. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/app/libs/auth/safe-redirect.spec.ts | 31 +++++++++++++++++++ apps/front/src/app/libs/auth/safe-redirect.ts | 13 ++++++++ .../src/app/pages/login/login.component.ts | 4 ++- 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 apps/front/src/app/libs/auth/safe-redirect.spec.ts create mode 100644 apps/front/src/app/libs/auth/safe-redirect.ts diff --git a/apps/front/src/app/libs/auth/safe-redirect.spec.ts b/apps/front/src/app/libs/auth/safe-redirect.spec.ts new file mode 100644 index 0000000..b41ab20 --- /dev/null +++ b/apps/front/src/app/libs/auth/safe-redirect.spec.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import { sanitizeRedirect } from './safe-redirect'; + +describe('sanitizeRedirect', () => { + it('accepts same-origin relative paths', () => { + expect(sanitizeRedirect('/profile')).toBe('/profile'); + expect(sanitizeRedirect('/users/1?tab=x#frag')).toBe('/users/1?tab=x#frag'); + expect(sanitizeRedirect('/')).toBe('/'); + }); + + it('rejects open-redirect payloads', () => { + for (const bad of [ + 'https://evil.com', + 'http://evil.com', + '//evil.com', + '/\\evil.com', + '/\\/evil.com', + 'javascript:alert(1)', + 'evil.com', + '\\\\evil.com', + ]) { + expect(sanitizeRedirect(bad)).toBe(''); + } + }); + + it('treats empty/nullish as no redirect', () => { + expect(sanitizeRedirect('')).toBe(''); + expect(sanitizeRedirect(null)).toBe(''); + expect(sanitizeRedirect(undefined)).toBe(''); + }); +}); diff --git a/apps/front/src/app/libs/auth/safe-redirect.ts b/apps/front/src/app/libs/auth/safe-redirect.ts new file mode 100644 index 0000000..849af75 --- /dev/null +++ b/apps/front/src/app/libs/auth/safe-redirect.ts @@ -0,0 +1,13 @@ +/** + * Returns `url` only if it is a safe same-origin relative path, otherwise ''. + * + * Blocks open-redirect payloads: absolute URLs (`https://evil.com`), scheme + * tricks (`javascript:...`), protocol-relative (`//evil.com`) and backslash + * variants (`/\evil.com`, `/\/evil.com`). Only a single leading '/' followed by + * non-backslash characters is accepted (paths, query and fragment included). + */ +export const sanitizeRedirect = (url: string | null | undefined): string => { + if (!url) return ''; + const isSameOriginPath = /^\/(?!\/)[^\\]*$/.test(url); + return isSameOriginPath ? url : ''; +}; diff --git a/apps/front/src/app/pages/login/login.component.ts b/apps/front/src/app/pages/login/login.component.ts index 9389b0c..932577b 100644 --- a/apps/front/src/app/pages/login/login.component.ts +++ b/apps/front/src/app/pages/login/login.component.ts @@ -6,6 +6,7 @@ import { Router, RouterModule } from '@angular/router'; import { TranslocoModule, TranslocoService } from '@jsverse/transloco'; import { Login } from '@front/app/libs/auth/auth.interface'; +import { sanitizeRedirect } from '@front/app/libs/auth/safe-redirect'; import { AuthService } from '@front/app/libs/auth/services/auth.service'; import { catchError, of, tap } from 'rxjs'; @@ -36,7 +37,8 @@ export default class LoginComponent implements OnInit { ngOnInit() { const navigation = this.router.currentNavigation(); const state = navigation?.extras.state as { currentRoute: string }; - this.redirectUrl = state?.currentRoute ?? ''; + // Only honour safe same-origin paths — never an attacker-controlled URL. + this.redirectUrl = sanitizeRedirect(state?.currentRoute); } doLogin() { From 62e99703f3edbf6932aa925756f2e7ff43bd742c Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:25:00 +0200 Subject: [PATCH 12/24] fix(security): only attach token/credentials to trusted origins [MEDIO] T-12: the auth interceptor added Authorization + withCredentials to EVERY request regardless of target. The moment the app calls a third-party absolute URL (CDN, analytics, avatar), the in-memory access token would leak in the Authorization header. - Attach token/withCredentials only when the request resolves to the app origin or the explicitly-configured API origin (AuthConfig.idpServer). - Don't capture an Authorization response header from an untrusted origin, and don't trigger logout on a third-party 401. - Tests for the cross-origin cases. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../interceptors/auth.interceptor.spec.ts | 23 ++++++++ .../auth/interceptors/auth.interceptor.ts | 59 ++++++++++++++----- 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/apps/front/src/app/libs/auth/interceptors/auth.interceptor.spec.ts b/apps/front/src/app/libs/auth/interceptors/auth.interceptor.spec.ts index 86bf329..dae82a6 100644 --- a/apps/front/src/app/libs/auth/interceptors/auth.interceptor.spec.ts +++ b/apps/front/src/app/libs/auth/interceptors/auth.interceptor.spec.ts @@ -86,6 +86,29 @@ describe('AuthInterceptor', () => { req.flush({}); }); + it('should NOT attach token/withCredentials to a cross-origin URL', () => { + mockAuthService.token.set('Bearer secret-token'); + + httpClient.get('https://evil.example.com/collect').subscribe(); + + const req = httpMock.expectOne('https://evil.example.com/collect'); + expect(req.request.headers.has('Authorization')).toBe(false); + expect(req.request.withCredentials).toBe(false); + + req.flush({}); + }); + + it('should not capture a token from a cross-origin response', () => { + mockAuthService.token.set('Bearer secret-token'); + + httpClient.get('https://evil.example.com/x').subscribe(); + + const req = httpMock.expectOne('https://evil.example.com/x'); + req.flush({}, { headers: { Authorization: 'Bearer attacker-token' } }); + + expect(mockAuthService.setToken).not.toHaveBeenCalled(); + }); + it('should preserve existing headers', () => { mockAuthService.token.set('Bearer test-token'); diff --git a/apps/front/src/app/libs/auth/interceptors/auth.interceptor.ts b/apps/front/src/app/libs/auth/interceptors/auth.interceptor.ts index 4444dff..0621f01 100644 --- a/apps/front/src/app/libs/auth/interceptors/auth.interceptor.ts +++ b/apps/front/src/app/libs/auth/interceptors/auth.interceptor.ts @@ -9,40 +9,69 @@ import { import { Injectable, inject } from '@angular/core'; import { catchError, tap, throwError } from 'rxjs'; +import { AUTH_CONFIGURATION } from '../auth.constants'; import { AuthService } from '../services/auth.service'; @Injectable() export class AuthInterceptor implements HttpInterceptor { #auth = inject(AuthService); + #config = inject(AUTH_CONFIGURATION, { optional: true }); + + /** + * Only same-origin requests (or the explicitly-configured API origin) may + * carry the in-memory access token and cookies. Without this guard, the day + * the app calls a third-party absolute URL (CDN, analytics, avatar service) + * the Authorization header would leak the token to that third party. + */ + #isTrustedOrigin(url: string): boolean { + try { + const appOrigin = window.location.origin; + const targetOrigin = new URL(url, appOrigin).origin; + if (targetOrigin === appOrigin) return true; + const idp = this.#config?.idpServer; + if (idp && /^https?:\/\//i.test(idp)) { + return targetOrigin === new URL(idp).origin; + } + return false; + } catch { + return false; + } + } intercept(req: HttpRequest, next: HttpHandler) { - const authToken = this.#auth.token(); + const attach = this.#isTrustedOrigin(req.url); - // Always include withCredentials to send cookies (RefreshToken) - const authReq = authToken - ? req.clone({ - headers: req.headers.set('Authorization', authToken), - withCredentials: true, - }) - : req.clone({ - withCredentials: true, - }); + let authReq = req; + if (attach) { + const authToken = this.#auth.token(); + authReq = authToken + ? req.clone({ + headers: req.headers.set('Authorization', authToken), + withCredentials: true, + }) + : req.clone({ withCredentials: true }); + } return next.handle(authReq).pipe( tap((event: HttpEvent) => { - if (event instanceof HttpResponse) { - // Capture new AccessToken from response header + // Never read an Authorization response header from an untrusted origin + // as our session token. + if (attach && event instanceof HttpResponse) { const token = event.headers.get('Authorization'); if (token) { this.#auth.setToken(token); } } }), - catchError(this.errorHandle.bind(this)), + catchError((error: HttpErrorResponse) => + this.#errorHandle(error, attach), + ), ); } - private errorHandle(error: HttpErrorResponse) { - if (error.status === 401) { + + #errorHandle(error: HttpErrorResponse, attach: boolean) { + // Only our own 401s should end the session; a third-party 401 must not. + if (attach && error.status === 401) { this.#auth.logout('/login'); } return throwError(() => error); From 9c4c8bd31e8060acbcd91f3efab24af155f6aa45 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:25:54 +0200 Subject: [PATCH 13/24] fix(security): generate internal requestId server-side; strip client trust headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [MEDIO] T-13: the gateway read the client's x-request-id and used it verbatim as the requestId embedded in the signed internal JWT and forwarded to the API, enabling correlation forgery/collision and log poisoning. Inbound internal headers were also not stripped defensively. - Always mint requestId with randomUUID() (never from the client header). - Strip inbound x-internal-auth / x-request-id at the start of the proxy, and removeHeader them on proxyReq before setting the gateway-minted values. - (API already derives requestId from the verified token claims, not the header — defense in depth already in place there.) Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/gateway/src/routes/proxy.routes.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/gateway/src/routes/proxy.routes.ts b/apps/gateway/src/routes/proxy.routes.ts index 1cc85e6..826454a 100644 --- a/apps/gateway/src/routes/proxy.routes.ts +++ b/apps/gateway/src/routes/proxy.routes.ts @@ -35,8 +35,12 @@ export const buildProxyRouter = (): Router => { if (!user) { return res.status(401).json({ error: 'Unauthorized' }); } - const requestId = - (req.header(INTERNAL_REQUEST_ID_HEADER) as string) ?? randomUUID(); + // Never trust client-supplied internal headers: strip them so a forged + // x-internal-auth / x-request-id can never reach the api or poison logs. + delete req.headers[INTERNAL_AUTH_HEADER]; + delete req.headers[INTERNAL_REQUEST_ID_HEADER]; + // The correlation id is always minted server-side. + const requestId = randomUUID(); try { const internalToken = await signUserContext( { @@ -66,6 +70,10 @@ export const buildProxyRouter = (): Router => { internalToken?: string; internalRequestId?: string; }; + // Defense in depth: drop any inbound internal headers that survived, + // then set only the gateway-minted values. + proxyReq.removeHeader(INTERNAL_AUTH_HEADER); + proxyReq.removeHeader(INTERNAL_REQUEST_ID_HEADER); if (carrier.internalToken) { proxyReq.setHeader(INTERNAL_AUTH_HEADER, carrier.internalToken); } From 91e5212f98efa581a5cf839a40369d6f70846430 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:27:50 +0200 Subject: [PATCH 14/24] feat(security): add security headers (helmet + nginx CSP) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [MEDIO] T-14: neither the gateway nor the API used helmet, and the front's nginx set no CSP/X-Frame-Options/nosniff. Since the access token is JS-visible by design, a strong CSP is a key XSS mitigation. - Gateway & API: helmet() (HSTS, nosniff, X-Frame-Options: DENY, …); CSP off there since they return JSON only — the document CSP belongs to the front. - Front nginx (nginx/default.conf): CSP (script-src 'self', object-src 'none', frame-ancestors 'none', style-src 'unsafe-inline' for Angular styles), X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy, HSTS; re-declared in the /assets and /index.html locations (nginx add_header inheritance caveat). Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/src/main.ts | 4 ++++ apps/gateway/src/main.ts | 6 ++++++ nginx/default.conf | 26 ++++++++++++++++++++++++-- package-lock.json | 13 +++++++++++++ package.json | 1 + 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 22f16f0..458d69e 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,5 +1,6 @@ import 'dotenv/config'; import express from 'express'; +import helmet from 'helmet'; import { getConnectionStats, testConnection } from './adapters/db/pg.connector'; import { UPLOAD_DIR } from './globals'; @@ -58,6 +59,9 @@ class Main { * in the gateway and is intentionally omitted here. */ this.#app.set('trust proxy', 1); + // Defense-in-depth security headers even though the API is private (behind + // the gateway). Returns JSON only, so the document CSP is left to the front. + this.#app.use(helmet({ contentSecurityPolicy: false })); this.#app.use(express.json()); this.#app.use(dbLoggingMiddleware); this.#app.use(dbErrorMiddleware); diff --git a/apps/gateway/src/main.ts b/apps/gateway/src/main.ts index 19ec0d5..00adbdf 100644 --- a/apps/gateway/src/main.ts +++ b/apps/gateway/src/main.ts @@ -2,6 +2,7 @@ import cookieParser from 'cookie-parser'; import cors from 'cors'; import 'dotenv/config'; import express from 'express'; +import helmet from 'helmet'; import api from './routes'; @@ -31,6 +32,11 @@ class Main { #config() { this.#app.set('trust proxy', 1); + // Security headers (HSTS, X-Content-Type-Options, X-Frame-Options: DENY, + // etc.). The document-level CSP is owned by the front's nginx; the gateway + // only returns JSON, so a document CSP here would be noise. + this.#app.use(helmet({ contentSecurityPolicy: false })); + const corsOptions: cors.CorsOptions = { origin: parseOrigins(process.env.CORS_ORIGIN) ?? true, credentials: true, diff --git a/nginx/default.conf b/nginx/default.conf index 48e8a19..a81f3ba 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -6,6 +6,19 @@ server { #charset koi8-r; #access_log /var/log/nginx/host.access.log main; + # ── Security headers ───────────────────────────────────────────────────── + # Applied at the server level. NOTE: nginx does NOT inherit add_header into + # a location that defines its own add_header, so any such location below + # re-declares these. The access token is JS-visible by design, so a strong + # CSP is the key mitigation against XSS-driven exfiltration. + # 'unsafe-inline' is required for Angular component styles (style-src only). + add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always; + add_header X-Frame-Options "DENY" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + # HSTS — cookies are Secure in prod and Nginx is the only public door. + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; + # Proxy to the gateway (same-origin for cookies). The gateway authenticates # the request and forwards it to internal microservices. location /api/ { @@ -35,6 +48,11 @@ server { # forces the browser to ask if /assets/files have changed location /assets { add_header Cache-Control "no-cache"; + # Re-declare security headers (this location overrides server-level ones). + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "DENY" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; root /usr/share/nginx/html; } @@ -48,8 +66,12 @@ server { location /index.html { # force donwload index.html add_header Cache-Control "no-store, no-cache"; - # prevent be embebed - add_header Content-Security-Policy "frame-ancestors 'self'"; + # Full security header set (this location overrides server-level ones). + add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always; + add_header X-Frame-Options "DENY" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; root /usr/share/nginx/html; } diff --git a/package-lock.json b/package-lock.json index d34d86e..0bbadab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "dotenv": "^17.4.2", "express": "^5.2.1", "express-rate-limit": "^7.5.1", + "helmet": "^8.2.0", "http-proxy-middleware": "^3.0.5", "jose": "^5.10.0", "jsonwebtoken": "^9.0.3", @@ -19186,6 +19187,18 @@ "he": "bin/he" } }, + "node_modules/helmet": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.2.0.tgz", + "integrity": "sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, "node_modules/homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", diff --git a/package.json b/package.json index b48fe2c..bc7edf8 100644 --- a/package.json +++ b/package.json @@ -129,6 +129,7 @@ "dotenv": "^17.4.2", "express": "^5.2.1", "express-rate-limit": "^7.5.1", + "helmet": "^8.2.0", "http-proxy-middleware": "^3.0.5", "jose": "^5.10.0", "jsonwebtoken": "^9.0.3", From acc9b655f87edaa1914ad1537e09cd3d687bd49c Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:29:03 +0200 Subject: [PATCH 15/24] fix(security): lock down API health detail endpoints [MEDIO] T-15: /v1/health/detailed and /v1/health/db exposed Node version, platform, pid, memory/cpu and DB host/port/name with no auth, aiding CVE targeting and lateral movement if the network were misconfigured. - Keep /v1/health (liveness) public but minimal (health + timestamp). - Gate /db and /detailed behind requireInternalAuth (gateway-signed token). - Strip fingerprinting: drop nodeVersion/pid/platform and DB host/port/name/ dialect; expose only connection booleans/counters + operational metrics. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/src/routes/health.routes.ts | 62 ++++++++++++++-------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/apps/api/src/routes/health.routes.ts b/apps/api/src/routes/health.routes.ts index 2f701bb..1da484d 100644 --- a/apps/api/src/routes/health.routes.ts +++ b/apps/api/src/routes/health.routes.ts @@ -1,55 +1,55 @@ +import { requireInternalAuth } from '@internal-auth'; import { Router } from 'express'; import { getConnectionStats, isConnected } from '../adapters/db/pg.connector'; const healthRouter = Router(); +// Detailed/db probes are gated behind a valid internal (gateway-signed) token. +// Any scope is accepted — these are operational endpoints, not business ones. +const requireInternal = requireInternalAuth({ + publicKey: process.env.INTERNAL_JWT_PUBLIC_KEY ?? '', +}); + +// Only the non-sensitive connection booleans/counters — never the DB host, +// port, name or dialect (those help an attacker target/fingerprint the DB). +const safeDbStats = () => { + const { + isConnected: connected, + poolSize, + available, + using, + waiting, + } = getConnectionStats(); + return { isConnected: connected, poolSize, available, using, waiting }; +}; + +/** Public liveness probe — intentionally minimal, leaks nothing. */ healthRouter.get('', (_, res) => { - res.status(200).json({ - health: '👌', - timestamp: new Date().toISOString(), - uptime: process.uptime(), - }); + res.status(200).json({ health: '👌', timestamp: new Date().toISOString() }); }); -healthRouter.get('/db', (_, res) => { - const dbStats = getConnectionStats(); +healthRouter.get('/db', requireInternal, (_, res) => { const isDbConnected = isConnected(); - - const response = { + res.status(isDbConnected ? 200 : 503).json({ health: isDbConnected ? '👌' : '⚠️', - database: { - connected: isDbConnected, - stats: dbStats, - }, + database: { connected: isDbConnected, stats: safeDbStats() }, timestamp: new Date().toISOString(), - uptime: process.uptime(), - }; - - res.status(isDbConnected ? 200 : 503).json(response); + }); }); -healthRouter.get('/detailed', (_, res) => { - const dbStats = getConnectionStats(); +healthRouter.get('/detailed', requireInternal, (_, res) => { const isDbConnected = isConnected(); - - const response = { + res.status(isDbConnected ? 200 : 503).json({ health: isDbConnected ? '👌' : '⚠️', + // Operational metrics only — no nodeVersion / pid / platform fingerprinting. system: { uptime: process.uptime(), memory: process.memoryUsage(), cpu: process.cpuUsage(), - platform: process.platform, - nodeVersion: process.version, - pid: process.pid, - }, - database: { - connected: isDbConnected, - stats: dbStats, }, + database: { connected: isDbConnected, stats: safeDbStats() }, timestamp: new Date().toISOString(), - }; - - res.status(isDbConnected ? 200 : 503).json(response); + }); }); export default healthRouter; From 933c2991629cfa9aebeba1526cae8903f396f063 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:29:47 +0200 Subject: [PATCH 16/24] fix(security): pass bcrypt cost as number; enforce minimum 12 [BAJO] T-16: hashPassword passed process.env.HASH_SALT_ROUNDS (a string) to bcrypt.hash, which treats a string as a pre-generated salt; cost was also only 10 with no floor. - parseInt the env and enforce a minimum cost of 12 (modern baseline). - Default HASH_SALT_ROUNDS to 12 in .env.example and compose.yaml. - Tests for numeric coercion and the minimum-cost clamp. Co-Authored-By: Claude Opus 4.8 (1M context) --- .env.example | 3 ++- apps/api/src/services/auth.service.spec.ts | 18 +++++++++++++++--- apps/api/src/services/auth.service.ts | 9 ++++++++- compose.yaml | 2 +- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index f3e326d..4fa7d49 100644 --- a/.env.example +++ b/.env.example @@ -79,4 +79,5 @@ DB_POOL_EVICT=1000 DB_CONNECT_TIMEOUT=60000 DB_ACQUIRE_TIMEOUT=60000 DB_TIMEOUT=60000 -HASH_SALT_ROUNDS=10 +# bcrypt cost factor. Minimum enforced in code is 12; raise as hardware allows. +HASH_SALT_ROUNDS=12 diff --git a/apps/api/src/services/auth.service.spec.ts b/apps/api/src/services/auth.service.spec.ts index e09b560..e78f905 100644 --- a/apps/api/src/services/auth.service.spec.ts +++ b/apps/api/src/services/auth.service.spec.ts @@ -18,7 +18,7 @@ vi.mock('bcrypt', () => ({ describe('AuthService (internal)', () => { beforeEach(() => { vi.clearAllMocks(); - process.env.HASH_SALT_ROUNDS = '10'; + process.env.HASH_SALT_ROUNDS = '14'; }); describe('validateCredentials', () => { @@ -74,13 +74,25 @@ describe('AuthService (internal)', () => { }); describe('hashPassword', () => { - it('delegates to bcrypt.hash with HASH_SALT_ROUNDS env', async () => { + it('delegates to bcrypt.hash with HASH_SALT_ROUNDS as a number', async () => { vi.mocked(bcrypt.hash).mockResolvedValue('hashed' as never); const result = await authService.hashPassword('pwd'); - expect(bcrypt.hash).toHaveBeenCalledWith('pwd', '10'); + expect(bcrypt.hash).toHaveBeenCalledWith('pwd', 14); expect(result).toBe('hashed'); }); + + it('enforces a minimum cost of 12 for low/invalid values', async () => { + vi.mocked(bcrypt.hash).mockResolvedValue('hashed' as never); + + process.env.HASH_SALT_ROUNDS = '10'; + await authService.hashPassword('pwd'); + expect(bcrypt.hash).toHaveBeenLastCalledWith('pwd', 12); + + process.env.HASH_SALT_ROUNDS = 'not-a-number'; + await authService.hashPassword('pwd'); + expect(bcrypt.hash).toHaveBeenLastCalledWith('pwd', 12); + }); }); }); diff --git a/apps/api/src/services/auth.service.ts b/apps/api/src/services/auth.service.ts index 930339b..d965674 100644 --- a/apps/api/src/services/auth.service.ts +++ b/apps/api/src/services/auth.service.ts @@ -25,7 +25,14 @@ class AuthService { await User.findOne({ where: { id, deleted: false } }); hashPassword = async (password: string) => { - return await hash(password, process.env.HASH_SALT_ROUNDS ?? 10); + // bcrypt expects a NUMBER of rounds; a string is treated as a pre-generated + // salt and silently changes behaviour. Parse it and enforce a sane modern + // minimum cost (12). + const MIN_ROUNDS = 12; + const parsed = parseInt(process.env.HASH_SALT_ROUNDS ?? '', 10); + const rounds = + Number.isFinite(parsed) && parsed >= MIN_ROUNDS ? parsed : MIN_ROUNDS; + return await hash(password, rounds); }; #comparePassword = async (password: string, hash: string) => { diff --git a/compose.yaml b/compose.yaml index 7af20f0..6b16ed5 100644 --- a/compose.yaml +++ b/compose.yaml @@ -37,7 +37,7 @@ services: - POSTGRESDB_PORT=${POSTGRESDB_PORT:-5432} - POSTGRESDB_HOST=postgresdb - INTERNAL_JWT_PUBLIC_KEY=${INTERNAL_JWT_PUBLIC_KEY} - - HASH_SALT_ROUNDS=${HASH_SALT_ROUNDS:-10} + - HASH_SALT_ROUNDS=${HASH_SALT_ROUNDS:-12} - NODE_UPLOAD_FILES=${NODE_UPLOAD_FILES:-uploads} networks: - internal-network From 56129e5a85a65f676d0a1f3f7e31e0d248d0a6ff Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:31:11 +0200 Subject: [PATCH 17/24] fix(security): filter deleted on put read-back + clamp pagination [BAJO] T-17: after a put the base controller re-read with getById({id}) without deleted:false, allowing read/edit of soft-deleted rows; and page/limit came straight from req.query (NaN/negative offset, unbounded limit -> memory/DoS). - put: pre-check the row with deleted:false (404 if gone), and read back with deleted:false so soft-deleted records stay invisible. - getAllPaged: parse+clamp via paginationQuerySchema (page>=1, 1<=limit<=100), falling back to defaults on invalid input; reads res.locals.query when a validate('query') middleware ran. - Add abstract-crud.controller spec. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../abstract-crud.controller.spec.ts | 90 +++++++++++++++++++ .../controllers/abstract-crud.controller.ts | 25 +++++- 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 apps/api/src/controllers/abstract-crud.controller.spec.ts diff --git a/apps/api/src/controllers/abstract-crud.controller.spec.ts b/apps/api/src/controllers/abstract-crud.controller.spec.ts new file mode 100644 index 0000000..debb6dc --- /dev/null +++ b/apps/api/src/controllers/abstract-crud.controller.spec.ts @@ -0,0 +1,90 @@ +import { describe, expect, it, vi } from 'vitest'; +import { AbstractCrudController } from './abstract-crud.controller'; + +class TestController extends AbstractCrudController { + constructor(service: never) { + super(service); + } +} + +const makeRes = () => { + const res: Record = { locals: {} }; + res.status = vi.fn().mockReturnValue(res); + res.json = vi.fn().mockReturnValue(res); + res.send = vi.fn().mockReturnValue(res); + return res as never; +}; + +describe('AbstractCrudController', () => { + describe('getAllPaged', () => { + it('clamps invalid pagination to sane defaults', async () => { + const service = { + getAllPaged: vi.fn().mockResolvedValue({ rows: [], count: 0 }), + }; + const ctrl = new TestController(service as never); + + await ctrl.getAllPaged( + { query: { page: 'abc', limit: '99999' } } as never, + makeRes(), + ); + + // Bad page -> 1; limit clamped to <=100 falls back to default 10 here. + expect(service.getAllPaged).toHaveBeenCalledWith(1, 10); + }); + + it('honours valid pagination', async () => { + const service = { + getAllPaged: vi.fn().mockResolvedValue({ rows: [], count: 0 }), + }; + const ctrl = new TestController(service as never); + + await ctrl.getAllPaged( + { query: { page: '2', limit: '25' } } as never, + makeRes(), + ); + + expect(service.getAllPaged).toHaveBeenCalledWith(2, 25); + }); + }); + + describe('put', () => { + it('404s and does not update a soft-deleted/absent row', async () => { + const service = { + getById: vi.fn().mockResolvedValue(null), + put: vi.fn(), + }; + const ctrl = new TestController(service as never); + const res = makeRes(); + + await ctrl.put({ params: { id: '7' }, body: {} } as never, res); + + expect(service.getById).toHaveBeenCalledWith({ id: '7', deleted: false }); + expect(service.put).not.toHaveBeenCalled(); + expect( + (res as { status: ReturnType }).status, + ).toHaveBeenCalledWith(404); + }); + + it('updates and reads back an existing row with deleted:false', async () => { + const service = { + getById: vi + .fn() + .mockResolvedValueOnce({ id: 7 }) + .mockResolvedValueOnce({ id: 7, name: 'new' }), + put: vi.fn().mockResolvedValue([1]), + }; + const ctrl = new TestController(service as never); + + await ctrl.put( + { params: { id: '7' }, body: { name: 'new' } } as never, + makeRes(), + ); + + expect(service.put).toHaveBeenCalledWith('7', { name: 'new' }); + expect(service.getById).toHaveBeenLastCalledWith({ + id: '7', + deleted: false, + }); + }); + }); +}); diff --git a/apps/api/src/controllers/abstract-crud.controller.ts b/apps/api/src/controllers/abstract-crud.controller.ts index e5c9884..b1ccecf 100644 --- a/apps/api/src/controllers/abstract-crud.controller.ts +++ b/apps/api/src/controllers/abstract-crud.controller.ts @@ -1,5 +1,6 @@ import HttpResponser from '@api/adapters/http/http.responser'; import { AbstractCrudService } from '@api/services/abstract-crud.service'; +import { paginationQuerySchema } from '@dto'; export abstract class AbstractCrudController { protected service: AbstractCrudService; @@ -10,7 +11,16 @@ export abstract class AbstractCrudController { getAllPaged = async (req, res) => { try { - const { page, limit } = req.query; + // Parse + clamp pagination (page>=1, 1<=limit<=100) so NaN/negative or + // huge limits can't skew the offset or pull the whole table (DoS). Bad + // input falls back to sane defaults rather than erroring. Uses the value a + // validate('query') middleware may have placed on res.locals. + const parsed = paginationQuerySchema.safeParse( + res.locals.query ?? req.query, + ); + const { page, limit } = parsed.success + ? parsed.data + : { page: 1, limit: 10 }; const data = await this.service.getAllPaged(page, limit); return HttpResponser.successJson(res, data); } catch (error) { @@ -50,8 +60,19 @@ export abstract class AbstractCrudController { put = async (req, res) => { try { + // Never read or mutate a soft-deleted row: it must behave as if gone. + const existing = await this.service.getById({ + id: req.params.id, + deleted: false, + }); + if (!existing) { + return HttpResponser.errorJson(res, { message: 'Not found' }, 404); + } await this.service.put(req.params.id, req.body); - const updated = await this.service.getById({ id: req.params.id }); + const updated = await this.service.getById({ + id: req.params.id, + deleted: false, + }); return HttpResponser.successJson(res, updated); } catch (error) { console.log(error); From 952e35622e6706540ff3f90dcf97ef217280aee4 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:32:49 +0200 Subject: [PATCH 18/24] =?UTF-8?q?fix(security):=20logging=20hygiene=20?= =?UTF-8?q?=E2=80=94=20no=20bodies/PII/full=20errors=20in=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [BAJO] T-18: - dbLoggingMiddleware logged the response body + client IP + User-Agent on any 5xx (PII / internals leakage) -> log only method/path/status/timestamp. - sequelizeErrorMiddleware logged the full error object (query fragments/values) -> log only { name, code }. - Remove orphan console.log(error) in AbstractCrudController.put. - pg.connector gated SQL logging on NODE_PRODUCTION only; now fail-safe on NODE_ENV==='production' || NODE_PRODUCTION==='true' so SQL+params are never logged in prod if one var is unset. - Update db-error middleware specs. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/src/adapters/db/pg.connector.ts | 8 +++++++- .../src/controllers/abstract-crud.controller.ts | 1 - .../src/middleware/db-error.middleware.spec.ts | 14 +++++++------- apps/api/src/middleware/db-error.middleware.ts | 15 +++++++++------ 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/apps/api/src/adapters/db/pg.connector.ts b/apps/api/src/adapters/db/pg.connector.ts index 23dbc43..556ec22 100644 --- a/apps/api/src/adapters/db/pg.connector.ts +++ b/apps/api/src/adapters/db/pg.connector.ts @@ -19,7 +19,13 @@ const dbConfig = { database: process.env.POSTGRESDB_DATABASE ?? 'your_db_name', username: process.env.POSTGRESDB_USER ?? 'postgres', password: process.env.POSTGRESDB_PASSWORD ?? 'password', - logging: process.env.NODE_PRODUCTION === 'true' ? false : console.log, + // Fail-safe: never log SQL (which includes parameter values like emails) when + // either standard NODE_ENV or the legacy NODE_PRODUCTION flag indicates prod. + logging: + process.env.NODE_ENV === 'production' || + process.env.NODE_PRODUCTION === 'true' + ? false + : console.log, pool: { max: parseInt(process.env.DB_POOL_MAX || '10'), min: parseInt(process.env.DB_POOL_MIN || '0'), diff --git a/apps/api/src/controllers/abstract-crud.controller.ts b/apps/api/src/controllers/abstract-crud.controller.ts index b1ccecf..b933b84 100644 --- a/apps/api/src/controllers/abstract-crud.controller.ts +++ b/apps/api/src/controllers/abstract-crud.controller.ts @@ -75,7 +75,6 @@ export abstract class AbstractCrudController { }); return HttpResponser.successJson(res, updated); } catch (error) { - console.log(error); return HttpResponser.errorJson(res, error); } }; diff --git a/apps/api/src/middleware/db-error.middleware.spec.ts b/apps/api/src/middleware/db-error.middleware.spec.ts index 39c15eb..a75ec2a 100644 --- a/apps/api/src/middleware/db-error.middleware.spec.ts +++ b/apps/api/src/middleware/db-error.middleware.spec.ts @@ -112,7 +112,10 @@ describe('Database Error Middleware', () => { ); // Assert - expect(consoleErrorSpy).toHaveBeenCalledWith('Database error:', error); + expect(consoleErrorSpy).toHaveBeenCalledWith('Database error:', { + name: 'SequelizeConnectionError', + code: undefined, + }); expect(statusMock).toHaveBeenCalledWith(503); expect(jsonMock).toHaveBeenCalledWith({ error: 'Service Unavailable', @@ -349,15 +352,12 @@ describe('Database Error Middleware', () => { // Simulate sending response mockResponse.send(testData); - // Assert + // Assert — metadata only: no body, no IP, no User-Agent. expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Database error on GET /api/test:', + 'Server error on GET /api/test:', { statusCode: 503, - body: testData, timestamp: '2025-10-27T12:00:00.000Z', - userAgent: 'Test User Agent', - ip: '127.0.0.1', }, ); }); @@ -421,7 +421,7 @@ describe('Database Error Middleware', () => { mockResponse.send({ error: 'Error' }); expect(consoleErrorSpy).toHaveBeenCalledWith( - expect.stringContaining('Database error'), + expect.stringContaining('Server error'), expect.objectContaining({ statusCode }), ); }); diff --git a/apps/api/src/middleware/db-error.middleware.ts b/apps/api/src/middleware/db-error.middleware.ts index 39551d5..b27b48e 100644 --- a/apps/api/src/middleware/db-error.middleware.ts +++ b/apps/api/src/middleware/db-error.middleware.ts @@ -36,7 +36,12 @@ export const sequelizeErrorMiddleware = ( ) => { // Verificar si es un error de Sequelize if (error.name && error.name.startsWith('Sequelize')) { - console.error('Database error:', error); + // Log only error metadata — never the full error object, which can carry + // query fragments and parameter values (PII / sensitive data). + console.error('Database error:', { + name: error.name, + code: error.original?.code ?? error.parent?.code, + }); // Mapear errores comunes de Sequelize switch (error.name) { @@ -117,14 +122,12 @@ export const dbLoggingMiddleware = ( const originalSend = res.send; res.send = function (data) { - // Log errores de base de datos + // Log only request/response metadata on 5xx — never the response body, + // client IP or User-Agent (PII and potential leakage of error internals). if (res.statusCode >= 500 && res.statusCode < 600) { - console.error(`Database error on ${req.method} ${req.path}:`, { + console.error(`Server error on ${req.method} ${req.path}:`, { statusCode: res.statusCode, - body: data, timestamp: new Date().toISOString(), - userAgent: req.get('User-Agent'), - ip: req.ip, }); } From 809439cc1446bc83ddca92d039d8860710bb8a6c Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:34:30 +0200 Subject: [PATCH 19/24] fix(security): pin nginx image + add clockTolerance to internal verify [BAJO] T-19: - apps/front/Dockerfile used FROM nginx (=> nginx:latest): non-reproducible builds. Pin to nginx:1.27 (Debian, keeps the groupadd/useradd block working; -alpine would break it). - verifyInternalAuth had no clockTolerance while the internal token TTL is 60s, so clock drift between containers could cause intermittent 401s on legit gateway->API calls. Add clockTolerance: '5s'. - Tests: expired-beyond-tolerance still rejected; small drift now tolerated. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/front/Dockerfile | 4 +++- .../src/lib/internal-auth.signer.spec.ts | 17 +++++++++++++++-- .../src/lib/internal-auth.signer.ts | 3 +++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/front/Dockerfile b/apps/front/Dockerfile index 09c7e5b..1119bae 100644 --- a/apps/front/Dockerfile +++ b/apps/front/Dockerfile @@ -28,7 +28,9 @@ ENV NODE_OPTIONS="--max-old-space-size=3072" RUN npm run build:front --verbose # Etapa 2: Imagen final (Nginx para servir la aplicación) -FROM nginx +# Pin the image for reproducible builds (avoid the moving nginx:latest tag). +# Debian-based variant (not -alpine) so the groupadd/useradd block below works. +FROM nginx:1.27 # Definir el directorio donde se servirán los archivos ENV APP_DIR /usr/share/nginx/html/ diff --git a/libs/internal-auth/src/lib/internal-auth.signer.spec.ts b/libs/internal-auth/src/lib/internal-auth.signer.spec.ts index 5a9f491..1981aba 100644 --- a/libs/internal-auth/src/lib/internal-auth.signer.spec.ts +++ b/libs/internal-auth/src/lib/internal-auth.signer.spec.ts @@ -64,13 +64,26 @@ describe('signUserContext / verifyInternalAuth', () => { ).rejects.toThrow(); }); - it('rejects expired tokens', async () => { + it('rejects expired tokens (beyond the 5s clock tolerance)', async () => { const token = await signUserContext( { userId: 1, permissions: [] }, { privateKey, ttlSeconds: 1 }, ); - await new Promise((resolve) => setTimeout(resolve, 1100)); + // Must exceed exp + the 5s clockTolerance to be rejected. + await new Promise((resolve) => setTimeout(resolve, 6500)); await expect(verifyInternalAuth(token, { publicKey })).rejects.toThrow(); + }, 10000); + + it('tolerates small clock drift (within 5s of expiry)', async () => { + const token = await signUserContext( + { userId: 1, permissions: [] }, + { privateKey, ttlSeconds: 1 }, + ); + // Expired by ~1.1s — inside the 5s tolerance, so still accepted. + await new Promise((resolve) => setTimeout(resolve, 1100)); + await expect( + verifyInternalAuth(token, { publicKey }), + ).resolves.toMatchObject({ sub: 1 }); }); it('rejects mismatched audience', async () => { diff --git a/libs/internal-auth/src/lib/internal-auth.signer.ts b/libs/internal-auth/src/lib/internal-auth.signer.ts index 9d4967b..c3402d1 100644 --- a/libs/internal-auth/src/lib/internal-auth.signer.ts +++ b/libs/internal-auth/src/lib/internal-auth.signer.ts @@ -113,6 +113,9 @@ export const verifyInternalAuth = async ( issuer: options.issuer ?? INTERNAL_AUTH_ISSUER, audience: options.audience ?? INTERNAL_AUTH_AUDIENCE_API, algorithms: [ALG], + // Small skew tolerance so minor clock drift between containers does not + // cause intermittent 401s on the 60s-TTL internal token. + clockTolerance: '5s', }); const subRaw = payload.sub ?? ''; From bd02ec1306dd56f6f4d83feeaf5783e878f3dbca Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:37:29 +0200 Subject: [PATCH 20/24] fix(security): harden gateway token flows (T-20) [BAJO] T-20, several: - CORS fail-closed: drop the dead '?? true' fallback; empty CORS_ORIGIN allows no cross-origin (logged as misconfig) instead of risking reflect-any. - Logout revokes the whole refresh family: embed familyId in the refresh JWT and revoke by familyId (fallback to jti), so logout ends the session everywhere. - Public tokens now carry issuer/audience and are verified (issuer+audience+ clockTolerance), so a token minted elsewhere can't be replayed. - express.json with an explicit 100kb limit, applied only to /api/v1/auth; proxy traffic is streamed, never parsed here. - Login always returns a generic 401 instead of forwarding the upstream message (prevents account enumeration). - Tests for iss/aud binding; keep typ-enforcement test meaningful. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/controllers/auth.controller.ts | 11 ++++++--- apps/gateway/src/main.ts | 23 ++++++++++++++++-- .../gateway/src/middleware/auth.middleware.ts | 7 +++++- .../src/services/token.service.spec.ts | 24 ++++++++++++++++++- apps/gateway/src/services/token.service.ts | 20 ++++++++++++++++ 5 files changed, 78 insertions(+), 7 deletions(-) diff --git a/apps/gateway/src/controllers/auth.controller.ts b/apps/gateway/src/controllers/auth.controller.ts index 17fe150..042a18a 100644 --- a/apps/gateway/src/controllers/auth.controller.ts +++ b/apps/gateway/src/controllers/auth.controller.ts @@ -40,9 +40,14 @@ class AuthController { { issueRefreshCookie: true, requestId }, ); return HttpResponser.successEmpty(res); - } catch (err) { - const status = (err as { statusCode?: number })?.statusCode ?? 401; - return HttpResponser.errorJson(res, err as Error, status); + } catch { + // Always a generic 401 — never forward the upstream message, which could + // reveal whether an account exists (user enumeration). + return HttpResponser.errorJson( + res, + { message: 'Invalid email or password' }, + 401, + ); } }; diff --git a/apps/gateway/src/main.ts b/apps/gateway/src/main.ts index 00adbdf..3a11763 100644 --- a/apps/gateway/src/main.ts +++ b/apps/gateway/src/main.ts @@ -37,8 +37,17 @@ class Main { // only returns JSON, so a document CSP here would be noise. this.#app.use(helmet({ contentSecurityPolicy: false })); + // Fail closed: with credentials:true, never fall back to reflecting any + // origin (that would enable account takeover). An empty CORS_ORIGIN means + // "no cross-origin allowed", and we log it loudly as a misconfiguration. + const allowedOrigins = parseOrigins(process.env.CORS_ORIGIN); + if (allowedOrigins.length === 0) { + console.error( + '⚠️ CORS_ORIGIN is empty — all cross-origin requests will be rejected. Set it in your env.', + ); + } const corsOptions: cors.CorsOptions = { - origin: parseOrigins(process.env.CORS_ORIGIN) ?? true, + origin: allowedOrigins, credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'], @@ -46,7 +55,17 @@ class Main { }; this.#app.use(cors(corsOptions)); this.#app.use(cookieParser()); - this.#app.use(express.json()); + + // Parse JSON only where a body is actually consumed (the auth endpoints), + // with an explicit small size limit. Proxied API traffic is streamed, never + // buffered/parsed here. + const jsonParser = express.json({ limit: '100kb' }); + this.#app.use((req, res, next) => { + if (req.path.startsWith('/api/v1/auth')) { + return jsonParser(req, res, next); + } + return next(); + }); } #setRoutes() { diff --git a/apps/gateway/src/middleware/auth.middleware.ts b/apps/gateway/src/middleware/auth.middleware.ts index 6a67440..69b734c 100644 --- a/apps/gateway/src/middleware/auth.middleware.ts +++ b/apps/gateway/src/middleware/auth.middleware.ts @@ -60,6 +60,7 @@ const issueRefreshAndRecord = async ( email: options.user.email, permissions: options.user.permissions, remember: options.user.remember, + familyId: options.familyId, jti, }); await ApiClient.recordRefresh( @@ -121,7 +122,11 @@ export const revokeCurrentRefreshFamily = async ( if (!refreshToken) return; try { const decoded = tokenService.verifyRefreshToken(refreshToken); - if (decoded.jti) { + // Revoke the whole family (all rotations of this session), not just the + // single presented jti, so logout actually ends the session everywhere. + if (decoded.familyId) { + await ApiClient.revokeRefresh({ familyId: decoded.familyId }, requestId); + } else if (decoded.jti) { await ApiClient.revokeRefresh({ jti: decoded.jti }, requestId); } } catch { diff --git a/apps/gateway/src/services/token.service.spec.ts b/apps/gateway/src/services/token.service.spec.ts index d602245..68e5e5a 100644 --- a/apps/gateway/src/services/token.service.spec.ts +++ b/apps/gateway/src/services/token.service.spec.ts @@ -83,13 +83,35 @@ describe('TokenService', () => { }); }); + describe('issuer / audience binding', () => { + it('rejects an access token with a wrong audience', async () => { + const jwt = await import('jsonwebtoken'); + const forged = jwt.default.sign( + { id: 1, email: 'a@b.com', permissions: [], typ: 'access' }, + process.env.JWT_ACCESS_SECRET as string, + { + algorithm: 'HS256', + expiresIn: '1h', + issuer: 'gateway', + audience: 'someone-else', + }, + ); + expect(() => tokenService.verifyAccessToken(forged)).toThrow(); + }); + }); + describe('typ enforcement', () => { it('refuses an access token shape signed with the access secret but typ=refresh', async () => { const jwt = await import('jsonwebtoken'); const forged = jwt.default.sign( { id: 1, email: 'a@b.com', permissions: [], typ: 'refresh' }, process.env.JWT_ACCESS_SECRET as string, - { algorithm: 'HS256', expiresIn: '1h' }, + { + algorithm: 'HS256', + expiresIn: '1h', + issuer: 'gateway', + audience: 'web', + }, ); expect(() => tokenService.verifyAccessToken(forged)).toThrow( /Expected access token/, diff --git a/apps/gateway/src/services/token.service.ts b/apps/gateway/src/services/token.service.ts index 75b0cc7..fb5286e 100644 --- a/apps/gateway/src/services/token.service.ts +++ b/apps/gateway/src/services/token.service.ts @@ -6,6 +6,13 @@ export type ClientTokenType = 'access' | 'refresh'; const DEFAULT_REMEMBER_DAYS = 30; +// Bind public tokens to an issuer/audience and verify them, so a token minted +// for a different context cannot be replayed here. Small clock tolerance avoids +// flakiness from minor drift. +const TOKEN_ISSUER = process.env.JWT_ISSUER ?? 'gateway'; +const TOKEN_AUDIENCE = process.env.JWT_AUDIENCE ?? 'web'; +const CLOCK_TOLERANCE_SECONDS = 5; + /** * Lifetime (in days) of a "remember me" refresh token / cookie. Single source * of truth shared by the JWT expiry and the cookie maxAge so they never drift. @@ -31,6 +38,7 @@ export interface RefreshTokenPayload extends JwtPayload { email: string; permissions: Permission[]; remember?: boolean; + familyId?: string; typ: 'refresh'; jti: string; } @@ -47,6 +55,7 @@ export interface RefreshTokenInput { email: string; permissions: Permission[]; remember?: boolean; + familyId?: string; jti?: string; } @@ -78,6 +87,8 @@ class TokenService { }; return jwt.sign(payload, this.#accessSecret(), { algorithm: 'HS256', + issuer: TOKEN_ISSUER, + audience: TOKEN_AUDIENCE, expiresIn: (process.env.JWT_EXPIRES_IN as jwt.SignOptions['expiresIn']) ?? '4h', }); @@ -92,6 +103,7 @@ class TokenService { email: input.email, permissions: input.permissions, remember: input.remember, + familyId: input.familyId, typ: 'refresh', jti: input.jti ?? randomUUID(), }; @@ -105,6 +117,8 @@ class TokenService { ) as jwt.SignOptions['expiresIn']; return jwt.sign(payload, this.#refreshSecret(), { algorithm: 'HS256', + issuer: TOKEN_ISSUER, + audience: TOKEN_AUDIENCE, expiresIn, }); }; @@ -112,6 +126,9 @@ class TokenService { verifyAccessToken = (token: string): AccessTokenPayload => { const decoded = jwt.verify(token, this.#accessSecret(), { algorithms: ['HS256'], + issuer: TOKEN_ISSUER, + audience: TOKEN_AUDIENCE, + clockTolerance: CLOCK_TOLERANCE_SECONDS, }) as AccessTokenPayload; if (decoded.typ !== 'access') { throw new Error(`Expected access token, got ${decoded.typ ?? 'unknown'}`); @@ -122,6 +139,9 @@ class TokenService { verifyRefreshToken = (token: string): RefreshTokenPayload => { const decoded = jwt.verify(token, this.#refreshSecret(), { algorithms: ['HS256'], + issuer: TOKEN_ISSUER, + audience: TOKEN_AUDIENCE, + clockTolerance: CLOCK_TOLERANCE_SECONDS, }) as RefreshTokenPayload; if (decoded.typ !== 'refresh') { throw new Error( From fddae8843ef570570ce07f6f3815655580de1fb1 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:38:34 +0200 Subject: [PATCH 21/24] fix(security): fix tokenDecoded truthiness bug; document CSRF posture [BAJO] T-21: - auth.service.tokenDecoded tested the signal reference (this.token, always truthy) instead of its value, so an empty token led to atob/JSON.parse on undefined and threw. Read this.token(), guard empty, and try/catch -> {}. - Document the CSRF posture in apps/front/AGENTS.md: state-changing requests require the Authorization header (non-cookie proof) + SameSite=strict refresh cookie in prod; no cookie XSRF token, so no withXsrfConfiguration. - Tests for empty and malformed tokens. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/front/AGENTS.md | 8 +++++++- .../src/app/libs/auth/services/auth.service.spec.ts | 9 +++++++++ .../src/app/libs/auth/services/auth.service.ts | 13 ++++++++++--- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/apps/front/AGENTS.md b/apps/front/AGENTS.md index ab4e1fa..03363b9 100644 --- a/apps/front/AGENTS.md +++ b/apps/front/AGENTS.md @@ -28,7 +28,13 @@ - `guards/auth.guard.ts` — gate routes by authentication. - `guards/auth-permission.guard.ts` — gate routes by `Permission` (from `@dto`). -- `interceptors/auth.interceptor.ts` — attach the access token / handle refresh. +- `interceptors/auth.interceptor.ts` — attach the access token / handle refresh + (only to same-origin / the configured API origin — never third parties). +- **CSRF posture**: state-changing calls require the in-memory access token in + the `Authorization` header (non-cookie proof a cross-site page cannot forge), + and the refresh cookie is `httpOnly` + `SameSite=strict` in prod. There is no + cookie-based XSRF token, so do not add `withXsrfConfiguration`. `tokenDecoded` + is presentation-only. - `auth.provider.ts` — wire it all into `app.config.ts`. - Use these instead of inlining auth logic in components. - **Protected routes MUST declare `canActivate`** in `app.routes.ts` diff --git a/apps/front/src/app/libs/auth/services/auth.service.spec.ts b/apps/front/src/app/libs/auth/services/auth.service.spec.ts index 740658e..c806cdd 100644 --- a/apps/front/src/app/libs/auth/services/auth.service.spec.ts +++ b/apps/front/src/app/libs/auth/services/auth.service.spec.ts @@ -69,6 +69,15 @@ describe('AuthService', () => { expect(service.tokenDecoded).toEqual(userData); }); + + it('returns empty claims when there is no token', () => { + expect(service.tokenDecoded).toEqual({}); + }); + + it('returns empty claims (no throw) for a malformed token', () => { + service.setToken('not-a-jwt'); + expect(service.tokenDecoded).toEqual({}); + }); }); describe('isLoggedIn$ observable', () => { diff --git a/apps/front/src/app/libs/auth/services/auth.service.ts b/apps/front/src/app/libs/auth/services/auth.service.ts index 8f55daa..8e1bccc 100644 --- a/apps/front/src/app/libs/auth/services/auth.service.ts +++ b/apps/front/src/app/libs/auth/services/auth.service.ts @@ -15,9 +15,16 @@ export class AuthService { private isInitialized = signal(false); get tokenDecoded(): UserTokenData { - return this.token - ? JSON.parse(window.atob(this.token().split('.')[1])) - : ({} as UserTokenData); + // NOTE: decoded claims are PRESENTATION-ONLY (UX). Authorization is enforced + // by the backend, never by trusting this client-side decode. + const token = this.token(); + if (!token) return {} as UserTokenData; + try { + return JSON.parse(window.atob(token.split('.')[1] ?? '')); + } catch { + // Malformed/non-JWT token — fail safe to empty claims. + return {} as UserTokenData; + } } #apiBase = '/login'; From 39415e55e2f163c3e4c8d4237e45606b3513436e Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Tue, 9 Jun 2026 23:39:28 +0200 Subject: [PATCH 22/24] docs(security): document internal-token replay assumption (T-22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [BAJO] T-22: the internal EdDSA token carries a requestId but the verifier does not enforce jti/nonce uniqueness, so within its 60s TTL a captured X-Internal-Auth could be replayed. This is acceptable for the starter given the network design — document the assumption explicitly in docs/SECURITY.md and describe how to harden (short-lived jti cache) if a less-trusted service ever crosses the internal boundary. Also refresh SECURITY.md for prior fixes: bounded remember lifetime (JWT_REFRESH_REMEMBER_DAYS), iss/aud on client tokens, authoritative permission re-read on rotation, server-minted requestId + header stripping, and logout-by-family. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/SECURITY.md | 46 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index a6f7c97..a8840d0 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -49,10 +49,17 @@ para otros microservicios futuros — sólo el gateway puede hacerlo. Cada login emite dos JWT distintos: -| Token | Secreto | TTL | Reside en | -| ------- | -------------------- | ----------------------------------------------- | ---------------------- | -| Access | `JWT_ACCESS_SECRET` | `JWT_EXPIRES_IN` (4h) | Header `Authorization` | -| Refresh | `JWT_REFRESH_SECRET` | `JWT_REFRESH_EXPIRES_IN` (8h, 365d si remember) | Cookie HttpOnly Secure | +| Token | Secreto | TTL | Reside en | +| ------- | -------------------- | --------------------------------------------------------------------------------------- | ---------------------- | +| Access | `JWT_ACCESS_SECRET` | `JWT_EXPIRES_IN` (4h) | Header `Authorization` | +| Refresh | `JWT_REFRESH_SECRET` | `JWT_REFRESH_EXPIRES_IN` (8h; `JWT_REFRESH_REMEMBER_DAYS`, 30 por defecto, si remember) | Cookie HttpOnly Secure | + +Los tokens del cliente además llevan `iss`/`aud` (`gateway`/`web`) que el +gateway verifica, de modo que un token emitido para otro contexto no se puede +reutilizar aquí. En cada **rotación** el gateway re-lee los permisos del usuario +desde el API (fuente autoritativa) en lugar de copiarlos del refresh viejo, así +que una cuenta degradada/revocada o borrada pierde acceso en la siguiente +rotación, no al cabo de toda la vida del refresh. Cada token lleva: @@ -71,8 +78,34 @@ La tabla `public.refresh_token_family` registra cada refresh JWT emitido: - Si el mismo `jti` se presenta **dos veces** (alguien interceptó la cookie y la usó después de la rotación), el API revoca la **familia completa** y devuelve 401. El gateway limpia la cookie del cliente. -- En `logout`, el gateway revoca la familia activa para invalidar todo - el linaje. +- En `logout`, el gateway revoca la **familia completa** (no sólo el `jti` + presentado): el `familyId` viaja dentro del refresh JWT, de modo que cerrar + sesión termina el linaje entero. + +## Token interno: suposición de red y replay + +El token interno (`X-Internal-Auth`, EdDSA) que el gateway firma para llamar al +API es **de un solo request y de vida muy corta** (TTL 60s, ver +`internal-auth.constants.ts`). Lleva un `requestId` de correlación, pero el +verificador **no** impone unicidad (no hay store de `jti`/nonce). En +consecuencia: + +> **Suposición de seguridad explícita.** Dentro de su ventana de TTL (60s, más +> una tolerancia de reloj de 5s), un `X-Internal-Auth` capturado podría +> **reusarse** contra el API. Esto está mitigado por el diseño de red: el token +> **nunca sale de `internal-network`** (red `internal: true`, sin entrada desde +> Internet) y el gateway es la única puerta pública. Explotarlo requiere estar +> ya **dentro** de la red interna, o un SSRF/leak separado. + +El `requestId` lo **genera siempre el gateway en servidor** (`randomUUID()`); el +API deriva el `requestId` del token verificado, nunca de una cabecera entrante +del cliente. El gateway, además, hace _strip_ de cualquier `x-internal-auth` / +`x-request-id` entrante antes de proxiar. + +**Si el borde interno llegara a ser cruzado por servicios menos confiables** +(p. ej. una malla de servicios multirust), endurecer añadiendo una caché de +`jti` de corta vida en `verifyInternalAuth` para rechazar replays: incluir un +`jti` único en el token interno y registrar los vistos durante su TTL. ## Pasos manuales antes de levantar @@ -106,6 +139,7 @@ ningún archivo a disco. > **Formato — el origen de los fallos de arranque más comunes.** La clave > **debe** ir en una sola línea entre comillas dobles con `\n` literales: +> > - PEM multilínea **sin comillas** → dotenv lo trunca en el primer salto y > jose falla con `Invalid keyData / Failed to read private key`. > - `\n` escapado de más (`\\n`) → `jose` lanza `InvalidCharacterError` en From 42733e552f7b337f132cfea203485135a75d3658 Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Wed, 10 Jun 2026 05:46:23 +0200 Subject: [PATCH 23/24] chore: update package lock --- package-lock.json | 336 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 275 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0bbadab..e7878b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2174,7 +2174,6 @@ "version": "21.2.10", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.10.tgz", "integrity": "sha512-FDcnj3ogRmnTca4m2GbKP2khFOCtoVvWDZyfw2ZCPAf+zsQlKTyscKvx4GpTFo+KHrYXpawUpDIWHORFpuqFEA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/core": "7.29.0", @@ -2261,7 +2260,6 @@ "version": "21.2.10", "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-21.2.10.tgz", "integrity": "sha512-pQNL1LwI/6e8SbwiPxGJkXhV5AZxODhS/UJMEeYTx2fRvm4Ob3aMxWTl3ixhFZeYAx9bcU3QpfEJyGr7YDO24w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/core": "7.29.0", @@ -2413,7 +2411,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2423,7 +2420,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -2454,14 +2450,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2471,7 +2465,6 @@ "version": "7.29.1", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", @@ -2501,7 +2494,6 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", @@ -2518,7 +2510,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2627,7 +2618,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2651,7 +2641,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.7", @@ -2665,7 +2654,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.29.7", @@ -2769,7 +2757,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2788,7 +2775,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2813,7 +2799,6 @@ "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", @@ -2827,7 +2812,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.7" @@ -4413,7 +4397,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -4428,7 +4411,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -4447,7 +4429,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.7", @@ -4464,7 +4445,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -6310,7 +6290,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -6321,7 +6300,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -6332,7 +6310,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -6353,14 +6330,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -11440,6 +11415,277 @@ "win32" ] }, + "node_modules/@rspack/binding": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.11.tgz", + "integrity": "sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "@rspack/binding-darwin-arm64": "1.7.11", + "@rspack/binding-darwin-x64": "1.7.11", + "@rspack/binding-linux-arm64-gnu": "1.7.11", + "@rspack/binding-linux-arm64-musl": "1.7.11", + "@rspack/binding-linux-x64-gnu": "1.7.11", + "@rspack/binding-linux-x64-musl": "1.7.11", + "@rspack/binding-wasm32-wasi": "1.7.11", + "@rspack/binding-win32-arm64-msvc": "1.7.11", + "@rspack/binding-win32-ia32-msvc": "1.7.11", + "@rspack/binding-win32-x64-msvc": "1.7.11" + } + }, + "node_modules/@rspack/binding-darwin-arm64": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.11.tgz", + "integrity": "sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rspack/binding-darwin-x64": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.11.tgz", + "integrity": "sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-arm64-gnu": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.11.tgz", + "integrity": "sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-arm64-musl": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.11.tgz", + "integrity": "sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-x64-gnu": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.11.tgz", + "integrity": "sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-x64-musl": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.11.tgz", + "integrity": "sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-wasm32-wasi": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.11.tgz", + "integrity": "sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@napi-rs/wasm-runtime": "1.0.7" + } + }, + "node_modules/@rspack/binding-win32-arm64-msvc": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.11.tgz", + "integrity": "sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rspack/binding-win32-ia32-msvc": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.11.tgz", + "integrity": "sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rspack/binding-win32-x64-msvc": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.11.tgz", + "integrity": "sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rspack/core": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.11.tgz", + "integrity": "sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@module-federation/runtime-tools": "0.22.0", + "@rspack/binding": "1.7.11", + "@rspack/lite-tapable": "1.1.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.1" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@rspack/core/node_modules/@module-federation/error-codes": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", + "integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@rspack/core/node_modules/@module-federation/runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.22.0.tgz", + "integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/runtime-core": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@rspack/core/node_modules/@module-federation/runtime-core": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.22.0.tgz", + "integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@rspack/core/node_modules/@module-federation/runtime-tools": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.22.0.tgz", + "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/webpack-bundler-runtime": "0.22.0" + } + }, + "node_modules/@rspack/core/node_modules/@module-federation/sdk": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz", + "integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@rspack/core/node_modules/@module-federation/webpack-bundler-runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.22.0.tgz", + "integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, "node_modules/@rspack/dev-server": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@rspack/dev-server/-/dev-server-1.2.1.tgz", @@ -12400,9 +12646,9 @@ "license": "Apache-2.0" }, "node_modules/@swc/helpers": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz", - "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -12532,7 +12778,6 @@ "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", @@ -12546,7 +12791,6 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -12556,7 +12800,6 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -12567,7 +12810,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.2" @@ -14750,7 +14992,6 @@ "version": "2.10.23", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz", "integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==", - "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -14955,7 +15196,6 @@ "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -15212,7 +15452,6 @@ "version": "1.0.30001791", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -15276,7 +15515,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "dev": true, "license": "MIT", "dependencies": { "readdirp": "^5.0.0" @@ -15813,7 +16051,6 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -17221,7 +17458,6 @@ "version": "1.5.344", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", - "dev": true, "license": "ISC" }, "node_modules/emittery": { @@ -18162,7 +18398,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" @@ -18799,7 +19034,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -18818,7 +19052,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -20809,7 +21042,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -20860,7 +21092,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -21676,7 +21907,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -22529,7 +22759,6 @@ "version": "2.0.38", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", - "dev": true, "license": "MIT" }, "node_modules/node-schedule": { @@ -24075,7 +24304,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -24843,7 +25071,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 20.19.0" @@ -24870,7 +25097,6 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "dev": true, "license": "Apache-2.0" }, "node_modules/regenerate": { @@ -27646,7 +27872,6 @@ "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -28140,7 +28365,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -28296,7 +28521,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, "funding": [ { "type": "opencollective", @@ -30022,7 +30246,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, "license": "ISC" }, "node_modules/yaml": { @@ -30045,7 +30268,6 @@ "version": "18.0.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^9.0.1", @@ -30072,7 +30294,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -30085,7 +30306,6 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -30098,7 +30318,6 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^7.2.0", @@ -30113,14 +30332,12 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, "license": "MIT" }, "node_modules/yargs/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==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -30138,7 +30355,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" @@ -30154,7 +30370,6 @@ "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==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", @@ -30172,7 +30387,6 @@ "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==", - "dev": true, "license": "ISC", "engines": { "node": "^20.19.0 || ^22.12.0 || >=23" From e0a0f201567ee7aa7d9ba4b696644412f78f835f Mon Sep 17 00:00:00 2001 From: Dani Herrero Date: Wed, 10 Jun 2026 10:59:18 +0200 Subject: [PATCH 24/24] style: fix prettier formatting in AGENTS.md and README_eng.md Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 30 +++++++++++++++--------------- docs/README_eng.md | 6 +++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 24a55a3..12e97c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,24 +27,24 @@ Each package has its own `AGENTS.md` with rules specific to that layer. Read the relevant one(s) before working in that directory — do **not** rely on this root file for layer detail. -| Package | Guide | What lives there | -| ---------------------- | ------------------------------ | --------------------------------------------------------- | -| `apps/api` | `apps/api/AGENTS.md` | Express routes, Sequelize models, services, `HttpResponser`, JWT | -| `apps/gateway` | `apps/gateway/AGENTS.md` | Reverse proxy, public auth, EdDSA internal-token signing | -| `apps/front` | `apps/front/AGENTS.md` | Angular components, signals, routing, forms, i18n | -| `libs/rest-dto` | `libs/rest-dto/AGENTS.md` | Shared DTOs and API contracts (the single source of truth) | -| `libs/internal-auth` | `libs/internal-auth/AGENTS.md` | EdDSA sign/verify for service-to-service auth | -| `db` | `db/AGENTS.md` | SQL migration files and naming conventions | +| Package | Guide | What lives there | +| -------------------- | ------------------------------ | ---------------------------------------------------------------- | +| `apps/api` | `apps/api/AGENTS.md` | Express routes, Sequelize models, services, `HttpResponser`, JWT | +| `apps/gateway` | `apps/gateway/AGENTS.md` | Reverse proxy, public auth, EdDSA internal-token signing | +| `apps/front` | `apps/front/AGENTS.md` | Angular components, signals, routing, forms, i18n | +| `libs/rest-dto` | `libs/rest-dto/AGENTS.md` | Shared DTOs and API contracts (the single source of truth) | +| `libs/internal-auth` | `libs/internal-auth/AGENTS.md` | EdDSA sign/verify for service-to-service auth | +| `db` | `db/AGENTS.md` | SQL migration files and naming conventions | ### TypeScript path aliases (from `tsconfig.base.json`) -| Alias | Resolves to | -| ----------------- | ---------------------------- | -| `@dto` | `libs/rest-dto/src/index.ts` | -| `@internal-auth` | `libs/internal-auth/src/index.ts` | -| `@front/*` | `apps/front/src/*` | -| `@api/*` | `apps/api/src/*` | -| `@gateway/*` | `apps/gateway/src/*` | +| Alias | Resolves to | +| ---------------- | --------------------------------- | +| `@dto` | `libs/rest-dto/src/index.ts` | +| `@internal-auth` | `libs/internal-auth/src/index.ts` | +| `@front/*` | `apps/front/src/*` | +| `@api/*` | `apps/api/src/*` | +| `@gateway/*` | `apps/gateway/src/*` | --- diff --git a/docs/README_eng.md b/docs/README_eng.md index 07c8feb..d8f750c 100644 --- a/docs/README_eng.md +++ b/docs/README_eng.md @@ -308,9 +308,9 @@ Always runs **last**, after all implementation agents have finished. ### Skills -| Skill | Invocation | Description | -| ------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------- | -| `angular-developer` | `/angular-developer` | Loads the official Angular guidelines before writing code. Invoked automatically by the frontend agent. | +| Skill | Invocation | Description | +| ------------------- | -------------------- | ------------------------------------------------------------------------------------------------------- | +| `angular-developer` | `/angular-developer` | Loads the official Angular guidelines before writing code. Invoked automatically by the frontend agent. | > **Task tracking**: this starter is not tied to any task manager. Use whatever your team already uses (Jira, Linear, GitHub Issues, etc.) or none; the orchestration does not require one.