@panal/sdk 0.24.0 → 0.25.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 CHANGED
@@ -272,6 +272,32 @@ encargo después de publicar (sin firma: solo entran si dan el `advertHash` y el
272
272
  `readBoardJobResult` se la da al cliente de verdad —en el escrow, el cliente de
273
273
  estas tareas es el contrato, que no puede firmar—.
274
274
 
275
+ **Con archivos, en los dos sentidos.** Igual que en un encargo normal, el hash
276
+ de cada archivo va DENTRO del texto que se ancla, así que la cadena cubre los
277
+ bytes; los bytes viajan por el buzón del tablón, que solo los da al cliente y a
278
+ su agente:
279
+
280
+ ```ts
281
+ // Cliente: sus hashes entran en el encargo; los bytes, al buzón después.
282
+ await panal.postBoardJob({ ...encargo, adjuntos: [{ name: 'ventas.csv', bytes, mime: 'text/csv' }] });
283
+
284
+ // Agente: lee el encargo, baja los adjuntos (comprobados) y entrega con archivos.
285
+ const brief = await panal.readBoardJobBrief(jobId);
286
+ const [csv] = await panal.downloadBoardJobFiles(jobId, parseAttachmentsManifest(brief), 'adjunto');
287
+ await panal.deliverBoardJob(jobId, 'Hecho.', [{ name: 'informe.pdf', bytes: pdf, mime: 'application/pdf' }]);
288
+
289
+ // Cliente: la entrega y sus archivos, comprobados contra el hash anclado.
290
+ const entrega = await panal.readBoardJobResult(jobId);
291
+ const [informe] = await panal.downloadBoardJobFiles(jobId, parseFilesManifest(entrega), 'entrega');
292
+ ```
293
+
294
+ Hasta 5 archivos y 30 MB por lado (25 MB cada uno): el buzón guarda 10 y 60 MB
295
+ por encargo entre los dos, y cada lado tiene su mitad (`problemaConArchivos`
296
+ lo dice antes de firmar). `postBoardJob` pregunta antes de cobrar si el buzón
297
+ ya acepta archivos (`buzonAceptaArchivos`), y `entregaConArchivos` compone el
298
+ texto de una entrega exactamente igual que `deliverBoardJob`, para quien tenga
299
+ que guardarlo antes de entregar.
300
+
275
301
  ### Cuánto es en dólares
276
302
 
277
303
  ```ts
package/dist/client.d.ts CHANGED
@@ -16,11 +16,12 @@
16
16
  * y en uso: el caso de "quiero probar esto ahora" no debería exigir un .env.
17
17
  */
18
18
  import type { Account, Address, Hex, PublicClient, WalletClient } from 'viem';
19
+ import { type HashedFile } from './files.js';
19
20
  import { type AskResult, type X402Accept } from './x402.js';
20
21
  import { type CallEnvelope } from './envelope.js';
21
22
  import { type PanalAddresses, type PanalNetwork } from './chains.js';
22
23
  import { type OpcionesPrecios, type Precios } from './precios.js';
23
- import { EstadoTablon, type EncargoConReglas, type OfertaConReglas, type PublicarEnElTablon, type SituacionEnElTablon } from './tablon-reglas.js';
24
+ import { EstadoTablon, type ArchivoParaSubir, type EncargoConReglas, type OfertaConReglas, type PublicarEnElTablon, type SituacionEnElTablon } from './tablon-reglas.js';
24
25
  import { type EncargoDelTablon } from './tablon.js';
25
26
  import { TaskStatus, type Agent, type AgentMetadata, type Task } from './types.js';
26
27
  export interface PanalClientOptions {
@@ -537,9 +538,34 @@ export declare class PanalClient {
537
538
  txHash: Hex;
538
539
  taskHash: Hex;
539
540
  advertHash: Hex;
541
+ /** El encargo tal como se hasheó, con los adjuntos anunciados: para reintentar. */
542
+ encargo: string;
540
543
  textosGuardados: boolean;
544
+ /** Si los adjuntos están en el buzón. Sin adjuntos, `true`. */
545
+ archivosGuardados: boolean;
541
546
  error?: string;
542
547
  }>;
548
+ /**
549
+ * Si el buzón del tablón ya guarda archivos: el que conoce la ruta contesta
550
+ * 400 a un POST sin credenciales; uno anterior, 404.
551
+ */
552
+ buzonAceptaArchivos(): Promise<boolean>;
553
+ /**
554
+ * Sube archivos al buzón del tablón, con una firma para todos. Su cliente,
555
+ * los adjuntos de un encargo ya publicado (solo entran los que el encargo
556
+ * anuncia); su agente, los de la entrega (lo que hace `deliverBoardJob`, o
557
+ * para volver a subirlos al retomar). Repetirlo no ocupa más.
558
+ */
559
+ publishBoardJobFiles(jobId: bigint, archivos: ArchivoParaSubir[]): Promise<void>;
560
+ private subirArchivoAlTablon;
561
+ /**
562
+ * Los archivos de un encargo del tablón, comprobados byte a byte contra su
563
+ * hash. `adjunto`: los que el cliente mandó con el encargo (los bajan él y
564
+ * su agente). `entrega`: los que entregó el agente (los baja su cliente).
565
+ * El hash viene del texto anclado —`readBoardJobBrief` o
566
+ * `readBoardJobResult`—, nunca del buzón. Una firma para todos.
567
+ */
568
+ downloadBoardJobFiles(jobId: bigint, archivos: HashedFile[], de: 'adjunto' | 'entrega'): Promise<Uint8Array[]>;
543
569
  /**
544
570
  * Deja en el buzón el anuncio y el encargo de un encargo ya publicado. Sin
545
571
  * firma: el buzón solo acepta los textos que dan los hashes de la cadena.
@@ -570,9 +596,10 @@ export declare class PanalClient {
570
596
  * se ancla su hash en el escrow. Al revés, un fallo del buzón dejaría al
571
597
  * cliente con una entrega anclada que no puede descargar.
572
598
  */
573
- deliverBoardJob(jobId: bigint, texto: string): Promise<{
599
+ deliverBoardJob(jobId: bigint, textoEscrito: string, archivos?: ArchivoParaSubir[]): Promise<{
574
600
  txHash: Hex;
575
601
  resultHash: Hex;
602
+ texto: string;
576
603
  }>;
577
604
  /**
578
605
  * La entrega, para su cliente. Se comprueba contra el `resultHash` anclado
package/dist/client.js CHANGED
@@ -18,13 +18,14 @@
18
18
  import { createPublicClient, createWalletClient, formatEther, getAddress, http, keccak256, parseEventLogs, toBytes, verifyMessage } from 'viem';
19
19
  import { erc20Abi, escrowAbi, namesAbi, registryAbi } from './abis.js';
20
20
  import { leerX402 } from './agent-card.js';
21
- import { assertPublicUrl, fetchLimited, rutaDeAgente } from './net.js';
21
+ import { assertPublicUrl, fetchBytesLimited, fetchLimited, rutaDeAgente } from './net.js';
22
+ import { MAX_FILE_BYTES, appendAttachmentsManifest, attachmentFrom, sanitizeFileName, verifyFileBytes, } from './files.js';
22
23
  import { X402Error, payAndAsk, quoteAsk } from './x402.js';
23
24
  import { descend, newEnvelope, remainingBudget } from './envelope.js';
24
25
  import { NATIVE_CURRENCY, addressesFor, chainFor } from './chains.js';
25
26
  import { leerPrecios, valorDeVenta } from './precios.js';
26
27
  import { panalTablonAbi } from './tablon-abi.js';
27
- import { EstadoTablon, MAX_CATEGORIAS, ModoTablon, encargoDeLaCadena, ofertaDeLaCadena, problemaAlPublicar, situacionDe, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
28
+ import { EstadoTablon, MAX_CATEGORIAS, ModoTablon, encargoDeLaCadena, entregaConArchivos, ofertaDeLaCadena, problemaAlPublicar, problemaConArchivos, situacionDe, tablonArchivoSignMessage, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
28
29
  import { BUZON_URL, TABLON, VENTANA_FIRMA_S, encargoSignMessage, entregaSignMessage, ofertaSignMessage, } from './tablon.js';
29
30
  import { TaskStatus, formatAgentMetadata, parseAgentMetadata, } from './types.js';
30
31
  /**
@@ -1278,7 +1279,16 @@ export class PanalClient {
1278
1279
  const moneda = p.moneda ?? NATIVE_CURRENCY;
1279
1280
  const nativa = moneda.toLowerCase() === NATIVE_CURRENCY.toLowerCase();
1280
1281
  await this.assertFunds(moneda, p.importe, nativa);
1281
- const taskHash = keccak256(toBytes(p.encargo));
1282
+ // Los adjuntos se anuncian DENTRO del encargo, con su hash: el `taskHash`
1283
+ // que va a la cadena los cubre desde el momento del pago.
1284
+ const adjuntos = p.adjuntos ?? [];
1285
+ // Antes de cobrar: un buzón anterior a los archivos aceptaría el encargo
1286
+ // y no sus bytes, y el agente cobraría por algo que no puede abrir.
1287
+ if (adjuntos.length > 0 && !(await this.buzonAceptaArchivos())) {
1288
+ throw new Error('El buzón del tablón todavía no acepta archivos: no se ha publicado nada. Publica sin adjuntos o inténtalo más tarde.');
1289
+ }
1290
+ const encargo = appendAttachmentsManifest(p.encargo, adjuntos.map((a) => attachmentFrom(a.name, a.bytes, a.mime)));
1291
+ const taskHash = keccak256(toBytes(encargo));
1282
1292
  const advertHash = keccak256(toBytes(p.anuncio));
1283
1293
  const seleccion = p.modo === ModoTablon.Seleccion;
1284
1294
  const params = {
@@ -1307,15 +1317,97 @@ export class PanalClient {
1307
1317
  // Los textos van DESPUÉS de la cadena: hasta que el encargo existe, el buzón
1308
1318
  // no tiene hash con qué comprobarlos. Si fallan, el encargo está pagado y
1309
1319
  // sin anunciar: se dice y se reintenta con `publishBoardJobTexts`.
1320
+ const base = { jobId, txHash: hash, taskHash, advertHash, encargo };
1321
+ try {
1322
+ await this.publishBoardJobTexts(jobId, { anuncio: p.anuncio, encargo });
1323
+ }
1324
+ catch (err) {
1325
+ const error = err instanceof Error ? err.message : String(err);
1326
+ return { ...base, textosGuardados: false, archivosGuardados: adjuntos.length === 0, error };
1327
+ }
1328
+ // Los bytes, después del encargo: el buzón solo acepta los que anuncia.
1310
1329
  try {
1311
- await this.publishBoardJobTexts(jobId, { anuncio: p.anuncio, encargo: p.encargo });
1312
- return { jobId, txHash: hash, taskHash, advertHash, textosGuardados: true };
1330
+ await this.publishBoardJobFiles(jobId, adjuntos);
1331
+ return { ...base, textosGuardados: true, archivosGuardados: true };
1313
1332
  }
1314
1333
  catch (err) {
1315
1334
  const error = err instanceof Error ? err.message : String(err);
1316
- return { jobId, txHash: hash, taskHash, advertHash, textosGuardados: false, error };
1335
+ return { ...base, textosGuardados: true, archivosGuardados: false, error };
1336
+ }
1337
+ }
1338
+ /**
1339
+ * Si el buzón del tablón ya guarda archivos: el que conoce la ruta contesta
1340
+ * 400 a un POST sin credenciales; uno anterior, 404.
1341
+ */
1342
+ async buzonAceptaArchivos() {
1343
+ try {
1344
+ const { status } = await fetchLimited(`${this.buzonUrl}/${this.tablon()}/archivo/0`, { method: 'POST', timeoutMs: 10_000, maxBytes: 10_000 });
1345
+ return status !== 404;
1346
+ }
1347
+ catch {
1348
+ return false;
1317
1349
  }
1318
1350
  }
1351
+ /**
1352
+ * Sube archivos al buzón del tablón, con una firma para todos. Su cliente,
1353
+ * los adjuntos de un encargo ya publicado (solo entran los que el encargo
1354
+ * anuncia); su agente, los de la entrega (lo que hace `deliverBoardJob`, o
1355
+ * para volver a subirlos al retomar). Repetirlo no ocupa más.
1356
+ */
1357
+ async publishBoardJobFiles(jobId, archivos) {
1358
+ if (archivos.length === 0)
1359
+ return;
1360
+ const cabeceras = await this.credencialesDeBuzon((e) => tablonArchivoSignMessage(jobId, e));
1361
+ for (const a of archivos)
1362
+ await this.subirArchivoAlTablon(jobId, a, cabeceras);
1363
+ }
1364
+ async subirArchivoAlTablon(jobId, a, cabeceras) {
1365
+ const nombre = sanitizeFileName(a.name);
1366
+ const { status, text } = await fetchLimited(`${this.buzonUrl}/${this.tablon()}/archivo/${jobId}`, {
1367
+ method: 'POST',
1368
+ headers: {
1369
+ ...cabeceras,
1370
+ 'content-type': a.mime || 'application/octet-stream',
1371
+ 'x-panal-filename': encodeURIComponent(nombre),
1372
+ },
1373
+ body: a.bytes,
1374
+ timeoutMs: 180_000,
1375
+ maxBytes: 10_000,
1376
+ });
1377
+ if (status !== 200) {
1378
+ throw new Error(`El buzón no aceptó «${nombre}» del #${jobId} (${status}: ${text.slice(0, 160)}).`);
1379
+ }
1380
+ }
1381
+ /**
1382
+ * Los archivos de un encargo del tablón, comprobados byte a byte contra su
1383
+ * hash. `adjunto`: los que el cliente mandó con el encargo (los bajan él y
1384
+ * su agente). `entrega`: los que entregó el agente (los baja su cliente).
1385
+ * El hash viene del texto anclado —`readBoardJobBrief` o
1386
+ * `readBoardJobResult`—, nunca del buzón. Una firma para todos.
1387
+ */
1388
+ async downloadBoardJobFiles(jobId, archivos, de) {
1389
+ if (archivos.length === 0)
1390
+ return [];
1391
+ const job = await this.getBoardJob(jobId);
1392
+ const yo = this.wallet() && this.account.address.toLowerCase();
1393
+ const esCliente = yo === job.cliente.toLowerCase();
1394
+ if (!esCliente && yo !== job.agente.toLowerCase()) {
1395
+ throw new Error(`Los archivos del #${jobId} solo los bajan su cliente y su agente.`);
1396
+ }
1397
+ // El cliente abre la entrega con la firma de llevársela; lo demás, con la de leer el encargo.
1398
+ const cabeceras = await this.credencialesDeBuzon((e) => de === 'entrega' && esCliente ? tablonResultadoSignMessage(jobId, e) : tablonEncargoSignMessage(jobId, e));
1399
+ const out = [];
1400
+ for (const f of archivos) {
1401
+ const { status, bytes } = await fetchBytesLimited(`${this.buzonUrl}/${this.tablon()}/archivo/${jobId}/${encodeURIComponent(sanitizeFileName(f.name))}?de=${de}`, { headers: cabeceras, timeoutMs: 180_000, maxBytes: Math.min(MAX_FILE_BYTES, f.size + 1024) });
1402
+ if (status === 404)
1403
+ throw new Error(`«${f.name}» no está en el buzón del tablón.`);
1404
+ if (status !== 200)
1405
+ throw new Error(`El buzón respondió ${status} al pedir «${f.name}».`);
1406
+ verifyFileBytes(f, bytes);
1407
+ out.push(bytes);
1408
+ }
1409
+ return out;
1410
+ }
1319
1411
  /**
1320
1412
  * Deja en el buzón el anuncio y el encargo de un encargo ya publicado. Sin
1321
1413
  * firma: el buzón solo acepta los textos que dan los hashes de la cadena.
@@ -1397,7 +1489,7 @@ export class PanalClient {
1397
1489
  * se ancla su hash en el escrow. Al revés, un fallo del buzón dejaría al
1398
1490
  * cliente con una entrega anclada que no puede descargar.
1399
1491
  */
1400
- async deliverBoardJob(jobId, texto) {
1492
+ async deliverBoardJob(jobId, textoEscrito, archivos = []) {
1401
1493
  this.wallet();
1402
1494
  const job = await this.getBoardJob(jobId);
1403
1495
  if (job.agente.toLowerCase() !== this.account.address.toLowerCase()) {
@@ -1405,6 +1497,18 @@ export class PanalClient {
1405
1497
  }
1406
1498
  if (job.estado !== EstadoTablon.Asignado)
1407
1499
  throw new Error(`El encargo #${jobId} no está asignado: no hay nada que entregar.`);
1500
+ // Los archivos primero, y su hash dentro del texto que se ancla: si el
1501
+ // buzón no los acepta, no se ancla nada.
1502
+ let texto = textoEscrito;
1503
+ if (archivos.length > 0) {
1504
+ const problema = problemaConArchivos(archivos);
1505
+ if (problema)
1506
+ throw new Error(problema);
1507
+ const cabecerasArchivos = await this.credencialesDeBuzon((e) => tablonArchivoSignMessage(jobId, e));
1508
+ for (const a of archivos)
1509
+ await this.subirArchivoAlTablon(jobId, a, cabecerasArchivos);
1510
+ texto = entregaConArchivos(jobId, textoEscrito, archivos);
1511
+ }
1408
1512
  const hashEntrega = keccak256(toBytes(texto));
1409
1513
  const cabeceras = await this.credencialesDeBuzon((e) => tablonEntregaSignMessage(jobId, hashEntrega, e));
1410
1514
  const { status, text } = await fetchLimited(`${this.buzonUrl}/${this.tablon()}/entrega/${jobId}`, {
@@ -1422,7 +1526,8 @@ export class PanalClient {
1422
1526
  if (status !== 200) {
1423
1527
  throw new Error(`El buzón no aceptó la entrega del #${jobId} (${status}: ${text.slice(0, 160)}). No se ha anclado nada.`);
1424
1528
  }
1425
- return this.deliverResult(job.escrowTaskId, texto);
1529
+ const anclada = await this.deliverResult(job.escrowTaskId, texto);
1530
+ return { ...anclada, texto };
1426
1531
  }
1427
1532
  /**
1428
1533
  * La entrega, para su cliente. Se comprueba contra el `resultHash` anclado
package/dist/index.d.ts CHANGED
@@ -54,5 +54,5 @@ 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, tablonArchivoSignMessage, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
58
+ export type { ArchivoParaSubir, AudienciaTablon, CategoriaTablon, EncargoConReglas, OfertaConReglas, PublicarEnElTablon, SituacionEnElTablon, } from './tablon-reglas.js';
package/dist/index.js CHANGED
@@ -67,4 +67,4 @@ 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, tablonArchivoSignMessage, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
@@ -185,6 +185,12 @@ 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[];
188
194
  }
189
195
  /**
190
196
  * Las comprobaciones de `post()` antes de firmar, con el motivo en claro.
@@ -201,3 +207,25 @@ export declare function tablonEncargoSignMessage(jobId: bigint, expira: number):
201
207
  export declare function tablonEntregaSignMessage(jobId: bigint, resultHash: Hex, expira: number): string;
202
208
  /** Su cliente se lleva la entrega. */
203
209
  export declare function tablonResultadoSignMessage(jobId: bigint, expira: number): string;
210
+ /** Su cliente sube un adjunto del encargo, o su agente un archivo de la entrega. */
211
+ export declare function tablonArchivoSignMessage(jobId: bigint, expira: number): string;
212
+ export declare const MAX_ARCHIVOS_TABLON = 5;
213
+ export declare const MAX_BYTES_ARCHIVOS_TABLON: number;
214
+ /** Un archivo para subir al tablón: su nombre, sus bytes y su tipo si se sabe. */
215
+ export interface ArchivoParaSubir {
216
+ name: string;
217
+ bytes: Uint8Array;
218
+ mime?: string;
219
+ }
220
+ /**
221
+ * El texto de una entrega del tablón con sus archivos anunciados: nombre,
222
+ * tamaño, tipo y hash de cada uno, dentro de lo que se ancla. La ruta es
223
+ * relativa al buzón del tablón (`${buzonUrl}/${tablon}`).
224
+ *
225
+ * UNA sola función para todos —SDK, web, app y plantilla— porque este texto se
226
+ * hashea: dos versiones que difieran en un espacio darían dos hashes, y el
227
+ * cliente vería una entrega que no cuadra con la anclada.
228
+ */
229
+ export declare function entregaConArchivos(jobId: bigint, texto: string, archivos: ArchivoParaSubir[]): string;
230
+ /** Por qué estos archivos no caben en un lado del encargo, o `null`. */
231
+ 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) {
@@ -222,6 +223,11 @@ export function problemaAlPublicar(p) {
222
223
  if (p.caducaEnS < ofertas + eleccion)
223
224
  return 'Caduca antes de que acabe el tiempo de elegir.';
224
225
  }
226
+ if (p.adjuntos?.length) {
227
+ const archivos = problemaConArchivos(p.adjuntos);
228
+ if (archivos)
229
+ return archivos;
230
+ }
225
231
  if (p.campana && (p.campana.topePorAgente < 1 || p.campana.topePorAgente > 255)) {
226
232
  return 'El tope de una campaña es de 1 a 255 encargos por agente.';
227
233
  }
@@ -251,3 +257,69 @@ export function tablonEntregaSignMessage(jobId, resultHash, expira) {
251
257
  export function tablonResultadoSignMessage(jobId, expira) {
252
258
  return `Panal tablón con reglas · resultado #${jobId.toString()} · ${expira}`;
253
259
  }
260
+ /** Su cliente sube un adjunto del encargo, o su agente un archivo de la entrega. */
261
+ export function tablonArchivoSignMessage(jobId, expira) {
262
+ return `Panal tablón con reglas · archivo #${jobId.toString()} · ${expira}`;
263
+ }
264
+ /*
265
+ * Archivos en el tablón.
266
+ *
267
+ * Los mismos que en un encargo normal: el hash de cada archivo va DENTRO del
268
+ * texto que se ancla —el encargo, en `[panal-attach/1]`; la entrega, en
269
+ * `[panal-files/1]`—, así que la cadena cubre los bytes y nadie los cambia
270
+ * después. Los bytes viajan por el buzón del tablón, que solo los da al cliente
271
+ * y a su agente.
272
+ *
273
+ * Los topes son POR LADO. El buzón guarda 10 archivos y 60 MB por encargo entre
274
+ * los dos (bot/src/buzon-store.ts); la mitad para los adjuntos del cliente y la
275
+ * mitad para la entrega, para que adjuntar mucho no deje al agente sin sitio.
276
+ */
277
+ export const MAX_ARCHIVOS_TABLON = 5;
278
+ export const MAX_BYTES_ARCHIVOS_TABLON = 30 * 1024 * 1024;
279
+ /**
280
+ * El texto de una entrega del tablón con sus archivos anunciados: nombre,
281
+ * tamaño, tipo y hash de cada uno, dentro de lo que se ancla. La ruta es
282
+ * relativa al buzón del tablón (`${buzonUrl}/${tablon}`).
283
+ *
284
+ * UNA sola función para todos —SDK, web, app y plantilla— porque este texto se
285
+ * hashea: dos versiones que difieran en un espacio darían dos hashes, y el
286
+ * cliente vería una entrega que no cuadra con la anclada.
287
+ */
288
+ export function entregaConArchivos(jobId, texto, archivos) {
289
+ const entregados = archivos.map((a) => {
290
+ const nombre = sanitizeFileName(a.name);
291
+ return {
292
+ name: nombre,
293
+ size: a.bytes.byteLength,
294
+ hash: keccak256(a.bytes),
295
+ ...(a.mime ? { mime: a.mime } : {}),
296
+ path: `/archivo/${jobId.toString()}/${encodeURIComponent(nombre)}?de=entrega`,
297
+ };
298
+ });
299
+ return appendFilesManifest(texto, entregados);
300
+ }
301
+ /** Por qué estos archivos no caben en un lado del encargo, o `null`. */
302
+ export function problemaConArchivos(archivos) {
303
+ if (archivos.length > MAX_ARCHIVOS_TABLON)
304
+ return `Como mucho ${MAX_ARCHIVOS_TABLON} archivos.`;
305
+ const nombres = [];
306
+ for (const a of archivos) {
307
+ try {
308
+ nombres.push(sanitizeFileName(a.name));
309
+ }
310
+ catch {
311
+ return `«${a.name}» no es un nombre de archivo que se pueda usar.`;
312
+ }
313
+ if (a.bytes.byteLength === 0)
314
+ return `«${a.name}» está vacío.`;
315
+ if (a.bytes.byteLength > MAX_FILE_BYTES)
316
+ return `«${a.name}» pasa de ${MAX_FILE_BYTES / 1024 / 1024} MB.`;
317
+ }
318
+ // Se piden por nombre: dos con el mismo serían uno.
319
+ if (new Set(nombres).size !== nombres.length)
320
+ return 'Hay dos archivos con el mismo nombre.';
321
+ const total = archivos.reduce((n, a) => n + a.bytes.byteLength, 0);
322
+ if (total > MAX_BYTES_ARCHIVOS_TABLON)
323
+ return `Entre todos pasan de ${MAX_BYTES_ARCHIVOS_TABLON / 1024 / 1024} MB.`;
324
+ return null;
325
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panal/sdk",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "SDK de Panal: contrata agentes de IA autonomos on-chain en Monad",
5
5
  "type": "module",
6
6
  "license": "MIT",