@panal/sdk 0.5.0 → 0.6.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/files.d.ts +122 -0
- package/dist/files.js +225 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/net.d.ts +14 -2
- package/dist/net.js +13 -5
- package/package.json +2 -2
package/dist/files.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panal SDK — entregar archivos sin renunciar a la garantía de la cadena.
|
|
3
|
+
*
|
|
4
|
+
* El escrow ancla `keccak256(texto de la entrega)` y nada más. Eso funciona
|
|
5
|
+
* para un informe escrito, pero no para un PDF, un audio o un vídeo: no caben
|
|
6
|
+
* en un string y meterlos en la cadena costaría más que el trabajo.
|
|
7
|
+
*
|
|
8
|
+
* La salida fácil sería entregar un enlace. Es una trampa: el hash anclado
|
|
9
|
+
* cubriría el enlace, no el archivo. Quien lo aloja puede cambiar el PDF
|
|
10
|
+
* después de cobrar y el cliente no tiene con qué demostrar nada. Un enlace
|
|
11
|
+
* pelado convierte el escrow en una promesa.
|
|
12
|
+
*
|
|
13
|
+
* Lo que se hace aquí es anclar el HASH DEL ARCHIVO dentro del texto:
|
|
14
|
+
*
|
|
15
|
+
* Aquí tienes el informe que pediste.
|
|
16
|
+
*
|
|
17
|
+
* [panal-files/1]
|
|
18
|
+
* name: informe.pdf
|
|
19
|
+
* size: 184320
|
|
20
|
+
* mime: application/pdf
|
|
21
|
+
* hash: 0x8f3a…
|
|
22
|
+
* path: /files/31/informe.pdf
|
|
23
|
+
*
|
|
24
|
+
* Ese texto entero es lo que se ancla. La cadena de custodia queda cerrada:
|
|
25
|
+
*
|
|
26
|
+
* resultHash on-chain → texto de la entrega → hash del archivo → bytes
|
|
27
|
+
*
|
|
28
|
+
* Cada eslabón se comprueba sin fiarse de nadie. El servidor que sirve el
|
|
29
|
+
* archivo deja de ser de confianza: puede ser el agente, un S3 o un CDN, y si
|
|
30
|
+
* los bytes no dan el hash pactado, la descarga falla.
|
|
31
|
+
*
|
|
32
|
+
* `path` se resuelve contra el `botUrl` que el agente tiene REGISTRADO
|
|
33
|
+
* on-chain, no contra algo que venga en el texto. Así un agente no puede
|
|
34
|
+
* mandar a su cliente a un tercero. `url` absoluta existe para quien aloja
|
|
35
|
+
* fuera, y es igual de segura porque la garantía la da el hash, no el sitio.
|
|
36
|
+
*/
|
|
37
|
+
import type { Hex } from 'viem';
|
|
38
|
+
import { type UrlGuardOptions } from './net.js';
|
|
39
|
+
/** Cabecera del bloque. Lleva versión porque el formato se ancla en la cadena. */
|
|
40
|
+
export declare const FILES_BLOCK = "[panal-files/1]";
|
|
41
|
+
/** Tope por defecto de una descarga: 25 MB. */
|
|
42
|
+
export declare const MAX_FILE_BYTES: number;
|
|
43
|
+
/** Un archivo anunciado en la entrega. */
|
|
44
|
+
export interface DeliveredFile {
|
|
45
|
+
/** Nombre del archivo, sin rutas. */
|
|
46
|
+
name: string;
|
|
47
|
+
/** Tamaño en bytes. Se comprueba junto al hash. */
|
|
48
|
+
size: number;
|
|
49
|
+
/** Tipo MIME, si el agente lo declaró. */
|
|
50
|
+
mime?: string;
|
|
51
|
+
/** keccak256 de los bytes. Es la garantía; todo lo demás es logística. */
|
|
52
|
+
hash: Hex;
|
|
53
|
+
/** Ruta relativa, a resolver contra el botUrl registrado del agente. */
|
|
54
|
+
path?: string;
|
|
55
|
+
/** URL absoluta, para quien aloja fuera de su propio servidor. */
|
|
56
|
+
url?: string;
|
|
57
|
+
}
|
|
58
|
+
export declare class FileVerificationError extends Error {
|
|
59
|
+
readonly file: string;
|
|
60
|
+
constructor(message: string, file: string);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Limpia un nombre de archivo para que no pueda salirse de su carpeta.
|
|
64
|
+
*
|
|
65
|
+
* El nombre viaja en el texto de la entrega y acaba usado como ruta en disco y
|
|
66
|
+
* en una URL. Sin esto, un `../../.env` como nombre convierte una entrega en
|
|
67
|
+
* una lectura arbitraria del servidor del agente.
|
|
68
|
+
*/
|
|
69
|
+
export declare function sanitizeFileName(name: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Construye el bloque del manifiesto.
|
|
72
|
+
*
|
|
73
|
+
* El orden de las claves es fijo y las líneas van con `\n`: este texto se
|
|
74
|
+
* anclará en la cadena, así que dos ejecuciones con los mismos archivos tienen
|
|
75
|
+
* que dar exactamente los mismos bytes.
|
|
76
|
+
*/
|
|
77
|
+
export declare function buildFilesManifest(files: DeliveredFile[]): string;
|
|
78
|
+
/** El texto de la entrega con el manifiesto pegado al final. */
|
|
79
|
+
export declare function appendFilesManifest(text: string, files: DeliveredFile[]): string;
|
|
80
|
+
/**
|
|
81
|
+
* Lee los archivos anunciados en el texto de una entrega.
|
|
82
|
+
*
|
|
83
|
+
* Nunca lanza por un bloque mal formado: devuelve los que sí se entienden. Un
|
|
84
|
+
* manifiesto roto no puede impedirle al cliente leer la parte escrita de su
|
|
85
|
+
* entrega, que ya pagó.
|
|
86
|
+
*/
|
|
87
|
+
export declare function parseFilesManifest(text: string): DeliveredFile[];
|
|
88
|
+
/**
|
|
89
|
+
* El texto de la entrega sin los bloques del manifiesto.
|
|
90
|
+
*
|
|
91
|
+
* Para enseñárselo a una persona: el manifiesto es para la máquina, y en un
|
|
92
|
+
* chat de Telegram no aporta más que ruido.
|
|
93
|
+
*/
|
|
94
|
+
export declare function stripFilesManifest(text: string): string;
|
|
95
|
+
/** Comprueba unos bytes contra lo que el manifiesto prometía. Lanza si no cuadra. */
|
|
96
|
+
export declare function verifyFileBytes(file: DeliveredFile, bytes: Uint8Array): void;
|
|
97
|
+
/**
|
|
98
|
+
* De dónde se baja un archivo.
|
|
99
|
+
*
|
|
100
|
+
* `path` se resuelve contra el `botUrl` que el agente publica EN EL REGISTRY,
|
|
101
|
+
* no contra nada que venga en el texto: si el agente pudiera elegir el host, un
|
|
102
|
+
* agente comprometido mandaría a su cliente donde quisiera. La URL absoluta se
|
|
103
|
+
* permite porque el hash la vigila igual, pero pasa por el guardia de SSRF.
|
|
104
|
+
*/
|
|
105
|
+
export declare function fileUrl(file: DeliveredFile, baseUrl: string | undefined): string;
|
|
106
|
+
export interface DownloadOptions extends UrlGuardOptions {
|
|
107
|
+
/** Base para las rutas relativas: el botUrl registrado del agente. */
|
|
108
|
+
baseUrl?: string;
|
|
109
|
+
/** Wallet del cliente; el agente solo entrega a quien pagó. */
|
|
110
|
+
address?: string;
|
|
111
|
+
/** Firma de `Panal resultado #<taskId>`, la misma que abre `/result/:id`. */
|
|
112
|
+
signature?: string;
|
|
113
|
+
maxBytes?: number;
|
|
114
|
+
timeoutMs?: number;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Baja un archivo de una entrega y lo verifica contra el hash anclado.
|
|
118
|
+
*
|
|
119
|
+
* Si los bytes no dan el hash, lanza en vez de devolverlos. Devolver un archivo
|
|
120
|
+
* que no cuadra "avisando" no serviría de nada: quien llama lo guardaría igual.
|
|
121
|
+
*/
|
|
122
|
+
export declare function downloadDeliveredFile(file: DeliveredFile, options?: DownloadOptions): Promise<Uint8Array>;
|
package/dist/files.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panal SDK — entregar archivos sin renunciar a la garantía de la cadena.
|
|
3
|
+
*
|
|
4
|
+
* El escrow ancla `keccak256(texto de la entrega)` y nada más. Eso funciona
|
|
5
|
+
* para un informe escrito, pero no para un PDF, un audio o un vídeo: no caben
|
|
6
|
+
* en un string y meterlos en la cadena costaría más que el trabajo.
|
|
7
|
+
*
|
|
8
|
+
* La salida fácil sería entregar un enlace. Es una trampa: el hash anclado
|
|
9
|
+
* cubriría el enlace, no el archivo. Quien lo aloja puede cambiar el PDF
|
|
10
|
+
* después de cobrar y el cliente no tiene con qué demostrar nada. Un enlace
|
|
11
|
+
* pelado convierte el escrow en una promesa.
|
|
12
|
+
*
|
|
13
|
+
* Lo que se hace aquí es anclar el HASH DEL ARCHIVO dentro del texto:
|
|
14
|
+
*
|
|
15
|
+
* Aquí tienes el informe que pediste.
|
|
16
|
+
*
|
|
17
|
+
* [panal-files/1]
|
|
18
|
+
* name: informe.pdf
|
|
19
|
+
* size: 184320
|
|
20
|
+
* mime: application/pdf
|
|
21
|
+
* hash: 0x8f3a…
|
|
22
|
+
* path: /files/31/informe.pdf
|
|
23
|
+
*
|
|
24
|
+
* Ese texto entero es lo que se ancla. La cadena de custodia queda cerrada:
|
|
25
|
+
*
|
|
26
|
+
* resultHash on-chain → texto de la entrega → hash del archivo → bytes
|
|
27
|
+
*
|
|
28
|
+
* Cada eslabón se comprueba sin fiarse de nadie. El servidor que sirve el
|
|
29
|
+
* archivo deja de ser de confianza: puede ser el agente, un S3 o un CDN, y si
|
|
30
|
+
* los bytes no dan el hash pactado, la descarga falla.
|
|
31
|
+
*
|
|
32
|
+
* `path` se resuelve contra el `botUrl` que el agente tiene REGISTRADO
|
|
33
|
+
* on-chain, no contra algo que venga en el texto. Así un agente no puede
|
|
34
|
+
* mandar a su cliente a un tercero. `url` absoluta existe para quien aloja
|
|
35
|
+
* fuera, y es igual de segura porque la garantía la da el hash, no el sitio.
|
|
36
|
+
*/
|
|
37
|
+
import { keccak256 } from 'viem';
|
|
38
|
+
import { assertPublicUrl, fetchBytesLimited } from './net.js';
|
|
39
|
+
/** Cabecera del bloque. Lleva versión porque el formato se ancla en la cadena. */
|
|
40
|
+
export const FILES_BLOCK = '[panal-files/1]';
|
|
41
|
+
/** Tope por defecto de una descarga: 25 MB. */
|
|
42
|
+
export const MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
43
|
+
export class FileVerificationError extends Error {
|
|
44
|
+
file;
|
|
45
|
+
constructor(message, file) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.file = file;
|
|
48
|
+
this.name = 'FileVerificationError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Limpia un nombre de archivo para que no pueda salirse de su carpeta.
|
|
53
|
+
*
|
|
54
|
+
* El nombre viaja en el texto de la entrega y acaba usado como ruta en disco y
|
|
55
|
+
* en una URL. Sin esto, un `../../.env` como nombre convierte una entrega en
|
|
56
|
+
* una lectura arbitraria del servidor del agente.
|
|
57
|
+
*/
|
|
58
|
+
export function sanitizeFileName(name) {
|
|
59
|
+
// Se corta por cualquier separador y se coge el último tramo: así `a/b/c.pdf`
|
|
60
|
+
// y `..\\..\\c.pdf` acaban los dos en `c.pdf`.
|
|
61
|
+
const base = name.split(/[/\\]/).pop() ?? '';
|
|
62
|
+
const limpio = base
|
|
63
|
+
.replace(/[\u0000-\u001f\u007f]/g, '') // caracteres de control
|
|
64
|
+
.replace(/^\.+/, '') // nada de nombres que empiezan por punto: '..' incluido
|
|
65
|
+
.trim();
|
|
66
|
+
if (!limpio)
|
|
67
|
+
throw new Error(`Nombre de archivo inservible: "${name}"`);
|
|
68
|
+
return limpio.slice(0, 120);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Construye el bloque del manifiesto.
|
|
72
|
+
*
|
|
73
|
+
* El orden de las claves es fijo y las líneas van con `\n`: este texto se
|
|
74
|
+
* anclará en la cadena, así que dos ejecuciones con los mismos archivos tienen
|
|
75
|
+
* que dar exactamente los mismos bytes.
|
|
76
|
+
*/
|
|
77
|
+
export function buildFilesManifest(files) {
|
|
78
|
+
return files
|
|
79
|
+
.map((f) => {
|
|
80
|
+
const lineas = [FILES_BLOCK, `name: ${sanitizeFileName(f.name)}`, `size: ${f.size}`];
|
|
81
|
+
if (f.mime)
|
|
82
|
+
lineas.push(`mime: ${f.mime}`);
|
|
83
|
+
lineas.push(`hash: ${f.hash}`);
|
|
84
|
+
if (f.path)
|
|
85
|
+
lineas.push(`path: ${f.path}`);
|
|
86
|
+
if (f.url)
|
|
87
|
+
lineas.push(`url: ${f.url}`);
|
|
88
|
+
return lineas.join('\n');
|
|
89
|
+
})
|
|
90
|
+
.join('\n\n');
|
|
91
|
+
}
|
|
92
|
+
/** El texto de la entrega con el manifiesto pegado al final. */
|
|
93
|
+
export function appendFilesManifest(text, files) {
|
|
94
|
+
if (files.length === 0)
|
|
95
|
+
return text;
|
|
96
|
+
const cuerpo = text.trimEnd();
|
|
97
|
+
return `${cuerpo}\n\n${buildFilesManifest(files)}\n`;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Lee los archivos anunciados en el texto de una entrega.
|
|
101
|
+
*
|
|
102
|
+
* Nunca lanza por un bloque mal formado: devuelve los que sí se entienden. Un
|
|
103
|
+
* manifiesto roto no puede impedirle al cliente leer la parte escrita de su
|
|
104
|
+
* entrega, que ya pagó.
|
|
105
|
+
*/
|
|
106
|
+
export function parseFilesManifest(text) {
|
|
107
|
+
const out = [];
|
|
108
|
+
const lineas = text.split(/\r?\n/);
|
|
109
|
+
for (let i = 0; i < lineas.length; i++) {
|
|
110
|
+
if (lineas[i].trim() !== FILES_BLOCK)
|
|
111
|
+
continue;
|
|
112
|
+
const campos = {};
|
|
113
|
+
for (let j = i + 1; j < lineas.length; j++) {
|
|
114
|
+
const linea = lineas[j];
|
|
115
|
+
if (!linea.trim() || linea.trim() === FILES_BLOCK)
|
|
116
|
+
break;
|
|
117
|
+
const sep = linea.indexOf(':');
|
|
118
|
+
if (sep === -1)
|
|
119
|
+
break;
|
|
120
|
+
campos[linea.slice(0, sep).trim().toLowerCase()] = linea.slice(sep + 1).trim();
|
|
121
|
+
}
|
|
122
|
+
const { name, size, hash, mime, path, url } = campos;
|
|
123
|
+
if (!name || !hash || !/^0x[0-9a-fA-F]{64}$/.test(hash))
|
|
124
|
+
continue;
|
|
125
|
+
const bytes = Number(size);
|
|
126
|
+
if (!Number.isInteger(bytes) || bytes < 0)
|
|
127
|
+
continue;
|
|
128
|
+
if (!path && !url)
|
|
129
|
+
continue;
|
|
130
|
+
try {
|
|
131
|
+
out.push({
|
|
132
|
+
name: sanitizeFileName(name),
|
|
133
|
+
size: bytes,
|
|
134
|
+
hash: hash.toLowerCase(),
|
|
135
|
+
...(mime ? { mime } : {}),
|
|
136
|
+
...(path ? { path } : {}),
|
|
137
|
+
...(url ? { url } : {}),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Nombre inservible: se descarta ese archivo, no la entrega entera.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* El texto de la entrega sin los bloques del manifiesto.
|
|
148
|
+
*
|
|
149
|
+
* Para enseñárselo a una persona: el manifiesto es para la máquina, y en un
|
|
150
|
+
* chat de Telegram no aporta más que ruido.
|
|
151
|
+
*/
|
|
152
|
+
export function stripFilesManifest(text) {
|
|
153
|
+
const lineas = text.split(/\r?\n/);
|
|
154
|
+
const fuera = [];
|
|
155
|
+
let dentro = false;
|
|
156
|
+
for (const linea of lineas) {
|
|
157
|
+
if (linea.trim() === FILES_BLOCK) {
|
|
158
|
+
dentro = true;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (dentro) {
|
|
162
|
+
// El bloque termina en la primera línea que ya no es `clave: valor`.
|
|
163
|
+
if (linea.trim() && linea.includes(':'))
|
|
164
|
+
continue;
|
|
165
|
+
dentro = false;
|
|
166
|
+
}
|
|
167
|
+
fuera.push(linea);
|
|
168
|
+
}
|
|
169
|
+
return fuera.join('\n').trimEnd();
|
|
170
|
+
}
|
|
171
|
+
/** Comprueba unos bytes contra lo que el manifiesto prometía. Lanza si no cuadra. */
|
|
172
|
+
export function verifyFileBytes(file, bytes) {
|
|
173
|
+
if (bytes.byteLength !== file.size) {
|
|
174
|
+
throw new FileVerificationError(`"${file.name}" mide ${bytes.byteLength} bytes y la entrega anunciaba ${file.size}.`, file.name);
|
|
175
|
+
}
|
|
176
|
+
const real = keccak256(bytes);
|
|
177
|
+
if (real.toLowerCase() !== file.hash.toLowerCase()) {
|
|
178
|
+
throw new FileVerificationError(`"${file.name}" no es el archivo que se entregó: su hash es ${real}, y el anclado en la cadena es ${file.hash}.`, file.name);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* De dónde se baja un archivo.
|
|
183
|
+
*
|
|
184
|
+
* `path` se resuelve contra el `botUrl` que el agente publica EN EL REGISTRY,
|
|
185
|
+
* no contra nada que venga en el texto: si el agente pudiera elegir el host, un
|
|
186
|
+
* agente comprometido mandaría a su cliente donde quisiera. La URL absoluta se
|
|
187
|
+
* permite porque el hash la vigila igual, pero pasa por el guardia de SSRF.
|
|
188
|
+
*/
|
|
189
|
+
export function fileUrl(file, baseUrl) {
|
|
190
|
+
if (file.url)
|
|
191
|
+
return file.url;
|
|
192
|
+
if (!file.path)
|
|
193
|
+
throw new Error(`La entrega anuncia "${file.name}" sin decir de dónde bajarlo.`);
|
|
194
|
+
if (!baseUrl) {
|
|
195
|
+
throw new Error(`"${file.name}" viene con una ruta relativa y el agente no publica endpoint en el registry: no hay contra qué resolverla.`);
|
|
196
|
+
}
|
|
197
|
+
return new URL(file.path, baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`).toString();
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Baja un archivo de una entrega y lo verifica contra el hash anclado.
|
|
201
|
+
*
|
|
202
|
+
* Si los bytes no dan el hash, lanza en vez de devolverlos. Devolver un archivo
|
|
203
|
+
* que no cuadra "avisando" no serviría de nada: quien llama lo guardaría igual.
|
|
204
|
+
*/
|
|
205
|
+
export async function downloadDeliveredFile(file, options = {}) {
|
|
206
|
+
const destino = new URL(fileUrl(file, options.baseUrl));
|
|
207
|
+
if (options.address)
|
|
208
|
+
destino.searchParams.set('address', options.address);
|
|
209
|
+
if (options.signature)
|
|
210
|
+
destino.searchParams.set('signature', options.signature);
|
|
211
|
+
await assertPublicUrl(destino.toString(), options);
|
|
212
|
+
// El tope se ata al tamaño ANUNCIADO, no al de por defecto: si el manifiesto
|
|
213
|
+
// dice 2 MB, no hay razón para dejar que lleguen 25.
|
|
214
|
+
const tope = Math.min(options.maxBytes ?? MAX_FILE_BYTES, Math.max(file.size, 1) + 1024);
|
|
215
|
+
const { status, bytes } = await fetchBytesLimited(destino, {
|
|
216
|
+
maxBytes: tope,
|
|
217
|
+
timeoutMs: options.timeoutMs ?? 120_000,
|
|
218
|
+
redirect: 'error',
|
|
219
|
+
});
|
|
220
|
+
if (status !== 200) {
|
|
221
|
+
throw new FileVerificationError(`El agente respondió ${status} al pedirle "${file.name}".`, file.name);
|
|
222
|
+
}
|
|
223
|
+
verifyFileBytes(file, bytes);
|
|
224
|
+
return bytes;
|
|
225
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -28,7 +28,9 @@ export type { Agent, AgentMetadata, Task } from './types.js';
|
|
|
28
28
|
export { erc20Abi, escrowAbi, registryAbi } from './abis.js';
|
|
29
29
|
export { X402_SCHEME, X402Error, payAndAsk, quoteAsk } from './x402.js';
|
|
30
30
|
export type { AskResult, PayAndAskOptions, PermitDomain, X402Accept, X402Quote } from './x402.js';
|
|
31
|
-
export { assertPublicUrl, fetchLimited, isPrivateIp } from './net.js';
|
|
31
|
+
export { assertPublicUrl, fetchBytesLimited, fetchLimited, isPrivateIp } from './net.js';
|
|
32
|
+
export { FILES_BLOCK, MAX_FILE_BYTES, FileVerificationError, appendFilesManifest, buildFilesManifest, downloadDeliveredFile, fileUrl, parseFilesManifest, sanitizeFileName, stripFilesManifest, verifyFileBytes, } from './files.js';
|
|
33
|
+
export type { DeliveredFile, DownloadOptions } from './files.js';
|
|
32
34
|
export { X402_VERSION, X402_SERVER_SCHEME, buildQuote, enqueueByPayer, parsePaymentHeader, permitNonce, permitTypedData, readPermitDomain, resourceId, splitSignature, verifyAndSettle, } from './x402-server.js';
|
|
33
35
|
export type { SettleDeps, SettleResult, X402Payment, X402ServerAccept, X402ServerQuote, } from './x402-server.js';
|
|
34
36
|
export { ENVELOPE_HEADERS, DEFAULT_DEPTH, MAX_DEPTH, BudgetExhausted, DepthExhausted, LoopDetected, assertCanServe, descend, envelopeHeaders, newEnvelope, parseEnvelope, remainingBudget, } from './envelope.js';
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,9 @@ export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, parseAgentMetadata,
|
|
|
25
25
|
export { erc20Abi, escrowAbi, registryAbi } from './abis.js';
|
|
26
26
|
// x402: pagar a otro agente por una consulta, sin escrow y sin humano.
|
|
27
27
|
export { X402_SCHEME, X402Error, payAndAsk, quoteAsk } from './x402.js';
|
|
28
|
-
export { assertPublicUrl, fetchLimited, isPrivateIp } from './net.js';
|
|
28
|
+
export { assertPublicUrl, fetchBytesLimited, fetchLimited, isPrivateIp } from './net.js';
|
|
29
|
+
// Archivos: entregar un PDF o un vídeo anclando SU hash, no el del enlace.
|
|
30
|
+
export { FILES_BLOCK, MAX_FILE_BYTES, FileVerificationError, appendFilesManifest, buildFilesManifest, downloadDeliveredFile, fileUrl, parseFilesManifest, sanitizeFileName, stripFilesManifest, verifyFileBytes, } from './files.js';
|
|
29
31
|
// x402: la otra mitad, cobrar por llamada. Portada del bot de LexPanal, donde
|
|
30
32
|
// lleva meses cobrando en produccion.
|
|
31
33
|
export { X402_VERSION, X402_SERVER_SCHEME, buildQuote, enqueueByPayer, parsePaymentHeader, permitNonce, permitTypedData, readPermitDomain, resourceId, splitSignature, verifyAndSettle, } from './x402-server.js';
|
package/dist/net.d.ts
CHANGED
|
@@ -30,9 +30,21 @@ export interface UrlGuardOptions {
|
|
|
30
30
|
*/
|
|
31
31
|
export declare function assertPublicUrl(raw: string, options?: UrlGuardOptions): Promise<URL>;
|
|
32
32
|
/**
|
|
33
|
-
* `fetch` con tope de tamaño y de tiempo
|
|
34
|
-
*
|
|
33
|
+
* `fetch` con tope de tamaño y de tiempo, devolviendo los bytes crudos.
|
|
34
|
+
*
|
|
35
|
+
* La respuesta viene de un servidor ajeno: sin tope, uno hostil se lleva por
|
|
36
|
+
* delante el proceso. El tope se aplica MIENTRAS se lee, no al final, para que
|
|
37
|
+
* un cuerpo infinito se corte en cuanto pasa del límite y no cuando ya no cabe.
|
|
35
38
|
*/
|
|
39
|
+
export declare function fetchBytesLimited(url: URL | string, init?: RequestInit & {
|
|
40
|
+
maxBytes?: number;
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
}): Promise<{
|
|
43
|
+
status: number;
|
|
44
|
+
headers: Headers;
|
|
45
|
+
bytes: Uint8Array;
|
|
46
|
+
}>;
|
|
47
|
+
/** Lo mismo, decodificando el cuerpo como texto UTF-8. */
|
|
36
48
|
export declare function fetchLimited(url: URL | string, init?: RequestInit & {
|
|
37
49
|
maxBytes?: number;
|
|
38
50
|
timeoutMs?: number;
|
package/dist/net.js
CHANGED
|
@@ -103,10 +103,13 @@ async function loadDnsLookup() {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
|
-
* `fetch` con tope de tamaño y de tiempo
|
|
107
|
-
*
|
|
106
|
+
* `fetch` con tope de tamaño y de tiempo, devolviendo los bytes crudos.
|
|
107
|
+
*
|
|
108
|
+
* La respuesta viene de un servidor ajeno: sin tope, uno hostil se lleva por
|
|
109
|
+
* delante el proceso. El tope se aplica MIENTRAS se lee, no al final, para que
|
|
110
|
+
* un cuerpo infinito se corte en cuanto pasa del límite y no cuando ya no cabe.
|
|
108
111
|
*/
|
|
109
|
-
export async function
|
|
112
|
+
export async function fetchBytesLimited(url, init = {}) {
|
|
110
113
|
const { maxBytes = 512 * 1024, timeoutMs = 30_000, ...rest } = init;
|
|
111
114
|
const controller = new AbortController();
|
|
112
115
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -114,7 +117,7 @@ export async function fetchLimited(url, init = {}) {
|
|
|
114
117
|
const res = await fetch(url, { ...rest, signal: controller.signal });
|
|
115
118
|
const reader = res.body?.getReader();
|
|
116
119
|
if (!reader)
|
|
117
|
-
return { status: res.status, headers: res.headers,
|
|
120
|
+
return { status: res.status, headers: res.headers, bytes: new Uint8Array(0) };
|
|
118
121
|
const chunks = [];
|
|
119
122
|
let total = 0;
|
|
120
123
|
for (;;) {
|
|
@@ -134,9 +137,14 @@ export async function fetchLimited(url, init = {}) {
|
|
|
134
137
|
merged.set(c, offset);
|
|
135
138
|
offset += c.byteLength;
|
|
136
139
|
}
|
|
137
|
-
return { status: res.status, headers: res.headers,
|
|
140
|
+
return { status: res.status, headers: res.headers, bytes: merged };
|
|
138
141
|
}
|
|
139
142
|
finally {
|
|
140
143
|
clearTimeout(timer);
|
|
141
144
|
}
|
|
142
145
|
}
|
|
146
|
+
/** Lo mismo, decodificando el cuerpo como texto UTF-8. */
|
|
147
|
+
export async function fetchLimited(url, init = {}) {
|
|
148
|
+
const { status, headers, bytes } = await fetchBytesLimited(url, init);
|
|
149
|
+
return { status, headers, text: new TextDecoder().decode(bytes) };
|
|
150
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panal/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "SDK de Panal: contrata agentes de IA autonomos on-chain en Monad",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsc -p tsconfig.json",
|
|
47
47
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
48
|
-
"test": "tsx test/sdk.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts"
|
|
48
|
+
"test": "tsx test/sdk.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts"
|
|
49
49
|
}
|
|
50
50
|
}
|