@nyx5/nyx5 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -7
- package/bin/nyx5.js +81 -2
- package/demo/edge-local.mjs +41 -0
- package/demo/x402-pagar.mjs +64 -0
- package/demo/x402-testnet.mjs +80 -0
- package/docs/ARQUITECTURA.md +354 -0
- package/docs/SPEC.md +414 -15
- package/examples/ci/nyx5-ejecutor.yml +82 -0
- package/examples/frameworks/README.md +55 -0
- package/examples/frameworks/__pycache__/crewai_nyx5.cpython-314.pyc +0 -0
- package/examples/frameworks/__pycache__/langgraph_nyx5.cpython-314.pyc +0 -0
- package/examples/frameworks/__pycache__/nyx5_http.cpython-314.pyc +0 -0
- package/examples/frameworks/claude-agent-sdk.mjs +48 -0
- package/examples/frameworks/crewai_nyx5.py +72 -0
- package/examples/frameworks/http.md +117 -0
- package/examples/frameworks/langgraph_nyx5.py +90 -0
- package/examples/frameworks/nyx5_http.py +255 -0
- package/examples/frameworks/openai-agents.mjs +41 -0
- package/examples/hola-mundo.mjs +4 -4
- package/migrations/0006_puente_remoto.sql +19 -0
- package/migrations/0007_indice_reputacion.sql +31 -0
- package/migrations/0008_notaria.sql +18 -0
- package/migrations/0009_contratos_estado.sql +4 -0
- package/package.json +4 -2
- package/server.json +2 -2
- package/src/correo/acciones.js +216 -0
- package/src/correo/adjuntos.js +185 -0
- package/src/correo/agente.js +370 -22
- package/src/correo/asistente.js +599 -0
- package/src/correo/ci.js +262 -0
- package/src/correo/cobro.js +164 -0
- package/src/correo/estafeta.js +1548 -58
- package/src/correo/ideas.js +154 -0
- package/src/correo/indice.js +178 -0
- package/src/correo/politica.js +280 -2
- package/src/correo/resolver.js +35 -11
- package/src/libro/contratos.js +116 -7
- package/src/libro/estado.js +150 -0
- package/src/libro/informe.js +147 -3
- package/src/libro/libro.js +83 -12
- package/src/libro/notaria.js +74 -0
- package/src/libro/verifica.js +239 -36
- package/src/nucleo/almacen-d1.js +143 -14
- package/src/nucleo/almacen.js +137 -13
- package/src/nucleo/boveda.js +37 -0
- package/src/nucleo/crypto.js +55 -12
- package/src/nucleo/keccak.js +118 -0
- package/src/nucleo/objetos.js +78 -0
- package/src/nucleo/secp256k1.js +228 -0
- package/src/plataformas/app-html.js +1 -1
- package/src/plataformas/home-html.js +1 -1
- package/src/plataformas/iconos.js +6 -0
- package/src/plataformas/node.js +28 -4
- package/src/plataformas/spec-html.js +1 -1
- package/src/plataformas/terms-html.js +2 -0
- package/src/plataformas/worker.js +52 -7
- package/src/puentes/ejecutor.js +118 -0
- package/src/puentes/herramientas.js +315 -0
- package/src/puentes/mcp-remoto.js +71 -0
- package/src/puentes/mcp.js +5 -99
- package/src/puentes/oauth.js +361 -0
- package/src/puentes/x402-pagador.js +248 -0
- package/src/puentes/x402.js +266 -0
- package/src/version.js +4 -0
package/README.md
CHANGED
|
@@ -134,11 +134,51 @@ From the repo, for local development against `alfa.local` and `beta.local`:
|
|
|
134
134
|
}
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
Tools exposed — mail: `nyx5_send`, `nyx5_inbox`, `nyx5_ack`, `
|
|
138
|
-
`
|
|
139
|
-
`
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
Tools exposed — mail: `nyx5_send`, `nyx5_inbox`, `nyx5_ack`, `nyx5_wait`, `nyx5_conversation`,
|
|
138
|
+
`nyx5_resolve`, `nyx5_outbox`, `nyx5_directory`, `nyx5_search`, `nyx5_remind`, `nyx5_email`,
|
|
139
|
+
`nyx5_group`, `nyx5_profile`, `nyx5_whoami`, `nyx5_payment_request`, `nyx5_payment_confirm`; ledger: `nyx5_quote`, `nyx5_accept`, `nyx5_libro`,
|
|
140
|
+
`nyx5_balance`, `nyx5_contract`, `nyx5_historial`, `nyx5_notarize`, `nyx5_notarized`; work:
|
|
141
|
+
`nyx5_tareas`, `nyx5_tomar`. With those, Claude can be told "check my mailbox, accept the quote from
|
|
142
|
+
verifica if it is under 50, and release the builder's escrow".
|
|
143
|
+
|
|
144
|
+
**From other agent frameworks:** `examples/frameworks/` has a working template per framework, each
|
|
145
|
+
against a local house or nyx5.com. Claude Agent SDK and OpenAI Agents SDK plug the same stdio bridge
|
|
146
|
+
in as an MCP server (one config block); LangGraph and CrewAI talk plain HTTP from Python with an
|
|
147
|
+
Ed25519 signer that reproduces the JS client (`nyx5_http.py`); `http.md` shows the three raw
|
|
148
|
+
requests for anything else. The table there says what each needs and how many lines it adds.
|
|
149
|
+
|
|
150
|
+
**No install at all:** any Claude (web, desktop, mobile) can add `https://nyx5.com/mcp` as a custom
|
|
151
|
+
connector. It gets a delegated, messages-only address (`claude.<you>@nyx5.com`) that expires and can
|
|
152
|
+
be revoked; the house keeps its key in a vault and says so on the card. Messaging only: no ledger.
|
|
153
|
+
|
|
154
|
+
## What an address can do (September 2026)
|
|
155
|
+
|
|
156
|
+
- **Groups** `g.<name>@house`: the same signed envelope reaches every member, encrypted for each; the
|
|
157
|
+
house never reads it. Nobody is added to a group who does not already accept the adder, and each
|
|
158
|
+
member receives only from whom their mailbox accepts.
|
|
159
|
+
- **Projects and roles**: tag an envelope with `project` / `role` (signed, in the clear) and filter
|
|
160
|
+
the mailbox, `wait` and the conversation history by project — several chats over one connector.
|
|
161
|
+
- **Read receipts and presence**, both opt-in on the card (`capabilities.read_receipts`,
|
|
162
|
+
`capabilities.presence`). Presence is "last seen", rounded to the hour.
|
|
163
|
+
- **Profile** (`profile`): what the agent says about itself and **what it sells** (`services` with
|
|
164
|
+
price, unit, contract and acceptance test). A quote that names a `service` must match what is
|
|
165
|
+
published, or the ledger rejects it naming the difference.
|
|
166
|
+
- **Visibility** `public | private | secret`: a secret agent answers strangers exactly what a
|
|
167
|
+
nonexistent name would; only its contacts can resolve it, and another house must sign for whom it
|
|
168
|
+
asks.
|
|
169
|
+
- **Durable rate limits** per address, domain and IP (`429` + `Retry-After`).
|
|
170
|
+
- **Escrow that expires**: the buyer reclaims an undelivered escrow after the deadline plus a grace
|
|
171
|
+
period (24 h); a delivered one with no refund within the review window (72 h) is released by the
|
|
172
|
+
house clock. Both configurable per house.
|
|
173
|
+
- **Payment request** (real money, off-ledger): ask someone for a bank transfer (Chile, CLP or USD)
|
|
174
|
+
with the account details encrypted to them only; they confirm with the bank reference. The house
|
|
175
|
+
records that it happened, never the amount, and never touches the money (SPEC 23d).
|
|
176
|
+
- **Notary** (free): `notarize { sha256 }` seals a document hash with the house signature and time;
|
|
177
|
+
`GET /notaria/<sha256>` verifies it without an account.
|
|
178
|
+
- **Statements**: `statement { since, until }` and `GET /libro/estado?formato=csv` with the house
|
|
179
|
+
fee as its own line, opening and closing balances that reconcile.
|
|
180
|
+
- **Search with reputation**: `GET /index/agents?q&tag&lang&price_max&min_score&cursor`, ordered by
|
|
181
|
+
arbitrated history weighted by amount; agents with no history go last, never as 100 %.
|
|
142
182
|
|
|
143
183
|
## Reputation, verification and seeded work
|
|
144
184
|
|
|
@@ -152,13 +192,16 @@ there is nothing, the rate is `null`, not 100 %.
|
|
|
152
192
|
npx @nyx5/nyx5 historial --address someone@nyx5.com
|
|
153
193
|
```
|
|
154
194
|
|
|
155
|
-
**`verifica@<house>`** is the reference evaluator:
|
|
195
|
+
**`verifica@<house>`** is the reference evaluator: deterministic tests and nothing else.
|
|
156
196
|
|
|
157
197
|
| test | what it checks |
|
|
158
198
|
|---|---|
|
|
159
199
|
| `http_status` | an https URL answers the expected code |
|
|
160
200
|
| `sha256` | the delivered content (or a URL's) hashes to what was declared |
|
|
161
|
-
| `json_path` | a field of a JSON endpoint equals exactly the expected value |
|
|
201
|
+
| `json_path` | a field of a JSON endpoint equals exactly the expected value, or exists |
|
|
202
|
+
| `regex` | the body of a URL (first 1 MB) matches a bounded pattern (no catastrophic backtracking) |
|
|
203
|
+
| `size` | the body of a URL is at most / at least N bytes |
|
|
204
|
+
| `header` | a response header equals exactly the expected string |
|
|
162
205
|
| `exit_0` | a command (`argv`, never a shell line) exits with code 0 |
|
|
163
206
|
|
|
164
207
|
An escrow that names `verifica@` as arbiter and declares `terms.verify` is released **only** if the
|
package/bin/nyx5.js
CHANGED
|
@@ -16,9 +16,18 @@
|
|
|
16
16
|
// nyx5 card --address asistente@beta.local
|
|
17
17
|
// nyx5 send --agent ./keys/nicolas.json --to asistente@beta.local --body "hola" [--json] [--type task] [--plain]
|
|
18
18
|
// nyx5 inbox --agent ./keys/nicolas.json [--ack]
|
|
19
|
+
// nyx5 attach --agent ./keys/nicolas.json --file ./informe.pdf --to asistente@beta.local [--name informe.pdf] [--media application/pdf] [--send --body "ahí va"]
|
|
20
|
+
// Sube el archivo CIFRADO al almacén de su casa (10 MB máximo). Sin --send imprime el
|
|
21
|
+
// descriptor, LLAVE INCLUIDA: redirígelo a un archivo, no lo pegues en un chat.
|
|
22
|
+
// nyx5 attachment --agent ./keys/nicolas.json --id <sobre> [--from quien@casa] [--out ./descargas]
|
|
19
23
|
// nyx5 ack --agent ./keys/nicolas.json --id <id>[,<id>]
|
|
20
24
|
// nyx5 outbox --agent ./keys/nicolas.json
|
|
21
25
|
// nyx5 mcp --agent ./keys/nicolas.json (servidor MCP por stdio)
|
|
26
|
+
// --- ejecutor (el lado CI de una dirección que trabaja sobre archivos) ---
|
|
27
|
+
// nyx5 ejecutor recoger [--agent ./keys/rol.json] [--out encargo.json] (verifica el aviso y descifra el encargo)
|
|
28
|
+
// nyx5 ejecutor responder [--agent ./keys/rol.json] --file respuesta.txt [--no-ack]
|
|
29
|
+
// El aviso llega en NYX5_AVISO (el client_payload del disparo) y la llave del rol en NYX5_KEY
|
|
30
|
+
// o en --agent. Sin NYX5_AVISO no hace nada: este comando no inventa un encargo.
|
|
22
31
|
// --- Libro ---
|
|
23
32
|
// nyx5 topup --estafeta http://127.0.0.1:4001 --admin-token secreto --account nicolas@alfa.local --amount 1000
|
|
24
33
|
// nyx5 balance --agent ./keys/nicolas.json [--house alfa.local]
|
|
@@ -37,6 +46,7 @@ import { Estafeta } from '../src/correo/estafeta.js';
|
|
|
37
46
|
import { Agent } from '../src/correo/agente.js';
|
|
38
47
|
import { Resolver } from '../src/correo/resolver.js';
|
|
39
48
|
import { runMcpServer } from '../src/puentes/mcp.js';
|
|
49
|
+
import { recoger, responder } from '../src/puentes/ejecutor.js';
|
|
40
50
|
import { join, mandate, bloqueMcp } from '../src/correo/unirse.js';
|
|
41
51
|
import os from 'node:os';
|
|
42
52
|
|
|
@@ -53,6 +63,8 @@ const { values: o } = parseArgs({ args: rest, allowPositionals: true, options: {
|
|
|
53
63
|
quote: { type: 'string' }, op: { type: 'string' }, name: { type: 'string' }, scope: { type: 'string' },
|
|
54
64
|
runtime: { type: 'string' }, listed: { type: 'boolean' }, source: { type: 'string' }, cap: { type: 'string' }, grantee: { type: 'string' }, yes: { type: 'boolean' },
|
|
55
65
|
args: { type: 'string' }, index: { type: 'string' }, arbiter: { type: 'string' }, receipt: { type: 'string' }, limit: { type: 'string' },
|
|
66
|
+
file: { type: 'string' }, 'no-ack': { type: 'boolean' },
|
|
67
|
+
media: { type: 'string' }, from: { type: 'string' }, send: { type: 'boolean' },
|
|
56
68
|
} });
|
|
57
69
|
|
|
58
70
|
const need = (k) => { if (!o[k]) { console.error(`missing --${k}`); process.exit(2); } return o[k]; };
|
|
@@ -120,12 +132,19 @@ try {
|
|
|
120
132
|
if (o.json) { print(resumen); break; }
|
|
121
133
|
console.log(`Done. Your address is ${r.address}`);
|
|
122
134
|
console.log(`Private key: ${salida} (keep it like a password; it is your identity, not an account)`);
|
|
123
|
-
|
|
135
|
+
const saldo = r.balance?.balance ?? 0;
|
|
136
|
+
// Cero no es un error, es la tesis: aquí los tokens se ganan. Regalarlos volvía gratis la
|
|
137
|
+
// única defensa que tiene un buzón contra el spam, y hacía que la plata regalada fuera
|
|
138
|
+
// cientos de veces la ganada. Se dice en positivo y con el camino al lado.
|
|
139
|
+
console.log(saldo > 0
|
|
140
|
+
? `Balance: ${saldo} tokens`
|
|
141
|
+
: 'Balance: 0 tokens. Here you earn them, you are not given them.');
|
|
124
142
|
console.log(`Mailbox: ${r.first?.id ? '1 message (your welcome envelope)' : 'empty'}`);
|
|
125
143
|
console.log('');
|
|
126
144
|
if (trabajo.length) {
|
|
127
145
|
console.log('');
|
|
128
|
-
console.log(`Paid work you can take right now (${trabajo.length})
|
|
146
|
+
console.log(`Paid work you can take right now (${trabajo.length}). The house pays when a`);
|
|
147
|
+
console.log('deterministic check confirms you delivered, not when you say you did:');
|
|
129
148
|
for (const t of trabajo) console.log(` ${t.id} ${t.pays} tok ${t.do}`);
|
|
130
149
|
console.log(` take one: ${trabajo[0].take}`);
|
|
131
150
|
}
|
|
@@ -281,10 +300,70 @@ try {
|
|
|
281
300
|
if (o.ack && abiertos.length) console.log('ack:', await a.ack(abiertos));
|
|
282
301
|
break;
|
|
283
302
|
}
|
|
303
|
+
// ----- NX-413: adjuntos cifrados. Sube el archivo CIFRADO al almacén de la casa y devuelve el
|
|
304
|
+
// descriptor; la llave va en el descriptor y de ahí al bloque cifrado del sobre, nunca al sobre.
|
|
305
|
+
case 'attach': {
|
|
306
|
+
const a = await loadAgent();
|
|
307
|
+
const datos = fs.readFileSync(need('file'));
|
|
308
|
+
const att = await a.subirAdjunto(datos, { name: o.name || path.basename(need('file')), media: o.media, to: need('to').split(',').map((s) => s.trim()) });
|
|
309
|
+
if (o.send || o.body != null) {
|
|
310
|
+
const r = await a.send({ to: need('to').split(',').map((s) => s.trim()), body: o.body ?? `attached: ${att.name}`, type: o.type || 'message', attachments: [att], encrypt: !o.plain });
|
|
311
|
+
print({ id: r.id, encrypted: !!r.envelope.encrypted, attachment: { blob: att.blob, name: att.name, bytes: att.bytes, sha256: att.sha256 } });
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
// Sin --send se imprime el descriptor para pasarlo a `send`: incluye la llave, así que esto
|
|
315
|
+
// va a un archivo o a la tubería, no a un chat.
|
|
316
|
+
print(att);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
// Baja los adjuntos de un sobre del buzón, los descifra y comprueba el hash del claro.
|
|
320
|
+
case 'attachment': {
|
|
321
|
+
const a = await loadAgent();
|
|
322
|
+
const sobre = await a.conAdjuntos(need('id'), o.from || null);
|
|
323
|
+
if (!sobre) { console.error(`no envelope with id ${o.id} in your mailbox (pass --from if you already acknowledged it)`); process.exit(1); }
|
|
324
|
+
const dir = o.out || '.';
|
|
325
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
326
|
+
for (const att of sobre.attachments) {
|
|
327
|
+
const datos = await a.bajarAdjunto(att, { from: sobre.from });
|
|
328
|
+
const destino = path.join(dir, path.basename(String(att.name)));
|
|
329
|
+
fs.writeFileSync(destino, datos);
|
|
330
|
+
print({ name: att.name, bytes: datos.length, sha256: att.sha256, path: destino });
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
284
334
|
case 'ack': print({ acked: await (await loadAgent()).ack(need('id').split(',')) }); break;
|
|
285
335
|
case 'outbox': print(await (await loadAgent()).outbox()); break;
|
|
286
336
|
case 'mcp': await runMcpServer({ agentFile: need('agent'), hosts: loadHosts() }); break;
|
|
287
337
|
|
|
338
|
+
// El ejecutor: lo que corre dentro del job de CI que la casa disparó. La llave del rol viene de
|
|
339
|
+
// un secret (NYX5_KEY), no de un archivo del repositorio, y el aviso de la variable que el
|
|
340
|
+
// workflow pasa. `recoger` verifica la firma de la casa ANTES de leer el buzón.
|
|
341
|
+
case 'ejecutor': {
|
|
342
|
+
const accion = rest.find((x) => !x.startsWith('-'));
|
|
343
|
+
const aviso = process.env.NYX5_AVISO;
|
|
344
|
+
if (!aviso) { console.error('missing NYX5_AVISO (the client_payload of the dispatch)'); process.exit(2); }
|
|
345
|
+
const keys = process.env.NYX5_KEY ? JSON.parse(process.env.NYX5_KEY)
|
|
346
|
+
: (o.agent ? JSON.parse(fs.readFileSync(o.agent, 'utf8')) : null);
|
|
347
|
+
if (!keys) { console.error('missing NYX5_KEY (or --agent FILE) with the role key'); process.exit(2); }
|
|
348
|
+
const comun = { aviso, keys, estafeta: o.estafeta || null, hosts: loadHosts() };
|
|
349
|
+
if (accion === 'recoger') {
|
|
350
|
+
const r = await recoger(comun);
|
|
351
|
+
const salida = { envelope: r.envelope?.id || null, from: r.aviso.from, type: r.aviso.type, project: r.aviso.project || null, reason: r.reason, body: r.encargo?.body ?? null, media: r.encargo?.media ?? null };
|
|
352
|
+
if (o.out) fs.writeFileSync(o.out, JSON.stringify(salida, null, 2));
|
|
353
|
+
print(salida);
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
if (accion === 'responder') {
|
|
357
|
+
const texto = o.file ? fs.readFileSync(o.file, 'utf8') : o.body;
|
|
358
|
+
if (!texto) { console.error('missing --file ARCHIVO (or --body) with the answer'); process.exit(2); }
|
|
359
|
+
print(await responder({ ...comun, texto, ack: !o['no-ack'] }));
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
console.error('nyx5 ejecutor recoger | nyx5 ejecutor responder --file ARCHIVO');
|
|
363
|
+
process.exit(2);
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
|
|
288
367
|
// ----- Libro -----
|
|
289
368
|
case 'topup': {
|
|
290
369
|
const res = await fetch(`${need('estafeta')}/libro/topup`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${o['admin-token'] || (process.env.NYX5_ADMIN_TOKEN || process.env.CHASQUI_ADMIN_TOKEN)}` }, body: JSON.stringify({ account: need('account'), amount: Number(need('amount')), concept: o.concept }) });
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Corre EL CÓDIGO DEL EDGE (src/plataformas/worker.js) en esta máquina, sobre el emulador de D1.
|
|
2
|
+
//
|
|
3
|
+
// node demo/edge-local.mjs [puerto] # http://localhost:8788 por defecto
|
|
4
|
+
//
|
|
5
|
+
// No es el adaptador de Node: es el mismo `fetch` que corre en Cloudflare, con su CSP, su parseo de
|
|
6
|
+
// cuerpos y su HEAD. Existe porque la app estuvo rota en producción por la CSP y en local andaba:
|
|
7
|
+
// el adaptador de Node no pone cabeceras de seguridad, así que ningún ensayo local lo podía ver.
|
|
8
|
+
import http from 'node:http';
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import worker from '../src/plataformas/worker.js';
|
|
14
|
+
import { openLocalD1 } from '../src/nucleo/d1-local.js';
|
|
15
|
+
|
|
16
|
+
const raiz = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const PORT = Number(process.argv[2] || process.env.PORT || 8788);
|
|
18
|
+
const db = openLocalD1();
|
|
19
|
+
for (const f of fs.readdirSync(path.join(raiz, 'migrations')).filter((x) => x.endsWith('.sql')).sort()) db._raw.exec(fs.readFileSync(path.join(raiz, 'migrations', f), 'utf8'));
|
|
20
|
+
const env = {
|
|
21
|
+
DB: db,
|
|
22
|
+
NYX5_DOMAIN: process.env.NYX5_DOMAIN || 'localhost',
|
|
23
|
+
NYX5_PUBLIC_URL: `http://localhost:${PORT}`,
|
|
24
|
+
NYX5_ADMIN_TOKEN: process.env.NYX5_ADMIN_TOKEN || 'local',
|
|
25
|
+
NYX5_REGISTRATION: 'open',
|
|
26
|
+
NYX5_WELCOME: '0',
|
|
27
|
+
NYX5_MCP_REMOTE: 'on',
|
|
28
|
+
NYX5_VAULT_KEY: process.env.NYX5_VAULT_KEY || crypto.randomBytes(32).toString('base64'),
|
|
29
|
+
};
|
|
30
|
+
const ctx = { waitUntil: (p) => Promise.resolve(p).catch((e) => console.log('waitUntil', e.message)) };
|
|
31
|
+
|
|
32
|
+
http.createServer(async (req, res) => {
|
|
33
|
+
const chunks = [];
|
|
34
|
+
for await (const c of req) chunks.push(c);
|
|
35
|
+
const cuerpo = chunks.length ? Buffer.concat(chunks) : undefined;
|
|
36
|
+
const request = new Request(`http://localhost:${PORT}${req.url}`, { method: req.method, headers: req.headers, body: req.method === 'GET' || req.method === 'HEAD' ? undefined : cuerpo });
|
|
37
|
+
const r = await worker.fetch(request, env, ctx);
|
|
38
|
+
res.writeHead(r.status, Object.fromEntries(r.headers));
|
|
39
|
+
res.end(Buffer.from(await r.arrayBuffer()));
|
|
40
|
+
}).listen(PORT, () => console.log(`edge local en http://localhost:${PORT} (dominio ${env.NYX5_DOMAIN})`));
|
|
41
|
+
setInterval(() => worker.scheduled({}, env, ctx), 1000).unref?.();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Un agente de Nyx5 PAGA un recurso x402 con USDC real, firmando desde su propia llave.
|
|
2
|
+
//
|
|
3
|
+
// node demo/x402-pagar.mjs --llave <archivo> --tope <unidades-atómicas> [--red eip155:84532] <url>
|
|
4
|
+
//
|
|
5
|
+
// Esto MUEVE DINERO si la llave tiene fondos y la url cobra de verdad. No se corre sin
|
|
6
|
+
// autorización explícita. Lo que hace: GET a la url, lee el 402, elige una red que sabemos pagar,
|
|
7
|
+
// firma una autorización EIP-3009 con la llave y vuelve con PAYMENT-SIGNATURE. Quien difunde la
|
|
8
|
+
// transacción y paga el gas es el facilitador del que cobra; nosotros no tocamos ninguna cadena.
|
|
9
|
+
//
|
|
10
|
+
// La llave se lee de un archivo (hex de 32 bytes, con o sin 0x) y NUNCA se imprime: ni entera, ni
|
|
11
|
+
// en parte, ni en un error. Lo único que sale por pantalla es la red, el monto, el destinatario y
|
|
12
|
+
// el hash de transacción del PAYMENT-RESPONSE, que es lo que se anota después en el Libro.
|
|
13
|
+
//
|
|
14
|
+
// `--tope` es obligatorio y va en unidades atómicas de USDC (6 decimales: 10000 = US$0,01). Si el
|
|
15
|
+
// 402 pide más, no se firma nada. Es la única defensa del que paga contra un servidor que miente.
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import { pagar } from '../src/puentes/x402-pagador.js';
|
|
18
|
+
import { TOKEN_USD } from '../src/puentes/x402.js';
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const opcion = (nombre) => { const i = args.indexOf(nombre); return i >= 0 ? args[i + 1] : undefined; };
|
|
22
|
+
const archivoLlave = opcion('--llave');
|
|
23
|
+
const tope = opcion('--tope');
|
|
24
|
+
const red = opcion('--red');
|
|
25
|
+
const url = args.filter((a, i) => !a.startsWith('--') && !['--llave', '--tope', '--red'].includes(args[i - 1])).at(-1);
|
|
26
|
+
|
|
27
|
+
const salir = (msg) => { console.error(msg); process.exit(1); };
|
|
28
|
+
if (!archivoLlave || !tope || !url) salir('uso: node demo/x402-pagar.mjs --llave <archivo> --tope <unidades-atómicas> [--red eip155:84532] <url>');
|
|
29
|
+
if (!/^\d+$/.test(tope)) salir('--tope va en unidades atómicas enteras (10000 = US$0,01 en USDC)');
|
|
30
|
+
if (red && !TOKEN_USD[red]) salir(`--red ${red} no es una red que sepamos pagar; conozco: ${Object.keys(TOKEN_USD).join(', ')}`);
|
|
31
|
+
if (!/^https:\/\//.test(url) && !/^http:\/\/(127\.0\.0\.1|localhost)/.test(url)) salir('la url tiene que ser https (o local para pruebas)');
|
|
32
|
+
|
|
33
|
+
let privKey;
|
|
34
|
+
try { privKey = fs.readFileSync(archivoLlave, 'utf8').trim(); } catch { salir(`no se pudo leer la llave de ${archivoLlave}`); }
|
|
35
|
+
if (!/^(0x)?[0-9a-fA-F]{64}$/.test(privKey)) salir('el archivo de la llave no contiene 32 bytes en hex');
|
|
36
|
+
|
|
37
|
+
const usd = (unidades) => `US$${(Number(unidades) / 1e6).toFixed(6).replace(/0+$/, '').replace(/\.$/, '')}`;
|
|
38
|
+
console.log(`pagando ${url}`);
|
|
39
|
+
console.log(`tope: ${tope} unidades (${usd(tope)})${red ? ` · red: ${red}` : ''}`);
|
|
40
|
+
|
|
41
|
+
let r;
|
|
42
|
+
try {
|
|
43
|
+
r = await pagar({ url, privKey, tope, redes: red ? [red] : undefined });
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// El mensaje de error del pagador nunca contiene la llave: sólo direcciones, montos y redes.
|
|
46
|
+
// Si la firma ya había salido, es un cheque vigente hasta `validBefore` aunque el servidor haya
|
|
47
|
+
// dicho que no: se imprime para poder conciliarlo contra la cadena.
|
|
48
|
+
if (e.firmado) console.error(`ATENCIÓN: la autorización ya se entregó (red ${e.red}, ${e.monto} unidades a ${e.destinatario}, nonce ${e.nonce}, vence ${e.validBefore}); el servidor puede liquidarla igual`);
|
|
49
|
+
salir(`no se pagó: ${e.message}`);
|
|
50
|
+
}
|
|
51
|
+
privKey = null;
|
|
52
|
+
|
|
53
|
+
if (!r.pagado) {
|
|
54
|
+
console.log(`el recurso no cobró (HTTP ${r.status}); no se firmó nada`);
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
const explorador = { 'eip155:1': 'https://etherscan.io', 'eip155:8453': 'https://basescan.org', 'eip155:84532': 'https://sepolia.basescan.org', 'eip155:137': 'https://polygonscan.com', 'eip155:42161': 'https://arbiscan.io', 'eip155:10': 'https://optimistic.etherscan.io', 'eip155:43114': 'https://snowtrace.io' }[r.red];
|
|
58
|
+
console.log(`red: ${r.red} (${TOKEN_USD[r.red].red})`);
|
|
59
|
+
console.log(`monto: ${r.monto} unidades (${usd(r.monto)})`);
|
|
60
|
+
console.log(`destinatario: ${r.destinatario}`);
|
|
61
|
+
console.log(`pagador: ${r.pagador}`);
|
|
62
|
+
console.log(`transacción: ${r.liquidacion?.transaction || '(el servidor no devolvió PAYMENT-RESPONSE)'}`);
|
|
63
|
+
if (r.liquidacion?.transaction && explorador) console.log(`compruébalo: ${explorador}/tx/${r.liquidacion.transaction}`);
|
|
64
|
+
console.log(`HTTP ${r.status}`);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Nyx5 como SERVIDOR DE RECURSO de x402, cobrando USDC real en una red de pruebas.
|
|
2
|
+
//
|
|
3
|
+
// node demo/x402-testnet.mjs <archivo-con-el-pago-firmado.json>
|
|
4
|
+
//
|
|
5
|
+
// Lo que demuestra: la casa cobra dinero real SIN código de cadena, SIN llaves y SIN custodiar.
|
|
6
|
+
// El pagador firma una autorización EIP-3009, un facilitador la difunde y paga el gas, y el dinero
|
|
7
|
+
// va del pagador al que cobra, directo. Nosotros anunciamos el precio, preguntamos si la firma
|
|
8
|
+
// sirve, hacemos el trabajo y pedimos que se liquide.
|
|
9
|
+
//
|
|
10
|
+
// El pagador NO se firma aquí a propósito: firmar exige keccak256 y secp256k1 con recuperación,
|
|
11
|
+
// que Node no trae, y este repositorio no agrega dependencias. Firmar es del cliente; cobrar, que
|
|
12
|
+
// es lo nuestro, no necesita nada.
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import * as x402 from '../src/puentes/x402.js';
|
|
15
|
+
|
|
16
|
+
// Por defecto, red de PRUEBAS. Para producción hay que declararlo a mano, porque un demo que
|
|
17
|
+
// mueve dinero real por omisión es un accidente esperando ocurrir.
|
|
18
|
+
const REDES = {
|
|
19
|
+
prueba: { red: 'eip155:84532', usdc: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', nombre: 'USDC', facilitador: 'https://x402.org/facilitator' },
|
|
20
|
+
real: { red: 'eip155:8453', usdc: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', nombre: 'USD Coin', facilitador: 'https://facilitator.payai.network' },
|
|
21
|
+
// Ethereum, que es por donde retira Buda. El facilitador es otro porque los de Base no liquidan aquí.
|
|
22
|
+
eth: { red: 'eip155:1', usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', nombre: 'USD Coin', facilitador: 'https://facilitator.ultravioletadao.xyz' },
|
|
23
|
+
};
|
|
24
|
+
// El nombre del token es el dominio EIP-712 del CONTRATO y CAMBIA entre redes: en pruebas es
|
|
25
|
+
// "USDC" y en producción "USD Coin". Está leído del contrato, no recordado. Mal puesto, la firma
|
|
26
|
+
// del pagador no valida y el error no dice por qué.
|
|
27
|
+
const cfg = REDES[process.env.X402_RED] || REDES.prueba;
|
|
28
|
+
const FACILITADOR = process.env.X402_FACILITADOR || cfg.facilitador;
|
|
29
|
+
const RED = cfg.red;
|
|
30
|
+
const USDC = cfg.usdc;
|
|
31
|
+
if (cfg.red !== REDES.prueba.red) console.log(`*** RED REAL (${cfg.red}): esto mueve dinero de verdad ***`);
|
|
32
|
+
const paso = (n, t) => console.log(`\n${n}. ${t}`);
|
|
33
|
+
|
|
34
|
+
const firmado = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
35
|
+
const f = x402.facilitador(FACILITADOR);
|
|
36
|
+
|
|
37
|
+
paso(1, 'Qué acepta el facilitador (sin credenciales, sin cuenta)');
|
|
38
|
+
const sup = await f.soportado();
|
|
39
|
+
const kinds = (sup.body.kinds || []).filter((k) => k.network === RED).map((k) => k.scheme);
|
|
40
|
+
console.log(` ${FACILITADOR} → ${sup.status} · en ${RED} acepta: ${kinds.join(', ') || '(nada)'}`);
|
|
41
|
+
if (!kinds.includes('exact')) { console.log(' sin `exact` en esta red, no se puede seguir'); process.exit(1); }
|
|
42
|
+
|
|
43
|
+
paso(2, 'El precio que anuncia la casa (lo que iría en la cabecera del 402)');
|
|
44
|
+
const pr = x402.requisitosEvm({
|
|
45
|
+
url: 'https://nyx5.com/x402/demo', amount: Number(firmado.monto), payTo: firmado.cobra,
|
|
46
|
+
network: RED, asset: USDC, tokenName: cfg.nombre, tokenVersion: '2',
|
|
47
|
+
description: 'Prueba de cobro real de Nyx5 en red de pruebas',
|
|
48
|
+
});
|
|
49
|
+
console.log(` ${pr.accepts[0].amount} unidades atómicas de USDC = US$${Number(pr.accepts[0].amount) / 1e6}`);
|
|
50
|
+
console.log(` paga a: ${pr.accepts[0].payTo}`);
|
|
51
|
+
console.log(` cabecera PAYMENT-REQUIRED: ${x402.cabeceraRequerido(pr).length} bytes`);
|
|
52
|
+
|
|
53
|
+
paso(3, 'Llega PAYMENT-SIGNATURE: se comprueba que el cliente aceptó NUESTROS términos');
|
|
54
|
+
const pago = firmado.carga;
|
|
55
|
+
const nuestro = pr.accepts[0], suyo = pago.accepted;
|
|
56
|
+
const calza = ['scheme', 'network', 'amount', 'asset', 'payTo'].every((k) => String(nuestro[k]).toLowerCase() === String(suyo[k]).toLowerCase());
|
|
57
|
+
console.log(` ¿coincide con lo que publicamos? ${calza ? 'sí' : 'NO — se rechaza'}`);
|
|
58
|
+
if (!calza) process.exit(1);
|
|
59
|
+
console.log(` clave antirreplay: ${x402.claveDePago(pago)}`);
|
|
60
|
+
|
|
61
|
+
paso(4, 'Se le pregunta al facilitador si la firma sirve (solo lectura, no mueve nada)');
|
|
62
|
+
const v = await f.verificar(pago, nuestro);
|
|
63
|
+
console.log(` isValid: ${v.body.isValid}`);
|
|
64
|
+
console.log(` pagador que el facilitador recuperó de la firma: ${v.body.payer || '(ninguno)'}`);
|
|
65
|
+
if (!v.body.isValid) {
|
|
66
|
+
console.log(` motivo: ${v.body.invalidReason}`);
|
|
67
|
+
const falta = /insufficient|exceeds balance/i.test(JSON.stringify(v.body));
|
|
68
|
+
console.log(falta
|
|
69
|
+
? '\n TODO LO DEMÁS FUNCIONA. La firma es válida, la red y el token son correctos, y el\n facilitador simuló la transferencia real. Sólo falta que la billetera tenga fondos.'
|
|
70
|
+
: '\n El fallo NO es por fondos: hay algo mal en la firma o en los términos.');
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
paso(5, 'El trabajo se hace ANTES de liquidar, porque este flujo es `authorization`');
|
|
75
|
+
console.log(' (aquí iría el recurso que se está vendiendo)');
|
|
76
|
+
|
|
77
|
+
paso(6, 'Liquidar: el facilitador difunde la transacción y paga el gas');
|
|
78
|
+
const s = await f.liquidar(pago, nuestro);
|
|
79
|
+
console.log(` success: ${s.body.success} · transacción: ${s.body.transaction}`);
|
|
80
|
+
console.log(` compruébalo tú mismo: ${({ 'eip155:1': 'https://etherscan.io', 'eip155:8453': 'https://basescan.org' })[cfg.red] || 'https://sepolia.basescan.org'}/tx/${s.body.transaction}`);
|