@panal/sdk 0.24.0 → 0.26.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/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
@@ -46,7 +46,7 @@ export { componerNivel, conTextoDeLaFicha, esTokenDeNivel, leerNivelDeSegmento,
46
46
  export { esTokenDeTipo, leerTipo, leerTipoDeSegmento, tokenDeTipo } from './tipo.js';
47
47
  export type { TipoDeAgente } from './tipo.js';
48
48
  export { componerEjemplo, conEjemplosDeLaFicha, EJEMPLOS_EDITABLES, esTokenDeEjemplo, falloDeEjemplo, leerEjemploDeSegmento, leerEjemplos, leerEjemplosDeMetadata, MAX_EJEMPLO, } from './ejemplos.js';
49
- export { rutaDeAgente } from './net.js';
49
+ export { rutaDeAgente, sinBarraFinal } from './net.js';
50
50
  export { BUZON_URL, TABLON, VENTANA_FIRMA_S, encargoSignMessage, entregaSignMessage, ofertaSignMessage, } from './tablon.js';
51
51
  export type { EncargoDelTablon } from './tablon.js';
52
52
  export { fichaEnIdioma, IDIOMAS, NOMBRE_IDIOMA, normalizarIdioma } from './idiomas.js';
@@ -54,5 +54,6 @@ export type { Idioma } from './idiomas.js';
54
54
  export { MERCADO_PANAL, PRECIO_MAX_EDAD_S, PYTH_MONAD, PYTH_MON_USD, enDolares, esFresco, formatearDolares, leerPrecios, valorDeVenta, } from './precios.js';
55
55
  export type { FuentePrecio, MonedaConPrecio, OpcionesPrecios, Precio, Precios } from './precios.js';
56
56
  export { panalTablonAbi } from './tablon-abi.js';
57
- export { CATEGORIAS_TABLON, EstadoTablon, MARGEN_ASIGNACION_S, MAX_ANUNCIO, MAX_CATEGORIAS, MAX_TEXTO_ANUNCIO, MAX_OFERTAS, MAX_VENTANA_ELECCION_S, MIN_PLAZO_ENTREGA_S, ModoTablon, anuncioPara, audienciaAdmite, categoriaDelTablon, categoriasDeMascara, claveDeCampana, encargoDeLaCadena, idDeCampana, leerAnuncio, ofertaDeLaCadena, problemaAlPublicar, situacionDe, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
58
- export type { AudienciaTablon, CategoriaTablon, EncargoConReglas, OfertaConReglas, PublicarEnElTablon, SituacionEnElTablon, } from './tablon-reglas.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, tablonAccesoSignMessage, tablonArchivoSignMessage, tablonDisputaSignMessage, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
58
+ export type { ArchivoParaSubir, AudienciaTablon, CategoriaTablon, EncargoConReglas, OfertaConReglas, PublicarEnElTablon, SituacionEnElTablon, } from './tablon-reglas.js';
59
+ 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
@@ -54,7 +54,7 @@ export { componerEjemplo, conEjemplosDeLaFicha, EJEMPLOS_EDITABLES, esTokenDeEje
54
54
  // propio agente y no un servicio de Panal.
55
55
  // Unir una ruta con la URL de un agente. Ver por qué en `net.ts`: un agente
56
56
  // puede vivir en un subcamino, y `new URL('/x', base)` se lo come.
57
- export { rutaDeAgente } from './net.js';
57
+ export { rutaDeAgente, sinBarraFinal } from './net.js';
58
58
  // El tablón: encargos sin dueño para que los coja un programa. Los mensajes de
59
59
  // firma se exportan porque cualquiera que hable con el buzón sin este cliente
60
60
  // —otro lenguaje, otro runtime— necesita producir exactamente estos bytes.
@@ -67,4 +67,7 @@ export { MERCADO_PANAL, PRECIO_MAX_EDAD_S, PYTH_MONAD, PYTH_MON_USD, enDolares,
67
67
  // el cliente (`…BoardJob`); aquí, el ABI, los tipos y lo que se calcula sin la
68
68
  // cadena. Ver tablon-reglas.ts y contracts/BOARD.md.
69
69
  export { panalTablonAbi } from './tablon-abi.js';
70
- export { CATEGORIAS_TABLON, EstadoTablon, MARGEN_ASIGNACION_S, MAX_ANUNCIO, MAX_CATEGORIAS, MAX_TEXTO_ANUNCIO, MAX_OFERTAS, MAX_VENTANA_ELECCION_S, MIN_PLAZO_ENTREGA_S, ModoTablon, anuncioPara, audienciaAdmite, categoriaDelTablon, categoriasDeMascara, claveDeCampana, encargoDeLaCadena, idDeCampana, leerAnuncio, ofertaDeLaCadena, problemaAlPublicar, situacionDe, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.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, tablonAccesoSignMessage, tablonArchivoSignMessage, tablonDisputaSignMessage, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
71
+ // Encargos cifrados de punta a punta: la clave sale de una firma, el sobre se
72
+ // abre solo con la de sus destinatarios, y el buzón guarda lo que no sabe leer.
73
+ 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
  }
@@ -185,6 +185,19 @@ export interface PublicarEnElTablon {
185
185
  id: Hex;
186
186
  topePorAgente: number;
187
187
  };
188
+ /**
189
+ * Archivos que acompañan al encargo. Su hash se anuncia dentro del encargo
190
+ * antes de publicar, así que el `taskHash` los cubre; los bytes se suben al
191
+ * buzón del tablón después.
192
+ */
193
+ adjuntos?: ArchivoParaSubir[];
194
+ /**
195
+ * Confidencial: el encargo y sus adjuntos se cifran para quien publica, y al
196
+ * elegir una oferta se le da acceso solo a quien se elige (que tiene que
197
+ * tener su clave publicada). El anuncio sigue siendo público. Solo en
198
+ * selección: en inmediato lo cogería cualquiera sin que nadie le diera acceso.
199
+ */
200
+ confidencial?: boolean;
188
201
  }
189
202
  /**
190
203
  * Las comprobaciones de `post()` antes de firmar, con el motivo en claro.
@@ -201,3 +214,32 @@ export declare function tablonEncargoSignMessage(jobId: bigint, expira: number):
201
214
  export declare function tablonEntregaSignMessage(jobId: bigint, resultHash: Hex, expira: number): string;
202
215
  /** Su cliente se lleva la entrega. */
203
216
  export declare function tablonResultadoSignMessage(jobId: bigint, expira: number): string;
217
+ /** Su cliente sube un adjunto del encargo, o su agente un archivo de la entrega. */
218
+ export declare function tablonArchivoSignMessage(jobId: bigint, expira: number): string;
219
+ /**
220
+ * Una parte da acceso a lo cifrado de su encargo: a quien lo cogió o a quien
221
+ * decide una disputa (`concederAcceso`, en cifrado.ts).
222
+ */
223
+ export declare function tablonAccesoSignMessage(jobId: bigint, expira: number): string;
224
+ /** Un firmante de la multisig lee una disputa: el encargo, la entrega y sus archivos. */
225
+ export declare function tablonDisputaSignMessage(jobId: bigint, expira: number): string;
226
+ export declare const MAX_ARCHIVOS_TABLON = 5;
227
+ export declare const MAX_BYTES_ARCHIVOS_TABLON: number;
228
+ /** Un archivo para subir al tablón: su nombre, sus bytes y su tipo si se sabe. */
229
+ export interface ArchivoParaSubir {
230
+ name: string;
231
+ bytes: Uint8Array;
232
+ mime?: string;
233
+ }
234
+ /**
235
+ * El texto de una entrega del tablón con sus archivos anunciados: nombre,
236
+ * tamaño, tipo y hash de cada uno, dentro de lo que se ancla. La ruta es
237
+ * relativa al buzón del tablón (`${buzonUrl}/${tablon}`).
238
+ *
239
+ * UNA sola función para todos —SDK, web, app y plantilla— porque este texto se
240
+ * hashea: dos versiones que difieran en un espacio darían dos hashes, y el
241
+ * cliente vería una entrega que no cuadra con la anclada.
242
+ */
243
+ export declare function entregaConArchivos(jobId: bigint, texto: string, archivos: ArchivoParaSubir[]): string;
244
+ /** Por qué estos archivos no caben en un lado del encargo, o `null`. */
245
+ export declare function problemaConArchivos(archivos: ArchivoParaSubir[]): string | null;
@@ -18,6 +18,7 @@
18
18
  * rechazaría. Lo que lee y firma está en client.ts.
19
19
  */
20
20
  import { encodeAbiParameters, keccak256, toBytes } from 'viem';
21
+ import { MAX_FILE_BYTES, appendFilesManifest, sanitizeFileName } from './files.js';
21
22
  /** Cómo se asigna un encargo. */
22
23
  export var ModoTablon;
23
24
  (function (ModoTablon) {
@@ -212,6 +213,9 @@ export function problemaAlPublicar(p) {
212
213
  if (leerAnuncio(p.anuncio).audiencia === 'personas' && p.modo !== ModoTablon.Seleccion) {
213
214
  return 'Un encargo solo para personas se publica en selección: así eliges tú a la persona.';
214
215
  }
216
+ if (p.confidencial && p.modo !== ModoTablon.Seleccion) {
217
+ return 'Un encargo confidencial se publica en selección: a quien lo coge se le da acceso al elegirlo.';
218
+ }
215
219
  if (p.modo === ModoTablon.Seleccion) {
216
220
  const ofertas = p.ventanaOfertasS ?? 0;
217
221
  const eleccion = p.ventanaEleccionS ?? 0;
@@ -222,6 +226,11 @@ export function problemaAlPublicar(p) {
222
226
  if (p.caducaEnS < ofertas + eleccion)
223
227
  return 'Caduca antes de que acabe el tiempo de elegir.';
224
228
  }
229
+ if (p.adjuntos?.length) {
230
+ const archivos = problemaConArchivos(p.adjuntos);
231
+ if (archivos)
232
+ return archivos;
233
+ }
225
234
  if (p.campana && (p.campana.topePorAgente < 1 || p.campana.topePorAgente > 255)) {
226
235
  return 'El tope de una campaña es de 1 a 255 encargos por agente.';
227
236
  }
@@ -251,3 +260,80 @@ export function tablonEntregaSignMessage(jobId, resultHash, expira) {
251
260
  export function tablonResultadoSignMessage(jobId, expira) {
252
261
  return `Panal tablón con reglas · resultado #${jobId.toString()} · ${expira}`;
253
262
  }
263
+ /** Su cliente sube un adjunto del encargo, o su agente un archivo de la entrega. */
264
+ export function tablonArchivoSignMessage(jobId, expira) {
265
+ return `Panal tablón con reglas · archivo #${jobId.toString()} · ${expira}`;
266
+ }
267
+ /**
268
+ * Una parte da acceso a lo cifrado de su encargo: a quien lo cogió o a quien
269
+ * decide una disputa (`concederAcceso`, en cifrado.ts).
270
+ */
271
+ export function tablonAccesoSignMessage(jobId, expira) {
272
+ return `Panal tablón con reglas · acceso #${jobId.toString()} · ${expira}`;
273
+ }
274
+ /** Un firmante de la multisig lee una disputa: el encargo, la entrega y sus archivos. */
275
+ export function tablonDisputaSignMessage(jobId, expira) {
276
+ return `Panal tablón con reglas · disputa #${jobId.toString()} · ${expira}`;
277
+ }
278
+ /*
279
+ * Archivos en el tablón.
280
+ *
281
+ * Los mismos que en un encargo normal: el hash de cada archivo va DENTRO del
282
+ * texto que se ancla —el encargo, en `[panal-attach/1]`; la entrega, en
283
+ * `[panal-files/1]`—, así que la cadena cubre los bytes y nadie los cambia
284
+ * después. Los bytes viajan por el buzón del tablón, que solo los da al cliente
285
+ * y a su agente.
286
+ *
287
+ * Los topes son POR LADO. El buzón guarda 10 archivos y 60 MB por encargo entre
288
+ * los dos (bot/src/buzon-store.ts); la mitad para los adjuntos del cliente y la
289
+ * mitad para la entrega, para que adjuntar mucho no deje al agente sin sitio.
290
+ */
291
+ export const MAX_ARCHIVOS_TABLON = 5;
292
+ export const MAX_BYTES_ARCHIVOS_TABLON = 30 * 1024 * 1024;
293
+ /**
294
+ * El texto de una entrega del tablón con sus archivos anunciados: nombre,
295
+ * tamaño, tipo y hash de cada uno, dentro de lo que se ancla. La ruta es
296
+ * relativa al buzón del tablón (`${buzonUrl}/${tablon}`).
297
+ *
298
+ * UNA sola función para todos —SDK, web, app y plantilla— porque este texto se
299
+ * hashea: dos versiones que difieran en un espacio darían dos hashes, y el
300
+ * cliente vería una entrega que no cuadra con la anclada.
301
+ */
302
+ export function entregaConArchivos(jobId, texto, archivos) {
303
+ const entregados = archivos.map((a) => {
304
+ const nombre = sanitizeFileName(a.name);
305
+ return {
306
+ name: nombre,
307
+ size: a.bytes.byteLength,
308
+ hash: keccak256(a.bytes),
309
+ ...(a.mime ? { mime: a.mime } : {}),
310
+ path: `/archivo/${jobId.toString()}/${encodeURIComponent(nombre)}?de=entrega`,
311
+ };
312
+ });
313
+ return appendFilesManifest(texto, entregados);
314
+ }
315
+ /** Por qué estos archivos no caben en un lado del encargo, o `null`. */
316
+ export function problemaConArchivos(archivos) {
317
+ if (archivos.length > MAX_ARCHIVOS_TABLON)
318
+ return `Como mucho ${MAX_ARCHIVOS_TABLON} archivos.`;
319
+ const nombres = [];
320
+ for (const a of archivos) {
321
+ try {
322
+ nombres.push(sanitizeFileName(a.name));
323
+ }
324
+ catch {
325
+ return `«${a.name}» no es un nombre de archivo que se pueda usar.`;
326
+ }
327
+ if (a.bytes.byteLength === 0)
328
+ return `«${a.name}» está vacío.`;
329
+ if (a.bytes.byteLength > MAX_FILE_BYTES)
330
+ return `«${a.name}» pasa de ${MAX_FILE_BYTES / 1024 / 1024} MB.`;
331
+ }
332
+ // Se piden por nombre: dos con el mismo serían uno.
333
+ if (new Set(nombres).size !== nombres.length)
334
+ return 'Hay dos archivos con el mismo nombre.';
335
+ const total = archivos.reduce((n, a) => n + a.bytes.byteLength, 0);
336
+ if (total > MAX_BYTES_ARCHIVOS_TABLON)
337
+ return `Entre todos pasan de ${MAX_BYTES_ARCHIVOS_TABLON / 1024 / 1024} MB.`;
338
+ return null;
339
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panal/sdk",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "SDK de Panal: contrata agentes de IA autonomos on-chain en Monad",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,6 +34,11 @@
34
34
  "engines": {
35
35
  "node": ">=20"
36
36
  },
37
+ "dependencies": {
38
+ "@noble/ciphers": "^1.3.0",
39
+ "@noble/curves": "^1.9.1",
40
+ "@noble/hashes": "^1.8.0"
41
+ },
37
42
  "peerDependencies": {
38
43
  "viem": "^2.21.0"
39
44
  },
@@ -45,7 +50,7 @@
45
50
  "scripts": {
46
51
  "build": "tsc -p tsconfig.json",
47
52
  "typecheck": "tsc -p tsconfig.json --noEmit",
48
- "test": "tsx test/sdk.test.ts && tsx test/currencies.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts && tsx test/attachments.test.ts && tsx test/llm.test.ts && tsx test/skill.test.ts && tsx test/agent-card.test.ts && tsx test/niveles.test.ts && tsx test/ejemplos.test.ts && tsx test/tablon.test.ts && tsx test/reserva.test.ts && tsx test/precios.test.ts && tsx test/tablon-reglas.test.ts",
53
+ "test": "tsx test/sdk.test.ts && tsx test/currencies.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts && tsx test/attachments.test.ts && tsx test/llm.test.ts && tsx test/skill.test.ts && tsx test/agent-card.test.ts && tsx test/niveles.test.ts && tsx test/ejemplos.test.ts && tsx test/tablon.test.ts && tsx test/reserva.test.ts && tsx test/precios.test.ts && tsx test/tablon-reglas.test.ts && tsx test/cifrado.test.ts",
49
54
  "test:nombres": "tsx test/nombres.test.ts"
50
55
  }
51
56
  }