@panal/sdk 0.25.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/README.md +49 -0
- package/dist/cifrado.d.ts +147 -0
- package/dist/cifrado.js +333 -0
- package/dist/client.d.ts +174 -2
- package/dist/client.js +451 -33
- package/dist/idiomas.js +2 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +5 -2
- package/dist/net.d.ts +9 -0
- package/dist/net.js +15 -1
- package/dist/tablon-reglas.d.ts +14 -0
- package/dist/tablon-reglas.js +14 -0
- package/package.json +7 -2
package/dist/client.js
CHANGED
|
@@ -15,17 +15,18 @@
|
|
|
15
15
|
* Sin configuración apunta a Monad mainnet, que es donde Panal está desplegado
|
|
16
16
|
* y en uso: el caso de "quiero probar esto ahora" no debería exigir un .env.
|
|
17
17
|
*/
|
|
18
|
-
import { createPublicClient, createWalletClient, formatEther, getAddress, http, keccak256, parseEventLogs, toBytes, verifyMessage } from 'viem';
|
|
18
|
+
import { createPublicClient, createWalletClient, formatEther, getAddress, http, keccak256, parseAbi, 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, fetchBytesLimited, fetchLimited, rutaDeAgente } from './net.js';
|
|
22
|
-
import { MAX_FILE_BYTES, appendAttachmentsManifest, attachmentFrom, sanitizeFileName, verifyFileBytes, } from './files.js';
|
|
21
|
+
import { assertPublicUrl, fetchBytesLimited, fetchLimited, rutaDeAgente, sinBarraFinal } from './net.js';
|
|
22
|
+
import { MAX_FILE_BYTES, appendAttachmentsManifest, attachmentFrom, parseAttachmentsManifest, parseFilesManifest, sanitizeFileName, verifyFileBytes, } from './files.js';
|
|
23
|
+
import { accesoSignMessage, cifrarArchivo, cifrarTexto, claveDesdeFirma, comprobarClavePublicada, concederAcceso, descifrarArchivo, descifrarTexto, esArchivoCifrado, esTextoCifrado, mensajeParaDerivarClave, mensajeParaPublicarClave, nombreEnBuzon, puedeAbrir, } from './cifrado.js';
|
|
23
24
|
import { X402Error, payAndAsk, quoteAsk } from './x402.js';
|
|
24
25
|
import { descend, newEnvelope, remainingBudget } from './envelope.js';
|
|
25
26
|
import { NATIVE_CURRENCY, addressesFor, chainFor } from './chains.js';
|
|
26
27
|
import { leerPrecios, valorDeVenta } from './precios.js';
|
|
27
28
|
import { panalTablonAbi } from './tablon-abi.js';
|
|
28
|
-
import { EstadoTablon, MAX_CATEGORIAS, ModoTablon, encargoDeLaCadena, entregaConArchivos, ofertaDeLaCadena, problemaAlPublicar, problemaConArchivos, situacionDe, tablonArchivoSignMessage, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
|
|
29
|
+
import { EstadoTablon, MAX_CATEGORIAS, ModoTablon, encargoDeLaCadena, entregaConArchivos, ofertaDeLaCadena, problemaAlPublicar, problemaConArchivos, situacionDe, tablonAccesoSignMessage, tablonArchivoSignMessage, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
|
|
29
30
|
import { BUZON_URL, TABLON, VENTANA_FIRMA_S, encargoSignMessage, entregaSignMessage, ofertaSignMessage, } from './tablon.js';
|
|
30
31
|
import { TaskStatus, formatAgentMetadata, parseAgentMetadata, } from './types.js';
|
|
31
32
|
/**
|
|
@@ -134,7 +135,7 @@ export class PanalClient {
|
|
|
134
135
|
'Usa network: "mainnet", o pasa `addresses` con los tuyos.');
|
|
135
136
|
}
|
|
136
137
|
this.indexerUrl = options.indexerUrl === undefined ? 'https://api.panal.lat' : options.indexerUrl;
|
|
137
|
-
this.buzonUrl = (options.buzonUrl ?? BUZON_URL)
|
|
138
|
+
this.buzonUrl = sinBarraFinal(options.buzonUrl ?? BUZON_URL);
|
|
138
139
|
const transport = http(options.rpcUrl ?? chain.rpcUrls.default.http[0]);
|
|
139
140
|
this.publicClient = createPublicClient({ chain, transport });
|
|
140
141
|
this.account = options.account;
|
|
@@ -220,6 +221,220 @@ export class PanalClient {
|
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
// -------------------------------------------------------------------------
|
|
224
|
+
// Cifrado
|
|
225
|
+
// -------------------------------------------------------------------------
|
|
226
|
+
/** La clave de cifrado de esta cuenta, una vez sacada: no se vuelve a firmar. */
|
|
227
|
+
clave;
|
|
228
|
+
/**
|
|
229
|
+
* Si lo de un encargo del tablón va cifrado y con qué accesos se abre, por
|
|
230
|
+
* `jobId:adjunto` o `jobId:entrega`. Se aprende al leer el encargo o la
|
|
231
|
+
* entrega y lo usa `downloadBoardJobFiles`, que sin esto pediría los
|
|
232
|
+
* archivos por su nombre de verdad y el buzón los guarda con uno opaco.
|
|
233
|
+
*/
|
|
234
|
+
cifradoDelTablon = new Map();
|
|
235
|
+
/**
|
|
236
|
+
* La clave de cifrado de esta cuenta. Sale de la firma de un mensaje fijo
|
|
237
|
+
* —la misma cuenta firma siempre lo mismo (RFC 6979)—, así que es la misma
|
|
238
|
+
* que en la web y en la app, y no se guarda en ningún sitio.
|
|
239
|
+
*/
|
|
240
|
+
async encryptionKey() {
|
|
241
|
+
if (this.clave)
|
|
242
|
+
return this.clave;
|
|
243
|
+
const firma = await this.wallet().signMessage({ account: this.account, message: mensajeParaDerivarClave(this.account.address) });
|
|
244
|
+
this.clave = claveDesdeFirma(this.account.address, firma);
|
|
245
|
+
return this.clave;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* La clave pública de una cuenta, comprobada contra la firma que la ata a
|
|
249
|
+
* ella. `null` si no ha publicado ninguna, o si lo que sirve el buzón no es
|
|
250
|
+
* suyo: cifrar para una clave falsa sería entregarle el encargo a quien la puso.
|
|
251
|
+
*/
|
|
252
|
+
async getPublicKey(cuenta) {
|
|
253
|
+
try {
|
|
254
|
+
const { status, text } = await fetchLimited(`${this.buzonUrl}/${getAddress(cuenta)}/clave`, { timeoutMs: 10_000, maxBytes: 10_000 });
|
|
255
|
+
if (status !== 200)
|
|
256
|
+
return null;
|
|
257
|
+
const b = JSON.parse(text);
|
|
258
|
+
if (typeof b.clave !== 'string' || typeof b.firma !== 'string')
|
|
259
|
+
return null;
|
|
260
|
+
const ok = await comprobarClavePublicada({ cuenta, clave: b.clave, firma: b.firma }, this.publicClient);
|
|
261
|
+
return ok ? b.clave : null;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Deja publicada la clave de esta cuenta (dos firmas, sin gas, una vez). Si
|
|
269
|
+
* ya estaba, no firma nada más. Si había OTRA, la sustituye y `cambio` lo
|
|
270
|
+
* dice: lo cifrado para la anterior ya no se abrirá con esta.
|
|
271
|
+
*/
|
|
272
|
+
async publishEncryptionKey() {
|
|
273
|
+
const clave = await this.encryptionKey();
|
|
274
|
+
const yo = this.account.address;
|
|
275
|
+
const publicada = await this.getPublicKey(yo);
|
|
276
|
+
if (publicada === clave.publica)
|
|
277
|
+
return { publica: clave.publica, cambio: false };
|
|
278
|
+
const firma = await this.wallet().signMessage({ account: this.account, message: mensajeParaPublicarClave(yo, clave.publica) });
|
|
279
|
+
const { status, text } = await fetchLimited(`${this.buzonUrl}/${yo}/clave`, {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
headers: { 'content-type': 'application/json' },
|
|
282
|
+
body: JSON.stringify({ clave: clave.publica, firma }),
|
|
283
|
+
timeoutMs: 15_000,
|
|
284
|
+
maxBytes: 10_000,
|
|
285
|
+
});
|
|
286
|
+
if (status !== 200)
|
|
287
|
+
throw new Error(`El buzón no aceptó la clave de cifrado (${status}: ${text.slice(0, 160)}).`);
|
|
288
|
+
return { publica: clave.publica, cambio: publicada !== null };
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Un texto, abierto: tal cual si va en claro; con la clave de esta cuenta y
|
|
292
|
+
* sus `accesos` si va cifrado. Falla diciendo que no hay acceso, sin pedir
|
|
293
|
+
* ninguna firma, si esta cuenta todavía no puede abrirlo.
|
|
294
|
+
*/
|
|
295
|
+
async decryptText(texto, accesos = []) {
|
|
296
|
+
if (!esTextoCifrado(texto))
|
|
297
|
+
return texto;
|
|
298
|
+
this.wallet();
|
|
299
|
+
if (!puedeAbrir(texto, this.account.address, accesos)) {
|
|
300
|
+
throw new Error('Va cifrado y esta cuenta todavía no tiene acceso: nadie se lo ha dado.');
|
|
301
|
+
}
|
|
302
|
+
return descifrarTexto(texto, this.account.address, (await this.encryptionKey()).secreta, accesos);
|
|
303
|
+
}
|
|
304
|
+
/** Los bytes de un archivo, abiertos si van cifrados. */
|
|
305
|
+
async decryptFile(bytes, accesos = []) {
|
|
306
|
+
if (!esArchivoCifrado(bytes))
|
|
307
|
+
return bytes;
|
|
308
|
+
this.wallet();
|
|
309
|
+
return descifrarArchivo(bytes, this.account.address, (await this.encryptionKey()).secreta, accesos);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* El encargo de una tarea DIRECTA, cifrado de punta a punta para quien lo
|
|
313
|
+
* hace y para esta cuenta, que tiene que poder releerlo y recibir la entrega
|
|
314
|
+
* cifrada (por eso publica también su clave, la primera vez).
|
|
315
|
+
*
|
|
316
|
+
* Solo tiene sentido con quien recibe en el buzón de Panal: a un agente con
|
|
317
|
+
* servidor propio el encargo le llega a SU máquina, y cifrárselo sería
|
|
318
|
+
* cifrar para el mismo que lo va a leer. Eso lo decide quien llama.
|
|
319
|
+
*
|
|
320
|
+
* Devuelve `null` si quien lo hace no ha publicado su clave: no hay a quién
|
|
321
|
+
* cifrar. Si no, el texto que se hashea y se manda —con el manifiesto de
|
|
322
|
+
* adjuntos dentro, con su nombre de verdad— y los adjuntos tal como se suben:
|
|
323
|
+
* cifrados y con su nombre opaco, que es lo único que ve el buzón.
|
|
324
|
+
*/
|
|
325
|
+
async encryptBriefFor(worker, texto, adjuntos = []) {
|
|
326
|
+
this.wallet();
|
|
327
|
+
const suya = await this.getPublicKey(worker);
|
|
328
|
+
if (!suya)
|
|
329
|
+
return null;
|
|
330
|
+
const mia = (await this.publishEncryptionKey()).publica;
|
|
331
|
+
const para = [
|
|
332
|
+
{ cuenta: getAddress(worker), clave: suya },
|
|
333
|
+
{ cuenta: this.account.address, clave: mia },
|
|
334
|
+
];
|
|
335
|
+
const cifrados = adjuntos.map((a) => ({ ...a, bytes: cifrarArchivo(a.bytes, para) }));
|
|
336
|
+
const enClaro = appendAttachmentsManifest(texto, cifrados.map((a) => attachmentFrom(a.name, a.bytes, a.mime)));
|
|
337
|
+
return {
|
|
338
|
+
brief: cifrarTexto(enClaro, para),
|
|
339
|
+
archivos: cifrados.map((a) => ({ file: attachmentFrom(nombreEnBuzon(keccak256(a.bytes)), a.bytes), bytes: a.bytes })),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Quiénes deciden las disputas: los firmantes de la multisig que es el
|
|
344
|
+
* `arbitrator` del escrow. Si el árbitro fuera una cuenta y no una
|
|
345
|
+
* multisig, es él.
|
|
346
|
+
*/
|
|
347
|
+
async getArbitrators() {
|
|
348
|
+
const arbitrator = (await this.publicClient.readContract({
|
|
349
|
+
address: this.addresses.escrow,
|
|
350
|
+
abi: parseAbi(['function arbitrator() view returns (address)']),
|
|
351
|
+
functionName: 'arbitrator',
|
|
352
|
+
}));
|
|
353
|
+
const firmantes = [];
|
|
354
|
+
for (let i = 0; i < 10; i++) {
|
|
355
|
+
try {
|
|
356
|
+
firmantes.push((await this.publicClient.readContract({
|
|
357
|
+
address: arbitrator,
|
|
358
|
+
abi: parseAbi(['function owners(uint256) view returns (address)']),
|
|
359
|
+
functionName: 'owners',
|
|
360
|
+
args: [BigInt(i)],
|
|
361
|
+
})));
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return firmantes.length > 0 ? firmantes : [arbitrator];
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Deja accesos en un buzón —el de un agente (`base` = su URL de buzón) o el
|
|
371
|
+
* del tablón— con una firma para todos. El buzón solo los acepta de una
|
|
372
|
+
* parte, y solo para la otra parte o para quien decide; van en trozos de
|
|
373
|
+
* 40, que es lo que admite por petición.
|
|
374
|
+
*/
|
|
375
|
+
async dejarAccesos(base, id, esTablon, accesos) {
|
|
376
|
+
if (accesos.length === 0)
|
|
377
|
+
return;
|
|
378
|
+
const expira = Math.floor(Date.now() / 1000) + VENTANA_FIRMA_S;
|
|
379
|
+
const signature = await this.wallet().signMessage({
|
|
380
|
+
account: this.account,
|
|
381
|
+
message: esTablon ? tablonAccesoSignMessage(id, expira) : accesoSignMessage(id, expira),
|
|
382
|
+
});
|
|
383
|
+
for (let i = 0; i < accesos.length; i += 40) {
|
|
384
|
+
const { status, text } = await fetchLimited(`${sinBarraFinal(base)}/access/${id}`, {
|
|
385
|
+
method: 'POST',
|
|
386
|
+
headers: { 'content-type': 'application/json' },
|
|
387
|
+
body: JSON.stringify({ accesos: accesos.slice(i, i + 40), address: this.account.address, signature, expira }),
|
|
388
|
+
timeoutMs: 15_000,
|
|
389
|
+
maxBytes: 10_000,
|
|
390
|
+
});
|
|
391
|
+
if (status !== 200)
|
|
392
|
+
throw new Error(`El buzón no aceptó los accesos del #${id} (${status}: ${text.slice(0, 160)}).`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Da acceso a quien decide la disputa a todo lo cifrado de `objetos` que
|
|
397
|
+
* esta cuenta puede abrir: textos anclados y bytes de archivos, tal como
|
|
398
|
+
* están en el buzón. Lo que va en claro se salta (eso ya lo lee).
|
|
399
|
+
*
|
|
400
|
+
* `base` es el buzón de quien trabaja (tarea directa, `id` = la tarea) o el
|
|
401
|
+
* del tablón (`tablon: true`, `id` = el encargo). Devuelve a cuántos
|
|
402
|
+
* firmantes se les dio y quiénes no tienen clave todavía: a esos no se les
|
|
403
|
+
* puede dar hasta que la publiquen.
|
|
404
|
+
*/
|
|
405
|
+
async grantArbitratorsAccess(o) {
|
|
406
|
+
this.wallet();
|
|
407
|
+
const cifrados = o.objetos.filter((x) => (typeof x === 'string' ? esTextoCifrado(x) : esArchivoCifrado(x)));
|
|
408
|
+
if (cifrados.length === 0)
|
|
409
|
+
return { arbitros: 0, sinClave: [], objetos: 0 };
|
|
410
|
+
const conClave = [];
|
|
411
|
+
const sinClave = [];
|
|
412
|
+
for (const a of await this.getArbitrators()) {
|
|
413
|
+
const clave = await this.getPublicKey(a);
|
|
414
|
+
if (clave)
|
|
415
|
+
conClave.push({ cuenta: a, clave });
|
|
416
|
+
else
|
|
417
|
+
sinClave.push(a);
|
|
418
|
+
}
|
|
419
|
+
if (conClave.length === 0)
|
|
420
|
+
return { arbitros: 0, sinClave, objetos: cifrados.length };
|
|
421
|
+
const mia = await this.encryptionKey();
|
|
422
|
+
const nuevos = [];
|
|
423
|
+
for (const objeto of cifrados) {
|
|
424
|
+
for (const d of conClave) {
|
|
425
|
+
// Lo que esta cuenta no puede abrir no lo puede compartir: se salta.
|
|
426
|
+
try {
|
|
427
|
+
nuevos.push(concederAcceso(objeto, this.account.address, mia.secreta, d, o.accesos ?? []));
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
/* no es de los suyos */
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
await this.dejarAccesos(o.base, o.id, o.tablon === true, nuevos);
|
|
435
|
+
return { arbitros: conClave.length, sinClave, objetos: cifrados.length };
|
|
436
|
+
}
|
|
437
|
+
// -------------------------------------------------------------------------
|
|
223
438
|
// Lectura
|
|
224
439
|
// -------------------------------------------------------------------------
|
|
225
440
|
/** Todos los agentes del registry, activos e inactivos. */
|
|
@@ -929,7 +1144,7 @@ export class PanalClient {
|
|
|
929
1144
|
throw new Error(`La tarea #${taskId} está "${TaskStatus[task.status]}": solo se entrega lo que sigue abierto.`);
|
|
930
1145
|
}
|
|
931
1146
|
const ficha = await this.leerAgente(yo);
|
|
932
|
-
const canal = ficha.metadata.botUrl
|
|
1147
|
+
const canal = ficha.metadata.botUrl ? sinBarraFinal(ficha.metadata.botUrl) : '';
|
|
933
1148
|
if (!canal) {
|
|
934
1149
|
throw new Error(`${yo} no publica ningún canal (\`bot:\`): el cliente no tendría de dónde recoger la entrega.`);
|
|
935
1150
|
}
|
|
@@ -1287,7 +1502,21 @@ export class PanalClient {
|
|
|
1287
1502
|
if (adjuntos.length > 0 && !(await this.buzonAceptaArchivos())) {
|
|
1288
1503
|
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
1504
|
}
|
|
1290
|
-
|
|
1505
|
+
let encargo = appendAttachmentsManifest(p.encargo, adjuntos.map((a) => attachmentFrom(a.name, a.bytes, a.mime)));
|
|
1506
|
+
// Lo que se sube: los adjuntos tal cual, o cifrados y con nombre opaco.
|
|
1507
|
+
let aSubir = adjuntos;
|
|
1508
|
+
if (p.confidencial) {
|
|
1509
|
+
/*
|
|
1510
|
+
* Cifrado solo para quien publica, antes de pagar nada: a quien coja el
|
|
1511
|
+
* encargo se le da acceso al elegirlo (`chooseBoardOffer`). Los adjuntos
|
|
1512
|
+
* se anuncian en abierto por su hash y su tamaño: el buzón solo acepta
|
|
1513
|
+
* los que el encargo anuncia, y no puede leerlo.
|
|
1514
|
+
*/
|
|
1515
|
+
const para = [{ cuenta: this.account.address, clave: (await this.publishEncryptionKey()).publica }];
|
|
1516
|
+
const cifrados = adjuntos.map((a) => ({ ...a, bytes: cifrarArchivo(a.bytes, para) }));
|
|
1517
|
+
encargo = cifrarTexto(appendAttachmentsManifest(p.encargo, cifrados.map((a) => attachmentFrom(a.name, a.bytes, a.mime))), para, { anunciar: cifrados.map((a) => ({ hash: keccak256(a.bytes), size: a.bytes.byteLength })) });
|
|
1518
|
+
aSubir = cifrados.map((a) => ({ name: nombreEnBuzon(keccak256(a.bytes)), bytes: a.bytes }));
|
|
1519
|
+
}
|
|
1291
1520
|
const taskHash = keccak256(toBytes(encargo));
|
|
1292
1521
|
const advertHash = keccak256(toBytes(p.anuncio));
|
|
1293
1522
|
const seleccion = p.modo === ModoTablon.Seleccion;
|
|
@@ -1317,7 +1546,7 @@ export class PanalClient {
|
|
|
1317
1546
|
// Los textos van DESPUÉS de la cadena: hasta que el encargo existe, el buzón
|
|
1318
1547
|
// no tiene hash con qué comprobarlos. Si fallan, el encargo está pagado y
|
|
1319
1548
|
// sin anunciar: se dice y se reintenta con `publishBoardJobTexts`.
|
|
1320
|
-
const base = { jobId, txHash: hash, taskHash, advertHash, encargo };
|
|
1549
|
+
const base = { jobId, txHash: hash, taskHash, advertHash, encargo, archivos: aSubir };
|
|
1321
1550
|
try {
|
|
1322
1551
|
await this.publishBoardJobTexts(jobId, { anuncio: p.anuncio, encargo });
|
|
1323
1552
|
}
|
|
@@ -1327,7 +1556,7 @@ export class PanalClient {
|
|
|
1327
1556
|
}
|
|
1328
1557
|
// Los bytes, después del encargo: el buzón solo acepta los que anuncia.
|
|
1329
1558
|
try {
|
|
1330
|
-
await this.publishBoardJobFiles(jobId,
|
|
1559
|
+
await this.publishBoardJobFiles(jobId, aSubir);
|
|
1331
1560
|
return { ...base, textosGuardados: true, archivosGuardados: true };
|
|
1332
1561
|
}
|
|
1333
1562
|
catch (err) {
|
|
@@ -1385,7 +1614,7 @@ export class PanalClient {
|
|
|
1385
1614
|
* El hash viene del texto anclado —`readBoardJobBrief` o
|
|
1386
1615
|
* `readBoardJobResult`—, nunca del buzón. Una firma para todos.
|
|
1387
1616
|
*/
|
|
1388
|
-
async downloadBoardJobFiles(jobId, archivos, de) {
|
|
1617
|
+
async downloadBoardJobFiles(jobId, archivos, de, opciones = {}) {
|
|
1389
1618
|
if (archivos.length === 0)
|
|
1390
1619
|
return [];
|
|
1391
1620
|
const job = await this.getBoardJob(jobId);
|
|
@@ -1394,11 +1623,38 @@ export class PanalClient {
|
|
|
1394
1623
|
if (!esCliente && yo !== job.agente.toLowerCase()) {
|
|
1395
1624
|
throw new Error(`Los archivos del #${jobId} solo los bajan su cliente y su agente.`);
|
|
1396
1625
|
}
|
|
1626
|
+
// Si el texto que los anuncia iba cifrado, el buzón los guarda con su
|
|
1627
|
+
// nombre opaco y hay que abrirlos. Si no se ha leído aún, se lee: un agente
|
|
1628
|
+
// que retoma un encargo tras reiniciarse no lo tiene en memoria.
|
|
1629
|
+
const estado = await this.estadoDeCifrado(jobId, de === 'entrega' && esCliente ? 'entrega' : 'adjunto');
|
|
1630
|
+
const crudos = await this.bajarDelTablon(jobId, archivos, de, estado.cifrado, esCliente);
|
|
1631
|
+
if (opciones.abrir === false)
|
|
1632
|
+
return crudos;
|
|
1633
|
+
const out = [];
|
|
1634
|
+
for (const bytes of crudos)
|
|
1635
|
+
out.push(await this.decryptFile(bytes, estado.accesos));
|
|
1636
|
+
return out;
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Abre un archivo de un encargo del tablón bajado con `abrir: false`: con
|
|
1640
|
+
* la clave de esta cuenta y sus accesos. Si no va cifrado, lo devuelve tal cual.
|
|
1641
|
+
*/
|
|
1642
|
+
async openBoardJobFile(jobId, bytes, de = 'adjunto') {
|
|
1643
|
+
if (!esArchivoCifrado(bytes))
|
|
1644
|
+
return bytes;
|
|
1645
|
+
const job = await this.getBoardJob(jobId);
|
|
1646
|
+
const esCliente = this.account?.address.toLowerCase() === job.cliente.toLowerCase();
|
|
1647
|
+
const estado = await this.estadoDeCifrado(jobId, de === 'entrega' && esCliente ? 'entrega' : 'adjunto');
|
|
1648
|
+
return this.decryptFile(bytes, estado.accesos);
|
|
1649
|
+
}
|
|
1650
|
+
/** Los bytes de unos archivos del tablón, comprobados contra su hash y SIN abrir. */
|
|
1651
|
+
async bajarDelTablon(jobId, archivos, de, cifrado, esCliente) {
|
|
1397
1652
|
// El cliente abre la entrega con la firma de llevársela; lo demás, con la de leer el encargo.
|
|
1398
1653
|
const cabeceras = await this.credencialesDeBuzon((e) => de === 'entrega' && esCliente ? tablonResultadoSignMessage(jobId, e) : tablonEncargoSignMessage(jobId, e));
|
|
1399
1654
|
const out = [];
|
|
1400
1655
|
for (const f of archivos) {
|
|
1401
|
-
const
|
|
1656
|
+
const nombre = cifrado ? nombreEnBuzon(f.hash) : sanitizeFileName(f.name);
|
|
1657
|
+
const { status, bytes } = await fetchBytesLimited(`${this.buzonUrl}/${this.tablon()}/archivo/${jobId}/${encodeURIComponent(nombre)}?de=${de}`, { headers: cabeceras, timeoutMs: 180_000, maxBytes: Math.min(MAX_FILE_BYTES, f.size + 1024) });
|
|
1402
1658
|
if (status === 404)
|
|
1403
1659
|
throw new Error(`«${f.name}» no está en el buzón del tablón.`);
|
|
1404
1660
|
if (status !== 200)
|
|
@@ -1408,6 +1664,22 @@ export class PanalClient {
|
|
|
1408
1664
|
}
|
|
1409
1665
|
return out;
|
|
1410
1666
|
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Si el encargo (`adjunto`) o la entrega (`entrega`) de un encargo del
|
|
1669
|
+
* tablón va cifrado, y los accesos de esta cuenta. Lo recuerda de la última
|
|
1670
|
+
* lectura; si no hay, lee. La entrega solo la lee su cliente: un agente
|
|
1671
|
+
* cifra la suya si el encargo iba cifrado, así que mira el encargo.
|
|
1672
|
+
*/
|
|
1673
|
+
async estadoDeCifrado(jobId, de) {
|
|
1674
|
+
const clave = `${jobId}:${de}`;
|
|
1675
|
+
const ya = this.cifradoDelTablon.get(clave);
|
|
1676
|
+
if (ya)
|
|
1677
|
+
return ya;
|
|
1678
|
+
const leido = de === 'entrega' ? await this.leerEntregaDelTablon(jobId) : await this.leerEncargoDelTablon(jobId);
|
|
1679
|
+
const estado = { cifrado: esTextoCifrado(leido.texto), accesos: leido.accesos };
|
|
1680
|
+
this.cifradoDelTablon.set(clave, estado);
|
|
1681
|
+
return estado;
|
|
1682
|
+
}
|
|
1411
1683
|
/**
|
|
1412
1684
|
* Deja en el buzón el anuncio y el encargo de un encargo ya publicado. Sin
|
|
1413
1685
|
* firma: el buzón solo acepta los textos que dan los hashes de la cadena.
|
|
@@ -1442,28 +1714,58 @@ export class PanalClient {
|
|
|
1442
1714
|
* Un anuncio que no cuadre no se devuelve: el buzón no puede inventarse uno.
|
|
1443
1715
|
*/
|
|
1444
1716
|
async listBoardJobAdverts(opciones = {}) {
|
|
1445
|
-
const [encargos,
|
|
1446
|
-
this.listBoardJobs({ limit: opciones.limit }),
|
|
1447
|
-
fetchLimited(`${this.buzonUrl}/${this.tablon()}/lista`, { timeoutMs: 15_000, maxBytes: 2_000_000 }),
|
|
1448
|
-
]);
|
|
1449
|
-
if (respuesta.status !== 200)
|
|
1450
|
-
throw new Error(`El buzón respondió ${respuesta.status} al pedir los anuncios.`);
|
|
1451
|
-
const { anuncios = [] } = JSON.parse(respuesta.text);
|
|
1452
|
-
const porId = new Map(anuncios.map((a) => [a.jobId, a.anuncio]));
|
|
1717
|
+
const [encargos, anuncios] = await Promise.all([this.listBoardJobs({ limit: opciones.limit }), this.anunciosDelBuzon()]);
|
|
1453
1718
|
return encargos.flatMap((j) => {
|
|
1454
|
-
const
|
|
1455
|
-
if (
|
|
1719
|
+
const a = anuncios.get(j.id.toString());
|
|
1720
|
+
if (!a)
|
|
1456
1721
|
return [];
|
|
1457
|
-
if (keccak256(toBytes(anuncio)).toLowerCase() !== j.advertHash.toLowerCase())
|
|
1722
|
+
if (keccak256(toBytes(a.anuncio)).toLowerCase() !== j.advertHash.toLowerCase())
|
|
1458
1723
|
return [];
|
|
1459
|
-
return [{ ...j, anuncio }];
|
|
1724
|
+
return [{ ...j, anuncio: a.anuncio, confidencial: a.cifrado }];
|
|
1460
1725
|
});
|
|
1461
1726
|
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Los anuncios que guarda el buzón del tablón, con si su encargo va cifrado
|
|
1729
|
+
* (confidencial). Eso lo dice el buzón y no la cadena: si mintiera, lo que
|
|
1730
|
+
* cambia es un aviso, y quien coja el encargo lo verá al abrirlo.
|
|
1731
|
+
*/
|
|
1732
|
+
async anunciosDelBuzon() {
|
|
1733
|
+
const respuesta = await fetchLimited(`${this.buzonUrl}/${this.tablon()}/lista`, { timeoutMs: 15_000, maxBytes: 2_000_000 });
|
|
1734
|
+
if (respuesta.status !== 200)
|
|
1735
|
+
throw new Error(`El buzón respondió ${respuesta.status} al pedir los anuncios.`);
|
|
1736
|
+
const { anuncios = [] } = JSON.parse(respuesta.text);
|
|
1737
|
+
return new Map(anuncios
|
|
1738
|
+
.filter((a) => typeof a.anuncio === 'string')
|
|
1739
|
+
.map((a) => [a.jobId, { anuncio: a.anuncio, cifrado: a.cifrado === true }]));
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Si un encargo del tablón es confidencial: su encargo va cifrado y solo lo
|
|
1743
|
+
* lee quien su cliente elija, que tiene que tener su clave publicada.
|
|
1744
|
+
*/
|
|
1745
|
+
async isBoardJobConfidential(jobId) {
|
|
1746
|
+
return (await this.anunciosDelBuzon()).get(jobId.toString())?.cifrado === true;
|
|
1747
|
+
}
|
|
1462
1748
|
/**
|
|
1463
1749
|
* El encargo privado. Lo leen su cliente y su agente en cuanto lo tiene, y
|
|
1464
1750
|
* se comprueba contra el `taskHash` de la cadena antes de devolverlo.
|
|
1465
1751
|
*/
|
|
1466
1752
|
async readBoardJobBrief(jobId) {
|
|
1753
|
+
const { texto, accesos } = await this.leerEncargoDelTablon(jobId);
|
|
1754
|
+
const cifrado = esTextoCifrado(texto);
|
|
1755
|
+
this.cifradoDelTablon.set(`${jobId}:adjunto`, { cifrado, accesos });
|
|
1756
|
+
// Confidencial: se abre con la clave de esta cuenta y el acceso que da su
|
|
1757
|
+
// cliente al elegirla. Sin acceso todavía, se dice, sin pedir firmas.
|
|
1758
|
+
if (cifrado && !puedeAbrir(texto, this.account.address, accesos)) {
|
|
1759
|
+
throw new Error(`El encargo #${jobId} es confidencial y su cliente todavía no le ha dado acceso a esta cuenta ` +
|
|
1760
|
+
'(se lo da al elegirla). Vuelve a intentarlo en un rato.');
|
|
1761
|
+
}
|
|
1762
|
+
return this.decryptText(texto, accesos);
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* El encargo del tablón tal como está anclado —en claro o cifrado—, con los
|
|
1766
|
+
* accesos que tiene esta cuenta. Comprobado contra el `taskHash`.
|
|
1767
|
+
*/
|
|
1768
|
+
async leerEncargoDelTablon(jobId) {
|
|
1467
1769
|
const job = await this.getBoardJob(jobId);
|
|
1468
1770
|
const yo = this.account?.address.toLowerCase();
|
|
1469
1771
|
if (yo !== job.cliente.toLowerCase() && yo !== job.agente.toLowerCase()) {
|
|
@@ -1478,11 +1780,11 @@ export class PanalClient {
|
|
|
1478
1780
|
throw new Error(`El buzón no tiene el encargo #${jobId}: su cliente no llegó a dejarlo.`);
|
|
1479
1781
|
if (status !== 200)
|
|
1480
1782
|
throw new Error(`El buzón respondió ${status} al pedir el encargo #${jobId}.`);
|
|
1481
|
-
const { brief } = JSON.parse(text);
|
|
1783
|
+
const { brief, accesos } = JSON.parse(text);
|
|
1482
1784
|
if (typeof brief !== 'string' || keccak256(toBytes(brief)).toLowerCase() !== job.taskHash.toLowerCase()) {
|
|
1483
1785
|
throw new Error(`El encargo #${jobId} del buzón no cuadra con el taskHash de la cadena: no se trabaja sobre él.`);
|
|
1484
1786
|
}
|
|
1485
|
-
return brief;
|
|
1787
|
+
return { texto: brief, accesos: Array.isArray(accesos) ? accesos : [] };
|
|
1486
1788
|
}
|
|
1487
1789
|
/**
|
|
1488
1790
|
* Entregar un encargo del tablón con reglas: se deja en el buzón y DESPUÉS
|
|
@@ -1497,18 +1799,42 @@ export class PanalClient {
|
|
|
1497
1799
|
}
|
|
1498
1800
|
if (job.estado !== EstadoTablon.Asignado)
|
|
1499
1801
|
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
1802
|
if (archivos.length > 0) {
|
|
1504
1803
|
const problema = problemaConArchivos(archivos);
|
|
1505
1804
|
if (problema)
|
|
1506
1805
|
throw new Error(problema);
|
|
1806
|
+
}
|
|
1807
|
+
/*
|
|
1808
|
+
* Confidencial: la entrega y sus archivos van cifrados para el cliente y
|
|
1809
|
+
* para quien entrega. Sin la clave del cliente no se entrega en claro: el
|
|
1810
|
+
* encargo era confidencial, y su entrega también.
|
|
1811
|
+
*/
|
|
1812
|
+
let para = null;
|
|
1813
|
+
if (esTextoCifrado((await this.leerEncargoDelTablon(jobId)).texto)) {
|
|
1814
|
+
const delCliente = await this.getPublicKey(job.cliente);
|
|
1815
|
+
if (!delCliente) {
|
|
1816
|
+
throw new Error(`El encargo #${jobId} es confidencial y su cliente no tiene clave de cifrado publicada: la entrega no ` +
|
|
1817
|
+
'puede ir cifrada, y en claro no se entrega. No se ha anclado nada; vuelve a intentarlo en un rato.');
|
|
1818
|
+
}
|
|
1819
|
+
para = [
|
|
1820
|
+
{ cuenta: job.cliente, clave: delCliente },
|
|
1821
|
+
{ cuenta: this.account.address, clave: (await this.publishEncryptionKey()).publica },
|
|
1822
|
+
];
|
|
1823
|
+
}
|
|
1824
|
+
// Lo que va en el manifiesto (nombre de verdad) y lo que se sube (cifrado y con nombre opaco).
|
|
1825
|
+
const anunciar = para ? archivos.map((a) => ({ ...a, bytes: cifrarArchivo(a.bytes, para) })) : archivos;
|
|
1826
|
+
const subir = para ? anunciar.map((a) => ({ name: nombreEnBuzon(keccak256(a.bytes)), bytes: a.bytes })) : archivos;
|
|
1827
|
+
// Los archivos primero, y su hash dentro del texto que se ancla: si el
|
|
1828
|
+
// buzón no los acepta, no se ancla nada.
|
|
1829
|
+
let texto = textoEscrito;
|
|
1830
|
+
if (archivos.length > 0) {
|
|
1507
1831
|
const cabecerasArchivos = await this.credencialesDeBuzon((e) => tablonArchivoSignMessage(jobId, e));
|
|
1508
|
-
for (const a of
|
|
1832
|
+
for (const a of subir)
|
|
1509
1833
|
await this.subirArchivoAlTablon(jobId, a, cabecerasArchivos);
|
|
1510
|
-
texto = entregaConArchivos(jobId, textoEscrito,
|
|
1834
|
+
texto = entregaConArchivos(jobId, textoEscrito, anunciar);
|
|
1511
1835
|
}
|
|
1836
|
+
if (para)
|
|
1837
|
+
texto = cifrarTexto(texto, para);
|
|
1512
1838
|
const hashEntrega = keccak256(toBytes(texto));
|
|
1513
1839
|
const cabeceras = await this.credencialesDeBuzon((e) => tablonEntregaSignMessage(jobId, hashEntrega, e));
|
|
1514
1840
|
const { status, text } = await fetchLimited(`${this.buzonUrl}/${this.tablon()}/entrega/${jobId}`, {
|
|
@@ -1534,6 +1860,13 @@ export class PanalClient {
|
|
|
1534
1860
|
* en el escrow: el buzón no puede cambiarla.
|
|
1535
1861
|
*/
|
|
1536
1862
|
async readBoardJobResult(jobId) {
|
|
1863
|
+
const { texto, accesos } = await this.leerEntregaDelTablon(jobId);
|
|
1864
|
+
this.cifradoDelTablon.set(`${jobId}:entrega`, { cifrado: esTextoCifrado(texto), accesos });
|
|
1865
|
+
// Cifrada para el cliente y para quien entregó: se abre con la clave de esta cuenta.
|
|
1866
|
+
return this.decryptText(texto, accesos);
|
|
1867
|
+
}
|
|
1868
|
+
/** La entrega tal como está anclada, con los accesos de esta cuenta. */
|
|
1869
|
+
async leerEntregaDelTablon(jobId) {
|
|
1537
1870
|
const job = await this.getBoardJob(jobId);
|
|
1538
1871
|
if (this.account?.address.toLowerCase() !== job.cliente.toLowerCase()) {
|
|
1539
1872
|
throw new Error(`La entrega del #${jobId} solo la descarga su cliente.`);
|
|
@@ -1547,12 +1880,12 @@ export class PanalClient {
|
|
|
1547
1880
|
throw new Error(`El encargo #${jobId} todavía no tiene entrega en el buzón.`);
|
|
1548
1881
|
if (status !== 200)
|
|
1549
1882
|
throw new Error(`El buzón respondió ${status} al pedir la entrega del #${jobId}.`);
|
|
1550
|
-
const { resultText } = JSON.parse(text);
|
|
1883
|
+
const { resultText, accesos } = JSON.parse(text);
|
|
1551
1884
|
const tarea = await this.getTask(job.escrowTaskId);
|
|
1552
1885
|
if (typeof resultText !== 'string' || keccak256(toBytes(resultText)).toLowerCase() !== tarea.resultHash.toLowerCase()) {
|
|
1553
1886
|
throw new Error(`La entrega del #${jobId} no cuadra con el resultHash anclado en el escrow.`);
|
|
1554
1887
|
}
|
|
1555
|
-
return resultText;
|
|
1888
|
+
return { texto: resultText, accesos: Array.isArray(accesos) ? accesos : [] };
|
|
1556
1889
|
}
|
|
1557
1890
|
/** Coger un encargo inmediato. Antes de firmar pregunta si se puede, y por qué no. */
|
|
1558
1891
|
async takeBoardJob(jobId) {
|
|
@@ -1573,6 +1906,10 @@ export class PanalClient {
|
|
|
1573
1906
|
const { ok, motivo } = await this.canTakeBoardJob(this.account.address, jobId);
|
|
1574
1907
|
if (!ok)
|
|
1575
1908
|
throw new Error(`No se puede ofertar en el encargo #${jobId}: ${motivo}.`);
|
|
1909
|
+
// Confidencial: sin clave publicada, el cliente no podría darle acceso al
|
|
1910
|
+
// elegirla y no podría leer lo que tiene que hacer. Se publica antes.
|
|
1911
|
+
if (await this.isBoardJobConfidential(jobId))
|
|
1912
|
+
await this.publishEncryptionKey();
|
|
1576
1913
|
return (await this.firmarEnTablon('La oferta', 'applyFor', [jobId, precio, BigInt(plazoEntregaS)])).hash;
|
|
1577
1914
|
}
|
|
1578
1915
|
async withdrawBoardOffer(jobId) {
|
|
@@ -1588,6 +1925,13 @@ export class PanalClient {
|
|
|
1588
1925
|
const oferta = ofertas.find((o) => o.agente.toLowerCase() === agent.toLowerCase());
|
|
1589
1926
|
if (!oferta)
|
|
1590
1927
|
throw new Error(`${agent} no tiene oferta en el encargo #${jobId}.`);
|
|
1928
|
+
// Confidencial: solo se elige a quien se le pueda dar acceso. Se mira antes
|
|
1929
|
+
// de pagar nada; elegir a quien no puede leer el encargo es pagar por nada.
|
|
1930
|
+
const confidencial = esTextoCifrado((await this.leerEncargoDelTablon(jobId)).texto);
|
|
1931
|
+
if (confidencial && !(await this.getPublicKey(agent))) {
|
|
1932
|
+
throw new Error(`El encargo #${jobId} es confidencial y ${agent} no ha publicado su clave de cifrado: no se le podría dar ` +
|
|
1933
|
+
'acceso y no podría leerlo. No se ha elegido nada; elige otra oferta.');
|
|
1934
|
+
}
|
|
1591
1935
|
const diferencia = oferta.precio > job.importe ? oferta.precio - job.importe : 0n;
|
|
1592
1936
|
const nativa = job.moneda.toLowerCase() === NATIVE_CURRENCY.toLowerCase();
|
|
1593
1937
|
if (diferencia > 0n) {
|
|
@@ -1596,7 +1940,81 @@ export class PanalClient {
|
|
|
1596
1940
|
await this.aprobarAlTablon(job.moneda, diferencia);
|
|
1597
1941
|
}
|
|
1598
1942
|
const { hash } = await this.firmarEnTablon('Elegir la oferta', 'choose', [jobId, getAddress(agent)], nativa ? diferencia : 0n);
|
|
1599
|
-
|
|
1943
|
+
const escrowTaskId = (await this.getBoardJob(jobId)).escrowTaskId;
|
|
1944
|
+
if (!confidencial)
|
|
1945
|
+
return { txHash: hash, escrowTaskId };
|
|
1946
|
+
// Elegido en la cadena: ahora, que pueda leer lo que tiene que hacer. Si
|
|
1947
|
+
// falla, el encargo ya es suyo; se reintenta con `grantBoardJobAccess`.
|
|
1948
|
+
try {
|
|
1949
|
+
return { txHash: hash, escrowTaskId, acceso: { ok: true, accesos: await this.grantBoardJobAccess(jobId, agent) } };
|
|
1950
|
+
}
|
|
1951
|
+
catch (err) {
|
|
1952
|
+
return { txHash: hash, escrowTaskId, acceso: { ok: false, error: err instanceof Error ? err.message : String(err) } };
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* El cliente de un encargo confidencial da acceso al encargo y a sus
|
|
1957
|
+
* adjuntos a quien lo tiene (o a `agent`). Se hace solo al elegir; esto es
|
|
1958
|
+
* para un encargo que se asignó solo o un acceso que falló. Repetirlo no
|
|
1959
|
+
* duplica nada. Devuelve cuántos accesos dejó.
|
|
1960
|
+
*/
|
|
1961
|
+
async grantBoardJobAccess(jobId, agent) {
|
|
1962
|
+
const job = await this.getBoardJob(jobId);
|
|
1963
|
+
const para = getAddress(agent ?? job.agente);
|
|
1964
|
+
if (para === getAddress(NATIVE_CURRENCY))
|
|
1965
|
+
throw new Error(`El encargo #${jobId} no lo tiene nadie todavía.`);
|
|
1966
|
+
const clave = await this.getPublicKey(para);
|
|
1967
|
+
if (!clave)
|
|
1968
|
+
throw new Error(`${para} no ha publicado su clave de cifrado: no se le puede dar acceso.`);
|
|
1969
|
+
const { objetos, accesos } = await this.reunirDelTablon(jobId, false);
|
|
1970
|
+
if (objetos.length === 0)
|
|
1971
|
+
return 0;
|
|
1972
|
+
const mia = await this.encryptionKey();
|
|
1973
|
+
const nuevos = objetos.map((o) => concederAcceso(o, this.account.address, mia.secreta, { cuenta: para, clave }, accesos));
|
|
1974
|
+
await this.dejarAccesos(`${this.buzonUrl}/${this.tablon()}`, jobId, true, nuevos);
|
|
1975
|
+
return nuevos.length;
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Da acceso a quien decide la disputa de un encargo del tablón (los
|
|
1979
|
+
* firmantes de la multisig) a lo cifrado que esta cuenta puede bajarse: el
|
|
1980
|
+
* encargo y sus adjuntos y, si es su cliente, la entrega y sus archivos. Sin
|
|
1981
|
+
* esto no podrían leerlo —ni Panal puede— y decidirían a ciegas.
|
|
1982
|
+
* `disputeBoardJob` no lo hace solo: es otra firma, y puede faltarles la clave.
|
|
1983
|
+
*/
|
|
1984
|
+
async grantBoardDisputeAccess(jobId) {
|
|
1985
|
+
const { objetos, accesos } = await this.reunirDelTablon(jobId, true);
|
|
1986
|
+
return this.grantArbitratorsAccess({ base: `${this.buzonUrl}/${this.tablon()}`, id: jobId, tablon: true, objetos, accesos });
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Todo lo cifrado de un encargo del tablón que esta cuenta puede bajarse,
|
|
1990
|
+
* tal como está en el buzón, y sus accesos. Con `conEntrega`, también la
|
|
1991
|
+
* entrega y sus archivos si es su cliente y ya la hay.
|
|
1992
|
+
*/
|
|
1993
|
+
async reunirDelTablon(jobId, conEntrega) {
|
|
1994
|
+
const job = await this.getBoardJob(jobId);
|
|
1995
|
+
const esCliente = this.account?.address.toLowerCase() === job.cliente.toLowerCase();
|
|
1996
|
+
const objetos = [];
|
|
1997
|
+
const accesos = [];
|
|
1998
|
+
const encargo = await this.leerEncargoDelTablon(jobId);
|
|
1999
|
+
accesos.push(...encargo.accesos);
|
|
2000
|
+
if (esTextoCifrado(encargo.texto)) {
|
|
2001
|
+
objetos.push(encargo.texto);
|
|
2002
|
+
const claro = await this.decryptText(encargo.texto, accesos);
|
|
2003
|
+
objetos.push(...(await this.bajarDelTablon(jobId, parseAttachmentsManifest(claro), 'adjunto', true, esCliente)));
|
|
2004
|
+
}
|
|
2005
|
+
if (conEntrega && esCliente && (job.estado === EstadoTablon.Asignado || job.estado === EstadoTablon.Cerrado)) {
|
|
2006
|
+
const tarea = await this.getTask(job.escrowTaskId);
|
|
2007
|
+
if (!/^0x0+$/.test(tarea.resultHash)) {
|
|
2008
|
+
const entrega = await this.leerEntregaDelTablon(jobId);
|
|
2009
|
+
accesos.push(...entrega.accesos);
|
|
2010
|
+
if (esTextoCifrado(entrega.texto)) {
|
|
2011
|
+
objetos.push(entrega.texto);
|
|
2012
|
+
const claro = await this.decryptText(entrega.texto, accesos);
|
|
2013
|
+
objetos.push(...(await this.bajarDelTablon(jobId, parseFilesManifest(claro), 'entrega', true, true)));
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
return { objetos, accesos };
|
|
1600
2018
|
}
|
|
1601
2019
|
/** Asignar un encargo de selección cuyo cliente no eligió. Lo puede llamar cualquiera. */
|
|
1602
2020
|
async autoAssignBoardJob(jobId) {
|
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
|
|
67
|
+
const base = `${sinBarraFinal(botUrl)}/agent.json`;
|
|
67
68
|
const lang = normalizarIdioma(idioma);
|
|
68
69
|
return lang ? `${base}?lang=${lang}` : base;
|
|
69
70
|
}
|