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 +22 -0
- package/README.md +65 -0
- package/init_lbh_sdk_js.sh +734 -0
- package/package.json +30 -0
- package/src/constants.js +21 -0
- package/src/exceptions.js +19 -0
- package/src/index.js +306 -0
- package/tests/test_sdk.js +165 -0
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
|
+
}
|
package/src/constants.js
ADDED
|
@@ -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
|
+
}
|