@panal/sdk 0.6.1 → 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/dist/client.d.ts +26 -0
- package/dist/client.js +109 -1
- package/dist/files.d.ts +9 -1
- package/dist/files.js +12 -2
- package/dist/types.d.ts +30 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -29,6 +29,20 @@ export interface PanalClientOptions {
|
|
|
29
29
|
account?: Account;
|
|
30
30
|
/** Sobrescribe direcciones concretas (para pruebas o un despliegue propio). */
|
|
31
31
|
addresses?: Partial<PanalAddresses>;
|
|
32
|
+
/**
|
|
33
|
+
* Indexador desde el que buscar agentes. `https://api.panal.lat` por defecto.
|
|
34
|
+
*
|
|
35
|
+
* Buscar leyendo el registro entero deja de funcionar justo cuando más falta
|
|
36
|
+
* hace: `searchAgents` pagina hasta 500 agentes y luego lanza 500 lecturas a
|
|
37
|
+
* la vez, y el RPC público corta a partir de ~50 concurrentes. O sea que
|
|
38
|
+
* cuantos más agentes hay, MENOS puede un agente encontrar a otro.
|
|
39
|
+
*
|
|
40
|
+
* Con el indexador es una petición. Si no responde, se vuelve al registro:
|
|
41
|
+
* peor y con tope, pero nunca sin respuesta.
|
|
42
|
+
*
|
|
43
|
+
* `null` lo desactiva y lee siempre de la cadena.
|
|
44
|
+
*/
|
|
45
|
+
indexerUrl?: string | null;
|
|
32
46
|
}
|
|
33
47
|
export interface HireParams {
|
|
34
48
|
/** Dirección del agente que hará el trabajo. */
|
|
@@ -62,11 +76,21 @@ export declare class PanalClient {
|
|
|
62
76
|
* cliente se creó sin cuenta, o sea en modo solo lectura.
|
|
63
77
|
*/
|
|
64
78
|
readonly walletClient?: WalletClient;
|
|
79
|
+
/** Indexador para buscar agentes, o null si se lee siempre de la cadena. */
|
|
80
|
+
readonly indexerUrl: string | null;
|
|
65
81
|
constructor(options?: PanalClientOptions);
|
|
66
82
|
/** El wallet client, o un error que dice exactamente qué falta. */
|
|
67
83
|
private wallet;
|
|
68
84
|
/** Todos los agentes del registry, activos e inactivos. */
|
|
69
85
|
listAgents(): Promise<Agent[]>;
|
|
86
|
+
/**
|
|
87
|
+
* Los agentes que dice el indexador, o null si no se puede contar con él.
|
|
88
|
+
*
|
|
89
|
+
* Devuelve null —y no una lista vacía— cuando no responde, va atrasado o
|
|
90
|
+
* contesta algo raro: quien llama tiene que poder distinguir «no hay
|
|
91
|
+
* ninguno» de «no lo sé», porque en el segundo caso toca leer la cadena.
|
|
92
|
+
*/
|
|
93
|
+
private buscarEnIndice;
|
|
70
94
|
/** Un agente concreto, con su metadata ya interpretada. */
|
|
71
95
|
getAgent(address: Address): Promise<Agent>;
|
|
72
96
|
/**
|
|
@@ -78,6 +102,8 @@ export declare class PanalClient {
|
|
|
78
102
|
*/
|
|
79
103
|
searchAgents(query?: string, options?: {
|
|
80
104
|
includeInactive?: boolean;
|
|
105
|
+
skill?: string;
|
|
106
|
+
limit?: number;
|
|
81
107
|
}): Promise<Agent[]>;
|
|
82
108
|
/** Una tarea por su id. */
|
|
83
109
|
getTask(taskId: bigint): Promise<Task>;
|
package/dist/client.js
CHANGED
|
@@ -26,6 +26,22 @@ import { TaskStatus, formatAgentMetadata, parseAgentMetadata, } from './types.js
|
|
|
26
26
|
const REGISTRY_PAGE = 50n;
|
|
27
27
|
/** Tope duro de agentes recorridos, por si el registro crece mucho. */
|
|
28
28
|
const REGISTRY_MAX = 500;
|
|
29
|
+
/**
|
|
30
|
+
* ¿Esto que manda el indexador es un nombre de PanalNames?
|
|
31
|
+
*
|
|
32
|
+
* Se valida como todo lo que llega de un servicio: si viene a medias se
|
|
33
|
+
* descarta, porque un `origen` inventado haria que la web avisara de una venta
|
|
34
|
+
* que no existio, o peor, que callara una que si.
|
|
35
|
+
*/
|
|
36
|
+
function esNombre(v) {
|
|
37
|
+
if (v === null || typeof v !== 'object')
|
|
38
|
+
return false;
|
|
39
|
+
const n = v;
|
|
40
|
+
return (typeof n.nombre === 'string' &&
|
|
41
|
+
n.nombre.length > 0 &&
|
|
42
|
+
typeof n.desdeTs === 'number' &&
|
|
43
|
+
(n.origen === 'reclamado' || n.origen === 'comprado' || n.origen === 'recibido'));
|
|
44
|
+
}
|
|
29
45
|
export class PanalClient {
|
|
30
46
|
network;
|
|
31
47
|
addresses;
|
|
@@ -37,6 +53,8 @@ export class PanalClient {
|
|
|
37
53
|
* cliente se creó sin cuenta, o sea en modo solo lectura.
|
|
38
54
|
*/
|
|
39
55
|
walletClient;
|
|
56
|
+
/** Indexador para buscar agentes, o null si se lee siempre de la cadena. */
|
|
57
|
+
indexerUrl;
|
|
40
58
|
constructor(options = {}) {
|
|
41
59
|
this.network = options.network ?? 'mainnet';
|
|
42
60
|
const chain = chainFor(this.network);
|
|
@@ -45,6 +63,7 @@ export class PanalClient {
|
|
|
45
63
|
throw new Error(`Panal no tiene contratos desplegados en ${this.network}. ` +
|
|
46
64
|
'Usa network: "mainnet", o pasa `addresses` con los tuyos.');
|
|
47
65
|
}
|
|
66
|
+
this.indexerUrl = options.indexerUrl === undefined ? 'https://api.panal.lat' : options.indexerUrl;
|
|
48
67
|
const transport = http(options.rpcUrl ?? chain.rpcUrls.default.http[0]);
|
|
49
68
|
this.publicClient = createPublicClient({ chain, transport });
|
|
50
69
|
this.account = options.account;
|
|
@@ -84,6 +103,85 @@ export class PanalClient {
|
|
|
84
103
|
}
|
|
85
104
|
return Promise.all(addresses.map((address) => this.getAgent(address)));
|
|
86
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Los agentes que dice el indexador, o null si no se puede contar con él.
|
|
108
|
+
*
|
|
109
|
+
* Devuelve null —y no una lista vacía— cuando no responde, va atrasado o
|
|
110
|
+
* contesta algo raro: quien llama tiene que poder distinguir «no hay
|
|
111
|
+
* ninguno» de «no lo sé», porque en el segundo caso toca leer la cadena.
|
|
112
|
+
*/
|
|
113
|
+
async buscarEnIndice(query, options) {
|
|
114
|
+
if (!this.indexerUrl)
|
|
115
|
+
return null;
|
|
116
|
+
try {
|
|
117
|
+
const url = new URL('/index/agents', this.indexerUrl);
|
|
118
|
+
if (query?.trim())
|
|
119
|
+
url.searchParams.set('q', query.trim());
|
|
120
|
+
if (options.skill?.trim())
|
|
121
|
+
url.searchParams.set('skill', options.skill.trim());
|
|
122
|
+
if (options.includeInactive)
|
|
123
|
+
url.searchParams.set('include_inactive', 'true');
|
|
124
|
+
url.searchParams.set('limit', String(Math.min(options.limit ?? 50, 200)));
|
|
125
|
+
const res = await fetchLimited(url.toString(), { timeoutMs: 8000 });
|
|
126
|
+
if (res.status !== 200)
|
|
127
|
+
return null;
|
|
128
|
+
const cuerpo = JSON.parse(res.text);
|
|
129
|
+
if (!Array.isArray(cuerpo.agents))
|
|
130
|
+
return null;
|
|
131
|
+
// `total` solo lo devuelve la respuesta del CATÁLOGO. Sin esta
|
|
132
|
+
// comprobación, un indexador viejo —que no entiende `q` ni `skill` pero
|
|
133
|
+
// responde igual con su lista de siempre— hacía creer que había filtrado:
|
|
134
|
+
// toda búsqueda devolvía todos los agentes, incluida una imposible.
|
|
135
|
+
// Un servidor que no entiende la pregunta y contesta es peor que uno que
|
|
136
|
+
// calla, porque no hay forma de notarlo desde fuera. Aquí sí.
|
|
137
|
+
if (typeof cuerpo.total !== 'number')
|
|
138
|
+
return null;
|
|
139
|
+
const out = [];
|
|
140
|
+
for (const raw of cuerpo.agents) {
|
|
141
|
+
// El indexador es un servicio, o sea que su respuesta se valida como
|
|
142
|
+
// la de cualquier desconocido: una ficha rota se descarta sin llevarse
|
|
143
|
+
// la búsqueda entera por delante.
|
|
144
|
+
const address = typeof raw.address === 'string' ? raw.address : null;
|
|
145
|
+
if (!address || !/^0x[0-9a-fA-F]{40}$/.test(address))
|
|
146
|
+
continue;
|
|
147
|
+
const skills = Array.isArray(raw.skills) ? raw.skills.filter((x) => typeof x === 'string') : [];
|
|
148
|
+
let pricePerTask;
|
|
149
|
+
let registeredAt;
|
|
150
|
+
try {
|
|
151
|
+
pricePerTask = BigInt(String(raw.pricePerTask ?? '0'));
|
|
152
|
+
registeredAt = BigInt(Number(raw.registeredAt ?? 0));
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const metadata = {
|
|
158
|
+
name: typeof raw.name === 'string' ? raw.name : '',
|
|
159
|
+
description: typeof raw.description === 'string' ? raw.description : '',
|
|
160
|
+
skills,
|
|
161
|
+
botUrl: typeof raw.botUrl === 'string' ? raw.botUrl : null,
|
|
162
|
+
};
|
|
163
|
+
out.push({
|
|
164
|
+
address: getAddress(address),
|
|
165
|
+
owner: getAddress(typeof raw.owner === 'string' && /^0x[0-9a-fA-F]{40}$/.test(raw.owner) ? raw.owner : address),
|
|
166
|
+
pricePerTask,
|
|
167
|
+
currency: getAddress(typeof raw.currency === 'string' && /^0x[0-9a-fA-F]{40}$/.test(raw.currency) ? raw.currency : NATIVE_CURRENCY),
|
|
168
|
+
active: raw.active !== false,
|
|
169
|
+
registeredAt,
|
|
170
|
+
metadataURI: formatAgentMetadata(metadata),
|
|
171
|
+
metadata,
|
|
172
|
+
// Solo `true` cuenta como verificado. Un indexador viejo no manda el
|
|
173
|
+
// campo, y tratar «no lo sé» como «sí» es justo al revés de lo que
|
|
174
|
+
// hay que hacer con una insignia de confianza.
|
|
175
|
+
verificado: raw.verificado === true,
|
|
176
|
+
...(esNombre(raw.nombre) ? { nombre: raw.nombre } : {}),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
87
185
|
/** Un agente concreto, con su metadata ya interpretada. */
|
|
88
186
|
async getAgent(address) {
|
|
89
187
|
const raw = (await this.publicClient.readContract({
|
|
@@ -111,6 +209,13 @@ export class PanalClient {
|
|
|
111
209
|
* paginada sale más barata que montar un índice.
|
|
112
210
|
*/
|
|
113
211
|
async searchAgents(query, options = {}) {
|
|
212
|
+
// Por el indexador primero. Leer el registro entero para buscar deja de
|
|
213
|
+
// funcionar justo cuando más falta hace: son 500 lecturas a la vez contra
|
|
214
|
+
// un RPC que corta a partir de ~50 concurrentes, y con más de 500 agentes
|
|
215
|
+
// ni siquiera los ve. Aquí es una petición.
|
|
216
|
+
const delIndice = await this.buscarEnIndice(query, options);
|
|
217
|
+
if (delIndice !== null)
|
|
218
|
+
return delIndice;
|
|
114
219
|
const all = await this.listAgents();
|
|
115
220
|
const pool = options.includeInactive ? all : all.filter((a) => a.active);
|
|
116
221
|
if (!query?.trim())
|
|
@@ -413,7 +518,10 @@ export class PanalClient {
|
|
|
413
518
|
const tope = remainingBudget(options.envelope ?? null, options.maxSpend);
|
|
414
519
|
if (tope <= 0n)
|
|
415
520
|
throw new X402Error('El presupuesto de la cadena está agotado: no se puede delegar más.');
|
|
416
|
-
|
|
521
|
+
// Se busca por SKILL, no por texto libre: encontrar a alguien porque la
|
|
522
|
+
// palabra aparece en su descripción no sirve para delegar. Si el indexador
|
|
523
|
+
// no está, `searchAgents` cae solo a la cadena con su texto libre.
|
|
524
|
+
const candidates = (await this.searchAgents(skill, { skill }))
|
|
417
525
|
.filter((a) => !excluded.has(a.address.toLowerCase()) && a.metadata.botUrl)
|
|
418
526
|
.slice(0, options.maxCandidates ?? 5);
|
|
419
527
|
if (!candidates.length)
|
package/dist/files.d.ts
CHANGED
|
@@ -108,8 +108,16 @@ export interface DownloadOptions extends UrlGuardOptions {
|
|
|
108
108
|
baseUrl?: string;
|
|
109
109
|
/** Wallet del cliente; el agente solo entrega a quien pagó. */
|
|
110
110
|
address?: string;
|
|
111
|
-
/** Firma de `Panal resultado #<taskId>`, la misma que abre `/result/:id`. */
|
|
111
|
+
/** Firma de `Panal resultado #<taskId> · <expira>`, la misma que abre `/result/:id`. */
|
|
112
112
|
signature?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Segundo en el que caduca esa firma, tal y como se firmó.
|
|
115
|
+
*
|
|
116
|
+
* Va con la firma porque el agente la necesita para reconstruir el mensaje.
|
|
117
|
+
* Mandarla en claro no regala nada: está DENTRO de lo firmado, así que
|
|
118
|
+
* cambiarla invalida la firma.
|
|
119
|
+
*/
|
|
120
|
+
expira?: number;
|
|
113
121
|
maxBytes?: number;
|
|
114
122
|
timeoutMs?: number;
|
|
115
123
|
}
|
package/dist/files.js
CHANGED
|
@@ -204,10 +204,19 @@ export function fileUrl(file, baseUrl) {
|
|
|
204
204
|
*/
|
|
205
205
|
export async function downloadDeliveredFile(file, options = {}) {
|
|
206
206
|
const destino = new URL(fileUrl(file, options.baseUrl));
|
|
207
|
+
// Las credenciales van en CABECERAS, no en la query.
|
|
208
|
+
//
|
|
209
|
+
// Esta firma abre el resultado y todos los archivos de la tarea, o sea que es
|
|
210
|
+
// un pase de acceso. En la query acababa escrita en el log de accesos del
|
|
211
|
+
// proxy y en el historial del navegador — se encontraron 23 en claro en un
|
|
212
|
+
// log de producción. Una cabecera no se registra por defecto.
|
|
213
|
+
const cabeceras = {};
|
|
207
214
|
if (options.address)
|
|
208
|
-
|
|
215
|
+
cabeceras['x-panal-address'] = options.address;
|
|
209
216
|
if (options.signature)
|
|
210
|
-
|
|
217
|
+
cabeceras['x-panal-signature'] = options.signature;
|
|
218
|
+
if (options.expira !== undefined)
|
|
219
|
+
cabeceras['x-panal-expira'] = String(options.expira);
|
|
211
220
|
await assertPublicUrl(destino.toString(), options);
|
|
212
221
|
// El tope se ata al tamaño ANUNCIADO, no al de por defecto: si el manifiesto
|
|
213
222
|
// dice 2 MB, no hay razón para dejar que lleguen 25.
|
|
@@ -216,6 +225,7 @@ export async function downloadDeliveredFile(file, options = {}) {
|
|
|
216
225
|
maxBytes: tope,
|
|
217
226
|
timeoutMs: options.timeoutMs ?? 120_000,
|
|
218
227
|
redirect: 'error',
|
|
228
|
+
headers: cabeceras,
|
|
219
229
|
});
|
|
220
230
|
if (status !== 200) {
|
|
221
231
|
throw new FileVerificationError(`El agente respondió ${status} al pedirle "${file.name}".`, file.name);
|
package/dist/types.d.ts
CHANGED
|
@@ -56,6 +56,36 @@ export interface Agent {
|
|
|
56
56
|
metadata: AgentMetadata;
|
|
57
57
|
/** La cadena cruda, por si quieres interpretarla tú. */
|
|
58
58
|
metadataURI: string;
|
|
59
|
+
/**
|
|
60
|
+
* Si su dominio confirma que esta dirección es suya.
|
|
61
|
+
*
|
|
62
|
+
* El nombre lo escribe el propio agente y no es único: cualquiera puede
|
|
63
|
+
* registrarse como "Lint". Lo que sí es de alguien es su dominio, y el
|
|
64
|
+
* `agent.json` que sirve declara su dirección, así que el indexador va a
|
|
65
|
+
* buscarla y la compara.
|
|
66
|
+
*
|
|
67
|
+
* **Elegir un agente sin mirar esto es el fallo que más caro sale**: un
|
|
68
|
+
* suplantador con el nombre y la descripción del original cuesta una
|
|
69
|
+
* transacción. `undefined` = no se sabe (sin indexador, o aún sin mirar);
|
|
70
|
+
* trátalo como «no verificado», nunca como «verificado».
|
|
71
|
+
*/
|
|
72
|
+
verificado?: boolean;
|
|
73
|
+
/** Su nombre único en PanalNames, si lo tiene. */
|
|
74
|
+
nombre?: NombreDeAgente;
|
|
75
|
+
}
|
|
76
|
+
/** El nombre de un agente en PanalNames, y cómo llegó a tenerlo. */
|
|
77
|
+
export interface NombreDeAgente {
|
|
78
|
+
nombre: string;
|
|
79
|
+
/** Cuándo pasó a ser de esta dirección (segundos epoch). */
|
|
80
|
+
desdeTs: number;
|
|
81
|
+
/**
|
|
82
|
+
* Reclamado de cero, comprado a otro, o recibido.
|
|
83
|
+
*
|
|
84
|
+
* Importa tanto como el nombre: en una venta lo único que viaja es el
|
|
85
|
+
* nombre, y la reputación se queda con el vendedor. Un `lint` comprado la
|
|
86
|
+
* semana pasada no hizo las tareas que hicieron valer ese nombre.
|
|
87
|
+
*/
|
|
88
|
+
origen: 'reclamado' | 'comprado' | 'recibido';
|
|
59
89
|
}
|
|
60
90
|
/** Una tarea del escrow. */
|
|
61
91
|
export interface Task {
|