blackboard-upc 1.0.9 → 1.0.10

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/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to `blackboard-upc` will be documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## [1.0.10] — 2026-04-24
8
+
9
+ ### Added
10
+ - **TTL real del SSO en `whoami`, `status` y `login`** — ahora se muestran las dos ventanas: la del token de Blackboard (~3h, se auto-renueva) y la del SSO de Microsoft (~90 días, la ventana real hasta que hay que re-loguearse). Ejemplo:
11
+ ```
12
+ SSO Microsoft: 89 días · Blackboard: 173 min
13
+ se auto-renueva hasta que el SSO expire o hagas logout
14
+ ```
15
+ - `getSsoExpiry()` helper en `src/auth/login.ts` — lee el expiry de `ESTSAUTHPERSISTENT` (la cookie de Microsoft que controla la persistencia del SSO) desde la sesión guardada, sin llamadas de red
16
+ - `formatSessionLifetime()` helper en `src/ui/theme.ts` — centraliza el formato de las dos líneas (resumen + nota) y maneja el caso donde el SSO es session-only
17
+
18
+ ### Changed
19
+ - `blackboard login` ya no muestra "expira en 3h" (engañoso, porque se auto-renueva); muestra la misma info que `whoami`/`status`
20
+
21
+ ---
22
+
7
23
  ## [1.0.9] — 2026-04-24
8
24
 
9
25
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blackboard-upc",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "CLI no oficial para UPC Aula Virtual (Blackboard Learn) — acceso desde la terminal y MCP para Claude",
5
5
  "main": "run.js",
6
6
  "bin": {
package/src/auth/login.ts CHANGED
@@ -57,6 +57,27 @@ export function resolveDisplayName(userData: any): string | undefined {
57
57
  return userData.studentId || undefined;
58
58
  }
59
59
 
60
+ // Microsoft SSO persistence is controlled specifically by ESTSAUTHPERSIST
61
+ // (set when the user accepts "Keep me signed in"). Other cookies on
62
+ // login.microsoftonline.com like MUID/fpc are tracking and persist ~1 year
63
+ // but don't keep the user signed in — ignore them.
64
+ const SSO_COOKIE_NAMES = new Set(['ESTSAUTHPERSISTENT', 'ESTSAUTHLIGHT', 'ESTSAUTH']);
65
+
66
+ export function getSsoExpiry(cookies: Cookie[]): number | undefined {
67
+ const ssoCookies = cookies.filter(c =>
68
+ SSO_COOKIE_NAMES.has(c.name) &&
69
+ (c.domain.includes('login.microsoftonline.com') || c.domain.includes('login.live.com'))
70
+ );
71
+ const now = Date.now();
72
+ const future = ssoCookies
73
+ .map(c => c.expires)
74
+ .filter((e): e is number => typeof e === 'number' && e > 0)
75
+ .map(e => e * 1000)
76
+ .filter(ms => ms > now);
77
+ if (future.length === 0) return undefined;
78
+ return Math.max(...future);
79
+ }
80
+
60
81
  function extractXsrf(cookies: Cookie[]): string {
61
82
  const bb = cookies.find(c => c.name === 'BbRouter');
62
83
  if (bb) {
@@ -2,11 +2,11 @@ import { Command } from 'commander';
2
2
  import inquirer from 'inquirer';
3
3
  import chalk from 'chalk';
4
4
  import ora from 'ora';
5
- import { login, resolveDisplayName } from '../auth/login.js';
5
+ import { login, resolveDisplayName, getSsoExpiry } from '../auth/login.js';
6
6
  import { loadSession, loadOrRefreshSession, saveSession, clearSession, isSessionValid } from '../auth/session.js';
7
7
  import { createClient } from '../api/client.js';
8
8
  import { getMe } from '../api/courses.js';
9
- import { ok, fail, warn, whatNext } from '../ui/theme.js';
9
+ import { ok, fail, warn, whatNext, formatSessionLifetime } from '../ui/theme.js';
10
10
 
11
11
  export function loginCommand(program: Command) {
12
12
  program
@@ -36,9 +36,11 @@ export function loginCommand(program: Command) {
36
36
  password: opts.password,
37
37
  });
38
38
 
39
- const remainingMin = Math.round((session.expiresAt - Date.now()) / 60_000);
40
- const remainingHr = (remainingMin / 60).toFixed(1);
41
- console.log(ok(`Sesión guardada — expira en ${remainingHr}h`));
39
+ const ssoExpiresAt = getSsoExpiry(session.cookies);
40
+ const { summary, note } = formatSessionLifetime(session.expiresAt, ssoExpiresAt);
41
+ console.log(ok(`Sesión guardada`));
42
+ console.log(chalk.gray(` ${summary}`));
43
+ console.log(chalk.gray(` ${note}`));
42
44
  if (session.userName) console.log(chalk.gray(` Usuario: ${session.userName}`));
43
45
  if (session.userId) console.log(chalk.gray(` ID: ${session.userId}`));
44
46
  whatNext();
@@ -85,7 +87,9 @@ export function loginCommand(program: Command) {
85
87
  }
86
88
 
87
89
  console.log(chalk.green(`Logged in as: ${chalk.bold(session!.userName || session!.userId || 'unknown')}`));
88
- const remaining = Math.round((session!.expiresAt - Date.now()) / 60_000);
89
- console.log(chalk.gray(`Session expires in: ${remaining} minutes`));
90
+ const ssoExpiresAt = getSsoExpiry(session!.cookies);
91
+ const { summary, note } = formatSessionLifetime(session!.expiresAt, ssoExpiresAt);
92
+ console.log(chalk.gray(summary));
93
+ console.log(chalk.gray(note));
90
94
  });
91
95
  }
package/src/index.ts CHANGED
@@ -9,15 +9,15 @@ import { assignmentsCommand } from './commands/assignments.js';
9
9
  import { loadSession, loadOrRefreshSession, saveSession, isSessionValid } from './auth/session.js';
10
10
  import { createClient } from './api/client.js';
11
11
  import { getMe, getSystemVersion } from './api/courses.js';
12
- import { resolveDisplayName } from './auth/login.js';
13
- import { BANNER, ok, fail, hint } from './ui/theme.js';
12
+ import { resolveDisplayName, getSsoExpiry } from './auth/login.js';
13
+ import { BANNER, ok, fail, hint, formatSessionLifetime } from './ui/theme.js';
14
14
 
15
15
  const program = new Command();
16
16
 
17
17
  program
18
18
  .name('blackboard')
19
19
  .description('CLI no oficial para UPC Aula Virtual (Blackboard Learn)')
20
- .version('1.0.9')
20
+ .version('1.0.10')
21
21
  .addHelpText('beforeAll', BANNER);
22
22
 
23
23
  // Auth commands
@@ -50,10 +50,13 @@ program
50
50
  } catch {}
51
51
  }
52
52
 
53
+ const ssoExpiresAt = valid ? getSsoExpiry(session!.cookies) : undefined;
54
+
53
55
  const result = {
54
56
  loggedIn: valid,
55
57
  user: valid ? { id: session!.userId, name: session!.userName } : null,
56
58
  sessionExpiresAt: valid ? new Date(session!.expiresAt).toISOString() : null,
59
+ ssoExpiresAt: ssoExpiresAt ? new Date(ssoExpiresAt).toISOString() : null,
57
60
  server: sysVersion?.learn ?? null,
58
61
  };
59
62
 
@@ -62,9 +65,10 @@ program
62
65
  const v = sysVersion?.learn;
63
66
  console.log(`\n Servidor: ${chalk.cyan(`Blackboard Learn ${v?.major}.${v?.minor}.${v?.patch} (${v?.build})`)}`);
64
67
  if (valid) {
65
- const remaining = Math.round((session!.expiresAt - Date.now()) / 60_000);
68
+ const { summary, note } = formatSessionLifetime(session!.expiresAt, ssoExpiresAt);
66
69
  console.log(` Sesión: ${ok(`autenticado como ${chalk.bold(session!.userName || session!.userId || 'unknown')}`)}`);
67
- console.log(` ${chalk.gray(`expira en ${remaining} min`)}`);
70
+ console.log(` ${chalk.gray(summary)}`);
71
+ console.log(` ${chalk.gray(note)}`);
68
72
  } else {
69
73
  console.log(` Sesión: ${fail('no autenticado')} — ejecuta: ${hint('blackboard login')}`);
70
74
  }
package/src/ui/theme.ts CHANGED
@@ -21,6 +21,32 @@ ${upcRed(' ██████ ███████ ██ ██ ███
21
21
  ${chalk.dim('CLI no oficial para UPC Aula Virtual · Blackboard Learn')}
22
22
  `;
23
23
 
24
+ export function formatSessionLifetime(bbExpiresAt: number, ssoExpiresAt?: number): {
25
+ summary: string;
26
+ note: string;
27
+ } {
28
+ const now = Date.now();
29
+ const bbMin = Math.max(0, Math.round((bbExpiresAt - now) / 60_000));
30
+
31
+ if (!ssoExpiresAt || ssoExpiresAt <= now) {
32
+ return {
33
+ summary: `Blackboard: ${bbMin} min`,
34
+ note: 'SSO sin "mantener sesión" — deberás reloguearte cuando expire',
35
+ };
36
+ }
37
+
38
+ const ssoMs = ssoExpiresAt - now;
39
+ const ssoDays = Math.floor(ssoMs / 86_400_000);
40
+ const ssoStr = ssoDays >= 1
41
+ ? `${ssoDays} día${ssoDays === 1 ? '' : 's'}`
42
+ : `${Math.round(ssoMs / 3_600_000)} h`;
43
+
44
+ return {
45
+ summary: `SSO Microsoft: ${ssoStr} · Blackboard: ${bbMin} min`,
46
+ note: 'se auto-renueva hasta que el SSO expire o hagas logout',
47
+ };
48
+ }
49
+
24
50
  export function whatNext() {
25
51
  console.log(`
26
52
  ${chalk.bold('¿Qué puedo hacer ahora?')}