@panal/sdk 0.25.0 → 0.27.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.
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Panal SDK — archivos en una pregunta pagada (x402) entre agentes.
3
+ *
4
+ * Una pregunta x402 es texto: 2.000 caracteres a la ida y una respuesta a la
5
+ * vuelta. Para pedirle a otro agente que lea una foto, transcriba un audio o
6
+ * resuma un PDF hacía falta poder mandarle el archivo, y que él pudiera
7
+ * devolver los suyos.
8
+ *
9
+ * Los bytes NO viajan en la pregunta. Van al buzón de Panal, a nombre de quien
10
+ * tiene que leerlos, y en la pregunta solo va la lista con su hash:
11
+ *
12
+ * POST /x402/ask { "prompt": "…", "files": [{ name, size, hash, mime }] }
13
+ *
14
+ * Quien recibe la lista baja cada archivo y comprueba sus bytes contra el hash
15
+ * antes de hacer nada: lo que le llega es exactamente lo que le anunciaron, y
16
+ * el buzón no puede cambiarlo por el camino. A la vuelta, igual al revés.
17
+ *
18
+ * En el buzón, cada archivo es de dos cuentas y de nadie más: quien lo dejó y
19
+ * su destinatario. Subirlo pide la firma de quien lo deja (con el destinatario
20
+ * y el hash dentro); bajarlo, la de una de las dos. Y si el destinatario tiene
21
+ * clave de cifrado publicada, se cifra para él antes de subirlo: entonces ni el
22
+ * buzón puede leerlo. Caduca como todo lo del buzón: es un relevo, no un archivo.
23
+ *
24
+ * Ningún mensaje lleva gas: son firmas EIP-191 con caducidad dentro.
25
+ */
26
+ import type { Address, Hex } from 'viem';
27
+ import { type HashedFile } from './files.js';
28
+ /** Cuántos archivos como mucho en una pregunta o en su respuesta. Como los adjuntos de un encargo. */
29
+ export declare const MAX_CONSULTA_FILES = 5;
30
+ /** Lo que anuncia un agente en su cotización si acepta archivos: cuántos y de qué tamaño. */
31
+ export interface ConsultaFilesCapability {
32
+ max: number;
33
+ maxBytes: number;
34
+ }
35
+ /** La capacidad con la que cotiza un agente con el motor de Panal. */
36
+ export declare const CONSULTA_FILES_CAPABILITY: ConsultaFilesCapability;
37
+ /** Un archivo para mandar: lo que se tiene antes de subirlo. */
38
+ export interface OutgoingFile {
39
+ name: string;
40
+ bytes: Uint8Array;
41
+ mime?: string;
42
+ }
43
+ /** Un archivo recibido, ya comprobado contra su hash y abierto si iba cifrado. */
44
+ export interface ReceivedFile {
45
+ name: string;
46
+ mime?: string;
47
+ size: number;
48
+ bytes: Uint8Array;
49
+ }
50
+ /** Firma que autoriza a dejar ESTOS bytes para ESTE destinatario. */
51
+ export declare function consultaUploadMessage(para: Address, hash: Hex, expira: number): string;
52
+ /** Firma que autoriza a leer un archivo, a su destinatario o a quien lo dejó. */
53
+ export declare function consultaReadMessage(hash: Hex, expira: number): string;
54
+ /** La ruta de un archivo de consulta en el buzón. */
55
+ export declare function consultaFileUrl(buzonUrl: string, para: Address, hash?: Hex): string;
56
+ /**
57
+ * Valida la lista de archivos que llega en una pregunta o en una respuesta.
58
+ * Entrada no fiable: devuelve `null` si algo no cuadra, para rechazarla entera
59
+ * —una pregunta con un archivo mal anunciado no se cobra a medias—.
60
+ */
61
+ export declare function parseConsultaFiles(value: unknown, max?: number): HashedFile[] | null;
62
+ /** Firma un mensaje con caducidad: lo que firma la cuenta que sube o que lee. */
63
+ export type SignMessage = (message: string) => Promise<Hex>;
64
+ /**
65
+ * Deja un archivo en el buzón para `para`. Los bytes se suben tal cual
66
+ * llegan: si hay que cifrarlos, se cifran ANTES (y el hash es el de lo
67
+ * cifrado, que es lo que se guarda y se comprueba).
68
+ */
69
+ export declare function uploadConsultaFile(params: {
70
+ buzonUrl: string;
71
+ para: Address;
72
+ de: Address;
73
+ file: OutgoingFile;
74
+ sign: SignMessage;
75
+ }): Promise<HashedFile>;
76
+ /**
77
+ * Baja un archivo de consulta y comprueba sus bytes contra el hash anunciado.
78
+ * Lanza si no cuadran: un archivo cambiado no se le pasa a nadie «avisando».
79
+ */
80
+ export declare function downloadConsultaFile(params: {
81
+ buzonUrl: string;
82
+ /** Su destinatario: en qué buzón está. */
83
+ para: Address;
84
+ /** Quien lo lee: el destinatario o quien lo dejó. */
85
+ lector: Address;
86
+ file: HashedFile;
87
+ sign: SignMessage;
88
+ }): Promise<Uint8Array>;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Panal SDK — archivos en una pregunta pagada (x402) entre agentes.
3
+ *
4
+ * Una pregunta x402 es texto: 2.000 caracteres a la ida y una respuesta a la
5
+ * vuelta. Para pedirle a otro agente que lea una foto, transcriba un audio o
6
+ * resuma un PDF hacía falta poder mandarle el archivo, y que él pudiera
7
+ * devolver los suyos.
8
+ *
9
+ * Los bytes NO viajan en la pregunta. Van al buzón de Panal, a nombre de quien
10
+ * tiene que leerlos, y en la pregunta solo va la lista con su hash:
11
+ *
12
+ * POST /x402/ask { "prompt": "…", "files": [{ name, size, hash, mime }] }
13
+ *
14
+ * Quien recibe la lista baja cada archivo y comprueba sus bytes contra el hash
15
+ * antes de hacer nada: lo que le llega es exactamente lo que le anunciaron, y
16
+ * el buzón no puede cambiarlo por el camino. A la vuelta, igual al revés.
17
+ *
18
+ * En el buzón, cada archivo es de dos cuentas y de nadie más: quien lo dejó y
19
+ * su destinatario. Subirlo pide la firma de quien lo deja (con el destinatario
20
+ * y el hash dentro); bajarlo, la de una de las dos. Y si el destinatario tiene
21
+ * clave de cifrado publicada, se cifra para él antes de subirlo: entonces ni el
22
+ * buzón puede leerlo. Caduca como todo lo del buzón: es un relevo, no un archivo.
23
+ *
24
+ * Ningún mensaje lleva gas: son firmas EIP-191 con caducidad dentro.
25
+ */
26
+ import { getAddress, isAddress, keccak256 } from 'viem';
27
+ import { fetchBytesLimited, fetchLimited, sinBarraFinal } from './net.js';
28
+ import { MAX_FILE_BYTES, sanitizeFileName, verifyFileBytes } from './files.js';
29
+ /** Cuántos archivos como mucho en una pregunta o en su respuesta. Como los adjuntos de un encargo. */
30
+ export const MAX_CONSULTA_FILES = 5;
31
+ /** Lo que dura una firma de subida o de lectura. */
32
+ const VENTANA_FIRMA_S = 10 * 60;
33
+ /** La capacidad con la que cotiza un agente con el motor de Panal. */
34
+ export const CONSULTA_FILES_CAPABILITY = { max: MAX_CONSULTA_FILES, maxBytes: MAX_FILE_BYTES };
35
+ /** Firma que autoriza a dejar ESTOS bytes para ESTE destinatario. */
36
+ export function consultaUploadMessage(para, hash, expira) {
37
+ return `Panal: dejo un archivo para ${getAddress(para)}\nhash: ${hash.toLowerCase()}\nexpira: ${expira}`;
38
+ }
39
+ /** Firma que autoriza a leer un archivo, a su destinatario o a quien lo dejó. */
40
+ export function consultaReadMessage(hash, expira) {
41
+ return `Panal: leer el archivo ${hash.toLowerCase()}\nexpira: ${expira}`;
42
+ }
43
+ /** La ruta de un archivo de consulta en el buzón. */
44
+ export function consultaFileUrl(buzonUrl, para, hash) {
45
+ // `sinBarraFinal` y no una expresión regular: con una URL llena de `/` una
46
+ // regex como /\/+$/ tarda en cuadrático (CodeQL js/polynomial-redos).
47
+ const base = `${sinBarraFinal(buzonUrl)}/${getAddress(para)}/consulta-archivo`;
48
+ return hash ? `${base}/${hash.toLowerCase()}` : base;
49
+ }
50
+ /**
51
+ * Valida la lista de archivos que llega en una pregunta o en una respuesta.
52
+ * Entrada no fiable: devuelve `null` si algo no cuadra, para rechazarla entera
53
+ * —una pregunta con un archivo mal anunciado no se cobra a medias—.
54
+ */
55
+ export function parseConsultaFiles(value, max = MAX_CONSULTA_FILES) {
56
+ if (value === undefined || value === null)
57
+ return [];
58
+ if (!Array.isArray(value) || value.length > max)
59
+ return null;
60
+ const out = [];
61
+ for (const v of value) {
62
+ if (!v || typeof v !== 'object')
63
+ return null;
64
+ const { name, size, hash, mime } = v;
65
+ if (typeof name !== 'string' || typeof hash !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(hash))
66
+ return null;
67
+ if (typeof size !== 'number' || !Number.isInteger(size) || size <= 0 || size > MAX_FILE_BYTES)
68
+ return null;
69
+ if (mime !== undefined && (typeof mime !== 'string' || mime.length > 120))
70
+ return null;
71
+ let limpio = '';
72
+ try {
73
+ limpio = sanitizeFileName(name);
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ if (!limpio)
79
+ return null;
80
+ out.push({ name: limpio, size, hash: hash.toLowerCase(), ...(mime ? { mime } : {}) });
81
+ }
82
+ return out;
83
+ }
84
+ /**
85
+ * Deja un archivo en el buzón para `para`. Los bytes se suben tal cual
86
+ * llegan: si hay que cifrarlos, se cifran ANTES (y el hash es el de lo
87
+ * cifrado, que es lo que se guarda y se comprueba).
88
+ */
89
+ export async function uploadConsultaFile(params) {
90
+ const { file } = params;
91
+ if (file.bytes.byteLength === 0)
92
+ throw new Error(`"${file.name}" está vacío.`);
93
+ if (file.bytes.byteLength > MAX_FILE_BYTES) {
94
+ throw new Error(`"${file.name}" pasa de ${Math.round(MAX_FILE_BYTES / 1024 / 1024)} MB.`);
95
+ }
96
+ const name = sanitizeFileName(file.name);
97
+ if (!name)
98
+ throw new Error('El archivo no tiene un nombre válido.');
99
+ const hash = keccak256(file.bytes);
100
+ const expira = Math.floor(Date.now() / 1000) + VENTANA_FIRMA_S;
101
+ const signature = await params.sign(consultaUploadMessage(params.para, hash, expira));
102
+ const { status, text } = await fetchLimited(consultaFileUrl(params.buzonUrl, params.para), {
103
+ method: 'POST',
104
+ headers: {
105
+ 'content-type': file.mime || 'application/octet-stream',
106
+ 'x-panal-address': getAddress(params.de),
107
+ 'x-panal-signature': signature,
108
+ 'x-panal-expira': String(expira),
109
+ 'x-panal-filename': encodeURIComponent(name),
110
+ },
111
+ body: file.bytes,
112
+ timeoutMs: 120_000,
113
+ maxBytes: 10_000,
114
+ });
115
+ if (status !== 200)
116
+ throw new Error(`El buzón no aceptó "${name}" (${status}: ${text.slice(0, 160)}).`);
117
+ return { name, size: file.bytes.byteLength, hash, ...(file.mime ? { mime: file.mime } : {}) };
118
+ }
119
+ /**
120
+ * Baja un archivo de consulta y comprueba sus bytes contra el hash anunciado.
121
+ * Lanza si no cuadran: un archivo cambiado no se le pasa a nadie «avisando».
122
+ */
123
+ export async function downloadConsultaFile(params) {
124
+ const { file } = params;
125
+ if (!isAddress(params.para) || !isAddress(params.lector))
126
+ throw new Error('Dirección no válida.');
127
+ const expira = Math.floor(Date.now() / 1000) + VENTANA_FIRMA_S;
128
+ const signature = await params.sign(consultaReadMessage(file.hash, expira));
129
+ const { status, bytes } = await fetchBytesLimited(consultaFileUrl(params.buzonUrl, params.para, file.hash), {
130
+ headers: {
131
+ 'x-panal-address': getAddress(params.lector),
132
+ 'x-panal-signature': signature,
133
+ 'x-panal-expira': String(expira),
134
+ },
135
+ timeoutMs: 120_000,
136
+ maxBytes: Math.min(MAX_FILE_BYTES, file.size) + 1024,
137
+ });
138
+ if (status !== 200)
139
+ throw new Error(`El buzón no dio "${file.name}" (${status}).`);
140
+ verifyFileBytes(file, bytes);
141
+ return bytes;
142
+ }
package/dist/files.d.ts CHANGED
@@ -92,6 +92,16 @@ export declare function sanitizeFileName(name: string): string;
92
92
  export declare function buildFilesManifest(files: DeliveredFile[]): string;
93
93
  /** El texto de la entrega con el manifiesto pegado al final. */
94
94
  export declare function appendFilesManifest(text: string, files: DeliveredFile[]): string;
95
+ /**
96
+ * Lee los bloques `clave: valor` que van bajo una cabecera dada.
97
+ *
98
+ * Lo comparten el manifiesto de entrega y el de adjuntos: los dos se anclan en
99
+ * la cadena y los dos tienen que leerse igual en todas partes. Un bloque
100
+ * termina en la primera línea vacía o que ya no es `clave: valor`, y eso basta
101
+ * para que dos manifiestos pegados no se contaminen: ninguna cabecera lleva
102
+ * dos puntos.
103
+ */
104
+ export declare function parseBlocks(text: string, tag: string): Record<string, string>[];
95
105
  /**
96
106
  * Lee los archivos anunciados en el texto de una entrega.
97
107
  *
@@ -101,10 +111,11 @@ export declare function appendFilesManifest(text: string, files: DeliveredFile[]
101
111
  */
102
112
  export declare function parseFilesManifest(text: string): DeliveredFile[];
103
113
  /**
104
- * El texto de la entrega sin los bloques del manifiesto.
114
+ * El texto de la entrega sin los bloques de Panal: archivos, adjuntos y
115
+ * subcontrataciones.
105
116
  *
106
- * Para enseñárselo a una persona: el manifiesto es para la máquina, y en un
107
- * chat de Telegram no aporta más que ruido.
117
+ * Para enseñárselo a una persona: los bloques son para la máquina, y en un
118
+ * chat de Telegram no aportan más que ruido. Lo que dicen se enseña aparte.
108
119
  */
109
120
  export declare function stripFilesManifest(text: string): string;
110
121
  /** Comprueba unos bytes contra lo que el manifiesto prometía. Lanza si no cuadra. */
package/dist/files.js CHANGED
@@ -111,7 +111,7 @@ export function appendFilesManifest(text, files) {
111
111
  * para que dos manifiestos pegados no se contaminen: ninguna cabecera lleva
112
112
  * dos puntos.
113
113
  */
114
- function parseBloques(text, tag) {
114
+ export function parseBlocks(text, tag) {
115
115
  const out = [];
116
116
  const lineas = text.split(/\r?\n/);
117
117
  for (let i = 0; i < lineas.length; i++) {
@@ -161,7 +161,7 @@ function parseComun(campos) {
161
161
  */
162
162
  export function parseFilesManifest(text) {
163
163
  const out = [];
164
- for (const campos of parseBloques(text, FILES_BLOCK)) {
164
+ for (const campos of parseBlocks(text, FILES_BLOCK)) {
165
165
  const comun = parseComun(campos);
166
166
  if (!comun)
167
167
  continue;
@@ -175,17 +175,24 @@ export function parseFilesManifest(text) {
175
175
  return out;
176
176
  }
177
177
  /**
178
- * El texto de la entrega sin los bloques del manifiesto.
178
+ * Cualquier cabecera de bloque de Panal: `[panal-files/1]`, `[panal-attach/1]`,
179
+ * `[panal-subcontracts/1]` y las que vengan. Se reconocen por la forma para que
180
+ * una versión nueva de un bloque no le salga a nadie como texto de la entrega.
181
+ */
182
+ const CABECERA_DE_BLOQUE = /^\[panal-[a-z]+\/\d+\]$/;
183
+ /**
184
+ * El texto de la entrega sin los bloques de Panal: archivos, adjuntos y
185
+ * subcontrataciones.
179
186
  *
180
- * Para enseñárselo a una persona: el manifiesto es para la máquina, y en un
181
- * chat de Telegram no aporta más que ruido.
187
+ * Para enseñárselo a una persona: los bloques son para la máquina, y en un
188
+ * chat de Telegram no aportan más que ruido. Lo que dicen se enseña aparte.
182
189
  */
183
190
  export function stripFilesManifest(text) {
184
191
  const lineas = text.split(/\r?\n/);
185
192
  const fuera = [];
186
193
  let dentro = false;
187
194
  for (const linea of lineas) {
188
- if (linea.trim() === FILES_BLOCK || linea.trim() === ATTACH_BLOCK) {
195
+ if (CABECERA_DE_BLOQUE.test(linea.trim())) {
189
196
  dentro = true;
190
197
  continue;
191
198
  }
@@ -332,7 +339,7 @@ export function appendAttachmentsManifest(brief, files) {
332
339
  /** Lee los adjuntos anunciados en un encargo. No lanza por un bloque roto. */
333
340
  export function parseAttachmentsManifest(brief) {
334
341
  const out = [];
335
- for (const campos of parseBloques(brief, ATTACH_BLOCK)) {
342
+ for (const campos of parseBlocks(brief, ATTACH_BLOCK)) {
336
343
  const comun = parseComun(campos);
337
344
  if (comun)
338
345
  out.push(comun);
package/dist/idiomas.js CHANGED
@@ -35,6 +35,7 @@
35
35
  * y que hoy son lo único que un francés no puede leer.
36
36
  * ───────────────────────────────────────────────────────────────────────────
37
37
  */
38
+ import { sinBarraFinal } from './net.js';
38
39
  /**
39
40
  * Los idiomas del marketplace, en códigos ISO 639-1.
40
41
  *
@@ -63,7 +64,7 @@ export function normalizarIdioma(v) {
63
64
  * en un código que no existe.
64
65
  */
65
66
  export function fichaEnIdioma(botUrl, idioma) {
66
- const base = `${botUrl.replace(/\/+$/, '')}/agent.json`;
67
+ const base = `${sinBarraFinal(botUrl)}/agent.json`;
67
68
  const lang = normalizarIdioma(idioma);
68
69
  return lang ? `${base}?lang=${lang}` : base;
69
70
  }
package/dist/index.d.ts CHANGED
@@ -27,6 +27,8 @@ export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, normalizeAgentLink,
27
27
  export type { Agent, AgentLinkKey, AgentLinks, 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
+ export { MAX_SUBCONTRACTS, SUBCONTRACTS_BLOCK, appendSubcontractsManifest, buildSubcontractsManifest, parseSubcontractsManifest, subcontractsFromJson, subcontractsOfAnswer, subcontractsToJson, type SubcontractJson, type SubcontractRecord, } from './subcontratos.js';
31
+ export { CONSULTA_FILES_CAPABILITY, MAX_CONSULTA_FILES, consultaFileUrl, consultaReadMessage, consultaUploadMessage, downloadConsultaFile, parseConsultaFiles, uploadConsultaFile, type ConsultaFilesCapability, type OutgoingFile, type ReceivedFile, type SignMessage, } from './consultas.js';
30
32
  export { X402_NATIVE, formatX402Amount, networkOfChain, parseX402Amount, x402Currencies, x402Currency, x402CurrencyByName, } from './currencies.js';
31
33
  export type { X402Currency, X402Scheme } from './currencies.js';
32
34
  export type { AskResult, PayAndAskOptions, PermitDomain, X402Accept, X402Quote } from './x402.js';
@@ -46,7 +48,7 @@ export { componerNivel, conTextoDeLaFicha, esTokenDeNivel, leerNivelDeSegmento,
46
48
  export { esTokenDeTipo, leerTipo, leerTipoDeSegmento, tokenDeTipo } from './tipo.js';
47
49
  export type { TipoDeAgente } from './tipo.js';
48
50
  export { componerEjemplo, conEjemplosDeLaFicha, EJEMPLOS_EDITABLES, esTokenDeEjemplo, falloDeEjemplo, leerEjemploDeSegmento, leerEjemplos, leerEjemplosDeMetadata, MAX_EJEMPLO, } from './ejemplos.js';
49
- export { rutaDeAgente } from './net.js';
51
+ export { rutaDeAgente, sinBarraFinal } from './net.js';
50
52
  export { BUZON_URL, TABLON, VENTANA_FIRMA_S, encargoSignMessage, entregaSignMessage, ofertaSignMessage, } from './tablon.js';
51
53
  export type { EncargoDelTablon } from './tablon.js';
52
54
  export { fichaEnIdioma, IDIOMAS, NOMBRE_IDIOMA, normalizarIdioma } from './idiomas.js';
@@ -54,5 +56,6 @@ export type { Idioma } from './idiomas.js';
54
56
  export { MERCADO_PANAL, PRECIO_MAX_EDAD_S, PYTH_MONAD, PYTH_MON_USD, enDolares, esFresco, formatearDolares, leerPrecios, valorDeVenta, } from './precios.js';
55
57
  export type { FuentePrecio, MonedaConPrecio, OpcionesPrecios, Precio, Precios } from './precios.js';
56
58
  export { panalTablonAbi } from './tablon-abi.js';
57
- export { CATEGORIAS_TABLON, EstadoTablon, MARGEN_ASIGNACION_S, MAX_ANUNCIO, MAX_ARCHIVOS_TABLON, MAX_BYTES_ARCHIVOS_TABLON, MAX_CATEGORIAS, MAX_TEXTO_ANUNCIO, MAX_OFERTAS, MAX_VENTANA_ELECCION_S, MIN_PLAZO_ENTREGA_S, ModoTablon, anuncioPara, audienciaAdmite, categoriaDelTablon, categoriasDeMascara, claveDeCampana, encargoDeLaCadena, entregaConArchivos, idDeCampana, leerAnuncio, ofertaDeLaCadena, problemaAlPublicar, problemaConArchivos, situacionDe, tablonArchivoSignMessage, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
59
+ export { CATEGORIAS_TABLON, EstadoTablon, MARGEN_ASIGNACION_S, MAX_ANUNCIO, MAX_ARCHIVOS_TABLON, MAX_BYTES_ARCHIVOS_TABLON, MAX_CATEGORIAS, MAX_TEXTO_ANUNCIO, MAX_OFERTAS, MAX_VENTANA_ELECCION_S, MIN_PLAZO_ENTREGA_S, ModoTablon, anuncioPara, audienciaAdmite, categoriaDelTablon, categoriasDeMascara, claveDeCampana, encargoDeLaCadena, entregaConArchivos, idDeCampana, leerAnuncio, ofertaDeLaCadena, problemaAlPublicar, problemaConArchivos, situacionDe, tablonAccesoSignMessage, tablonArchivoSignMessage, tablonDisputaSignMessage, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
58
60
  export type { ArchivoParaSubir, AudienciaTablon, CategoriaTablon, EncargoConReglas, OfertaConReglas, PublicarEnElTablon, SituacionEnElTablon, } from './tablon-reglas.js';
61
+ export { CIFRADO_V1, aBase64, accesoSignMessage, archivosAnunciadosEnSobre, claveDesdeFirma, concederAcceso, cifrarArchivo, cifrarTexto, comprobarClavePublicada, deBase64, descifrarArchivo, descifrarTexto, destinatariosDe, disputaSignMessage, esArchivoCifrado, esTextoCifrado, mensajeParaDerivarClave, mensajeParaPublicarClave, nombreEnBuzon, puedeAbrir, type Acceso, type ArchivoAnunciado, type ClaveDeCifrado, type ClavePublicada, type Destinatario, } from './cifrado.js';
package/dist/index.js CHANGED
@@ -25,6 +25,8 @@ export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, normalizeAgentLink,
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 { MAX_SUBCONTRACTS, SUBCONTRACTS_BLOCK, appendSubcontractsManifest, buildSubcontractsManifest, parseSubcontractsManifest, subcontractsFromJson, subcontractsOfAnswer, subcontractsToJson, } from './subcontratos.js';
29
+ export { CONSULTA_FILES_CAPABILITY, MAX_CONSULTA_FILES, consultaFileUrl, consultaReadMessage, consultaUploadMessage, downloadConsultaFile, parseConsultaFiles, uploadConsultaFile, } from './consultas.js';
28
30
  export { X402_NATIVE, formatX402Amount, networkOfChain, parseX402Amount, x402Currencies, x402Currency, x402CurrencyByName, } from './currencies.js';
29
31
  export { assertPublicUrl, fetchBytesLimited, fetchLimited, isPrivateIp } from './net.js';
30
32
  // Archivos: entregar un PDF o un vídeo anclando SU hash, no el del enlace.
@@ -54,7 +56,7 @@ export { componerEjemplo, conEjemplosDeLaFicha, EJEMPLOS_EDITABLES, esTokenDeEje
54
56
  // propio agente y no un servicio de Panal.
55
57
  // Unir una ruta con la URL de un agente. Ver por qué en `net.ts`: un agente
56
58
  // puede vivir en un subcamino, y `new URL('/x', base)` se lo come.
57
- export { rutaDeAgente } from './net.js';
59
+ export { rutaDeAgente, sinBarraFinal } from './net.js';
58
60
  // El tablón: encargos sin dueño para que los coja un programa. Los mensajes de
59
61
  // firma se exportan porque cualquiera que hable con el buzón sin este cliente
60
62
  // —otro lenguaje, otro runtime— necesita producir exactamente estos bytes.
@@ -67,4 +69,7 @@ export { MERCADO_PANAL, PRECIO_MAX_EDAD_S, PYTH_MONAD, PYTH_MON_USD, enDolares,
67
69
  // el cliente (`…BoardJob`); aquí, el ABI, los tipos y lo que se calcula sin la
68
70
  // cadena. Ver tablon-reglas.ts y contracts/BOARD.md.
69
71
  export { panalTablonAbi } from './tablon-abi.js';
70
- export { CATEGORIAS_TABLON, EstadoTablon, MARGEN_ASIGNACION_S, MAX_ANUNCIO, MAX_ARCHIVOS_TABLON, MAX_BYTES_ARCHIVOS_TABLON, MAX_CATEGORIAS, MAX_TEXTO_ANUNCIO, MAX_OFERTAS, MAX_VENTANA_ELECCION_S, MIN_PLAZO_ENTREGA_S, ModoTablon, anuncioPara, audienciaAdmite, categoriaDelTablon, categoriasDeMascara, claveDeCampana, encargoDeLaCadena, entregaConArchivos, idDeCampana, leerAnuncio, ofertaDeLaCadena, problemaAlPublicar, problemaConArchivos, situacionDe, tablonArchivoSignMessage, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
72
+ export { CATEGORIAS_TABLON, EstadoTablon, MARGEN_ASIGNACION_S, MAX_ANUNCIO, MAX_ARCHIVOS_TABLON, MAX_BYTES_ARCHIVOS_TABLON, MAX_CATEGORIAS, MAX_TEXTO_ANUNCIO, MAX_OFERTAS, MAX_VENTANA_ELECCION_S, MIN_PLAZO_ENTREGA_S, ModoTablon, anuncioPara, audienciaAdmite, categoriaDelTablon, categoriasDeMascara, claveDeCampana, encargoDeLaCadena, entregaConArchivos, idDeCampana, leerAnuncio, ofertaDeLaCadena, problemaAlPublicar, problemaConArchivos, situacionDe, tablonAccesoSignMessage, tablonArchivoSignMessage, tablonDisputaSignMessage, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
73
+ // Encargos cifrados de punta a punta: la clave sale de una firma, el sobre se
74
+ // abre solo con la de sus destinatarios, y el buzón guarda lo que no sabe leer.
75
+ export { CIFRADO_V1, aBase64, accesoSignMessage, archivosAnunciadosEnSobre, claveDesdeFirma, concederAcceso, cifrarArchivo, cifrarTexto, comprobarClavePublicada, deBase64, descifrarArchivo, descifrarTexto, destinatariosDe, disputaSignMessage, esArchivoCifrado, esTextoCifrado, mensajeParaDerivarClave, mensajeParaPublicarClave, nombreEnBuzon, puedeAbrir, } from './cifrado.js';
package/dist/net.d.ts CHANGED
@@ -68,3 +68,12 @@ export declare function fetchLimited(url: URL | string, init?: RequestInit & {
68
68
  * rutas se hace en un sitio y no a mano en cada llamada.
69
69
  */
70
70
  export declare function rutaDeAgente(base: string, ruta: string): string;
71
+ /**
72
+ * Una URL sin las barras del final.
73
+ *
74
+ * Con un bucle y no con `replace(/\/+$/, '')`: esa expresión, sobre un texto
75
+ * lleno de barras que no acaba en barra, vuelve a probar desde cada una y
76
+ * tarda un tiempo cuadrático. Y la URL de un agente la escribe él en su
77
+ * ficha: con unos miles de barras bastaría para colgar a quien la lea.
78
+ */
79
+ export declare function sinBarraFinal(url: string): string;
package/dist/net.js CHANGED
@@ -163,5 +163,19 @@ export async function fetchLimited(url, init = {}) {
163
163
  * rutas se hace en un sitio y no a mano en cada llamada.
164
164
  */
165
165
  export function rutaDeAgente(base, ruta) {
166
- return `${base.replace(/\/+$/, '')}/${ruta.replace(/^\/+/, '')}`;
166
+ return `${sinBarraFinal(base)}/${ruta.replace(/^\/+/, '')}`;
167
+ }
168
+ /**
169
+ * Una URL sin las barras del final.
170
+ *
171
+ * Con un bucle y no con `replace(/\/+$/, '')`: esa expresión, sobre un texto
172
+ * lleno de barras que no acaba en barra, vuelve a probar desde cada una y
173
+ * tarda un tiempo cuadrático. Y la URL de un agente la escribe él en su
174
+ * ficha: con unos miles de barras bastaría para colgar a quien la lea.
175
+ */
176
+ export function sinBarraFinal(url) {
177
+ let fin = url.length;
178
+ while (fin > 0 && url.charCodeAt(fin - 1) === 47)
179
+ fin--;
180
+ return url.slice(0, fin);
167
181
  }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Panal SDK — a quién pagó un agente para hacer tu encargo, dicho en la entrega.
3
+ *
4
+ * Un agente puede subcontratar: preguntarle a otro, pagándole de lo que cobró,
5
+ * lo que no sabe hacer. Hasta ahora eso solo quedaba en los logs de quien lo
6
+ * hacía, y el cliente recibía un trabajo en el que había participado gente que
7
+ * no conocía. Si además le manda sus archivos a ese otro, deja de ser un
8
+ * detalle interno: es su información en manos de un tercero.
9
+ *
10
+ * Por eso va DENTRO del texto que se entrega, como el manifiesto de archivos:
11
+ *
12
+ * Aquí tienes la revisión.
13
+ *
14
+ * [panal-subcontracts/1]
15
+ * agent: 0x8e9c…
16
+ * skill: tests
17
+ * amount: 150000000000000000
18
+ * currency: $PANAL
19
+ * asset: 0x2E2e…7777
20
+ * decimals: 18
21
+ * tx: 0x51ab…
22
+ *
23
+ * Ese texto es el que se ancla en la cadena, así que el apartado no se puede
24
+ * quitar ni cambiar después de entregar, y cada pago se comprueba en el
25
+ * explorador con su `tx`. Lo escribe el motor del agente, no su autor: ningún
26
+ * `handleTask` puede olvidarse de decirlo.
27
+ *
28
+ * `via` es para la cadena entera. Si el agente que contrataste pagó a B y B, a
29
+ * su vez, pagó a C, la entrega lleva a B y a C, y C lleva `via: B`. El dinero
30
+ * de C salió de B, no de ti, pero tu información pasó por los dos.
31
+ *
32
+ * Por la red, en la respuesta de una pregunta x402, la misma lista va en JSON
33
+ * (`subcontracts`): así el que pregunta sabe a quién subcontrató el que le
34
+ * respondió, sin leer su texto.
35
+ */
36
+ import type { Address, Hex } from 'viem';
37
+ /** Cabecera del bloque. Lleva versión porque el formato se ancla en la cadena. */
38
+ export declare const SUBCONTRACTS_BLOCK = "[panal-subcontracts/1]";
39
+ /**
40
+ * Cuántas subcontrataciones se cuentan como mucho en una entrega.
41
+ *
42
+ * Una cadena tiene un tope de saltos y cada agente un presupuesto, así que en
43
+ * la práctica son pocas; el tope es para que una respuesta hostil no llene una
44
+ * entrega con miles de líneas.
45
+ */
46
+ export declare const MAX_SUBCONTRACTS = 32;
47
+ /** Una subcontratación: a quién, para qué y cuánto se le pagó. */
48
+ export interface SubcontractRecord {
49
+ /** El agente que cobró y respondió. */
50
+ agent: Address;
51
+ /** Para qué: la skill con la que se le buscó. */
52
+ skill: string;
53
+ /** Lo pagado, en unidades mínimas de `currency`. */
54
+ amount: bigint;
55
+ /** En qué se pagó. */
56
+ currency: {
57
+ symbol: string;
58
+ address: Address;
59
+ decimals: number;
60
+ };
61
+ /** La transacción del cobro, si el agente la reportó. */
62
+ txHash?: Hex;
63
+ /** Si no lo contrató el agente del encargo sino uno de sus subcontratados: cuál. */
64
+ via?: Address;
65
+ /** Cuántos archivos se le mandaron con la pregunta: tu información pasó por él. */
66
+ files?: number;
67
+ }
68
+ /**
69
+ * Construye el bloque. Orden de claves fijo y `\n`: se ancla en la cadena, y
70
+ * las mismas subcontrataciones tienen que dar exactamente los mismos bytes.
71
+ */
72
+ export declare function buildSubcontractsManifest(list: SubcontractRecord[]): string;
73
+ /** El texto de la entrega con las subcontrataciones al final. Sin ninguna, igual que estaba. */
74
+ export declare function appendSubcontractsManifest(text: string, list: SubcontractRecord[]): string;
75
+ /**
76
+ * Lee las subcontrataciones anunciadas en una entrega.
77
+ *
78
+ * Nunca lanza: un bloque mal formado se salta y los demás se leen. Que una
79
+ * línea rota impida leer la entrega sería peor que no enseñar esa línea.
80
+ */
81
+ export declare function parseSubcontractsManifest(text: string): SubcontractRecord[];
82
+ /** La forma JSON, para la respuesta de una pregunta x402. */
83
+ export interface SubcontractJson {
84
+ agent: string;
85
+ skill: string;
86
+ amount: string;
87
+ currency: string;
88
+ asset: string;
89
+ decimals: number;
90
+ tx?: string;
91
+ via?: string;
92
+ files?: number;
93
+ }
94
+ export declare function subcontractsToJson(list: SubcontractRecord[]): SubcontractJson[];
95
+ /**
96
+ * Lee la lista que manda otro agente en su respuesta. Es entrada no fiable:
97
+ * se valida entrada a entrada y lo que no cuadra se descarta, nunca se lanza.
98
+ */
99
+ export declare function subcontractsFromJson(value: unknown): SubcontractRecord[];
100
+ /**
101
+ * Lo que apunta un agente al subcontratar: la subcontratación directa y, detrás,
102
+ * las que hizo a su vez el que respondió, con `via` para saber de dónde vienen.
103
+ */
104
+ export declare function subcontractsOfAnswer(direct: Omit<SubcontractRecord, 'via'>, nested?: SubcontractRecord[]): SubcontractRecord[];