@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/README.md +2 -4
- package/agents.md +1 -1
- package/bin/nyx5.js +34 -33
- package/docs/SPEC.md +311 -308
- package/package.json +1 -3
- package/server.json +2 -2
- package/src/correo/agente.js +5 -5
- package/src/correo/estafeta.js +67 -72
- package/src/correo/politica.js +3 -3
- package/src/correo/resolver.js +9 -9
- package/src/correo/unirse.js +6 -6
- package/src/libro/contratos.js +25 -25
- package/src/libro/libro.js +3 -3
- package/src/libro/tareas.js +9 -9
- package/src/nucleo/almacen.js +1 -1
- package/src/nucleo/crypto.js +1 -1
- package/src/nucleo/d1-local.js +1 -1
- package/src/plataformas/home-html.js +1 -2
- package/src/plataformas/node.js +1 -1
- package/src/plataformas/spec-html.js +3 -4
- package/src/plataformas/worker.js +1 -1
- package/src/puentes/email.js +13 -7
- package/src/puentes/mcp.js +27 -18
- package/README.es.md +0 -201
- package/docs/SPEC.en.md +0 -489
package/src/correo/unirse.js
CHANGED
|
@@ -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(`
|
|
62
|
-
if (Estafeta.RESERVED.has(local)) throw new Error(`
|
|
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('
|
|
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: `
|
|
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('
|
|
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('
|
|
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 });
|
package/src/libro/contratos.js
CHANGED
|
@@ -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
|
|
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, '
|
|
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,
|
|
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, '
|
|
75
|
-
must(c.state === 'held', 409, `
|
|
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
|
|
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, '
|
|
119
|
-
parseAddress(body.verifier); must(body.verifier !== from, 400, '
|
|
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
|
|
122
|
-
if (body.arbiter) { try { parseAddress(body.arbiter); } catch { fail(400, 'arbiter
|
|
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
|
|
126
|
-
scopeCap(ctx, body.amount, '
|
|
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, '
|
|
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, '
|
|
160
|
-
must(parent.grantee === from, 403, '
|
|
161
|
-
must(body.cap <= parent.cap - parent.spent, 403, `
|
|
162
|
-
if (parent.expires) must(!body.expires || Date.parse(body.expires) <= Date.parse(parent.expires), 403, '
|
|
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, '
|
|
176
|
-
must(Number.isInteger(body.amount) && body.amount > 0, 400, '
|
|
177
|
-
scopeCap(ctx, body.amount, '
|
|
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, `
|
|
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, `
|
|
183
|
-
if (cur.scope?.concepts?.length) must(cur.scope.concepts.includes(body.concept), 403, `
|
|
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, '
|
|
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, '
|
|
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
|
};
|
package/src/libro/libro.js
CHANGED
|
@@ -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, `
|
|
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: `
|
|
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: `
|
|
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 });
|
package/src/libro/tareas.js
CHANGED
|
@@ -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: `
|
|
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: `
|
|
68
|
-
if (this.porDia > 0 && delDia.length >= this.porDia) return { ok: false, reason: `
|
|
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: `
|
|
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: '
|
|
81
|
-
if (q.price !== tarea.price) return { ok: false, reason: `
|
|
82
|
-
if (q.arbiter !== arbitro) return { ok: false, reason: `
|
|
83
|
-
if (JSON.stringify(q.terms?.verify) !== JSON.stringify(tarea.verify)) return { ok: false, reason: `
|
|
84
|
-
if (q.terms?.seed_task !== tarea.id) return { ok: false, reason: '
|
|
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
|
|
package/src/nucleo/almacen.js
CHANGED
|
@@ -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(`
|
|
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
|
}
|
package/src/nucleo/crypto.js
CHANGED
|
@@ -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
|
|
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),
|
package/src/nucleo/d1-local.js
CHANGED
|
@@ -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
|
|
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 —
|
|
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";
|
package/src/plataformas/node.js
CHANGED
|
@@ -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
|
|
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
|
}
|