essalud-cli 0.1.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/LICENSE +77 -0
- package/README.md +141 -0
- package/dist/essalud/api.d.ts +140 -0
- package/dist/essalud/api.js +136 -0
- package/dist/essalud/cmd-cancelar.d.ts +5 -0
- package/dist/essalud/cmd-cancelar.js +74 -0
- package/dist/essalud/cmd-citas.d.ts +1 -0
- package/dist/essalud/cmd-citas.js +47 -0
- package/dist/essalud/cmd-especialidades.d.ts +1 -0
- package/dist/essalud/cmd-especialidades.js +30 -0
- package/dist/essalud/cmd-fechas.d.ts +1 -0
- package/dist/essalud/cmd-fechas.js +47 -0
- package/dist/essalud/cmd-login.d.ts +7 -0
- package/dist/essalud/cmd-login.js +223 -0
- package/dist/essalud/cmd-perfil.d.ts +1 -0
- package/dist/essalud/cmd-perfil.js +38 -0
- package/dist/essalud/cmd-reservar.d.ts +14 -0
- package/dist/essalud/cmd-reservar.js +147 -0
- package/dist/essalud/cmd-token.d.ts +1 -0
- package/dist/essalud/cmd-token.js +62 -0
- package/dist/essalud/har.d.ts +7 -0
- package/dist/essalud/har.js +76 -0
- package/dist/essalud/index.d.ts +2 -0
- package/dist/essalud/index.js +87 -0
- package/dist/essalud/interactive.d.ts +5 -0
- package/dist/essalud/interactive.js +495 -0
- package/dist/essalud/jwt.d.ts +12 -0
- package/dist/essalud/jwt.js +29 -0
- package/dist/essalud-bin.d.ts +2 -0
- package/dist/essalud-bin.js +24 -0
- package/package.json +64 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getProgramacionDisponible } from "./api.js";
|
|
2
|
+
function formatCupo(c, idx) {
|
|
3
|
+
console.log(`\n [${idx + 1}] ${c.apeNomProf}`);
|
|
4
|
+
console.log(` Fecha : ${c.fechaCitaProg}`);
|
|
5
|
+
console.log(` Turno : ${c.turnoIni} - ${c.turnoFin}`);
|
|
6
|
+
console.log(` Consultorio: ${c.consultorio}`);
|
|
7
|
+
console.log(` codProgAsis: ${c.codProgAsis}`);
|
|
8
|
+
if (c.vCupoDisp && c.vCupoDisp.length > 0) {
|
|
9
|
+
console.log(` Slots disponibles (${c.vCupoDisp.length}):`);
|
|
10
|
+
for (const slot of c.vCupoDisp) {
|
|
11
|
+
console.log(` cupo ${String(slot.nroCupo).padStart(3)} hora ${slot.hora}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
console.log(" Sin slots disponibles.");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function cmdFechas(codCentro, codServicioHosp, codActSubAct) {
|
|
19
|
+
let cupos;
|
|
20
|
+
try {
|
|
21
|
+
cupos = await getProgramacionDisponible({
|
|
22
|
+
codCentro,
|
|
23
|
+
codServicioHosp,
|
|
24
|
+
codActSubAct,
|
|
25
|
+
codTurnoDeseado: "0",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
console.error(String(err));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
if (cupos.length === 0) {
|
|
33
|
+
console.log(`Sin programación disponible para centro=${codCentro} servicio=${codServicioHosp} actividad=${codActSubAct}.`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.log(`Cupos disponibles — centro=${codCentro} servicio=${codServicioHosp} actividad=${codActSubAct} (${cupos.length} turnos)`);
|
|
37
|
+
console.log("─".repeat(60));
|
|
38
|
+
cupos.forEach(formatCupo);
|
|
39
|
+
console.log("");
|
|
40
|
+
console.log("Para reservar, elige un slot y pasa el JSON del cupo:");
|
|
41
|
+
console.log(" essalud reservar --cupo-json '<json>' --nro-cupo <nro> --hora-slot <hora> --confirm");
|
|
42
|
+
console.log("Ejemplo (primer slot del primer turno):");
|
|
43
|
+
if (cupos[0]?.vCupoDisp[0]) {
|
|
44
|
+
const ejemplo = { ...cupos[0] };
|
|
45
|
+
console.log(` essalud reservar --cupo-json '${JSON.stringify(ejemplo)}' --nro-cupo ${cupos[0].vCupoDisp[0].nroCupo} --hora-slot ${cupos[0].vCupoDisp[0].hora}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { chromium } from "playwright";
|
|
4
|
+
import { getPerfil, PACIENTE_PATH, TOKEN_PATH } from "./api.js";
|
|
5
|
+
import { extractPacienteFromHar, extractTokenFromHar, parsePacienteFromLgBody } from "./har.js";
|
|
6
|
+
import { decodeJwtPayload, JWT_RE, looksLikeEsSaludJwt } from "./jwt.js";
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Constants
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const LOGIN_URL = "https://miconsulta.essalud.gob.pe/login";
|
|
11
|
+
const API_HOST = "api.miconsulta.essalud.gob.pe";
|
|
12
|
+
/** Tiempo máximo que esperamos a que el usuario complete el login en el navegador. */
|
|
13
|
+
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Login por navegador (Playwright)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
function sleep(ms) {
|
|
18
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Abre un navegador real (headed), espera a que el usuario se loguee y captura
|
|
22
|
+
* el Bearer token y los datos del paciente directamente de la red.
|
|
23
|
+
* Devuelve null si no se pudo capturar (timeout, navegador cerrado, etc.).
|
|
24
|
+
*/
|
|
25
|
+
async function loginWithBrowser() {
|
|
26
|
+
let browser;
|
|
27
|
+
try {
|
|
28
|
+
browser = await chromium.launch({ headless: false });
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
const msg = String(err);
|
|
32
|
+
if (/Executable doesn't exist|playwright install/i.test(msg)) {
|
|
33
|
+
console.error("\nFalta el navegador de Playwright. Instálalo una sola vez con:");
|
|
34
|
+
console.error(" npx playwright install chromium\n");
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.error(`\nNo se pudo abrir el navegador: ${msg}\n`);
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const context = await browser.newContext();
|
|
42
|
+
const page = await context.newPage();
|
|
43
|
+
let jwt = null;
|
|
44
|
+
let paciente = null;
|
|
45
|
+
// Capturar el Bearer de cualquier request autenticado a la API.
|
|
46
|
+
context.on("request", (req) => {
|
|
47
|
+
if (jwt || !req.url().includes(API_HOST))
|
|
48
|
+
return;
|
|
49
|
+
const auth = req.headers().authorization ?? "";
|
|
50
|
+
const m = auth.match(/Bearer\s+(\S+)/i);
|
|
51
|
+
if (m && looksLikeEsSaludJwt(m[1]))
|
|
52
|
+
jwt = m[1];
|
|
53
|
+
});
|
|
54
|
+
// Capturar paciente (y, como respaldo, el token) del body del login /api/lg.
|
|
55
|
+
context.on("response", async (res) => {
|
|
56
|
+
if (!res.url().includes("/api/lg"))
|
|
57
|
+
return;
|
|
58
|
+
let body;
|
|
59
|
+
try {
|
|
60
|
+
body = await res.text();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
paciente = parsePacienteFromLgBody(body) ?? paciente;
|
|
66
|
+
if (!jwt) {
|
|
67
|
+
const m = body.match(JWT_RE);
|
|
68
|
+
if (m && looksLikeEsSaludJwt(m[0]))
|
|
69
|
+
jwt = m[0];
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
await page.goto(LOGIN_URL, { waitUntil: "domcontentloaded" }).catch(() => { });
|
|
73
|
+
console.log();
|
|
74
|
+
console.log("─".repeat(60));
|
|
75
|
+
console.log("Se abrió un navegador en:");
|
|
76
|
+
console.log(` ${LOGIN_URL}`);
|
|
77
|
+
console.log();
|
|
78
|
+
console.log("Pasos:");
|
|
79
|
+
console.log(" 1. Ingresa tu DNI y clave.");
|
|
80
|
+
console.log(" 2. Completa el captcha de Cloudflare Turnstile.");
|
|
81
|
+
console.log(" 3. Espera a ver tu panel (lista de citas).");
|
|
82
|
+
console.log();
|
|
83
|
+
console.log("Voy a capturar tu sesión automáticamente. No cierres esta terminal.");
|
|
84
|
+
console.log("─".repeat(60));
|
|
85
|
+
let browserClosed = false;
|
|
86
|
+
browser.on("disconnected", () => {
|
|
87
|
+
browserClosed = true;
|
|
88
|
+
});
|
|
89
|
+
const start = Date.now();
|
|
90
|
+
while (!jwt && !browserClosed && Date.now() - start < LOGIN_TIMEOUT_MS) {
|
|
91
|
+
await sleep(500);
|
|
92
|
+
}
|
|
93
|
+
await browser.close().catch(() => { });
|
|
94
|
+
if (!jwt)
|
|
95
|
+
return null;
|
|
96
|
+
return { jwt, paciente };
|
|
97
|
+
}
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Persistencia
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
async function saveToken(jwt) {
|
|
102
|
+
await mkdir(dirname(TOKEN_PATH), { recursive: true });
|
|
103
|
+
await writeFile(TOKEN_PATH, `${jwt}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
104
|
+
// writeFile mode puede quedar enmascarado por el umask — forzamos los permisos.
|
|
105
|
+
await chmod(TOKEN_PATH, 0o600);
|
|
106
|
+
}
|
|
107
|
+
async function savePaciente(paciente) {
|
|
108
|
+
await mkdir(dirname(PACIENTE_PATH), { recursive: true });
|
|
109
|
+
await writeFile(PACIENTE_PATH, `${JSON.stringify(paciente, null, 2)}\n`, {
|
|
110
|
+
encoding: "utf-8",
|
|
111
|
+
mode: 0o600,
|
|
112
|
+
});
|
|
113
|
+
await chmod(PACIENTE_PATH, 0o600);
|
|
114
|
+
}
|
|
115
|
+
function pacienteLabel(paciente) {
|
|
116
|
+
const nombre = [paciente.apePaterno, paciente.apeMaterno, paciente.nombres]
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.join(" ");
|
|
119
|
+
return `${nombre} · ${paciente.desCentro}`;
|
|
120
|
+
}
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Validación: confirma el token llamando a /perfil
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
async function validateAndPrint(jwt) {
|
|
125
|
+
let perfil;
|
|
126
|
+
try {
|
|
127
|
+
perfil = await getPerfil();
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
throw new Error(`Token guardado pero /perfil falló: ${String(err)}`);
|
|
131
|
+
}
|
|
132
|
+
const nombre = [perfil.nombreAsegurado, perfil.apellidoPatAsegurado, perfil.apellidoMatAsegurado]
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.join(" ");
|
|
135
|
+
const payload = decodeJwtPayload(jwt);
|
|
136
|
+
let expInfo = "";
|
|
137
|
+
if (payload?.exp) {
|
|
138
|
+
const expDate = new Date(payload.exp * 1000);
|
|
139
|
+
expInfo = ` | Expira: ${expDate.toLocaleString("es-PE", { timeZone: "America/Lima" })}`;
|
|
140
|
+
}
|
|
141
|
+
console.log(`\n✓ Logueado como: ${nombre || "(sin nombre en perfil)"}${expInfo}`);
|
|
142
|
+
}
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Comando principal
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
export async function cmdLogin(opts = {}) {
|
|
147
|
+
// Plan B — token pegado a mano.
|
|
148
|
+
if (opts.token) {
|
|
149
|
+
const jwt = opts.token.trim().replace(/^Bearer\s+/i, "");
|
|
150
|
+
if (!jwt.startsWith("ey")) {
|
|
151
|
+
console.error("Error: el token no parece un JWT válido (debería empezar con 'ey...').");
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
await saveToken(jwt);
|
|
155
|
+
console.log("Token guardado.");
|
|
156
|
+
await validateAndPrint(jwt);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Plan B — importar desde un HAR exportado de DevTools.
|
|
160
|
+
if (opts.fromHar) {
|
|
161
|
+
let content;
|
|
162
|
+
try {
|
|
163
|
+
content = await readFile(opts.fromHar, "utf-8");
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
console.error(`Error: no se pudo leer el HAR: ${opts.fromHar}`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
const jwt = extractTokenFromHar(opts.fromHar, content);
|
|
170
|
+
if (!jwt) {
|
|
171
|
+
console.error(`No se encontró ningún header Authorization: Bearer en requests a ${API_HOST} en el HAR.`);
|
|
172
|
+
console.error("Verifica que el HAR incluya requests autenticados al panel (no solo el login).");
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
await saveToken(jwt);
|
|
176
|
+
console.log("Token extraído del HAR y guardado.");
|
|
177
|
+
const paciente = extractPacienteFromHar(content);
|
|
178
|
+
if (paciente) {
|
|
179
|
+
await savePaciente(paciente);
|
|
180
|
+
console.log(`Paciente guardado: ${pacienteLabel(paciente)}`);
|
|
181
|
+
}
|
|
182
|
+
await validateAndPrint(jwt);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// Plan A — navegador headed + captura automática (Playwright).
|
|
186
|
+
const result = await loginWithBrowser();
|
|
187
|
+
if (result) {
|
|
188
|
+
await saveToken(result.jwt);
|
|
189
|
+
if (result.paciente) {
|
|
190
|
+
await savePaciente(result.paciente);
|
|
191
|
+
console.log(`Paciente guardado: ${pacienteLabel(result.paciente)}`);
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
await validateAndPrint(result.jwt);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
console.error(String(err));
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// Falló la captura automática → ofrecer los caminos manuales.
|
|
203
|
+
console.error();
|
|
204
|
+
console.error("No se pudo capturar el token automáticamente.");
|
|
205
|
+
console.error("Posibles causas: no completaste el login, Turnstile bloqueó, o cerraste el navegador.");
|
|
206
|
+
console.error();
|
|
207
|
+
console.error("Opciones para continuar:");
|
|
208
|
+
console.error();
|
|
209
|
+
console.error(" Opción A — pegar el token manualmente:");
|
|
210
|
+
console.error(" 1. Abre https://miconsulta.essalud.gob.pe en Chrome");
|
|
211
|
+
console.error(" 2. Inicia sesión y abre DevTools (F12) → Network");
|
|
212
|
+
console.error(" 3. Filtra por 'api.miconsulta' → haz click en cualquier request");
|
|
213
|
+
console.error(" 4. En Headers, copia el valor de: Authorization: Bearer <token>");
|
|
214
|
+
console.error(" 5. Corre:");
|
|
215
|
+
console.error(" essalud login --token <token>");
|
|
216
|
+
console.error();
|
|
217
|
+
console.error(" Opción B — importar desde HAR:");
|
|
218
|
+
console.error(" 1. En DevTools → Network → Export HAR (ícono de descarga)");
|
|
219
|
+
console.error(" 2. Corre:");
|
|
220
|
+
console.error(" essalud login --from-har ~/Downloads/captura.har");
|
|
221
|
+
console.error();
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cmdPerfil(): Promise<void>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getPaciente, getPerfil } from "./api.js";
|
|
2
|
+
function printField(label, value) {
|
|
3
|
+
if (value === null || value === undefined || value === "")
|
|
4
|
+
return;
|
|
5
|
+
if (typeof value === "object") {
|
|
6
|
+
// Expandir objetos anidados (p.ej. contacto)
|
|
7
|
+
for (const [k, v] of Object.entries(value)) {
|
|
8
|
+
printField(`${label}.${k}`, v);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
console.log(`${label.padEnd(20)} : ${String(value)}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function cmdPerfil() {
|
|
16
|
+
let perfil;
|
|
17
|
+
try {
|
|
18
|
+
perfil = await getPerfil();
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
console.error(String(err));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const paciente = await getPaciente();
|
|
25
|
+
console.log("Perfil EsSalud");
|
|
26
|
+
console.log("─".repeat(40));
|
|
27
|
+
// Datos enriquecidos desde paciente.json (que /perfil no trae)
|
|
28
|
+
if (paciente) {
|
|
29
|
+
const nombre = [paciente.apePaterno, paciente.apeMaterno, paciente.nombres]
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join(" ");
|
|
32
|
+
console.log(`${"Nombre".padEnd(20)} : ${nombre}`);
|
|
33
|
+
console.log(`${"Centro".padEnd(20)} : ${paciente.desCentro}`);
|
|
34
|
+
}
|
|
35
|
+
for (const [k, v] of Object.entries(perfil)) {
|
|
36
|
+
printField(k, v);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ReservarOptions {
|
|
2
|
+
cupoJson?: string;
|
|
3
|
+
nroCupo?: string;
|
|
4
|
+
horaSlot?: string;
|
|
5
|
+
codProgAsis?: string;
|
|
6
|
+
consultorio?: string;
|
|
7
|
+
fecha?: string;
|
|
8
|
+
turnoIni?: string;
|
|
9
|
+
turnoFin?: string;
|
|
10
|
+
celular?: string;
|
|
11
|
+
email?: string;
|
|
12
|
+
confirm: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function cmdReservar(opts: ReservarOptions): Promise<void>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { generarCita, getPerfil } from "./api.js";
|
|
3
|
+
async function confirmarInteractivo(mensaje) {
|
|
4
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
rl.question(`${mensaje} [s/N] `, (answer) => {
|
|
7
|
+
rl.close();
|
|
8
|
+
resolve(answer.toLowerCase() === "s");
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export async function cmdReservar(opts) {
|
|
13
|
+
// Resolver el cupo desde --cupo-json o desde flags individuales
|
|
14
|
+
let cupoData = {};
|
|
15
|
+
if (opts.cupoJson) {
|
|
16
|
+
try {
|
|
17
|
+
cupoData = JSON.parse(opts.cupoJson);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
console.error("Error: --cupo-json no es JSON válido.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
if (opts.codProgAsis)
|
|
26
|
+
cupoData.codProgAsis = opts.codProgAsis;
|
|
27
|
+
if (opts.consultorio)
|
|
28
|
+
cupoData.consultorio = opts.consultorio;
|
|
29
|
+
if (opts.fecha)
|
|
30
|
+
cupoData.fechaCitaProg = opts.fecha;
|
|
31
|
+
if (opts.turnoIni)
|
|
32
|
+
cupoData.turnoIni = opts.turnoIni;
|
|
33
|
+
if (opts.turnoFin)
|
|
34
|
+
cupoData.turnoFin = opts.turnoFin;
|
|
35
|
+
}
|
|
36
|
+
// --nro-cupo es obligatorio (elige el slot de vCupoDisp)
|
|
37
|
+
if (!opts.nroCupo) {
|
|
38
|
+
console.error("Error: falta --nro-cupo (elige un slot de la salida de 'fechas').");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const nroCupo = Number(opts.nroCupo);
|
|
42
|
+
if (Number.isNaN(nroCupo)) {
|
|
43
|
+
console.error(`Error: --nro-cupo debe ser un número; se recibió "${opts.nroCupo}".`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
// Validar campos obligatorios del cupo
|
|
47
|
+
const requiredFields = [
|
|
48
|
+
"codProgAsis",
|
|
49
|
+
"consultorio",
|
|
50
|
+
"fechaCitaProg",
|
|
51
|
+
"turnoIni",
|
|
52
|
+
"turnoFin",
|
|
53
|
+
];
|
|
54
|
+
for (const field of requiredFields) {
|
|
55
|
+
if (!cupoData[field]) {
|
|
56
|
+
console.error(`Error: falta el campo "${field}". Usa --cupo-json '<json>' o los flags individuales.`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Resolver numCelular y email: flags > perfil
|
|
61
|
+
let numCelular = opts.celular ?? "";
|
|
62
|
+
let email = opts.email ?? "";
|
|
63
|
+
if (!numCelular || !email) {
|
|
64
|
+
let perfil;
|
|
65
|
+
try {
|
|
66
|
+
perfil = await getPerfil();
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.error(`Error al obtener perfil: ${String(err)}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
if (!numCelular)
|
|
73
|
+
numCelular = perfil.contacto?.nroCelular ?? "";
|
|
74
|
+
if (!email)
|
|
75
|
+
email = perfil.contacto?.email ?? "";
|
|
76
|
+
}
|
|
77
|
+
if (!numCelular || !email) {
|
|
78
|
+
console.error("Error: no se encontró celular/email en el perfil. Pásalos con --celular y --email.");
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
// Encontrar la hora del slot si se pasó --hora-slot (para mostrar)
|
|
82
|
+
let horaDisplay = opts.horaSlot ?? "(no especificada)";
|
|
83
|
+
if (!opts.horaSlot && cupoData.vCupoDisp) {
|
|
84
|
+
const slot = cupoData.vCupoDisp.find((s) => s.nroCupo === nroCupo);
|
|
85
|
+
if (slot)
|
|
86
|
+
horaDisplay = slot.hora;
|
|
87
|
+
}
|
|
88
|
+
// Construir el payload final
|
|
89
|
+
const payload = {
|
|
90
|
+
codProgAsis: cupoData.codProgAsis,
|
|
91
|
+
consultorio: cupoData.consultorio,
|
|
92
|
+
fechaCitaPro: cupoData.fechaCitaProg,
|
|
93
|
+
nroCupo,
|
|
94
|
+
turnoIni: cupoData.turnoIni,
|
|
95
|
+
turnoFin: cupoData.turnoFin,
|
|
96
|
+
numCelular,
|
|
97
|
+
email,
|
|
98
|
+
};
|
|
99
|
+
// Mostrar resumen
|
|
100
|
+
console.log("Resumen de la cita a reservar:");
|
|
101
|
+
console.log("─".repeat(50));
|
|
102
|
+
if (cupoData.apeNomProf)
|
|
103
|
+
console.log(` Profesional : ${cupoData.apeNomProf}`);
|
|
104
|
+
console.log(` Fecha : ${payload.fechaCitaPro}`);
|
|
105
|
+
console.log(` Hora del slot : ${horaDisplay}`);
|
|
106
|
+
console.log(` Cupo nro : ${payload.nroCupo}`);
|
|
107
|
+
console.log(` Turno : ${payload.turnoIni} - ${payload.turnoFin}`);
|
|
108
|
+
console.log(` Consultorio : ${payload.consultorio}`);
|
|
109
|
+
console.log(` codProgAsis : ${payload.codProgAsis}`);
|
|
110
|
+
console.log(` Celular : ${payload.numCelular}`);
|
|
111
|
+
console.log(` Email : ${payload.email}`);
|
|
112
|
+
console.log("");
|
|
113
|
+
// DRY-RUN por defecto
|
|
114
|
+
if (!opts.confirm) {
|
|
115
|
+
console.log("[dry-run] No se realizó ninguna reserva real.");
|
|
116
|
+
console.log("Para reservar de verdad: agrega --confirm al comando.");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Confirmación interactiva obligatoria
|
|
120
|
+
const ok = await confirmarInteractivo("ADVERTENCIA: Esto agenda una cita REAL en EsSalud. ¿Confirmas? (ocupa un cupo)");
|
|
121
|
+
if (!ok) {
|
|
122
|
+
console.log("Cancelado. No se realizó ninguna reserva.");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// POST generarCita
|
|
126
|
+
let result;
|
|
127
|
+
try {
|
|
128
|
+
result = await generarCita(payload);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
console.error(`Error al generar la cita: ${String(err)}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
const cita = result[0];
|
|
135
|
+
console.log("\nCita reservada exitosamente.");
|
|
136
|
+
console.log("─".repeat(50));
|
|
137
|
+
if (cita?.numCitaCreada)
|
|
138
|
+
console.log(` Nro cita : ${cita.numCitaCreada}`);
|
|
139
|
+
if (cita?.fechaCita)
|
|
140
|
+
console.log(` Fecha : ${cita.fechaCita}`);
|
|
141
|
+
if (cita?.horaCita)
|
|
142
|
+
console.log(` Hora : ${cita.horaCita}`);
|
|
143
|
+
if (cita?.apeNomProf)
|
|
144
|
+
console.log(` Profesional: ${cita.apeNomProf}`);
|
|
145
|
+
if (cita?.desServHosp)
|
|
146
|
+
console.log(` Servicio : ${cita.desServHosp}`);
|
|
147
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cmdToken(): Promise<void>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readToken, TOKEN_PATH } from "./api.js";
|
|
2
|
+
import { decodeJwtPayload } from "./jwt.js";
|
|
3
|
+
export async function cmdToken() {
|
|
4
|
+
let token;
|
|
5
|
+
try {
|
|
6
|
+
token = await readToken();
|
|
7
|
+
}
|
|
8
|
+
catch (err) {
|
|
9
|
+
console.error(String(err));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
const payload = decodeJwtPayload(token);
|
|
13
|
+
if (!payload) {
|
|
14
|
+
console.log("Token guardado pero no es un JWT válido (no se puede decodificar).");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const exp = typeof payload.exp === "number" ? payload.exp : null;
|
|
18
|
+
const iat = typeof payload.iat === "number" ? payload.iat : null;
|
|
19
|
+
// El JWT de EsSalud usa user_name (no sub) y scope es un array
|
|
20
|
+
const userName = typeof payload.user_name === "string"
|
|
21
|
+
? payload.user_name
|
|
22
|
+
: typeof payload.sub === "string"
|
|
23
|
+
? payload.sub
|
|
24
|
+
: null;
|
|
25
|
+
const scope = Array.isArray(payload.scope)
|
|
26
|
+
? payload.scope.join(", ")
|
|
27
|
+
: typeof payload.scope === "string"
|
|
28
|
+
? payload.scope
|
|
29
|
+
: null;
|
|
30
|
+
const clientId = typeof payload.client_id === "string" ? payload.client_id : null;
|
|
31
|
+
const now = Math.floor(Date.now() / 1000);
|
|
32
|
+
const expired = exp !== null ? now > exp : null;
|
|
33
|
+
console.log("Token EsSalud");
|
|
34
|
+
console.log("─".repeat(40));
|
|
35
|
+
console.log(`Archivo : ${TOKEN_PATH}`);
|
|
36
|
+
if (userName)
|
|
37
|
+
console.log(`Usuario : ${userName}`);
|
|
38
|
+
if (clientId)
|
|
39
|
+
console.log(`Cliente : ${clientId}`);
|
|
40
|
+
if (scope)
|
|
41
|
+
console.log(`Scope : ${scope}`);
|
|
42
|
+
if (iat)
|
|
43
|
+
console.log(`Emitido : ${new Date(iat * 1000).toLocaleString("es-PE")}`);
|
|
44
|
+
if (exp) {
|
|
45
|
+
const expiresAt = new Date(exp * 1000).toLocaleString("es-PE");
|
|
46
|
+
const secsLeft = exp - now;
|
|
47
|
+
if (expired) {
|
|
48
|
+
console.log(`Vence : ${expiresAt} (VENCIDO hace ${Math.abs(secsLeft)}s)`);
|
|
49
|
+
console.log("\nEl token está vencido. Necesitas obtener uno nuevo.");
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const minsLeft = Math.floor(secsLeft / 60);
|
|
53
|
+
const hoursLeft = Math.floor(minsLeft / 60);
|
|
54
|
+
const display = hoursLeft > 0 ? `${hoursLeft}h ${minsLeft % 60}min` : `${minsLeft} min`;
|
|
55
|
+
console.log(`Vence : ${expiresAt} (en ${display})`);
|
|
56
|
+
console.log("\nToken vigente.");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log("Vence : (sin campo exp)");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PacienteData } from "./api.js";
|
|
2
|
+
/** Extrae los datos del paciente del body JSON de la respuesta /api/lg. */
|
|
3
|
+
export declare function parsePacienteFromLgBody(responseText: string): PacienteData | null;
|
|
4
|
+
/** Extrae el JWT Bearer más reciente de un HAR. */
|
|
5
|
+
export declare function extractTokenFromHar(harPath: string, harContent: string): string | null;
|
|
6
|
+
/** Busca la entry /api/lg en el HAR y extrae datos del paciente. */
|
|
7
|
+
export declare function extractPacienteFromHar(harContent: string): PacienteData | null;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Helpers puros para extraer el token y los datos del paciente de un HAR
|
|
2
|
+
// exportado de DevTools, o del body de la respuesta /api/lg del login.
|
|
3
|
+
import { JWT_RE, looksLikeEsSaludJwt } from "./jwt.js";
|
|
4
|
+
/** Extrae los datos del paciente del body JSON de la respuesta /api/lg. */
|
|
5
|
+
export function parsePacienteFromLgBody(responseText) {
|
|
6
|
+
if (!responseText)
|
|
7
|
+
return null;
|
|
8
|
+
try {
|
|
9
|
+
const json = JSON.parse(responseText);
|
|
10
|
+
const raw = json?.data?.paciente;
|
|
11
|
+
if (!raw)
|
|
12
|
+
return null;
|
|
13
|
+
return {
|
|
14
|
+
codCentro: raw.codCentro ?? "",
|
|
15
|
+
desCentro: raw.desCentro ?? "",
|
|
16
|
+
apePaterno: raw.apePaterno ?? "",
|
|
17
|
+
apeMaterno: raw.apeMaterno ?? "",
|
|
18
|
+
nombres: raw.nombres ?? null,
|
|
19
|
+
email: raw.email ?? null,
|
|
20
|
+
celular: raw.nroCelular ?? raw.celular ?? null,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Extrae el JWT Bearer más reciente de un HAR. */
|
|
28
|
+
export function extractTokenFromHar(harPath, harContent) {
|
|
29
|
+
let har;
|
|
30
|
+
try {
|
|
31
|
+
har = JSON.parse(harContent);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new Error(`HAR inválido (no es JSON válido): ${harPath}`);
|
|
35
|
+
}
|
|
36
|
+
const entries = har.log?.entries ?? [];
|
|
37
|
+
const candidates = [];
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const req = entry.request;
|
|
40
|
+
const resp = entry.response;
|
|
41
|
+
// 1) Header Authorization: Bearer <jwt> (cuando el HAR lo conserva).
|
|
42
|
+
const authHeader = req?.headers?.find((h) => h.name.toLowerCase() === "authorization");
|
|
43
|
+
const authMatch = authHeader?.value.match(/Bearer\s+(\S+)/i);
|
|
44
|
+
if (authMatch && looksLikeEsSaludJwt(authMatch[1]))
|
|
45
|
+
candidates.push(authMatch[1]);
|
|
46
|
+
// 2) JWT embebido en el body (el login /api/lg devuelve el token ahí; Chrome
|
|
47
|
+
// NO exporta el header Authorization al HAR, pero sí el body).
|
|
48
|
+
for (const blob of [resp?.content?.text ?? "", req?.postData?.text ?? ""]) {
|
|
49
|
+
const m = blob.match(JWT_RE);
|
|
50
|
+
if (m && looksLikeEsSaludJwt(m[0]))
|
|
51
|
+
candidates.push(m[0]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (candidates.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
// El más reciente (los HAR son cronológicos).
|
|
57
|
+
return candidates[candidates.length - 1];
|
|
58
|
+
}
|
|
59
|
+
/** Busca la entry /api/lg en el HAR y extrae datos del paciente. */
|
|
60
|
+
export function extractPacienteFromHar(harContent) {
|
|
61
|
+
let har;
|
|
62
|
+
try {
|
|
63
|
+
har = JSON.parse(harContent);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
for (const entry of har.log?.entries ?? []) {
|
|
69
|
+
if (!(entry.request?.url ?? "").includes("/api/lg"))
|
|
70
|
+
continue;
|
|
71
|
+
const paciente = parsePacienteFromLgBody(entry.response?.content?.text ?? "");
|
|
72
|
+
if (paciente)
|
|
73
|
+
return paciente;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|