lbh-sdk-hormigasais 0.3.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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cristhiam Leonardo HernΓ‘ndez QuiΓ±onez (CLHQ)
4
+ HormigasAIS β€” Nodo A16-SanMiguel-SV
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # lbh-sdk-js 🐜
2
+
3
+ **SDK oficial del Protocolo LBH v2.0 para JavaScript/Node.js β€” HormigasAIS**
4
+
5
+ ImplementaciΓ³n pΓΊblica basada en la especificaciΓ³n LBH_SPEC_v2.0.md.
6
+
7
+ ## InstalaciΓ³n
8
+
9
+ ```bash
10
+ npm install @hormigasais/lbh-sdk
11
+ ```
12
+
13
+ O sin npm:
14
+ ```bash
15
+ git clone https://github.com/Thrumanshow/lbh-sdk-js.git
16
+ ```
17
+
18
+ ## Uso rΓ‘pido
19
+
20
+ ```javascript
21
+ const sdk = require('@hormigasais/lbh-sdk');
22
+
23
+ // Codificar un paquete SEAL
24
+ const frame = sdk.encodePacket(
25
+ sdk.makeHeader('A16', '00'),
26
+ sdk.typeCode('SEAL'),
27
+ '{"asset":"documento.pdf","owner":"CLHQ"}'
28
+ );
29
+
30
+ // Decodificar
31
+ const decoded = sdk.decodePacket(frame);
32
+ console.log(decoded.typeName); // β†’ "SEAL"
33
+ console.log(decoded.payload); // β†’ '{"asset":"documento.pdf",...}'
34
+
35
+ // Validar
36
+ const { valid } = sdk.validatePacket(frame);
37
+ console.log(valid); // β†’ true
38
+
39
+ // Sellar un activo
40
+ const fs = require('fs');
41
+ const sello = sdk.sealAsset(fs.readFileSync('documento.pdf'), 'CLHQ', 'tu_clave');
42
+
43
+ // Verificar
44
+ const { valid: ok } = sdk.verifySeal(fs.readFileSync('documento.pdf'), sello, 'tu_clave');
45
+ ```
46
+
47
+ ## Tests
48
+
49
+ ```bash
50
+ node tests/test_sdk.js
51
+ ```
52
+
53
+ ## DocumentaciΓ³n relacionada
54
+
55
+ - **EspecificaciΓ³n LBH_SPEC_v2.0**: [Lenguaje-Binario-HormigasAIS-](https://github.com/Thrumanshow/Lenguaje-Binario-HormigasAIS-)
56
+ - **SDK Python**: [lbh-sdk](https://github.com/Thrumanshow/lbh-sdk) β€” `pip install lbh-sdk`
57
+ - **Web**: [hormigasais.com](https://hormigasais.com)
58
+ - **DOI**: 10.5281/zenodo.19177759
59
+
60
+ ## Autor
61
+
62
+ **Cristhiam Leonardo HernΓ‘ndez QuiΓ±onez (CLHQ)**
63
+ Fundador de HormigasAIS β€” Nodo A16 Β· San Miguel Β· El Salvador
64
+
65
+ CERT::LBH-SDK-JS-V2-CLHQ
@@ -0,0 +1,734 @@
1
+
2
+ #!/data/data/com.termux/files/usr/bin/bash
3
+ # ============================================================
4
+ # 🐜 HormigasAIS β€” init_lbh_sdk_js.sh
5
+ # Inicializa la estructura completa del repo lbh-sdk-js:
6
+ # - src/index.js (librerΓ­a principal)
7
+ # - src/constants.js (TYPE_CODES)
8
+ # - src/exceptions.js (errores personalizados)
9
+ # - tests/test_sdk.js (suite de conformidad)
10
+ # - package.json (configuraciΓ³n npm)
11
+ # - README.md
12
+ # - LICENSE (MIT)
13
+ #
14
+ # Ejecutar desde: ~/lbh-sdk-js
15
+ # ============================================================
16
+
17
+ set -uo pipefail
18
+
19
+ echo "🐜 Inicializando lbh-sdk-js β€” Fase III del SDK HormigasAIS"
20
+ echo "------------------------------------------------------------"
21
+
22
+ # ── 1. package.json ──────────────────────────────────────────
23
+ cat > package.json << 'PKGEOF'
24
+ {
25
+ "name": "@hormigasais/lbh-sdk",
26
+ "version": "0.3.0",
27
+ "description": "SDK oficial de referencia del protocolo LBH (Lenguaje Binario HormigasAIS) para JavaScript/Node.js",
28
+ "main": "src/index.js",
29
+ "scripts": {
30
+ "test": "node tests/test_sdk.js"
31
+ },
32
+ "keywords": [
33
+ "lbh",
34
+ "hormigasais",
35
+ "protocol",
36
+ "sdk",
37
+ "edge-computing",
38
+ "hmac",
39
+ "binary-protocol",
40
+ "iot",
41
+ "m2m"
42
+ ],
43
+ "author": "Cristhiam Leonardo HernΓ‘ndez QuiΓ±onez (CLHQ) <clhq@hormigasais.com>",
44
+ "license": "MIT",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/Thrumanshow/lbh-sdk-js.git"
48
+ },
49
+ "homepage": "https://hormigasais.com",
50
+ "engines": {
51
+ "node": ">=14.0.0"
52
+ }
53
+ }
54
+ PKGEOF
55
+ echo "βœ… package.json creado"
56
+
57
+ # ── 2. src/constants.js ──────────────────────────────────────
58
+ mkdir -p src
59
+ cat > src/constants.js << 'CONSTEOF'
60
+ /**
61
+ * constants.js β€” TYPE_CODEs oficiales LBH v2.0
62
+ * Propuesta formal de estandarizaciΓ³n segΓΊn LBH_SPEC_v2.0.md
63
+ * En v1.x el campo es libre; en v2.0 se estandariza este enum.
64
+ */
65
+
66
+ const TYPE_CODES = {
67
+ SEAL: '5345414c', // EmisiΓ³n de sello criptogrΓ‘fico
68
+ VERI: '56455249', // VerificaciΓ³n de activo sellado
69
+ SYNC: '53594e43', // SincronizaciΓ³n entre nodos
70
+ PING: '50494e47', // Latido / health check
71
+ FUEL: '4655454c', // Feromona de activaciΓ³n
72
+ ACKK: '41434b4b', // ConfirmaciΓ³n de recepciΓ³n
73
+ ERRR: '45525252', // Error / rechazo
74
+ };
75
+
76
+ const TYPE_CODES_REVERSE = Object.fromEntries(
77
+ Object.entries(TYPE_CODES).map(([k, v]) => [v, k])
78
+ );
79
+
80
+ module.exports = { TYPE_CODES, TYPE_CODES_REVERSE };
81
+ CONSTEOF
82
+ echo "βœ… src/constants.js creado"
83
+
84
+ # ── 3. src/exceptions.js ─────────────────────────────────────
85
+ cat > src/exceptions.js << 'EXCEOF'
86
+ /**
87
+ * exceptions.js β€” Errores personalizados LBH SDK
88
+ */
89
+
90
+ class InvalidPacketError extends Error {
91
+ constructor(message) {
92
+ super(message);
93
+ this.name = 'InvalidPacketError';
94
+ }
95
+ }
96
+
97
+ class InvalidPayloadError extends Error {
98
+ constructor(message) {
99
+ super(message);
100
+ this.name = 'InvalidPayloadError';
101
+ }
102
+ }
103
+
104
+ module.exports = { InvalidPacketError, InvalidPayloadError };
105
+ EXCEOF
106
+ echo "βœ… src/exceptions.js creado"
107
+
108
+ # ── 4. src/index.js ──────────────────────────────────────────
109
+ cat > src/index.js << 'INDEXEOF'
110
+ /**
111
+ * lbh-sdk β€” Lenguaje Binario HormigasAIS SDK (JavaScript)
112
+ * =========================================================
113
+ *
114
+ * SDK de referencia para el Protocolo LBH v2.0.
115
+ * Permite codificar, decodificar, validar y firmar
116
+ * paquetes LBH sin acceso al nodo A16.
117
+ *
118
+ * EspecificaciΓ³n: LBH_SPEC_v2.0.md
119
+ * Repo spec: github.com/Thrumanshow/Lenguaje-Binario-HormigasAIS-
120
+ * Repo SDK JS: github.com/Thrumanshow/lbh-sdk-js
121
+ * Autor: Cristhiam Leonardo HernΓ‘ndez QuiΓ±onez (CLHQ)
122
+ * Web: https://hormigasais.com
123
+ * DOI: 10.5281/zenodo.19177759
124
+ */
125
+
126
+ 'use strict';
127
+
128
+ const crypto = require('crypto');
129
+ const { TYPE_CODES, TYPE_CODES_REVERSE } = require('./constants');
130
+ const { InvalidPacketError, InvalidPayloadError } = require('./exceptions');
131
+
132
+ const VERSION = '0.3.0';
133
+ const SPEC = 'LBH_SPEC_v2.0';
134
+ const AUTHOR = 'CLHQ β€” HormigasAIS';
135
+
136
+ // ─────────────────────────────────────────────────────────────
137
+ // ENCODER
138
+ // ─────────────────────────────────────────────────────────────
139
+ /**
140
+ * Codifica un paquete LBH v2.0.
141
+ *
142
+ * @param {string} header - 8 chars hex β€” ID nodo + versiΓ³n
143
+ * @param {string} typeCode - 8 chars hex β€” tipo de operaciΓ³n
144
+ * @param {string} payload - string UTF-8 β€” contenido JSON
145
+ * @returns {string} Frame completo como string hexadecimal
146
+ * @throws {InvalidPacketError} si header o typeCode son invΓ‘lidos
147
+ */
148
+ function encodePacket(header, typeCode, payload) {
149
+ if (header.length !== 8) {
150
+ throw new InvalidPacketError(
151
+ `header debe tener 8 chars hex, tiene ${header.length}`
152
+ );
153
+ }
154
+ if (typeCode.length !== 8) {
155
+ throw new InvalidPacketError(
156
+ `typeCode debe tener 8 chars hex, tiene ${typeCode.length}`
157
+ );
158
+ }
159
+
160
+ const payloadHex = Buffer.from(payload, 'utf-8').toString('hex');
161
+ const length = (payloadHex.length / 2).toString(16).padStart(8, '0');
162
+ return `${header}${typeCode}${length}${payloadHex}`;
163
+ }
164
+
165
+ // ─────────────────────────────────────────────────────────────
166
+ // DECODER
167
+ // ─────────────────────────────────────────────────────────────
168
+ /**
169
+ * Decodifica un paquete LBH v2.0.
170
+ *
171
+ * @param {string} frame - string hexadecimal completo
172
+ * @returns {object} { header, typeCode, typeName, length, payload }
173
+ * o { error: string } si el frame es invΓ‘lido
174
+ */
175
+ function decodePacket(frame) {
176
+ try {
177
+ if (frame.length < 24) {
178
+ return { error: `Frame demasiado corto: ${frame.length} chars (mΓ­nimo 24)` };
179
+ }
180
+
181
+ const header = frame.slice(0, 8);
182
+ const typeCode = frame.slice(8, 16);
183
+ const lengthHex = frame.slice(16, 24);
184
+ const payloadHex = frame.slice(24);
185
+
186
+ const payload = Buffer.from(payloadHex, 'hex').toString('utf-8');
187
+ const length = parseInt(lengthHex, 16);
188
+ const typeName = TYPE_CODES_REVERSE[typeCode] || null;
189
+
190
+ return { header, typeCode, typeName, length, payload };
191
+ } catch (e) {
192
+ return { error: e.message };
193
+ }
194
+ }
195
+
196
+ // ─────────────────────────────────────────────────────────────
197
+ // VALIDADOR
198
+ // ─────────────────────────────────────────────────────────────
199
+ /**
200
+ * Valida un paquete LBH v2.0 segΓΊn LBH_SPEC_v2.0 Β§6.
201
+ *
202
+ * @param {string} frame - string hexadecimal completo
203
+ * @returns {{ valid: boolean, reason: string|null }}
204
+ */
205
+ function validatePacket(frame) {
206
+ if (frame.length < 24) {
207
+ return { valid: false, reason: `Longitud mΓ­nima no cumplida: ${frame.length} < 24` };
208
+ }
209
+
210
+ let declaredLength;
211
+ try {
212
+ declaredLength = parseInt(frame.slice(16, 24), 16);
213
+ } catch (e) {
214
+ return { valid: false, reason: 'Campo LENGTH no es hex vΓ‘lido' };
215
+ }
216
+
217
+ let payloadBytes;
218
+ try {
219
+ payloadBytes = Buffer.from(frame.slice(24), 'hex');
220
+ } catch (e) {
221
+ return { valid: false, reason: 'PAYLOAD no es hex vΓ‘lido' };
222
+ }
223
+
224
+ try {
225
+ payloadBytes.toString('utf-8');
226
+ } catch (e) {
227
+ return { valid: false, reason: 'PAYLOAD no decodifica como UTF-8' };
228
+ }
229
+
230
+ if (declaredLength !== payloadBytes.length) {
231
+ return {
232
+ valid: false,
233
+ reason: `LENGTH declarado (${declaredLength}) no coincide con bytes reales (${payloadBytes.length})`
234
+ };
235
+ }
236
+
237
+ return { valid: true, reason: null };
238
+ }
239
+
240
+ // ─────────────────────────────────────────────────────────────
241
+ // SEGURIDAD β€” HMAC-SHA256
242
+ // ─────────────────────────────────────────────────────────────
243
+ /**
244
+ * Genera HMAC-SHA256 para un mensaje LBH.
245
+ *
246
+ * @param {string} message - frame o mensaje a firmar
247
+ * @param {string} secretKey - clave secreta (cargar desde env LBH_SECRET)
248
+ * @returns {string} hexdigest de 64 chars
249
+ */
250
+ function generateHmac(message, secretKey) {
251
+ const key = secretKey || process.env.LBH_SECRET;
252
+ if (!key) {
253
+ throw new Error(
254
+ 'Se requiere secretKey o la variable de entorno LBH_SECRET. ' +
255
+ 'Nunca hardcodees la clave.'
256
+ );
257
+ }
258
+ return crypto
259
+ .createHmac('sha256', key)
260
+ .update(message, 'utf-8')
261
+ .digest('hex');
262
+ }
263
+
264
+ /**
265
+ * Valida un HMAC-SHA256 usando comparaciΓ³n segura (timing-safe).
266
+ *
267
+ * @param {string} message - mensaje original
268
+ * @param {string} secretKey - clave secreta
269
+ * @param {string} receivedHmac - HMAC a validar
270
+ * @returns {boolean}
271
+ */
272
+ function validateHmac(message, secretKey, receivedHmac) {
273
+ const expected = generateHmac(message, secretKey);
274
+ try {
275
+ return crypto.timingSafeEqual(
276
+ Buffer.from(expected, 'hex'),
277
+ Buffer.from(receivedHmac, 'hex')
278
+ );
279
+ } catch (e) {
280
+ return false;
281
+ }
282
+ }
283
+
284
+ // ─────────────────────────────────────────────────────────────
285
+ // SELLO DE ACTIVOS
286
+ // ─────────────────────────────────────────────────────────────
287
+ /**
288
+ * Genera un sello LBH para un activo digital.
289
+ *
290
+ * @param {Buffer|string} content - contenido binario del activo
291
+ * @param {string} owner - propietario (ej: "CLHQ")
292
+ * @param {string} secretKey - clave HMAC (o usa LBH_SECRET del env)
293
+ * @returns {object} sello con sha256, timestamp, firma, owner
294
+ */
295
+ function sealAsset(content, owner, secretKey) {
296
+ const key = secretKey || process.env.LBH_SECRET;
297
+ if (!key) {
298
+ throw new Error('Se requiere secretKey o LBH_SECRET en el entorno.');
299
+ }
300
+
301
+ const buf = Buffer.isBuffer(content) ? content : Buffer.from(content);
302
+ const sha256 = crypto.createHash('sha256').update(buf).digest('hex');
303
+ const timestamp = Math.floor(Date.now() / 1000);
304
+ const payloadFirma = `${sha256}|${owner}|${timestamp}`;
305
+
306
+ const firma = crypto
307
+ .createHmac('sha256', key)
308
+ .update(payloadFirma, 'utf-8')
309
+ .digest('hex');
310
+
311
+ return {
312
+ sha256,
313
+ owner,
314
+ timestamp,
315
+ payloadFirma,
316
+ firma,
317
+ versionLbh: SPEC,
318
+ autoridad: 'CLHQ',
319
+ };
320
+ }
321
+
322
+ /**
323
+ * Verifica la integridad y autenticidad de un sello LBH.
324
+ *
325
+ * @param {Buffer|string} content - contenido original del activo
326
+ * @param {object} sello - objeto retornado por sealAsset()
327
+ * @param {string} secretKey - clave HMAC
328
+ * @returns {{ valid: boolean, reason: string|null }}
329
+ */
330
+ function verifySeal(content, sello, secretKey) {
331
+ const key = secretKey || process.env.LBH_SECRET;
332
+ if (!key) {
333
+ throw new Error('Se requiere secretKey o LBH_SECRET en el entorno.');
334
+ }
335
+
336
+ const buf = Buffer.isBuffer(content) ? content : Buffer.from(content);
337
+ const sha256Actual = crypto.createHash('sha256').update(buf).digest('hex');
338
+
339
+ if (sha256Actual !== sello.sha256) {
340
+ return { valid: false, reason: 'SHA256 no coincide β€” activo modificado' };
341
+ }
342
+
343
+ const firmaEsperada = crypto
344
+ .createHmac('sha256', key)
345
+ .update(sello.payloadFirma, 'utf-8')
346
+ .digest('hex');
347
+
348
+ try {
349
+ const match = crypto.timingSafeEqual(
350
+ Buffer.from(firmaEsperada, 'hex'),
351
+ Buffer.from(sello.firma, 'hex')
352
+ );
353
+ if (!match) {
354
+ return { valid: false, reason: 'HMAC invΓ‘lido β€” firma no autΓ©ntica' };
355
+ }
356
+ } catch (e) {
357
+ return { valid: false, reason: 'Error comparando firmas' };
358
+ }
359
+
360
+ return { valid: true, reason: null };
361
+ }
362
+
363
+ // ─────────────────────────────────────────────────────────────
364
+ // HELPERS
365
+ // ─────────────────────────────────────────────────────────────
366
+ /**
367
+ * Genera un HEADER LBH de 8 chars hex.
368
+ *
369
+ * @param {string} nodeId - ID del nodo (ej: "A16")
370
+ * @param {string} version - versiΓ³n (ej: "00")
371
+ * @returns {string} 8 chars hex
372
+ */
373
+ function makeHeader(nodeId = 'A16', version = '00') {
374
+ const idHex = Buffer.from(nodeId, 'utf-8').toString('hex').slice(0, 4).padEnd(4, '0');
375
+ const verHex = Buffer.from(version, 'utf-8').toString('hex').slice(0, 4).padEnd(4, '0');
376
+ return `${idHex}${verHex}`;
377
+ }
378
+
379
+ /**
380
+ * Retorna el hex de un TYPE_CODE por nombre.
381
+ *
382
+ * @param {string} name - "SEAL", "VERI", "SYNC", etc.
383
+ * @returns {string} 8 chars hex
384
+ * @throws {Error} si el nombre no estΓ‘ en TYPE_CODES
385
+ */
386
+ function typeCode(name) {
387
+ if (!TYPE_CODES[name]) {
388
+ throw new Error(
389
+ `TYPE_CODE '${name}' no reconocido. VΓ‘lidos: ${Object.keys(TYPE_CODES).join(', ')}`
390
+ );
391
+ }
392
+ return TYPE_CODES[name];
393
+ }
394
+
395
+ // ─────────────────────────────────────────────────────────────
396
+ // EXPORTS
397
+ // ─────────────────────────────────────────────────────────────
398
+ module.exports = {
399
+ VERSION,
400
+ SPEC,
401
+ AUTHOR,
402
+ TYPE_CODES,
403
+ TYPE_CODES_REVERSE,
404
+ encodePacket,
405
+ decodePacket,
406
+ validatePacket,
407
+ generateHmac,
408
+ validateHmac,
409
+ sealAsset,
410
+ verifySeal,
411
+ makeHeader,
412
+ typeCode,
413
+ InvalidPacketError,
414
+ InvalidPayloadError,
415
+ };
416
+ INDEXEOF
417
+ echo "βœ… src/index.js creado"
418
+
419
+ # ── 5. tests/test_sdk.js ─────────────────────────────────────
420
+ mkdir -p tests
421
+ cat > tests/test_sdk.js << 'TESTEOF'
422
+ /**
423
+ * test_sdk.js β€” Suite de conformidad LBH_SPEC_v2.0 (JavaScript)
424
+ * Verifica que lbh-sdk-js cumple la especificaciΓ³n formal.
425
+ */
426
+
427
+ 'use strict';
428
+
429
+ const sdk = require('../src/index');
430
+
431
+ let passed = 0;
432
+ let failed = 0;
433
+
434
+ function assert(condition, testName, detail = '') {
435
+ if (condition) {
436
+ console.log(`βœ… ${testName}`);
437
+ passed++;
438
+ } else {
439
+ console.log(`❌ ${testName}${detail ? ': ' + detail : ''}`);
440
+ failed++;
441
+ }
442
+ }
443
+
444
+ // ── Tests de encoding ────────────────────────────────────────
445
+ function test_encode_decode_roundtrip() {
446
+ const header = '41313600';
447
+ const tc = sdk.TYPE_CODES.SEAL;
448
+ const payload = '{"asset":"test.pdf","owner":"CLHQ"}';
449
+ const frame = sdk.encodePacket(header, tc, payload);
450
+ const decoded = sdk.decodePacket(frame);
451
+
452
+ assert(decoded.header === header, 'encode_decode: header correcto');
453
+ assert(decoded.typeCode === tc, 'encode_decode: typeCode correcto');
454
+ assert(decoded.typeName === 'SEAL', 'encode_decode: typeName correcto');
455
+ assert(decoded.payload === payload, 'encode_decode: payload correcto');
456
+ }
457
+
458
+ // ── Tests de validaciΓ³n ──────────────────────────────────────
459
+ function test_validate_valid_packet() {
460
+ const frame = sdk.encodePacket('41313600', sdk.TYPE_CODES.PING, 'ping');
461
+ const result = sdk.validatePacket(frame);
462
+ assert(result.valid === true, 'validate: paquete vΓ‘lido aceptado');
463
+ }
464
+
465
+ function test_validate_short_frame() {
466
+ const result = sdk.validatePacket('41313600');
467
+ assert(result.valid === false, 'validate: frame corto rechazado');
468
+ assert(result.reason.includes('24'), 'validate: reason menciona mΓ­nimo 24');
469
+ }
470
+
471
+ function test_validate_length_mismatch() {
472
+ // LENGTH declara 1 byte pero payload tiene mΓ‘s
473
+ const result = sdk.validatePacket('4131360053594e430000000141');
474
+ assert(result.valid === false, 'validate: length mismatch rechazado');
475
+ }
476
+
477
+ // ── Tests de TYPE_CODES ──────────────────────────────────────
478
+ function test_type_codes() {
479
+ assert(sdk.typeCode('SEAL') === '5345414c', 'typeCode: SEAL correcto');
480
+ assert(sdk.typeCode('VERI') === '56455249', 'typeCode: VERI correcto');
481
+ assert(sdk.typeCode('PING') === '50494e47', 'typeCode: PING correcto');
482
+ assert(sdk.typeCode('SYNC') === '53594e43', 'typeCode: SYNC correcto');
483
+ }
484
+
485
+ function test_type_code_invalid() {
486
+ try {
487
+ sdk.typeCode('INVALID');
488
+ assert(false, 'typeCode: invΓ‘lido deberΓ­a lanzar error');
489
+ } catch (e) {
490
+ assert(true, 'typeCode: invΓ‘lido lanza error correctamente');
491
+ }
492
+ }
493
+
494
+ // ── Tests de makeHeader ──────────────────────────────────────
495
+ function test_make_header() {
496
+ const h = sdk.makeHeader('A16', '00');
497
+ assert(h.length === 8, `makeHeader: longitud 8 chars (tiene ${h.length})`);
498
+ }
499
+
500
+ // ── Tests de HMAC ────────────────────────────────────────────
501
+ function test_hmac_generate_validate() {
502
+ const key = 'test-secret-lbh';
503
+ const message = 'frame-lbh-test';
504
+ const digest = sdk.generateHmac(message, key);
505
+
506
+ assert(digest.length === 64, 'hmac: digest tiene 64 chars');
507
+ assert(sdk.validateHmac(message, key, digest), 'hmac: validaciΓ³n correcta');
508
+ assert(!sdk.validateHmac(message, key, '0'.repeat(64)), 'hmac: digest incorrecto rechazado');
509
+ }
510
+
511
+ function test_hmac_no_key() {
512
+ delete process.env.LBH_SECRET;
513
+ try {
514
+ sdk.generateHmac('test', null);
515
+ assert(false, 'hmac: sin clave deberΓ­a lanzar error');
516
+ } catch (e) {
517
+ assert(true, 'hmac: sin clave lanza error correctamente');
518
+ }
519
+ }
520
+
521
+ // ── Tests de sello ───────────────────────────────────────────
522
+ function test_seal_and_verify() {
523
+ const key = 'test-secret-lbh';
524
+ const content = Buffer.from('documento de prueba HormigasAIS');
525
+ const sello = sdk.sealAsset(content, 'CLHQ', key);
526
+
527
+ assert(sello.sha256.length === 64, 'seal: sha256 tiene 64 chars');
528
+ assert(sello.firma.length === 64, 'seal: firma tiene 64 chars');
529
+ assert(sello.owner === 'CLHQ', 'seal: owner correcto');
530
+
531
+ const result = sdk.verifySeal(content, sello, key);
532
+ assert(result.valid === true, 'seal: verificaciΓ³n correcta');
533
+ }
534
+
535
+ function test_verify_tampered_content() {
536
+ const key = 'test-secret-lbh';
537
+ const content = Buffer.from('contenido original');
538
+ const sello = sdk.sealAsset(content, 'CLHQ', key);
539
+ const tampered = Buffer.from('contenido modificado');
540
+
541
+ const result = sdk.verifySeal(tampered, sello, key);
542
+ assert(result.valid === false, 'seal: contenido modificado rechazado');
543
+ assert(result.reason.includes('SHA256'), 'seal: reason menciona SHA256');
544
+ }
545
+
546
+ // ── Tests de excepciones ─────────────────────────────────────
547
+ function test_invalid_packet_error() {
548
+ try {
549
+ sdk.encodePacket('1234', sdk.TYPE_CODES.SEAL, 'payload');
550
+ assert(false, 'exception: header corto deberΓ­a lanzar error');
551
+ } catch (e) {
552
+ assert(
553
+ e instanceof sdk.InvalidPacketError,
554
+ 'exception: InvalidPacketError lanzado correctamente'
555
+ );
556
+ }
557
+ }
558
+
559
+ // ── Correr todos los tests ───────────────────────────────────
560
+ console.log('');
561
+ console.log('🐜 Suite de conformidad LBH_SPEC_v2.0 β€” JavaScript SDK');
562
+ console.log('------------------------------------------------------------');
563
+
564
+ test_encode_decode_roundtrip();
565
+ test_validate_valid_packet();
566
+ test_validate_short_frame();
567
+ test_validate_length_mismatch();
568
+ test_type_codes();
569
+ test_type_code_invalid();
570
+ test_make_header();
571
+ test_hmac_generate_validate();
572
+ test_hmac_no_key();
573
+ test_seal_and_verify();
574
+ test_verify_tampered_content();
575
+ test_invalid_packet_error();
576
+
577
+ console.log('');
578
+ console.log('------------------------------------------------------------');
579
+ console.log(`Resultado: ${passed}/${passed + failed} tests pasaron`);
580
+ if (failed === 0) {
581
+ console.log('βœ… SDK conforme con LBH_SPEC_v2.0');
582
+ process.exit(0);
583
+ } else {
584
+ console.log(`❌ ${failed} test(s) fallaron β€” revisar antes de publicar`);
585
+ process.exit(1);
586
+ }
587
+ TESTEOF
588
+ echo "βœ… tests/test_sdk.js creado"
589
+
590
+ # ── 6. README.md ─────────────────────────────────────────────
591
+ cat > README.md << 'READMEOF'
592
+ # lbh-sdk-js 🐜
593
+
594
+ **SDK oficial del Protocolo LBH v2.0 para JavaScript/Node.js β€” HormigasAIS**
595
+
596
+ ImplementaciΓ³n pΓΊblica basada en la especificaciΓ³n LBH_SPEC_v2.0.md.
597
+
598
+ ## InstalaciΓ³n
599
+
600
+ ```bash
601
+ npm install @hormigasais/lbh-sdk
602
+ ```
603
+
604
+ O sin npm:
605
+ ```bash
606
+ git clone https://github.com/Thrumanshow/lbh-sdk-js.git
607
+ ```
608
+
609
+ ## Uso rΓ‘pido
610
+
611
+ ```javascript
612
+ const sdk = require('@hormigasais/lbh-sdk');
613
+
614
+ // Codificar un paquete SEAL
615
+ const frame = sdk.encodePacket(
616
+ sdk.makeHeader('A16', '00'),
617
+ sdk.typeCode('SEAL'),
618
+ '{"asset":"documento.pdf","owner":"CLHQ"}'
619
+ );
620
+
621
+ // Decodificar
622
+ const decoded = sdk.decodePacket(frame);
623
+ console.log(decoded.typeName); // β†’ "SEAL"
624
+ console.log(decoded.payload); // β†’ '{"asset":"documento.pdf",...}'
625
+
626
+ // Validar
627
+ const { valid } = sdk.validatePacket(frame);
628
+ console.log(valid); // β†’ true
629
+
630
+ // Sellar un activo
631
+ const fs = require('fs');
632
+ const sello = sdk.sealAsset(fs.readFileSync('documento.pdf'), 'CLHQ', 'tu_clave');
633
+
634
+ // Verificar
635
+ const { valid: ok } = sdk.verifySeal(fs.readFileSync('documento.pdf'), sello, 'tu_clave');
636
+ ```
637
+
638
+ ## Tests
639
+
640
+ ```bash
641
+ node tests/test_sdk.js
642
+ ```
643
+
644
+ ## DocumentaciΓ³n relacionada
645
+
646
+ - **EspecificaciΓ³n LBH_SPEC_v2.0**: [Lenguaje-Binario-HormigasAIS-](https://github.com/Thrumanshow/Lenguaje-Binario-HormigasAIS-)
647
+ - **SDK Python**: [lbh-sdk](https://github.com/Thrumanshow/lbh-sdk) β€” `pip install lbh-sdk`
648
+ - **Web**: [hormigasais.com](https://hormigasais.com)
649
+ - **DOI**: 10.5281/zenodo.19177759
650
+
651
+ ## Autor
652
+
653
+ **Cristhiam Leonardo HernΓ‘ndez QuiΓ±onez (CLHQ)**
654
+ Fundador de HormigasAIS β€” Nodo A16 Β· San Miguel Β· El Salvador
655
+
656
+ CERT::LBH-SDK-JS-V2-CLHQ
657
+ READMEOF
658
+ echo "βœ… README.md creado"
659
+
660
+ # ── 7. LICENSE ────────────────────────────────────────────────
661
+ cat > LICENSE << 'LICEOF'
662
+ MIT License
663
+
664
+ Copyright (c) 2026 Cristhiam Leonardo HernΓ‘ndez QuiΓ±onez (CLHQ)
665
+ HormigasAIS β€” Nodo A16-SanMiguel-SV
666
+
667
+ Permission is hereby granted, free of charge, to any person obtaining a copy
668
+ of this software and associated documentation files (the "Software"), to deal
669
+ in the Software without restriction, including without limitation the rights
670
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
671
+ copies of the Software, and to permit persons to whom the Software is
672
+ furnished to do so, subject to the following conditions:
673
+
674
+ The above copyright notice and this permission notice shall be included in all
675
+ copies or substantial portions of the Software.
676
+
677
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
678
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
679
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
680
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
681
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
682
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
683
+ SOFTWARE.
684
+ LICEOF
685
+ echo "βœ… LICENSE creado"
686
+
687
+ # ── 8. .gitignore ─────────────────────────────────────────────
688
+ cat > .gitignore << 'GITEOF'
689
+ node_modules/
690
+ .npmrc
691
+ *.log
692
+ GITEOF
693
+ echo "βœ… .gitignore creado"
694
+
695
+ # ── 9. Correr tests ──────────────────────────────────────────
696
+ echo ""
697
+ echo "------------------------------------------------------------"
698
+ echo "πŸ” Corriendo tests de conformidad..."
699
+ echo "------------------------------------------------------------"
700
+ node tests/test_sdk.js
701
+
702
+ EXIT_TESTS=$?
703
+ if [ $EXIT_TESTS -ne 0 ]; then
704
+ echo "❌ Tests fallaron. NO se harÑ commit hasta que pasen."
705
+ exit 1
706
+ fi
707
+
708
+ # ── 10. Commit y push ────────────────────────────────────────
709
+ echo ""
710
+ echo "πŸš€ Tests OK β€” haciendo commit..."
711
+
712
+ git config user.email "clhq@hormigasais.com"
713
+ git config user.name "CLHQ β€” HormigasAIS"
714
+
715
+ git add .
716
+ git commit -m "🐜 INIT: lbh-sdk-js v0.3.0 β€” SDK JavaScript conforme LBH_SPEC_v2.0 (encode/decode/validate/hmac/seal)"
717
+ git push origin main
718
+
719
+ echo ""
720
+ echo "------------------------------------------------------------"
721
+ echo "βœ… lbh-sdk-js publicado en github.com/Thrumanshow/lbh-sdk-js"
722
+ echo ""
723
+ echo "Estructura:"
724
+ echo " src/"
725
+ echo " index.js ← mΓ³dulo principal"
726
+ echo " constants.js ← TYPE_CODES"
727
+ echo " exceptions.js ← errores personalizados"
728
+ echo " tests/"
729
+ echo " test_sdk.js ← suite de conformidad"
730
+ echo " package.json"
731
+ echo " README.md"
732
+ echo " LICENSE (MIT)"
733
+ echo "------------------------------------------------------------"
734
+
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "lbh-sdk-hormigasais",
3
+ "version": "0.3.0",
4
+ "description": "SDK oficial de referencia del protocolo LBH (Lenguaje Binario HormigasAIS) para JavaScript/Node.js",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "node tests/test_sdk.js"
8
+ },
9
+ "keywords": [
10
+ "lbh",
11
+ "hormigasais",
12
+ "protocol",
13
+ "sdk",
14
+ "edge-computing",
15
+ "hmac",
16
+ "binary-protocol",
17
+ "iot",
18
+ "m2m"
19
+ ],
20
+ "author": "Cristhiam Leonardo HernΓ‘ndez QuiΓ±onez (CLHQ) <clhq@hormigasais.com>",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/Thrumanshow/lbh-sdk-js.git"
25
+ },
26
+ "homepage": "https://hormigasais.com",
27
+ "engines": {
28
+ "node": ">=14.0.0"
29
+ }
30
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * constants.js β€” TYPE_CODEs oficiales LBH v2.0
3
+ * Propuesta formal de estandarizaciΓ³n segΓΊn LBH_SPEC_v2.0.md
4
+ * En v1.x el campo es libre; en v2.0 se estandariza este enum.
5
+ */
6
+
7
+ const TYPE_CODES = {
8
+ SEAL: '5345414c', // EmisiΓ³n de sello criptogrΓ‘fico
9
+ VERI: '56455249', // VerificaciΓ³n de activo sellado
10
+ SYNC: '53594e43', // SincronizaciΓ³n entre nodos
11
+ PING: '50494e47', // Latido / health check
12
+ FUEL: '4655454c', // Feromona de activaciΓ³n
13
+ ACKK: '41434b4b', // ConfirmaciΓ³n de recepciΓ³n
14
+ ERRR: '45525252', // Error / rechazo
15
+ };
16
+
17
+ const TYPE_CODES_REVERSE = Object.fromEntries(
18
+ Object.entries(TYPE_CODES).map(([k, v]) => [v, k])
19
+ );
20
+
21
+ module.exports = { TYPE_CODES, TYPE_CODES_REVERSE };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * exceptions.js β€” Errores personalizados LBH SDK
3
+ */
4
+
5
+ class InvalidPacketError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = 'InvalidPacketError';
9
+ }
10
+ }
11
+
12
+ class InvalidPayloadError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = 'InvalidPayloadError';
16
+ }
17
+ }
18
+
19
+ module.exports = { InvalidPacketError, InvalidPayloadError };
package/src/index.js ADDED
@@ -0,0 +1,306 @@
1
+ /**
2
+ * lbh-sdk β€” Lenguaje Binario HormigasAIS SDK (JavaScript)
3
+ * =========================================================
4
+ *
5
+ * SDK de referencia para el Protocolo LBH v2.0.
6
+ * Permite codificar, decodificar, validar y firmar
7
+ * paquetes LBH sin acceso al nodo A16.
8
+ *
9
+ * EspecificaciΓ³n: LBH_SPEC_v2.0.md
10
+ * Repo spec: github.com/Thrumanshow/Lenguaje-Binario-HormigasAIS-
11
+ * Repo SDK JS: github.com/Thrumanshow/lbh-sdk-js
12
+ * Autor: Cristhiam Leonardo HernΓ‘ndez QuiΓ±onez (CLHQ)
13
+ * Web: https://hormigasais.com
14
+ * DOI: 10.5281/zenodo.19177759
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const crypto = require('crypto');
20
+ const { TYPE_CODES, TYPE_CODES_REVERSE } = require('./constants');
21
+ const { InvalidPacketError, InvalidPayloadError } = require('./exceptions');
22
+
23
+ const VERSION = '0.3.0';
24
+ const SPEC = 'LBH_SPEC_v2.0';
25
+ const AUTHOR = 'CLHQ β€” HormigasAIS';
26
+
27
+ // ─────────────────────────────────────────────────────────────
28
+ // ENCODER
29
+ // ─────────────────────────────────────────────────────────────
30
+ /**
31
+ * Codifica un paquete LBH v2.0.
32
+ *
33
+ * @param {string} header - 8 chars hex β€” ID nodo + versiΓ³n
34
+ * @param {string} typeCode - 8 chars hex β€” tipo de operaciΓ³n
35
+ * @param {string} payload - string UTF-8 β€” contenido JSON
36
+ * @returns {string} Frame completo como string hexadecimal
37
+ * @throws {InvalidPacketError} si header o typeCode son invΓ‘lidos
38
+ */
39
+ function encodePacket(header, typeCode, payload) {
40
+ if (header.length !== 8) {
41
+ throw new InvalidPacketError(
42
+ `header debe tener 8 chars hex, tiene ${header.length}`
43
+ );
44
+ }
45
+ if (typeCode.length !== 8) {
46
+ throw new InvalidPacketError(
47
+ `typeCode debe tener 8 chars hex, tiene ${typeCode.length}`
48
+ );
49
+ }
50
+
51
+ const payloadHex = Buffer.from(payload, 'utf-8').toString('hex');
52
+ const length = (payloadHex.length / 2).toString(16).padStart(8, '0');
53
+ return `${header}${typeCode}${length}${payloadHex}`;
54
+ }
55
+
56
+ // ─────────────────────────────────────────────────────────────
57
+ // DECODER
58
+ // ─────────────────────────────────────────────────────────────
59
+ /**
60
+ * Decodifica un paquete LBH v2.0.
61
+ *
62
+ * @param {string} frame - string hexadecimal completo
63
+ * @returns {object} { header, typeCode, typeName, length, payload }
64
+ * o { error: string } si el frame es invΓ‘lido
65
+ */
66
+ function decodePacket(frame) {
67
+ try {
68
+ if (frame.length < 24) {
69
+ return { error: `Frame demasiado corto: ${frame.length} chars (mΓ­nimo 24)` };
70
+ }
71
+
72
+ const header = frame.slice(0, 8);
73
+ const typeCode = frame.slice(8, 16);
74
+ const lengthHex = frame.slice(16, 24);
75
+ const payloadHex = frame.slice(24);
76
+
77
+ const payload = Buffer.from(payloadHex, 'hex').toString('utf-8');
78
+ const length = parseInt(lengthHex, 16);
79
+ const typeName = TYPE_CODES_REVERSE[typeCode] || null;
80
+
81
+ return { header, typeCode, typeName, length, payload };
82
+ } catch (e) {
83
+ return { error: e.message };
84
+ }
85
+ }
86
+
87
+ // ─────────────────────────────────────────────────────────────
88
+ // VALIDADOR
89
+ // ─────────────────────────────────────────────────────────────
90
+ /**
91
+ * Valida un paquete LBH v2.0 segΓΊn LBH_SPEC_v2.0 Β§6.
92
+ *
93
+ * @param {string} frame - string hexadecimal completo
94
+ * @returns {{ valid: boolean, reason: string|null }}
95
+ */
96
+ function validatePacket(frame) {
97
+ if (frame.length < 24) {
98
+ return { valid: false, reason: `Longitud mΓ­nima no cumplida: ${frame.length} < 24` };
99
+ }
100
+
101
+ let declaredLength;
102
+ try {
103
+ declaredLength = parseInt(frame.slice(16, 24), 16);
104
+ } catch (e) {
105
+ return { valid: false, reason: 'Campo LENGTH no es hex vΓ‘lido' };
106
+ }
107
+
108
+ let payloadBytes;
109
+ try {
110
+ payloadBytes = Buffer.from(frame.slice(24), 'hex');
111
+ } catch (e) {
112
+ return { valid: false, reason: 'PAYLOAD no es hex vΓ‘lido' };
113
+ }
114
+
115
+ try {
116
+ payloadBytes.toString('utf-8');
117
+ } catch (e) {
118
+ return { valid: false, reason: 'PAYLOAD no decodifica como UTF-8' };
119
+ }
120
+
121
+ if (declaredLength !== payloadBytes.length) {
122
+ return {
123
+ valid: false,
124
+ reason: `LENGTH declarado (${declaredLength}) no coincide con bytes reales (${payloadBytes.length})`
125
+ };
126
+ }
127
+
128
+ return { valid: true, reason: null };
129
+ }
130
+
131
+ // ─────────────────────────────────────────────────────────────
132
+ // SEGURIDAD β€” HMAC-SHA256
133
+ // ─────────────────────────────────────────────────────────────
134
+ /**
135
+ * Genera HMAC-SHA256 para un mensaje LBH.
136
+ *
137
+ * @param {string} message - frame o mensaje a firmar
138
+ * @param {string} secretKey - clave secreta (cargar desde env LBH_SECRET)
139
+ * @returns {string} hexdigest de 64 chars
140
+ */
141
+ function generateHmac(message, secretKey) {
142
+ const key = secretKey || process.env.LBH_SECRET;
143
+ if (!key) {
144
+ throw new Error(
145
+ 'Se requiere secretKey o la variable de entorno LBH_SECRET. ' +
146
+ 'Nunca hardcodees la clave.'
147
+ );
148
+ }
149
+ return crypto
150
+ .createHmac('sha256', key)
151
+ .update(message, 'utf-8')
152
+ .digest('hex');
153
+ }
154
+
155
+ /**
156
+ * Valida un HMAC-SHA256 usando comparaciΓ³n segura (timing-safe).
157
+ *
158
+ * @param {string} message - mensaje original
159
+ * @param {string} secretKey - clave secreta
160
+ * @param {string} receivedHmac - HMAC a validar
161
+ * @returns {boolean}
162
+ */
163
+ function validateHmac(message, secretKey, receivedHmac) {
164
+ const expected = generateHmac(message, secretKey);
165
+ try {
166
+ return crypto.timingSafeEqual(
167
+ Buffer.from(expected, 'hex'),
168
+ Buffer.from(receivedHmac, 'hex')
169
+ );
170
+ } catch (e) {
171
+ return false;
172
+ }
173
+ }
174
+
175
+ // ─────────────────────────────────────────────────────────────
176
+ // SELLO DE ACTIVOS
177
+ // ─────────────────────────────────────────────────────────────
178
+ /**
179
+ * Genera un sello LBH para un activo digital.
180
+ *
181
+ * @param {Buffer|string} content - contenido binario del activo
182
+ * @param {string} owner - propietario (ej: "CLHQ")
183
+ * @param {string} secretKey - clave HMAC (o usa LBH_SECRET del env)
184
+ * @returns {object} sello con sha256, timestamp, firma, owner
185
+ */
186
+ function sealAsset(content, owner, secretKey) {
187
+ const key = secretKey || process.env.LBH_SECRET;
188
+ if (!key) {
189
+ throw new Error('Se requiere secretKey o LBH_SECRET en el entorno.');
190
+ }
191
+
192
+ const buf = Buffer.isBuffer(content) ? content : Buffer.from(content);
193
+ const sha256 = crypto.createHash('sha256').update(buf).digest('hex');
194
+ const timestamp = Math.floor(Date.now() / 1000);
195
+ const payloadFirma = `${sha256}|${owner}|${timestamp}`;
196
+
197
+ const firma = crypto
198
+ .createHmac('sha256', key)
199
+ .update(payloadFirma, 'utf-8')
200
+ .digest('hex');
201
+
202
+ return {
203
+ sha256,
204
+ owner,
205
+ timestamp,
206
+ payloadFirma,
207
+ firma,
208
+ versionLbh: SPEC,
209
+ autoridad: 'CLHQ',
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Verifica la integridad y autenticidad de un sello LBH.
215
+ *
216
+ * @param {Buffer|string} content - contenido original del activo
217
+ * @param {object} sello - objeto retornado por sealAsset()
218
+ * @param {string} secretKey - clave HMAC
219
+ * @returns {{ valid: boolean, reason: string|null }}
220
+ */
221
+ function verifySeal(content, sello, secretKey) {
222
+ const key = secretKey || process.env.LBH_SECRET;
223
+ if (!key) {
224
+ throw new Error('Se requiere secretKey o LBH_SECRET en el entorno.');
225
+ }
226
+
227
+ const buf = Buffer.isBuffer(content) ? content : Buffer.from(content);
228
+ const sha256Actual = crypto.createHash('sha256').update(buf).digest('hex');
229
+
230
+ if (sha256Actual !== sello.sha256) {
231
+ return { valid: false, reason: 'SHA256 no coincide β€” activo modificado' };
232
+ }
233
+
234
+ const firmaEsperada = crypto
235
+ .createHmac('sha256', key)
236
+ .update(sello.payloadFirma, 'utf-8')
237
+ .digest('hex');
238
+
239
+ try {
240
+ const match = crypto.timingSafeEqual(
241
+ Buffer.from(firmaEsperada, 'hex'),
242
+ Buffer.from(sello.firma, 'hex')
243
+ );
244
+ if (!match) {
245
+ return { valid: false, reason: 'HMAC invΓ‘lido β€” firma no autΓ©ntica' };
246
+ }
247
+ } catch (e) {
248
+ return { valid: false, reason: 'Error comparando firmas' };
249
+ }
250
+
251
+ return { valid: true, reason: null };
252
+ }
253
+
254
+ // ─────────────────────────────────────────────────────────────
255
+ // HELPERS
256
+ // ─────────────────────────────────────────────────────────────
257
+ /**
258
+ * Genera un HEADER LBH de 8 chars hex.
259
+ *
260
+ * @param {string} nodeId - ID del nodo (ej: "A16")
261
+ * @param {string} version - versiΓ³n (ej: "00")
262
+ * @returns {string} 8 chars hex
263
+ */
264
+ function makeHeader(nodeId = 'A16', version = '00') {
265
+ const idHex = Buffer.from(nodeId, 'utf-8').toString('hex').slice(0, 4).padEnd(4, '0');
266
+ const verHex = Buffer.from(version, 'utf-8').toString('hex').slice(0, 4).padEnd(4, '0');
267
+ return `${idHex}${verHex}`;
268
+ }
269
+
270
+ /**
271
+ * Retorna el hex de un TYPE_CODE por nombre.
272
+ *
273
+ * @param {string} name - "SEAL", "VERI", "SYNC", etc.
274
+ * @returns {string} 8 chars hex
275
+ * @throws {Error} si el nombre no estΓ‘ en TYPE_CODES
276
+ */
277
+ function typeCode(name) {
278
+ if (!TYPE_CODES[name]) {
279
+ throw new Error(
280
+ `TYPE_CODE '${name}' no reconocido. VΓ‘lidos: ${Object.keys(TYPE_CODES).join(', ')}`
281
+ );
282
+ }
283
+ return TYPE_CODES[name];
284
+ }
285
+
286
+ // ─────────────────────────────────────────────────────────────
287
+ // EXPORTS
288
+ // ─────────────────────────────────────────────────────────────
289
+ module.exports = {
290
+ VERSION,
291
+ SPEC,
292
+ AUTHOR,
293
+ TYPE_CODES,
294
+ TYPE_CODES_REVERSE,
295
+ encodePacket,
296
+ decodePacket,
297
+ validatePacket,
298
+ generateHmac,
299
+ validateHmac,
300
+ sealAsset,
301
+ verifySeal,
302
+ makeHeader,
303
+ typeCode,
304
+ InvalidPacketError,
305
+ InvalidPayloadError,
306
+ };
@@ -0,0 +1,165 @@
1
+ /**
2
+ * test_sdk.js β€” Suite de conformidad LBH_SPEC_v2.0 (JavaScript)
3
+ * Verifica que lbh-sdk-js cumple la especificaciΓ³n formal.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const sdk = require('../src/index');
9
+
10
+ let passed = 0;
11
+ let failed = 0;
12
+
13
+ function assert(condition, testName, detail = '') {
14
+ if (condition) {
15
+ console.log(`βœ… ${testName}`);
16
+ passed++;
17
+ } else {
18
+ console.log(`❌ ${testName}${detail ? ': ' + detail : ''}`);
19
+ failed++;
20
+ }
21
+ }
22
+
23
+ // ── Tests de encoding ────────────────────────────────────────
24
+ function test_encode_decode_roundtrip() {
25
+ const header = '41313600';
26
+ const tc = sdk.TYPE_CODES.SEAL;
27
+ const payload = '{"asset":"test.pdf","owner":"CLHQ"}';
28
+ const frame = sdk.encodePacket(header, tc, payload);
29
+ const decoded = sdk.decodePacket(frame);
30
+
31
+ assert(decoded.header === header, 'encode_decode: header correcto');
32
+ assert(decoded.typeCode === tc, 'encode_decode: typeCode correcto');
33
+ assert(decoded.typeName === 'SEAL', 'encode_decode: typeName correcto');
34
+ assert(decoded.payload === payload, 'encode_decode: payload correcto');
35
+ }
36
+
37
+ // ── Tests de validaciΓ³n ──────────────────────────────────────
38
+ function test_validate_valid_packet() {
39
+ const frame = sdk.encodePacket('41313600', sdk.TYPE_CODES.PING, 'ping');
40
+ const result = sdk.validatePacket(frame);
41
+ assert(result.valid === true, 'validate: paquete vΓ‘lido aceptado');
42
+ }
43
+
44
+ function test_validate_short_frame() {
45
+ const result = sdk.validatePacket('41313600');
46
+ assert(result.valid === false, 'validate: frame corto rechazado');
47
+ assert(result.reason.includes('24'), 'validate: reason menciona mΓ­nimo 24');
48
+ }
49
+
50
+ function test_validate_length_mismatch() {
51
+ // LENGTH declara 1 byte pero payload tiene mΓ‘s
52
+ const result = sdk.validatePacket('4131360053594e430000000541');
53
+ assert(result.valid === false, 'validate: length mismatch rechazado');
54
+ }
55
+
56
+ // ── Tests de TYPE_CODES ──────────────────────────────────────
57
+ function test_type_codes() {
58
+ assert(sdk.typeCode('SEAL') === '5345414c', 'typeCode: SEAL correcto');
59
+ assert(sdk.typeCode('VERI') === '56455249', 'typeCode: VERI correcto');
60
+ assert(sdk.typeCode('PING') === '50494e47', 'typeCode: PING correcto');
61
+ assert(sdk.typeCode('SYNC') === '53594e43', 'typeCode: SYNC correcto');
62
+ }
63
+
64
+ function test_type_code_invalid() {
65
+ try {
66
+ sdk.typeCode('INVALID');
67
+ assert(false, 'typeCode: invΓ‘lido deberΓ­a lanzar error');
68
+ } catch (e) {
69
+ assert(true, 'typeCode: invΓ‘lido lanza error correctamente');
70
+ }
71
+ }
72
+
73
+ // ── Tests de makeHeader ──────────────────────────────────────
74
+ function test_make_header() {
75
+ const h = sdk.makeHeader('A16', '00');
76
+ assert(h.length === 8, `makeHeader: longitud 8 chars (tiene ${h.length})`);
77
+ }
78
+
79
+ // ── Tests de HMAC ────────────────────────────────────────────
80
+ function test_hmac_generate_validate() {
81
+ const key = 'test-secret-lbh';
82
+ const message = 'frame-lbh-test';
83
+ const digest = sdk.generateHmac(message, key);
84
+
85
+ assert(digest.length === 64, 'hmac: digest tiene 64 chars');
86
+ assert(sdk.validateHmac(message, key, digest), 'hmac: validaciΓ³n correcta');
87
+ assert(!sdk.validateHmac(message, key, '0'.repeat(64)), 'hmac: digest incorrecto rechazado');
88
+ }
89
+
90
+ function test_hmac_no_key() {
91
+ delete process.env.LBH_SECRET;
92
+ try {
93
+ sdk.generateHmac('test', null);
94
+ assert(false, 'hmac: sin clave deberΓ­a lanzar error');
95
+ } catch (e) {
96
+ assert(true, 'hmac: sin clave lanza error correctamente');
97
+ }
98
+ }
99
+
100
+ // ── Tests de sello ───────────────────────────────────────────
101
+ function test_seal_and_verify() {
102
+ const key = 'test-secret-lbh';
103
+ const content = Buffer.from('documento de prueba HormigasAIS');
104
+ const sello = sdk.sealAsset(content, 'CLHQ', key);
105
+
106
+ assert(sello.sha256.length === 64, 'seal: sha256 tiene 64 chars');
107
+ assert(sello.firma.length === 64, 'seal: firma tiene 64 chars');
108
+ assert(sello.owner === 'CLHQ', 'seal: owner correcto');
109
+
110
+ const result = sdk.verifySeal(content, sello, key);
111
+ assert(result.valid === true, 'seal: verificaciΓ³n correcta');
112
+ }
113
+
114
+ function test_verify_tampered_content() {
115
+ const key = 'test-secret-lbh';
116
+ const content = Buffer.from('contenido original');
117
+ const sello = sdk.sealAsset(content, 'CLHQ', key);
118
+ const tampered = Buffer.from('contenido modificado');
119
+
120
+ const result = sdk.verifySeal(tampered, sello, key);
121
+ assert(result.valid === false, 'seal: contenido modificado rechazado');
122
+ assert(result.reason.includes('SHA256'), 'seal: reason menciona SHA256');
123
+ }
124
+
125
+ // ── Tests de excepciones ─────────────────────────────────────
126
+ function test_invalid_packet_error() {
127
+ try {
128
+ sdk.encodePacket('1234', sdk.TYPE_CODES.SEAL, 'payload');
129
+ assert(false, 'exception: header corto deberΓ­a lanzar error');
130
+ } catch (e) {
131
+ assert(
132
+ e instanceof sdk.InvalidPacketError,
133
+ 'exception: InvalidPacketError lanzado correctamente'
134
+ );
135
+ }
136
+ }
137
+
138
+ // ── Correr todos los tests ───────────────────────────────────
139
+ console.log('');
140
+ console.log('🐜 Suite de conformidad LBH_SPEC_v2.0 β€” JavaScript SDK');
141
+ console.log('------------------------------------------------------------');
142
+
143
+ test_encode_decode_roundtrip();
144
+ test_validate_valid_packet();
145
+ test_validate_short_frame();
146
+ test_validate_length_mismatch();
147
+ test_type_codes();
148
+ test_type_code_invalid();
149
+ test_make_header();
150
+ test_hmac_generate_validate();
151
+ test_hmac_no_key();
152
+ test_seal_and_verify();
153
+ test_verify_tampered_content();
154
+ test_invalid_packet_error();
155
+
156
+ console.log('');
157
+ console.log('------------------------------------------------------------');
158
+ console.log(`Resultado: ${passed}/${passed + failed} tests pasaron`);
159
+ if (failed === 0) {
160
+ console.log('βœ… SDK conforme con LBH_SPEC_v2.0');
161
+ process.exit(0);
162
+ } else {
163
+ console.log(`❌ ${failed} test(s) fallaron β€” revisar antes de publicar`);
164
+ process.exit(1);
165
+ }