komplian 0.4.1 → 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 +6 -2
- package/komplian-postman.mjs +425 -84
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,11 +24,15 @@ npx komplian postman --yes
|
|
|
24
24
|
|
|
25
25
|
Opcional: `export POSTMAN_API_KEY=…` solo para la sesión actual (tiene prioridad sobre el archivo).
|
|
26
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`.
|
|
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).
|
|
28
30
|
|
|
29
31
|
- Solo exportar archivos (sin subir por API): `npx komplian postman --yes --export-only`
|
|
30
32
|
- Otro workspace: `POSTMAN_WORKSPACE_ID=<id>`
|
|
31
|
-
- Ruta alternativa del archivo de clave: `KOMPLIAN_POSTMAN_KEY_FILE`
|
|
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).
|
|
32
36
|
|
|
33
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.
|
|
34
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-postman.mjs
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Requisitos: Node 18+. Primera vez: npx komplian postman login
|
|
6
6
|
*
|
|
7
7
|
* Clave: POSTMAN_API_KEY o ~/.komplian/postman-api-key (npx komplian postman login)
|
|
8
|
-
*
|
|
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
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import {
|
|
@@ -36,6 +37,175 @@ function log(s = "") {
|
|
|
36
37
|
console.log(s);
|
|
37
38
|
}
|
|
38
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
|
+
|
|
39
209
|
function defaultKeyPath() {
|
|
40
210
|
const override = process.env.KOMPLIAN_POSTMAN_KEY_FILE?.trim();
|
|
41
211
|
if (override) return resolve(override);
|
|
@@ -65,11 +235,102 @@ function printMissingKeyHelp() {
|
|
|
65
235
|
log(``);
|
|
66
236
|
log(` ${c.dim}O en la sesión actual: export POSTMAN_API_KEY=…${c.reset}`);
|
|
67
237
|
log(
|
|
68
|
-
`${c.dim} Archivo manual: ${defaultKeyPath()}${c.reset}`
|
|
238
|
+
`${c.dim} Archivo manual: ${formatHomePath(defaultKeyPath())}${c.reset}`
|
|
69
239
|
);
|
|
70
240
|
process.exit(1);
|
|
71
241
|
}
|
|
72
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
|
+
|
|
73
334
|
function headers(apiKey) {
|
|
74
335
|
return {
|
|
75
336
|
"X-API-Key": apiKey,
|
|
@@ -279,52 +540,50 @@ function buildCollection() {
|
|
|
279
540
|
};
|
|
280
541
|
}
|
|
281
542
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
+
};
|
|
295
562
|
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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"),
|
|
310
577
|
];
|
|
311
578
|
|
|
312
579
|
return [
|
|
313
580
|
{
|
|
314
581
|
name: "Komplian — Local Dev",
|
|
315
|
-
values:
|
|
316
|
-
v.key === "baseUrl"
|
|
317
|
-
? { ...v, value: "http://localhost:4000", type: "default" }
|
|
318
|
-
: { ...v }
|
|
319
|
-
),
|
|
582
|
+
values: rowsForBase("http://localhost:4000"),
|
|
320
583
|
},
|
|
321
584
|
{
|
|
322
585
|
name: "Komplian — Production",
|
|
323
|
-
values:
|
|
324
|
-
v.key === "baseUrl"
|
|
325
|
-
? { ...v, value: "https://api.komplian.com", type: "default" }
|
|
326
|
-
: { ...v }
|
|
327
|
-
),
|
|
586
|
+
values: rowsForBase("https://api.komplian.com"),
|
|
328
587
|
},
|
|
329
588
|
];
|
|
330
589
|
}
|
|
@@ -358,7 +617,7 @@ async function verifyKomplianEmail(apiKey, domain) {
|
|
|
358
617
|
}
|
|
359
618
|
if (!emailAllowed(email, domain)) {
|
|
360
619
|
log(
|
|
361
|
-
`${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}`
|
|
362
621
|
);
|
|
363
622
|
log(
|
|
364
623
|
`${c.dim} Crea la API key desde una cuenta @${domain} o añade ese email a tu usuario en Postman.${c.reset}`
|
|
@@ -366,7 +625,7 @@ async function verifyKomplianEmail(apiKey, domain) {
|
|
|
366
625
|
process.exit(1);
|
|
367
626
|
}
|
|
368
627
|
log(
|
|
369
|
-
`${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})`
|
|
370
629
|
);
|
|
371
630
|
return email;
|
|
372
631
|
}
|
|
@@ -387,13 +646,93 @@ async function createEnvironment(apiKey, workspaceId, env) {
|
|
|
387
646
|
});
|
|
388
647
|
}
|
|
389
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
|
+
|
|
390
728
|
function parseArgs(argv) {
|
|
391
|
-
const out = { yes: false, exportOnly: false, outDir: "", help: false };
|
|
729
|
+
const out = { yes: false, exportOnly: false, outDir: "", help: false, dotenv: "" };
|
|
392
730
|
for (let i = 0; i < argv.length; i++) {
|
|
393
731
|
const a = argv[i];
|
|
394
732
|
if (a === "--yes" || a === "-y") out.yes = true;
|
|
395
733
|
else if (a === "--export-only") out.exportOnly = true;
|
|
396
734
|
else if (a === "--out") out.outDir = argv[++i] || "";
|
|
735
|
+
else if (a === "--dotenv") out.dotenv = argv[++i] || "";
|
|
397
736
|
else if (a === "-h" || a === "--help") out.help = true;
|
|
398
737
|
else if (a.startsWith("-")) {
|
|
399
738
|
log(`${c.red}✗${c.reset} Opción desconocida: ${a}`);
|
|
@@ -423,8 +762,10 @@ function usage() {
|
|
|
423
762
|
log(` -y, --yes Sin prompts extra`);
|
|
424
763
|
log(` --export-only Solo escribe JSON en disco (no llama a la API de Postman)`);
|
|
425
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)`);
|
|
426
766
|
log(` -h, --help`);
|
|
427
767
|
log(``);
|
|
768
|
+
log(` Variables Komplian en Postman: API_KEY, ADMIN_API_KEY, WORKSPACE_ID, … (env o .env)`);
|
|
428
769
|
log(` Opcional: POSTMAN_WORKSPACE_ID, KOMPLIAN_EMAIL_DOMAIN, KOMPLIAN_POSTMAN_KEY_FILE`);
|
|
429
770
|
}
|
|
430
771
|
|
|
@@ -436,25 +777,6 @@ function usageLogin() {
|
|
|
436
777
|
log(` Postman → Settings (avatar) → API keys → Generate`);
|
|
437
778
|
}
|
|
438
779
|
|
|
439
|
-
async function promptApiKey() {
|
|
440
|
-
if (input.isTTY) {
|
|
441
|
-
const rl = createInterface({ input, output });
|
|
442
|
-
const line = await rl.question(
|
|
443
|
-
`${c.cyan}Pega tu Postman API key${c.reset} (Settings → API keys): `
|
|
444
|
-
);
|
|
445
|
-
rl.close();
|
|
446
|
-
return line.trim();
|
|
447
|
-
}
|
|
448
|
-
return new Promise((resolve, reject) => {
|
|
449
|
-
const chunks = [];
|
|
450
|
-
input.on("data", (d) => chunks.push(d));
|
|
451
|
-
input.on("end", () => {
|
|
452
|
-
resolve(Buffer.concat(chunks).toString("utf8").trim());
|
|
453
|
-
});
|
|
454
|
-
input.on("error", reject);
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
|
|
458
780
|
async function runPostmanLogin(argv) {
|
|
459
781
|
const la = parseLoginArgs(argv);
|
|
460
782
|
if (la.help) {
|
|
@@ -471,7 +793,9 @@ async function runPostmanLogin(argv) {
|
|
|
471
793
|
const domain = (process.env.KOMPLIAN_EMAIL_DOMAIN || "komplian.com").trim();
|
|
472
794
|
const keyPath = defaultKeyPath();
|
|
473
795
|
|
|
474
|
-
let apiKey = await
|
|
796
|
+
let apiKey = await readPasswordLine(
|
|
797
|
+
"Postman API key (no se muestra al escribir; Settings → API keys): "
|
|
798
|
+
);
|
|
475
799
|
if (!apiKey) {
|
|
476
800
|
log(`${c.red}✗${c.reset} Clave vacía.`);
|
|
477
801
|
process.exit(1);
|
|
@@ -479,7 +803,7 @@ async function runPostmanLogin(argv) {
|
|
|
479
803
|
|
|
480
804
|
await verifyKomplianEmail(apiKey, domain);
|
|
481
805
|
|
|
482
|
-
|
|
806
|
+
ensureSecureKomplianDir();
|
|
483
807
|
writeFileSync(keyPath, `${apiKey}\n`, { encoding: "utf8", mode: 0o600 });
|
|
484
808
|
try {
|
|
485
809
|
chmodSync(keyPath, 0o600);
|
|
@@ -489,7 +813,7 @@ async function runPostmanLogin(argv) {
|
|
|
489
813
|
|
|
490
814
|
log("");
|
|
491
815
|
log(
|
|
492
|
-
`${c.green}✓${c.reset} Guardada en ${c.bold}${keyPath}${c.reset} (
|
|
816
|
+
`${c.green}✓${c.reset} Guardada en ${c.bold}${formatHomePath(keyPath)}${c.reset} (permiso 600; no subir a git).`
|
|
493
817
|
);
|
|
494
818
|
log(
|
|
495
819
|
`${c.dim} Siguiente: ${c.reset}${c.cyan}npx komplian postman --yes${c.reset}`
|
|
@@ -522,14 +846,29 @@ export async function runPostman(argv) {
|
|
|
522
846
|
printMissingKeyHelp();
|
|
523
847
|
}
|
|
524
848
|
if (source && source !== "POSTMAN_API_KEY") {
|
|
525
|
-
log(`${c.dim}→ API key: ${source}${c.reset}`);
|
|
849
|
+
log(`${c.dim}→ API key Postman desde: ${formatHomePath(source)}${c.reset}`);
|
|
526
850
|
}
|
|
527
851
|
|
|
528
852
|
const domain = (process.env.KOMPLIAN_EMAIL_DOMAIN || "komplian.com").trim();
|
|
529
853
|
await verifyKomplianEmail(apiKey, domain);
|
|
530
854
|
|
|
531
855
|
const collection = buildCollection();
|
|
532
|
-
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
|
+
|
|
533
872
|
const workspaceId = await pickWorkspaceId(
|
|
534
873
|
apiKey,
|
|
535
874
|
process.env.POSTMAN_WORKSPACE_ID
|
|
@@ -547,10 +886,10 @@ export async function runPostman(argv) {
|
|
|
547
886
|
writeFileSync(collPath, JSON.stringify(collection, null, 2), "utf8");
|
|
548
887
|
log(`${c.green}✓${c.reset} Export: ${c.dim}${collPath}${c.reset}`);
|
|
549
888
|
|
|
550
|
-
for (const env of
|
|
889
|
+
for (const env of envsForExport) {
|
|
551
890
|
const safe = env.name.replace(/[\\/]/g, "-") + ".postman_environment.json";
|
|
552
891
|
writeFileSync(join(outBase, safe), JSON.stringify({ ...env }, null, 2), "utf8");
|
|
553
|
-
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}`);
|
|
554
893
|
}
|
|
555
894
|
|
|
556
895
|
if (args.exportOnly) {
|
|
@@ -562,38 +901,40 @@ export async function runPostman(argv) {
|
|
|
562
901
|
}
|
|
563
902
|
|
|
564
903
|
log("");
|
|
565
|
-
log(`${c.cyan}━━
|
|
904
|
+
log(`${c.cyan}━━ Sincronizar Postman (crear o actualizar) ━━${c.reset}`);
|
|
566
905
|
|
|
567
|
-
const cr = await
|
|
906
|
+
const cr = await upsertCollection(apiKey, workspaceId, collection);
|
|
568
907
|
if (!cr.ok) {
|
|
908
|
+
logApiFailure("Colección API", cr.status, cr.body);
|
|
569
909
|
log(
|
|
570
|
-
`${c.
|
|
571
|
-
);
|
|
572
|
-
log(
|
|
573
|
-
`${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}`
|
|
574
911
|
);
|
|
575
912
|
} else {
|
|
913
|
+
const verb = cr.op === "update" ? "actualizada" : "creada";
|
|
576
914
|
const uid = cr.body?.collection?.uid || cr.body?.collection?.id || "?";
|
|
577
|
-
log(`${c.green}✓${c.reset} Colección
|
|
915
|
+
log(`${c.green}✓${c.reset} Colección ${verb}: ${c.bold}Komplian API${c.reset} (${uid})`);
|
|
578
916
|
}
|
|
579
917
|
|
|
580
|
-
for (const env of
|
|
581
|
-
const er = await
|
|
918
|
+
for (const env of envsForUpload) {
|
|
919
|
+
const er = await upsertEnvironment(apiKey, workspaceId, {
|
|
582
920
|
name: env.name,
|
|
583
921
|
values: env.values,
|
|
584
922
|
});
|
|
585
923
|
if (!er.ok) {
|
|
924
|
+
logApiFailure(`Entorno "${env.name}"`, er.status, er.body);
|
|
925
|
+
} else {
|
|
926
|
+
const verb = er.op === "update" ? "actualizado" : "creado";
|
|
586
927
|
log(
|
|
587
|
-
`${c.
|
|
928
|
+
`${c.green}✓${c.reset} Entorno ${verb}: ${c.bold}${env.name}${c.reset}`
|
|
588
929
|
);
|
|
589
|
-
} else {
|
|
590
|
-
log(`${c.green}✓${c.reset} Entorno: ${c.bold}${env.name}${c.reset}`);
|
|
591
930
|
}
|
|
592
931
|
}
|
|
593
932
|
|
|
594
933
|
log("");
|
|
595
|
-
log(`${c.green}✓${c.reset}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
+
}
|
|
599
940
|
}
|