@starterlib/cli 0.10.11 → 0.10.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,24 +48,25 @@ Codes de sortie : `0` conforme ; `1` une etape a echoue ; `2` la commande ne peu
48
48
 
49
49
  ## Les etapes de `verify`
50
50
 
51
- | Etape | Role | Ce qu'elle garde |
52
- | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
- | `versions` | les deux | Versions exactes de la matrice, greffon de federation lie a Angular, pnpm, Node, train du socle |
54
- | `dependances` | les deux | Dependances de production revues, paquets interdits, aucun outil du socle recopie sous `tools/` |
55
- | `jetons` | les deux | Chaque `var(--st-*)` sans repli existe dans `@starterlib/tokens` (feuilles et styles en ligne) |
56
- | `format` | les deux | `prettier --check .` |
57
- | `lint` | les deux | Prefixe des selecteurs (jamais `st-`), `eslint .`, puis stylelint (configuration du socle exigee) |
58
- | `i18n` | les deux | Memes cles et arguments en fr et en, messages lisibles, cles appelees presentes, aide de chaque page (`<perimetre>.aide.<page>`), types a jour |
59
- | `test` | les deux | `ng test --no-watch` |
60
- | `contrat` | les deux | `pnpm run contrat:check` s'il existe (client d'API engendre, lot 175), sinon sautee en le disant |
61
- | `simulations` | remote | Les stubs WireMock du service, tels que l'hote de developpement les rejoue, a jour (lot 269) ; sautee sans simulateur ; un stub hors du sous-ensemble est nomme |
62
- | `build` | les deux | `starter-web construire` |
63
- | `manifeste` | les deux | `validerLeManifesteDeRemote` et le schema de `@starterlib/core` INSTALLE ; le nom partout le meme |
64
- | `federation` | les deux | Partage en singleton strict a plage explicite, Angular et le socle partages, aucun import nu orphelin |
65
- | `entree` | hote | Aucun paquet partage importe statiquement avant `initFederation` |
66
- | `budget` | les deux | En brotli. Remote : chaque page (60 Ko), le total (250 Ko), les catalogues. Hote : premier ecran |
67
- | `amorcage` | hote | Aucun pair lourd (CDK, aria, formulaires, surfaces) dans le premier ecran |
68
- | `securite` | les deux | Puits de l'ADR 0029 dans le code de la SPA, politiques Trusted Types exactes, cartes, SVG, meta CSP |
51
+ | Etape | Role | Ce qu'elle garde |
52
+ | ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
+ | `versions` | les deux | Versions exactes de la matrice, greffon de federation lie a Angular, pnpm, Node, train du socle |
54
+ | `dependances` | les deux | Dependances de production revues, paquets interdits, aucun outil du socle recopie sous `tools/` |
55
+ | `jetons` | les deux | Chaque `var(--st-*)` sans repli existe dans `@starterlib/tokens` (feuilles et styles en ligne) |
56
+ | `format` | les deux | `prettier --check .` |
57
+ | `lint` | les deux | Prefixe des selecteurs (jamais `st-`), `eslint .`, puis stylelint (configuration du socle exigee) |
58
+ | `i18n` | les deux | Memes cles et arguments en fr et en, messages lisibles, cles appelees presentes, aide de chaque page (`<perimetre>.aide.<page>`), types a jour |
59
+ | `test` | les deux | `ng test --no-watch` |
60
+ | `contrat` | les deux | `pnpm run contrat:check` s'il existe (client d'API engendre, lot 175), sinon sautee en le disant |
61
+ | `simulations` | remote | Les stubs WireMock du service, tels que l'hote de developpement les rejoue, a jour (lot 269) ; sautee sans simulateur ; un stub hors du sous-ensemble est nomme |
62
+ | `build` | les deux | `starter-web construire` |
63
+ | `vulnerabilites` | les deux | `pnpm audit --prod` : HIGH et CRITICAL bloquent sauf exception DATEE de `securite/exceptions-npm.json` (un trimestre au plus) ; SBOM CycloneDX dans `dist/sbom-front.cdx.json`, a cote du dossier livre (jamais servie) ; licences AGPL, SSPL et inconnues refusees (lot 139) |
64
+ | `manifeste` | les deux | `validerLeManifesteDeRemote` et le schema de `@starterlib/core` INSTALLE ; le nom partout le meme |
65
+ | `federation` | les deux | Partage en singleton strict a plage explicite, Angular et le socle partages, aucun import nu orphelin |
66
+ | `entree` | hote | Aucun paquet partage importe statiquement avant `initFederation` |
67
+ | `budget` | les deux | En brotli. Remote : chaque page (60 Ko), le total (250 Ko), les catalogues. Hote : premier ecran |
68
+ | `amorcage` | hote | Aucun pair lourd (CDK, aria, formulaires, surfaces) dans le premier ecran |
69
+ | `securite` | les deux | Puits de l'ADR 0029 dans le code de la SPA, politiques Trusted Types exactes, cartes, SVG, meta CSP |
69
70
 
70
71
  `--etapes a,b` n'en joue que certaines ; `--sauter a,b` en retire.
71
72
 
@@ -31,6 +31,18 @@ proprietaire du 2026-09-26).
31
31
  | `stylelint`, `postcss-scss` | `17.15.0`, `4.0.9` | Les feuilles du socle et des SPA (lot 178, `@starterlib/eslint-config/stylelint`) ; `postcss-scss` lit le SCSS |
32
32
  | `jsdom`, `axe-core` | `30.0.1`, `4.13.0` | Le DOM des essais unitaires et le controle d'accessibilite : un correctif change ce qu'un essai voit |
33
33
 
34
+ ## Prochain train : `2026.2` (prevu, non applique)
35
+
36
+ Lot 297 : le train suivant se monte a la promotion LTS de Node 26 (calendrier de Node : fin octobre
37
+ 2026), et pas avant. Ce que la montee changera, dans un commit par depot (socle puis les quatre
38
+ interfaces), avec `pnpm verify` et `banc-d-assemblage.sh --construire` verts :
39
+
40
+ - `engines.node` exact sur Node 26, `setup-node` des workflows, `build-spa.yml` et
41
+ `frontend.node.version` de `platform-parent` ;
42
+ - pnpm 12 une fois publie et age de trois jours (`packageManager`, `frontend.pnpm.version`) ;
43
+ - reevaluation des API de la section 3 de `regles-front.md` (`withViewTransitions`, `debounced`,
44
+ `@boundary`, ancrage CSS avec le banc sur trois moteurs).
45
+
34
46
  ## A destination des depots de SPA (pas de ce depot)
35
47
 
36
48
  | Element | Version | Avertissement |
@@ -53,8 +65,25 @@ maximum, fonction par fonction, de ce que les feuilles et le noyau emploient san
53
65
  | `transition-behavior: allow-discrete` | 117 | 17.4 | 129 | admis pour une sortie qui anime `display` |
54
66
 
55
67
  Sous ce plancher, rien ne casse sans le dire : une surface sans `@starting-style` apparait d'un coup ; une
56
- couleur sans `light-dark()` n'existe pas, d'ou le plancher. Le banc tourne sur Chromium ; Firefox et WebKit
57
- entrent au banc d'assemblage (lot 276).
68
+ couleur sans `light-dark()` n'existe pas, d'ou le plancher.
69
+
70
+ Ce plancher est plus haut que celui d'Angular 22.2 (« baseline widely available » au 2026-05-07 : Chrome et
71
+ Edge 119, Firefox 119, Safari et iOS 17) : la construction ne vise pas plus bas que le socle n'en a besoin.
72
+
73
+ **Mesure (banc d'assemblage du BFF, lot 276, 2026-10-01).** Playwright 1.63 : Chromium 153, Firefox 155,
74
+ WebKit 26.6, Pixel 7 et iPhone 15 emules. Chromium et le telephone Android jouent tout le banc a chaque
75
+ poussee ; Firefox, WebKit et l'iPhone jouent les scenarios `@moteurs` et `@mobile` chaque semaine
76
+ (`BANC_MOTEURS=tous`).
77
+
78
+ | Moteur | Version mesuree | Plancher | Ce qui se degrade entre le plancher et la mesure |
79
+ | ------------- | --------------- | -------- | ---------------------------------------------------------------------------------- |
80
+ | Chromium/Edge | 153 | 123 | rien de connu : les types de confiance y sont appliques depuis bien avant 123 |
81
+ | Firefox | 155 | 129 | la protection des puits DOM par les types de confiance, si la version ne les a pas |
82
+ | Safari/iOS | 26.6 | 17.5 | idem ; la politique bloque toujours les scripts sans nonce |
83
+
84
+ **WebView.** Android System WebView suit Chromium, WKWebView suit le Safari du systeme : memes planchers
85
+ (decisions mobiles, ADR 0043 du socle `api/starter`). **Revision** : a chaque montee de Playwright (nouveaux moteurs) et au plus
86
+ tard a chaque majeure d'Angular.
58
87
 
59
88
  ## Regle de retard
60
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starterlib/cli",
3
- "version": "0.10.11",
3
+ "version": "0.10.13",
4
4
  "description": "Chaine de verification et de construction des SPA du socle frontal starter : starter-web verify, construire, sprite, manifeste, precompresser. Jamais en dependance de production.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Etape `vulnerabilites` (lot 139) : `pnpm audit --prod` bloquant sur HIGH et CRITICAL, sauf
3
+ * exception datee de `securite/exceptions-npm.json` ; SBOM CycloneDX des dependances de production
4
+ * dans `dist/sbom-front.cdx.json` (a cote du dossier livre, jamais servie) ; licences AGPL, SSPL et
5
+ * inconnues refusees. Le registre npm doit repondre : sans lui, on refuse plutot que supposer.
6
+ */
7
+ import { auditerLesDependances } from '../vulnerabilites.mjs';
8
+
9
+ export default {
10
+ nom: 'vulnerabilites',
11
+ roles: ['remote', 'hote'],
12
+ executer(projet) {
13
+ try {
14
+ const { defauts, exceptes, composants } = auditerLesDependances(projet.racine);
15
+ const sous = exceptes.length > 0 ? `, ${String(exceptes.length)} avis excepte(s)` : '';
16
+ return {
17
+ defauts,
18
+ resume: `${String(composants)} composant(s) de production audites et sous licence admise${sous} (dist/sbom-front.cdx.json).`,
19
+ };
20
+ } catch (erreur) {
21
+ return { defauts: [erreur instanceof Error ? erreur.message : String(erreur)] };
22
+ }
23
+ },
24
+ };
package/src/verifier.mjs CHANGED
@@ -21,6 +21,7 @@ import { contrat, format, lint, test } from './etapes/outillage.mjs';
21
21
  import securite from './etapes/securite.mjs';
22
22
  import simulations from './etapes/simulations.mjs';
23
23
  import versions from './etapes/versions.mjs';
24
+ import vulnerabilites from './etapes/vulnerabilites.mjs';
24
25
 
25
26
  const build = {
26
27
  nom: 'build',
@@ -46,6 +47,7 @@ export const ETAPES = [
46
47
  contrat,
47
48
  simulations,
48
49
  build,
50
+ vulnerabilites,
49
51
  manifeste,
50
52
  federation,
51
53
  entree,
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Vulnerabilites et licences des dependances npm (lot 139), partagees par le socle frontal
3
+ * (`tools/verifier-dependances.mjs`) et chaque SPA (etape `vulnerabilites` de `starter-web verify`).
4
+ *
5
+ * - `pnpm audit --prod` : tout avis HIGH ou CRITICAL bloque, sauf exception DATEE du registre
6
+ * `securite/exceptions-npm.json` ; une exception sans date, perimee ou a plus d'un trimestre
7
+ * bloque aussi (politique commune : api/starter docs/security/gestion-des-vulnerabilites.md) ;
8
+ * - `pnpm sbom` : la SBOM CycloneDX des dependances de production, ecrite dans
9
+ * `dist/sbom-front.cdx.json`, A COTE du dossier livre (jamais servie : elle dirait a un attaquant
10
+ * chaque version installee) ;
11
+ * - licences : AGPL, SSPL ou inconnue interdites dans les dependances de production.
12
+ */
13
+ import { spawnSync } from 'node:child_process';
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+
17
+ /** Une exception ne vaut qu'un trimestre : au-dela, elle se renouvelle, justification relue. */
18
+ export const HORIZON_JOURS = 92;
19
+ const GRAVES = new Set(['high', 'critical']);
20
+ const IDENTIFIANT = /^(GHSA(-[23456789cfghjmpqrvwx]{4}){3}|CVE-\d{4}-\d{4,})$/;
21
+ const LICENCE_INTERDITE = /\b(AGPL|SSPL)/i;
22
+
23
+ const jour = (date) => date.toISOString().slice(0, 10);
24
+
25
+ /** Lit le registre : les exceptions valables, et les defauts de celles qui ne le sont pas. */
26
+ export function lireExceptions(registre, aujourdHui = new Date()) {
27
+ const defauts = [];
28
+ const valables = [];
29
+ const limite = new Date(aujourdHui.getTime() + HORIZON_JOURS * 86_400_000);
30
+ for (const [indice, exception] of (registre.exceptions ?? []).entries()) {
31
+ const nom = `exception ${String(indice + 1)} (${String(exception.avis)})`;
32
+ if (typeof exception.avis !== 'string' || !IDENTIFIANT.test(exception.avis)) {
33
+ defauts.push(`${nom} : avis absent ou illisible (GHSA-xxxx-xxxx-xxxx ou CVE-AAAA-NNNN)`);
34
+ } else if (
35
+ typeof exception.justification !== 'string' ||
36
+ exception.justification.trim().length < 20
37
+ ) {
38
+ defauts.push(`${nom} : justification absente ou trop courte`);
39
+ } else if (
40
+ typeof exception.echeance !== 'string' ||
41
+ !/^\d{4}-\d{2}-\d{2}$/.test(exception.echeance) ||
42
+ Number.isNaN(Date.parse(exception.echeance))
43
+ ) {
44
+ defauts.push(`${nom} : sans echeance datee (AAAA-MM-JJ)`);
45
+ } else if (exception.echeance < jour(aujourdHui)) {
46
+ defauts.push(`${nom} : perimee depuis le ${exception.echeance}`);
47
+ } else if (exception.echeance > jour(limite)) {
48
+ defauts.push(
49
+ `${nom} : echeance a plus de ${String(HORIZON_JOURS)} jours (${exception.echeance})`,
50
+ );
51
+ } else {
52
+ valables.push(exception);
53
+ }
54
+ }
55
+ return { valables, defauts };
56
+ }
57
+
58
+ /** Les avis graves de `pnpm audit --json` : bloquants, ou couverts par une exception valable. */
59
+ export function trierLesAvis(audit, exceptions) {
60
+ const bloquants = [];
61
+ const exceptes = [];
62
+ for (const avis of Object.values(audit.advisories ?? {})) {
63
+ if (!GRAVES.has(String(avis.severity).toLowerCase())) continue;
64
+ const identifiants = [avis.github_advisory_id, ...(avis.cves ?? [])].filter(Boolean);
65
+ const exception = exceptions.find((candidate) => identifiants.includes(candidate.avis));
66
+ const ligne =
67
+ `${String(avis.severity).toUpperCase()} ${String(avis.module_name)} ` +
68
+ `${identifiants.join(' ')} (corrige : ${String(avis.patched_versions)})`;
69
+ if (exception === undefined) bloquants.push(ligne);
70
+ else exceptes.push(`${ligne}, excepte jusqu'au ${String(exception.echeance)}`);
71
+ }
72
+ return { bloquants, exceptes };
73
+ }
74
+
75
+ /** Les composants de la SBOM dont la licence est interdite ou inconnue. */
76
+ export function licencesRefusees(sbom) {
77
+ const refus = [];
78
+ for (const composant of sbom.components ?? []) {
79
+ const licences = (composant.licenses ?? [])
80
+ .map((entree) => entree.expression ?? entree.license?.id ?? entree.license?.name)
81
+ .filter((licence) => typeof licence === 'string' && licence.trim() !== '');
82
+ const nom =
83
+ `${composant.group ? `${String(composant.group)}/` : ''}` +
84
+ `${String(composant.name)}@${String(composant.version)}`;
85
+ if (licences.length === 0) refus.push(`${nom} : licence inconnue`);
86
+ else if (licences.some((licence) => LICENCE_INTERDITE.test(licence))) {
87
+ refus.push(`${nom} : licence interdite (${licences.join(', ')})`);
88
+ }
89
+ }
90
+ return refus;
91
+ }
92
+
93
+ /** `pnpm` en sortie capturee ; sous Windows, un script `.cmd`, d'ou l'interpreteur. */
94
+ function pnpm(arguments_, racine) {
95
+ const resultat = spawnSync('pnpm', arguments_, {
96
+ cwd: racine,
97
+ encoding: 'utf8',
98
+ maxBuffer: 64 * 1024 * 1024,
99
+ stdio: ['ignore', 'pipe', 'pipe'],
100
+ shell: process.platform === 'win32',
101
+ });
102
+ return {
103
+ code: resultat.status ?? 2,
104
+ stdout: resultat.stdout ?? '',
105
+ stderr: resultat.stderr ?? '',
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Audit, SBOM et licences d'un projet pnpm. Rend `{ defauts, exceptes, composants, sbom }` ;
111
+ * `sbom` est le chemin ecrit. Leve une erreur quand le registre ne repond pas (on refuse plutot
112
+ * que supposer).
113
+ */
114
+ export function auditerLesDependances(racine, { lancer = pnpm, aujourdHui = new Date() } = {}) {
115
+ const chemin = join(racine, 'securite/exceptions-npm.json');
116
+ const registre = existsSync(chemin) ? JSON.parse(readFileSync(chemin, 'utf8')) : {};
117
+ const { valables, defauts } = lireExceptions(registre, aujourdHui);
118
+
119
+ // `pnpm audit` rend 1 quand il trouve des avis : seule une sortie illisible empeche de juger.
120
+ const audit = lancer(['audit', '--prod', '--json'], racine);
121
+ let rapport;
122
+ try {
123
+ rapport = JSON.parse(audit.stdout);
124
+ } catch {
125
+ throw new Error(`pnpm audit illisible (registre injoignable ?) : ${audit.stderr.trim()}`);
126
+ }
127
+ const { bloquants, exceptes } = trierLesAvis(rapport, valables);
128
+ defauts.push(...bloquants.map((ligne) => `avis non excepte : ${ligne}`));
129
+
130
+ const sbom = lancer(['sbom', '--sbom-format', 'cyclonedx', '--prod'], racine);
131
+ if (sbom.code !== 0) throw new Error(`pnpm sbom a echoue : ${sbom.stderr.trim()}`);
132
+ const document = JSON.parse(sbom.stdout);
133
+ mkdirSync(join(racine, 'dist'), { recursive: true });
134
+ const sortie = join(racine, 'dist/sbom-front.cdx.json');
135
+ writeFileSync(sortie, `${JSON.stringify(document, null, 2)}\n`);
136
+ defauts.push(...licencesRefusees(document));
137
+
138
+ return { defauts, exceptes, composants: document.components?.length ?? 0, sbom: sortie };
139
+ }