kooni-bot 0.2.16 → 0.3.1

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.
Files changed (2) hide show
  1. package/bin/kooni.js +55 -29
  2. package/package.json +1 -1
package/bin/kooni.js CHANGED
@@ -16,13 +16,24 @@ import { emitKeypressEvents } from "node:readline";
16
16
  import { stdin as input, stdout as output } from "node:process";
17
17
  import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync, readdirSync, statSync, cpSync } from "node:fs";
18
18
  import { realpathSync } from "node:fs";
19
- import { join, basename } from "node:path";
19
+ import { join, basename, dirname } from "node:path";
20
20
  import { homedir } from "node:os";
21
21
  import { execFileSync, spawnSync } from "node:child_process";
22
- import { randomUUID, createHmac } from "node:crypto";
22
+ import { randomUUID, createPublicKey, verify as edVerify } from "node:crypto";
23
23
  import { fileURLToPath } from "node:url";
24
24
 
25
- const CLI_VERSION = "0.2.16";
25
+ // Leída del package.json publicado junto a este archivo — nunca hardcodeada,
26
+ // para que --version y la telemetría (cliVersion) no se desincronicen del
27
+ // número real publicado en npm (bug encontrado: quedó fija en "0.2.17" tras
28
+ // el bump a 0.3.0).
29
+ const CLI_VERSION = (() => {
30
+ try {
31
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
32
+ return JSON.parse(readFileSync(pkgPath, "utf8")).version || "0.0.0";
33
+ } catch {
34
+ return "0.0.0";
35
+ }
36
+ })();
26
37
 
27
38
  const REPO = process.env.KOONI_REPO || "iamnocodeveloper/kooni-bot";
28
39
  const BRANCH = process.env.KOONI_BRANCH || "main";
@@ -37,12 +48,20 @@ const INSTALLS_FILE = join(CFG_DIR, "installs.json");
37
48
  const MARKER = ".kooni-bot.json";
38
49
  const SKILL_DIR = join(homedir(), ".claude", "skills", "kooni");
39
50
 
40
- // Llave maestra de licencias (HMAC de los códigos KOONI-PRO-…). DEBE coincidir
41
- // con el secret LICENSE_MASTER_KEY del panel de licencias (InsForge kooni-licencias)
42
- // para que los códigos que genera el panel validen en TODAS las instalaciones.
43
- // Tradeoff conocido de la validación local HMAC: la llave viaja en cada worker.
44
- // La mejora recomendada (firma asimétrica, pública en el worker) está en PLAN.md.
45
- const LICENSE_MASTER_KEY = process.env.KOONI_LICENSE_MASTER_KEY || "KqQGmLK7yMl-MISS2JtMd0bCY5lws_a926ksFCIkZkk";
51
+ // Clave PÚBLICA de licencias (Ed25519). Verifica códigos KOONI-PRO-V2-… pero NO
52
+ // puede firmarlos: es segura de publicar en npm — ese es el punto de la v2.
53
+ // DEBE ser la misma que está embebida en src/license.ts del bot. La PRIVADA vive
54
+ // solo en el panel de licencias (InsForge, secret LICENSE_PRIVATE_KEY).
55
+ //
56
+ // v2 (2026-09-01): reemplaza a LICENSE_MASTER_KEY (HMAC), que viajaba en este
57
+ // mismo archivo público y permitía falsificar licencias — hallazgo S2 del PLAN.
58
+ const LICENSE_PUBLIC_KEY = process.env.KOONI_LICENSE_PUBLIC_KEY || "MCowBQYDK2VwAyEAqpP9OBrju8ebMWjQM4uYLsUV5yqWG8k8ieozT8Me8EQ=";
59
+
60
+ // Token compartido de registro/uso (X-Kooni-Token). DEBE coincidir con el secret
61
+ // REGISTER_TOKEN del panel de licencias (InsForge). Es "seguridad por oscuridad"
62
+ // (vive en el CLI público y en cada worker — ver tarea S4 en PLAN.md): el blindaje
63
+ // real lo dan la validación de formato y el rechazo de uids desconocidos.
64
+ const KOONI_REGISTER_TOKEN = process.env.KOONI_REGISTER_TOKEN || "1f8740b841b71f21eba510eb12b8e6870b5949a916cf1162";
46
65
 
47
66
  // ── color ─────────────────────────────────────────────────────────────────────
48
67
  const C = {
@@ -664,6 +683,13 @@ function stampWrangler(dir, answers, botUid) {
664
683
  s = s.replace(/^(\s*\[vars\][^\n]*\n)/m, `$1BOT_INSTANCE_ID = "${uid}"\n`);
665
684
  }
666
685
 
686
+ // Token compartido de registro/uso (panel de licencias del dueño).
687
+ if (hasInVars("KOONI_REGISTER_TOKEN")) {
688
+ set(/KOONI_REGISTER_TOKEN\s*=\s*"[^"]*"/g, `KOONI_REGISTER_TOKEN = "${KOONI_REGISTER_TOKEN}"`);
689
+ } else {
690
+ s = s.replace(/^(\s*\[vars\][^\n]*\n)/m, `$1KOONI_REGISTER_TOKEN = "${KOONI_REGISTER_TOKEN}"\n`);
691
+ }
692
+
667
693
  if (answers.provider !== "anthropic") {
668
694
  if (hasInVars("LLM_PROVIDER")) {
669
695
  set(/LLM_PROVIDER\s*=\s*"[^"]*"/g, `LLM_PROVIDER = "${answers.provider}"`);
@@ -751,9 +777,10 @@ function writeDevVars(dir, answers, kbToken) {
751
777
  lines.push("# " + answers.secret + "=<tu-api-key> ← para wrangler dev local, pégalo aquí (o usa el panel)");
752
778
  lines.push(`DASHBOARD_PASSWORD=${answers.dashPassword || "kooni-local-password"}`);
753
779
  lines.push(`KB_REINDEX_TOKEN=${kbToken}`);
754
- // Llave maestra de licencias: igual en todas las instalaciones (debe coincidir
755
- // con la del panel de licencias para que los códigos KOONI-PRO validen).
756
- lines.push(`LICENSE_MASTER_KEY=${LICENSE_MASTER_KEY}`);
780
+ // (v2) Ya NO se escribe ninguna llave de licencias: el bot verifica con la
781
+ // clave pública que trae embebida y no necesita ningún secret para eso.
782
+ // Token compartido de registro/uso (X-Kooni-Token para el panel de licencias).
783
+ lines.push(`KOONI_REGISTER_TOKEN=${KOONI_REGISTER_TOKEN}`);
757
784
  writeFileSync(join(dir, ".dev.vars"), lines.join("\n") + "\n");
758
785
  }
759
786
 
@@ -887,7 +914,7 @@ async function onboarding(rl, answers, defaultDir) {
887
914
  : !!(flags.license || answers.licenseCode);
888
915
  if (wantsPro) {
889
916
  const code = answers.licenseCode || (interactive()
890
- ? await ask(rl, m("Pega tu código KOONI-PRO-…:", "Paste your KOONI-PRO-… code:"), undefined)
917
+ ? await ask(rl, m("Pega tu código KOONI-PRO-V2-…:", "Paste your KOONI-PRO-V2-… code:"), undefined)
891
918
  : String(flags.license || "").trim());
892
919
  if (code && verifyKooniLicense(code)) {
893
920
  answers.licenseCode = code.trim();
@@ -901,12 +928,13 @@ async function onboarding(rl, answers, defaultDir) {
901
928
  }
902
929
 
903
930
  // ── licencia Pro ─────────────────────────────────────────────────────────────
904
- // Valida un código KOONI-PRO-… localmente (mismo formato que src/license.ts del
905
- // bot): HMAC-SHA256(payload, LICENSE_MASTER_KEY). No exige que esté ligado a
906
- // esta instalación (eso lo revalida el panel al leerlo).
931
+ // Valida un código KOONI-PRO-V2-… localmente (mismo formato que src/license.ts
932
+ // del bot): firma Ed25519 verificada con la clave PÚBLICA. No exige que esté
933
+ // ligado a esta instalación (eso lo revalida el panel al leerlo).
934
+ // Sin red: la verificación es puramente local, igual que antes.
907
935
  function verifyKooniLicense(code) {
908
936
  const trimmed = String(code || "").trim();
909
- const prefix = "KOONI-PRO-";
937
+ const prefix = "KOONI-PRO-V2-";
910
938
  if (!trimmed.startsWith(prefix)) return false;
911
939
  const rest = trimmed.slice(prefix.length);
912
940
  const dot = rest.lastIndexOf(".");
@@ -914,8 +942,12 @@ function verifyKooniLicense(code) {
914
942
  const enc = rest.slice(0, dot);
915
943
  const sig = rest.slice(dot + 1);
916
944
  try {
917
- const calc = createHmac("sha256", LICENSE_MASTER_KEY).update(enc).digest("hex");
918
- if (calc !== sig) return false;
945
+ const pub = createPublicKey({
946
+ key: Buffer.from(LICENSE_PUBLIC_KEY, "base64"),
947
+ format: "der",
948
+ type: "spki",
949
+ });
950
+ if (!edVerify(null, Buffer.from(enc, "utf8"), pub, Buffer.from(sig, "hex"))) return false;
919
951
  const payload = JSON.parse(Buffer.from(enc, "base64url").toString("utf8"));
920
952
  if (payload.kind === "monthly" && payload.expiry && Date.now() > payload.expiry) return false;
921
953
  return true;
@@ -1168,14 +1200,8 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
1168
1200
  }
1169
1201
  }
1170
1202
 
1171
- // Llave maestra de licencias: TODAS las instalaciones la llevan, para que los
1172
- // códigos KOONI-PRO-… generados desde el panel validen aquí.
1173
- if (LICENSE_MASTER_KEY && LICENSE_MASTER_KEY !== "KqQGmLK7yMl-MISS2JtMd0bCY5lws_a926ksFCIkZkk") {
1174
- try {
1175
- wrangler(dir, ["secret", "put", "LICENSE_MASTER_KEY"], { input: LICENSE_MASTER_KEY, capture: true });
1176
- console.log(" " + C.green("✓") + " " + t().secretOk("LICENSE_MASTER_KEY"));
1177
- } catch {}
1178
- }
1203
+ // (v2) No hay secret de licencias que instalar: el worker verifica los códigos
1204
+ // con la clave PÚBLICA embebida en su propio código. Una llave menos viajando.
1179
1205
 
1180
1206
  // dependencias + migraciones + deploy (progreso visible en vivo)
1181
1207
  console.log("\n " + C.dim(t().installing));
@@ -1251,7 +1277,7 @@ async function checkin(dir, answers, version) {
1251
1277
  const slug = answers.slug || marker.slug || basename(dir);
1252
1278
  await fetchTimeout(CHECKIN_URL, {
1253
1279
  method: "POST",
1254
- headers: { "Content-Type": "application/json" },
1280
+ headers: { "Content-Type": "application/json", "X-Kooni-Token": KOONI_REGISTER_TOKEN },
1255
1281
  body: JSON.stringify({
1256
1282
  email: answers.email || undefined,
1257
1283
  slug,
@@ -1700,7 +1726,7 @@ ${C.dim(" Flags de init (modo no-interactivo, para agentes):")}
1700
1726
  ${C.dim(" --yes --slug <slug> --negocio <nombre> --bot-name <nombre> --lang es-MX|es-ES|en|pt-BR")}
1701
1727
  ${C.dim(" --tier free|pro --cerebro claude|chatgpt|grok|gateway --base-url <url>")}
1702
1728
  ${C.dim(" --que --ofrece --horario --ubicacion --telefono --web --pagos --faq --reglas --tono")}
1703
- ${C.dim(" --no-deploy --no-agent-skill --email <correo> --license <codigo-KOONI-PRO>")}
1729
+ ${C.dim(" --no-deploy --no-agent-skill --email <correo> --license <codigo-KOONI-PRO-V2>")}
1704
1730
  `);
1705
1731
  }
1706
1732
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kooni-bot",
3
- "version": "0.2.16",
3
+ "version": "0.3.1",
4
4
  "description": "Kooni — instala tu asistente de IA multicanal (WhatsApp, Instagram, Messenger, Telegram) en TU Cloudflare, en un comando.",
5
5
  "license": "MIT",
6
6
  "type": "module",