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.
- package/bin/kooni.js +55 -29
- 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,
|
|
22
|
+
import { randomUUID, createPublicKey, verify as edVerify } from "node:crypto";
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
24
|
|
|
25
|
-
|
|
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
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
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
|
-
//
|
|
755
|
-
//
|
|
756
|
-
|
|
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
|
|
905
|
-
// bot):
|
|
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
|
|
918
|
-
|
|
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
|
-
//
|
|
1172
|
-
//
|
|
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