create-panal-agent 0.6.1 → 0.7.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/i18n.js CHANGED
@@ -76,6 +76,7 @@ The language is taken from --lang, then PANAL_LANG, then your system locale.`,
76
76
  x402: 'Charge per call (optional). Leave it empty and your agent only takes escrow jobs.\nThe price is per request, in an EIP-2612 token — it cannot be MON, the scheme needs `permit`.',
77
77
  subcontrata: "Subcontracting (optional, off by default). Your agent can pay another one for what it cannot do itself (see ctx.consultar in agent.ts). Without a number here it never delegates.\nIt is in the x402 currency, NOT a cut of what you charge per task: a task is paid in MON and a question in $PANAL, and converting one into the other by eye would be inventing the budget.",
78
78
  vigilante: "The watchman. Every 60 s it reviews your open tasks and recovers what got stuck: a delivery that was never anchored, work that died halfway, or a job that was paid for and never arrived.\nVIGILANTE=off turns it off. PUBLIC_URL is your https endpoint, used in the lost-job warning.",
79
+ seguridad: "Hardening. LIMITE_POR_MINUTO caps requests per IP (0 disables it); every unauthenticated request costs one RPC call, so without a cap a curl loop leaves your agent unable to deliver.\nTRAS_PROXY=1 only if a reverse proxy sits in front, so x-forwarded-for can be trusted. AUTH_ESTRICTA=1 rejects download signatures with no expiry (the old format).",
79
80
  },
80
81
  readme: `# {name}
81
82
 
@@ -100,6 +101,8 @@ PUBLIC_URL=https://your-domain npm run register
100
101
  | \`src/agent.ts\` | **The only one you edit.** Your work goes in \`handleTask()\`. |
101
102
  | \`src/server.ts\` | Receives the brief, verifies signatures, delivers on-chain. |
102
103
  | \`src/register.ts\` | Your listing: name, description, skills, price. |
104
+ | \`src/pdf.ts\` | Turns text into a PDF, with no dependencies. Delete it if you deliver no files. |
105
+ | \`src/vigilante.ts\` | Recovers tasks left hanging: a delivery never anchored, work that died halfway. |
103
106
  | \`.env\` | Your private key and your model key. Never commit it. |
104
107
 
105
108
  ## How you get paid
@@ -168,6 +171,7 @@ El idioma sale de --lang, luego de PANAL_LANG, y si no del locale del sistema.`,
168
171
  x402: 'Cobro por llamada (opcional). Déjalo vacío y tu agente solo acepta encargos del escrow.\nEl precio es por petición, en un token EIP-2612: no puede ser MON, el esquema necesita `permit`.',
169
172
  subcontrata: "Subcontratar (opcional, apagado por defecto). Tu agente puede pagar a otro por lo que no sepa hacer (ver ctx.consultar en agent.ts). Sin un numero aqui, no delega nunca.\nVa en la moneda del x402 y NO se deduce de lo que cobras por tarea: una tarea se cobra en MON y una consulta en $PANAL, y convertir una en otra a ojo seria inventarse el presupuesto.",
170
173
  vigilante: "El vigilante. Cada 60 s repasa tus tareas abiertas y recupera lo que se quedo colgado: una entrega que no llego a anclarse, un trabajo que murio a medias, o un encargo pagado que nunca llego.\nVIGILANTE=off lo apaga. PUBLIC_URL es tu endpoint https, para el aviso de encargo perdido.",
174
+ seguridad: "Endurecimiento. LIMITE_POR_MINUTO acota las peticiones por IP (0 lo desactiva); cada petición sin autenticar cuesta una llamada al RPC, así que sin tope un bucle de curl deja a tu agente sin poder entregar.\nTRAS_PROXY=1 solo si tienes un proxy delante, para poder fiarte de x-forwarded-for. AUTH_ESTRICTA=1 rechaza las firmas de descarga sin caducidad (el formato antiguo).",
171
175
  },
172
176
  readme: `# {name}
173
177
 
@@ -193,6 +197,8 @@ PUBLIC_URL=https://tu-dominio npm run register
193
197
  | \`src/agent.ts\` | **El único que tocas.** Tu trabajo va en \`handleTask()\`. |
194
198
  | \`src/server.ts\` | Recibe el encargo, verifica firmas y entrega en la cadena. |
195
199
  | \`src/register.ts\` | Tu ficha: nombre, descripción, skills y precio. |
200
+ | \`src/pdf.ts\` | Convierte texto en PDF, sin dependencias. Bórralo si no entregas archivos. |
201
+ | \`src/vigilante.ts\` | Recupera lo que se quedó colgado: una entrega sin anclar, un trabajo a medias. |
196
202
  | \`.env\` | Tu clave privada y la de tu modelo. No lo subas nunca. |
197
203
 
198
204
  ## Cómo cobras
@@ -261,6 +267,7 @@ const zh = {
261
267
  x402: '按次收费(可选)。留空则你的代理只接受托管订单。\n价格按每次请求计算,使用支持 EIP-2612 的代币——不能是 MON,该方案依赖 `permit`。',
262
268
  subcontrata: "转包(可选,默认关闭)。你的代理可以为自己做不了的事付钱给另一个代理(见 agent.ts 的 ctx.consultar)。这里没有数字就永远不会转包。\n它使用 x402 的币种,不是按任务收入的比例:任务用 MON 结算,提问用 $PANAL,凭感觉换算等于凭空编造预算。",
263
269
  vigilante: "守望者。每 60 秒检查一次你未完成的任务,并挽回卡住的部分:没有上链的交付、中途死掉的工作,或已付款却从未送达的委托。\nVIGILANTE=off 可关闭。PUBLIC_URL 是你的 https 端点,用于丢失委托的提醒。",
270
+ seguridad: "加固。LIMITE_POR_MINUTO 限制每个 IP 的请求数(0 表示关闭);每个未认证请求都要消耗一次 RPC 调用,没有上限时一个 curl 循环就能让你的代理无法交付。\nTRAS_PROXY=1 仅在前面有反向代理时使用,这样才能信任 x-forwarded-for。AUTH_ESTRICTA=1 拒绝没有有效期的下载签名(旧格式)。",
264
271
  },
265
272
  readme: `# {name}
266
273
 
@@ -285,6 +292,8 @@ PUBLIC_URL=https://你的域名 npm run register
285
292
  | \`src/agent.ts\` | **唯一需要你修改的文件。** 你的业务逻辑写在 \`handleTask()\` 里。 |
286
293
  | \`src/server.ts\` | 接收任务、验证签名、在链上交付。 |
287
294
  | \`src/register.ts\` | 你的名片:名称、描述、技能、价格。 |
295
+ | \`src/pdf.ts\` | 把文本转成 PDF,无依赖。不交付文件就删掉它。 |
296
+ | \`src/vigilante.ts\` | 挽回卡住的任务:没有上链的交付、中途死掉的工作。 |
288
297
  | \`.env\` | 你的私钥和模型密钥。绝不要提交到仓库。 |
289
298
 
290
299
  ## 你如何收款
@@ -349,6 +358,7 @@ const hi = {
349
358
  x402: 'प्रति कॉल शुल्क (वैकल्पिक)। खाली छोड़ें तो आपका एजेंट केवल एस्क्रो वाले काम लेगा।\nकीमत प्रति अनुरोध है, EIP-2612 टोकन में — MON नहीं चल सकता, इस योजना को `permit` चाहिए।',
350
359
  subcontrata: "सबकॉन्ट्रैक्टिंग (वैकल्पिक, डिफ़ॉल्ट रूप से बंद)। आपका एजेंट जो खुद नहीं कर सकता, उसके लिए दूसरे को भुगतान कर सकता है (agent.ts में ctx.consultar देखें)। यहाँ संख्या के बिना वह कभी नहीं सौंपता।\nयह x402 की मुद्रा में है, प्रति कार्य आय का हिस्सा नहीं: कार्य MON में और प्रश्न $PANAL में चुकाया जाता है।",
351
360
  vigilante: "प्रहरी। हर 60 सेकंड में आपके खुले कार्यों की जाँच करता है और जो अटक गया उसे वापस लाता है: वह डिलीवरी जो चेन पर दर्ज नहीं हुई, अधूरा रह गया काम, या भुगतान किया गया आदेश जो कभी नहीं पहुँचा।\nVIGILANTE=off इसे बंद करता है। PUBLIC_URL आपका https एंडपॉइंट है।",
361
+ seguridad: "सुरक्षा सख्ती। LIMITE_POR_MINUTO प्रति IP अनुरोध सीमित करता है (0 से बंद); हर बिना प्रमाणित अनुरोध एक RPC कॉल खर्च करता है, इसलिए बिना सीमा के एक curl लूप आपके एजेंट को डिलीवर करने से रोक सकता है।\nTRAS_PROXY=1 केवल तभी जब आगे रिवर्स प्रॉक्सी हो। AUTH_ESTRICTA=1 बिना समय-सीमा वाले डाउनलोड हस्ताक्षर अस्वीकार करता है।",
352
362
  },
353
363
  readme: `# {name}
354
364
 
@@ -373,6 +383,8 @@ PUBLIC_URL=https://आपका-डोमेन npm run register
373
383
  | \`src/agent.ts\` | **केवल यही आप बदलते हैं।** आपका काम \`handleTask()\` में जाता है। |
374
384
  | \`src/server.ts\` | काम प्राप्त करता है, हस्ताक्षर जाँचता है, चेन पर डिलीवर करता है। |
375
385
  | \`src/register.ts\` | आपकी प्रोफ़ाइल: नाम, विवरण, कौशल, कीमत। |
386
+ | \`src/pdf.ts\` | टेक्स्ट को PDF में बदलता है, बिना निर्भरता के। फाइलें न भेजें तो हटा दें। |
387
+ | \`src/vigilante.ts\` | अटके हुए कार्य वापस लाता है: चेन पर दर्ज न हुई डिलीवरी, अधूरा काम। |
376
388
  | \`.env\` | आपकी निजी कुंजी और मॉडल कुंजी। इसे कभी कमिट न करें। |
377
389
 
378
390
  ## भुगतान कैसे मिलता है
@@ -441,6 +453,7 @@ const ar = {
441
453
  x402: 'التحصيل لكل استدعاء (اختياري). اتركه فارغًا فيقبل وكيلك مهام الضمان فقط.\nالسعر لكل طلب، بعملة تدعم EIP-2612 — لا يصلح MON، فالمخطط يحتاج `permit`.',
442
454
  subcontrata: "التعاقد من الباطن (اختياري، معطل افتراضيا). يمكن لوكيلك ان يدفع لوكيل اخر مقابل ما لا يجيده (انظر ctx.consultar في agent.ts). بدون رقم هنا لن يفوض ابدا.\nيكون بعملة x402، وليس نسبة مما تتقاضاه عن المهمة: المهمة تدفع بـ MON والسؤال بـ $PANAL.",
443
455
  vigilante: "الحارس. كل 60 ثانية يراجع مهامك المفتوحة ويستعيد ما تعطل: تسليم لم يثبت على السلسلة، او عمل مات في منتصفه، او طلب مدفوع لم يصل قط.\nVIGILANTE=off يوقفه. PUBLIC_URL هو نقطة الوصول https لديك.",
456
+ seguridad: "تقوية. LIMITE_POR_MINUTO يحد الطلبات لكل IP (0 يعطله)؛ كل طلب غير موثق يكلف استدعاء RPC، فبدون حد يكفي حلقة curl لتترك وكيلك عاجزا عن التسليم.\nTRAS_PROXY=1 فقط إذا كان أمامه بروكسي عكسي. AUTH_ESTRICTA=1 يرفض تواقيع التنزيل بلا انتهاء صلاحية.",
444
457
  },
445
458
  readme: `# {name}
446
459
 
@@ -465,6 +478,8 @@ PUBLIC_URL=https://نطاقك npm run register
465
478
  | \`src/agent.ts\` | **الوحيد الذي تعدّله.** عملك يكتب داخل \`handleTask()\`. |
466
479
  | \`src/server.ts\` | يستقبل الطلب، ويتحقق من التواقيع، ويسلّم على السلسلة. |
467
480
  | \`src/register.ts\` | بطاقتك: الاسم والوصف والمهارات والسعر. |
481
+ | \`src/pdf.ts\` | يحوّل النص إلى PDF، دون تبعيات. احذفه إن كنت لا تسلّم ملفات. |
482
+ | \`src/vigilante.ts\` | يستعيد المهام المعطلة: تسليم لم يثبت على السلسلة، أو عمل مات في منتصفه. |
468
483
  | \`.env\` | مفتاحك الخاص ومفتاح النموذج. لا ترفعه إلى المستودع أبدًا. |
469
484
 
470
485
  ## كيف تتقاضى أجرك
@@ -532,6 +547,7 @@ La langue vient de --lang, puis de PANAL_LANG, puis de la locale du système.`,
532
547
  x402: "Facturation à l'appel (facultatif). Laissez vide et votre agent ne prend que des missions sous entiercement.\nLe prix est par requête, dans un jeton EIP-2612 : pas de MON, le schéma exige `permit`.",
533
548
  subcontrata: "Sous-traitance (optionnelle, desactivee par defaut). Votre agent peut payer un autre pour ce qu il ne sait pas faire (voir ctx.consultar dans agent.ts). Sans un nombre ici, il ne delegue jamais.\nC est dans la devise x402, PAS une part de ce que vous facturez par tache : une tache se paie en MON et une question en $PANAL.",
534
549
  vigilante: "La sentinelle. Toutes les 60 s, elle passe en revue vos taches ouvertes et recupere ce qui est reste bloque : une livraison jamais ancree, un travail mort a mi-chemin, ou une commande payee qui n est jamais arrivee.\nVIGILANTE=off la desactive. PUBLIC_URL est votre endpoint https.",
550
+ seguridad: "Durcissement. LIMITE_POR_MINUTO limite les requêtes par IP (0 le désactive) ; chaque requête non authentifiée coûte un appel RPC, donc sans plafond une boucle curl empêche votre agent de livrer.\nTRAS_PROXY=1 seulement avec un proxy devant. AUTH_ESTRICTA=1 rejette les signatures de téléchargement sans expiration.",
535
551
  },
536
552
  readme: `# {name}
537
553
 
@@ -557,6 +573,8 @@ PUBLIC_URL=https://votre-domaine npm run register
557
573
  | \`src/agent.ts\` | **Le seul que vous modifiez.** Votre travail va dans \`handleTask()\`. |
558
574
  | \`src/server.ts\` | Reçoit la commande, vérifie les signatures, livre sur la chaîne. |
559
575
  | \`src/register.ts\` | Votre fiche : nom, description, compétences, prix. |
576
+ | \`src/pdf.ts\` | Transforme du texte en PDF, sans dépendances. Supprimez-le si vous ne livrez pas de fichiers. |
577
+ | \`src/vigilante.ts\` | Récupère ce qui est resté bloqué : une livraison jamais ancrée, un travail à moitié fait. |
560
578
  | \`.env\` | Votre clé privée et celle de votre modèle. Ne le committez jamais. |
561
579
 
562
580
  ## Comment vous êtes payé
@@ -627,6 +645,7 @@ O idioma vem de --lang, depois de PANAL_LANG e, por fim, do locale do sistema.`,
627
645
  x402: 'Cobrança por chamada (opcional). Deixe vazio e o seu agente só aceita trabalhos do escrow.\nO preço é por pedido, num token EIP-2612: não pode ser MON, o esquema precisa de `permit`.',
628
646
  subcontrata: "Subcontratar (opcional, desligado por omissao). O seu agente pode pagar a outro pelo que nao sabe fazer (ver ctx.consultar em agent.ts). Sem um numero aqui, nunca delega.\nVai na moeda do x402 e NAO e uma fatia do que cobra por tarefa: uma tarefa paga-se em MON e uma pergunta em $PANAL.",
629
647
  vigilante: "O vigia. A cada 60 s reve as suas tarefas abertas e recupera o que ficou preso: uma entrega que nunca foi ancorada, trabalho que morreu a meio, ou uma encomenda paga que nunca chegou.\nVIGILANTE=off desliga-o. PUBLIC_URL e o seu endpoint https.",
648
+ seguridad: "Endurecimento. LIMITE_POR_MINUTO limita os pedidos por IP (0 desativa); cada pedido não autenticado custa uma chamada RPC, por isso sem limite um ciclo de curl deixa o seu agente sem poder entregar.\nTRAS_PROXY=1 só com um proxy à frente. AUTH_ESTRICTA=1 rejeita assinaturas de descarga sem validade.",
630
649
  },
631
650
  readme: `# {name}
632
651
 
@@ -652,6 +671,8 @@ PUBLIC_URL=https://o-seu-dominio npm run register
652
671
  | \`src/agent.ts\` | **O único que edita.** O seu trabalho vai em \`handleTask()\`. |
653
672
  | \`src/server.ts\` | Recebe o pedido, verifica assinaturas e entrega na cadeia. |
654
673
  | \`src/register.ts\` | A sua ficha: nome, descrição, competências, preço. |
674
+ | \`src/pdf.ts\` | Converte texto em PDF, sem dependências. Apague-o se não entregar ficheiros. |
675
+ | \`src/vigilante.ts\` | Recupera o que ficou preso: uma entrega sem âncora, trabalho a meio. |
655
676
  | \`.env\` | A sua chave privada e a do modelo. Nunca faça commit dele. |
656
677
 
657
678
  ## Como recebe
@@ -720,6 +741,7 @@ const ru = {
720
741
  x402: 'Плата за вызов (необязательно). Оставьте пустым — агент будет брать только заказы через эскроу.\nЦена за один запрос, в токене с EIP-2612: MON не подходит, схеме нужен `permit`.',
721
742
  subcontrata: "Субподряд (необязательно, по умолчанию выключен). Ваш агент может заплатить другому за то, чего не умеет сам (см. ctx.consultar в agent.ts). Без числа здесь он никогда не делегирует.\nУказывается в валюте x402, а НЕ долей от оплаты за задачу: задача оплачивается в MON, а вопрос в $PANAL.",
722
743
  vigilante: "Сторож. Каждые 60 с проверяет ваши открытые задачи и вытаскивает то, что застряло: доставку, которая не попала в блокчейн, работу, оборвавшуюся на середине, или оплаченный заказ, который так и не пришел.\nVIGILANTE=off выключает его. PUBLIC_URL ваш https-эндпоинт.",
744
+ seguridad: "Усиление защиты. LIMITE_POR_MINUTO ограничивает запросы с одного IP (0 отключает); каждый неаутентифицированный запрос стоит одного вызова RPC, поэтому без лимита цикл curl лишает агента возможности сдавать работу.\nTRAS_PROXY=1 только если впереди стоит обратный прокси. AUTH_ESTRICTA=1 отклоняет подписи скачивания без срока действия.",
723
745
  },
724
746
  readme: `# {name}
725
747
 
@@ -745,6 +767,8 @@ PUBLIC_URL=https://ваш-домен npm run register
745
767
  | \`src/agent.ts\` | **Единственный, который вы правите.** Ваша работа — в \`handleTask()\`. |
746
768
  | \`src/server.ts\` | Принимает заказ, проверяет подписи, сдаёт работу в блокчейн. |
747
769
  | \`src/register.ts\` | Ваша карточка: имя, описание, навыки, цена. |
770
+ | \`src/pdf.ts\` | Превращает текст в PDF, без зависимостей. Удалите, если не отдаёте файлы. |
771
+ | \`src/vigilante.ts\` | Вытаскивает застрявшее: доставку без записи в блокчейн, работу на середине. |
748
772
  | \`.env\` | Ваш приватный ключ и ключ модели. Никогда не коммитьте его. |
749
773
 
750
774
  ## Как вы получаете деньги
@@ -814,6 +838,7 @@ const bn = {
814
838
  x402: 'প্রতি কলে চার্জ (ঐচ্ছিক)। ফাঁকা রাখলে আপনার এজেন্ট কেবল এসক্রো কাজ নেবে।\nদাম প্রতি অনুরোধে, EIP-2612 টোকেনে — MON চলবে না, স্কিমটির `permit` দরকার।',
815
839
  subcontrata: "সাবকন্ট্রাক্টিং (ঐচ্ছিক, ডিফল্টে বন্ধ)। আপনার এজেন্ট যা নিজে পারে না, তার জন্য অন্যকে অর্থ দিতে পারে (agent.ts-এ ctx.consultar দেখুন)। এখানে সংখ্যা না থাকলে সে কখনও দায়িত্ব দেয় না।\nএটি x402-এর মুদ্রায়, কাজপ্রতি আয়ের অংশ নয়: কাজের দাম MON-এ আর প্রশ্নের দাম $PANAL-এ।",
816
840
  vigilante: "প্রহরী। প্রতি ৬০ সেকেন্ডে আপনার খোলা কাজগুলি দেখে এবং আটকে যাওয়া জিনিস উদ্ধার করে: চেইনে না ওঠা ডেলিভারি, মাঝপথে মরে যাওয়া কাজ, বা টাকা দেওয়া হয়েছে অথচ কখনও পৌঁছায়নি এমন আদেশ।\nVIGILANTE=off এটি বন্ধ করে। PUBLIC_URL আপনার https এন্ডপয়েন্ট।",
841
+ seguridad: "কঠোরকরণ। LIMITE_POR_MINUTO প্রতি IP অনুরোধ সীমিত করে (0 দিলে বন্ধ); প্রতিটি অননুমোদিত অনুরোধে একটি RPC কল খরচ হয়, তাই সীমা ছাড়া একটি curl লুপ আপনার এজেন্টকে ডেলিভারি করতে অক্ষম করে দেয়।\nTRAS_PROXY=1 কেবল সামনে রিভার্স প্রক্সি থাকলে। AUTH_ESTRICTA=1 মেয়াদহীন ডাউনলোড স্বাক্ষর প্রত্যাখ্যান করে।",
817
842
  },
818
843
  readme: `# {name}
819
844
 
@@ -838,6 +863,8 @@ PUBLIC_URL=https://আপনার-ডোমেইন npm run register
838
863
  | \`src/agent.ts\` | **কেবল এটিই আপনি বদলান।** আপনার কাজ যায় \`handleTask()\`-এ। |
839
864
  | \`src/server.ts\` | কাজ গ্রহণ করে, স্বাক্ষর যাচাই করে, চেইনে ডেলিভার করে। |
840
865
  | \`src/register.ts\` | আপনার পরিচিতি: নাম, বিবরণ, দক্ষতা, দাম। |
866
+ | \`src/pdf.ts\` | টেক্সট থেকে PDF বানায়, নির্ভরতা ছাড়াই। ফাইল না দিলে মুছে দিন। |
867
+ | \`src/vigilante.ts\` | আটকে যাওয়া কাজ উদ্ধার করে: চেইনে না ওঠা ডেলিভারি, মাঝপথে থামা কাজ। |
841
868
  | \`.env\` | আপনার প্রাইভেট কী ও মডেল কী। কখনও কমিট করবেন না। |
842
869
 
843
870
  ## আপনি কীভাবে অর্থ পান
@@ -905,6 +932,7 @@ const ur = {
905
932
  x402: 'فی کال وصولی (اختیاری)۔ خالی چھوڑ دیں تو آپ کا ایجنٹ صرف ایسکرو کے کام لے گا۔\nقیمت فی درخواست ہے، EIP-2612 ٹوکن میں — MON نہیں چل سکتا، اس اسکیم کو `permit` چاہیے۔',
906
933
  subcontrata: "سب کنٹریکٹنگ (اختیاری، بذریعہ ڈیفالٹ بند)۔ آپ کا ایجنٹ جو خود نہیں کر سکتا اس کے لیے دوسرے کو ادائیگی کر سکتا ہے (agent.ts میں ctx.consultar دیکھیں)۔ یہاں نمبر کے بغیر وہ کبھی نہیں سونپتا۔\nیہ x402 کی کرنسی میں ہے، فی ٹاسک آمدنی کا حصہ نہیں: ٹاسک MON میں اور سوال $PANAL میں ادا ہوتا ہے۔",
907
934
  vigilante: "نگہبان۔ ہر 60 سیکنڈ میں آپ کے کھلے کام دیکھتا ہے اور جو اٹکا ہے اسے بچاتا ہے: وہ ڈیلیوری جو چین پر درج نہ ہوئی، وہ کام جو بیچ میں مر گیا، یا ادا شدہ آرڈر جو کبھی نہ پہنچا۔\nVIGILANTE=off اسے بند کرتا ہے۔ PUBLIC_URL آپ کا https اینڈ پوائنٹ ہے۔",
935
+ seguridad: "سختی۔ LIMITE_POR_MINUTO فی IP درخواستیں محدود کرتا ہے (0 سے بند)؛ ہر غیر تصدیق شدہ درخواست ایک RPC کال خرچ کرتی ہے، اس لیے حد کے بغیر ایک curl لوپ آپ کے ایجنٹ کو ڈیلیور کرنے سے روک سکتا ہے۔\nTRAS_PROXY=1 صرف اس صورت جب آگے ریورس پراکسی ہو۔ AUTH_ESTRICTA=1 بغیر میعاد والے ڈاؤن لوڈ دستخط مسترد کرتا ہے۔",
908
936
  },
909
937
  readme: `# {name}
910
938
 
@@ -929,6 +957,8 @@ PUBLIC_URL=https://آپ-کا-ڈومین npm run register
929
957
  | \`src/agent.ts\` | **صرف یہی آپ بدلتے ہیں۔** آپ کا کام \`handleTask()\` میں جاتا ہے۔ |
930
958
  | \`src/server.ts\` | کام وصول کرتا ہے، دستخط جانچتا ہے، چین پر ڈیلیور کرتا ہے۔ |
931
959
  | \`src/register.ts\` | آپ کا تعارف: نام، تفصیل، مہارتیں، قیمت۔ |
960
+ | \`src/pdf.ts\` | متن کو PDF میں بدلتا ہے، بغیر انحصار کے۔ فائلیں نہ دیں تو حذف کر دیں۔ |
961
+ | \`src/vigilante.ts\` | اٹکے ہوئے کام واپس لاتا ہے: چین پر درج نہ ہوئی ڈیلیوری، ادھورا کام۔ |
932
962
  | \`.env\` | آپ کی نجی کلید اور ماڈل کی کلید۔ اسے کبھی کمٹ نہ کریں۔ |
933
963
 
934
964
  ## آپ کو ادائیگی کیسے ملتی ہے
package/dist/index.js CHANGED
@@ -177,6 +177,11 @@ function writeEnvExample(dest, t) {
177
177
  'VIGILANTE_SEGUNDOS=60',
178
178
  'PUBLIC_URL=',
179
179
  '',
180
+ bloque(t.env.seguridad),
181
+ 'LIMITE_POR_MINUTO=60',
182
+ '# TRAS_PROXY=1',
183
+ '# AUTH_ESTRICTA=1',
184
+ '',
180
185
  ].join('\n');
181
186
  writeFileSync(join(dest, '.env.example'), contenido, 'utf8');
182
187
  return contenido;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-panal-agent",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Crea un agente de IA para Panal, funcionando y cobrando on-chain, en cinco minutos",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "@panal/sdk": "^0.6.1",
13
+ "@panal/sdk": "^0.7.0",
14
14
  "dotenv": "^17.0.0",
15
15
  "tsx": "^4.19.0",
16
16
  "viem": "^2.21.0"
@@ -248,7 +248,21 @@ const inFlight = new Set<string>();
248
248
  // ---------------------------------------------------------------------------
249
249
 
250
250
  const briefSignMessage = (taskId: bigint) => `Panal brief #${taskId}`;
251
- const resultSignMessage = (taskId: bigint) => `Panal resultado #${taskId}`;
251
+ /** Formato ANTIGUO, sin caducidad. Se sigue aceptando; ver `credencialValida`. */
252
+ const resultSignMessageLegacy = (taskId: bigint) => `Panal resultado #${taskId}`;
253
+ /** Formato actual: la firma dice hasta cuándo vale. */
254
+ const resultSignMessage = (taskId: bigint, expira: number) => `Panal resultado #${taskId} · ${expira}`;
255
+
256
+ /**
257
+ * Cuánto puede durar como mucho una firma de descarga.
258
+ *
259
+ * El cliente elige cuándo caduca la suya y este tope acota lo que se acepta:
260
+ * sin él, firmar una válida hasta el año 2100 sería lo mismo que no caducar.
261
+ */
262
+ const MAX_VENTANA_S = 15 * 60;
263
+
264
+ /** Rechaza el formato antiguo (sin caducidad). Pásalo a 1 cuando puedas. */
265
+ const AUTH_ESTRICTA = process.env.AUTH_ESTRICTA === '1';
252
266
 
253
267
  async function signedBy(message: string, signature: string, expected: Address): Promise<boolean> {
254
268
  try {
@@ -258,6 +272,91 @@ async function signedBy(message: string, signature: string, expected: Address):
258
272
  }
259
273
  }
260
274
 
275
+ /**
276
+ * Las credenciales de una descarga: de dónde se leen y si valen.
277
+ *
278
+ * SE LEEN DE LAS CABECERAS, no de la query. La firma abre el resultado y TODOS
279
+ * los archivos de una tarea, así que es un pase de acceso — y en la query
280
+ * acababa escrita en el log de accesos del proxy, en el historial del navegador
281
+ * y en cualquier intermediario del camino. Se encontraron 23 en un log de
282
+ * producción, en claro. Un pase que se registra en un archivo de texto no es
283
+ * un pase.
284
+ *
285
+ * La query se sigue leyendo porque los clientes publicados antes de esto la
286
+ * usan, y romperles la descarga no arregla nada. Pero avisa.
287
+ */
288
+ function credencialesDe(
289
+ req: IncomingMessage,
290
+ url: URL,
291
+ ): { address: string | null; signature: string | null; expira: string | null; porQuery: boolean } {
292
+ const cabecera = (n: string): string | null => {
293
+ const v = req.headers[n];
294
+ return typeof v === 'string' && v.trim() ? v.trim() : null;
295
+ };
296
+ const address = cabecera('x-panal-address');
297
+ const signature = cabecera('x-panal-signature');
298
+ if (address && signature) {
299
+ return { address, signature, expira: cabecera('x-panal-expira'), porQuery: false };
300
+ }
301
+ return {
302
+ address: url.searchParams.get('address'),
303
+ signature: url.searchParams.get('signature'),
304
+ expira: url.searchParams.get('expira'),
305
+ porQuery: true,
306
+ };
307
+ }
308
+
309
+ /**
310
+ * ¿La firma abre esta tarea?
311
+ *
312
+ * La caducidad la MANDA el cliente y va dentro de lo firmado, así que no se
313
+ * puede estirar: cambiar el número invalida la firma. Mandarla en claro no
314
+ * regala nada y ahorra lo que sí sería un problema — adivinarla probando
315
+ * segundo a segundo son cientos de verificaciones de firma por petición, o
316
+ * sea un ataque de denegación montado por uno mismo.
317
+ */
318
+ /** Un aviso por tarea: repetirlo en cada archivo llenaría el log de ruido. */
319
+ const avisadasPorQuery = new Set<string>();
320
+ function avisaQuery(taskId: bigint): void {
321
+ const k = taskId.toString();
322
+ if (avisadasPorQuery.has(k)) return;
323
+ avisadasPorQuery.add(k);
324
+ console.error(
325
+ `[panal] #${taskId} credenciales por QUERY STRING. Acaban en el log de accesos del proxy ` +
326
+ 'y en el historial del navegador. Actualiza el cliente: van en cabeceras.',
327
+ );
328
+ }
329
+
330
+ async function credencialValida(
331
+ taskId: bigint,
332
+ signature: string,
333
+ expiraCrudo: string | null,
334
+ cliente: Address,
335
+ ): Promise<boolean> {
336
+ const ahora = Math.floor(Date.now() / 1000);
337
+
338
+ if (expiraCrudo !== null) {
339
+ const expira = Number(expiraCrudo);
340
+ if (!Number.isInteger(expira)) return false;
341
+ // Ni caducada, ni válida durante un año: el tope es lo que impide que una
342
+ // firma filtrada valga para siempre, que es el motivo de todo esto.
343
+ if (expira <= ahora || expira > ahora + MAX_VENTANA_S) return false;
344
+ return signedBy(resultSignMessage(taskId, expira), signature, cliente);
345
+ }
346
+
347
+ // Sin caducidad: formato antiguo. Se acepta para no romper a los clientes ya
348
+ // publicados, y se avisa en cada uso.
349
+ if (AUTH_ESTRICTA) return false;
350
+ if (await signedBy(resultSignMessageLegacy(taskId), signature, cliente)) {
351
+ console.error(
352
+ `[panal] #${taskId} descarga con firma SIN CADUCIDAD (formato antiguo). ` +
353
+ 'Actualiza el cliente; con AUTH_ESTRICTA=1 esto se rechaza.',
354
+ );
355
+ return true;
356
+ }
357
+ return false;
358
+ }
359
+
261
360
  // ---------------------------------------------------------------------------
262
361
  // El trabajo
263
362
  // ---------------------------------------------------------------------------
@@ -443,17 +542,106 @@ async function readBody(req: IncomingMessage): Promise<string> {
443
542
  return Buffer.concat(chunks).toString('utf8');
444
543
  }
445
544
 
545
+ // ---------------------------------------------------------------------------
546
+ // Aguantar el ruido: límite de peticiones y caché de la tarea
547
+ // ---------------------------------------------------------------------------
548
+ //
549
+ // Cada petición a /result, /files o /brief cuesta una llamada al RPC ANTES de
550
+ // poder verificar nada — hay que leer la tarea para saber quién es su cliente.
551
+ // El RPC público limita a ~15 llamadas/s, así que un bucle de curl sin
552
+ // autenticar agotaba esa cuota y dejaba al agente sin poder entregar su trabajo
553
+ // real, con el dinero de clientes legítimos bloqueado hasta que vencía el plazo.
554
+
555
+ /** Peticiones por minuto y por IP. 0 lo desactiva. */
556
+ const LIMITE_POR_MINUTO = (() => {
557
+ const n = Number(process.env.LIMITE_POR_MINUTO?.trim() || '60');
558
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 60;
559
+ })();
560
+
561
+ /**
562
+ * Confiar en `x-forwarded-for`. Solo con un proxy delante (Caddy, nginx).
563
+ *
564
+ * Apagado por defecto a propósito: si se confía sin proxy, cualquiera manda esa
565
+ * cabecera con una IP inventada por petición y el límite deja de existir.
566
+ */
567
+ const TRAS_PROXY = process.env.TRAS_PROXY === '1';
568
+
569
+ const cubos = new Map<string, { n: number; hasta: number }>();
570
+
571
+ function ipDe(req: IncomingMessage): string {
572
+ if (TRAS_PROXY) {
573
+ const xff = req.headers['x-forwarded-for'];
574
+ const primera = (Array.isArray(xff) ? xff[0] : xff)?.split(',')[0]?.trim();
575
+ if (primera) return primera;
576
+ }
577
+ return req.socket.remoteAddress ?? 'desconocida';
578
+ }
579
+
580
+ /** true si hay que rechazar. Ventana fija de un minuto, que sobra aquí. */
581
+ function pasaDelLimite(req: IncomingMessage): boolean {
582
+ if (LIMITE_POR_MINUTO === 0) return false;
583
+ const ahora = Date.now();
584
+ const ip = ipDe(req);
585
+ const cubo = cubos.get(ip);
586
+ if (!cubo || cubo.hasta <= ahora) {
587
+ // Se limpia aquí y no con un temporizador: sin esto el mapa crece sin fin
588
+ // con una IP distinta por petición, que es su propia forma de tumbarlo.
589
+ if (cubos.size > 10_000) for (const [k, v] of cubos) if (v.hasta <= ahora) cubos.delete(k);
590
+ cubos.set(ip, { n: 1, hasta: ahora + 60_000 });
591
+ return false;
592
+ }
593
+ cubo.n += 1;
594
+ return cubo.n > LIMITE_POR_MINUTO;
595
+ }
596
+
597
+ /**
598
+ * La tarea, cacheada unos segundos.
599
+ *
600
+ * Solo para las rutas de LECTURA (/result, /files), y solo se usa su `client`,
601
+ * que no cambia nunca. El camino del encargo NO la usa: ahí se mira el estado y
602
+ * el hash, y servir un estado de hace cinco segundos podría aceptar un encargo
603
+ * de una tarea que acaba de cerrarse.
604
+ *
605
+ * Además de aguantar el ruido, ahorra lo obvio: bajarse cuatro archivos de una
606
+ * entrega hacía cuatro lecturas idénticas de la misma tarea.
607
+ */
608
+ const CACHE_TAREA_MS = 5_000;
609
+ const tareasCache = new Map<string, { cliente: Address; hasta: number }>();
610
+
611
+ async function clienteDeTarea(taskId: bigint): Promise<Address> {
612
+ const k = taskId.toString();
613
+ const ahora = Date.now();
614
+ const cacheada = tareasCache.get(k);
615
+ if (cacheada && cacheada.hasta > ahora) return cacheada.cliente;
616
+ const task = await panal.getTask(taskId);
617
+ if (tareasCache.size > 1_000) for (const [kk, v] of tareasCache) if (v.hasta <= ahora) tareasCache.delete(kk);
618
+ tareasCache.set(k, { cliente: task.client, hasta: ahora + CACHE_TAREA_MS });
619
+ return task.client;
620
+ }
621
+
446
622
  const server = createServer((req, res) => {
447
623
  void (async () => {
448
624
  const url = new URL(req.url ?? '/', 'http://localhost');
449
625
 
626
+ if (pasaDelLimite(req)) {
627
+ res.setHeader('retry-after', '60');
628
+ json(res, 429, { error: 'too many requests' });
629
+ return;
630
+ }
631
+
450
632
  // El dashboard vive en otro dominio: sin CORS el cliente no puede ni
451
633
  // mandarte el brief ni descargar su resultado.
452
634
  res.setHeader('access-control-allow-origin', 'https://panal.lat');
453
635
  res.setHeader('vary', 'origin');
454
636
  if (req.method === 'OPTIONS') {
455
637
  res.setHeader('access-control-allow-methods', 'GET, POST, OPTIONS');
456
- res.setHeader('access-control-allow-headers', 'content-type');
638
+ // Las credenciales van en cabeceras propias, y esas NO son simples: sin
639
+ // declararlas aquí el navegador bloquea la descarga en el preflight.
640
+ res.setHeader(
641
+ 'access-control-allow-headers',
642
+ 'content-type, x-panal-address, x-panal-signature, x-panal-expira, x-payment, x-payment-payer',
643
+ );
644
+ res.setHeader('access-control-max-age', '86400');
457
645
  res.writeHead(204).end();
458
646
  return;
459
647
  }
@@ -686,19 +874,20 @@ const server = createServer((req, res) => {
686
874
  const match = /^\/result\/(\d+)$/.exec(url.pathname);
687
875
  if (match && req.method === 'GET') {
688
876
  const taskId = BigInt(match[1]!);
689
- const address = url.searchParams.get('address');
690
- const signature = url.searchParams.get('signature');
691
- if (!address || !signature) {
692
- json(res, 400, { error: 'faltan address y signature' });
877
+ const cred = credencialesDe(req, url);
878
+ if (!cred.address || !cred.signature) {
879
+ json(res, 400, { error: 'faltan address y signature (cabeceras x-panal-address / x-panal-signature)' });
693
880
  return;
694
881
  }
695
- const task = await panal.getTask(taskId);
696
- if (address.toLowerCase() !== task.client.toLowerCase()) {
882
+ if (cred.porQuery) avisaQuery(taskId);
883
+ // Cacheado: aquí solo se usa el cliente, que no cambia nunca.
884
+ const cliente = await clienteDeTarea(taskId);
885
+ if (cred.address.toLowerCase() !== cliente.toLowerCase()) {
697
886
  json(res, 403, { error: 'solo el cliente de la tarea puede descargar el resultado' });
698
887
  return;
699
888
  }
700
- if (!(await signedBy(resultSignMessage(taskId), signature, task.client))) {
701
- json(res, 401, { error: 'firma inválida' });
889
+ if (!(await credencialValida(taskId, cred.signature, cred.expira, cliente))) {
890
+ json(res, 401, { error: 'firma inválida o caducada' });
702
891
  return;
703
892
  }
704
893
  const text = loadResult(taskId);
@@ -718,19 +907,20 @@ const server = createServer((req, res) => {
718
907
  const archivo = /^\/files\/(\d+)\/([^/]+)$/.exec(url.pathname);
719
908
  if (archivo && req.method === 'GET') {
720
909
  const taskId = BigInt(archivo[1]!);
721
- const address = url.searchParams.get('address');
722
- const signature = url.searchParams.get('signature');
723
- if (!address || !signature) {
724
- json(res, 400, { error: 'faltan address y signature' });
910
+ const cred = credencialesDe(req, url);
911
+ if (!cred.address || !cred.signature) {
912
+ json(res, 400, { error: 'faltan address y signature (cabeceras x-panal-address / x-panal-signature)' });
725
913
  return;
726
914
  }
727
- const task = await panal.getTask(taskId);
728
- if (address.toLowerCase() !== task.client.toLowerCase()) {
915
+ if (cred.porQuery) avisaQuery(taskId);
916
+ // Cacheado: aquí solo se usa el cliente, que no cambia nunca.
917
+ const cliente = await clienteDeTarea(taskId);
918
+ if (cred.address.toLowerCase() !== cliente.toLowerCase()) {
729
919
  json(res, 403, { error: 'solo el cliente de la tarea puede descargar sus archivos' });
730
920
  return;
731
921
  }
732
- if (!(await signedBy(resultSignMessage(taskId), signature, task.client))) {
733
- json(res, 401, { error: 'firma inválida' });
922
+ if (!(await credencialValida(taskId, cred.signature, cred.expira, cliente))) {
923
+ json(res, 401, { error: 'firma inválida o caducada' });
734
924
  return;
735
925
  }
736
926
 
@@ -63,13 +63,31 @@ export interface VigilanteDeps {
63
63
  urlPublica?: string;
64
64
  }
65
65
 
66
- /** Cada cuánto se mira, en segundos. */
66
+ /** Cada cuánto se mira cuando hay movimiento, en segundos. */
67
67
  const CADA = (() => {
68
68
  const n = Number(process.env.VIGILANTE_SEGUNDOS?.trim() || '60');
69
69
  // Menos de 15 s no aporta nada y sí gasta el límite del RPC.
70
70
  return Number.isFinite(n) && n >= 15 ? Math.floor(n) : 60;
71
71
  })();
72
72
 
73
+ /**
74
+ * Cuántas vueltas en blanco antes de bajar el ritmo, y a cuánto se baja.
75
+ *
76
+ * Un agente parado pregunta `getTaskCount()` cada 60 s aunque no pase nada.
77
+ * Una gota. Pero el RPC público es COMPARTIDO y corta cerca de 50 llamadas
78
+ * concurrentes: con mil agentes son 16,7 llamadas/s permanentes, y entre
79
+ * todos ahogan el pozo del que bebe también el indexador — que es de quien
80
+ * depende el catálogo entero del mercado.
81
+ *
82
+ * Así que tras un rato sin encontrar nada —el caso normal— se pasa a mirar
83
+ * cada cinco minutos. Al primer hallazgo se vuelve al ritmo corto.
84
+ *
85
+ * Lo que cuesta: un encargo perdido se detecta en cinco minutos en vez de en
86
+ * uno. Los plazos se miden en horas, así que no cambia nada para nadie.
87
+ */
88
+ const VUELTAS_EN_BLANCO = 20;
89
+ const CADA_TRANQUILO = Math.max(CADA, 300);
90
+
73
91
  /**
74
92
  * Cuánto se espera antes de dar por perdido un encargo.
75
93
  *
@@ -112,7 +130,8 @@ export function arrancarVigilante(deps: VigilanteDeps): { parar: () => void } {
112
130
  }
113
131
  };
114
132
 
115
- async function repasar(): Promise<void> {
133
+ /** true si encontro algo que atender: eso es lo que decide el ritmo. */
134
+ async function repasar(): Promise<boolean> {
116
135
  const total = await deps.panal.getTaskCount();
117
136
  const marca = leerMarca();
118
137
  // La primera vez se miran las últimas REPASO_INICIAL en vez de las 30.000
@@ -124,11 +143,11 @@ export function arrancarVigilante(deps: VigilanteDeps): { parar: () => void } {
124
143
  for (let i = desde; i < total; i++) pendientes.add(i.toString());
125
144
  if (pendientes.size === 0) {
126
145
  escribirMarca(total - 1n);
127
- return;
146
+ return false;
128
147
  }
129
148
 
130
149
  for (const id of pendientes) {
131
- if (parado) return;
150
+ if (parado) return true;
132
151
  const taskId = BigInt(id);
133
152
  try {
134
153
  await revisarUna(taskId);
@@ -137,6 +156,8 @@ export function arrancarVigilante(deps: VigilanteDeps): { parar: () => void } {
137
156
  }
138
157
  }
139
158
  escribirMarca(total - 1n);
159
+ // Habia algo que mirar, aunque no fuera nuestro: no es una vuelta en blanco.
160
+ return true;
140
161
  }
141
162
 
142
163
  async function revisarUna(taskId: bigint): Promise<void> {
@@ -228,17 +249,48 @@ export function arrancarVigilante(deps: VigilanteDeps): { parar: () => void } {
228
249
  // se perdió mientras el proceso estaba caído.
229
250
  void repasar().catch((err) => console.error(`[vigilante] primer repaso: ${err instanceof Error ? err.message : err}`));
230
251
 
231
- const timer = setInterval(() => {
232
- void repasar().catch((err) => console.error(`[vigilante] ${err instanceof Error ? err.message : err}`));
233
- }, CADA * 1000);
234
- // Sin unref, este intervalo mantiene vivo el proceso para siempre aunque
235
- // todo lo demás haya terminado.
236
- timer.unref?.();
252
+ // El ritmo se reprograma en vez de usar un intervalo fijo: así puede
253
+ // aflojar solo cuando lleva un rato sin encontrar nada.
254
+ let enBlanco = 0;
255
+ let tranquilo = false;
256
+ let timer: ReturnType<typeof setTimeout> | null = null;
257
+
258
+ const programar = (ms: number): void => {
259
+ timer = setTimeout(() => {
260
+ void (async () => {
261
+ let hizoAlgo = false;
262
+ try {
263
+ hizoAlgo = await repasar();
264
+ } catch (err) {
265
+ console.error(`[vigilante] ${err instanceof Error ? err.message : err}`);
266
+ }
267
+
268
+ if (hizoAlgo) {
269
+ enBlanco = 0;
270
+ if (tranquilo) {
271
+ tranquilo = false;
272
+ console.log(`[vigilante] hay movimiento: vuelvo a mirar cada ${CADA} s`);
273
+ }
274
+ } else if (++enBlanco >= VUELTAS_EN_BLANCO && !tranquilo) {
275
+ tranquilo = true;
276
+ console.log(
277
+ `[vigilante] ${VUELTAS_EN_BLANCO} vueltas sin nada: paso a mirar cada ${CADA_TRANQUILO} s ` +
278
+ 'para no cargar el RPC compartido. Vuelvo al ritmo corto en cuanto haya algo.',
279
+ );
280
+ }
281
+ if (!parado) programar((tranquilo ? CADA_TRANQUILO : CADA) * 1000);
282
+ })();
283
+ }, ms);
284
+ // Sin unref, este temporizador mantiene vivo el proceso para siempre
285
+ // aunque todo lo demás haya terminado.
286
+ timer.unref?.();
287
+ };
288
+ programar(CADA * 1000);
237
289
 
238
290
  return {
239
291
  parar: () => {
240
292
  parado = true;
241
- clearInterval(timer);
293
+ if (timer) clearTimeout(timer);
242
294
  },
243
295
  };
244
296
  }