@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.
- package/CLAUDE.md +12 -4
- package/README.md +1 -1
- package/bin/swl-mcp-server.js +187 -187
- package/bin/swl-webhook-server.js +198 -0
- package/comandos/swl/.evolved.json +22 -22
- package/comandos/swl/adoptar-proyecto.md +21 -1
- package/comandos/swl/claudemd.md +14 -1
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/exportar-vault.md +207 -7
- package/comandos/swl/nuevo-proyecto.md +24 -2
- package/gateway/adapters/base.js +109 -0
- package/gateway/adapters/discord.js +167 -0
- package/gateway/adapters/email.js +221 -0
- package/gateway/adapters/slack.js +192 -0
- package/gateway/adapters/telegram.js +183 -0
- package/gateway/adapters/webhook.js +113 -0
- package/gateway/adapters/whatsapp.js +214 -0
- package/gateway/agent-executor.js +322 -0
- package/gateway/command-relay.js +271 -0
- package/gateway/cron/jobs.js +263 -0
- package/gateway/cron/scheduler.js +322 -0
- package/gateway/cron/store.js +335 -0
- package/gateway/index.js +320 -0
- package/gateway/lib/event-channel.js +191 -0
- package/gateway/session.js +131 -0
- package/gateway/webhook-server.js +324 -0
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/benchmark-memoria/SKILL.md +186 -186
- package/habilidades/build-errors-nextjs/SKILL.md +55 -1
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/SKILL.md +171 -171
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/eval-framework/SKILL.md +212 -212
- package/habilidades/extractor-de-aprendizajes/SKILL.md +24 -10
- package/habilidades/harness-claude-code/SKILL.md +299 -299
- package/habilidades/infra-github-actions/SKILL.md +166 -166
- package/habilidades/legacy-code-rescue/SKILL.md +267 -267
- package/habilidades/manejo-errores/.evolved.json +8 -8
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
- package/habilidades/nextjs-testing/SKILL.md +89 -5
- package/habilidades/node-experto/SKILL.md +37 -1
- package/habilidades/patrones-python/SKILL.md +229 -229
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/planear-fase/SKILL.md +319 -319
- package/habilidades/react-experto/SKILL.md +45 -4
- package/habilidades/release-semver/.evolved.json +8 -8
- package/habilidades/swl-claudemd/SKILL.md +15 -1
- package/habilidades/tdd-workflow/SKILL.md +36 -4
- package/habilidades/testing-python/SKILL.md +340 -340
- package/hooks/claudemd-bloat-detector.js +161 -161
- package/hooks/inyeccion-contexto.js +8 -3
- package/hooks/lib/agent-routing.js +107 -107
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/merkle-audit.js +96 -96
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/rate-limit-ip.js +177 -0
- 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/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/toon-compressor.js +245 -245
- package/hooks/lib/webhook-dedup.js +184 -0
- package/hooks/lib/webhook-verify.js +123 -0
- package/hooks/proteccion-rutas.js +120 -15
- package/hooks/registro-turnos.js +209 -209
- package/hooks/sugerir-regenerar-inventario.js +170 -170
- package/hooks/validar-formato-post-subagente.js +140 -140
- package/hooks/validar-memoria-hook.js +218 -218
- package/instintos/prompt-appendices.yaml +57 -57
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/modulos.json +1 -0
- package/manifiestos/skills-lock.json +37 -37
- package/package.json +5 -3
- 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/analisis-previo-tareas-grandes.md +172 -172
- package/reglas/arreglar-al-detectar.md +147 -147
- package/reglas/fragmentos-compartidos.md +152 -152
- package/reglas/harness-claude-code.md +213 -213
- package/reglas/usar-context7.md +226 -226
- package/reglas/usar-sistema-swl.md +251 -0
- package/schemas/diary-entry.schema.json +80 -80
- package/scripts/benchmark-memoria.js +167 -167
- package/scripts/comandos/skills.js +251 -2
- 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-inventario.js +420 -420
- package/scripts/generar-matriz-lenguajes.js +271 -271
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/configurar-ci.js +380 -380
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- 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/jaccard-similarity.js +98 -98
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/npm-version.js +261 -261
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/scoring-instintos.js +277 -277
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/mcp-server/README.md +128 -128
- package/scripts/mcp-server/handlers.js +206 -206
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +201 -201
- package/scripts/publicar.js +511 -511
- package/scripts/run-eval.js +141 -141
- package/scripts/validar-manifest.js +195 -195
- package/scripts/validar-userland-vacio.js +110 -110
- 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
|
+
};
|