@nyx5/nyx5 0.1.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.
@@ -0,0 +1,117 @@
1
+ // Demo del Libro: una casa (alfa.local) con su ledger, y agentes que cotizan, aceptan, entregan,
2
+ // afianzan, delegan y pagan estampillas. Un agente de otra casa (beta.local) también participa:
3
+ // su identidad viene verificada por el Correo, así que puede tener cuenta aquí sin login.
4
+ //
5
+ // node demo/contratos.js
6
+
7
+ import fs from 'node:fs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { Estafeta } from '../src/correo/estafeta.js';
11
+ import { Agent } from '../src/correo/agente.js';
12
+ import { sha256hex } from '../src/nucleo/crypto.js';
13
+
14
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'chasqui-libro-'));
15
+ const hosts = { 'alfa.local': { url: 'http://127.0.0.1:4001' }, 'beta.local': { url: 'http://127.0.0.1:4002' } };
16
+ const mk = (domain, port, token, libro) => new Estafeta({ domain, port, dataDir: path.join(tmp, domain), adminToken: token, hosts, workerIntervalMs: 200, libro, log: () => {} });
17
+ const step = (n, t) => console.log(`\n[${n}] ${t}`);
18
+ const H = 'alfa.local';
19
+
20
+ const alfa = await mk('alfa.local', 4001, 'a', { feePct: 0.10, welcome: 100 }).start();
21
+ const beta = await mk('beta.local', 4002, 'b').start();
22
+ const nicolas = Agent.create('nicolas@alfa.local', 'http://127.0.0.1:4001', { hosts });
23
+ const verifica = Agent.create('verifica@alfa.local', 'http://127.0.0.1:4001', { hosts });
24
+ const constructor = Agent.create('constructor@alfa.local', 'http://127.0.0.1:4001', { hosts });
25
+ const foraneo = Agent.create('foraneo@beta.local', 'http://127.0.0.1:4002', { hosts });
26
+ for (const a of [nicolas, verifica, constructor]) await a.register({ adminToken: 'a' });
27
+ await foraneo.register({ adminToken: 'b' });
28
+ const saldos = async () => console.log(' saldos:', Object.entries(await alfa.store.libroState().balances).map(([k, v]) => `${k.replace('@alfa.local', '')}=${v}`).join(' '));
29
+
30
+ step(1, 'La casa carga saldo a nicolas (1000). Los demás recibieron 100 de bienvenida al registrarse');
31
+ await alfa.libro.topup('nicolas@alfa.local', 1000, 'presupuesto mensual Sigo');
32
+ await saldos();
33
+
34
+ step(2, 'SPOT: verifica cotiza a nicolas una verificación por 40; nicolas acepta; el asiento se ejecuta con 10 % para la casa');
35
+ const q1 = await verifica.quote({ to: 'nicolas@alfa.local', contract: 'spot', price: 40, concept: 'verificación de despliegue' });
36
+ const m1 = await nicolas.waitFor((e) => e.id === q1.id);
37
+ const cot = (await nicolas.open(m1.envelope)).content.body;
38
+ console.log(' cotización recibida (cifrada en tránsito), firmada por', cot.seller, '->', cot.price, 'tok');
39
+ const acc = await nicolas.accept(cot);
40
+ const r1 = await nicolas.awaitReceipt(acc.id);
41
+ console.log(' recibo de libro@:', r1.receipt.contract.state, '| asiento', r1.receipt.asiento.n, r1.receipt.asiento.lines.map((l) => `${l.account.replace('@alfa.local', '')}:${l.delta}`).join(' '));
42
+ console.log(' el recibo lleva hash de la cotización y de la aceptación:', r1.receipt.cotizacion_sha256.slice(0, 12) + '…', r1.receipt.op_sha256.slice(0, 12) + '…');
43
+ await saldos();
44
+
45
+ step(3, 'ESCROW: nicolas encarga a constructor por 300 con criterio de aceptación; los tokens quedan retenidos');
46
+ const q2 = await constructor.quote({ to: 'nicolas@alfa.local', contract: 'escrow', price: 300, concept: 'landing Drake Review', terms: { acceptance: 'lighthouse >= 90 en móvil', deadline: '2026-09-15' }, arbiter: 'verifica@alfa.local' });
47
+ const cot2 = (await nicolas.open((await nicolas.waitFor((e) => e.id === q2.id)).envelope)).content.body;
48
+ const acc2 = await nicolas.accept(cot2);
49
+ const r2 = await nicolas.awaitReceipt(acc2.id);
50
+ const contrato = r2.receipt.contract.id;
51
+ console.log(' contrato', contrato.slice(0, 8), 'estado', r2.receipt.contract.state);
52
+ await saldos();
53
+ console.log(' constructor entrega con hash de la evidencia');
54
+ const evidencia = sha256hex('reporte lighthouse: performance 94');
55
+ const del = await constructor.deliver(H, contrato, { evidence_sha256: evidencia, note: 'lighthouse 94 móvil' });
56
+ await constructor.awaitReceipt(del.id);
57
+ console.log(' nicolas libera: constructor recibe 270, la casa 30');
58
+ const rel = await nicolas.release(H, contrato);
59
+ const r3 = await nicolas.awaitReceipt(rel.id);
60
+ console.log(' estado', r3.receipt.contract.state, '| historial:', r3.receipt.contract.history.map((h) => h.op).join(' -> '));
61
+ await saldos();
62
+
63
+ step(4, 'FIANZA: constructor afirma "desplegado y verificado" y deposita 50; verifica lo derriba y la fianza va a la casa');
64
+ const b = await constructor.bond(H, { amount: 50, claim: 'sigo.uk desplegado y verificado en producción', verifier: 'verifica@alfa.local', evidence_sha256: sha256hex('log de deploy') });
65
+ const rb = await constructor.awaitReceipt(b.id);
66
+ console.log(' fianza', rb.receipt.contract.id.slice(0, 8), 'estado', rb.receipt.contract.state);
67
+ const ff = await verifica.forfeit(H, rb.receipt.contract.id, 'el endpoint /health responde 500');
68
+ const rf = await verifica.awaitReceipt(ff.id);
69
+ console.log(' verifica ejecuta la fianza:', rf.receipt.contract.state, '| razón registrada:', rf.receipt.contract.history.at(-1).reason);
70
+ await saldos();
71
+
72
+ step(5, 'MANDATO EN CADENA: nicolas da mandato 200 a constructor; constructor sub-delega 80 a un subagente; el subagente cobra y paga nicolas');
73
+ const md = await nicolas.mandate(H, { grantee: 'constructor@alfa.local', cap: 200, scope: { concepts: ['tokens de modelo', 'hosting'] } });
74
+ const mandato = (await nicolas.awaitReceipt(md.id)).receipt.mandate;
75
+ console.log(' mandato raíz', mandato.id.slice(0, 8), 'cap', mandato.cap);
76
+ const tester = await constructor.delegate('tester', { scope: { types: ['message', 'task', 'result', 'receipt'], cap: 100 } });
77
+ console.log(' subagente delegado:', tester.address, '| tarjeta certificada por el dominio y firmada por constructor');
78
+ const sub = await constructor.mandate(H, { grantee: tester.address, cap: 80, parent: mandato.id });
79
+ const submandato = (await constructor.awaitReceipt(sub.id)).receipt.mandate;
80
+ console.log(' sub-mandato', submandato.id.slice(0, 8), 'cap', submandato.cap, 'cadena', submandato.chain.map((x) => x.slice(0, 8)).join(' > '));
81
+ const ch = await tester.charge(H, { mandate: submandato.id, amount: 30, concept: 'tokens de modelo' });
82
+ const rc = await tester.awaitReceipt(ch.id);
83
+ console.log(' cobro de 30: paga', rc.receipt.asiento.lines[0].account, '| cadena descontada:', rc.receipt.chain.map((x) => `${x.spent}/${x.cap}`).join(', '));
84
+ const over = await tester.charge(H, { mandate: submandato.id, amount: 70, concept: 'tokens de modelo' });
85
+ const bounce = await tester.waitFor((e) => e.type === 'receipt' && e.from === 'postmaster@alfa.local' && e.in_reply_to === over.id);
86
+ console.log(' cobro de 70 (supera lo que queda):', (await tester.open(bounce.envelope)).content.body.reason);
87
+ await saldos();
88
+
89
+ step(6, 'AGENTE DELEGADO CON ÁMBITO: tester solo puede enviar ciertos tipos y aceptar hasta 100');
90
+ try { await tester.send({ to: 'nicolas@alfa.local', type: 'intro', body: 'hola' }); } catch (e) { console.log(' intro bloqueado por la estafeta:', e.message); }
91
+ const q3 = await verifica.quote({ to: tester.address, contract: 'spot', price: 150, concept: 'auditoría' });
92
+ const cot3 = (await tester.open((await tester.waitFor((e) => e.id === q3.id)).envelope)).content.body;
93
+ const acc3 = await tester.accept(cot3);
94
+ const b3 = await tester.waitFor((e) => e.type === 'receipt' && e.from === 'postmaster@alfa.local' && e.in_reply_to === acc3.id);
95
+ console.log(' aceptar 150 con tope 100:', (await tester.open(b3.envelope)).content.body.reason);
96
+
97
+ step(7, 'ESTAMPILLA: caro@ cobra 5 tok por escribirle; un agente de otra casa paga desde su cuenta aquí; uno sin saldo rebota');
98
+ const caro = Agent.create('caro@alfa.local', 'http://127.0.0.1:4001', { hosts });
99
+ await caro.register({ adminToken: 'a', inbox: { policy: 'stamp', price: 5 } });
100
+ await alfa.libro.topup('foraneo@beta.local', 20, 'cuenta de un agente de otra casa');
101
+ const st = await foraneo.send({ to: 'caro@alfa.local', body: 'hola desde beta, con estampilla' });
102
+ const got = await caro.waitFor((e) => e.id === st.id);
103
+ console.log(' llegó con estampilla, asiento', got.stamp.slice(0, 8), '| stamp en el sobre:', JSON.stringify(st.envelope.stamp));
104
+ const pobre = Agent.create('pobre@beta.local', 'http://127.0.0.1:4002', { hosts });
105
+ await pobre.register({ adminToken: 'b' });
106
+ const st2 = await pobre.send({ to: 'caro@alfa.local', body: 'sin saldo' });
107
+ const b4 = await pobre.waitFor((e) => e.type === 'receipt' && e.in_reply_to === st2.id);
108
+ console.log(' sin saldo:', (await pobre.open(b4.envelope)).content.body.reason);
109
+ await saldos();
110
+
111
+ step(8, 'El diario: cada asiento firmado por la casa, con referencias a los sobres que lo causaron');
112
+ for (const a of await alfa.libro.journal()) console.log(` #${a.n} ${a.concept.slice(0, 48).padEnd(48)} ${a.lines.map((l) => `${l.account.replace('@alfa.local', '').replace('@beta.local', '@beta')}:${l.delta > 0 ? '+' : ''}${l.delta}`).join(' ')}`);
113
+ const total = Object.values(await alfa.store.libroState().balances).reduce((s, v) => s + v, 0);
114
+ console.log(' suma de todos los saldos (debe ser 0):', total);
115
+
116
+ await alfa.stop(); await beta.stop();
117
+ console.log(`\nListo. Datos en ${tmp}/alfa.local/libro`);
package/demo/e2e.js ADDED
@@ -0,0 +1,57 @@
1
+ // Demo end-to-end: dos dominios (alfa.local y beta.local), cada uno con su estafeta,
2
+ // un agente en cada uno, un envío cifrado y firmado, y la respuesta de vuelta.
3
+ //
4
+ // node demo/e2e.js
5
+
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { Estafeta } from '../src/correo/estafeta.js';
10
+ import { Agent } from '../src/correo/agente.js';
11
+
12
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'chasqui-'));
13
+ const hosts = { 'alfa.local': { url: 'http://127.0.0.1:4001' }, 'beta.local': { url: 'http://127.0.0.1:4002' } };
14
+ const step = (n, t) => console.log(`\n[${n}] ${t}`);
15
+
16
+ step(1, 'Levantando las estafetas de alfa.local (4001) y beta.local (4002)');
17
+ const alfa = await new Estafeta({ domain: 'alfa.local', port: 4001, dataDir: path.join(tmp, 'alfa'), adminToken: 'admin-alfa', hosts, workerIntervalMs: 300, log: () => {} }).start();
18
+ const beta = await new Estafeta({ domain: 'beta.local', port: 4002, dataDir: path.join(tmp, 'beta'), adminToken: 'admin-beta', hosts, workerIntervalMs: 300, log: () => {} }).start();
19
+ console.log(' clave pública de alfa.local:', alfa.keys.sig.slice(0, 16) + '…');
20
+ console.log(' clave pública de beta.local:', beta.keys.sig.slice(0, 16) + '…');
21
+
22
+ step(2, 'Registrando nicolas@alfa.local y asistente@beta.local (cada uno genera sus claves, el dominio las certifica)');
23
+ const nicolas = Agent.create('nicolas@alfa.local', 'http://127.0.0.1:4001', { hosts });
24
+ const asistente = Agent.create('asistente@beta.local', 'http://127.0.0.1:4002', { hosts });
25
+ await nicolas.register({ adminToken: 'admin-alfa' });
26
+ await asistente.register({ adminToken: 'admin-beta', capabilities: { accepts: ['application/json'], mcp: 'http://127.0.0.1:4010/mcp' } });
27
+ console.log(' tarjeta de asistente certificada por', (await nicolas.resolver.agentCard('asistente@beta.local')).certification.kid.slice(0, 16) + '…');
28
+
29
+ step(3, 'nicolas envía una tarea cifrada extremo a extremo a asistente');
30
+ const sent = await nicolas.send({ to: 'asistente@beta.local', type: 'task', body: { skill: 'resumir', input: 'Resume en una frase qué es Nyx5.' }, receipt: 'delivered' });
31
+ console.log(' id del sobre:', sent.id);
32
+ console.log(' el sobre viaja cifrado; la estafeta ve esto:', JSON.stringify(sent.envelope.encrypted).slice(0, 80) + '…');
33
+
34
+ step(4, 'La estafeta de beta verifica la cadena dominio -> agente -> sobre, aplica política y guarda en el buzón');
35
+ const recibido = await asistente.waitFor((e) => e.id === sent.id);
36
+ console.log(' en buzón. from_verified =', recibido.from_verified, '| relay_verified =', recibido.relay_verified);
37
+
38
+ step(5, 'asistente abre el sobre (verifica firma y descifra), procesa, confirma (ack) y responde');
39
+ const abierto = await asistente.open(recibido.envelope);
40
+ console.log(' contenido descifrado:', JSON.stringify(abierto.content.body));
41
+ await asistente.ack(abierto.id);
42
+ const respuesta = await asistente.reply(recibido.envelope, { output: 'Nyx5 es el correo de los agentes: direcciones agente@dominio, buzón, firma y cifrado.' });
43
+ console.log(' respuesta enviada:', respuesta.id, '(mismo thread:', respuesta.envelope.thread === sent.id, ')');
44
+
45
+ step(6, 'nicolas recibe la respuesta y el acuse de entrega del postmaster');
46
+ const resp = await nicolas.waitFor((e) => e.id === respuesta.id);
47
+ const abiertaResp = await nicolas.open(resp.envelope);
48
+ console.log(' resultado:', abiertaResp.content.body.output);
49
+ const acuse = await nicolas.waitFor((e) => e.type === 'receipt' && e.from === 'postmaster@alfa.local');
50
+ console.log(' acuse postmaster:', JSON.stringify((await nicolas.open(acuse.envelope)).content.body));
51
+ await nicolas.ack([resp.envelope.id, acuse.envelope.id]);
52
+
53
+ step(7, 'Estado de la bandeja de salida de nicolas');
54
+ for (const s of await nicolas.outbox()) console.log(` ${s.id.slice(0, 8)} -> ${s.to.join(',')} : ${s.status} (${s.attempts} intento/s)`);
55
+
56
+ console.log(`\nListo. Datos en ${tmp} (revisa mailbox/, archive/, outbox/, queue/ de cada dominio).`);
57
+ await alfa.stop(); await beta.stop();
@@ -0,0 +1,46 @@
1
+ // Demo store-and-forward: el destino está apagado cuando se envía; la estafeta emisora guarda,
2
+ // reintenta con backoff y entrega cuando el destino vuelve. Nada se pierde.
3
+ //
4
+ // node demo/offline.js
5
+
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { Estafeta } from '../src/correo/estafeta.js';
10
+ import { Agent } from '../src/correo/agente.js';
11
+
12
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'chasqui-off-'));
13
+ const hosts = { 'alfa.local': { url: 'http://127.0.0.1:4001' }, 'beta.local': { url: 'http://127.0.0.1:4002' } };
14
+ const mk = (domain, port, token) => new Estafeta({ domain, port, dataDir: path.join(tmp, domain), adminToken: token, hosts, workerIntervalMs: 300, retry: { baseMs: 300, maxMs: 1500 }, log: () => {} });
15
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
16
+ const step = (n, t) => console.log(`\n[${n}] ${t}`);
17
+
18
+ step(1, 'Levantando ambas estafetas y registrando agentes');
19
+ const alfa = await mk('alfa.local', 4001, 'a').start();
20
+ let beta = await mk('beta.local', 4002, 'b').start();
21
+ const nicolas = Agent.create('nicolas@alfa.local', 'http://127.0.0.1:4001', { hosts });
22
+ const asistente = Agent.create('asistente@beta.local', 'http://127.0.0.1:4002', { hosts });
23
+ await nicolas.register({ adminToken: 'a' });
24
+ await asistente.register({ adminToken: 'b' });
25
+ await nicolas.resolver.agentCard('asistente@beta.local'); // tarjeta en caché, como pasaría en uso normal
26
+
27
+ step(2, 'Apagando la estafeta de beta.local');
28
+ await beta.stop();
29
+
30
+ step(3, 'nicolas envía igual: su estafeta acepta el sobre y lo encola');
31
+ const sent = await nicolas.send({ to: 'asistente@beta.local', body: 'Hola, te escribo mientras estás apagado.' });
32
+ await sleep(1500);
33
+ for (const s of await nicolas.outbox()) console.log(` ${s.id.slice(0, 8)} : ${s.status}, ${s.attempts} intento/s, próximo ${s.next_attempt.slice(11, 19)}`);
34
+ console.log(' trabajos en cola de alfa:', await alfa.store.listQueue().length);
35
+
36
+ step(4, 'beta.local vuelve a la vida');
37
+ beta = await mk('beta.local', 4002, 'b').start();
38
+
39
+ step(5, 'La estafeta de alfa reintenta y entrega');
40
+ const m = await asistente.waitFor((e) => e.id === sent.id, { timeoutMs: 15_000 });
41
+ console.log(' recibido en beta:', (await asistente.open(m.envelope)).content.body);
42
+ for (const s of await nicolas.outbox()) console.log(` ${s.id.slice(0, 8)} : ${s.status}, ${s.attempts} intento/s`);
43
+ console.log(' trabajos en cola de alfa:', await alfa.store.listQueue().length);
44
+
45
+ await alfa.stop(); await beta.stop();
46
+ console.log('\nListo.');
@@ -0,0 +1,146 @@
1
+ // D4 — Piloto con la flota propia.
2
+ //
3
+ // Una casa, un coordinador con presupuesto (topup por frente), trabajo delegado como escrow,
4
+ // un verificador que cobra por uso (metered), y un REPORTE DE COSTO POR ENTREGA leído del
5
+ // diario del Libro —no de la aritmética de este script—: el ledger es el terreno.
6
+ //
7
+ // node demo/piloto-d4.mjs # local, rápido y determinista (por defecto)
8
+ //
9
+ // Cada "frente" es una entrega: el coordinador delega una tarea al worker con un escrow
10
+ // (los fondos quedan retenidos hasta la entrega), el verificador la revisa cobrando metered,
11
+ // y recién ahí el coordinador libera el escrow. El costo de la entrega para el coordinador es
12
+ // lo que sus asientos muestran que salió de su cuenta: pago al worker (con fee de la casa) + verificación.
13
+
14
+ import fs from 'node:fs';
15
+ import os from 'node:os';
16
+ import path from 'node:path';
17
+ import { Estafeta } from '../src/correo/estafeta.js';
18
+ import { Agent } from '../src/correo/agente.js';
19
+ import { sha256hex } from '../src/nucleo/crypto.js';
20
+
21
+ const DOM = 'pilot.local';
22
+ const URL = 'http://127.0.0.1:4090';
23
+ const hosts = { [DOM]: { url: URL } };
24
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'chasqui-d4-'));
25
+
26
+ // La casa: fee 20% (como b.chsq.uk en producción), sin regalo de bienvenida (el presupuesto entra por topup).
27
+ const casa = await new Estafeta({ domain: DOM, port: 4090, dataDir: path.join(tmp, DOM), adminToken: 'p',
28
+ hosts, workerIntervalMs: 80, libro: { feeBps: 2000, welcome: 0 }, log: () => {} }).start();
29
+
30
+ const coord = Agent.create(`coordinador@${DOM}`, URL, { hosts });
31
+ const worker = Agent.create(`worker@${DOM}`, URL, { hosts });
32
+ const verif = Agent.create(`verificador@${DOM}`, URL, { hosts });
33
+ for (const a of [coord, worker, verif]) await a.register({ adminToken: 'p' });
34
+
35
+ const money = (n) => String(n).padStart(6);
36
+ const bal = async (addr) => (await casa.store.libroState()).balances[addr] || 0;
37
+
38
+ // Los frentes del piloto: cada uno es una entrega con su precio de trabajo y su costo de verificación.
39
+ const FRENTES = [
40
+ { frente: 'Sigo · edición diaria', trabajo: 3000, verificacion: 500 },
41
+ { frente: 'Rosetta · sellado de lote', trabajo: 5000, verificacion: 800 },
42
+ { frente: 'Q-Ready · barrido de sitio', trabajo: 2000, verificacion: 400 },
43
+ ];
44
+
45
+ // Presupuesto: la casa carga el total a la cuenta del coordinador (topup por frente, un asiento por frente).
46
+ console.log('\n=== D4 · Piloto con la flota propia (casa pilot.local, fee 20%) ===\n');
47
+ let presupuesto = 0;
48
+ for (const f of FRENTES) {
49
+ const monto = f.trabajo + f.verificacion;
50
+ presupuesto += monto;
51
+ await casa.libro.topup(coord.address, monto, `presupuesto ${f.frente}`);
52
+ }
53
+ console.log(`Presupuesto cargado al coordinador: ${presupuesto} tok (${FRENTES.length} frentes)\n`);
54
+
55
+ // Una entrega de punta a punta: escrow -> deliver -> verificación metered -> release.
56
+ async function entrega(f) {
57
+ const salioAntes = await bal(coord.address);
58
+
59
+ // 1. El worker cotiza la tarea como escrow; el coordinador acepta -> se retiene el monto.
60
+ const q = await worker.quote({ to: coord.address, house: DOM, contract: 'escrow', price: f.trabajo,
61
+ concept: f.frente, terms: { acceptance: 'entrega verificada' } });
62
+ const qm = await coord.waitFor((e) => e.id === q.id);
63
+ const cot = (await coord.open(qm.envelope)).content.body;
64
+ const acc = await coord.accept(cot);
65
+ const rAcc = await coord.awaitReceipt(acc.id);
66
+ const contrato = rAcc.receipt.contract.id;
67
+
68
+ // 2. El worker declara la entrega con el hash de la evidencia.
69
+ const evidencia = sha256hex(`entregable de ${f.frente} @ ${new Date().toISOString()}`);
70
+ const del = await worker.deliver(DOM, contrato, { evidence_sha256: evidencia, note: 'listo para verificar' });
71
+ await worker.awaitReceipt(del.id);
72
+
73
+ // 3. El verificador ofrece su servicio como metered; el coordinador acepta -> mandato de gasto.
74
+ const qv = await verif.quote({ to: coord.address, house: DOM, contract: 'metered', price: f.verificacion,
75
+ concept: `verificación · ${f.frente}`, terms: { scope: { concepts: ['verificación'] } } });
76
+ const qvm = await coord.waitFor((e) => e.id === qv.id);
77
+ const cotv = (await coord.open(qvm.envelope)).content.body;
78
+ const accv = await coord.accept(cotv);
79
+ const rAccv = await coord.awaitReceipt(accv.id);
80
+ const mandato = rAccv.receipt.contract.mandate;
81
+
82
+ // 4. El verificador cobra por la revisión (una pasada); el mandato descuenta del presupuesto.
83
+ const chg = await verif.charge(DOM, { mandate: mandato, amount: f.verificacion, concept: 'verificación' });
84
+ await verif.awaitReceipt(chg.id);
85
+
86
+ // 5. Verificada, el coordinador libera el escrow: el worker cobra menos el fee de la casa.
87
+ const rel = await coord.release(DOM, contrato);
88
+ await coord.awaitReceipt(rel.id);
89
+
90
+ const salioDespues = await bal(coord.address);
91
+ return { ...f, contrato, costoReal: salioAntes - salioDespues };
92
+ }
93
+
94
+ const filas = [];
95
+ for (const f of FRENTES) { console.log(`→ ejecutando: ${f.frente} …`); filas.push(await entrega(f)); }
96
+
97
+ // ---- REPORTE DE COSTO POR ENTREGA, derivado del diario (el terreno, no la aritmética del script) ----
98
+ const { balances, journal } = { balances: (await casa.store.libroState()).balances, journal: await casa.libro.journal() };
99
+ const casaAddr = `casa@${DOM}`;
100
+ // Para cada contrato: cuánto recibió el worker (release), cuánto el fee de la casa, cuánto el verificador (charge).
101
+ const porContrato = {};
102
+ for (const fila of filas) porContrato[fila.contrato] = { worker: 0, fee: 0, verif: 0 };
103
+ for (const a of journal) {
104
+ const c = a.meta?.contract;
105
+ if (a.meta?.kind === 'release') {
106
+ if (porContrato[c]) { porContrato[c].fee += a.meta.fee || 0;
107
+ for (const l of a.lines) if (l.account === worker.address) porContrato[c].worker += l.delta; }
108
+ }
109
+ }
110
+ // El cobro del verificador va por mandato: lo tomamos de las líneas que suman al verificador con kind charge.
111
+ for (const a of journal) {
112
+ if (a.meta?.kind === 'charge') {
113
+ for (const l of a.lines) if (l.account === verif.address) {
114
+ // atribuir al frente por el concepto del mandato si está, si no, repartir por orden
115
+ const fila = filas.find((f) => !porContrato[f.contrato]._verifDone);
116
+ if (fila) { porContrato[fila.contrato].verif += l.delta; porContrato[fila.contrato]._verifDone = true; porContrato[fila.contrato].feeVerif = a.meta.fee || 0; }
117
+ }
118
+ }
119
+ }
120
+
121
+ console.log('\n=== Reporte de costo por entrega (leído del diario) ===\n');
122
+ console.log('FRENTE escrow worker_neto fee_casa verif_neto COSTO/ENTREGA');
123
+ let totCosto = 0, totWorker = 0, totFee = 0, totVerif = 0;
124
+ for (const fila of filas) {
125
+ const p = porContrato[fila.contrato];
126
+ const feeTotal = p.fee + (p.feeVerif || 0);
127
+ console.log(`${fila.frente.padEnd(28)} ${money(fila.trabajo)} ${money(p.worker)} ${money(feeTotal)} ${money(p.verif)} ${money(fila.costoReal)}`);
128
+ totCosto += fila.costoReal; totWorker += p.worker; totFee += feeTotal; totVerif += p.verif;
129
+ }
130
+ console.log(''.padEnd(28, '─') + ' ' + '─'.repeat(56));
131
+ console.log(`${'TOTAL'.padEnd(28)} ${' '.repeat(6)} ${money(totWorker)} ${money(totFee)} ${money(totVerif)} ${money(totCosto)}`);
132
+
133
+ console.log('\n=== Cierre presupuestario ===');
134
+ console.log(`presupuesto: ${money(presupuesto)}`);
135
+ console.log(`gastado (entregas): ${money(totCosto)}`);
136
+ console.log(`restante coord: ${money(await bal(coord.address))}`);
137
+ console.log(`fee recaudado casa: ${money(totFee)} (sale del medio de cada pago, no se suma al costo)`);
138
+ console.log(`cobró worker: ${money(balances[worker.address] || 0)}`);
139
+ console.log(`cobró verificador: ${money(balances[verif.address] || 0)}`);
140
+ // Cuadre de doble entrada: todo lo cargado sigue en el sistema (worker + verif + fee de la casa).
141
+ const cuadre = (balances[worker.address] || 0) + (balances[verif.address] || 0) + totFee;
142
+ console.log(`cuadre: ${money(cuadre)} (worker + verificador + fee = presupuesto ${cuadre === presupuesto ? '✓' : '✗'})`);
143
+ console.log('\nCada token movido tiene su asiento firmado por la casa y su cadena de autoridad. El costo');
144
+ console.log('por entrega no es una estimación: es lo que el diario dice que salió de la cuenta del frente.\n');
145
+
146
+ await casa.stop();
package/demo/spam.js ADDED
@@ -0,0 +1,65 @@
1
+ // Demo de la capa de confianza: qué pasa con sobres falsificados, remitentes desconocidos,
2
+ // buzones con allowlist, buzones que exigen proof-of-work y entregas duplicadas.
3
+ //
4
+ // node demo/spam.js
5
+
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { Estafeta } from '../src/correo/estafeta.js';
10
+ import { Agent } from '../src/correo/agente.js';
11
+ import { generateKeys, signObject, uuid } from '../src/nucleo/crypto.js';
12
+
13
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'chasqui-spam-'));
14
+ const hosts = { 'alfa.local': { url: 'http://127.0.0.1:4001' }, 'beta.local': { url: 'http://127.0.0.1:4002' } };
15
+ const mk = (domain, port, token) => new Estafeta({ domain, port, dataDir: path.join(tmp, domain), adminToken: token, hosts, workerIntervalMs: 200, log: () => {} });
16
+ const step = (n, t) => console.log(`\n[${n}] ${t}`);
17
+ const inbound = async (env) => {
18
+ const res = await fetch('http://127.0.0.1:4002/inbound', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(env) });
19
+ return { status: res.status, ...(await res.json()) };
20
+ };
21
+
22
+ const alfa = await mk('alfa.local', 4001, 'a').start();
23
+ const beta = await mk('beta.local', 4002, 'b').start();
24
+ const nicolas = Agent.create('nicolas@alfa.local', 'http://127.0.0.1:4001', { hosts });
25
+ const abierto = Agent.create('abierto@beta.local', 'http://127.0.0.1:4002', { hosts });
26
+ const selecto = Agent.create('selecto@beta.local', 'http://127.0.0.1:4002', { hosts });
27
+ const caro = Agent.create('caro@beta.local', 'http://127.0.0.1:4002', { hosts });
28
+ await nicolas.register({ adminToken: 'a' });
29
+ await abierto.register({ adminToken: 'b', inbox: { policy: 'open' } });
30
+ await selecto.register({ adminToken: 'b', inbox: { policy: 'allowlist', allowlist: ['socio@gamma.local'] } });
31
+ await caro.register({ adminToken: 'b', inbox: { policy: 'pow', pow_bits: 12 } });
32
+
33
+ step(1, 'Sobre con firma falsificada (dice ser nicolas@alfa.local pero firma con otra clave)');
34
+ const falsas = generateKeys();
35
+ const forjado = signObject({ nyx5: '1', id: uuid(), from: 'nicolas@alfa.local', to: ['abierto@beta.local'], created: new Date().toISOString(), type: 'message', content: { media: 'text/plain', body: 'soy nicolas, dame tus datos' } }, falsas);
36
+ console.log(' ', await inbound(forjado));
37
+
38
+ step(2, 'Remitente que no existe en su dominio (fantasma@alfa.local)');
39
+ const fantasma = signObject({ nyx5: '1', id: uuid(), from: 'fantasma@alfa.local', to: ['abierto@beta.local'], created: new Date().toISOString(), type: 'message', content: { media: 'text/plain', body: 'hola' } }, falsas);
40
+ console.log(' ', await inbound(fantasma));
41
+
42
+ step(3, 'Sobre sin firma');
43
+ const sinFirma = { nyx5: '1', id: uuid(), from: 'nicolas@alfa.local', to: ['abierto@beta.local'], created: new Date().toISOString(), type: 'message', content: { media: 'text/plain', body: 'hola' } };
44
+ console.log(' ', await inbound(sinFirma));
45
+
46
+ step(4, 'nicolas (legítimo) escribe a selecto, que tiene allowlist: solo se acepta un intro pequeño');
47
+ try { const r = await nicolas.send({ to: 'selecto@beta.local', body: 'hola' }); await new Promise((x) => setTimeout(x, 800)); console.log(' envío normal ->', (await nicolas.outbox()).find((s) => s.id === r.id).status, '(rebote al remitente)'); } catch (e) { console.log(' ', e.message); }
48
+ const intro = await nicolas.send({ to: 'selecto@beta.local', type: 'intro', body: { who: 'Nicolás, Patagonia Capital', why: 'coordinar tasaciones' } });
49
+ const m = await selecto.waitFor((e) => e.id === intro.id);
50
+ console.log(' intro aceptado ->', (await selecto.open(m.envelope)).content.body.why);
51
+
52
+ step(5, 'nicolas escribe a caro, que exige proof-of-work de 12 bits (el cliente lo calcula solo)');
53
+ const pw = await nicolas.send({ to: 'caro@beta.local', body: 'con estampilla' });
54
+ console.log(' pow adjunto:', pw.envelope.pow, '->', (await caro.waitFor((e) => e.id === pw.id)) ? 'aceptado' : 'rechazado');
55
+ const sinPow = signObject({ nyx5: '1', id: uuid(), from: 'nicolas@alfa.local', to: ['caro@beta.local'], created: new Date().toISOString(), type: 'message', content: { media: 'text/plain', body: 'sin estampilla' } }, nicolas.keys);
56
+ console.log(' sin pow directo a /inbound:', await inbound(sinPow));
57
+
58
+ step(6, 'Entrega duplicada del mismo sobre (idempotencia por id)');
59
+ const ok = await nicolas.send({ to: 'abierto@beta.local', body: 'una sola vez' });
60
+ await abierto.waitFor((e) => e.id === ok.id);
61
+ console.log(' segunda entrega:', await inbound(ok.envelope));
62
+ console.log(' copias en buzón de abierto:', (await abierto.inbox()).filter((x) => x.envelope.id === ok.id).length);
63
+
64
+ await alfa.stop(); await beta.stop();
65
+ console.log('\nListo.');