@saulwade/swl-ses 1.3.7 → 1.4.0

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.
Files changed (129) hide show
  1. package/CLAUDE.md +12 -4
  2. package/README.md +1 -1
  3. package/bin/swl-mcp-server.js +187 -187
  4. package/bin/swl-webhook-server.js +198 -0
  5. package/comandos/swl/.evolved.json +22 -22
  6. package/comandos/swl/adoptar-proyecto.md +21 -1
  7. package/comandos/swl/claudemd.md +14 -1
  8. package/comandos/swl/contribuir.md +233 -233
  9. package/comandos/swl/exportar-vault.md +207 -7
  10. package/comandos/swl/nuevo-proyecto.md +24 -2
  11. package/gateway/adapters/base.js +109 -0
  12. package/gateway/adapters/discord.js +167 -0
  13. package/gateway/adapters/email.js +221 -0
  14. package/gateway/adapters/slack.js +192 -0
  15. package/gateway/adapters/telegram.js +183 -0
  16. package/gateway/adapters/webhook.js +113 -0
  17. package/gateway/adapters/whatsapp.js +214 -0
  18. package/gateway/agent-executor.js +322 -0
  19. package/gateway/command-relay.js +271 -0
  20. package/gateway/cron/jobs.js +263 -0
  21. package/gateway/cron/scheduler.js +322 -0
  22. package/gateway/cron/store.js +335 -0
  23. package/gateway/index.js +320 -0
  24. package/gateway/lib/event-channel.js +191 -0
  25. package/gateway/session.js +131 -0
  26. package/gateway/webhook-server.js +324 -0
  27. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  28. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  29. package/habilidades/build-errors-nextjs/SKILL.md +55 -1
  30. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  31. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  32. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  33. package/habilidades/eval-framework/SKILL.md +212 -212
  34. package/habilidades/extractor-de-aprendizajes/SKILL.md +24 -10
  35. package/habilidades/harness-claude-code/SKILL.md +299 -299
  36. package/habilidades/infra-github-actions/SKILL.md +166 -166
  37. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  38. package/habilidades/manejo-errores/.evolved.json +8 -8
  39. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  40. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  41. package/habilidades/nextjs-testing/SKILL.md +89 -5
  42. package/habilidades/node-experto/SKILL.md +37 -1
  43. package/habilidades/patrones-python/SKILL.md +229 -229
  44. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  45. package/habilidades/planear-fase/SKILL.md +319 -319
  46. package/habilidades/react-experto/SKILL.md +45 -4
  47. package/habilidades/release-semver/.evolved.json +8 -8
  48. package/habilidades/swl-claudemd/SKILL.md +15 -1
  49. package/habilidades/tdd-workflow/SKILL.md +36 -4
  50. package/habilidades/testing-python/SKILL.md +340 -340
  51. package/hooks/claudemd-bloat-detector.js +161 -161
  52. package/hooks/inyeccion-contexto.js +8 -3
  53. package/hooks/lib/agent-routing.js +107 -107
  54. package/hooks/lib/auto-consolidator.js +335 -335
  55. package/hooks/lib/error-classifier.js +308 -308
  56. package/hooks/lib/merkle-audit.js +96 -96
  57. package/hooks/lib/provenance-tracker.js +191 -191
  58. package/hooks/lib/rate-limit-ip.js +177 -0
  59. package/hooks/lib/rate-limit-tracker.js +253 -253
  60. package/hooks/lib/resource-quota.js +122 -122
  61. package/hooks/lib/retry-jitter.js +165 -165
  62. package/hooks/lib/skill-auditor.js +588 -588
  63. package/hooks/lib/sync-status.js +228 -228
  64. package/hooks/lib/taint-tracker.js +107 -107
  65. package/hooks/lib/text-similarity.js +241 -241
  66. package/hooks/lib/toon-compressor.js +245 -245
  67. package/hooks/lib/webhook-dedup.js +184 -0
  68. package/hooks/lib/webhook-verify.js +123 -0
  69. package/hooks/proteccion-rutas.js +120 -15
  70. package/hooks/registro-turnos.js +209 -209
  71. package/hooks/sugerir-regenerar-inventario.js +170 -170
  72. package/hooks/validar-formato-post-subagente.js +140 -140
  73. package/hooks/validar-memoria-hook.js +218 -218
  74. package/instintos/prompt-appendices.yaml +57 -57
  75. package/manifiestos/agent-output-schemas.json +57 -57
  76. package/manifiestos/modulos.json +1 -0
  77. package/manifiestos/skills-lock.json +37 -37
  78. package/package.json +5 -3
  79. package/plantillas/auditor-veto-template.md +105 -105
  80. package/plantillas/github-workflows/README.md +47 -47
  81. package/plantillas/github-workflows/release-please.yml +44 -44
  82. package/plantillas/github-workflows/swl-ci.yml +107 -107
  83. package/plantillas/github-workflows/swl-security.yml +51 -51
  84. package/plugin.json +1 -1
  85. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  86. package/reglas/arreglar-al-detectar.md +147 -147
  87. package/reglas/fragmentos-compartidos.md +152 -152
  88. package/reglas/harness-claude-code.md +213 -213
  89. package/reglas/usar-context7.md +226 -226
  90. package/reglas/usar-sistema-swl.md +251 -0
  91. package/schemas/diary-entry.schema.json +80 -80
  92. package/scripts/benchmark-memoria.js +167 -167
  93. package/scripts/comandos/skills.js +251 -2
  94. package/scripts/configurar-branch-protection.js +418 -418
  95. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  96. package/scripts/field-report.js +199 -199
  97. package/scripts/generar-checklists-consolidados.js +273 -273
  98. package/scripts/generar-inventario.js +420 -420
  99. package/scripts/generar-matriz-lenguajes.js +271 -271
  100. package/scripts/lib/artefactos-python.js +43 -43
  101. package/scripts/lib/benchmark-metrics.js +160 -160
  102. package/scripts/lib/budget-enforcer.js +252 -252
  103. package/scripts/lib/configurar-ci.js +380 -380
  104. package/scripts/lib/contadores-inventario.js +217 -217
  105. package/scripts/lib/detectar-stack-detallado.js +307 -307
  106. package/scripts/lib/diary-entry.js +234 -234
  107. package/scripts/lib/eval-metrics-store.js +218 -218
  108. package/scripts/lib/eval-quality.js +171 -171
  109. package/scripts/lib/eval-schemas.js +144 -144
  110. package/scripts/lib/eval-self-correct.js +106 -106
  111. package/scripts/lib/eval-validator.js +185 -185
  112. package/scripts/lib/jaccard-similarity.js +98 -98
  113. package/scripts/lib/longmemeval-runner.js +125 -125
  114. package/scripts/lib/npm-version.js +261 -261
  115. package/scripts/lib/paquetes-conocidos.js +50 -50
  116. package/scripts/lib/prompt-builder.js +264 -264
  117. package/scripts/lib/rrf-fusion.js +175 -175
  118. package/scripts/lib/scoring-instintos.js +277 -277
  119. package/scripts/lib/semantic-search.js +252 -252
  120. package/scripts/limpiar-artefactos-python.js +131 -131
  121. package/scripts/mcp-server/README.md +128 -128
  122. package/scripts/mcp-server/handlers.js +206 -206
  123. package/scripts/migrar-csv-a-array.js +168 -168
  124. package/scripts/migrar-fase-dominio.js +201 -201
  125. package/scripts/publicar.js +511 -511
  126. package/scripts/run-eval.js +141 -141
  127. package/scripts/validar-manifest.js +195 -195
  128. package/scripts/validar-userland-vacio.js +110 -110
  129. package/scripts/verificar-release.js +110 -0
@@ -0,0 +1,324 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * webhook-server.js — Servidor HTTP local para webhooks entrantes firmados.
5
+ *
6
+ * Orquesta las tres librerías de las fases anteriores:
7
+ *
8
+ * webhook-verify → valida HMAC SHA-256 (GitHub) o Bearer (generic)
9
+ * rate-limit-ip → token bucket per-IP
10
+ * webhook-dedup → idempotencia por event-id en ledger JSONL
11
+ *
12
+ * Endpoints:
13
+ *
14
+ * POST /webhooks/github — requiere X-Hub-Signature-256 + secret
15
+ * POST /webhooks/generic — requiere Authorization: Bearer + secret
16
+ * GET /healthz — liveness (sin auth)
17
+ * * — 404
18
+ *
19
+ * Pipeline por request:
20
+ *
21
+ * 1. IP allowlist (si está configurada)
22
+ * 2. Rate limit per-IP (barato; aborta antes de HMAC costoso)
23
+ * 3. Lectura de body con cap de tamaño
24
+ * 4. HMAC verify (GitHub) o Bearer verify (generic)
25
+ * 5. Extracción de event-id (X-GitHub-Delivery o X-Request-ID o hash del body)
26
+ * 6. Dedup (rechazo idempotente: 200 OK pero no escribe)
27
+ * 7. Escritura a .planning/inbox/cmd-*.json (schema compatible con /swl:inbox)
28
+ *
29
+ * Diseño:
30
+ *
31
+ * - `http` nativo de Node, sin Express ni Fastify (zero-deps).
32
+ * - Función factory `crearServidor(deps)` que retorna http.Server sin
33
+ * arrancar. El bootstrap CLI (Fase 4) llama `.listen()`. Esto permite
34
+ * tests con puerto efímero `0` y dependencias mock.
35
+ * - Configuración 100 % vía objeto `deps` inyectado — sin lectura directa
36
+ * de process.env (eso lo hace el bootstrap CLI).
37
+ * - Bind por defecto `127.0.0.1` (ADR-0017 decisión #1). Cambiar requiere
38
+ * intencionalidad explícita en el bootstrap.
39
+ *
40
+ * @module gateway/webhook-server
41
+ */
42
+
43
+ const http = require('http');
44
+ const crypto = require('crypto');
45
+ const fs = require('fs');
46
+ const path = require('path');
47
+
48
+ const { verifyGithubSignature, verifyBearer } = require('../hooks/lib/webhook-verify');
49
+
50
+ const HEADER_GITHUB_SIG = 'x-hub-signature-256';
51
+ const HEADER_GITHUB_DELIVERY = 'x-hub-delivery';
52
+ const HEADER_GITHUB_DELIVERY_ALT = 'x-github-delivery';
53
+ const HEADER_GITHUB_EVENT = 'x-github-event';
54
+ const HEADER_AUTHORIZATION = 'authorization';
55
+ const HEADER_REQUEST_ID = 'x-request-id';
56
+
57
+ /**
58
+ * Crea un servidor HTTP listo para arrancar con `.listen()`.
59
+ *
60
+ * @param {object} deps Dependencias inyectadas.
61
+ * @param {string} deps.inboxDir Ruta absoluta al directorio inbox.
62
+ * @param {object} deps.dedup Instancia de WebhookDedup.
63
+ * @param {object} deps.rateLimiter Instancia de RateLimiterIP.
64
+ * @param {string|null} deps.githubSecret Secreto HMAC (null = endpoint deshabilitado).
65
+ * @param {string|null} deps.bearerSecret Token Bearer (null = endpoint deshabilitado).
66
+ * @param {number} deps.maxPayloadBytes Tope de tamaño del body.
67
+ * @param {Array<string>|null} deps.allowIps Lista de IPs permitidas (null = todas).
68
+ * @param {function} [deps.logger] Función de logging (default: console.error).
69
+ * @returns {http.Server}
70
+ */
71
+ function crearServidor(deps) {
72
+ validarDeps(deps);
73
+ const logger = deps.logger || ((nivel, msg, extra) => {
74
+ const linea = JSON.stringify({ nivel, msg, ...(extra || {}), ts: new Date().toISOString() });
75
+ if (nivel === 'error' || nivel === 'warn') console.error(linea);
76
+ else console.log(linea);
77
+ });
78
+
79
+ const server = http.createServer((req, res) => {
80
+ manejarRequest(req, res, deps, logger).catch(err => {
81
+ logger('error', 'manejarRequest threw', { err: err.message });
82
+ enviarRespuesta(res, 500, { error: 'internal-error' });
83
+ });
84
+ });
85
+
86
+ return server;
87
+ }
88
+
89
+ async function manejarRequest(req, res, deps, logger) {
90
+ const url = req.url || '';
91
+ const metodo = req.method || 'GET';
92
+ const ip = obtenerIp(req);
93
+
94
+ // 1. Healthz (sin auth, sin rate limit)
95
+ if (metodo === 'GET' && url === '/healthz') {
96
+ return enviarRespuesta(res, 200, { status: 'ok' });
97
+ }
98
+
99
+ // 2. Solo POST para endpoints de webhook
100
+ if (metodo !== 'POST') {
101
+ return enviarRespuesta(res, 404, { error: 'not-found' });
102
+ }
103
+
104
+ // 3. Path conocido
105
+ const proveedor = identificarProveedor(url);
106
+ if (!proveedor) {
107
+ return enviarRespuesta(res, 404, { error: 'not-found' });
108
+ }
109
+
110
+ // 4. IP allowlist
111
+ if (deps.allowIps && deps.allowIps.length > 0 && !ipPermitida(ip, deps.allowIps)) {
112
+ logger('warn', 'ip-blocked', { ip, url });
113
+ return enviarRespuesta(res, 403, { error: 'forbidden' });
114
+ }
115
+
116
+ // 5. Rate limit
117
+ if (!deps.rateLimiter.permite(ip)) {
118
+ logger('warn', 'rate-limit', { ip, url });
119
+ return enviarRespuesta(res, 429, { error: 'rate-limit-exceeded' });
120
+ }
121
+
122
+ // 6. Endpoint habilitado (secreto configurado)
123
+ const secretoRequerido = proveedor === 'github' ? deps.githubSecret : deps.bearerSecret;
124
+ if (!secretoRequerido) {
125
+ logger('warn', 'endpoint-disabled', { proveedor });
126
+ return enviarRespuesta(res, 404, { error: 'not-found' });
127
+ }
128
+
129
+ // 7. Leer body con cap
130
+ let body;
131
+ try {
132
+ body = await leerBody(req, deps.maxPayloadBytes);
133
+ } catch (err) {
134
+ if (err.code === 'PAYLOAD_TOO_LARGE') {
135
+ return enviarRespuesta(res, 413, { error: 'payload-too-large' });
136
+ }
137
+ logger('error', 'read-body-error', { err: err.message });
138
+ return enviarRespuesta(res, 400, { error: 'bad-request' });
139
+ }
140
+
141
+ // 8. Verify firma
142
+ const autorizado = proveedor === 'github'
143
+ ? verifyGithubSignature(body, req.headers[HEADER_GITHUB_SIG], deps.githubSecret)
144
+ : verifyBearer(req.headers[HEADER_AUTHORIZATION], deps.bearerSecret);
145
+
146
+ if (!autorizado) {
147
+ logger('warn', 'unauthorized', { ip, proveedor });
148
+ return enviarRespuesta(res, 401, { error: 'unauthorized' });
149
+ }
150
+
151
+ // 9. Extraer event-id
152
+ const eventId = extraerEventId(req, body, proveedor);
153
+
154
+ // 10. Dedup
155
+ if (deps.dedup.yaVisto(eventId)) {
156
+ logger('info', 'duplicate', { proveedor, eventId });
157
+ return enviarRespuesta(res, 200, { status: 'duplicate', event_id: eventId });
158
+ }
159
+
160
+ // 11. Escribir al inbox
161
+ let bodyParseado;
162
+ try {
163
+ bodyParseado = JSON.parse(body.toString('utf8'));
164
+ } catch (_) {
165
+ // Body no es JSON — guardarlo como string
166
+ bodyParseado = body.toString('utf8');
167
+ }
168
+
169
+ const eventoGithub = req.headers[HEADER_GITHUB_EVENT];
170
+ const cmdId = `cmd-${Date.now().toString(36)}-${crypto.randomBytes(3).toString('hex')}`;
171
+ const cmd = {
172
+ id: cmdId,
173
+ platform: 'webhook',
174
+ userId: proveedor,
175
+ userName: `webhook-${proveedor}`,
176
+ texto: resumirEvento(proveedor, eventoGithub, bodyParseado),
177
+ recibidoEn: new Date().toISOString(),
178
+ estado: 'pending',
179
+ chatId: null,
180
+ comando: null,
181
+ webhookProvider: proveedor,
182
+ webhookEventId: eventId,
183
+ webhookEventType: eventoGithub || null,
184
+ webhookBody: bodyParseado,
185
+ };
186
+
187
+ try {
188
+ fs.mkdirSync(deps.inboxDir, { recursive: true });
189
+ fs.writeFileSync(path.join(deps.inboxDir, `${cmdId}.json`), JSON.stringify(cmd, null, 2), 'utf8');
190
+ deps.dedup.registrar(eventId, proveedor);
191
+ } catch (err) {
192
+ logger('error', 'inbox-write-error', { err: err.message });
193
+ return enviarRespuesta(res, 500, { error: 'write-error' });
194
+ }
195
+
196
+ logger('info', 'accepted', { proveedor, eventId, cmdId });
197
+ return enviarRespuesta(res, 200, { status: 'accepted', event_id: eventId, cmd_id: cmdId });
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Helpers
202
+ // ---------------------------------------------------------------------------
203
+
204
+ function validarDeps(deps) {
205
+ if (!deps || typeof deps !== 'object') throw new Error('webhook-server: deps requerido');
206
+ if (!deps.inboxDir || typeof deps.inboxDir !== 'string') {
207
+ throw new Error('webhook-server: deps.inboxDir requerido');
208
+ }
209
+ if (!deps.dedup || typeof deps.dedup.yaVisto !== 'function') {
210
+ throw new Error('webhook-server: deps.dedup inválido');
211
+ }
212
+ if (!deps.rateLimiter || typeof deps.rateLimiter.permite !== 'function') {
213
+ throw new Error('webhook-server: deps.rateLimiter inválido');
214
+ }
215
+ if (!Number.isFinite(deps.maxPayloadBytes) || deps.maxPayloadBytes <= 0) {
216
+ throw new Error('webhook-server: deps.maxPayloadBytes requerido y positivo');
217
+ }
218
+ }
219
+
220
+ function identificarProveedor(url) {
221
+ // url puede traer query string; comparar solo el path
222
+ const pathOnly = url.split('?')[0];
223
+ if (pathOnly === '/webhooks/github') return 'github';
224
+ if (pathOnly === '/webhooks/generic') return 'generic';
225
+ return null;
226
+ }
227
+
228
+ function obtenerIp(req) {
229
+ // En este servidor sin proxy, socket.remoteAddress es la IP real.
230
+ // Si se pone detrás de reverse proxy, hay que leer X-Forwarded-For — pero
231
+ // eso es responsabilidad del proxy y requiere config explícita (no implementado).
232
+ const ip = req.socket && req.socket.remoteAddress;
233
+ if (!ip) return '0.0.0.0';
234
+ // Normalizar IPv4-mapped IPv6 (::ffff:127.0.0.1 → 127.0.0.1)
235
+ return ip.startsWith('::ffff:') ? ip.slice(7) : ip;
236
+ }
237
+
238
+ function ipPermitida(ip, allowIps) {
239
+ // Comparación exacta. CIDR queda fuera de scope de Fase 3 — si se necesita,
240
+ // el bootstrap usa una librería de CIDR matching. Aquí: equality match.
241
+ return allowIps.includes(ip);
242
+ }
243
+
244
+ function leerBody(req, maxBytes) {
245
+ return new Promise((resolve, reject) => {
246
+ const chunks = [];
247
+ let total = 0;
248
+ let abortado = false;
249
+
250
+ req.on('data', chunk => {
251
+ if (abortado) return;
252
+ total += chunk.length;
253
+ if (total > maxBytes) {
254
+ abortado = true;
255
+ // Pausar la lectura — NO destruir el socket. Destruir cerraría la
256
+ // conexión antes de que el handler pueda enviar la respuesta 413,
257
+ // causando ECONNRESET en el cliente. Con pause, el socket queda
258
+ // vivo, res.end() envía la respuesta y luego Node cierra el socket.
259
+ req.pause();
260
+ const err = new Error('payload too large');
261
+ err.code = 'PAYLOAD_TOO_LARGE';
262
+ return reject(err);
263
+ }
264
+ chunks.push(chunk);
265
+ });
266
+ req.on('end', () => {
267
+ if (!abortado) resolve(Buffer.concat(chunks));
268
+ });
269
+ req.on('error', err => {
270
+ if (!abortado) reject(err);
271
+ });
272
+ });
273
+ }
274
+
275
+ function extraerEventId(req, body, proveedor) {
276
+ if (proveedor === 'github') {
277
+ const id = req.headers[HEADER_GITHUB_DELIVERY] || req.headers[HEADER_GITHUB_DELIVERY_ALT];
278
+ if (id) return String(id);
279
+ }
280
+ const reqId = req.headers[HEADER_REQUEST_ID];
281
+ if (reqId) return String(reqId);
282
+ // Fallback: hash del body. Mismo body = mismo id = idempotente.
283
+ return 'sha256-' + crypto.createHash('sha256').update(body).digest('hex');
284
+ }
285
+
286
+ function resumirEvento(proveedor, eventoGithub, body) {
287
+ if (proveedor === 'github' && eventoGithub) {
288
+ // Resumen útil para que /swl:inbox decida qué hacer
289
+ const ref = body && typeof body === 'object' ? body.ref : null;
290
+ const action = body && typeof body === 'object' ? body.action : null;
291
+ if (ref) return `github:${eventoGithub} ${ref}`;
292
+ if (action) return `github:${eventoGithub} ${action}`;
293
+ return `github:${eventoGithub}`;
294
+ }
295
+ if (proveedor === 'generic') {
296
+ if (body && typeof body === 'object' && body.command) {
297
+ return `generic:${String(body.command)}`;
298
+ }
299
+ const raw = typeof body === 'string' ? body : JSON.stringify(body);
300
+ return `generic:${raw.slice(0, 200)}`;
301
+ }
302
+ return `${proveedor}:event`;
303
+ }
304
+
305
+ function enviarRespuesta(res, status, body) {
306
+ const payload = JSON.stringify(body);
307
+ res.writeHead(status, {
308
+ 'Content-Type': 'application/json',
309
+ 'Content-Length': Buffer.byteLength(payload),
310
+ });
311
+ res.end(payload);
312
+ }
313
+
314
+ module.exports = {
315
+ crearServidor,
316
+ // exportar helpers para tests de unidad si crece la necesidad
317
+ _internals: {
318
+ identificarProveedor,
319
+ obtenerIp,
320
+ ipPermitida,
321
+ extraerEventId,
322
+ resumirEvento,
323
+ },
324
+ };