@saulwade/swl-ses 2.3.0 → 2.3.1
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/CLAUDE.md +47 -6
- package/README.md +1 -1
- package/agentes/accesibilidad-wcag-swl.md +690 -690
- package/agentes/arquitecto-swl.md +267 -267
- package/agentes/auto-evolucion-swl.md +908 -908
- package/agentes/backend-api-swl.md +472 -472
- package/agentes/backend-csharp-swl.md +420 -420
- package/agentes/backend-go-swl.md +390 -390
- package/agentes/backend-java-swl.md +281 -281
- package/agentes/backend-node-swl.md +479 -479
- package/agentes/backend-python-swl.md +605 -605
- package/agentes/backend-rust-swl.md +364 -364
- package/agentes/backend-workers-swl.md +482 -482
- package/agentes/cloud-infra-swl.md +509 -509
- package/agentes/consolidador-swl.md +541 -541
- package/agentes/datos-swl.md +605 -605
- package/agentes/depurador-swl.md +352 -352
- package/agentes/devops-ci-swl.md +400 -400
- package/agentes/disenador-ui-swl.md +569 -569
- package/agentes/documentador-swl.md +345 -345
- package/agentes/frontend-angular-swl.md +621 -621
- package/agentes/frontend-css-swl.md +716 -716
- package/agentes/frontend-react-swl.md +692 -692
- package/agentes/frontend-swl.md +496 -496
- package/agentes/frontend-tailwind-swl.md +826 -826
- package/agentes/gh-fix-ci-swl.md +2 -2
- package/agentes/implementador-swl.md +328 -328
- package/agentes/investigador-swl.md +432 -432
- package/agentes/investigador-ux-swl.md +505 -505
- package/agentes/llm-apps-swl.md +278 -278
- package/agentes/migrador-swl.md +442 -442
- package/agentes/mobile-android-swl.md +511 -511
- package/agentes/mobile-cross-swl.md +541 -541
- package/agentes/mobile-ios-swl.md +502 -502
- package/agentes/mobile-testing-swl.md +302 -302
- package/agentes/nemesis-auditor-swl.md +285 -285
- package/agentes/notificador-swl.md +918 -918
- package/agentes/observabilidad-swl.md +438 -438
- package/agentes/orquestador-swl.md +1026 -1026
- package/agentes/pagos-swl.md +310 -310
- package/agentes/perfilador-usuario-swl.md +321 -321
- package/agentes/planificador-swl.md +399 -399
- package/agentes/producto-prd-swl.md +589 -589
- package/agentes/red-team-swl.md +1 -1
- package/agentes/release-manager-swl.md +590 -590
- package/agentes/rendimiento-swl.md +713 -713
- package/agentes/resolutor-build-swl.md +245 -245
- package/agentes/revisor-angular-swl.md +278 -278
- package/agentes/revisor-codigo-swl.md +505 -505
- package/agentes/revisor-csharp-swl.md +264 -264
- package/agentes/revisor-go-swl.md +259 -259
- package/agentes/revisor-java-swl.md +257 -257
- package/agentes/revisor-kotlin-swl.md +273 -273
- package/agentes/revisor-nextjs-swl.md +281 -281
- package/agentes/revisor-php-swl.md +271 -271
- package/agentes/revisor-react-swl.md +278 -278
- package/agentes/revisor-rust-swl.md +346 -346
- package/agentes/revisor-seguridad-swl.md +399 -399
- package/agentes/revisor-swift-swl.md +268 -268
- package/agentes/revisor-typescript-swl.md +346 -346
- package/agentes/sre-swl.md +291 -291
- package/agentes/tdd-qa-swl.md +393 -393
- package/comandos/swl/briefing.md +119 -119
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/predecir.md +139 -139
- package/comandos/swl/sesiones.md +1 -1
- package/gateway/agent-executor.js +1 -1
- package/gateway/lib/event-channel.js +191 -191
- package/habilidades/agent-deep-links/SKILL.md +148 -148
- package/habilidades/agentes-como-servicio/SKILL.md +2 -2
- package/habilidades/angular-moderno/SKILL.md +20 -1
- package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
- package/habilidades/backend-async-postgres-testing/SKILL.md +215 -215
- package/habilidades/backend-error-design/SKILL.md +221 -221
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/benchmark-memoria/SKILL.md +186 -186
- package/habilidades/calidad-anti-patrones-universales/SKILL.md +368 -368
- package/habilidades/calidad-contract-testing/SKILL.md +165 -165
- package/habilidades/calidad-mutation-testing/SKILL.md +170 -170
- package/habilidades/changelog-generator/scripts/parse-commits.js +367 -367
- package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -60
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/drift-detection/SKILL.md +179 -179
- package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
- package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +1 -1
- package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -57
- package/habilidades/eval-framework/SKILL.md +212 -212
- package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
- package/habilidades/harness-claude-code/recursos/disciplina-harness-regla.md +252 -252
- package/habilidades/legacy-code-rescue/SKILL.md +267 -267
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/perfil-usuario/SKILL.md +200 -200
- package/habilidades/prevencion-sobreingenieria/recursos/EXAMPLES.md +580 -580
- package/habilidades/proceso-ddia-fundamentos/SKILL.md +255 -255
- package/habilidades/proceso-ddia-streaming/SKILL.md +231 -231
- package/habilidades/proceso-debate-adversarial/recursos/personas.md +105 -105
- package/habilidades/proceso-discovery-machote/SKILL.md +157 -157
- package/habilidades/proceso-dynamic-workflows/SKILL.md +138 -138
- package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -65
- package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -65
- package/habilidades/proceso-intent-engineering/SKILL.md +269 -269
- package/habilidades/proceso-modular-split/SKILL.md +256 -256
- package/habilidades/prompt-engineering/recursos/patrones-avanzados.md +2 -2
- package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
- package/habilidades/structured-outputs/SKILL.md +2 -2
- package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -83
- package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -85
- package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -94
- package/habilidades/swl-dashboard/SKILL.md +2 -2
- package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -111
- package/habilidades/validacion-ci-sistema/recursos/validadores-completos.md +1 -1
- package/habilidades/validacion-ci-sistema/scripts/validar-sistema.sh +1 -1
- package/hooks/ciclo-evolucion-subagente.js +26 -26
- package/hooks/ciclo-evolucion.js +26 -26
- package/hooks/claudemd-bloat-detector.js +161 -161
- package/hooks/claudemd-duplicacion-detector.js +170 -170
- package/hooks/contexto-iteracion.js +144 -144
- package/hooks/guardrail-modelo.js +1 -1
- package/hooks/lib/agent-routing.js +107 -107
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/autonomia.js +208 -208
- package/hooks/lib/ciclo-evolucion.js +47 -47
- package/hooks/lib/deep-links.js +185 -185
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/etapa-auto-evolucion.js +701 -701
- package/hooks/lib/etapa-metricas.js +388 -388
- package/hooks/lib/etapa-perfil-usuario.js +376 -376
- package/hooks/lib/loop-telemetry.js +321 -321
- package/hooks/lib/merkle-audit.js +96 -96
- package/hooks/lib/model-router.js +2 -2
- package/hooks/lib/propose-step.js +358 -358
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/rate-limit-tracker.js +253 -253
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/security-net.js +201 -201
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/token-estimator.js +1 -0
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/lib/usage-model.js +1 -1
- package/hooks/linea-estado.js +2 -0
- package/hooks/registro-turnos.js +209 -209
- package/hooks/session-briefing.js +98 -98
- package/hooks/spec-gate.js +211 -211
- package/hooks/sugerir-regenerar-inventario.js +170 -170
- package/hooks/tdd-gate.js +241 -241
- package/hooks/telemetria-skill-routing.js +100 -100
- package/hooks/validar-formato-post-subagente.js +140 -140
- package/hooks/validar-intent-spec.js +242 -242
- package/hooks/validar-memoria-hook.js +218 -218
- package/hooks/validar-planning-paths.js +134 -134
- package/instintos/autonomia.yaml +27 -27
- package/instintos/prompt-appendices.yaml +57 -57
- package/llms.txt +29 -29
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/canonical-hashes.json +2291 -1964
- package/manifiestos/planning-paths.json +44 -44
- package/manifiestos/skills-lock.json +1282 -1282
- package/package.json +1 -1
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/README.md +47 -47
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +1 -1
- package/reglas/accesibilidad.md +10 -10
- package/reglas/analisis-previo-tareas-grandes.md +172 -172
- package/reglas/api-diseno.md +9 -9
- package/reglas/arreglar-al-detectar.md +240 -240
- package/reglas/auditorias-documentales-estructurales.md +7 -7
- package/reglas/cloud-infra.md +8 -8
- package/reglas/consultar-vault-primero.md +195 -195
- package/reglas/debatir-antes-de-aceptar.md +158 -158
- package/reglas/fragmentos-compartidos.md +183 -183
- package/reglas/hooks.md +6 -6
- package/reglas/intent-engineering.md +218 -218
- package/reglas/markitdown.md +8 -8
- package/reglas/monitor-ci.md +309 -309
- package/reglas/patrones.md +6 -6
- package/reglas/sesiones-paralelas.md +180 -180
- package/reglas/sin-duplicacion-reglas-globales.md +182 -182
- package/reglas/skills-estandar.md +6 -6
- package/reglas/testing.md +7 -7
- package/reglas/tests-cleanup.md +224 -224
- package/reglas/usar-code-review-graph.md +155 -155
- package/reglas/usar-context7.md +226 -226
- package/reglas/verificar-citas-normativas.md +548 -548
- package/schemas/agent-frontmatter.schema.json +3 -3
- package/schemas/agent-message.schema.json +73 -73
- package/schemas/agent-output-implementacion.schema.json +114 -114
- package/schemas/agent-output-planificacion.schema.json +150 -150
- package/schemas/agent-output-review.schema.json +98 -98
- package/schemas/diary-entry.schema.json +112 -112
- package/schemas/hook-profiles.schema.json +54 -54
- package/schemas/hooks-config.schema.json +89 -89
- package/schemas/modulos.schema.json +38 -38
- package/schemas/perfiles.schema.json +36 -36
- package/schemas/plugin.schema.json +77 -77
- package/schemas/skill-evals.schema.json +119 -119
- package/schemas/skill-frontmatter.schema.json +245 -245
- package/scripts/audit-tools/audit-history.js +330 -330
- package/scripts/audit-tools/bundle-tracker.js +290 -290
- package/scripts/audit-tools/canary-monitor.js +352 -352
- package/scripts/audit-tools/code-profiler.js +605 -605
- package/scripts/audit-tools/dep-doctor.js +320 -320
- package/scripts/audit-tools/env-validator.js +206 -206
- package/scripts/audit-tools/lib/fs-walk.js +48 -48
- package/scripts/audit-tools/lib/output.js +23 -23
- package/scripts/audit-tools/migration-checker.js +392 -392
- package/scripts/audit-tools/pentest-scanner.js +1436 -1436
- package/scripts/benchmark-memoria.js +167 -167
- package/scripts/cli/aprobar-plan.js +73 -73
- package/scripts/cli/briefing.js +23 -23
- package/scripts/cli/ciclo-evolucion.js +26 -26
- package/scripts/cli/configurar-ci.js +40 -40
- package/scripts/cli/derivar-feature-list.js +25 -25
- package/scripts/cli/detectar-host.js +27 -27
- package/scripts/cli/diary-entry.js +69 -69
- package/scripts/cli/execution-state.js +18 -18
- package/scripts/cli/gateway-notify.js +41 -41
- package/scripts/cli/liberar-fase.js +42 -42
- package/scripts/cli/loop-telemetry.js +125 -125
- package/scripts/cli/mark-evolved.js +56 -56
- package/scripts/cli/metricas-dora.js +26 -26
- package/scripts/cli/near-duplicate.js +55 -55
- package/scripts/cli/notificaciones.js +123 -123
- package/scripts/cli/propose-step.js +29 -29
- package/scripts/cli/schedule-parse.js +19 -19
- package/scripts/cli/sugerir-modelo.js +20 -20
- package/scripts/cli/verificar-plan.js +36 -36
- package/scripts/cli/verificar-trazabilidad.js +35 -35
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/field-report.js +199 -199
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-matriz-lenguajes.js +271 -271
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/auditar-invocaciones-comandos.js +104 -104
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/ci-reader.js +193 -193
- package/scripts/lib/claude-sessions.js +1 -0
- package/scripts/lib/configurar-ci.js +380 -380
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-host-swl.js +175 -175
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/detector-autoduplicacion-intra-archivo.js +234 -234
- package/scripts/lib/detector-reglas-duplicadas.js +220 -220
- package/scripts/lib/diary-entry.js +234 -234
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/evidencia-release.js +322 -322
- package/scripts/lib/expandir-targets.js +71 -71
- package/scripts/lib/gate-hooks-requires.js +249 -249
- package/scripts/lib/gate-licencias.js +212 -212
- package/scripts/lib/git-metricas.js +257 -257
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/metricas-dora.js +204 -204
- package/scripts/lib/npm-version.js +261 -261
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/plan-lock.js +275 -275
- package/scripts/lib/pr-analyzer.js +399 -399
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/reglas-globales-conocidas.json +180 -180
- package/scripts/lib/resolver-plan-fase.js +37 -37
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/schema-version.js +164 -164
- package/scripts/lib/scoring-instintos.js +277 -277
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/lib/toml-merge.js +204 -204
- package/scripts/lib/tool-cost-analyzer.js +1 -0
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/mcp-server/auth.js +105 -105
- package/scripts/mcp-server/cache.js +106 -106
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +200 -200
- package/scripts/publicar.js +497 -497
- package/scripts/run-eval.js +141 -141
- package/scripts/tui/componentes/selector-multi.js +189 -189
- package/scripts/tui/componentes/selector-unico.js +158 -158
- package/scripts/tui/ejecutores.js +375 -375
- package/scripts/tui/index.js +162 -162
- package/scripts/tui/lib/colores.js +129 -129
- package/scripts/tui/lib/render.js +264 -264
- package/scripts/tui/lib/teclas.js +113 -113
- package/scripts/tui/pantallas/inspect.js +173 -173
- package/scripts/tui/pantallas/install-wizard.js +334 -334
- package/scripts/tui/pantallas/menu-principal.js +52 -52
- package/scripts/tui/pantallas/progreso.js +274 -274
- package/scripts/tui/pantallas/resumen.js +132 -132
- package/scripts/tui/pantallas/uninstall-wizard.js +208 -208
- package/scripts/tui/pantallas/update-wizard.js +232 -232
- package/scripts/tui/pantallas/welcome.js +187 -187
- package/scripts/validar-userland-vacio.js +110 -110
|
@@ -1,322 +1,322 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* evidencia-release.js — Evidencia de procedencia del release (Fase 14, ADR-0038).
|
|
5
|
-
*
|
|
6
|
-
* Genera, como paso de `/swl:release`, los artefactos de cadena de suministro:
|
|
7
|
-
* 1. SBOM CycloneDX del árbol runtime (`npm sbom --sbom-format cyclonedx --omit dev`).
|
|
8
|
-
* 2. SHA256SUMS del tarball producido por `npm pack`.
|
|
9
|
-
* Ambos se escriben en `releases/vX.Y.Z/` (evidencia versionada en el repo, fuera
|
|
10
|
-
* del tarball npm — `package.json#files` es allowlist y no incluye `releases/`).
|
|
11
|
-
*
|
|
12
|
-
* Diseño: la parte PURA (nombres, formato, sección markdown, validación semver)
|
|
13
|
-
* se separa de la parte de SUBPROCESO (npm), inyectable vía `opts.ejecutarNpm`
|
|
14
|
-
* para tests sin tocar npm real.
|
|
15
|
-
*
|
|
16
|
-
* Subproceso npm cross-platform: se invoca `node <npm-cli.js> ...` (no `npm.cmd`)
|
|
17
|
-
* con execFileSync sin shell — patrón de scripts/lib/npm-version.js (evita la
|
|
18
|
-
* diferencia .cmd/POSIX y DEP0190 en Node 20+).
|
|
19
|
-
*
|
|
20
|
-
* Zero-dependencies. CLI: `node scripts/lib/evidencia-release.js --version X.Y.Z`.
|
|
21
|
-
*
|
|
22
|
-
* Origen: Fase 14 — cadena de suministro en release (P4, ADR-0038).
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
const fs = require('fs');
|
|
26
|
-
const os = require('os');
|
|
27
|
-
const path = require('path');
|
|
28
|
-
const crypto = require('crypto');
|
|
29
|
-
const { execFileSync } = require('child_process');
|
|
30
|
-
|
|
31
|
-
// Escritura atómica con fallback defensivo idéntico a plan-lock.js (los archivos
|
|
32
|
-
// de evidencia son write-once por release, pero respetamos la convención del repo).
|
|
33
|
-
let atomicWriteSync;
|
|
34
|
-
try {
|
|
35
|
-
({ atomicWriteSync } = require(path.join(__dirname, '..', '..', 'hooks', 'lib', 'atomic-write.js')));
|
|
36
|
-
} catch (err) {
|
|
37
|
-
// Solo degradar a writeFileSync si el módulo no existe (destino aplanado).
|
|
38
|
-
// Otros errores (permisos, disco) deben propagarse, no enmascararse.
|
|
39
|
-
if (err.code !== 'MODULE_NOT_FOUND') throw err;
|
|
40
|
-
atomicWriteSync = (p, c, e) => {
|
|
41
|
-
const dir = path.dirname(p);
|
|
42
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
43
|
-
fs.writeFileSync(p, c, e);
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Semver estricto (core + prerelease + build), suficiente para validar la versión
|
|
48
|
-
// del release sin dependencias. No valida rangos — solo una versión concreta.
|
|
49
|
-
const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
|
|
50
|
-
const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
|
|
51
|
-
// Nombre de tarball seguro: lo que produce `npm pack` (scope-paquete-version.tgz).
|
|
52
|
-
// Rechaza newlines, espacios y control chars que corromperían SHA256SUMS.
|
|
53
|
-
const TARBALL_NOMBRE_RE = /^[A-Za-z0-9._@+-]+\.tgz$/;
|
|
54
|
-
// Cota de tamaño del SBOM antes de JSON.parse (defensa DoS por memoria).
|
|
55
|
-
const MAX_SBOM_BYTES = 32 * 1024 * 1024;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Localiza npm-cli.js de la instalación de Node para invocarlo vía `node`.
|
|
59
|
-
*
|
|
60
|
-
* Duplicación DELIBERADA del helper de scripts/lib/npm-version.js (~15 líneas).
|
|
61
|
-
* NO se importa de allí a propósito: `npm-version.js` NO se distribuye al destino
|
|
62
|
-
* (no está en ningún módulo de modulos.json), mientras esta lib SÍ viaja en
|
|
63
|
-
* `comandos-core`. Un `require('./npm-version')` rompería con MODULE_NOT_FOUND en
|
|
64
|
-
* el destino aplanado. La autocontención es el requisito de distribución, no una
|
|
65
|
-
* omisión — acoplar las dos libs introduciría fragilidad de runtime
|
|
66
|
-
* (arquitectura.md § "mantener código similar separado cuando la abstracción
|
|
67
|
-
* compartida sería vaga"). Si se agrega un candidato de ruta nuevo, replicarlo
|
|
68
|
-
* en ambas.
|
|
69
|
-
*
|
|
70
|
-
* @returns {string|null}
|
|
71
|
-
*/
|
|
72
|
-
function localizarNpmCli() {
|
|
73
|
-
const dir = path.dirname(process.execPath);
|
|
74
|
-
const candidatos = [
|
|
75
|
-
path.join(dir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
76
|
-
path.join(dir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
77
|
-
path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
78
|
-
];
|
|
79
|
-
for (const c of candidatos) {
|
|
80
|
-
try {
|
|
81
|
-
if (fs.existsSync(c)) return c;
|
|
82
|
-
} catch { /* continuar */ }
|
|
83
|
-
}
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Valida que `version` sea semver estricto; lanza si no.
|
|
89
|
-
* @param {string} version
|
|
90
|
-
*/
|
|
91
|
-
function assertSemver(version) {
|
|
92
|
-
if (typeof version !== 'string' || !SEMVER_RE.test(version)) {
|
|
93
|
-
throw new Error(`versión inválida (no es semver): ${JSON.stringify(version)}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Nombres de los artefactos de evidencia para una versión dada.
|
|
99
|
-
* @param {string} version semver, ej. "2.0.0"
|
|
100
|
-
* @returns {{dir: string, sbom: string, sums: string}}
|
|
101
|
-
*/
|
|
102
|
-
function nombreArtefactos(version) {
|
|
103
|
-
assertSemver(version);
|
|
104
|
-
return {
|
|
105
|
-
dir: `releases/v${version}`,
|
|
106
|
-
sbom: `sbom-v${version}.cdx.json`,
|
|
107
|
-
sums: 'SHA256SUMS',
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Una línea de SHA256SUMS en formato `sha256sum -c` (hash + DOS espacios + nombre).
|
|
113
|
-
* @param {string} hash sha256 hex de 64 chars minúsculas
|
|
114
|
-
* @param {string} archivo nombre del archivo (tarball)
|
|
115
|
-
* @returns {string}
|
|
116
|
-
*/
|
|
117
|
-
function formatearSums(hash, archivo) {
|
|
118
|
-
if (typeof hash !== 'string' || !SHA256_HEX_RE.test(hash)) {
|
|
119
|
-
throw new Error(`hash inválido (se esperaba sha256 hex de 64 chars): ${JSON.stringify(hash)}`);
|
|
120
|
-
}
|
|
121
|
-
if (typeof archivo !== 'string' || !TARBALL_NOMBRE_RE.test(archivo)) {
|
|
122
|
-
// Evita que un nombre con newlines/espacios corrompa el formato sha256sum -c.
|
|
123
|
-
throw new Error(`nombre de tarball inválido: ${JSON.stringify(archivo)}`);
|
|
124
|
-
}
|
|
125
|
-
return `${hash} ${archivo}`;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Sección markdown "Integridad y verificación" para insertar en RELEASE-NOTES.
|
|
130
|
-
* @param {{version: string, hash: string, tarball: string}} datos
|
|
131
|
-
* @returns {string}
|
|
132
|
-
*/
|
|
133
|
-
function seccionVerificacionNotas({ version, hash, tarball }) {
|
|
134
|
-
assertSemver(version);
|
|
135
|
-
if (!SHA256_HEX_RE.test(hash)) {
|
|
136
|
-
throw new Error(`hash inválido en seccionVerificacionNotas: ${JSON.stringify(hash)}`);
|
|
137
|
-
}
|
|
138
|
-
const n = nombreArtefactos(version);
|
|
139
|
-
return [
|
|
140
|
-
'## Integridad y verificación',
|
|
141
|
-
'',
|
|
142
|
-
'Evidencia de procedencia versionada en `' + n.dir + '/`:',
|
|
143
|
-
'',
|
|
144
|
-
'- **SBOM** (CycloneDX): `' + n.sbom + '`',
|
|
145
|
-
'- **Checksums**: `SHA256SUMS`',
|
|
146
|
-
'',
|
|
147
|
-
'SHA256 del tarball publicado:',
|
|
148
|
-
'',
|
|
149
|
-
'```',
|
|
150
|
-
formatearSums(hash, tarball),
|
|
151
|
-
'```',
|
|
152
|
-
'',
|
|
153
|
-
'Verificar la integridad tras descargar el paquete:',
|
|
154
|
-
'',
|
|
155
|
-
'```bash',
|
|
156
|
-
`# Bash`,
|
|
157
|
-
`npm pack @saulwade/swl-ses@${version}`,
|
|
158
|
-
`sha256sum -c ${n.dir}/SHA256SUMS`,
|
|
159
|
-
'```',
|
|
160
|
-
'',
|
|
161
|
-
'```powershell',
|
|
162
|
-
`# PowerShell`,
|
|
163
|
-
`(Get-FileHash ${tarball} -Algorithm SHA256).Hash.ToLower()`,
|
|
164
|
-
`# debe coincidir con el hash de arriba`,
|
|
165
|
-
'```',
|
|
166
|
-
'',
|
|
167
|
-
'Detalle completo en `docs/verificacion-consumidor.md`.',
|
|
168
|
-
'',
|
|
169
|
-
].join('\n');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Ejecutor npm real (default). Inyectable en tests vía opts.ejecutarNpm.
|
|
174
|
-
* @returns {{sbom: () => string, pack: (destino: string) => string}}
|
|
175
|
-
*/
|
|
176
|
-
function ejecutorNpmReal() {
|
|
177
|
-
const npmCli = localizarNpmCli();
|
|
178
|
-
if (!npmCli) {
|
|
179
|
-
throw new Error(
|
|
180
|
-
'no se encontró npm-cli.js de la instalación de Node; ' +
|
|
181
|
-
'evidencia-release requiere npm para generar SBOM y empacar el tarball'
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
const correr = (args, opts = {}) =>
|
|
185
|
-
execFileSync(process.execPath, [npmCli, ...args], {
|
|
186
|
-
encoding: 'utf8',
|
|
187
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
188
|
-
...opts,
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
sbom() {
|
|
193
|
-
return correr(['sbom', '--sbom-format', 'cyclonedx', '--omit', 'dev']);
|
|
194
|
-
},
|
|
195
|
-
pack(destino) {
|
|
196
|
-
fs.mkdirSync(destino, { recursive: true });
|
|
197
|
-
// NO usar `npm pack --json`: los scripts de ciclo de vida (prepack)
|
|
198
|
-
// escriben a stdout y contaminan el JSON. En su lugar empacamos a un
|
|
199
|
-
// directorio limpio y leemos el único .tgz resultante — robusto ante
|
|
200
|
-
// cualquier salida de prepack/postpack.
|
|
201
|
-
correr(['pack', '--pack-destination', destino]);
|
|
202
|
-
const tgz = fs.readdirSync(destino).filter((f) => f.endsWith('.tgz'));
|
|
203
|
-
if (tgz.length === 0) {
|
|
204
|
-
throw new Error(`npm pack no produjo ningún .tgz en ${destino}`);
|
|
205
|
-
}
|
|
206
|
-
if (tgz.length > 1) {
|
|
207
|
-
// El destino es temporal y exclusivo por invocación (pid+timestamp);
|
|
208
|
-
// más de un tarball indica un destino reutilizado — usar el más reciente.
|
|
209
|
-
tgz.sort((a, b) =>
|
|
210
|
-
fs.statSync(path.join(destino, b)).mtimeMs - fs.statSync(path.join(destino, a)).mtimeMs
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
return path.join(destino, tgz[0]);
|
|
214
|
-
},
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Valida y persiste el SBOM. Rechaza salidas no-CycloneDX o demasiado grandes.
|
|
220
|
-
* @param {string} dir directorio releases/vX.Y.Z
|
|
221
|
-
* @param {string} sbomTexto salida cruda de `npm sbom`
|
|
222
|
-
* @param {string} nombre nombre del archivo SBOM
|
|
223
|
-
* @returns {string} ruta del SBOM escrito
|
|
224
|
-
*/
|
|
225
|
-
function persistirSbom(dir, sbomTexto, nombre) {
|
|
226
|
-
if (typeof sbomTexto !== 'string' || sbomTexto.length > MAX_SBOM_BYTES) {
|
|
227
|
-
throw new Error(`SBOM ausente o excede ${MAX_SBOM_BYTES} bytes`);
|
|
228
|
-
}
|
|
229
|
-
const sbomObj = JSON.parse(sbomTexto);
|
|
230
|
-
if (!sbomObj || sbomObj.bomFormat !== 'CycloneDX') {
|
|
231
|
-
throw new Error(`SBOM no es CycloneDX (bomFormat: ${sbomObj && sbomObj.bomFormat})`);
|
|
232
|
-
}
|
|
233
|
-
const sbomPath = path.join(dir, nombre);
|
|
234
|
-
atomicWriteSync(sbomPath, `${JSON.stringify(sbomObj, null, 2)}\n`, 'utf8');
|
|
235
|
-
return sbomPath;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Empaca el tarball en un dir temporal, calcula su sha256 y limpia el temporal.
|
|
240
|
-
* @param {object} npm ejecutor con pack(destino)
|
|
241
|
-
* @returns {{hash: string, tarball: string}}
|
|
242
|
-
*/
|
|
243
|
-
function calcularChecksumTarball(npm) {
|
|
244
|
-
const packTmp = path.join(os.tmpdir(), `swl-pack-${process.pid}-${Date.now()}`);
|
|
245
|
-
try {
|
|
246
|
-
const tarballPath = npm.pack(packTmp);
|
|
247
|
-
const buffer = fs.readFileSync(tarballPath);
|
|
248
|
-
const hash = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
249
|
-
return { hash, tarball: path.basename(tarballPath) };
|
|
250
|
-
} finally {
|
|
251
|
-
try {
|
|
252
|
-
fs.rmSync(packTmp, { recursive: true, force: true });
|
|
253
|
-
} catch { /* best-effort */ }
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Genera la evidencia de procedencia para `version` bajo `baseDir`.
|
|
259
|
-
* Escribe releases/vX.Y.Z/{sbom-vX.Y.Z.cdx.json, SHA256SUMS}.
|
|
260
|
-
*
|
|
261
|
-
* @param {string} baseDir raíz del proyecto donde se escribe releases/
|
|
262
|
-
* @param {string} version semver del release
|
|
263
|
-
* @param {object} [opts]
|
|
264
|
-
* @param {object} [opts.ejecutarNpm] ejecutor {sbom(), pack(destino)} inyectable
|
|
265
|
-
* @returns {{hash: string, tarball: string, dir: string, sbomPath: string, sumsPath: string}}
|
|
266
|
-
*/
|
|
267
|
-
function generarEvidencia(baseDir, version, opts = {}) {
|
|
268
|
-
assertSemver(version); // valida ANTES de invocar npm
|
|
269
|
-
const npm = opts.ejecutarNpm || ejecutorNpmReal();
|
|
270
|
-
const n = nombreArtefactos(version);
|
|
271
|
-
const dir = path.join(baseDir, n.dir);
|
|
272
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
273
|
-
|
|
274
|
-
const sbomPath = persistirSbom(dir, npm.sbom(), n.sbom);
|
|
275
|
-
const { hash, tarball } = calcularChecksumTarball(npm);
|
|
276
|
-
const sumsPath = path.join(dir, n.sums);
|
|
277
|
-
atomicWriteSync(sumsPath, `${formatearSums(hash, tarball)}\n`, 'utf8');
|
|
278
|
-
|
|
279
|
-
return { hash, tarball, dir, sbomPath, sumsPath };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// --- CLI ---------------------------------------------------------------------
|
|
283
|
-
|
|
284
|
-
function parseArgs(argv) {
|
|
285
|
-
const out = { version: null };
|
|
286
|
-
for (let i = 0; i < argv.length; i++) {
|
|
287
|
-
if (argv[i] === '--version') {
|
|
288
|
-
out.version = argv[i + 1];
|
|
289
|
-
i++;
|
|
290
|
-
} else if (argv[i].startsWith('--version=')) {
|
|
291
|
-
out.version = argv[i].slice('--version='.length);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
return out;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (require.main === module) {
|
|
298
|
-
const { version } = parseArgs(process.argv.slice(2));
|
|
299
|
-
if (!version) {
|
|
300
|
-
console.error('uso: node scripts/lib/evidencia-release.js --version X.Y.Z');
|
|
301
|
-
process.exit(2);
|
|
302
|
-
}
|
|
303
|
-
try {
|
|
304
|
-
const res = generarEvidencia(process.cwd(), version);
|
|
305
|
-
console.log(`Evidencia generada en ${path.relative(process.cwd(), res.dir)}/`);
|
|
306
|
-
console.log(` SBOM: ${path.basename(res.sbomPath)}`);
|
|
307
|
-
console.log(` SHA256SUMS: ${res.hash} ${res.tarball}`);
|
|
308
|
-
} catch (err) {
|
|
309
|
-
console.error(`error generando evidencia: ${err.message}`);
|
|
310
|
-
process.exit(1);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
module.exports = {
|
|
315
|
-
nombreArtefactos,
|
|
316
|
-
formatearSums,
|
|
317
|
-
seccionVerificacionNotas,
|
|
318
|
-
generarEvidencia,
|
|
319
|
-
ejecutorNpmReal,
|
|
320
|
-
localizarNpmCli,
|
|
321
|
-
SEMVER_RE,
|
|
322
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* evidencia-release.js — Evidencia de procedencia del release (Fase 14, ADR-0038).
|
|
5
|
+
*
|
|
6
|
+
* Genera, como paso de `/swl:release`, los artefactos de cadena de suministro:
|
|
7
|
+
* 1. SBOM CycloneDX del árbol runtime (`npm sbom --sbom-format cyclonedx --omit dev`).
|
|
8
|
+
* 2. SHA256SUMS del tarball producido por `npm pack`.
|
|
9
|
+
* Ambos se escriben en `releases/vX.Y.Z/` (evidencia versionada en el repo, fuera
|
|
10
|
+
* del tarball npm — `package.json#files` es allowlist y no incluye `releases/`).
|
|
11
|
+
*
|
|
12
|
+
* Diseño: la parte PURA (nombres, formato, sección markdown, validación semver)
|
|
13
|
+
* se separa de la parte de SUBPROCESO (npm), inyectable vía `opts.ejecutarNpm`
|
|
14
|
+
* para tests sin tocar npm real.
|
|
15
|
+
*
|
|
16
|
+
* Subproceso npm cross-platform: se invoca `node <npm-cli.js> ...` (no `npm.cmd`)
|
|
17
|
+
* con execFileSync sin shell — patrón de scripts/lib/npm-version.js (evita la
|
|
18
|
+
* diferencia .cmd/POSIX y DEP0190 en Node 20+).
|
|
19
|
+
*
|
|
20
|
+
* Zero-dependencies. CLI: `node scripts/lib/evidencia-release.js --version X.Y.Z`.
|
|
21
|
+
*
|
|
22
|
+
* Origen: Fase 14 — cadena de suministro en release (P4, ADR-0038).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const crypto = require('crypto');
|
|
29
|
+
const { execFileSync } = require('child_process');
|
|
30
|
+
|
|
31
|
+
// Escritura atómica con fallback defensivo idéntico a plan-lock.js (los archivos
|
|
32
|
+
// de evidencia son write-once por release, pero respetamos la convención del repo).
|
|
33
|
+
let atomicWriteSync;
|
|
34
|
+
try {
|
|
35
|
+
({ atomicWriteSync } = require(path.join(__dirname, '..', '..', 'hooks', 'lib', 'atomic-write.js')));
|
|
36
|
+
} catch (err) {
|
|
37
|
+
// Solo degradar a writeFileSync si el módulo no existe (destino aplanado).
|
|
38
|
+
// Otros errores (permisos, disco) deben propagarse, no enmascararse.
|
|
39
|
+
if (err.code !== 'MODULE_NOT_FOUND') throw err;
|
|
40
|
+
atomicWriteSync = (p, c, e) => {
|
|
41
|
+
const dir = path.dirname(p);
|
|
42
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
43
|
+
fs.writeFileSync(p, c, e);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Semver estricto (core + prerelease + build), suficiente para validar la versión
|
|
48
|
+
// del release sin dependencias. No valida rangos — solo una versión concreta.
|
|
49
|
+
const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
|
|
50
|
+
const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
|
|
51
|
+
// Nombre de tarball seguro: lo que produce `npm pack` (scope-paquete-version.tgz).
|
|
52
|
+
// Rechaza newlines, espacios y control chars que corromperían SHA256SUMS.
|
|
53
|
+
const TARBALL_NOMBRE_RE = /^[A-Za-z0-9._@+-]+\.tgz$/;
|
|
54
|
+
// Cota de tamaño del SBOM antes de JSON.parse (defensa DoS por memoria).
|
|
55
|
+
const MAX_SBOM_BYTES = 32 * 1024 * 1024;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Localiza npm-cli.js de la instalación de Node para invocarlo vía `node`.
|
|
59
|
+
*
|
|
60
|
+
* Duplicación DELIBERADA del helper de scripts/lib/npm-version.js (~15 líneas).
|
|
61
|
+
* NO se importa de allí a propósito: `npm-version.js` NO se distribuye al destino
|
|
62
|
+
* (no está en ningún módulo de modulos.json), mientras esta lib SÍ viaja en
|
|
63
|
+
* `comandos-core`. Un `require('./npm-version')` rompería con MODULE_NOT_FOUND en
|
|
64
|
+
* el destino aplanado. La autocontención es el requisito de distribución, no una
|
|
65
|
+
* omisión — acoplar las dos libs introduciría fragilidad de runtime
|
|
66
|
+
* (arquitectura.md § "mantener código similar separado cuando la abstracción
|
|
67
|
+
* compartida sería vaga"). Si se agrega un candidato de ruta nuevo, replicarlo
|
|
68
|
+
* en ambas.
|
|
69
|
+
*
|
|
70
|
+
* @returns {string|null}
|
|
71
|
+
*/
|
|
72
|
+
function localizarNpmCli() {
|
|
73
|
+
const dir = path.dirname(process.execPath);
|
|
74
|
+
const candidatos = [
|
|
75
|
+
path.join(dir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
76
|
+
path.join(dir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
77
|
+
path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
78
|
+
];
|
|
79
|
+
for (const c of candidatos) {
|
|
80
|
+
try {
|
|
81
|
+
if (fs.existsSync(c)) return c;
|
|
82
|
+
} catch { /* continuar */ }
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Valida que `version` sea semver estricto; lanza si no.
|
|
89
|
+
* @param {string} version
|
|
90
|
+
*/
|
|
91
|
+
function assertSemver(version) {
|
|
92
|
+
if (typeof version !== 'string' || !SEMVER_RE.test(version)) {
|
|
93
|
+
throw new Error(`versión inválida (no es semver): ${JSON.stringify(version)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Nombres de los artefactos de evidencia para una versión dada.
|
|
99
|
+
* @param {string} version semver, ej. "2.0.0"
|
|
100
|
+
* @returns {{dir: string, sbom: string, sums: string}}
|
|
101
|
+
*/
|
|
102
|
+
function nombreArtefactos(version) {
|
|
103
|
+
assertSemver(version);
|
|
104
|
+
return {
|
|
105
|
+
dir: `releases/v${version}`,
|
|
106
|
+
sbom: `sbom-v${version}.cdx.json`,
|
|
107
|
+
sums: 'SHA256SUMS',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Una línea de SHA256SUMS en formato `sha256sum -c` (hash + DOS espacios + nombre).
|
|
113
|
+
* @param {string} hash sha256 hex de 64 chars minúsculas
|
|
114
|
+
* @param {string} archivo nombre del archivo (tarball)
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
function formatearSums(hash, archivo) {
|
|
118
|
+
if (typeof hash !== 'string' || !SHA256_HEX_RE.test(hash)) {
|
|
119
|
+
throw new Error(`hash inválido (se esperaba sha256 hex de 64 chars): ${JSON.stringify(hash)}`);
|
|
120
|
+
}
|
|
121
|
+
if (typeof archivo !== 'string' || !TARBALL_NOMBRE_RE.test(archivo)) {
|
|
122
|
+
// Evita que un nombre con newlines/espacios corrompa el formato sha256sum -c.
|
|
123
|
+
throw new Error(`nombre de tarball inválido: ${JSON.stringify(archivo)}`);
|
|
124
|
+
}
|
|
125
|
+
return `${hash} ${archivo}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Sección markdown "Integridad y verificación" para insertar en RELEASE-NOTES.
|
|
130
|
+
* @param {{version: string, hash: string, tarball: string}} datos
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
function seccionVerificacionNotas({ version, hash, tarball }) {
|
|
134
|
+
assertSemver(version);
|
|
135
|
+
if (!SHA256_HEX_RE.test(hash)) {
|
|
136
|
+
throw new Error(`hash inválido en seccionVerificacionNotas: ${JSON.stringify(hash)}`);
|
|
137
|
+
}
|
|
138
|
+
const n = nombreArtefactos(version);
|
|
139
|
+
return [
|
|
140
|
+
'## Integridad y verificación',
|
|
141
|
+
'',
|
|
142
|
+
'Evidencia de procedencia versionada en `' + n.dir + '/`:',
|
|
143
|
+
'',
|
|
144
|
+
'- **SBOM** (CycloneDX): `' + n.sbom + '`',
|
|
145
|
+
'- **Checksums**: `SHA256SUMS`',
|
|
146
|
+
'',
|
|
147
|
+
'SHA256 del tarball publicado:',
|
|
148
|
+
'',
|
|
149
|
+
'```',
|
|
150
|
+
formatearSums(hash, tarball),
|
|
151
|
+
'```',
|
|
152
|
+
'',
|
|
153
|
+
'Verificar la integridad tras descargar el paquete:',
|
|
154
|
+
'',
|
|
155
|
+
'```bash',
|
|
156
|
+
`# Bash`,
|
|
157
|
+
`npm pack @saulwade/swl-ses@${version}`,
|
|
158
|
+
`sha256sum -c ${n.dir}/SHA256SUMS`,
|
|
159
|
+
'```',
|
|
160
|
+
'',
|
|
161
|
+
'```powershell',
|
|
162
|
+
`# PowerShell`,
|
|
163
|
+
`(Get-FileHash ${tarball} -Algorithm SHA256).Hash.ToLower()`,
|
|
164
|
+
`# debe coincidir con el hash de arriba`,
|
|
165
|
+
'```',
|
|
166
|
+
'',
|
|
167
|
+
'Detalle completo en `docs/verificacion-consumidor.md`.',
|
|
168
|
+
'',
|
|
169
|
+
].join('\n');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Ejecutor npm real (default). Inyectable en tests vía opts.ejecutarNpm.
|
|
174
|
+
* @returns {{sbom: () => string, pack: (destino: string) => string}}
|
|
175
|
+
*/
|
|
176
|
+
function ejecutorNpmReal() {
|
|
177
|
+
const npmCli = localizarNpmCli();
|
|
178
|
+
if (!npmCli) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
'no se encontró npm-cli.js de la instalación de Node; ' +
|
|
181
|
+
'evidencia-release requiere npm para generar SBOM y empacar el tarball'
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const correr = (args, opts = {}) =>
|
|
185
|
+
execFileSync(process.execPath, [npmCli, ...args], {
|
|
186
|
+
encoding: 'utf8',
|
|
187
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
188
|
+
...opts,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
sbom() {
|
|
193
|
+
return correr(['sbom', '--sbom-format', 'cyclonedx', '--omit', 'dev']);
|
|
194
|
+
},
|
|
195
|
+
pack(destino) {
|
|
196
|
+
fs.mkdirSync(destino, { recursive: true });
|
|
197
|
+
// NO usar `npm pack --json`: los scripts de ciclo de vida (prepack)
|
|
198
|
+
// escriben a stdout y contaminan el JSON. En su lugar empacamos a un
|
|
199
|
+
// directorio limpio y leemos el único .tgz resultante — robusto ante
|
|
200
|
+
// cualquier salida de prepack/postpack.
|
|
201
|
+
correr(['pack', '--pack-destination', destino]);
|
|
202
|
+
const tgz = fs.readdirSync(destino).filter((f) => f.endsWith('.tgz'));
|
|
203
|
+
if (tgz.length === 0) {
|
|
204
|
+
throw new Error(`npm pack no produjo ningún .tgz en ${destino}`);
|
|
205
|
+
}
|
|
206
|
+
if (tgz.length > 1) {
|
|
207
|
+
// El destino es temporal y exclusivo por invocación (pid+timestamp);
|
|
208
|
+
// más de un tarball indica un destino reutilizado — usar el más reciente.
|
|
209
|
+
tgz.sort((a, b) =>
|
|
210
|
+
fs.statSync(path.join(destino, b)).mtimeMs - fs.statSync(path.join(destino, a)).mtimeMs
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return path.join(destino, tgz[0]);
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Valida y persiste el SBOM. Rechaza salidas no-CycloneDX o demasiado grandes.
|
|
220
|
+
* @param {string} dir directorio releases/vX.Y.Z
|
|
221
|
+
* @param {string} sbomTexto salida cruda de `npm sbom`
|
|
222
|
+
* @param {string} nombre nombre del archivo SBOM
|
|
223
|
+
* @returns {string} ruta del SBOM escrito
|
|
224
|
+
*/
|
|
225
|
+
function persistirSbom(dir, sbomTexto, nombre) {
|
|
226
|
+
if (typeof sbomTexto !== 'string' || sbomTexto.length > MAX_SBOM_BYTES) {
|
|
227
|
+
throw new Error(`SBOM ausente o excede ${MAX_SBOM_BYTES} bytes`);
|
|
228
|
+
}
|
|
229
|
+
const sbomObj = JSON.parse(sbomTexto);
|
|
230
|
+
if (!sbomObj || sbomObj.bomFormat !== 'CycloneDX') {
|
|
231
|
+
throw new Error(`SBOM no es CycloneDX (bomFormat: ${sbomObj && sbomObj.bomFormat})`);
|
|
232
|
+
}
|
|
233
|
+
const sbomPath = path.join(dir, nombre);
|
|
234
|
+
atomicWriteSync(sbomPath, `${JSON.stringify(sbomObj, null, 2)}\n`, 'utf8');
|
|
235
|
+
return sbomPath;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Empaca el tarball en un dir temporal, calcula su sha256 y limpia el temporal.
|
|
240
|
+
* @param {object} npm ejecutor con pack(destino)
|
|
241
|
+
* @returns {{hash: string, tarball: string}}
|
|
242
|
+
*/
|
|
243
|
+
function calcularChecksumTarball(npm) {
|
|
244
|
+
const packTmp = path.join(os.tmpdir(), `swl-pack-${process.pid}-${Date.now()}`);
|
|
245
|
+
try {
|
|
246
|
+
const tarballPath = npm.pack(packTmp);
|
|
247
|
+
const buffer = fs.readFileSync(tarballPath);
|
|
248
|
+
const hash = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
249
|
+
return { hash, tarball: path.basename(tarballPath) };
|
|
250
|
+
} finally {
|
|
251
|
+
try {
|
|
252
|
+
fs.rmSync(packTmp, { recursive: true, force: true });
|
|
253
|
+
} catch { /* best-effort */ }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Genera la evidencia de procedencia para `version` bajo `baseDir`.
|
|
259
|
+
* Escribe releases/vX.Y.Z/{sbom-vX.Y.Z.cdx.json, SHA256SUMS}.
|
|
260
|
+
*
|
|
261
|
+
* @param {string} baseDir raíz del proyecto donde se escribe releases/
|
|
262
|
+
* @param {string} version semver del release
|
|
263
|
+
* @param {object} [opts]
|
|
264
|
+
* @param {object} [opts.ejecutarNpm] ejecutor {sbom(), pack(destino)} inyectable
|
|
265
|
+
* @returns {{hash: string, tarball: string, dir: string, sbomPath: string, sumsPath: string}}
|
|
266
|
+
*/
|
|
267
|
+
function generarEvidencia(baseDir, version, opts = {}) {
|
|
268
|
+
assertSemver(version); // valida ANTES de invocar npm
|
|
269
|
+
const npm = opts.ejecutarNpm || ejecutorNpmReal();
|
|
270
|
+
const n = nombreArtefactos(version);
|
|
271
|
+
const dir = path.join(baseDir, n.dir);
|
|
272
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
273
|
+
|
|
274
|
+
const sbomPath = persistirSbom(dir, npm.sbom(), n.sbom);
|
|
275
|
+
const { hash, tarball } = calcularChecksumTarball(npm);
|
|
276
|
+
const sumsPath = path.join(dir, n.sums);
|
|
277
|
+
atomicWriteSync(sumsPath, `${formatearSums(hash, tarball)}\n`, 'utf8');
|
|
278
|
+
|
|
279
|
+
return { hash, tarball, dir, sbomPath, sumsPath };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// --- CLI ---------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
function parseArgs(argv) {
|
|
285
|
+
const out = { version: null };
|
|
286
|
+
for (let i = 0; i < argv.length; i++) {
|
|
287
|
+
if (argv[i] === '--version') {
|
|
288
|
+
out.version = argv[i + 1];
|
|
289
|
+
i++;
|
|
290
|
+
} else if (argv[i].startsWith('--version=')) {
|
|
291
|
+
out.version = argv[i].slice('--version='.length);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (require.main === module) {
|
|
298
|
+
const { version } = parseArgs(process.argv.slice(2));
|
|
299
|
+
if (!version) {
|
|
300
|
+
console.error('uso: node scripts/lib/evidencia-release.js --version X.Y.Z');
|
|
301
|
+
process.exit(2);
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const res = generarEvidencia(process.cwd(), version);
|
|
305
|
+
console.log(`Evidencia generada en ${path.relative(process.cwd(), res.dir)}/`);
|
|
306
|
+
console.log(` SBOM: ${path.basename(res.sbomPath)}`);
|
|
307
|
+
console.log(` SHA256SUMS: ${res.hash} ${res.tarball}`);
|
|
308
|
+
} catch (err) {
|
|
309
|
+
console.error(`error generando evidencia: ${err.message}`);
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
module.exports = {
|
|
315
|
+
nombreArtefactos,
|
|
316
|
+
formatearSums,
|
|
317
|
+
seccionVerificacionNotas,
|
|
318
|
+
generarEvidencia,
|
|
319
|
+
ejecutorNpmReal,
|
|
320
|
+
localizarNpmCli,
|
|
321
|
+
SEMVER_RE,
|
|
322
|
+
};
|