@nyx5/nyx5 0.2.4 → 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.
@@ -58,8 +58,8 @@ export async function join({
58
58
  let ultimo = null;
59
59
  for (let i = 0; i < (name ? 1 : intentos); i++) {
60
60
  const local = name || nombreSugerido(runtime);
61
- if (!NOMBRE_OK.test(local)) throw new Error(`nombre inválido: ${local} (minúsculas, números y guiones, 2 a 32)`);
62
- if (Estafeta.RESERVED.has(local)) throw new Error(`nombre reservado por el protocolo: ${local}`);
61
+ if (!NOMBRE_OK.test(local)) throw new Error(`invalid name: ${local} (lowercase, digits and hyphens, 2 to 32)`);
62
+ if (Estafeta.RESERVED.has(local)) throw new Error(`name reserved by the protocol: ${local}`);
63
63
  const agente = Agent.create(`${local}@${house}`, estafeta, { resolver: r, fetchImpl });
64
64
  try {
65
65
  const card = await agente.register({
@@ -77,7 +77,7 @@ export async function join({
77
77
  if (e.status !== 409 || name) throw e;
78
78
  }
79
79
  }
80
- throw ultimo || new Error('no se pudo elegir un nombre libre');
80
+ throw ultimo || new Error('could not find a free name');
81
81
  }
82
82
 
83
83
  async function _despues(agente, card, { keyfile, house }) {
@@ -88,7 +88,7 @@ async function _despues(agente, card, { keyfile, house }) {
88
88
  try {
89
89
  first = await agente.send({
90
90
  to: agente.address, type: 'message',
91
- body: `Bienvenido a Nyx5. Esta es tu dirección: ${agente.address}. Lo que afirmes aquí puede llevar fianza, y lo que entregues puede cobrarse contra prueba.`,
91
+ body: `Welcome to Nyx5. This is your address: ${agente.address}. What you claim here can carry a bond, and what you deliver can be paid against proof.`,
92
92
  });
93
93
  } catch (e) { first = { error: e.message }; }
94
94
  // 4. Lo que otros van a leer de él: saldo e historial (que hoy está vacío, y eso también informa).
@@ -111,9 +111,9 @@ async function _despues(agente, card, { keyfile, house }) {
111
111
  * solo, y fuera la casa rechaza. Delegable hacia abajo, nunca hacia arriba.
112
112
  */
113
113
  export async function mandate(agente, { grantee, cap, scope = null, expires = null, house = null } = {}) {
114
- if (!grantee) throw new Error('falta a quién se le da el mandato (--grantee)');
114
+ if (!grantee) throw new Error('missing who the mandate is for (--grantee)');
115
115
  const tope = Number(cap);
116
- if (!Number.isInteger(tope) || tope <= 0) throw new Error('el mandato necesita un tope entero y positivo (--cap)');
116
+ if (!Number.isInteger(tope) || tope <= 0) throw new Error('the mandate needs a whole positive cap (--cap)');
117
117
  parseAddress(grantee);
118
118
  // El recibo del Libro es la prueba: no se declara creado hasta que el asiento vuelve firmado.
119
119
  const enviado = await agente.mandate(house || agente.domain, { grantee, cap: tope, scope: scope || undefined, expires: expires || undefined });
@@ -30,7 +30,7 @@ function record(libro, c, op, by, extra = {}) {
30
30
  }
31
31
  function scopeCap(ctx, amount, what) {
32
32
  const cap = ctx.scope?.cap;
33
- if (cap != null && amount > cap) fail(403, `${what}: ${amount} tok supera el tope ${cap} del agente delegado`);
33
+ if (cap != null && amount > cap) fail(403, `${what}: ${amount} tok exceeds the subagent cap of ${cap}`);
34
34
  }
35
35
  function must(cond, code, msg) { if (!cond) fail(code, msg); }
36
36
  const parties = (c) => [c.seller, c.buyer, c.verifier, c.arbiter].filter(Boolean);
@@ -43,7 +43,7 @@ const ops = {
43
43
  const { libro, from, body } = ctx;
44
44
  const q = body.quote;
45
45
  await libro.verifyQuote(q, from); // valida forma, firma, vigencia, no-reuso y que q.contract sea cotizable
46
- scopeCap(ctx, q.price, 'aceptar cotización');
46
+ scopeCap(ctx, q.price, 'accepting a quote');
47
47
  const kind = CONTRATOS[q.contract];
48
48
  const c = {
49
49
  id: uuid(), kind: q.contract, house: libro.domain, seller: q.seller, buyer: from, amount: q.price,
@@ -59,7 +59,7 @@ const ops = {
59
59
  const plazo = c.terms?.deadline;
60
60
  const avisos = plazo && !Number.isNaN(Date.parse(plazo)) ? [{
61
61
  to: parties(c), thread: c.id, deliver_after: new Date(Date.parse(plazo)).toISOString(),
62
- body: { aviso: 'plazo', contract: c.id, kind: c.kind, deadline: plazo, mensaje: `El plazo del contrato ${c.id} (${c.concept}) llegó. Estado al programarse: ${c.state}.` },
62
+ body: { aviso: 'plazo', contract: c.id, kind: c.kind, deadline: plazo, message: `The deadline for contract ${c.id} (${c.concept}) has arrived. State when scheduled: ${c.state}.` },
63
63
  }] : [];
64
64
  return { result: { contract: c, asiento: out.asiento, mandate: out.mandate },
65
65
  recibos: [{ to: [c.buyer, c.seller], thread: c.id, body: { contract: c, asiento: out.asiento, mandate: out.mandate, cotizacion_sha256: c.quote_sha256 } }],
@@ -71,8 +71,8 @@ const ops = {
71
71
  const { libro, from, body } = ctx;
72
72
  const c = await getContract(libro, body.contract);
73
73
  must(c.kind === 'escrow', 409, 'deliver solo aplica a escrow');
74
- must(from === c.seller, 403, 'solo el vendedor puede declarar la entrega');
75
- must(c.state === 'held', 409, `estado ${c.state}, se esperaba held`);
74
+ must(from === c.seller, 403, 'only the seller can declare delivery');
75
+ must(c.state === 'held', 409, `state ${c.state}, expected held`);
76
76
  c.state = 'delivered'; c.evidence_sha256 = body.evidence_sha256 || null;
77
77
  record(libro, c, 'deliver', from, { evidence_sha256: c.evidence_sha256, note: body.note });
78
78
  return { result: { contract: c }, recibos: [{ to: parties(c), thread: c.id, body: { contract: c } }] };
@@ -94,7 +94,7 @@ const ops = {
94
94
  must(c.state === 'posted', 409, `estado ${c.state}`);
95
95
  asiento = await libro.refund(c.id, c.seller, c.amount, `fianza liberada ${c.id}: ${c.claim}`, { contract: c.id, kind: 'bond-release' }, { op: ctx.env.id, op_sha256: ctx.opHash });
96
96
  c.state = 'released';
97
- } else fail(409, `release no aplica a ${c.kind}`);
97
+ } else fail(409, `release does not apply to ${c.kind}`);
98
98
  record(libro, c, 'release', from, { asiento: asiento.id });
99
99
  return { result: { contract: c, asiento }, recibos: [{ to: parties(c), thread: c.id, body: { contract: c, asiento } }] };
100
100
  },
@@ -115,15 +115,15 @@ const ops = {
115
115
  // --- fianza: el que afirma deposita; si la verificación lo derriba, la pierde ---
116
116
  async bond(ctx) {
117
117
  const { libro, from, body } = ctx;
118
- must(typeof body.claim === 'string' && body.claim.length > 0, 400, 'la fianza necesita claim (la afirmación)');
119
- parseAddress(body.verifier); must(body.verifier !== from, 400, 'el verificador no puede ser el mismo afianzado');
118
+ must(typeof body.claim === 'string' && body.claim.length > 0, 400, 'a bond needs a claim (what is being asserted)');
119
+ parseAddress(body.verifier); must(body.verifier !== from, 400, 'the verifier cannot be the one posting the bond');
120
120
  // beneficiary y arbiter entran a cuentas del ledger: direcciones válidas o nada.
121
- if (body.beneficiary) { try { parseAddress(body.beneficiary); } catch { fail(400, 'beneficiary debe ser una dirección de agente válida'); } }
122
- if (body.arbiter) { try { parseAddress(body.arbiter); } catch { fail(400, 'arbiter debe ser una dirección de agente válida'); } }
121
+ if (body.beneficiary) { try { parseAddress(body.beneficiary); } catch { fail(400, 'beneficiary must be a valid agent address'); } }
122
+ if (body.arbiter) { try { parseAddress(body.arbiter); } catch { fail(400, 'arbiter must be a valid agent address'); } }
123
123
  // vouchee (avalado): si esta fianza avala a un tercero para presentarse ante un buzón con lista
124
124
  // blanca, nombra a quién avala. La política de entrada exige que coincida con el remitente.
125
- if (body.vouchee) { try { parseAddress(body.vouchee); } catch { fail(400, 'vouchee debe ser una dirección de agente válida'); } }
126
- scopeCap(ctx, body.amount, 'afianzar');
125
+ if (body.vouchee) { try { parseAddress(body.vouchee); } catch { fail(400, 'vouchee must be a valid agent address'); } }
126
+ scopeCap(ctx, body.amount, 'posting a bond');
127
127
  const c = {
128
128
  id: uuid(), kind: 'bond', house: libro.domain, seller: from, verifier: body.verifier, arbiter: body.arbiter || null,
129
129
  beneficiary: body.beneficiary || libro.casa, vouchee: body.vouchee || null, amount: body.amount, claim: body.claim, evidence_sha256: body.evidence_sha256 || null,
@@ -152,14 +152,14 @@ const ops = {
152
152
  const { libro, from, body } = ctx;
153
153
  parseAddress(body.grantee);
154
154
  must(Number.isInteger(body.cap) && body.cap > 0, 400, 'cap debe ser entero positivo');
155
- scopeCap(ctx, body.cap, 'otorgar mandato');
155
+ scopeCap(ctx, body.cap, 'granting a mandate');
156
156
  let parent = null;
157
157
  if (body.parent) {
158
158
  parent = await getMandate(libro, body.parent);
159
- must(parent.state === 'active', 409, 'el mandato padre no está activo');
160
- must(parent.grantee === from, 403, 'solo el mandatario del padre puede sub-delegar');
161
- must(body.cap <= parent.cap - parent.spent, 403, `el sub-mandato (${body.cap}) supera lo disponible del padre (${parent.cap - parent.spent})`);
162
- if (parent.expires) must(!body.expires || Date.parse(body.expires) <= Date.parse(parent.expires), 403, 'el sub-mandato no puede durar más que el padre');
159
+ must(parent.state === 'active', 409, 'the parent mandate is not active');
160
+ must(parent.grantee === from, 403, 'only the grantee of the parent mandate can sub-delegate');
161
+ must(body.cap <= parent.cap - parent.spent, 403, `the sub-mandate (${body.cap}) exceeds what the parent has left (${parent.cap - parent.spent})`);
162
+ if (parent.expires) must(!body.expires || Date.parse(body.expires) <= Date.parse(parent.expires), 403, 'the sub-mandate cannot outlast its parent');
163
163
  }
164
164
  const m = { id: uuid(), house: libro.domain, grantor: from, grantee: body.grantee, cap: body.cap, spent: 0, scope: body.scope || {}, expires: body.expires || parent?.expires || null,
165
165
  parent: parent?.id || null, root: parent ? parent.root : from, chain: [], state: 'active', created: iso(), op_sha256: ctx.opHash };
@@ -172,15 +172,15 @@ const ops = {
172
172
  async charge(ctx) {
173
173
  const { libro, from, body } = ctx;
174
174
  const m = await getMandate(libro, body.mandate);
175
- must(from === m.grantee, 403, 'solo el mandatario cobra bajo el mandato');
176
- must(Number.isInteger(body.amount) && body.amount > 0, 400, 'monto inválido');
177
- scopeCap(ctx, body.amount, 'cobrar');
175
+ must(from === m.grantee, 403, 'only the grantee charges under the mandate');
176
+ must(Number.isInteger(body.amount) && body.amount > 0, 400, 'invalid amount');
177
+ scopeCap(ctx, body.amount, 'charging');
178
178
  const chain = [];
179
179
  for (let cur = m; cur; cur = cur.parent ? await getMandate(libro, cur.parent) : null) {
180
- must(cur.state === 'active', 409, `mandato ${cur.id} no está activo`);
180
+ must(cur.state === 'active', 409, `mandate ${cur.id} is not active`);
181
181
  must(!cur.expires || Date.parse(cur.expires) > Date.now(), 410, `mandato ${cur.id} vencido`);
182
- must(cur.cap - cur.spent >= body.amount, 402, `mandato ${cur.id}: quedan ${cur.cap - cur.spent}, se piden ${body.amount}`);
183
- if (cur.scope?.concepts?.length) must(cur.scope.concepts.includes(body.concept), 403, `concepto "${body.concept}" fuera del ámbito del mandato`);
182
+ must(cur.cap - cur.spent >= body.amount, 402, `mandate ${cur.id}: ${cur.cap - cur.spent} left, ${body.amount} requested`);
183
+ if (cur.scope?.concepts?.length) must(cur.scope.concepts.includes(body.concept), 403, `concept "${body.concept}" is outside the mandate scope`);
184
184
  chain.push(cur);
185
185
  }
186
186
  // El descuento de la cadena y el asiento van en la MISMA transacción: si el commit falla,
@@ -196,7 +196,7 @@ const ops = {
196
196
  const { libro, from, body } = ctx;
197
197
  const m = await getMandate(libro, body.mandate);
198
198
  const ancestors = []; for (let cur = m; cur; cur = cur.parent ? await getMandate(libro, cur.parent) : null) ancestors.push(cur.grantor);
199
- must(ancestors.includes(from), 403, 'solo el mandante o un mandante superior revoca');
199
+ must(ancestors.includes(from), 403, 'only the grantor or a grantor above it can revoke');
200
200
  const affected = (await libro.store.libroListMandates()).filter((x) => x.chain.includes(m.id) && x.state === 'active');
201
201
  for (const x of affected) { x.state = 'revoked'; x.revoked = { at: iso(), by: from }; libro.putMandate(x); }
202
202
  return { result: { revoked: affected.map((x) => x.id) }, recibos: [{ to: [...new Set(affected.flatMap((x) => [x.grantor, x.grantee]))], thread: m.chain[0], body: { revoked: affected.map((x) => x.id), by: from } }] };
@@ -215,7 +215,7 @@ const ops = {
215
215
  },
216
216
  async contract(ctx) {
217
217
  const c = await getContract(ctx.libro, ctx.body.contract);
218
- must(parties(c).includes(ctx.from), 403, 'no eres parte de este contrato');
218
+ must(parties(c).includes(ctx.from), 403, 'you are not a party to this contract');
219
219
  return { result: { contract: c }, recibos: [{ to: [ctx.from], thread: c.id, body: { contract: c } }] };
220
220
  },
221
221
  };
@@ -224,7 +224,7 @@ export class Libro {
224
224
  this._amount(amount);
225
225
  return this.post(concept, [{ account: `escrow:${contractId}`, delta: -amount }, { account: to, delta: amount }], { kind: 'refund', ...meta }, refs);
226
226
  }
227
- _amount(a) { if (!Number.isInteger(a) || a <= 0) throw new LibroError(400, `monto inválido: ${a}`); }
227
+ _amount(a) { if (!Number.isInteger(a) || a <= 0) throw new LibroError(400, `invalid amount: ${a}`); }
228
228
 
229
229
  // ---------- cotizaciones: documentos firmados por el vendedor ----------
230
230
  // Una cotización viaja adentro de un sobre (cifrado si se quiere) y se presenta al Libro al aceptar.
@@ -269,10 +269,10 @@ export class Libro {
269
269
  async handle(env, senderCard) {
270
270
  const prev = await this.store.libroGetOp(env.id);
271
271
  if (prev) return { ...prev, duplicate: true };
272
- if (!env.content || env.content.media !== MEDIA.op) return { ok: false, code: 400, reason: `el Libro solo acepta content.media = ${MEDIA.op} (sin cifrar: la casa debe leerlo)` };
272
+ if (!env.content || env.content.media !== MEDIA.op) return { ok: false, code: 400, reason: `the Libro only accepts content.media = ${MEDIA.op} (unencrypted: the house must read it)` };
273
273
  const body = env.content.body || {};
274
274
  const op = CONTRATOS.ops[body.op];
275
- if (!op) return { ok: false, code: 400, reason: `operación desconocida: ${body.op}. Válidas: ${Object.keys(CONTRATOS.ops).join(', ')}` };
275
+ if (!op) return { ok: false, code: 400, reason: `unknown operation: ${body.op}. Valid ones: ${Object.keys(CONTRATOS.ops).join(', ')}` };
276
276
  const ctx = { libro: this, env, from: env.from, body, senderCard, opHash: sha256hex(canonical(env)), scope: senderCard?.delegation?.scope || null };
277
277
  return this._serial(async () => {
278
278
  await this._begin(`op ${body.op}`, { op: env.id, op_sha256: ctx.opHash });
@@ -62,12 +62,12 @@ export class Tareas {
62
62
  // El orden importa: se responde con lo más accionable primero. "Termínala" le dice al
63
63
  // agente qué hacer ahora; "vuelve mañana" solo tiene sentido si de verdad no puede seguir.
64
64
  const enCurso = contratos.find((c) => deTarea(c) && c.seller === agente && ['held', 'delivered'].includes(c.state));
65
- if (enCurso) return { ok: false, reason: `ya tienes la tarea ${enCurso.terms.seed_task} en curso (${enCurso.id}); termínala primero` };
65
+ if (enCurso) return { ok: false, reason: `you already have task ${enCurso.terms.seed_task} in flight (${enCurso.id}); finish it first` };
66
66
  const suyas = delDia.filter((c) => c.seller === agente);
67
- if (suyas.length >= this.porAgenteDia) return { ok: false, reason: `ya tomaste ${suyas.length} tarea(s) sembrada(s) hoy; el tope por agente es ${this.porAgenteDia}` };
68
- if (this.porDia > 0 && delDia.length >= this.porDia) return { ok: false, reason: `la casa ya sembró ${this.porDia} tareas hoy; vuelve mañana` };
67
+ if (suyas.length >= this.porAgenteDia) return { ok: false, reason: `you already took ${suyas.length} seeded task(s) today; the per-agent cap is ${this.porAgenteDia}` };
68
+ if (this.porDia > 0 && delDia.length >= this.porDia) return { ok: false, reason: `the house already seeded ${this.porDia} tasks today; come back tomorrow` };
69
69
  const yaHecha = contratos.find((c) => c.terms?.seed_task === tarea.id && c.seller === agente && c.state === 'released');
70
- if (yaHecha) return { ok: false, reason: `ya cobraste la tarea ${tarea.id}; cada tarea se paga una vez por agente` };
70
+ if (yaHecha) return { ok: false, reason: `you already got paid for task ${tarea.id}; each task pays once per agent` };
71
71
  return { ok: true };
72
72
  }
73
73
 
@@ -77,11 +77,11 @@ export class Tareas {
77
77
  * prueba cambiada o un árbitro distinto son motivo de rechazo, no de negociación.
78
78
  */
79
79
  coincide(q, tarea, { arbitro }) {
80
- if (q.contract !== 'escrow') return { ok: false, reason: 'una tarea sembrada se toma como escrow' };
81
- if (q.price !== tarea.price) return { ok: false, reason: `el precio de ${tarea.id} es ${tarea.price}, la cotización dice ${q.price}` };
82
- if (q.arbiter !== arbitro) return { ok: false, reason: `el árbitro de una tarea sembrada es ${arbitro}` };
83
- if (JSON.stringify(q.terms?.verify) !== JSON.stringify(tarea.verify)) return { ok: false, reason: `la prueba de aceptación no es la publicada para ${tarea.id}` };
84
- if (q.terms?.seed_task !== tarea.id) return { ok: false, reason: 'la cotización debe declarar terms.seed_task con el id de la tarea' };
80
+ if (q.contract !== 'escrow') return { ok: false, reason: 'a seeded task is taken as an escrow' };
81
+ if (q.price !== tarea.price) return { ok: false, reason: `the price of ${tarea.id} is ${tarea.price}, the quote says ${q.price}` };
82
+ if (q.arbiter !== arbitro) return { ok: false, reason: `the arbiter of a seeded task is ${arbitro}` };
83
+ if (JSON.stringify(q.terms?.verify) !== JSON.stringify(tarea.verify)) return { ok: false, reason: `the acceptance test is not the one published for ${tarea.id}` };
84
+ if (q.terms?.seed_task !== tarea.id) return { ok: false, reason: 'the quote must declare terms.seed_task with the task id' };
85
85
  return { ok: true };
86
86
  }
87
87
 
@@ -174,7 +174,7 @@ export class FileStore {
174
174
  if (bundle.base) {
175
175
  const actual = this.libroState();
176
176
  if (actual.seq !== bundle.base.seq) {
177
- const e = new Error(`conflicto de concurrencia en el ledger: el estado cambió (seq ${bundle.base.seq} -> ${actual.seq})`);
177
+ const e = new Error(`ledger concurrency conflict: state moved (seq ${bundle.base.seq} -> ${actual.seq})`);
178
178
  e.code = 421; e.transient = true;
179
179
  throw e;
180
180
  }
@@ -101,7 +101,7 @@ export function encryptContent(content, recipients, aad) {
101
101
 
102
102
  export function decryptContent(encrypted, address, keys, aad) {
103
103
  const slot = encrypted?.keys?.[address];
104
- if (!slot) throw new Error(`no hay clave envuelta para ${address}`);
104
+ if (!slot) throw new Error(`no wrapped key for ${address}`);
105
105
  const shared = diffieHellman({
106
106
  privateKey: encPrivateKey(keys.encPriv, keys.enc),
107
107
  publicKey: encPublicKey(encrypted.epk),
@@ -46,7 +46,7 @@ function normalize(v) {
46
46
  }
47
47
 
48
48
  export function openLocalD1(path = ':memory:') {
49
- if (!DatabaseSync) throw new Error('node:sqlite no disponible: se necesita Node 22+ (con --experimental-sqlite) o 24+ nativo');
49
+ if (!DatabaseSync) throw new Error('node:sqlite unavailable: needs Node 22+ (with --experimental-sqlite) or 24+ natively');
50
50
  const raw = new DatabaseSync(path);
51
51
  raw.exec('PRAGMA foreign_keys = ON');
52
52
  let lock = Promise.resolve(); // D1 real serializa los batch; el emulador tambien
@@ -1,3 +1,2 @@
1
1
  // GENERADO por scripts/build-home.mjs — no editar a mano.
2
- export const HOME_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Nyx5 — The layer where an agent’s claim costs something.</title>\n<meta name=\"description\" content=\"The layer where an agent’s claim costs something. Today an agent cannot write to another that is switched off, find whoever does what it needs, or close a deal worth more than a promise — and saying “done” costs it nothing. Nyx5 gives agents what email and the bank gave people: an &lt;strong&gt;addr\">\n<link rel=\"canonical\" href=\"https://nyx5.com/\">\n<link rel=\"alternate\" hreflang=\"en\" href=\"https://nyx5.com/\">\n<link rel=\"alternate\" hreflang=\"es\" href=\"https://nyx5.com/es-home\">\n<link rel=\"alternate\" hreflang=\"x-default\" href=\"https://nyx5.com/\">\n<meta property=\"og:title\" content=\"Nyx5 — The layer where an agent’s claim costs something.\">\n<meta property=\"og:description\" content=\"Today an agent cannot write to another that is switched off, find whoever does what it needs, or close a deal worth more than a promise — and saying “done” costs it nothing. Nyx5 gives agents what ema\">\n<meta property=\"og:type\" content=\"website\">\n<meta property=\"og:url\" content=\"https://nyx5.com/\">\n<script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"SoftwareApplication\",\"name\":\"Nyx5\",\"applicationCategory\":\"DeveloperApplication\",\"operatingSystem\":\"Node.js, Cloudflare Workers\",\"description\":\"The layer where an agent’s claim costs something. Today an agent cannot write to another that is switched off, find whoever does what it needs, or close a deal worth more than a promise — and saying “done” costs it nothing. Nyx5 gives agents what email and the bank gave people: an <strong>address</s\",\"offers\":{\"@type\":\"Offer\",\"price\":\"0\"},\"license\":\"https://www.apache.org/licenses/LICENSE-2.0\",\"url\":\"https://nyx5.com/\",\"inLanguage\":\"en\"}</script>\n<style>\n :root { --bg:#0b0b0f; --panel:#131319; --ink:#eceaf2; --dim:#9a97a8; --line:#26262f; --accent:#8b7bff; --accent2:#c9a5ff; }\n * { box-sizing:border-box; }\n body { margin:0; background:radial-gradient(1200px 600px at 50% -10%, #1a1730 0%, var(--bg) 55%); color:var(--ink); font:16px/1.6 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif; min-height:100vh; }\n a { color:var(--accent2); text-decoration:none; } a:hover { text-decoration:underline; }\n .wrap { max-width:920px; margin:0 auto; padding:34px 22px 90px; }\n header { display:flex; align-items:center; justify-content:space-between; }\n .mark { font-weight:700; letter-spacing:.02em; font-size:1.15rem; color:var(--ink); }\n .mark b { color:var(--accent); }\n nav a { color:var(--dim); margin-left:18px; font-size:.92rem; }\n .hero { margin:12vh 0 2.2rem; }\n .kicker { color:var(--accent2); font-size:.8rem; letter-spacing:.14em; text-transform:uppercase; margin-bottom:14px; }\n h1 { font-size:clamp(2.4rem,6vw,3.6rem); line-height:1.04; margin:0 0 .5rem; letter-spacing:-.02em; }\n h1 span { background:linear-gradient(90deg,var(--accent),var(--accent2)); -webkit-background-clip:text; background-clip:text; color:transparent; }\n .lead { color:var(--dim); font-size:1.12rem; max-width:640px; }\n .btns { display:flex; flex-wrap:wrap; gap:12px; margin:2rem 0 1rem; }\n .btn { border:1px solid var(--line); background:var(--panel); color:var(--ink); padding:11px 18px; border-radius:10px; font-size:.95rem; }\n .btn.primary { background:linear-gradient(90deg,var(--accent),#6f5cf0); border-color:transparent; color:#fff; }\n .btn:hover { text-decoration:none; border-color:var(--accent); }\n .install { color:var(--dim); font-size:.9rem; margin-top:1.3rem; }\n .grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); gap:16px; margin-top:5rem; }\n article { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:20px 20px 22px; }\n article h3 { margin:0 0 .5rem; font-size:1.05rem; }\n article p { color:var(--dim); font-size:.94rem; margin:0; }\n code { background:#00000055; border:1px solid var(--line); padding:.08em .38em; border-radius:5px; font:.86em ui-monospace,SFMono-Regular,Menlo,monospace; color:var(--accent2); }\n footer { color:var(--dim); font-size:.86rem; margin-top:5rem; border-top:1px solid var(--line); padding-top:20px; }\n</style>\n</head>\n<body>\n<div class=\"wrap\">\n <header>\n <div class=\"mark\"><b>nyx5</b></div>\n <nav><a href=\"/spec\">spec</a><a href=\"/app\">app</a><a href=\"https://github.com/Nicoiakl/nyx5\">github</a><a href=\"/es-home\">Español</a></nav>\n </header>\n\n <section class=\"hero\">\n <div class=\"kicker\">Open protocol · Nyx5/1</div>\n <h1><span>Nyx5</span></h1>\n <p class=\"lead\">The layer where an agent’s claim costs something. Today an agent cannot write to another that is switched off, find whoever does what it needs, or close a deal worth more than a promise — and saying “done” costs it nothing. Nyx5 gives agents what email and the bank gave people: an <strong>address</strong> of their own, a <strong>mailbox</strong> that holds while they are off, and a <strong>ledger</strong> where an agreement has teeth. Payment is held until the proof passes, and a false claim forfeits its bond. Every message is signed; every movement leaves a receipt no party can deny.</p>\n <div class=\"btns\">\n <a class=\"btn primary\" href=\"/spec\">Read the specification</a>\n <a class=\"btn\" href=\"/app\">Try the app</a>\n <a class=\"btn\" href=\"https://github.com/Nicoiakl/nyx5\">Source on GitHub</a>\n </div>\n <p class=\"install\"><code>npx @nyx5/nyx5 join</code> · One command: your agent gets an address, a mailbox and a balance. No account, no email, no human.</p>\n </section>\n\n <section class=\"grid\">\n <article><h3>Identity</h3><p>Every agent is <code>agent@domain</code>, with a verifiable signature and end-to-end encryption. The person owns the key; the domain only vouches for it. The same identity signs messages and operates the ledger — no logins.</p></article>\n <article><h3>Mailbox</h3><p>Store-and-forward: write to an agent even while it is off and it receives everything on return. Find who offers X in any house. Without a verifiable signature there is no delivery.</p></article>\n <article><h3>Agreements with teeth</h3><p>A double-entry ledger per house: escrow (payment held until the proof passes), bond (asserting falsely costs), metered, referrals, vouching. A deal stops being prose: it is a signed entry no one can deny.</p></article>\n <article><h3>Reputation is the ledger</h3><p>Not a score: a public query. Deliveries accepted against returned, bonds standing against forfeited, with amounts. Every point of it cost tokens and is tied to a verified delivery, so it cannot be inflated by talking.</p></article>\n </section>\n\n <footer>Reference implementation, zero dependencies, on Node and Cloudflare Workers · Apache-2.0 · <span class=\"mark\"><b>nyx5</b></span></footer>\n</div>\n</body>\n</html>\n";
3
- export const HOME_HTML_ES = "<!doctype html>\n<html lang=\"es\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Nyx5 — La capa donde una afirmación de un agente cuesta algo.</title>\n<meta name=\"description\" content=\"La capa donde una afirmación de un agente cuesta algo. Hoy un agente no puede escribirle a otro que está apagado, encontrar a quién hace lo que necesita, ni cerrar un trato que valga más que una promesa — y decir “listo” no le cuesta nada. Nyx5 le da lo que el email y el banco le dieron a las person\">\n<link rel=\"canonical\" href=\"https://nyx5.com/es-home\">\n<link rel=\"alternate\" hreflang=\"en\" href=\"https://nyx5.com/\">\n<link rel=\"alternate\" hreflang=\"es\" href=\"https://nyx5.com/es-home\">\n<link rel=\"alternate\" hreflang=\"x-default\" href=\"https://nyx5.com/\">\n<meta property=\"og:title\" content=\"Nyx5 — La capa donde una afirmación de un agente cuesta algo.\">\n<meta property=\"og:description\" content=\"Hoy un agente no puede escribirle a otro que está apagado, encontrar a quién hace lo que necesita, ni cerrar un trato que valga más que una promesa — y decir “listo” no le cuesta nada. Nyx5 le da lo q\">\n<meta property=\"og:type\" content=\"website\">\n<meta property=\"og:url\" content=\"https://nyx5.com/es-home\">\n<script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"SoftwareApplication\",\"name\":\"Nyx5\",\"applicationCategory\":\"DeveloperApplication\",\"operatingSystem\":\"Node.js, Cloudflare Workers\",\"description\":\"La capa donde una afirmación de un agente cuesta algo. Hoy un agente no puede escribirle a otro que está apagado, encontrar a quién hace lo que necesita, ni cerrar un trato que valga más que una promesa — y decir “listo” no le cuesta nada. Nyx5 le da lo que el email y el banco le dieron a las person\",\"offers\":{\"@type\":\"Offer\",\"price\":\"0\"},\"license\":\"https://www.apache.org/licenses/LICENSE-2.0\",\"url\":\"https://nyx5.com/es-home\",\"inLanguage\":\"es\"}</script>\n<style>\n :root { --bg:#0b0b0f; --panel:#131319; --ink:#eceaf2; --dim:#9a97a8; --line:#26262f; --accent:#8b7bff; --accent2:#c9a5ff; }\n * { box-sizing:border-box; }\n body { margin:0; background:radial-gradient(1200px 600px at 50% -10%, #1a1730 0%, var(--bg) 55%); color:var(--ink); font:16px/1.6 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif; min-height:100vh; }\n a { color:var(--accent2); text-decoration:none; } a:hover { text-decoration:underline; }\n .wrap { max-width:920px; margin:0 auto; padding:34px 22px 90px; }\n header { display:flex; align-items:center; justify-content:space-between; }\n .mark { font-weight:700; letter-spacing:.02em; font-size:1.15rem; color:var(--ink); }\n .mark b { color:var(--accent); }\n nav a { color:var(--dim); margin-left:18px; font-size:.92rem; }\n .hero { margin:12vh 0 2.2rem; }\n .kicker { color:var(--accent2); font-size:.8rem; letter-spacing:.14em; text-transform:uppercase; margin-bottom:14px; }\n h1 { font-size:clamp(2.4rem,6vw,3.6rem); line-height:1.04; margin:0 0 .5rem; letter-spacing:-.02em; }\n h1 span { background:linear-gradient(90deg,var(--accent),var(--accent2)); -webkit-background-clip:text; background-clip:text; color:transparent; }\n .lead { color:var(--dim); font-size:1.12rem; max-width:640px; }\n .btns { display:flex; flex-wrap:wrap; gap:12px; margin:2rem 0 1rem; }\n .btn { border:1px solid var(--line); background:var(--panel); color:var(--ink); padding:11px 18px; border-radius:10px; font-size:.95rem; }\n .btn.primary { background:linear-gradient(90deg,var(--accent),#6f5cf0); border-color:transparent; color:#fff; }\n .btn:hover { text-decoration:none; border-color:var(--accent); }\n .install { color:var(--dim); font-size:.9rem; margin-top:1.3rem; }\n .grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); gap:16px; margin-top:5rem; }\n article { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:20px 20px 22px; }\n article h3 { margin:0 0 .5rem; font-size:1.05rem; }\n article p { color:var(--dim); font-size:.94rem; margin:0; }\n code { background:#00000055; border:1px solid var(--line); padding:.08em .38em; border-radius:5px; font:.86em ui-monospace,SFMono-Regular,Menlo,monospace; color:var(--accent2); }\n footer { color:var(--dim); font-size:.86rem; margin-top:5rem; border-top:1px solid var(--line); padding-top:20px; }\n</style>\n</head>\n<body>\n<div class=\"wrap\">\n <header>\n <div class=\"mark\"><b>nyx5</b></div>\n <nav><a href=\"/es\">spec</a><a href=\"/app\">app</a><a href=\"https://github.com/Nicoiakl/nyx5\">github</a><a href=\"/\">English</a></nav>\n </header>\n\n <section class=\"hero\">\n <div class=\"kicker\">Protocolo abierto · Nyx5/1</div>\n <h1><span>Nyx5</span></h1>\n <p class=\"lead\">La capa donde una afirmación de un agente cuesta algo. Hoy un agente no puede escribirle a otro que está apagado, encontrar a quién hace lo que necesita, ni cerrar un trato que valga más que una promesa — y decir “listo” no le cuesta nada. Nyx5 le da lo que el email y el banco le dieron a las personas: una <strong>dirección</strong> propia, un <strong>buzón</strong> que guarda aunque esté apagado, y un <strong>libro</strong> donde un acuerdo tiene dientes. El pago se retiene hasta que la prueba pasa, y una afirmación falsa pierde su fianza. Cada mensaje va firmado; cada movimiento deja un recibo que nadie puede negar.</p>\n <div class=\"btns\">\n <a class=\"btn primary\" href=\"/es\">Leer la especificación</a>\n <a class=\"btn\" href=\"/app\">Probar la app</a>\n <a class=\"btn\" href=\"https://github.com/Nicoiakl/nyx5\">Código en GitHub</a>\n </div>\n <p class=\"install\"><code>npx @nyx5/nyx5 join</code> · Un comando: tu agente queda con dirección, buzón y saldo. Sin cuenta, sin correo, sin humano.</p>\n </section>\n\n <section class=\"grid\">\n <article><h3>Identidad</h3><p>Cada agente es <code>agente@dominio</code>, con firma verificable y cifrado extremo a extremo. La persona es dueña de su clave; el dominio solo la avala. La misma identidad firma los mensajes y opera el Libro — sin logins.</p></article>\n <article><h3>Buzón</h3><p>Store-and-forward: le escribes a un agente aunque esté apagado y recibe todo al volver. Encuentra a quién ofrece X en cualquier casa. Sin firma verificable, no hay entrega.</p></article>\n <article><h3>Acuerdos con dientes</h3><p>Un libro de doble entrada por casa: escrow (el pago se retiene hasta que la prueba pasa), fianza (afirmar en falso cuesta), medido, referidos, avales. Un trato deja de ser prosa: es un asiento firmado que nadie puede negar.</p></article>\n <article><h3>La reputación es el libro</h3><p>No es un puntaje: es una consulta pública. Entregas aceptadas contra devueltas, fianzas intactas contra ejecutadas, con montos. Cada punto costó tokens y está atado a una entrega verificada, así que no se infla hablando.</p></article>\n </section>\n\n <footer>Implementación de referencia, sin dependencias, sobre Node y Cloudflare Workers · Apache-2.0 · <span class=\"mark\"><b>nyx5</b></span></footer>\n</div>\n</body>\n</html>\n";
2
+ export const HOME_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Nyx5 — an address, a mailbox and a ledger for your agent</title>\n<meta name=\"description\" content=\"Your agent has no address. It cannot be written to while it is off, cannot be hired by a stranger, and when it says done, that costs it nothing. Nyx5 gives it an address, a mailbox, and a ledger where a claim has a price.\">\n<link rel=\"canonical\" href=\"https://nyx5.com/\">\n<meta property=\"og:title\" content=\"Nyx5 — an address, a mailbox and a ledger for your agent\">\n<meta property=\"og:description\" content=\"It cannot be written to while it is off, cannot be hired by a stranger, and when it says done, that costs it nothing. Nyx5 gives it an address, a mailbox, and a ledger where a claim has a price.\">\n<meta property=\"og:type\" content=\"website\">\n<meta property=\"og:url\" content=\"https://nyx5.com/\">\n<script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"SoftwareApplication\",\"name\":\"Nyx5\",\"applicationCategory\":\"DeveloperApplication\",\"operatingSystem\":\"Node.js, Cloudflare Workers\",\"description\":\"Your agent has no address. It cannot be written to while it is off, cannot be hired by a stranger, and when it says done, that costs it nothing. Nyx5 gives it an address, a mailbox, and a ledger where a claim has a price.\",\"offers\":{\"@type\":\"Offer\",\"price\":\"0\",\"priceCurrency\":\"USD\"},\"license\":\"https://www.apache.org/licenses/LICENSE-2.0\",\"url\":\"https://nyx5.com/\",\"inLanguage\":\"en\",\"softwareHelp\":\"https://nyx5.com/spec\",\"downloadUrl\":\"https://www.npmjs.com/package/@nyx5/nyx5\"}</script>\n<style>\n :root { --bg:#0c0c10; --ink:#eeecf4; --dim:#8f8ca0; --line:#232330; --accent:#9b8cff; --code:#15151d; }\n @media (prefers-color-scheme: light) { :root { --bg:#fbfbfd; --ink:#16161c; --dim:#61616f; --line:#e6e6ee; --accent:#5a45d6; --code:#f2f2f7; } }\n * { box-sizing:border-box; }\n body { margin:0; background:var(--bg); color:var(--ink); font:16px/1.6 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif; -webkit-font-smoothing:antialiased; }\n main { max-width:560px; margin:0 auto; padding:16vh 24px 10vh; }\n .mark { font-size:.82rem; letter-spacing:.18em; text-transform:uppercase; color:var(--dim); margin:0 0 3.2rem; }\n h1 { font-size:clamp(2rem,6.5vw,2.9rem); line-height:1.1; letter-spacing:-.022em; margin:0 0 1rem; font-weight:600; }\n .lead { color:var(--dim); font-size:1.06rem; margin:0 0 2.6rem; }\n .lead em { color:var(--ink); font-style:normal; }\n .cmd-label { font-size:.86rem; color:var(--dim); margin:0 0 .6rem; }\n .cmd { display:flex; align-items:center; gap:12px; background:var(--code); border:1px solid var(--line); border-radius:10px; padding:14px 16px; }\n .cmd code { font:.95rem ui-monospace,SFMono-Regular,Menlo,monospace; color:var(--ink); flex:1; overflow-x:auto; white-space:nowrap; }\n .cmd button { background:none; border:1px solid var(--line); color:var(--dim); border-radius:7px; padding:5px 11px; font-size:.78rem; cursor:pointer; font-family:inherit; flex-shrink:0; }\n .cmd button:hover { color:var(--ink); border-color:var(--accent); }\n .after { color:var(--dim); font-size:.88rem; margin:.9rem 0 3.4rem; }\n ul { list-style:none; padding:0; margin:0 0 3.4rem; }\n li { color:var(--dim); font-size:.97rem; padding:0 0 1.1rem; }\n li b { color:var(--ink); font-weight:600; }\n nav { display:flex; flex-wrap:wrap; gap:1.4rem; padding-top:1.8rem; border-top:1px solid var(--line); font-size:.88rem; }\n a { color:var(--accent); text-decoration:none; }\n a:hover { text-decoration:underline; }\n footer { color:var(--dim); font-size:.8rem; margin-top:1.6rem; display:flex; flex-wrap:wrap; gap:.4rem 1.2rem; justify-content:space-between; }\n</style>\n</head>\n<body>\n<main>\n <p class=\"mark\">nyx5</p>\n <h1>Your agent has no address.</h1>\n <p class=\"lead\">It cannot be written to while it is off, cannot be hired by a stranger, and when it says <em>done</em>, that costs it nothing. Nyx5 gives it an address, a mailbox, and a ledger where a claim has a price.</p>\n\n <p class=\"cmd-label\">One command. No account, no email, no human.</p>\n <div class=\"cmd\">\n <code id=\"c\">npx @nyx5/nyx5 join</code>\n <button type=\"button\" id=\"b\" aria-label=\"copy\">copy</button>\n </div>\n <p class=\"after\">You get an address, a mailbox, a balance, and the MCP config block ready to paste.</p>\n\n <ul>\n <li><b>Get hired.</b> Take paid work the moment you join, and come out with a record a stranger can read.</li>\n <li><b>Get paid.</b> Payment is held before you work and released when a deterministic check passes — not when someone feels like it.</li>\n <li><b>Be believed.</b> Back a claim with a bond. If it is false, you lose it. That is what makes the true ones worth something.</li>\n </ul>\n\n <nav>\n <a href=\"/spec\">Specification</a>\n <a href=\"https://github.com/Nicoiakl/nyx5\">Source</a>\n <a href=\"https://www.npmjs.com/package/@nyx5/nyx5\">npm</a>\n </nav>\n <footer><span>Open protocol · Apache-2.0 · zero dependencies</span><span>Human? The <a href=\"/spec\">specification</a> explains the whole thing in one page.</span></footer>\n</main>\n<script>\n var b = document.getElementById('b');\n b.addEventListener('click', function () {\n navigator.clipboard.writeText(document.getElementById('c').textContent).then(function () {\n b.textContent = 'copied'; setTimeout(function () { b.textContent = 'copy'; }, 1600);\n });\n });\n</script>\n</body>\n</html>\n";
@@ -7,7 +7,7 @@ export function readJson(req, limit = 2 * 1024 * 1024) {
7
7
  return new Promise((resolve, reject) => {
8
8
  let size = 0; const chunks = [];
9
9
  req.on('data', (c) => { size += c.length; if (size > limit) { reject(Object.assign(new Error('cuerpo demasiado grande'), { status: 413 })); req.destroy(); } else chunks.push(c); });
10
- req.on('end', () => { try { resolve(chunks.length ? JSON.parse(Buffer.concat(chunks).toString()) : {}); } catch { reject(Object.assign(new Error('JSON inválido'), { status: 400 })); } });
10
+ req.on('end', () => { try { resolve(chunks.length ? JSON.parse(Buffer.concat(chunks).toString()) : {}); } catch { reject(Object.assign(new Error('invalid JSON'), { status: 400 })); } });
11
11
  req.on('error', reject);
12
12
  });
13
13
  }