From b08b38a5c2424882e48ec158c0bcfbfb9b8269e2 Mon Sep 17 00:00:00 2001 From: Mathbech Date: Sat, 8 Aug 2026 11:21:57 +0200 Subject: [PATCH 1/4] chore: setup contribution (lint, prettier, tests, CI, docs, badges) --- .env.local.example | 17 + .github/ISSUE_TEMPLATE/bug_report.md | 37 + .github/ISSUE_TEMPLATE/feature_request.md | 23 + .github/PULL_REQUEST_TEMPLATE.md | 20 + .github/workflows/ci.yml | 38 + .prettierignore | 5 + .prettierrc.json | 7 + CODE_OF_CONDUCT.md | 36 + CONTRIBUTING.md | 51 + LICENSE | 21 + README.md | 28 +- eslint.config.js | 29 + package-lock.json | 1182 +++++++++++++++++++++ package.json | 15 +- scripts/gen-previews.js | 25 +- scripts/smoke.js | 36 +- server.js | 5 +- src/cards/top-langs.js | 41 +- src/github.js | 13 +- src/utils.js | 4 +- test/cards.test.js | 41 + test/rank.test.js | 30 + test/themes.test.js | 34 + test/utils.test.js | 43 + 24 files changed, 1749 insertions(+), 32 deletions(-) create mode 100644 .env.local.example create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/ci.yml create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 eslint.config.js create mode 100644 test/cards.test.js create mode 100644 test/rank.test.js create mode 100644 test/themes.test.js create mode 100644 test/utils.test.js diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..d695276 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,17 @@ +# Copie ce fichier en `.env.local` puis mets ton vrai token. +# `.env.local` est ignoré par git — ne committe JAMAIS de secret. + +# Personal Access Token GitHub (scope: public_repo / read:user). +PAT_1=ghp_xxxxxxxxxxxxxxxxxxxx + +# Optionnel : plusieurs tokens pour la rotation (PAT_2, PAT_3, ...). +# PAT_2= + +# Cache HTTP en secondes (0 = désactivé). +CACHE_SECONDS=21600 + +# Usernames autorisés (séparés par des virgules). Vide = tous autorisés. +WHITELIST= + +# Port interne (défaut 9000). +# PORT=9000 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..58269f5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Rapport de bug +about: Signaler un problème pour nous aider à l'améliorer +title: "[Bug] " +labels: bug +assignees: "" +--- + +## Description + +Description claire et concise du bug. + +## Reproduction + +Étapes pour reproduire : + +1. Appeler l'URL '...' +2. Avec les paramètres '...' +3. Voir l'erreur + +## Comportement attendu + +Ce à quoi tu t'attendais. + +## Capture / SVG + +Si possible, joins la carte générée ou une capture. + +## Environnement + +- Node.js : [ex. 20.11] +- OS : [ex. Windows 11] +- Version / commit : [ex. `dev` @ abc123] + +## Contexte additionnel + +Tout autre élément utile. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..3a0c3f8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Demande de fonctionnalité +about: Proposer une idée pour ce projet +title: "[Feat] " +labels: enhancement +assignees: "" +--- + +## Problème lié + +Le problème que ça résout. Ex. « Je suis frustré quand [...] ». + +## Solution souhaitée + +Description claire de ce que tu aimerais voir. + +## Alternatives envisagées + +Autres pistes que tu as considérées. + +## Contexte additionnel + +Maquettes, exemples d'URL, liens vers github-readme-stats, etc. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..9de2de9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ +## Description + +Que fait cette PR et pourquoi ? + +Closes #(numéro d'issue si applicable) + +## Type de changement + +- [ ] Correction de bug +- [ ] Nouvelle fonctionnalité +- [ ] Refactoring / technique +- [ ] Documentation + +## Checklist + +- [ ] `npm run lint` passe +- [ ] `npm test` passe +- [ ] `npm run smoke` passe +- [ ] J'ai ajouté / mis à jour les tests si nécessaire +- [ ] Aucun secret ni `.env.local` n'est committé diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cb70d6c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [dev, preprod, prod] + pull_request: + branches: [dev, preprod, prod] + +jobs: + quality: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [20, 22] + steps: + - uses: actions/checkout@v4 + + - name: Setup Node ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Format check + run: npm run format:check + + - name: Tests + run: npm test + + - name: Smoke test (rendu hors-ligne) + run: npm run smoke diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..d33fd23 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +node_modules/ +assets/ +logs/ +package-lock.json +scripts/preview-* diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..17d2b4e --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 80, + "tabWidth": 2 +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..659661a --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,36 @@ +# Code de conduite + +## Notre engagement + +Dans l'intérêt de favoriser un environnement ouvert et accueillant, nous nous +engageons, en tant que contributeurs et responsables, à faire de la +participation à ce projet une expérience exempte de harcèlement pour tout le +monde, quel que soit l'âge, la corpulence, le handicap, l'origine ethnique, +l'identité et l'expression de genre, le niveau d'expérience, la nationalité, +l'apparence, la religion ou l'orientation sexuelle. + +## Nos standards + +Exemples de comportements qui contribuent à un environnement positif : + +- Faire preuve d'empathie et de bienveillance. +- Respecter les opinions et expériences différentes. +- Accepter les critiques constructives avec grâce. +- Se concentrer sur ce qui est le mieux pour la communauté. + +Exemples de comportements inacceptables : + +- Propos ou images à caractère sexuel, et avances non sollicitées. +- Trolling, insultes, commentaires désobligeants, attaques personnelles ou + politiques. +- Harcèlement public ou privé. +- Publication d'informations privées d'autrui sans autorisation. + +## Application + +Les cas de comportement abusif peuvent être signalés au responsable du projet +via une issue ou par email. Toutes les plaintes seront examinées et traitées +de manière équitable. + +Ce code de conduite est adapté du [Contributor Covenant](https://www.contributor-covenant.org), +version 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..68a354f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# Contribuer à my-git-stats + +Merci de vouloir contribuer ! Ce projet est ouvert : **aucun accès en écriture n'est nécessaire**. Tout passe par un _fork_ et une _pull request_. + +## En bref + +1. **Forke** le dépôt (bouton _Fork_ en haut à droite sur GitHub). +2. **Clone** ton fork puis crée une branche : + ```bash + git clone https://github.com//my-git-stats.git + cd my-git-stats + git checkout -b feat/ma-fonctionnalite + ``` +3. **Installe et lance** : + ```bash + npm install + cp .env.local.example .env.local # ajoute ton PAT GitHub dans .env.local + npm run dev + ``` +4. **Vérifie** avant de pousser : + ```bash + npm run lint # style + erreurs + npm run format # applique Prettier + npm test # tests unitaires + ``` +5. **Pousse** ta branche et ouvre une **Pull Request** vers `dev`. + +## Prérequis + +- Node.js >= 20 (la CI teste aussi sur 22). +- Un _Personal Access Token_ GitHub pour les tests contre l'API réelle. Il va **uniquement** dans `.env.local` (ignoré par git). Ne committe jamais de token. + +## Conventions + +- **Branches** : `feat/...`, `fix/...`, `docs/...`, `chore/...`. +- **Commits** : messages clairs à l'impératif. Les [Conventional Commits](https://www.conventionalcommits.org/) sont appréciés mais pas obligatoires. +- **Style** : ESLint + Prettier gèrent le formatage. Ne te bats pas contre eux, lance `npm run format`. +- **Tests** : ajoute ou mets à jour les tests quand tu changes une logique (`test/`). Le smoke test hors-ligne (`npm run smoke`) doit toujours passer. + +## Que peut-on faire ? + +Regarde les _issues_ ouvertes, en particulier celles étiquetées `good first issue`. Des idées d'évolution sont listées dans le README (nouvelles cartes, cache, pagination…). Pour un changement important, ouvre d'abord une issue pour en discuter avant de coder. + +## Checklist de PR + +- [ ] `npm run lint` passe. +- [ ] `npm test` passe. +- [ ] La description explique le _quoi_ et le _pourquoi_. +- [ ] Pas de secret ni de `.env.local` committé. + +Toute PR est relue avant d'être fusionnée. Merci ! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d6f63ad --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mathbech + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 172aaa3..ad43432 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,15 @@ # my-git-stats +[![CI](https://github.com/Mathbech/my-git-stats/actions/workflows/ci.yml/badge.svg)](https://github.com/Mathbech/my-git-stats/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) +[![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](https://nodejs.org) +[![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md) + + + Ton propre service de cartes de stats GitHub en SVG, écrit de zéro — **aucun fork**. Node + Express interrogent l'API GraphQL de GitHub et génèrent les images. L'API reproduit les URLs de github-readme-stats, donc tu remplaces juste `...vercel.app` par ton domaine dans tes READMEs. ``` @@ -12,21 +22,21 @@ Aperçus générés avec des données factices (`node scripts/gen-previews.js`). ### Carte de stats — `/api` -| Thème | Aperçu | URL | -| --- | --- | --- | -| `default` | ![](./assets/stats-default.png) | `https://my-git-stats.bebech.fr/api?username=Mathbech` | -| `dark` | ![](./assets/stats-dark.png) | `https://my-git-stats.bebech.fr/api?username=Mathbech&theme=dark` | -| `radical` | ![](./assets/stats-radical.png) | `https://my-git-stats.bebech.fr/api?username=Mathbech&theme=radical` | +| Thème | Aperçu | URL | +| ------------ | ---------------------------------- | ----------------------------------------------------------------------- | +| `default` | ![](./assets/stats-default.png) | `https://my-git-stats.bebech.fr/api?username=Mathbech` | +| `dark` | ![](./assets/stats-dark.png) | `https://my-git-stats.bebech.fr/api?username=Mathbech&theme=dark` | +| `radical` | ![](./assets/stats-radical.png) | `https://my-git-stats.bebech.fr/api?username=Mathbech&theme=radical` | | `ocean_dark` | ![](./assets/stats-ocean_dark.png) | `https://my-git-stats.bebech.fr/api?username=Mathbech&theme=ocean_dark` | | `tokyonight` | ![](./assets/stats-tokyonight.png) | `https://my-git-stats.bebech.fr/api?username=Mathbech&theme=tokyonight` | ### Top langages — `/api/top-langs` -| Layout / thème | Aperçu | URL | -| --- | --- | --- | -| `normal` / `default` | ![](./assets/langs-normal-default.png) | `https://my-git-stats.bebech.fr/api/top-langs?username=Mathbech` | +| Layout / thème | Aperçu | URL | +| ------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------ | +| `normal` / `default` | ![](./assets/langs-normal-default.png) | `https://my-git-stats.bebech.fr/api/top-langs?username=Mathbech` | | `compact` / `tokyonight` | ![](./assets/langs-compact-tokyonight.png) | `https://my-git-stats.bebech.fr/api/top-langs?username=Mathbech&layout=compact&theme=tokyonight` | -| `compact` / `ocean_dark` | ![](./assets/langs-compact-ocean.png) | `https://my-git-stats.bebech.fr/api/top-langs?username=Mathbech&layout=compact&theme=ocean_dark` | +| `compact` / `ocean_dark` | ![](./assets/langs-compact-ocean.png) | `https://my-git-stats.bebech.fr/api/top-langs?username=Mathbech&layout=compact&theme=ocean_dark` | ### Paramètres d'URL diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..a1d7e50 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,29 @@ +// ESLint flat config (ESLint 9+). Projet ESM Node. +import js from "@eslint/js"; +import globals from "globals"; +import prettier from "eslint-config-prettier"; + +export default [ + { + ignores: ["node_modules/**", "assets/**", "logs/**", "scripts/preview-*"], + }, + js.configs.recommended, + { + files: ["**/*.js"], + languageOptions: { + ecmaVersion: 2023, + sourceType: "module", + globals: { + ...globals.node, + }, + }, + rules: { + "no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], + "no-console": "off", + eqeqeq: ["error", "smart"], + "prefer-const": "error", + }, + }, + // Désactive les règles de style qui entrent en conflit avec Prettier. + prettier, +]; diff --git a/package-lock.json b/package-lock.json index 5b50cb2..513c4a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,304 @@ "dependencies": { "express": "^4.19.2" }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "eslint": "^9.9.0", + "eslint-config-prettier": "^9.1.0", + "globals": "^15.9.0", + "prettier": "^3.3.3" + }, "engines": { "node": ">=20" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -27,12 +321,82 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", @@ -57,6 +421,17 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -95,6 +470,60 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -131,6 +560,21 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -140,6 +584,13 @@ "ms": "2.0.0" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -224,6 +675,211 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -279,6 +935,40 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -297,6 +987,44 @@ "node": ">= 0.8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -361,6 +1089,32 @@ "node": ">= 0.4" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -373,6 +1127,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -429,6 +1193,43 @@ "node": ">=0.10.0" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -444,6 +1245,127 @@ "node": ">= 0.10" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -513,12 +1435,32 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -552,6 +1494,69 @@ "node": ">= 0.8" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -561,12 +1566,58 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-to-regexp": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -580,6 +1631,16 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.15.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", @@ -620,6 +1681,16 @@ "node": ">= 0.8" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -697,6 +1768,29 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", @@ -778,6 +1872,32 @@ "node": ">= 0.8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -787,6 +1907,19 @@ "node": ">=0.6" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -809,6 +1942,16 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -826,6 +1969,45 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 310b58c..c1c4a1b 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,25 @@ "scripts": { "start": "node server.js", "dev": "node --watch server.js", - "smoke": "node scripts/smoke.js" + "smoke": "node scripts/smoke.js", + "test": "node --test test/*.test.js", + "coverage": "node --test --experimental-test-coverage test/*.test.js", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "engines": { "node": ">=20" }, "dependencies": { "express": "^4.19.2" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "eslint": "^9.9.0", + "eslint-config-prettier": "^9.1.0", + "globals": "^15.9.0", + "prettier": "^3.3.3" } } diff --git a/scripts/gen-previews.js b/scripts/gen-previews.js index 7c7b952..ac71716 100644 --- a/scripts/gen-previews.js +++ b/scripts/gen-previews.js @@ -10,7 +10,15 @@ import { renderTopLangsCard } from "../src/cards/top-langs.js"; const outDir = join(dirname(fileURLToPath(import.meta.url)), "..", "assets"); mkdirSync(outDir, { recursive: true }); -const stats = { name: "Mathbech", stars: 6626, commits: 1843, prs: 214, issues: 57, reviews: 42, followers: 389 }; +const stats = { + name: "Mathbech", + stars: 6626, + commits: 1843, + prs: 214, + issues: 57, + reviews: 42, + followers: 389, +}; const langs = [ { name: "JavaScript", color: "#f1e05a", percent: 41.2 }, { name: "TypeScript", color: "#3178c6", percent: 28.7 }, @@ -25,9 +33,18 @@ const files = []; for (const theme of themes) { files.push([`stats-${theme}.svg`, renderStatsCard(stats, { theme })]); } -files.push(["langs-normal-default.svg", renderTopLangsCard(langs, { theme: "default" })]); -files.push(["langs-compact-tokyonight.svg", renderTopLangsCard(langs, { theme: "tokyonight", layout: "compact" })]); -files.push(["langs-compact-ocean.svg", renderTopLangsCard(langs, { theme: "ocean_dark", layout: "compact" })]); +files.push([ + "langs-normal-default.svg", + renderTopLangsCard(langs, { theme: "default" }), +]); +files.push([ + "langs-compact-tokyonight.svg", + renderTopLangsCard(langs, { theme: "tokyonight", layout: "compact" }), +]); +files.push([ + "langs-compact-ocean.svg", + renderTopLangsCard(langs, { theme: "ocean_dark", layout: "compact" }), +]); for (const [name, svg] of files) { writeFileSync(join(outDir, name), svg); diff --git a/scripts/smoke.js b/scripts/smoke.js index 00b3f53..fb5b9f2 100644 --- a/scripts/smoke.js +++ b/scripts/smoke.js @@ -4,7 +4,15 @@ import { writeFileSync } from "node:fs"; import { renderStatsCard } from "../src/cards/stats.js"; import { renderTopLangsCard } from "../src/cards/top-langs.js"; -const fakeStats = { name: "Mathbech", stars: 6626, commits: 1843, prs: 214, issues: 57, reviews: 42, followers: 389 }; +const fakeStats = { + name: "Mathbech", + stars: 6626, + commits: 1843, + prs: 214, + issues: 57, + reviews: 42, + followers: 389, +}; const fakeLangs = [ { name: "JavaScript", color: "#f1e05a", percent: 41.2 }, { name: "TypeScript", color: "#3178c6", percent: 28.7 }, @@ -14,16 +22,24 @@ const fakeLangs = [ ]; function assertWellFormed(svg, label) { - if (!svg.startsWith("")) throw new Error(`${label}: ne finit pas par `); + if (!svg.startsWith("")) + throw new Error(`${label}: ne finit pas par `); const open = (svg.match(/<[a-zA-Z]/g) || []).length; const close = (svg.match(/<\/|\/>/g) || []).length; if (close < 1) throw new Error(`${label}: aucune balise fermante`); console.log(`OK ${label} (${svg.length} octets, ~${open} balises)`); } -const stats = renderStatsCard(fakeStats, { theme: "ocean_dark", show_icons: "true" }); -const langsCompact = renderTopLangsCard(fakeLangs, { theme: "ocean_dark", layout: "compact" }); +const stats = renderStatsCard(fakeStats, { + theme: "ocean_dark", + show_icons: "true", +}); +const langsCompact = renderTopLangsCard(fakeLangs, { + theme: "ocean_dark", + layout: "compact", +}); const langsNormal = renderTopLangsCard(fakeLangs, {}); assertWellFormed(stats, "stats"); @@ -31,6 +47,12 @@ assertWellFormed(langsCompact, "top-langs compact"); assertWellFormed(langsNormal, "top-langs normal"); writeFileSync(new URL("./preview-stats.svg", import.meta.url), stats); -writeFileSync(new URL("./preview-langs-compact.svg", import.meta.url), langsCompact); -writeFileSync(new URL("./preview-langs-normal.svg", import.meta.url), langsNormal); +writeFileSync( + new URL("./preview-langs-compact.svg", import.meta.url), + langsCompact, +); +writeFileSync( + new URL("./preview-langs-normal.svg", import.meta.url), + langsNormal, +); console.log("Aperçus écrits dans scripts/preview-*.svg"); diff --git a/server.js b/server.js index 24c5283..bde065d 100644 --- a/server.js +++ b/server.js @@ -10,7 +10,10 @@ import { logError } from "./src/logger.js"; const app = express(); const PORT = process.env.PORT || 9000; -const CACHE_SECONDS = Math.max(parseInt(process.env.CACHE_SECONDS || "21600", 10), 0); +const CACHE_SECONDS = Math.max( + parseInt(process.env.CACHE_SECONDS || "21600", 10), + 0, +); const WHITELIST = (process.env.WHITELIST || "") .split(",") .map((s) => s.trim().toLowerCase()) diff --git a/src/cards/top-langs.js b/src/cards/top-langs.js index 92dc356..c44cf8d 100644 --- a/src/cards/top-langs.js +++ b/src/cards/top-langs.js @@ -11,16 +11,37 @@ export function renderTopLangsCard(langs, query = {}) { const hideBorder = parseBool(query.hide_border, false); const compact = query.layout === "compact"; - const count = Math.min(Math.max(parseInt(query.langs_count || "5", 10) || 5, 1), 20); - const title = query.custom_title ? escapeXml(query.custom_title) : "Most Used Languages"; + const count = Math.min( + Math.max(parseInt(query.langs_count || "5", 10) || 5, 1), + 20, + ); + const title = query.custom_title + ? escapeXml(query.custom_title) + : "Most Used Languages"; const top = langs.slice(0, count); const width = Number(query.card_width) || 300; if (compact) { - return renderCompact(top, { title, width, title_color, text_color, bg_color, border_color, hideBorder }); + return renderCompact(top, { + title, + width, + title_color, + text_color, + bg_color, + border_color, + hideBorder, + }); } - return renderNormal(top, { title, width, title_color, text_color, bg_color, border_color, hideBorder }); + return renderNormal(top, { + title, + width, + title_color, + text_color, + bg_color, + border_color, + hideBorder, + }); } function renderNormal(top, o) { @@ -73,7 +94,11 @@ function renderCompact(top, o) { const rows = Math.ceil(top.length / 2); const height = 90 + rows * 25 + 10; - return svgWrap(o, height, `${segments}${legend}`); + return svgWrap( + o, + height, + `${segments}${legend}`, + ); } function svgWrap(o, height, inner) { @@ -87,5 +112,9 @@ function svgWrap(o, height, inner) { function escapeColor(c) { if (!c) return "#858585"; - return /^#?[0-9a-fA-F]{3,8}$/.test(c) ? (c.startsWith("#") ? c : `#${c}`) : "#858585"; + return /^#?[0-9a-fA-F]{3,8}$/.test(c) + ? c.startsWith("#") + ? c + : `#${c}` + : "#858585"; } diff --git a/src/github.js b/src/github.js index 21dad83..df71965 100644 --- a/src/github.js +++ b/src/github.js @@ -5,7 +5,8 @@ const GITHUB_GRAPHQL = "https://api.github.com/graphql"; function getToken() { const token = process.env.PAT_1 || process.env.GITHUB_TOKEN; - if (!token) throw new Error("Aucun token : définis PAT_1 dans l'environnement."); + if (!token) + throw new Error("Aucun token : définis PAT_1 dans l'environnement."); return token; } @@ -62,7 +63,9 @@ export async function fetchStats(username, { includeAllCommits = false } = {}) { ); const commits = user.contributionsCollection.totalCommitContributions + - (includeAllCommits ? user.contributionsCollection.restrictedContributionsCount : 0); + (includeAllCommits + ? user.contributionsCollection.restrictedContributionsCount + : 0); return { name: user.name || user.login, @@ -101,7 +104,11 @@ export async function fetchTopLangs(username) { for (const repo of user.repositories.nodes) { for (const edge of repo.languages.edges) { const name = edge.node.name; - const prev = totals.get(name) || { name, color: edge.node.color || "#858585", size: 0 }; + const prev = totals.get(name) || { + name, + color: edge.node.color || "#858585", + size: 0, + }; prev.size += edge.size; totals.set(name, prev); } diff --git a/src/utils.js b/src/utils.js index 152414e..4365945 100644 --- a/src/utils.js +++ b/src/utils.js @@ -28,7 +28,9 @@ export function formatNumber(n) { export function sanitizeHex(value, fallback) { if (!value) return fallback; const v = String(value).replace(/^#/, ""); - return /^([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(v) + return /^([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test( + v, + ) ? `#${v}` : fallback; } diff --git a/test/cards.test.js b/test/cards.test.js new file mode 100644 index 0000000..224652b --- /dev/null +++ b/test/cards.test.js @@ -0,0 +1,41 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { renderStatsCard } from "../src/cards/stats.js"; +import { renderTopLangsCard } from "../src/cards/top-langs.js"; + +const stats = { + name: "Mathbech", + stars: 6626, + commits: 1843, + prs: 214, + issues: 57, + reviews: 42, + followers: 389, +}; +const langs = [ + { name: "JavaScript", color: "#f1e05a", percent: 41.2 }, + { name: "TypeScript", color: "#3178c6", percent: 28.7 }, + { name: "Python", color: "#3572A5", percent: 30.1 }, +]; + +function assertWellFormedSvg(svg) { + assert.ok(svg.startsWith(""), "finit par "); +} + +test("renderStatsCard produit un SVG bien formé", () => { + const svg = renderStatsCard(stats, { + theme: "ocean_dark", + show_icons: "true", + }); + assertWellFormedSvg(svg); + assert.ok(svg.includes("Mathbech")); +}); + +test("renderTopLangsCard (normal) produit un SVG bien formé", () => { + assertWellFormedSvg(renderTopLangsCard(langs, {})); +}); + +test("renderTopLangsCard (compact) produit un SVG bien formé", () => { + assertWellFormedSvg(renderTopLangsCard(langs, { layout: "compact" })); +}); diff --git a/test/rank.test.js b/test/rank.test.js new file mode 100644 index 0000000..a75564f --- /dev/null +++ b/test/rank.test.js @@ -0,0 +1,30 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { calculateRank } from "../src/rank.js"; + +const LEVELS = ["S", "A+", "A", "A-", "B+", "B", "B-", "C+", "C"]; + +test("un profil vide donne le niveau le plus bas", () => { + const { level, percentile } = calculateRank({}); + assert.equal(level, "C"); + assert.ok(percentile > 87.5 && percentile <= 100); +}); + +test("un profil très actif donne un meilleur rang qu'un profil faible", () => { + const strong = calculateRank({ + commits: 5000, + prs: 500, + issues: 300, + reviews: 100, + stars: 10000, + followers: 2000, + }); + const weak = calculateRank({ commits: 10, stars: 1 }); + assert.ok(LEVELS.indexOf(strong.level) < LEVELS.indexOf(weak.level)); + assert.ok(strong.percentile < weak.percentile); +}); + +test("le niveau retourné fait toujours partie de l'échelle", () => { + const { level } = calculateRank({ commits: 250, prs: 50, stars: 50 }); + assert.ok(LEVELS.includes(level)); +}); diff --git a/test/themes.test.js b/test/themes.test.js new file mode 100644 index 0000000..bbf8555 --- /dev/null +++ b/test/themes.test.js @@ -0,0 +1,34 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { THEMES, resolveTheme } from "../src/themes.js"; + +test("chaque thème définit les cinq couleurs", () => { + const keys = [ + "title_color", + "text_color", + "icon_color", + "bg_color", + "border_color", + ]; + for (const [name, theme] of Object.entries(THEMES)) { + for (const k of keys) { + assert.ok(theme[k], `${name} manque ${k}`); + } + } +}); + +test("resolveTheme retombe sur default pour un thème inconnu", () => { + assert.deepEqual(resolveTheme({ theme: "inexistant" }), { + title_color: THEMES.default.title_color, + text_color: THEMES.default.text_color, + icon_color: THEMES.default.icon_color, + bg_color: THEMES.default.bg_color, + border_color: THEMES.default.border_color, + }); +}); + +test("resolveTheme applique les surcharges d'URL", () => { + const resolved = resolveTheme({ theme: "dark", title_color: "ff0000" }); + assert.equal(resolved.title_color, "ff0000"); + assert.equal(resolved.text_color, THEMES.dark.text_color); +}); diff --git a/test/utils.test.js b/test/utils.test.js new file mode 100644 index 0000000..8fa9db9 --- /dev/null +++ b/test/utils.test.js @@ -0,0 +1,43 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + escapeXml, + formatNumber, + sanitizeHex, + parseBool, +} from "../src/utils.js"; + +test("escapeXml échappe les caractères spéciaux", () => { + assert.equal( + escapeXml(`& '`), + "<a href="x">& '", + ); + assert.equal(escapeXml(), ""); + assert.equal(escapeXml(42), "42"); +}); + +test("formatNumber abrège les grands nombres", () => { + assert.equal(formatNumber(0), "0"); + assert.equal(formatNumber(999), "999"); + assert.equal(formatNumber(1000), "1k"); + assert.equal(formatNumber(6626), "6.6k"); + assert.equal(formatNumber(1_500_000), "1.5M"); + assert.equal(formatNumber(null), "0"); + assert.equal(formatNumber(undefined), "0"); +}); + +test("sanitizeHex valide et normalise les couleurs", () => { + assert.equal(sanitizeHex("fff", "000"), "#fff"); + assert.equal(sanitizeHex("#2f80ed", "000"), "#2f80ed"); + assert.equal(sanitizeHex("nope", "abcabc"), "abcabc"); + assert.equal(sanitizeHex("", "fallback"), "fallback"); +}); + +test("parseBool interprète les valeurs de query string", () => { + assert.equal(parseBool("true"), true); + assert.equal(parseBool("1"), true); + assert.equal(parseBool(""), true); + assert.equal(parseBool("false"), false); + assert.equal(parseBool(undefined), false); + assert.equal(parseBool(undefined, true), true); +}); From f2f4b272ef110831fa10d7e708e8f6a89a54d115 Mon Sep 17 00:00:00 2001 From: Mathbech Date: Sat, 8 Aug 2026 11:32:04 +0200 Subject: [PATCH 2/4] =?UTF-8?q?ci:=20int=C3=A9gration=20SonarCloud=20(cove?= =?UTF-8?q?rage=20LCOV=20+=20scan)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++++++ .gitignore | 3 +++ README.md | 5 ++--- package.json | 1 + sonar-project.properties | 16 ++++++++++++++++ 5 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 sonar-project.properties diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb70d6c..0256949 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,3 +36,33 @@ jobs: - name: Smoke test (rendu hors-ligne) run: npm run smoke + + sonarcloud: + name: SonarCloud + runs-on: ubuntu-latest + needs: quality + # Le scan ne tourne pas sur les PR issues d'un fork (le secret SONAR_TOKEN + # n'y est pas exposé). + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Sonar a besoin de l'historique complet (blame). + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Coverage (LCOV) + run: npm run coverage:lcov + + - name: SonarCloud Scan + uses: SonarSource/sonarqube-scan-action@v5 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: https://sonarcloud.io diff --git a/.gitignore b/.gitignore index 47983f5..b54c98b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,8 @@ scripts/preview-*.png logs/ *.log +# Couverture de code (générée par les tests / la CI) +coverage/ + # Divers .DS_Store diff --git a/README.md b/README.md index ad43432..f95582d 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,9 @@ [![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](https://nodejs.org) [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md) - +[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=Mathbech_my-git-stats&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=Mathbech_my-git-stats) Ton propre service de cartes de stats GitHub en SVG, écrit de zéro — **aucun fork**. Node + Express interrogent l'API GraphQL de GitHub et génèrent les images. L'API reproduit les URLs de github-readme-stats, donc tu remplaces juste `...vercel.app` par ton domaine dans tes READMEs. diff --git a/package.json b/package.json index c1c4a1b..ebb4689 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "smoke": "node scripts/smoke.js", "test": "node --test test/*.test.js", "coverage": "node --test --experimental-test-coverage test/*.test.js", + "coverage:lcov": "node -e \"require('fs').mkdirSync('coverage',{recursive:true})\" && node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info test/*.test.js", "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier --write .", diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..e073fe5 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,16 @@ +# --- Identité du projet (à vérifier sur sonarcloud.io) --- +# La clé d'organisation est visible dans SonarCloud > ton orga > Administration. +sonar.organization=mathbech +sonar.projectKey=Mathbech_my-git-stats +sonar.projectName=my-git-stats + +# --- Analyse --- +sonar.sources=src,server.js +sonar.tests=test +sonar.sourceEncoding=UTF-8 + +# Rapport de couverture généré par `npm run coverage:lcov`. +sonar.javascript.lcov.reportPaths=coverage/lcov.info + +# Fichiers hors périmètre d'analyse. +sonar.exclusions=assets/**,scripts/preview-*,node_modules/**,coverage/** From cfd235116844380a393af9a622a1f4ac2532a979 Mon Sep 17 00:00:00 2001 From: Mathbech Date: Sat, 8 Aug 2026 11:40:39 +0200 Subject: [PATCH 3/4] chore: add security policy and vulnerability reporting guidelines --- CONTRIBUTING.md | 1 + SECURITY.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 SECURITY.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 68a354f..5713c5e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,6 +34,7 @@ Merci de vouloir contribuer ! Ce projet est ouvert : **aucun accès en écriture - **Branches** : `feat/...`, `fix/...`, `docs/...`, `chore/...`. - **Commits** : messages clairs à l'impératif. Les [Conventional Commits](https://www.conventionalcommits.org/) sont appréciés mais pas obligatoires. +- **Signature** : les commits doivent être signés (GPG/SSH) pour apparaître « Verified ». Configure `git config commit.gpgsign true` et ta `user.signingkey`. Voir la [doc GitHub](https://docs.github.com/authentication/managing-commit-signature-verification). - **Style** : ESLint + Prettier gèrent le formatage. Ne te bats pas contre eux, lance `npm run format`. - **Tests** : ajoute ou mets à jour les tests quand tu changes une logique (`test/`). Le smoke test hors-ligne (`npm run smoke`) doit toujours passer. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..01eb6bd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,30 @@ +# Politique de sécurité + +## Versions supportées + +Le projet est en développement actif ; seule la dernière version de la branche +par défaut reçoit des correctifs de sécurité. + +## Signaler une vulnérabilité + +**Merci de ne pas ouvrir d'issue publique pour une faille de sécurité.** + +Signale-la en privé via l'onglet **Security → Report a vulnerability** du dépôt +GitHub ([Private vulnerability reporting](https://github.com/Mathbech/my-git-stats/security/advisories/new)), +ou par email à **contact@bebech.fr**. + +Merci d'inclure, si possible : + +- une description du problème et de son impact ; +- les étapes pour le reproduire (URL, paramètres, payload) ; +- la version / le commit concerné. + +Tu peux t'attendre à un premier retour sous quelques jours. Une fois le correctif +disponible, ta contribution au signalement sera créditée si tu le souhaites. + +## Bonnes pratiques rappelées + +- Aucun secret (token GitHub, etc.) ne doit être committé : ils vivent dans + `.env.local`, ignoré par git. +- Le token utilisé en production reste sur le serveur ; il n'est jamais exposé + dans le code ni dans les logs. From aa2d5196439b6c625a3a269e64da7037a1ee4972 Mon Sep 17 00:00:00 2001 From: Mathbech Date: Sat, 8 Aug 2026 11:42:25 +0200 Subject: [PATCH 4/4] chore: update Node.js version matrix to include version 24 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0256949..b778f08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20, 22] + node-version: [20, 22, 24] steps: - uses: actions/checkout@v4