@nyx5/nyx5 0.2.4 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyx5/nyx5",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "description": "Nyx5/1 — mail and ledger for AI agents: agent@domain addresses, a store-and-forward mailbox, and a ledger where a claim costs something. One command to join, escrow released only against a deterministic check, and reputation that is just a query on the ledger. Zero dependencies.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -21,14 +21,12 @@
21
21
  "files": [
22
22
  "CONTRIBUTING.md",
23
23
  "LICENSE",
24
- "README.es.md",
25
24
  "README.md",
26
25
  "SECURITY.md",
27
26
  "agents.md",
28
27
  "bin/",
29
28
  "demo/",
30
29
  "docs/ARQUITECTURA.md",
31
- "docs/SPEC.en.md",
32
30
  "docs/SPEC.md",
33
31
  "examples/",
34
32
  "migrations/",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "url": "https://github.com/Nicoiakl/nyx5",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.2.4",
10
+ "version": "0.3.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@nyx5/nyx5",
16
- "version": "0.2.4",
16
+ "version": "0.3.1",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"
@@ -89,20 +89,20 @@ export class Agent {
89
89
  // Tarjetas de los destinatarios: para cifrar (clave enc) y para saber si exigen proof-of-work.
90
90
  const cards = await Promise.all(recipients.map((r) => this.resolver.agentCard(r).catch((e) => ({ address: r, _error: e.message }))));
91
91
  const missing = cards.filter((c) => c._error);
92
- if (missing.length) throw new Error(`no se pudo resolver: ${missing.map((c) => `${c.address} (${c._error})`).join(', ')}`);
92
+ if (missing.length) throw new Error(`could not resolve: ${missing.map((c) => `${c.address} (${c._error})`).join(', ')}`);
93
93
 
94
94
  let env;
95
95
  if (encrypt && cards.every((c) => c.enc)) {
96
96
  env = { ...base, encrypted: encryptContent(content, cards.map((c) => ({ address: c.address, enc: c.enc })), aad(base)) };
97
97
  } else {
98
- if (encrypt === 'required') throw new Error('algún destinatario no publica clave de cifrado');
98
+ if (encrypt === 'required') throw new Error('a recipient does not publish an encryption key');
99
99
  env = { ...base, content };
100
100
  }
101
101
  const powBits = Math.max(0, ...cards.map((c) => (c.inbox?.policy === 'pow' ? c.inbox.pow_bits ?? 16 : 0)));
102
102
  if (powBits) env.pow = mintPow(id, powBits);
103
103
  const stamped = cards.find((c) => c.inbox?.policy === 'stamp');
104
104
  if (stamped) {
105
- if (recipients.length > 1) throw new Error('un sobre con estampilla lleva un solo destinatario');
105
+ if (recipients.length > 1) throw new Error('a stamped envelope carries a single recipient');
106
106
  env.stamp = { house: stamped.inbox.house || parseAddress(stamped.address).domain, amount: stamped.inbox.price ?? 1 };
107
107
  }
108
108
 
@@ -221,9 +221,9 @@ export class Agent {
221
221
  }
222
222
  const card = await this.resolver.agentCardForKid(envelope.from, envelope.signature?.kid);
223
223
  const verified = Resolver.acceptedKids(card).includes(envelope.signature?.kid) && verifyObject(envelope, envelope.signature.kid);
224
- if (!verified) throw new Error(`firma inválida en sobre ${envelope.id} de ${envelope.from}`);
224
+ if (!verified) throw new Error(`invalid signature on envelope ${envelope.id} from ${envelope.from}`);
225
225
  if (envelope.expires && Date.parse(envelope.expires) < Date.now()) throw new Error(`sobre vencido: ${envelope.id}`);
226
- if (!envelope.encrypted && !envelope.to.includes(this.address)) throw new Error(`sobre ${envelope.id} no dirigido a ${this.address}`);
226
+ if (!envelope.encrypted && !envelope.to.includes(this.address)) throw new Error(`envelope ${envelope.id} is not addressed to ${this.address}`);
227
227
  const content = envelope.encrypted ? decryptContent(envelope.encrypted, this.address, this.keys, aad(envelope)) : envelope.content;
228
228
  return { id: envelope.id, from: envelope.from, to: envelope.to, type: envelope.type, thread: envelope.thread, in_reply_to: envelope.in_reply_to, created: envelope.created, encrypted: !!envelope.encrypted, sender: card, content };
229
229
  }
@@ -27,8 +27,8 @@ import { veredicto, pruebasDe, pruebasDisponibles } from '../libro/verifica.js';
27
27
  import { contratoPublico, ACP } from '../libro/contratos.js';
28
28
  import { Tareas } from '../libro/tareas.js';
29
29
  import { APP_HTML } from '../plataformas/app-html.js';
30
- import { SPEC_HTML, SPEC_HTML_ES, LLMS_TXT } from '../plataformas/spec-html.js';
31
- import { HOME_HTML, HOME_HTML_ES } from '../plataformas/home-html.js';
30
+ import { SPEC_HTML, LLMS_TXT } from '../plataformas/spec-html.js';
31
+ import { HOME_HTML } from '../plataformas/home-html.js';
32
32
  import { inboundEnvelope, outboundPayload, isEmailAddress } from '../puentes/email.js';
33
33
 
34
34
  const now = () => Date.now();
@@ -156,10 +156,10 @@ export class Estafeta {
156
156
  // N veces en paralelo, multiplicando el regalo de bienvenida (emisión no autorizada).
157
157
  async _consumeInvite(code) {
158
158
  const inv = code && await this.store.getInvite(String(code));
159
- if (!inv) throw Object.assign(new Error('invitación inexistente'), { status: 403 });
160
- if (inv.expires && Date.parse(inv.expires) < now()) throw Object.assign(new Error('invitación vencida'), { status: 403 });
159
+ if (!inv) throw Object.assign(new Error('no such invitation'), { status: 403 });
160
+ if (inv.expires && Date.parse(inv.expires) < now()) throw Object.assign(new Error('invitation expired'), { status: 403 });
161
161
  const usada = await this.store.consumeInvite(String(code), iso());
162
- if (!usada) throw Object.assign(new Error('invitación agotada'), { status: 403 });
162
+ if (!usada) throw Object.assign(new Error('invitation used up'), { status: 403 });
163
163
  return usada;
164
164
  }
165
165
  // Directorio público de la casa: tarjetas sin datos privados, con filtros por capacidad.
@@ -198,27 +198,27 @@ export class Estafeta {
198
198
  local = String(local).toLowerCase();
199
199
  const address = `${local}@${this.domain}`;
200
200
  parseAddress(address);
201
- if (Estafeta.RESERVED.has(local) && !this.isSystem(local)) throw Object.assign(new Error(`nombre reservado: ${local}`), { status: 409 });
201
+ if (Estafeta.RESERVED.has(local) && !this.isSystem(local)) throw Object.assign(new Error(`name reserved by the protocol: ${local}`), { status: 409 });
202
202
  if (delegation) {
203
203
  // Tarjeta delegada: el agente padre firma { by, address, sig, scope, valid_until }; el dominio la certifica igual.
204
204
  const { local: parentLocal, domain: parentDomain } = parseAddress(delegation.by);
205
- if (parentDomain !== this.domain || !local.endsWith(`.${parentLocal}`)) throw Object.assign(new Error(`un agente delegado de ${delegation.by} debe llamarse <nombre>.${parentLocal}@${this.domain}`), { status: 400 });
205
+ if (parentDomain !== this.domain || !local.endsWith(`.${parentLocal}`)) throw Object.assign(new Error(`a subagent of ${delegation.by} must be named <name>.${parentLocal}@${this.domain}`), { status: 400 });
206
206
  const parent = await this.store.getAgent(parentLocal);
207
207
  if (!parent) throw Object.assign(new Error('agente padre inexistente'), { status: 404 });
208
- if (delegation.address !== address || delegation.sig !== sig || !verifyObject(delegation, parent.sig)) throw Object.assign(new Error('delegación inválida: debe estar firmada por el padre y coincidir con la tarjeta'), { status: 403 });
208
+ if (delegation.address !== address || delegation.sig !== sig || !verifyObject(delegation, parent.sig)) throw Object.assign(new Error('invalid delegation: it must be signed by the parent and match the card'), { status: 403 });
209
209
  // Un delegado nunca tiene más ámbito que su padre (invariante 6). Lo que el hijo no declara
210
210
  // lo HEREDA: declarar menos no puede ser una forma de escapar del tope de la cadena.
211
211
  const ps = parent.delegation?.scope;
212
212
  if (ps) {
213
213
  const hs = delegation.scope || {};
214
214
  if (ps.cap != null && (hs.cap == null || hs.cap > ps.cap)) {
215
- if (hs.cap != null) throw Object.assign(new Error(`el delegado no puede tener más tope que su padre (${ps.cap})`), { status: 403 });
215
+ if (hs.cap != null) throw Object.assign(new Error(`a subagent cannot have a higher cap than its parent (${ps.cap})`), { status: 403 });
216
216
  hs.cap = ps.cap; // sin tope declarado: hereda el del padre
217
217
  }
218
218
  for (const campo of ['types', 'to_domains']) {
219
219
  if (ps[campo]?.length) {
220
220
  if (!hs[campo]?.length) hs[campo] = [...ps[campo]];
221
- else if (!hs[campo].every((x) => ps[campo].includes(x))) throw Object.assign(new Error(`el delegado no puede ampliar ${campo} respecto de su padre`), { status: 403 });
221
+ else if (!hs[campo].every((x) => ps[campo].includes(x))) throw Object.assign(new Error(`a subagent cannot widen ${campo} beyond its parent`), { status: 403 });
222
222
  }
223
223
  }
224
224
  delegation = { ...delegation, scope: hs };
@@ -243,7 +243,7 @@ export class Estafeta {
243
243
  creado = await (this.store.putAgentIfAbsent
244
244
  ? this.store.putAgentIfAbsent(local, { ...card, webhook, notify_email })
245
245
  : (this.store.putAgent(local, { ...card, webhook, notify_email }), true));
246
- if (!creado) throw Object.assign(new Error('ese nombre acaba de ser tomado; solo su dueño o la casa pueden actualizarlo'), { status: 409 });
246
+ if (!creado) throw Object.assign(new Error('that name was just taken; only its owner or the house can update it'), { status: 409 });
247
247
  } else {
248
248
  await this.store.putAgent(local, { ...card, webhook, notify_email });
249
249
  }
@@ -279,15 +279,15 @@ export class Estafeta {
279
279
  let rec;
280
280
  if (domain === this.domain) {
281
281
  rec = await this.store.getAgent(local);
282
- if (!rec) throw Object.assign(new Error('agente no registrado'), { status: 401 });
282
+ if (!rec) throw Object.assign(new Error('agent not registered'), { status: 401 });
283
283
  } else {
284
- if (!allowForeign) throw Object.assign(new Error('el agente no pertenece a este dominio'), { status: 401 });
285
- try { rec = await this.resolver.agentCard(claims.address); } catch (e) { throw Object.assign(new Error(`agente foráneo no verificable: ${e.message}`), { status: 401 }); }
284
+ if (!allowForeign) throw Object.assign(new Error('the agent does not belong to this domain'), { status: 401 });
285
+ try { rec = await this.resolver.agentCard(claims.address); } catch (e) { throw Object.assign(new Error(`foreign agent could not be verified: ${e.message}`), { status: 401 }); }
286
286
  }
287
- if (Math.abs(now() - Date.parse(claims.ts)) > 300_000) throw Object.assign(new Error('token vencido (ventana de 5 min)'), { status: 401 });
288
- if (claims.method !== rx.method || claims.path !== path) throw Object.assign(new Error('token no corresponde a esta petición'), { status: 401 });
289
- if (claims.host !== this.authHost) throw Object.assign(new Error(`token emitido para otra casa (host ${claims.host || 'ausente'}, se espera ${this.authHost})`), { status: 401 });
290
- if (!verifyBytes(canonical(claims), m[2], rec.sig)) throw Object.assign(new Error('firma de token inválida'), { status: 401 });
287
+ if (Math.abs(now() - Date.parse(claims.ts)) > 300_000) throw Object.assign(new Error('token expired (5 minute window)'), { status: 401 });
288
+ if (claims.method !== rx.method || claims.path !== path) throw Object.assign(new Error('token does not match this request'), { status: 401 });
289
+ if (claims.host !== this.authHost) throw Object.assign(new Error(`token issued for another house (host ${claims.host || 'missing'}, expected ${this.authHost})`), { status: 401 });
290
+ if (!verifyBytes(canonical(claims), m[2], rec.sig)) throw Object.assign(new Error('invalid token signature'), { status: 401 });
291
291
  if (!await this.store.useNonce(`${claims.address}:${claims.nonce}`, now())) throw Object.assign(new Error('nonce reutilizado'), { status: 401 });
292
292
  return { local, address: claims.address, record: rec };
293
293
  }
@@ -296,8 +296,8 @@ export class Estafeta {
296
296
  async outbound(env, submitter) {
297
297
  const v = validateEnvelope(env, { maxBytes: this.policy.max_bytes });
298
298
  if (!v.ok) return v;
299
- if (env.from !== submitter.address) return { ok: false, code: 403, reason: 'from no coincide con el agente autenticado' };
300
- if (env.signature.kid !== submitter.record.sig || !verifyObject(env, submitter.record.sig)) return { ok: false, code: 403, reason: 'firma del sobre inválida' };
299
+ if (env.from !== submitter.address) return { ok: false, code: 403, reason: 'from does not match the authenticated agent' };
300
+ if (env.signature.kid !== submitter.record.sig || !verifyObject(env, submitter.record.sig)) return { ok: false, code: 403, reason: 'invalid envelope signature' };
301
301
  const scope = submitter.record.delegation?.scope;
302
302
  if (scope?.types?.length && !scope.types.includes(env.type)) return { ok: false, code: 403, reason: `agente delegado: solo puede enviar type ${scope.types.join('|')}` };
303
303
  if (scope?.to_domains?.length && !env.to.every((t) => scope.to_domains.includes(parseAddress(t).domain))) return { ok: false, code: 403, reason: `agente delegado: solo puede escribir a ${scope.to_domains.join(', ')}` };
@@ -335,7 +335,7 @@ export class Estafeta {
335
335
  async _deliver(job) {
336
336
  // Un sobre diferido pudo vencer mientras esperaba: no desaparece en silencio, rebota al remitente.
337
337
  if (job.envelope.expires && Date.parse(job.envelope.expires) < now()) {
338
- for (const to of job.to) await this._bounce(job, to, 'el sobre venció esperando en la cola');
338
+ for (const to of job.to) await this._bounce(job, to, 'the envelope expired while waiting in the queue');
339
339
  job.status = 'failed'; await this.store.removeJob(job.id); await this._outbox(job); return;
340
340
  }
341
341
  job.attempts += 1;
@@ -378,20 +378,20 @@ export class Estafeta {
378
378
  for (const r of outcome.body.rejected || []) (RETRYABLE.has(r.code) ? retry : failed).push(r);
379
379
  // Destinatarios que la respuesta no menciona: transitorio, no éxito silencioso.
380
380
  const mentioned = new Set([...delivered, ...(outcome.body.rejected || []).map((r) => r.to)]);
381
- for (const to of job.to) if (!mentioned.has(to)) retry.push({ to, code: outcome.status, reason: 'destinatario sin veredicto en la respuesta' });
381
+ for (const to of job.to) if (!mentioned.has(to)) retry.push({ to, code: outcome.status, reason: 'recipient missing a verdict in the response' });
382
382
  } else if (outcome.status === 0 || RETRYABLE.has(outcome.status)) {
383
383
  retry.push(...job.to.map((to) => ({ to, code: outcome.status, reason: outcome.error || outcome.body?.reason })));
384
384
  } else {
385
385
  failed.push(...job.to.map((to) => ({ to, code: outcome.status, reason: outcome.body?.reason || 'rechazo permanente' })));
386
386
  }
387
387
 
388
- for (const f of failed) await this._bounce(job, f.to, `rechazado (${f.code}): ${f.reason}`);
389
- if (delivered.length && job.envelope.receipt === 'delivered') for (const to of delivered) await this._notify(job, to, 'delivered', 'entregado en la estafeta destino');
388
+ for (const f of failed) await this._bounce(job, f.to, `rejected (${f.code}): ${f.reason}`);
389
+ if (delivered.length && job.envelope.receipt === 'delivered') for (const to of delivered) await this._notify(job, to, 'delivered', 'delivered to the destination estafeta');
390
390
 
391
391
  if (retry.length) {
392
392
  const age = now() - Date.parse(job.created);
393
393
  if (age > this.retry.giveUpMs) {
394
- for (const r of retry) await this._bounce(job, r.to, `sin respuesta tras ${job.attempts} intentos: ${r.reason}`);
394
+ for (const r of retry) await this._bounce(job, r.to, `no answer after ${job.attempts} attempts: ${r.reason}`);
395
395
  job.status = 'failed'; await this.store.removeJob(job.id); await this._outbox(job); return;
396
396
  }
397
397
  const backoff = Math.min(this.retry.baseMs * 2 ** (job.attempts - 1), this.retry.maxMs) * (0.8 + Math.random() * 0.4);
@@ -463,12 +463,12 @@ export class Estafeta {
463
463
  async _tomarTarea(env, senderCard) {
464
464
  const q = env.content?.body;
465
465
  if (env.content?.media !== MEDIA.cotizacion || q?.tipo !== 'cotizacion') {
466
- return { ok: false, code: 400, reason: `tareas@ acepta una cotización (${MEDIA.cotizacion}) por la tarea publicada; mira GET /tareas` };
466
+ return { ok: false, code: 400, reason: `tareas@ accepts a quote (${MEDIA.cotizacion}) for a published task; see GET /tareas` };
467
467
  }
468
468
  const tarea = this.tareas.tarea(q.terms?.seed_task);
469
- if (!tarea) return { ok: false, code: 404, reason: `no hay una tarea sembrada con id ${q.terms?.seed_task}. Las publicadas están en GET /tareas` };
470
- if (q.seller !== env.from) return { ok: false, code: 403, reason: 'el vendedor de la cotización debe ser quien la manda' };
471
- if (q.buyer !== `tareas@${this.domain}`) return { ok: false, code: 400, reason: `la cotización debe ir dirigida a tareas@${this.domain}` };
469
+ if (!tarea) return { ok: false, code: 404, reason: `no seeded task with id ${q.terms?.seed_task}. The published ones are at GET /tareas` };
470
+ if (q.seller !== env.from) return { ok: false, code: 403, reason: 'the seller of the quote must be whoever sends it' };
471
+ if (q.buyer !== `tareas@${this.domain}`) return { ok: false, code: 400, reason: `the quote must be addressed to tareas@${this.domain}` };
472
472
  const encaja = this.tareas.coincide(q, tarea, { arbitro: `verifica@${this.domain}` });
473
473
  if (!encaja.ok) return { ok: false, code: 409, reason: encaja.reason };
474
474
  const contratos = await this.store.libroListContracts();
@@ -484,7 +484,7 @@ export class Estafeta {
484
484
  content: { media: MEDIA.op, body: { op: 'accept', quote: q } },
485
485
  }, this.keys);
486
486
  const r = await this.inbound(aceptacion);
487
- if (!r.ok) return { ok: false, code: r.code || 409, reason: `la casa no pudo tomar la tarea: ${r.reason}` };
487
+ if (!r.ok) return { ok: false, code: r.code || 409, reason: `the house could not take the task: ${r.reason}` };
488
488
  const contrato = Object.values(r.results || {})[0]?.contract || null;
489
489
  this.log(`tarea ${tarea.id} tomada por ${env.from} (contrato ${contrato?.id})`);
490
490
  await this._evento('seed_task_taken', env.from, { task: tarea.id, contract: contrato?.id || null, price: tarea.price });
@@ -527,16 +527,16 @@ export class Estafeta {
527
527
  const yaAceptados = new Set(seen?.accepted || []);
528
528
 
529
529
  const locals = env.to.filter((t) => parseAddress(t).domain === this.domain);
530
- if (!locals.length) return { ok: false, code: 404, reason: 'ningún destinatario pertenece a este dominio' };
530
+ if (!locals.length) return { ok: false, code: 404, reason: 'no recipient belongs to this domain' };
531
531
  const pendientes = locals.filter((t) => !yaAceptados.has(t));
532
532
  if (!pendientes.length) return { ok: true, code: 200, duplicate: true, accepted: [...yaAceptados], rejected: [] };
533
533
 
534
534
  // Cadena de confianza: dominio emisor -> agente emisor -> firma del sobre
535
535
  let senderCard;
536
536
  try { senderCard = await this.resolver.agentCardForKid(env.from, env.signature.kid); }
537
- catch (e) { return { ok: false, code: e.permanent ? 403 : 421, reason: `no se pudo verificar al remitente: ${e.message}` }; }
537
+ catch (e) { return { ok: false, code: e.permanent ? 403 : 421, reason: `could not verify the sender: ${e.message}` }; }
538
538
  const validKids = Resolver.acceptedKids(senderCard);
539
- if (!validKids.includes(env.signature.kid) || !verifyObject(env, env.signature.kid)) return { ok: false, code: 403, reason: 'la firma del sobre no corresponde al remitente' };
539
+ if (!validKids.includes(env.signature.kid) || !verifyObject(env, env.signature.kid)) return { ok: false, code: 403, reason: 'the envelope signature does not match the sender' };
540
540
 
541
541
  // Firma de relay (segunda capa: la estafeta emisora también firma, análogo a SPF/DKIM)
542
542
  const { domain: fromDomain } = parseAddress(env.from);
@@ -545,8 +545,8 @@ export class Estafeta {
545
545
  const r = Object.fromEntries(relayHeader.replace(/^nyx51\s*/, '').split(';').map((p) => p.trim().split('=').map((x) => x.trim())).filter((p) => p[0]));
546
546
  relayVerified = r.domain === fromDomain && senderCard._domain.keys.some((k) => k.sig === r.kid) && verifyBytes(`relay:${env.id}:${this.domain}`, r.sig, r.kid);
547
547
  }
548
- if (this.policy.require_relay && !relayVerified) return { ok: false, code: 403, reason: 'este dominio exige firma de relay válida' };
549
- if (!this.rate.allow(fromDomain)) return { ok: false, code: 429, reason: 'límite de tasa del dominio emisor' };
548
+ if (this.policy.require_relay && !relayVerified) return { ok: false, code: 403, reason: 'this domain requires a valid relay signature' };
549
+ if (!this.rate.allow(fromDomain)) return { ok: false, code: 429, reason: 'rate limit for the sending domain' };
550
550
 
551
551
  const accepted = [], rejected = [], results = {};
552
552
  const mails = [], libroBundles = [];
@@ -556,7 +556,7 @@ export class Estafeta {
556
556
  for (const to of pendientes) {
557
557
  const { local } = parseAddress(to);
558
558
  const rec = await this.store.getAgent(local);
559
- if (!rec) { rejected.push({ to, code: 404, reason: 'agente inexistente' }); continue; }
559
+ if (!rec) { rejected.push({ to, code: 404, reason: 'no such agent' }); continue; }
560
560
  const p = applyInboxPolicy(env, rec, senderCard._domain);
561
561
  if (!p.ok) { rejected.push({ to, ...p, ok: undefined }); continue; }
562
562
  try {
@@ -578,7 +578,7 @@ export class Estafeta {
578
578
  results[to] = r.result;
579
579
  } else if (p.stamp) {
580
580
  // Una estampilla paga UN buzón: el sobre declara un monto, no un monto por destinatario.
581
- if (stampUsed) { rejected.push({ to, code: 402, reason: 'la estampilla del sobre ya se usó en otro destinatario' }); continue; }
581
+ if (stampUsed) { rejected.push({ to, code: 402, reason: 'the envelope stamp was already spent on another recipient' }); continue; }
582
582
  const { asiento, bundle } = await this.libro.stamp(env, to, p.stamp.price);
583
583
  stampUsed = true;
584
584
  libroBundles.push(bundle);
@@ -596,7 +596,7 @@ export class Estafeta {
596
596
  : b.beneficiary !== p.vouch.beneficiary ? 'la fianza no tiene al receptor como beneficiario'
597
597
  : b.verifier !== p.vouch.beneficiary ? 'el receptor no puede ejecutar la fianza (no es su verificador)'
598
598
  : null;
599
- if (malo) { rejected.push({ to, code: 403, reason: `aval inválido: ${malo}` }); continue; }
599
+ if (malo) { rejected.push({ to, code: 403, reason: `invalid vouch: ${malo}` }); continue; }
600
600
  mails.push({ local, envelope: env, meta: { from_verified: true, relay_verified: relayVerified, sender_kid: env.signature.kid, vouched_by: b.seller, vouch_bond: b.id } });
601
601
  } else {
602
602
  mails.push({ local, envelope: env, meta: { from_verified: true, relay_verified: relayVerified, sender_kid: env.signature.kid } });
@@ -652,8 +652,8 @@ export class Estafeta {
652
652
  if (emailOrig && emailOrig === rec.notify_email) return; // no te avises de tu propio correo
653
653
  const quien = emailOrig || env.from;
654
654
  await this.emailOut({ fromAgent: `${local}@${this.domain}`, to: rec.notify_email,
655
- subject: `Tienes un mensaje nuevo en Nyx5 de ${quien}`,
656
- text: `${quien} le escribió a ${local}@${this.domain}.\n\nÁbrelo en tu buzón: https://${this.domain}/app\n\n(Este es un aviso automático; el contenido está en tu buzón, no en este correo.)` });
655
+ subject: `New message on Nyx5 from ${quien}`,
656
+ text: `${quien} wrote to ${local}@${this.domain}.\n\nOpen it in the mailbox: https://${this.domain}/app\n\n(Automatic notice. The content is in the mailbox, not in this email.)` });
657
657
  }).catch((e) => this.log(`notify_email ${local} falló: ${e.message}`));
658
658
  this._pushes.push(pendiente);
659
659
  return pendiente;
@@ -667,11 +667,11 @@ export class Estafeta {
667
667
  // verifica igual por la cadena normal (DNS -> dominio -> agente) al momento de usarla.
668
668
  async indexAddHouse(domain) {
669
669
  domain = String(domain || '').toLowerCase();
670
- if (!/^[a-z0-9.-]+$/.test(domain)) throw Object.assign(new Error('dominio inválido'), { status: 400 });
670
+ if (!/^[a-z0-9.-]+$/.test(domain)) throw Object.assign(new Error('invalid domain'), { status: 400 });
671
671
  const houses = await this.store.indexListHouses();
672
- if (houses.length >= this.index.maxHouses && !houses.some((h) => h.domain === domain)) throw Object.assign(new Error('índice lleno'), { status: 507 });
672
+ if (houses.length >= this.index.maxHouses && !houses.some((h) => h.domain === domain)) throw Object.assign(new Error('index full'), { status: 507 });
673
673
  // La verificación ES la puerta: solo se lista lo que resuelve y firma como casa Nyx5.
674
- const dc = await this.resolver.domainCard(domain).catch((e) => { throw Object.assign(new Error(`casa no verificable: ${e.message}`), { status: 422 }); });
674
+ const dc = await this.resolver.domainCard(domain).catch((e) => { throw Object.assign(new Error(`house could not be verified: ${e.message}`), { status: 422 }); });
675
675
  const h = { domain, estafeta: dc._estafeta, added: iso(), last_ok: null, fails: 0 };
676
676
  await this.store.indexPutHouse(h);
677
677
  await this._indexCrawlHouse(h);
@@ -719,12 +719,12 @@ export class Estafeta {
719
719
  // from_verified:false y via:'email'. No pasa por /inbound ni finge estar firmado (invariante 1).
720
720
  async receiveEmail({ from, to, subject, text, messageId } = {}) {
721
721
  await this.init();
722
- if (!isEmailAddress(from)) return { ok: false, code: 400, reason: 'remitente de correo inválido' };
722
+ if (!isEmailAddress(from)) return { ok: false, code: 400, reason: 'invalid email sender' };
723
723
  let local, dom;
724
- try { ({ local, domain: dom } = parseAddress(to)); } catch { return { ok: false, code: 400, reason: 'destinatario inválido' }; }
725
- if (dom !== this.domain) return { ok: false, code: 400, reason: `el correo es para ${dom}, no ${this.domain}` };
724
+ try { ({ local, domain: dom } = parseAddress(to)); } catch { return { ok: false, code: 400, reason: 'invalid recipient' }; }
725
+ if (dom !== this.domain) return { ok: false, code: 400, reason: `the email is for ${dom}, not ${this.domain}` };
726
726
  const rec = await this.store.getAgent(local);
727
- if (!rec) return { ok: false, code: 404, reason: 'agente inexistente' };
727
+ if (!rec) return { ok: false, code: 404, reason: 'no such agent' };
728
728
  const env = inboundEnvelope({ from, to: `${local}@${this.domain}`, subject, text, messageId });
729
729
  // dedupe por id (message-id del correo o uuid) igual que un sobre normal
730
730
  const nuevo = await this.store.markSeenIfNew(env.id, { from: `email:${from}`, accepted: [to] });
@@ -736,13 +736,13 @@ export class Estafeta {
736
736
  // SALIDA: un agente le escribe a una dirección de correo. Con proveedor, se envía (Reply-To = el
737
737
  // agente, para que la respuesta vuelva por ENTRADA). Sin proveedor, queda pendiente: no se inventa canal.
738
738
  async emailOut({ fromAgent, to, subject, text }) {
739
- if (!isEmailAddress(to)) return { ok: false, code: 400, reason: 'destino de correo inválido' };
739
+ if (!isEmailAddress(to)) return { ok: false, code: 400, reason: 'invalid email destination' };
740
740
  // El pie viaja solo si la casa lo enciende (`email.footer`). Apagado por defecto: el texto
741
741
  // que un tercero recibe es decisión del operador de la casa, no del código.
742
742
  const payload = outboundPayload({ fromAgent, to, subject, text, footer: this.email.footer === true, domain: this.domain });
743
- if (!this.email.provider) return { ok: false, code: 503, pending: true, reason: 'el puente de correo no tiene proveedor de salida configurado' };
743
+ if (!this.email.provider) return { ok: false, code: 503, pending: true, reason: 'the email bridge has no outbound provider configured' };
744
744
  try { const r = await this.email.provider(payload); return { ok: true, code: 202, provider: r?.id || null }; }
745
- catch (e) { return { ok: false, code: 502, reason: `el proveedor de correo falló: ${e.message}` }; }
745
+ catch (e) { return { ok: false, code: 502, reason: `the email provider failed: ${e.message}` }; }
746
746
  }
747
747
 
748
748
  async indexSearch(params) {
@@ -761,15 +761,10 @@ export class Estafeta {
761
761
  if (rx.method === 'GET' && path === '/health') return send(200, { ok: true, domain: this.domain, agents: (await this.store.listAgents()).length, queue: (await this.store.listQueue()).length });
762
762
  // Cliente web para personas: se sirve desde la propia casa (mismo origen, sin CORS).
763
763
  // El HTML genera las llaves en el navegador del usuario; la casa nunca las ve.
764
- // Inglés primario en la portada; el español vive en /es-home. Ambas se enlazan con hreflang.
765
764
  if (rx.method === 'GET' && path === '/') return { status: 200, body: HOME_HTML, contentType: 'text/html; charset=utf-8' };
766
- if (rx.method === 'GET' && (path === '/es-home' || path === '/es-home/')) return { status: 200, body: HOME_HTML_ES, contentType: 'text/html; charset=utf-8' };
767
765
  if (rx.method === 'GET' && (path === '/app' || path === '/app/')) return { status: 200, body: APP_HTML, contentType: 'text/html; charset=utf-8' };
768
766
  // La especificación en una página, indexable. Se genera desde docs/SPEC.md (build:spec).
769
- // El inglés es la versión canónica de la spec (es donde busca quien integra un protocolo);
770
- // el español vive en /es y ambas se enlazan entre sí con hreflang.
771
767
  if (rx.method === 'GET' && (path === '/spec' || path === '/spec/')) return { status: 200, body: SPEC_HTML, contentType: 'text/html; charset=utf-8' };
772
- if (rx.method === 'GET' && (path === '/es' || path === '/es/' || path === '/spec/es')) return { status: 200, body: SPEC_HTML_ES, contentType: 'text/html; charset=utf-8' };
773
768
  if (rx.method === 'GET' && path === '/llms.txt') return { status: 200, body: LLMS_TXT, contentType: 'text/plain; charset=utf-8' };
774
769
  if (rx.method === 'GET' && path === '/.well-known/nyx5.json') return send(200, await this.domainCard());
775
770
  let m;
@@ -778,12 +773,12 @@ export class Estafeta {
778
773
  // contrapartes: solo cuántas entregas, cuántas fianzas y cuántos tokens se movieron.
779
774
  if (rx.method === 'GET' && (m = /^\/agents\/([^/]+)\/historial$/.exec(path))) {
780
775
  const local = decodeURIComponent(m[1]).toLowerCase();
781
- if (!(await this.store.getAgent(local))) return send(404, { reason: 'agente inexistente' });
776
+ if (!(await this.store.getAgent(local))) return send(404, { reason: 'no such agent' });
782
777
  return send(200, await this.libro.historial(`${local}@${this.domain}`));
783
778
  }
784
779
  if (rx.method === 'GET' && (m = /^\/agents\/([^/]+)$/.exec(path))) {
785
780
  const card = await this.agentCard(decodeURIComponent(m[1]).toLowerCase());
786
- return card ? send(200, card) : send(404, { reason: 'agente inexistente' });
781
+ return card ? send(200, card) : send(404, { reason: 'no such agent' });
787
782
  }
788
783
  if (rx.method === 'GET' && path === '/agents') {
789
784
  const p = Object.fromEntries(rx.query);
@@ -792,7 +787,7 @@ export class Estafeta {
792
787
  if (rx.method === 'POST' && path === '/agents') {
793
788
  const body = rx.body || {};
794
789
  const local = String(body.local || '').toLowerCase();
795
- if (!Estafeta.validLocal(local)) return send(400, { reason: 'nombre de agente inválido' });
790
+ if (!Estafeta.validLocal(local)) return send(400, { reason: 'invalid agent name' });
796
791
  const exists = !!(await this.store.getAgent(local));
797
792
  const isAdmin = (rx.headers.authorization || '') === `Bearer ${this.adminToken}`;
798
793
  let ok = isAdmin, via = 'admin';
@@ -803,13 +798,13 @@ export class Estafeta {
803
798
  }
804
799
  if (!ok) {
805
800
  // Auto-registro: el cuerpo viene firmado por la clave que se inscribe (prueba de posesión).
806
- if (exists) return send(409, { reason: 'ese nombre ya está tomado; solo su dueño o la casa pueden actualizarlo' });
807
- if (!body.signature || body.signature.kid !== body.sig || !verifyObject(body, body.sig)) return send(401, { reason: 'para auto-registrarse, firma el cuerpo con la clave sig que inscribes (prueba de posesión)' });
808
- if (Math.abs(now() - Date.parse(body.ts || 0)) > 300_000) return send(401, { reason: 'la solicitud firmada necesita ts (ISO) dentro de 5 minutos' });
809
- if (!this.regRate.allow(rx.ip || 'x')) return send(429, { reason: 'demasiados registros desde esta dirección' });
801
+ if (exists) return send(409, { reason: 'that name is taken; only its owner or the house can update it' });
802
+ if (!body.signature || body.signature.kid !== body.sig || !verifyObject(body, body.sig)) return send(401, { reason: 'to self-register, sign the body with the same sig key you are enrolling (proof of possession)' });
803
+ if (Math.abs(now() - Date.parse(body.ts || 0)) > 300_000) return send(401, { reason: 'the signed request needs a ts (ISO) within 5 minutes' });
804
+ if (!this.regRate.allow(rx.ip || 'x')) return send(429, { reason: 'too many registrations from this address' });
810
805
  if (this.policy.registration === 'open') via = 'open';
811
806
  else if (this.policy.registration === 'invite') { const inv = await this._consumeInvite(body.invite); via = `invite:${inv.code}`; if (inv.welcome != null) body._welcome = inv.welcome; }
812
- else return send(403, { reason: `esta casa no acepta auto-registro (registration=${this.policy.registration}); pide una invitación` });
807
+ else return send(403, { reason: `this house does not accept self-registration (registration=${this.policy.registration}); ask for an invitation` });
813
808
  }
814
809
  // `source` es atribución y NO entra en la tarjeta: se descarta aquí y viaja al evento.
815
810
  const { signature: _s, ts: _t, invite: _i, _welcome, source: _src, ...clean } = body;
@@ -822,11 +817,11 @@ export class Estafeta {
822
817
  return send(201, { ...card, registered_via: via });
823
818
  }
824
819
  if (rx.method === 'POST' && path === '/invitations') {
825
- if ((rx.headers.authorization || '') !== `Bearer ${this.adminToken}`) return send(401, { reason: 'solo la casa emite invitaciones' });
820
+ if ((rx.headers.authorization || '') !== `Bearer ${this.adminToken}`) return send(401, { reason: 'only the house issues invitations' });
826
821
  return send(201, await this.createInvite(rx.body || {}));
827
822
  }
828
823
  if (rx.method === 'GET' && path === '/invitations') {
829
- if ((rx.headers.authorization || '') !== `Bearer ${this.adminToken}`) return send(401, { reason: 'solo la casa lista invitaciones' });
824
+ if ((rx.headers.authorization || '') !== `Bearer ${this.adminToken}`) return send(401, { reason: 'only the house lists invitations' });
830
825
  return send(200, { invitations: await this.store.listInvites() });
831
826
  }
832
827
  if (rx.method === 'POST' && path === '/outbound') {
@@ -850,7 +845,7 @@ export class Estafeta {
850
845
  }
851
846
  if (rx.method === 'GET' && (m = /^\/mailbox\/([^/]+)$/.exec(path))) {
852
847
  const who = await this._authenticate(rx, path);
853
- if (who.local !== decodeURIComponent(m[1]).toLowerCase()) return send(403, { reason: 'buzón ajeno' });
848
+ if (who.local !== decodeURIComponent(m[1]).toLowerCase()) return send(403, { reason: 'not your mailbox' });
854
849
  const limit = Number(rx.query.get('limit') || 50);
855
850
  // los N más recientes (listMail viene en orden cronológico): con muchos mensajes viejos
856
851
  // sin ackear, slice(0,limit) escondía justo los nuevos. slice(-limit) muestra los últimos.
@@ -858,7 +853,7 @@ export class Estafeta {
858
853
  }
859
854
  if (rx.method === 'POST' && (m = /^\/mailbox\/([^/]+)\/ack$/.exec(path))) {
860
855
  const who = await this._authenticate(rx, path);
861
- if (who.local !== decodeURIComponent(m[1]).toLowerCase()) return send(403, { reason: 'buzón ajeno' });
856
+ if (who.local !== decodeURIComponent(m[1]).toLowerCase()) return send(403, { reason: 'not your mailbox' });
862
857
  const { ids = [] } = rx.body || {};
863
858
  const acked = [];
864
859
  for (const id of ids) if (await this.store.ackMail(who.local, id)) acked.push(id);
@@ -875,11 +870,11 @@ export class Estafeta {
875
870
  const who = await this._authenticate(rx, path, { allowForeign: true });
876
871
  const c = await this.store.libroGetContract(decodeURIComponent(m[1]));
877
872
  if (!c) return send(404, { reason: 'contrato inexistente' });
878
- if (![c.seller, c.buyer, c.verifier, c.arbiter].includes(who.address)) return send(403, { reason: 'no eres parte' });
873
+ if (![c.seller, c.buyer, c.verifier, c.arbiter].includes(who.address)) return send(403, { reason: 'you are not a party to this' });
879
874
  return send(200, contratoPublico(c));
880
875
  }
881
876
  if (rx.method === 'POST' && path === '/libro/topup') {
882
- if ((rx.headers.authorization || '') !== `Bearer ${this.adminToken}`) return send(401, { reason: 'solo la casa carga saldo' });
877
+ if ((rx.headers.authorization || '') !== `Bearer ${this.adminToken}`) return send(401, { reason: 'only the house tops up balances' });
883
878
  const { account, amount, concept } = rx.body || {};
884
879
  return send(201, await this.libro.topup(account, amount, concept || 'carga de la casa'));
885
880
  }
@@ -889,7 +884,7 @@ export class Estafeta {
889
884
  // El catálogo es público: un agente que acaba de unirse tiene que poder leer qué hay
890
885
  // que hacer, cuánto paga y con qué prueba se comprueba, sin autenticarse.
891
886
  if (rx.method === 'GET' && path === '/tareas') {
892
- if (!this.tareas.enabled) return send(404, { reason: 'esta casa no siembra trabajo' });
887
+ if (!this.tareas.enabled) return send(404, { reason: 'this house does not seed work' });
893
888
  return send(200, {
894
889
  mostrador: `tareas@${this.domain}`, arbitro: `verifica@${this.domain}`,
895
890
  por_agente_dia: this.tareas.porAgenteDia,
@@ -898,7 +893,7 @@ export class Estafeta {
898
893
  });
899
894
  }
900
895
  if (rx.method === 'GET' && path === '/eventos') {
901
- if ((rx.headers.authorization || '') !== `Bearer ${this.adminToken}`) return send(401, { reason: 'solo la casa lee sus eventos' });
896
+ if ((rx.headers.authorization || '') !== `Bearer ${this.adminToken}`) return send(401, { reason: 'only the house reads its events' });
902
897
  const p = Object.fromEntries(rx.query);
903
898
  const eventos = (await this.store.listEvents?.({ name: p.name || null, since: p.since || null, limit: Number(p.limit || 500) })) || [];
904
899
  const conteo = {};
@@ -906,7 +901,7 @@ export class Estafeta {
906
901
  return send(200, { conteo, eventos });
907
902
  }
908
903
  if (rx.method === 'GET' && path === '/libro/diario') {
909
- if ((rx.headers.authorization || '') !== `Bearer ${this.adminToken}`) return send(401, { reason: 'solo la casa lee el diario completo' });
904
+ if ((rx.headers.authorization || '') !== `Bearer ${this.adminToken}`) return send(401, { reason: 'only the house reads the full journal' });
910
905
  return send(200, { balances: (await this.store.libroState()).balances, journal: await this.libro.journal() });
911
906
  }
912
907
  if (rx.method === 'GET' && (m = /^\/outbox\/([^/]+)$/.exec(path))) {
@@ -19,7 +19,7 @@ export function validateEnvelope(env, { maxBytes = 1_048_576 } = {}) {
19
19
  if (typeof env.id !== 'string' || !/^[A-Za-z0-9._:-]{8,128}$/.test(env.id)) return fail('id inválido (se espera [A-Za-z0-9._:-]{8,128})');
20
20
  try { parseAddress(env.from); } catch { return fail('from inválido'); }
21
21
  if (!Array.isArray(env.to) || env.to.length < 1 || env.to.length > 50) return fail('to debe ser una lista de 1 a 50 direcciones');
22
- for (const t of env.to) { try { parseAddress(t); } catch { return fail(`destinatario inválido: ${t}`); } }
22
+ for (const t of env.to) { try { parseAddress(t); } catch { return fail(`invalid recipient: ${t}`); } }
23
23
  if (!TYPES.has(env.type)) return fail(`type inválido: ${env.type}`);
24
24
  if (Number.isNaN(Date.parse(env.created))) return fail('created debe ser ISO-8601');
25
25
  if (env.expires != null && Number.isNaN(Date.parse(env.expires))) return fail('expires debe ser ISO-8601');
@@ -39,7 +39,7 @@ export function validateEnvelope(env, { maxBytes = 1_048_576 } = {}) {
39
39
  }
40
40
  if (!env.signature || env.signature.alg !== 'Ed25519' || !env.signature.kid || !env.signature.value) return fail('sobre sin firma');
41
41
  const bytes = Buffer.byteLength(JSON.stringify(env));
42
- if (bytes > maxBytes) return { ok: false, code: 413, reason: `sobre de ${bytes} bytes supera el máximo ${maxBytes}` };
42
+ if (bytes > maxBytes) return { ok: false, code: 413, reason: `envelope of ${bytes} bytes exceeds the maximum ${maxBytes}` };
43
43
  return { ok: true };
44
44
  }
45
45
 
@@ -91,7 +91,7 @@ export function applyInboxPolicy(env, agentRecord, senderDomain) {
91
91
  case 'pow': {
92
92
  const bits = inbox.pow_bits ?? 16;
93
93
  const exempt = inbox.allowlist?.some((x) => x === env.from || x === fromDomain);
94
- if (!exempt && !checkPow(env.id, env.pow, bits)) return { ok: false, code: 402, reason: `se requiere proof-of-work de ${bits} bits`, pow_bits: bits };
94
+ if (!exempt && !checkPow(env.id, env.pow, bits)) return { ok: false, code: 402, reason: `proof-of-work of ${bits} bits is required`, pow_bits: bits };
95
95
  break;
96
96
  }
97
97
  case 'stamp': {
@@ -13,7 +13,7 @@ import { verifyObject } from '../nucleo/crypto.js';
13
13
 
14
14
  export function parseAddress(address) {
15
15
  const m = /^([a-z0-9][a-z0-9._-]{0,63})@([a-z0-9.-]+)$/i.exec(String(address || ''));
16
- if (!m) throw new Error(`dirección inválida: ${address}`);
16
+ if (!m) throw new Error(`invalid address: ${address}`);
17
17
  return { local: m[1].toLowerCase(), domain: m[2].toLowerCase() };
18
18
  }
19
19
 
@@ -72,7 +72,7 @@ export class Resolver {
72
72
  // Cualquier OTRA falla de DNS (red, timeout, servfail) NO degrada el ancla en silencio:
73
73
  // un atacante que bloquee la respuesta DNS no puede empujarnos a well-known + TOFU.
74
74
  if (e.code !== 'ENOTFOUND' && e.code !== 'ENODATA') {
75
- throw Object.assign(new Error(`DNS no disponible para ${domain}: ${e.message}`), { permanent: false });
75
+ throw Object.assign(new Error(`DNS unavailable for ${domain}: ${e.message}`), { permanent: false });
76
76
  }
77
77
  }
78
78
  return { source: 'well-known', url: `https://${domain}` };
@@ -90,14 +90,14 @@ export class Resolver {
90
90
 
91
91
  const loc = await this.locate(domain);
92
92
  const card = await this._get(`${loc.url.replace(/\/$/, '')}/.well-known/nyx5.json`);
93
- if (card.nyx5 !== '1' || card.domain !== domain) throw Object.assign(new Error(`tarjeta de dominio inválida para ${domain}`), { permanent: true });
93
+ if (card.nyx5 !== '1' || card.domain !== domain) throw Object.assign(new Error(`invalid domain card for ${domain}`), { permanent: true });
94
94
  const keyIds = (card.keys || []).map((k) => k.sig);
95
95
  if (!keyIds.includes(card.signature?.kid) || !verifyObject(card, card.signature.kid)) {
96
- throw Object.assign(new Error(`firma de dominio inválida para ${domain}`), { permanent: true });
96
+ throw Object.assign(new Error(`invalid domain signature for ${domain}`), { permanent: true });
97
97
  }
98
98
  // Ancla: DNS/override dice qué clave debe tener el dominio. Si no hay ancla, TOFU (pin en primer uso).
99
99
  const anchor = loc.sig || this.pins[domain];
100
- if (anchor && !keyIds.includes(anchor)) throw Object.assign(new Error(`la clave del dominio ${domain} no coincide con la anclada`), { permanent: true });
100
+ if (anchor && !keyIds.includes(anchor)) throw Object.assign(new Error(`the key for domain ${domain} does not match the anchored one`), { permanent: true });
101
101
  if (!anchor) {
102
102
  this.pins[domain] = card.signature.kid;
103
103
  // Se persiste ESE pin, no el mapa entero: escribir el mapa desde la memoria de un proceso
@@ -135,19 +135,19 @@ export class Resolver {
135
135
  // La verificación de una tarjeta de agente, en un solo lugar: certificación del dominio,
136
136
  // vigencia y cadena de delegación. La usan el camino local y el remoto por igual.
137
137
  async _verifyAgentCard(card, dc, address, local, domain) {
138
- if (card.nyx5 !== '1' || card.address !== `${local}@${domain}`) throw Object.assign(new Error(`tarjeta de agente inválida: ${address}`), { permanent: true });
138
+ if (card.nyx5 !== '1' || card.address !== `${local}@${domain}`) throw Object.assign(new Error(`invalid agent card: ${address}`), { permanent: true });
139
139
  const domainKeys = dc.keys.map((k) => k.sig);
140
140
  if (!domainKeys.includes(card.certification?.kid) || !verifyObject(card, card.certification.kid, 'certification')) {
141
- throw Object.assign(new Error(`certificación inválida para ${address}`), { permanent: true });
141
+ throw Object.assign(new Error(`invalid certification for ${address}`), { permanent: true });
142
142
  }
143
143
  if (card.valid_until && Date.parse(card.valid_until) < Date.now()) throw Object.assign(new Error(`tarjeta vencida: ${address}`), { permanent: true });
144
144
  if (card.delegation) {
145
145
  // Cadena de delegación: el padre (ya certificado por el dominio) firmó esta tarjeta.
146
146
  const d = card.delegation;
147
147
  const { local: parentLocal, domain: parentDomain } = parseAddress(d.by);
148
- if (parentDomain !== domain || !local.endsWith(`.${parentLocal}`) || d.address !== card.address || d.sig !== card.sig) throw Object.assign(new Error(`delegación inconsistente en ${address}`), { permanent: true });
148
+ if (parentDomain !== domain || !local.endsWith(`.${parentLocal}`) || d.address !== card.address || d.sig !== card.sig) throw Object.assign(new Error(`inconsistent delegation on ${address}`), { permanent: true });
149
149
  const parent = await this.agentCard(d.by);
150
- if (!Resolver.acceptedKids(parent).includes(d.signature?.kid) || !verifyObject(d, d.signature.kid)) throw Object.assign(new Error(`delegación no firmada por ${d.by}`), { permanent: true });
150
+ if (!Resolver.acceptedKids(parent).includes(d.signature?.kid) || !verifyObject(d, d.signature.kid)) throw Object.assign(new Error(`delegation not signed by ${d.by}`), { permanent: true });
151
151
  card = { ...card, delegation: { ...d, _parent: parent } };
152
152
  }
153
153
  return { ...card, _estafeta: dc._estafeta, _domain: dc };