komplian 0.4.0 → 0.4.3
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 +16 -4
- package/komplian-onboard.mjs +2 -1
- package/komplian-postman.mjs +553 -79
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,17 +10,29 @@
|
|
|
10
10
|
|
|
11
11
|
1. En [Postman](https://postman.com) usa una cuenta con email **`@komplian.com`** (o añade ese email a tu perfil).
|
|
12
12
|
2. Crea una **API key**: Settings → **API keys** → Generate.
|
|
13
|
-
3.
|
|
13
|
+
3. **Una vez por máquina** (guarda la clave en `~/.komplian/postman-api-key`; no hace falta `export` después):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx komplian postman login
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
4. Cuando quieras sincronizar la colección:
|
|
14
20
|
|
|
15
21
|
```bash
|
|
16
|
-
export POSTMAN_API_KEY=pmak-…
|
|
17
22
|
npx komplian postman --yes
|
|
18
23
|
```
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
Opcional: `export POSTMAN_API_KEY=…` solo para la sesión actual (tiene prioridad sobre el archivo).
|
|
26
|
+
|
|
27
|
+
El comando llama a `GET https://api.getpostman.com/me` y **solo continúa** si el email de la cuenta es `@komplian.com`. **Si ya existen** la colección **Komplian API** y los entornos con el mismo nombre en ese workspace, se **actualizan**; si no, se crean.
|
|
28
|
+
|
|
29
|
+
**Variables de la API Komplian** (`apiKey`, `adminApiKey`, `workspaceId`, …) se rellenan en Postman automáticamente si están en `process.env` o en archivos `.env` (busca `api/.env`, `.env`, etc.; o `KOMPLIAN_DOTENV` / `--dotenv ruta`). Nombres típicos: `API_KEY`, `ADMIN_API_KEY`, `KOMPLIAN_WORKSPACE_ID`. Los JSON exportados en `./komplian-postman/` **no** incluyen secretos (para no commitearlos).
|
|
21
30
|
|
|
22
31
|
- Solo exportar archivos (sin subir por API): `npx komplian postman --yes --export-only`
|
|
23
|
-
- Otro workspace: `POSTMAN_WORKSPACE_ID=<id>`
|
|
32
|
+
- Otro workspace: `POSTMAN_WORKSPACE_ID=<id>`
|
|
33
|
+
- Ruta alternativa del archivo de clave Postman: `KOMPLIAN_POSTMAN_KEY_FILE`
|
|
34
|
+
|
|
35
|
+
**Seguridad (CLI Postman):** clave en `~/.komplian/` (permisos reforzados por el CLI), prompt sin eco, errores redactados; detalle técnico arriba. **Normativa del equipo:** archivo **`SECURITY.md`** en la raíz del monorepo Komplian (no va en el paquete npm).
|
|
24
36
|
|
|
25
37
|
No OAuth App registration — `gh` uses GitHub’s built-in flow. **Default workspace:** current working directory (`process.cwd()`), not `~/komplian`. Pass a path as last argument to clone elsewhere.
|
|
26
38
|
**Dependencies:** repos with `package-lock.json` use **`npm ci`** (does not modify the lockfile, so no spurious git changes). Repos without a lockfile use **`npm install --no-package-lock`** so onboarding does not create a new `package-lock.json`. Yarn / pnpm repos use frozen lock installs when `yarn` / `pnpm` is on PATH. Unless `KOMPLIAN_NPM_AUDIT=1`, npm runs with `--no-audit --no-fund`.
|
package/komplian-onboard.mjs
CHANGED
|
@@ -396,7 +396,8 @@ function npmInstallEach(workspace) {
|
|
|
396
396
|
function usage() {
|
|
397
397
|
log(`Uso: komplian onboard [opciones] [carpeta] | komplian postman [opciones]`);
|
|
398
398
|
log(` npx komplian onboard --yes`);
|
|
399
|
-
log(` npx komplian postman
|
|
399
|
+
log(` npx komplian postman login ${c.dim}(una vez · guarda API key)${c.reset}`);
|
|
400
|
+
log(` npx komplian postman --yes ${c.dim}(email @komplian.com)${c.reset}`);
|
|
400
401
|
log(``);
|
|
401
402
|
log(` Antes (una vez): gh auth login -h github.com -s repo -s read:org -w`);
|
|
402
403
|
log(` Requisitos: Node 18+, git, GitHub CLI (gh)`);
|
package/komplian-postman.mjs
CHANGED
|
@@ -2,17 +2,24 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Komplian Postman — API key + email @komplian.com (Postman /me), luego colección + entornos.
|
|
4
4
|
*
|
|
5
|
-
* Requisitos: Node 18
|
|
5
|
+
* Requisitos: Node 18+. Primera vez: npx komplian postman login
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Opcional: POSTMAN_WORKSPACE_ID (si no, usa el primer workspace con permiso de escritura)
|
|
11
|
-
* Opcional: KOMPLIAN_EMAIL_DOMAIN=komplian.com (solo para pruebas internas del script)
|
|
7
|
+
* Clave: POSTMAN_API_KEY o ~/.komplian/postman-api-key (npx komplian postman login)
|
|
8
|
+
* Seguridad: no registrar secretos; errores API redactados; login sin eco (TTY); ~/.komplian 700 + key 600.
|
|
9
|
+
* Opcional: POSTMAN_WORKSPACE_ID, KOMPLIAN_EMAIL_DOMAIN, KOMPLIAN_POSTMAN_KEY_FILE, KOMPLIAN_DEBUG
|
|
12
10
|
*/
|
|
13
11
|
|
|
14
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
writeFileSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
existsSync,
|
|
17
|
+
chmodSync,
|
|
18
|
+
} from "node:fs";
|
|
15
19
|
import { join, resolve } from "node:path";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
22
|
+
import { createInterface } from "node:readline/promises";
|
|
16
23
|
|
|
17
24
|
const POSTMAN_API = "https://api.getpostman.com";
|
|
18
25
|
|
|
@@ -30,6 +37,300 @@ function log(s = "") {
|
|
|
30
37
|
console.log(s);
|
|
31
38
|
}
|
|
32
39
|
|
|
40
|
+
/** Ruta para logs sin exponer home completo (solo prefijo ~). */
|
|
41
|
+
function formatHomePath(absPath) {
|
|
42
|
+
const h = homedir();
|
|
43
|
+
if (absPath.startsWith(h)) return `~${absPath.slice(h.length)}`;
|
|
44
|
+
return absPath;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function maskEmail(email) {
|
|
48
|
+
if (!email || typeof email !== "string" || !email.includes("@")) {
|
|
49
|
+
return "[sin email]";
|
|
50
|
+
}
|
|
51
|
+
const [user, domain] = email.split("@");
|
|
52
|
+
if (!domain) return "[sin email]";
|
|
53
|
+
const u =
|
|
54
|
+
user.length <= 1 ? "*" : `${user[0]}${"*".repeat(Math.min(3, user.length - 1))}`;
|
|
55
|
+
return `${u}@${domain}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function looksLikeSecretString(s) {
|
|
59
|
+
return (
|
|
60
|
+
typeof s === "string" &&
|
|
61
|
+
s.length >= 24 &&
|
|
62
|
+
/^[A-Za-z0-9_\-.+/=]+$/.test(s)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Solo mensajes genéricos; nunca volcar cuerpos crudos de la API (pueden incluir tokens). */
|
|
67
|
+
function safeApiErrorHint(body) {
|
|
68
|
+
if (body == null || typeof body !== "object") return "";
|
|
69
|
+
const parts = [];
|
|
70
|
+
const code = body.error?.name || body.error?.code || body.error?.type;
|
|
71
|
+
if (typeof code === "string" && code.length < 64 && !looksLikeSecretString(code)) {
|
|
72
|
+
parts.push(code);
|
|
73
|
+
}
|
|
74
|
+
const msg = body.error?.message || body.message;
|
|
75
|
+
if (
|
|
76
|
+
typeof msg === "string" &&
|
|
77
|
+
msg.length > 0 &&
|
|
78
|
+
msg.length < 160 &&
|
|
79
|
+
!looksLikeSecretString(msg)
|
|
80
|
+
) {
|
|
81
|
+
parts.push(msg);
|
|
82
|
+
}
|
|
83
|
+
return parts.join(" · ").slice(0, 200);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sanitizeForLog(value, depth = 0) {
|
|
87
|
+
if (depth > 8) return "[…]";
|
|
88
|
+
if (value == null) return value;
|
|
89
|
+
if (typeof value === "string") {
|
|
90
|
+
if (looksLikeSecretString(value)) return "[redacted]";
|
|
91
|
+
return value.length > 80 ? `${value.slice(0, 40)}…` : value;
|
|
92
|
+
}
|
|
93
|
+
if (typeof value !== "object") return value;
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
return value.slice(0, 15).map((v) => sanitizeForLog(v, depth + 1));
|
|
96
|
+
}
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const [k, v] of Object.entries(value)) {
|
|
99
|
+
const lower = k.toLowerCase();
|
|
100
|
+
if (
|
|
101
|
+
/(password|secret|token|apikey|api_key|authorization|credential|bearer|pmak|pm_)/i.test(
|
|
102
|
+
k
|
|
103
|
+
) ||
|
|
104
|
+
lower === "value" ||
|
|
105
|
+
(lower === "key" && depth > 0)
|
|
106
|
+
) {
|
|
107
|
+
out[k] = "[redacted]";
|
|
108
|
+
} else {
|
|
109
|
+
out[k] = sanitizeForLog(v, depth + 1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function logApiFailure(context, status, body) {
|
|
116
|
+
const hint = safeApiErrorHint(body);
|
|
117
|
+
let extra = "";
|
|
118
|
+
if (
|
|
119
|
+
process.env.KOMPLIAN_DEBUG === "1" ||
|
|
120
|
+
process.env.KOMPLIAN_DEBUG === "true"
|
|
121
|
+
) {
|
|
122
|
+
try {
|
|
123
|
+
extra = ` ${JSON.stringify(sanitizeForLog(body)).slice(0, 480)}`;
|
|
124
|
+
} catch {
|
|
125
|
+
extra = " [debug: no serializable]";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
log(
|
|
129
|
+
`${c.yellow}○${c.reset} ${context}: HTTP ${status}${hint ? ` — ${hint}` : ""}${extra}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function ensureSecureKomplianDir() {
|
|
134
|
+
const dir = join(homedir(), ".komplian");
|
|
135
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
136
|
+
try {
|
|
137
|
+
chmodSync(dir, 0o700);
|
|
138
|
+
} catch {
|
|
139
|
+
/* Windows u otros */
|
|
140
|
+
}
|
|
141
|
+
return dir;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Lectura de API key sin eco (TTY). Fallback con aviso si no hay raw mode (p. ej. algunos Windows).
|
|
146
|
+
*/
|
|
147
|
+
async function readPasswordLine(promptText) {
|
|
148
|
+
const prompt = `${c.cyan}${promptText}${c.reset}`;
|
|
149
|
+
if (!input.isTTY) {
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
const chunks = [];
|
|
152
|
+
input.on("data", (d) => chunks.push(d));
|
|
153
|
+
input.on("end", () => {
|
|
154
|
+
resolve(Buffer.concat(chunks).toString("utf8").trim());
|
|
155
|
+
});
|
|
156
|
+
input.on("error", reject);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (typeof input.setRawMode !== "function") {
|
|
160
|
+
log(
|
|
161
|
+
`${c.dim}○ Entrada visible (terminal sin modo oculto). No compartas pantalla con la clave.${c.reset}`
|
|
162
|
+
);
|
|
163
|
+
const rl = createInterface({ input, output });
|
|
164
|
+
const line = await rl.question(
|
|
165
|
+
`${c.cyan}Postman API key${c.reset} (Settings → API keys): `
|
|
166
|
+
);
|
|
167
|
+
rl.close();
|
|
168
|
+
return line.trim();
|
|
169
|
+
}
|
|
170
|
+
return new Promise((resolve) => {
|
|
171
|
+
process.stderr.write(prompt);
|
|
172
|
+
let buf = "";
|
|
173
|
+
const cleanup = () => {
|
|
174
|
+
try {
|
|
175
|
+
input.setRawMode(false);
|
|
176
|
+
} catch {
|
|
177
|
+
/* ignore */
|
|
178
|
+
}
|
|
179
|
+
input.removeListener("data", onData);
|
|
180
|
+
};
|
|
181
|
+
const onData = (chunk) => {
|
|
182
|
+
const s = chunk.toString("utf8");
|
|
183
|
+
for (let i = 0; i < s.length; i++) {
|
|
184
|
+
const code = s.charCodeAt(i);
|
|
185
|
+
if (code === 13 || code === 10) {
|
|
186
|
+
cleanup();
|
|
187
|
+
process.stderr.write("\n");
|
|
188
|
+
resolve(buf.trim());
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (code === 3) {
|
|
192
|
+
cleanup();
|
|
193
|
+
process.exit(130);
|
|
194
|
+
}
|
|
195
|
+
if (code === 127 || code === 8) {
|
|
196
|
+
buf = buf.slice(0, -1);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
buf += s[i];
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
input.setRawMode(true);
|
|
203
|
+
input.resume();
|
|
204
|
+
input.setEncoding("utf8");
|
|
205
|
+
input.on("data", onData);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function defaultKeyPath() {
|
|
210
|
+
const override = process.env.KOMPLIAN_POSTMAN_KEY_FILE?.trim();
|
|
211
|
+
if (override) return resolve(override);
|
|
212
|
+
return join(homedir(), ".komplian", "postman-api-key");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Orden: POSTMAN_API_KEY → ~/.komplian/postman-api-key (o KOMPLIAN_POSTMAN_KEY_FILE).
|
|
217
|
+
*/
|
|
218
|
+
function resolveApiKey() {
|
|
219
|
+
const env = process.env.POSTMAN_API_KEY?.trim();
|
|
220
|
+
if (env) return { key: env, source: "POSTMAN_API_KEY" };
|
|
221
|
+
const path = defaultKeyPath();
|
|
222
|
+
if (existsSync(path)) {
|
|
223
|
+
const k = readFileSync(path, "utf8").trim();
|
|
224
|
+
if (k) return { key: k, source: path };
|
|
225
|
+
}
|
|
226
|
+
return { key: "", source: null };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function printMissingKeyHelp() {
|
|
230
|
+
log(`${c.red}✗${c.reset} No hay API key de Postman.`);
|
|
231
|
+
log(``);
|
|
232
|
+
log(` ${c.bold}Una vez por máquina:${c.reset}`);
|
|
233
|
+
log(` ${c.cyan}npx komplian postman login${c.reset}`);
|
|
234
|
+
log(` (pega la clave de Postman → Settings → API keys → Generate)`);
|
|
235
|
+
log(``);
|
|
236
|
+
log(` ${c.dim}O en la sesión actual: export POSTMAN_API_KEY=…${c.reset}`);
|
|
237
|
+
log(
|
|
238
|
+
`${c.dim} Archivo manual: ${formatHomePath(defaultKeyPath())}${c.reset}`
|
|
239
|
+
);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Parseo mínimo .env (sin dependencia dotenv). */
|
|
244
|
+
function parseEnvFile(text) {
|
|
245
|
+
const out = {};
|
|
246
|
+
for (const line of text.split("\n")) {
|
|
247
|
+
const t = line.trim();
|
|
248
|
+
if (!t || t.startsWith("#")) continue;
|
|
249
|
+
const eq = t.indexOf("=");
|
|
250
|
+
if (eq < 1) continue;
|
|
251
|
+
const key = t.slice(0, eq).trim();
|
|
252
|
+
let val = t.slice(eq + 1).trim();
|
|
253
|
+
if (
|
|
254
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
255
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
256
|
+
) {
|
|
257
|
+
val = val.slice(1, -1);
|
|
258
|
+
}
|
|
259
|
+
out[key] = val;
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function discoverDotenvPaths(cwd, extraPath) {
|
|
265
|
+
const paths = [];
|
|
266
|
+
if (extraPath?.trim()) paths.push(resolve(extraPath.trim()));
|
|
267
|
+
if (process.env.KOMPLIAN_DOTENV?.trim()) {
|
|
268
|
+
paths.push(resolve(process.env.KOMPLIAN_DOTENV.trim()));
|
|
269
|
+
}
|
|
270
|
+
for (const rel of [
|
|
271
|
+
"api/.env",
|
|
272
|
+
"../api/.env",
|
|
273
|
+
".env",
|
|
274
|
+
"app/.env",
|
|
275
|
+
"../app/.env",
|
|
276
|
+
]) {
|
|
277
|
+
paths.push(resolve(cwd, rel));
|
|
278
|
+
}
|
|
279
|
+
return [...new Set(paths)];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function loadMergedEnvFiles(cwd, extraDotenv) {
|
|
283
|
+
const merged = {};
|
|
284
|
+
for (const p of discoverDotenvPaths(cwd, extraDotenv)) {
|
|
285
|
+
if (!existsSync(p)) continue;
|
|
286
|
+
try {
|
|
287
|
+
Object.assign(merged, parseEnvFile(readFileSync(p, "utf8")));
|
|
288
|
+
} catch {
|
|
289
|
+
/* ignore */
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return merged;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Valores para variables de Postman (API Komplian): API_KEY, ADMIN_API_KEY, IDs, etc.
|
|
297
|
+
* Orden: process.env por cada alias, luego claves en .env fusionados.
|
|
298
|
+
*/
|
|
299
|
+
function loadKomplianSecrets(cwd, extraDotenv) {
|
|
300
|
+
const file = loadMergedEnvFiles(cwd, extraDotenv);
|
|
301
|
+
const get = (...keys) => {
|
|
302
|
+
for (const k of keys) {
|
|
303
|
+
const e = process.env[k];
|
|
304
|
+
if (e != null && String(e).trim()) return String(e).trim();
|
|
305
|
+
}
|
|
306
|
+
for (const k of keys) {
|
|
307
|
+
const f = file[k];
|
|
308
|
+
if (f != null && String(f).trim()) return String(f).trim();
|
|
309
|
+
}
|
|
310
|
+
return "";
|
|
311
|
+
};
|
|
312
|
+
return {
|
|
313
|
+
apiKey: get("KOMPLIAN_API_KEY", "API_KEY"),
|
|
314
|
+
adminApiKey: get("KOMPLIAN_ADMIN_API_KEY", "ADMIN_API_KEY"),
|
|
315
|
+
workspaceId: get("KOMPLIAN_WORKSPACE_ID", "WORKSPACE_ID"),
|
|
316
|
+
widgetId: get("KOMPLIAN_WIDGET_ID", "WIDGET_ID"),
|
|
317
|
+
sessionId: get("KOMPLIAN_SESSION_ID", "SESSION_ID"),
|
|
318
|
+
leadId: get("KOMPLIAN_LEAD_ID", "LEAD_ID"),
|
|
319
|
+
flowId: get("KOMPLIAN_FLOW_ID", "FLOW_ID"),
|
|
320
|
+
sourceId: get("KOMPLIAN_SOURCE_ID", "SOURCE_ID"),
|
|
321
|
+
documentId: get("KOMPLIAN_DOCUMENT_ID", "DOCUMENT_ID"),
|
|
322
|
+
queryId: get("KOMPLIAN_QUERY_ID", "QUERY_ID"),
|
|
323
|
+
userId: get("KOMPLIAN_USER_ID", "USER_ID"),
|
|
324
|
+
entryId: get("KOMPLIAN_ENTRY_ID", "ENTRY_ID"),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function summarizeSecretsLoaded(s) {
|
|
329
|
+
return Object.keys(s).filter(
|
|
330
|
+
(k) => s[k] != null && String(s[k]).trim() !== ""
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
33
334
|
function headers(apiKey) {
|
|
34
335
|
return {
|
|
35
336
|
"X-API-Key": apiKey,
|
|
@@ -239,52 +540,50 @@ function buildCollection() {
|
|
|
239
540
|
};
|
|
240
541
|
}
|
|
241
542
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
543
|
+
/**
|
|
544
|
+
* @param {Record<string, string>} secrets - desde API_KEY / api/.env (no subir a git los JSON locales).
|
|
545
|
+
*/
|
|
546
|
+
function buildEnvironments(secrets = {}) {
|
|
547
|
+
const pick = (key, fallback = "") => {
|
|
548
|
+
const v = secrets[key];
|
|
549
|
+
if (v != null && String(v).trim() !== "") return String(v).trim();
|
|
550
|
+
return fallback;
|
|
551
|
+
};
|
|
552
|
+
const row = (key, def = "") => {
|
|
553
|
+
const val = pick(key, def);
|
|
554
|
+
const isSecret = key === "apiKey" || key === "adminApiKey";
|
|
555
|
+
return {
|
|
556
|
+
key,
|
|
557
|
+
value: val,
|
|
558
|
+
type: isSecret ? "secret" : "default",
|
|
559
|
+
enabled: true,
|
|
560
|
+
};
|
|
561
|
+
};
|
|
255
562
|
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
563
|
+
const rowsForBase = (baseUrl) => [
|
|
564
|
+
row("baseUrl", baseUrl),
|
|
565
|
+
row("apiKey"),
|
|
566
|
+
row("adminApiKey"),
|
|
567
|
+
row("workspaceId"),
|
|
568
|
+
row("widgetId"),
|
|
569
|
+
row("sessionId"),
|
|
570
|
+
row("leadId"),
|
|
571
|
+
row("flowId"),
|
|
572
|
+
row("sourceId"),
|
|
573
|
+
row("documentId"),
|
|
574
|
+
row("queryId"),
|
|
575
|
+
row("userId"),
|
|
576
|
+
row("entryId"),
|
|
270
577
|
];
|
|
271
578
|
|
|
272
579
|
return [
|
|
273
580
|
{
|
|
274
581
|
name: "Komplian — Local Dev",
|
|
275
|
-
values:
|
|
276
|
-
v.key === "baseUrl"
|
|
277
|
-
? { ...v, value: "http://localhost:4000", type: "default" }
|
|
278
|
-
: { ...v }
|
|
279
|
-
),
|
|
582
|
+
values: rowsForBase("http://localhost:4000"),
|
|
280
583
|
},
|
|
281
584
|
{
|
|
282
585
|
name: "Komplian — Production",
|
|
283
|
-
values:
|
|
284
|
-
v.key === "baseUrl"
|
|
285
|
-
? { ...v, value: "https://api.komplian.com", type: "default" }
|
|
286
|
-
: { ...v }
|
|
287
|
-
),
|
|
586
|
+
values: rowsForBase("https://api.komplian.com"),
|
|
288
587
|
},
|
|
289
588
|
];
|
|
290
589
|
}
|
|
@@ -305,7 +604,7 @@ async function verifyKomplianEmail(apiKey, domain) {
|
|
|
305
604
|
const { ok, status, body } = await pmFetch(apiKey, "/me");
|
|
306
605
|
if (!ok) {
|
|
307
606
|
log(
|
|
308
|
-
`${c.red}✗${c.reset}
|
|
607
|
+
`${c.red}✗${c.reset} API key de Postman inválida o sin acceso (${status}).`
|
|
309
608
|
);
|
|
310
609
|
process.exit(1);
|
|
311
610
|
}
|
|
@@ -318,7 +617,7 @@ async function verifyKomplianEmail(apiKey, domain) {
|
|
|
318
617
|
}
|
|
319
618
|
if (!emailAllowed(email, domain)) {
|
|
320
619
|
log(
|
|
321
|
-
`${c.red}✗${c.reset} Este comando solo permite cuentas **@${domain}**. Sesión
|
|
620
|
+
`${c.red}✗${c.reset} Este comando solo permite cuentas **@${domain}**. Sesión: ${c.bold}${maskEmail(email)}${c.reset}`
|
|
322
621
|
);
|
|
323
622
|
log(
|
|
324
623
|
`${c.dim} Crea la API key desde una cuenta @${domain} o añade ese email a tu usuario en Postman.${c.reset}`
|
|
@@ -326,7 +625,7 @@ async function verifyKomplianEmail(apiKey, domain) {
|
|
|
326
625
|
process.exit(1);
|
|
327
626
|
}
|
|
328
627
|
log(
|
|
329
|
-
`${c.green}✓${c.reset} Postman: ${c.bold}${email}${c.reset} (${c.dim}dominio permitido${c.reset})`
|
|
628
|
+
`${c.green}✓${c.reset} Postman: ${c.bold}${maskEmail(email)}${c.reset} (${c.dim}dominio permitido${c.reset})`
|
|
330
629
|
);
|
|
331
630
|
return email;
|
|
332
631
|
}
|
|
@@ -347,13 +646,93 @@ async function createEnvironment(apiKey, workspaceId, env) {
|
|
|
347
646
|
});
|
|
348
647
|
}
|
|
349
648
|
|
|
649
|
+
async function listCollections(apiKey, workspaceId) {
|
|
650
|
+
const q = new URLSearchParams({ workspace: workspaceId });
|
|
651
|
+
return pmFetch(apiKey, `/collections?${q}`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function listEnvironments(apiKey, workspaceId) {
|
|
655
|
+
const q = new URLSearchParams({ workspace: workspaceId });
|
|
656
|
+
return pmFetch(apiKey, `/environments?${q}`);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function firstArray(...candidates) {
|
|
660
|
+
for (const c of candidates) {
|
|
661
|
+
if (Array.isArray(c)) return c;
|
|
662
|
+
}
|
|
663
|
+
return [];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function findCollectionByName(body, name) {
|
|
667
|
+
const arr = firstArray(
|
|
668
|
+
body.collections,
|
|
669
|
+
body.data?.collections,
|
|
670
|
+
body.data,
|
|
671
|
+
Array.isArray(body) ? body : null
|
|
672
|
+
);
|
|
673
|
+
return arr.find(
|
|
674
|
+
(c) =>
|
|
675
|
+
c.name === name ||
|
|
676
|
+
c.collection?.name === name ||
|
|
677
|
+
c.info?.name === name
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function findEnvironmentByName(body, name) {
|
|
682
|
+
const arr = firstArray(
|
|
683
|
+
body.environments,
|
|
684
|
+
body.data?.environments,
|
|
685
|
+
body.data,
|
|
686
|
+
Array.isArray(body) ? body : null
|
|
687
|
+
);
|
|
688
|
+
return arr.find((e) => e.name === name);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async function upsertCollection(apiKey, workspaceId, collection) {
|
|
692
|
+
const name = collection.info?.name;
|
|
693
|
+
const list = await listCollections(apiKey, workspaceId);
|
|
694
|
+
if (!list.ok) {
|
|
695
|
+
return { ok: false, status: list.status, body: list.body };
|
|
696
|
+
}
|
|
697
|
+
const existing = findCollectionByName(list.body, name);
|
|
698
|
+
if (!existing) {
|
|
699
|
+
const r = await createCollection(apiKey, workspaceId, collection);
|
|
700
|
+
return { ...r, op: "create" };
|
|
701
|
+
}
|
|
702
|
+
const uid = existing.uid ?? existing.id;
|
|
703
|
+
const r = await pmFetch(apiKey, `/collections/${uid}`, {
|
|
704
|
+
method: "PUT",
|
|
705
|
+
body: JSON.stringify({ collection }),
|
|
706
|
+
});
|
|
707
|
+
return { ...r, op: "update" };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function upsertEnvironment(apiKey, workspaceId, env) {
|
|
711
|
+
const list = await listEnvironments(apiKey, workspaceId);
|
|
712
|
+
if (!list.ok) {
|
|
713
|
+
return { ok: false, status: list.status, body: list.body };
|
|
714
|
+
}
|
|
715
|
+
const existing = findEnvironmentByName(list.body, env.name);
|
|
716
|
+
if (!existing) {
|
|
717
|
+
const r = await createEnvironment(apiKey, workspaceId, env);
|
|
718
|
+
return { ...r, op: "create" };
|
|
719
|
+
}
|
|
720
|
+
const uid = existing.uid ?? existing.id;
|
|
721
|
+
const r = await pmFetch(apiKey, `/environments/${uid}`, {
|
|
722
|
+
method: "PUT",
|
|
723
|
+
body: JSON.stringify({ environment: env }),
|
|
724
|
+
});
|
|
725
|
+
return { ...r, op: "update" };
|
|
726
|
+
}
|
|
727
|
+
|
|
350
728
|
function parseArgs(argv) {
|
|
351
|
-
const out = { yes: false, exportOnly: false, outDir: "", help: false };
|
|
729
|
+
const out = { yes: false, exportOnly: false, outDir: "", help: false, dotenv: "" };
|
|
352
730
|
for (let i = 0; i < argv.length; i++) {
|
|
353
731
|
const a = argv[i];
|
|
354
732
|
if (a === "--yes" || a === "-y") out.yes = true;
|
|
355
733
|
else if (a === "--export-only") out.exportOnly = true;
|
|
356
734
|
else if (a === "--out") out.outDir = argv[++i] || "";
|
|
735
|
+
else if (a === "--dotenv") out.dotenv = argv[++i] || "";
|
|
357
736
|
else if (a === "-h" || a === "--help") out.help = true;
|
|
358
737
|
else if (a.startsWith("-")) {
|
|
359
738
|
log(`${c.red}✗${c.reset} Opción desconocida: ${a}`);
|
|
@@ -363,40 +742,133 @@ function parseArgs(argv) {
|
|
|
363
742
|
return out;
|
|
364
743
|
}
|
|
365
744
|
|
|
745
|
+
function parseLoginArgs(argv) {
|
|
746
|
+
const out = { help: false };
|
|
747
|
+
for (const a of argv) {
|
|
748
|
+
if (a === "-h" || a === "--help") out.help = true;
|
|
749
|
+
else if (a.startsWith("-")) {
|
|
750
|
+
log(`${c.red}✗${c.reset} Opción desconocida: ${a}`);
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return out;
|
|
755
|
+
}
|
|
756
|
+
|
|
366
757
|
function usage() {
|
|
367
|
-
log(`Uso: komplian postman [opciones]`);
|
|
368
|
-
log(` ${c.dim}POSTMAN_API_KEY${c.reset}
|
|
369
|
-
log(`
|
|
758
|
+
log(`Uso: komplian postman [opciones] | komplian postman login`);
|
|
759
|
+
log(` Clave: ${c.dim}POSTMAN_API_KEY${c.reset} o archivo ${c.dim}~/.komplian/postman-api-key${c.reset} (véase ${c.cyan}postman login${c.reset})`);
|
|
760
|
+
log(` Dominio email: solo @komplian.com (GET /me)`);
|
|
370
761
|
log(``);
|
|
371
|
-
log(` -y, --yes Sin
|
|
762
|
+
log(` -y, --yes Sin prompts extra`);
|
|
372
763
|
log(` --export-only Solo escribe JSON en disco (no llama a la API de Postman)`);
|
|
373
764
|
log(` --out <dir> Carpeta para export (por defecto: ./komplian-postman)`);
|
|
765
|
+
log(` --dotenv <ruta> .env extra (además de api/.env, .env, KOMPLIAN_DOTENV)`);
|
|
374
766
|
log(` -h, --help`);
|
|
375
767
|
log(``);
|
|
376
|
-
log(`
|
|
768
|
+
log(` Variables Komplian en Postman: API_KEY, ADMIN_API_KEY, WORKSPACE_ID, … (env o .env)`);
|
|
769
|
+
log(` Opcional: POSTMAN_WORKSPACE_ID, KOMPLIAN_EMAIL_DOMAIN, KOMPLIAN_POSTMAN_KEY_FILE`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function usageLogin() {
|
|
773
|
+
log(`Uso: komplian postman login`);
|
|
774
|
+
log(` Guarda la API key de Postman en ${c.dim}~/.komplian/postman-api-key${c.reset} (permiso 600).`);
|
|
775
|
+
log(` Tras esto: ${c.cyan}npx komplian postman --yes${c.reset} sin exportar variables.`);
|
|
776
|
+
log(``);
|
|
777
|
+
log(` Postman → Settings (avatar) → API keys → Generate`);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function runPostmanLogin(argv) {
|
|
781
|
+
const la = parseLoginArgs(argv);
|
|
782
|
+
if (la.help) {
|
|
783
|
+
usageLogin();
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
log(`${c.cyan}━━ Postman login (Komplian) ━━${c.reset}`);
|
|
788
|
+
log(
|
|
789
|
+
`${c.dim}Cuenta con email @komplian.com. Genera la clave en Postman → Settings → API keys.${c.reset}`
|
|
790
|
+
);
|
|
791
|
+
log("");
|
|
792
|
+
|
|
793
|
+
const domain = (process.env.KOMPLIAN_EMAIL_DOMAIN || "komplian.com").trim();
|
|
794
|
+
const keyPath = defaultKeyPath();
|
|
795
|
+
|
|
796
|
+
let apiKey = await readPasswordLine(
|
|
797
|
+
"Postman API key (no se muestra al escribir; Settings → API keys): "
|
|
798
|
+
);
|
|
799
|
+
if (!apiKey) {
|
|
800
|
+
log(`${c.red}✗${c.reset} Clave vacía.`);
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
await verifyKomplianEmail(apiKey, domain);
|
|
805
|
+
|
|
806
|
+
ensureSecureKomplianDir();
|
|
807
|
+
writeFileSync(keyPath, `${apiKey}\n`, { encoding: "utf8", mode: 0o600 });
|
|
808
|
+
try {
|
|
809
|
+
chmodSync(keyPath, 0o600);
|
|
810
|
+
} catch {
|
|
811
|
+
/* Windows puede ignorar chmod */
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
log("");
|
|
815
|
+
log(
|
|
816
|
+
`${c.green}✓${c.reset} Guardada en ${c.bold}${formatHomePath(keyPath)}${c.reset} (permiso 600; no subir a git).`
|
|
817
|
+
);
|
|
818
|
+
log(
|
|
819
|
+
`${c.dim} Siguiente: ${c.reset}${c.cyan}npx komplian postman --yes${c.reset}`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function normalizePostmanArgv(argv) {
|
|
824
|
+
const a = [...argv];
|
|
825
|
+
if (a[0] === "login") {
|
|
826
|
+
return { mode: "login", rest: a.slice(1) };
|
|
827
|
+
}
|
|
828
|
+
return { mode: "sync", rest: a };
|
|
377
829
|
}
|
|
378
830
|
|
|
379
831
|
export async function runPostman(argv) {
|
|
380
|
-
const
|
|
832
|
+
const { mode, rest } = normalizePostmanArgv(argv);
|
|
833
|
+
if (mode === "login") {
|
|
834
|
+
await runPostmanLogin(rest);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const args = parseArgs(rest);
|
|
381
839
|
if (args.help) {
|
|
382
840
|
usage();
|
|
383
841
|
return;
|
|
384
842
|
}
|
|
385
843
|
|
|
386
|
-
const apiKey =
|
|
844
|
+
const { key: apiKey, source } = resolveApiKey();
|
|
387
845
|
if (!apiKey) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
);
|
|
392
|
-
process.exit(1);
|
|
846
|
+
printMissingKeyHelp();
|
|
847
|
+
}
|
|
848
|
+
if (source && source !== "POSTMAN_API_KEY") {
|
|
849
|
+
log(`${c.dim}→ API key Postman desde: ${formatHomePath(source)}${c.reset}`);
|
|
393
850
|
}
|
|
394
851
|
|
|
395
852
|
const domain = (process.env.KOMPLIAN_EMAIL_DOMAIN || "komplian.com").trim();
|
|
396
853
|
await verifyKomplianEmail(apiKey, domain);
|
|
397
854
|
|
|
398
855
|
const collection = buildCollection();
|
|
399
|
-
const
|
|
856
|
+
const cwd = process.cwd();
|
|
857
|
+
const secrets = loadKomplianSecrets(cwd, args.dotenv);
|
|
858
|
+
const envsForUpload = buildEnvironments(secrets);
|
|
859
|
+
const envsForExport = buildEnvironments({});
|
|
860
|
+
|
|
861
|
+
const filled = summarizeSecretsLoaded(secrets);
|
|
862
|
+
if (filled.length) {
|
|
863
|
+
log(
|
|
864
|
+
`${c.green}✓${c.reset} Komplian → Postman: ${c.dim}${filled.join(", ")}${c.reset} (process.env / .env)`
|
|
865
|
+
);
|
|
866
|
+
} else {
|
|
867
|
+
log(
|
|
868
|
+
`${c.dim}○ Sin API_KEY / ADMIN_API_KEY / IDs en env o .env — los entornos en Postman quedarán vacíos en esos campos.${c.reset}`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
400
872
|
const workspaceId = await pickWorkspaceId(
|
|
401
873
|
apiKey,
|
|
402
874
|
process.env.POSTMAN_WORKSPACE_ID
|
|
@@ -414,10 +886,10 @@ export async function runPostman(argv) {
|
|
|
414
886
|
writeFileSync(collPath, JSON.stringify(collection, null, 2), "utf8");
|
|
415
887
|
log(`${c.green}✓${c.reset} Export: ${c.dim}${collPath}${c.reset}`);
|
|
416
888
|
|
|
417
|
-
for (const env of
|
|
889
|
+
for (const env of envsForExport) {
|
|
418
890
|
const safe = env.name.replace(/[\\/]/g, "-") + ".postman_environment.json";
|
|
419
891
|
writeFileSync(join(outBase, safe), JSON.stringify({ ...env }, null, 2), "utf8");
|
|
420
|
-
log(`${c.green}✓${c.reset} Export: ${c.dim}${join(outBase, safe)}${c.reset}`);
|
|
892
|
+
log(`${c.green}✓${c.reset} Export: ${c.dim}${join(outBase, safe)}${c.reset} ${c.dim}(sin secretos; no commitear)${c.reset}`);
|
|
421
893
|
}
|
|
422
894
|
|
|
423
895
|
if (args.exportOnly) {
|
|
@@ -429,38 +901,40 @@ export async function runPostman(argv) {
|
|
|
429
901
|
}
|
|
430
902
|
|
|
431
903
|
log("");
|
|
432
|
-
log(`${c.cyan}━━
|
|
904
|
+
log(`${c.cyan}━━ Sincronizar Postman (crear o actualizar) ━━${c.reset}`);
|
|
433
905
|
|
|
434
|
-
const cr = await
|
|
906
|
+
const cr = await upsertCollection(apiKey, workspaceId, collection);
|
|
435
907
|
if (!cr.ok) {
|
|
908
|
+
logApiFailure("Colección API", cr.status, cr.body);
|
|
436
909
|
log(
|
|
437
|
-
`${c.
|
|
438
|
-
);
|
|
439
|
-
log(
|
|
440
|
-
`${c.dim} Si el nombre ya existe, borra la colección antigua en Postman o importa el JSON exportado.${c.reset}`
|
|
910
|
+
`${c.dim} Revisa permisos del workspace o importa el JSON exportado. Depuración: KOMPLIAN_DEBUG=1 (salida redactada).${c.reset}`
|
|
441
911
|
);
|
|
442
912
|
} else {
|
|
913
|
+
const verb = cr.op === "update" ? "actualizada" : "creada";
|
|
443
914
|
const uid = cr.body?.collection?.uid || cr.body?.collection?.id || "?";
|
|
444
|
-
log(`${c.green}✓${c.reset} Colección
|
|
915
|
+
log(`${c.green}✓${c.reset} Colección ${verb}: ${c.bold}Komplian API${c.reset} (${uid})`);
|
|
445
916
|
}
|
|
446
917
|
|
|
447
|
-
for (const env of
|
|
448
|
-
const er = await
|
|
918
|
+
for (const env of envsForUpload) {
|
|
919
|
+
const er = await upsertEnvironment(apiKey, workspaceId, {
|
|
449
920
|
name: env.name,
|
|
450
921
|
values: env.values,
|
|
451
922
|
});
|
|
452
923
|
if (!er.ok) {
|
|
924
|
+
logApiFailure(`Entorno "${env.name}"`, er.status, er.body);
|
|
925
|
+
} else {
|
|
926
|
+
const verb = er.op === "update" ? "actualizado" : "creado";
|
|
453
927
|
log(
|
|
454
|
-
`${c.
|
|
928
|
+
`${c.green}✓${c.reset} Entorno ${verb}: ${c.bold}${env.name}${c.reset}`
|
|
455
929
|
);
|
|
456
|
-
} else {
|
|
457
|
-
log(`${c.green}✓${c.reset} Entorno: ${c.bold}${env.name}${c.reset}`);
|
|
458
930
|
}
|
|
459
931
|
}
|
|
460
932
|
|
|
461
933
|
log("");
|
|
462
|
-
log(`${c.green}✓${c.reset}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
934
|
+
log(`${c.green}✓${c.reset} En Postman: elige entorno ${c.dim}(Local Dev / Production)${c.reset} y prueba las peticiones.`);
|
|
935
|
+
if (!filled.length) {
|
|
936
|
+
log(
|
|
937
|
+
`${c.dim} Para rellenar apiKey: exporta API_KEY o pon api/.env y vuelve a ejecutar este comando.${c.reset}`
|
|
938
|
+
);
|
|
939
|
+
}
|
|
466
940
|
}
|