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 +16 -0
- package/package.json +1 -1
- package/src/auth/login.ts +21 -0
- package/src/commands/login.ts +11 -7
- package/src/index.ts +9 -5
- package/src/ui/theme.ts +26 -0
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
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) {
|
package/src/commands/login.ts
CHANGED
|
@@ -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
|
|
40
|
-
const
|
|
41
|
-
console.log(ok(`Sesión guardada
|
|
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
|
|
89
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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?')}
|