campus-cli
Advanced tools
| import type { CampusAccount } from './types.js'; | ||
| export declare const CAMPUS_ACCOUNT_URL: string; | ||
| export declare function buildAuthorizeUrl(input: { | ||
| redirectUri: string; | ||
| codeChallenge: string; | ||
| state: string; | ||
| }): string; | ||
| export declare function exchangeCode(input: { | ||
| code: string; | ||
| verifier: string; | ||
| redirectUri: string; | ||
| }): Promise<{ | ||
| account: CampusAccount; | ||
| accessToken: string; | ||
| refreshToken: string; | ||
| expiresIn: number; | ||
| }>; | ||
| export declare function refreshTokens(refreshToken: string): Promise<{ | ||
| account: CampusAccount; | ||
| accessToken: string; | ||
| refreshToken: string; | ||
| expiresIn: number; | ||
| }>; | ||
| export declare function fetchAccount(accessToken: string): Promise<CampusAccount>; |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.CAMPUS_ACCOUNT_URL = void 0; | ||
| exports.buildAuthorizeUrl = buildAuthorizeUrl; | ||
| exports.exchangeCode = exchangeCode; | ||
| exports.refreshTokens = refreshTokens; | ||
| exports.fetchAccount = fetchAccount; | ||
| // The hosted "Campus account" service (Google login, OAuth2+PKCE) — same | ||
| // backend that serves the MCP connector at mcp.campuscli.com/.well-known/oauth-authorization-server. | ||
| exports.CAMPUS_ACCOUNT_URL = (process.env.CAMPUS_ACCOUNT_URL ?? 'https://mcp.campuscli.com').replace(/\/$/, ''); | ||
| function buildAuthorizeUrl(input) { | ||
| const url = new URL(`${exports.CAMPUS_ACCOUNT_URL}/v1/auth/google/start`); | ||
| url.searchParams.set('redirect_uri', input.redirectUri); | ||
| url.searchParams.set('code_challenge', input.codeChallenge); | ||
| url.searchParams.set('state', input.state); | ||
| return url.toString(); | ||
| } | ||
| async function exchangeCode(input) { | ||
| const res = await fetch(`${exports.CAMPUS_ACCOUNT_URL}/v1/auth/exchange`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ code: input.code, verifier: input.verifier, redirectUri: input.redirectUri }), | ||
| }); | ||
| if (!res.ok) | ||
| throw new Error(`No se pudo completar el login (${res.status})`); | ||
| return res.json(); | ||
| } | ||
| async function refreshTokens(refreshToken) { | ||
| const res = await fetch(`${exports.CAMPUS_ACCOUNT_URL}/v1/auth/refresh`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ refreshToken }), | ||
| }); | ||
| if (!res.ok) | ||
| throw new Error(`No se pudo renovar la sesión de cuenta (${res.status})`); | ||
| return res.json(); | ||
| } | ||
| async function fetchAccount(accessToken) { | ||
| const res = await fetch(`${exports.CAMPUS_ACCOUNT_URL}/v1/account`, { | ||
| headers: { Authorization: `Bearer ${accessToken}` }, | ||
| }); | ||
| if (!res.ok) | ||
| throw new Error(`No se pudo obtener la cuenta (${res.status})`); | ||
| const data = await res.json(); | ||
| return data.account; | ||
| } |
| import type { Command } from 'commander'; | ||
| export declare function accountCommand(program: Command): void; |
| "use strict"; | ||
| var __importDefault = (this && this.__importDefault) || function (mod) { | ||
| return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
| }; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.accountCommand = accountCommand; | ||
| const chalk_1 = __importDefault(require("chalk")); | ||
| const ora_1 = __importDefault(require("ora")); | ||
| const login_js_1 = require("./login.js"); | ||
| const session_js_1 = require("./session.js"); | ||
| const store_js_1 = require("./store.js"); | ||
| const theme_js_1 = require("../ui/theme.js"); | ||
| const login_js_2 = require("../providers/blackboard/commands/login.js"); | ||
| function accountCommand(program) { | ||
| const account = program | ||
| .command('account') | ||
| .description('Cuenta Campus (Google) — identidad compartida entre las apps de Campus, separada de tu sesión de Blackboard'); | ||
| account | ||
| .command('login') | ||
| .description('Inicia sesión con tu cuenta Campus (Google) en el navegador') | ||
| .action(async () => { | ||
| console.log(chalk_1.default.cyan('\nAbriendo el navegador para iniciar sesión con Google...\n')); | ||
| try { | ||
| const session = await (0, login_js_1.loginWithCampusAccount)(); | ||
| (0, store_js_1.saveAccountSession)(session); | ||
| console.log((0, theme_js_1.ok)(`Sesión iniciada como ${chalk_1.default.bold(session.account.name)} (${session.account.email})`)); | ||
| } | ||
| catch (err) { | ||
| console.error((0, theme_js_1.fail)(`No se pudo iniciar sesión: ${err.message}`)); | ||
| process.exit(1); | ||
| } | ||
| // Once the shared Campus identity is set up, continue straight into | ||
| // the (unchanged, fully local) Blackboard SSO flow. | ||
| await (0, login_js_2.runBlackboardLogin)(); | ||
| }); | ||
| account | ||
| .command('whoami') | ||
| .description('Muestra la cuenta Campus activa') | ||
| .action(async () => { | ||
| const stored = (0, store_js_1.loadAccountSession)(); | ||
| if (!stored) { | ||
| console.log((0, theme_js_1.fail)('No hay sesión de cuenta Campus.')); | ||
| console.log(`Ejecuta: ${(0, theme_js_1.hint)('campus account login')}`); | ||
| return; | ||
| } | ||
| const spinner = (0, ora_1.default)('Verificando sesión...').start(); | ||
| const session = await (0, session_js_1.getValidAccountSession)(); | ||
| if (!session) { | ||
| spinner.fail('Sesión expirada o revocada.'); | ||
| console.log(`Ejecuta: ${(0, theme_js_1.hint)('campus account login')}`); | ||
| process.exit(1); | ||
| } | ||
| spinner.stop(); | ||
| console.log((0, theme_js_1.ok)(`${chalk_1.default.bold(session.account.name)} (${session.account.email})`)); | ||
| if (session.account.universityId) | ||
| console.log(chalk_1.default.gray(` Universidad: ${session.account.universityId}`)); | ||
| }); | ||
| account | ||
| .command('logout') | ||
| .description('Cierra la sesión de la cuenta Campus en este equipo') | ||
| .action(() => { | ||
| (0, store_js_1.clearAccountSession)(); | ||
| console.log((0, theme_js_1.ok)('Sesión de cuenta Campus cerrada.')); | ||
| }); | ||
| } |
| import type { CampusAccountSession } from './types.js'; | ||
| export declare function loginWithCampusAccount(): Promise<CampusAccountSession>; |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.loginWithCampusAccount = loginWithCampusAccount; | ||
| const node_http_1 = require("node:http"); | ||
| const node_crypto_1 = require("node:crypto"); | ||
| const node_child_process_1 = require("node:child_process"); | ||
| const client_js_1 = require("./client.js"); | ||
| const CALLBACK_TIMEOUT_MS = 5 * 60_000; | ||
| function openBrowser(url) { | ||
| const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; | ||
| (0, node_child_process_1.execFile)(command, [url], () => { }); | ||
| } | ||
| function callbackPage(input) { | ||
| const accent = input.ok ? '#087f5b' : '#e31837'; | ||
| const iconPath = input.ok | ||
| ? '<path d="M20 6L9 17l-5-5" stroke="#087f5b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' | ||
| : '<path d="M18 6L6 18M6 6l12 12" stroke="#e31837" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'; | ||
| return `<!doctype html> | ||
| <html lang="es"> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| <title>campus-cli</title> | ||
| <style> | ||
| :root { --ink: #102a3a; --muted: #506d7f; --line: #d4e1e6; --canvas: #f4f8f8; --paper: #ffffff; } | ||
| * { box-sizing: border-box; } | ||
| body { | ||
| margin: 0; min-height: 100vh; display: grid; place-items: center; | ||
| background: var(--canvas); color: var(--ink); | ||
| font-family: -apple-system, "Segoe UI", Roboto, sans-serif; | ||
| } | ||
| .card { | ||
| width: min(92vw, 420px); padding: 40px 32px; border: 1px solid var(--line); border-radius: 16px; | ||
| background: var(--paper); box-shadow: 0 24px 54px rgba(16, 42, 58, .08); text-align: center; | ||
| } | ||
| .icon { | ||
| width: 56px; height: 56px; margin: 0 auto 20px; border-radius: 50%; display: grid; place-items: center; | ||
| background: ${input.ok ? '#d9f1e8' : '#fbe3e7'}; | ||
| } | ||
| h1 { font-size: 19px; margin: 0 0 8px; } | ||
| p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.5; } | ||
| .brand { margin-top: 28px; font-size: 12px; color: var(--muted); letter-spacing: .02em; } | ||
| .brand b { color: var(--ink); } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div class="card"> | ||
| <div class="icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none">${iconPath}</svg></div> | ||
| <h1>${input.title}</h1> | ||
| <p>${input.message}</p> | ||
| <div class="brand"><b>campus-cli</b> · puedes cerrar esta pestaña</div> | ||
| </div> | ||
| </body> | ||
| </html>`; | ||
| } | ||
| function pkcePair() { | ||
| const verifier = (0, node_crypto_1.randomBytes)(64).toString('base64url'); | ||
| const challenge = (0, node_crypto_1.createHash)('sha256').update(verifier).digest('base64url'); | ||
| return { verifier, challenge }; | ||
| } | ||
| // Loopback (127.0.0.1) redirect, like `gh auth login`/`gcloud auth login`: a | ||
| // throwaway local HTTP server catches Google's redirect after Campus account | ||
| // consent, so no browser extension or custom URL scheme is needed. | ||
| async function loginWithCampusAccount() { | ||
| const { verifier, challenge } = pkcePair(); | ||
| const state = (0, node_crypto_1.randomBytes)(24).toString('hex'); | ||
| const { code, redirectUri } = await new Promise((resolve, reject) => { | ||
| const server = (0, node_http_1.createServer)((req, res) => { | ||
| const url = new URL(req.url ?? '/', 'http://127.0.0.1'); | ||
| if (url.pathname !== '/callback') { | ||
| res.writeHead(404).end(); | ||
| return; | ||
| } | ||
| const receivedState = url.searchParams.get('state'); | ||
| const error = url.searchParams.get('error'); | ||
| const receivedCode = url.searchParams.get('code'); | ||
| res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); | ||
| res.end(error | ||
| ? callbackPage({ ok: false, title: 'Login cancelado', message: 'No se completó el inicio de sesión. Puedes cerrar esta pestaña y volver a la terminal.' }) | ||
| : callbackPage({ ok: true, title: '¡Listo!', message: 'Tu cuenta Campus quedó conectada. Puedes cerrar esta pestaña y volver a la terminal.' })); | ||
| clearTimeout(timeout); | ||
| server.close(); | ||
| if (error) | ||
| return reject(new Error(`Login cancelado: ${error}`)); | ||
| if (receivedState !== state) | ||
| return reject(new Error('El estado de OAuth no coincide (posible ataque CSRF)')); | ||
| if (!receivedCode) | ||
| return reject(new Error('Google no devolvió un código de autorización')); | ||
| resolve({ code: receivedCode, redirectUri: currentRedirectUri }); | ||
| }); | ||
| const timeout = setTimeout(() => { | ||
| server.close(); | ||
| reject(new Error('Tiempo de espera agotado esperando el login en el navegador')); | ||
| }, CALLBACK_TIMEOUT_MS); | ||
| let currentRedirectUri = ''; | ||
| server.listen(0, '127.0.0.1', () => { | ||
| const address = server.address(); | ||
| const port = typeof address === 'object' && address ? address.port : 0; | ||
| currentRedirectUri = `http://127.0.0.1:${port}/callback`; | ||
| openBrowser((0, client_js_1.buildAuthorizeUrl)({ redirectUri: currentRedirectUri, codeChallenge: challenge, state })); | ||
| }); | ||
| }); | ||
| const { account, accessToken, refreshToken, expiresIn } = await (0, client_js_1.exchangeCode)({ code, verifier, redirectUri }); | ||
| return { | ||
| account, | ||
| accessToken, | ||
| refreshToken, | ||
| accessTokenExpiresAt: Date.now() + expiresIn * 1000, | ||
| }; | ||
| } |
| import type { CampusAccountSession } from './types.js'; | ||
| export declare function getValidAccountSession(): Promise<CampusAccountSession | null>; |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.getValidAccountSession = getValidAccountSession; | ||
| const client_js_1 = require("./client.js"); | ||
| const store_js_1 = require("./store.js"); | ||
| const REFRESH_MARGIN_MS = 60_000; | ||
| // Auto-refreshes the access token when it's close to expiry, so other | ||
| // commands (and future Campus products) can just call this and get a | ||
| // live session without dealing with the OAuth token dance themselves. | ||
| async function getValidAccountSession() { | ||
| const session = (0, store_js_1.loadAccountSession)(); | ||
| if (!session) | ||
| return null; | ||
| if (session.accessTokenExpiresAt - Date.now() > REFRESH_MARGIN_MS) | ||
| return session; | ||
| try { | ||
| const { account, accessToken, refreshToken, expiresIn } = await (0, client_js_1.refreshTokens)(session.refreshToken); | ||
| const refreshed = { | ||
| account, | ||
| accessToken, | ||
| refreshToken, | ||
| accessTokenExpiresAt: Date.now() + expiresIn * 1000, | ||
| }; | ||
| (0, store_js_1.saveAccountSession)(refreshed); | ||
| return refreshed; | ||
| } | ||
| catch { | ||
| return null; // refresh token expired/revoked — caller must prompt `campus account login` | ||
| } | ||
| } |
| import type { CampusAccountSession } from './types.js'; | ||
| export declare function saveAccountSession(session: CampusAccountSession): void; | ||
| export declare function loadAccountSession(): CampusAccountSession | null; | ||
| export declare function clearAccountSession(): void; |
| "use strict"; | ||
| var __importDefault = (this && this.__importDefault) || function (mod) { | ||
| return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
| }; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.saveAccountSession = saveAccountSession; | ||
| exports.loadAccountSession = loadAccountSession; | ||
| exports.clearAccountSession = clearAccountSession; | ||
| const fs_1 = __importDefault(require("fs")); | ||
| const path_1 = __importDefault(require("path")); | ||
| const os_1 = __importDefault(require("os")); | ||
| const ACCOUNT_DIR = path_1.default.join(os_1.default.homedir(), '.blackboard-cli'); | ||
| const ACCOUNT_FILE = path_1.default.join(ACCOUNT_DIR, 'account.json'); | ||
| function saveAccountSession(session) { | ||
| if (!fs_1.default.existsSync(ACCOUNT_DIR)) { | ||
| fs_1.default.mkdirSync(ACCOUNT_DIR, { recursive: true, mode: 0o700 }); | ||
| } | ||
| fs_1.default.writeFileSync(ACCOUNT_FILE, JSON.stringify(session, null, 2), { mode: 0o600 }); | ||
| } | ||
| function loadAccountSession() { | ||
| try { | ||
| if (!fs_1.default.existsSync(ACCOUNT_FILE)) | ||
| return null; | ||
| return JSON.parse(fs_1.default.readFileSync(ACCOUNT_FILE, 'utf-8')); | ||
| } | ||
| catch { | ||
| return null; | ||
| } | ||
| } | ||
| function clearAccountSession() { | ||
| try { | ||
| if (fs_1.default.existsSync(ACCOUNT_FILE)) | ||
| fs_1.default.unlinkSync(ACCOUNT_FILE); | ||
| } | ||
| catch { } | ||
| } |
| export type CampusAccount = { | ||
| id: string; | ||
| name: string; | ||
| email: string; | ||
| picture?: string; | ||
| universityId?: string; | ||
| }; | ||
| export type CampusAccountSession = { | ||
| account: CampusAccount; | ||
| accessToken: string; | ||
| refreshToken: string; | ||
| accessTokenExpiresAt: number; | ||
| }; |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); |
+11
-0
@@ -7,2 +7,13 @@ # Changelog | ||
| ## [1.3.0] — 2026-08-05 | ||
| ### Added | ||
| - `campus account login|whoami|logout` — cuenta Campus (Google), la identidad compartida entre las apps del ecosistema Campus (CLI, y las que vienen: Profe, Trámites...), separada de la sesión de Blackboard de cada universidad. El login es vía navegador con OAuth2+PKCE (redirect a `127.0.0.1`, igual que `gh auth login`/`gcloud auth login`) contra el servicio ya hospedado en `mcp.campuscli.com`. | ||
| - `campus login` ahora exige tener una cuenta Campus activa antes de iniciar el SSO de Blackboard (que sigue siendo 100% local, sin cambios): si no hay cuenta, te pide correr `campus account login` primero. `campus account login`, al terminar, encadena automáticamente el SSO de Blackboard — no hace falta correr los dos comandos por separado. | ||
| ### Notes | ||
| - El SSO de Blackboard no cambió: sigue abriendo Chromium/Chrome/Edge local vía Playwright, sin pasar por ningún servidor propio. Solo la nueva cuenta Campus (Google) usa el backend hospedado. | ||
| --- | ||
| ## [1.2.0] — 2026-08-05 | ||
@@ -9,0 +20,0 @@ |
+3
-0
@@ -49,2 +49,3 @@ #!/usr/bin/env node | ||
| const assignments_js_1 = require("./providers/blackboard/commands/assignments.js"); | ||
| const command_js_1 = require("./account/command.js"); | ||
| const session_js_1 = require("./providers/blackboard/auth/session.js"); | ||
@@ -141,2 +142,4 @@ const client_js_1 = require("./providers/blackboard/api/client.js"); | ||
| (0, assignments_js_1.assignmentsCommand)(program); | ||
| // Cuenta Campus (Google) — identidad compartida entre apps del ecosistema | ||
| (0, command_js_1.accountCommand)(program); | ||
| // API passthrough para LLMs / power users | ||
@@ -143,0 +146,0 @@ program |
| import { Command } from 'commander'; | ||
| export type BlackboardLoginOptions = { | ||
| headless?: boolean; | ||
| username?: string; | ||
| password?: string; | ||
| }; | ||
| export declare function runBlackboardLogin(opts?: BlackboardLoginOptions): Promise<void>; | ||
| export declare function loginCommand(program: Command): void; |
@@ -6,2 +6,3 @@ "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.runBlackboardLogin = runBlackboardLogin; | ||
| exports.loginCommand = loginCommand; | ||
@@ -16,2 +17,42 @@ const inquirer_1 = __importDefault(require("inquirer")); | ||
| const analytics_js_1 = require("../../../analytics.js"); | ||
| const session_js_2 = require("../../../account/session.js"); | ||
| // Shared by `campus login` and `campus account login` (which chains into this | ||
| // right after the Campus account is set up) — same Blackboard SSO flow either way. | ||
| async function runBlackboardLogin(opts = {}) { | ||
| const existing = (0, session_js_1.loadSession)(); | ||
| if ((0, session_js_1.isSessionValid)(existing)) { | ||
| console.log(chalk_1.default.yellow(`Already logged in as ${chalk_1.default.bold(existing.userName || 'unknown')}`)); | ||
| const { relogin } = await inquirer_1.default.prompt([ | ||
| { type: 'confirm', name: 'relogin', message: 'Re-authenticate?', default: false }, | ||
| ]); | ||
| if (!relogin) | ||
| return; | ||
| } | ||
| console.log(chalk_1.default.cyan('\nOpening browser for Microsoft login...')); | ||
| console.log(chalk_1.default.gray('A browser window will open. Complete the login and it will close automatically.\n')); | ||
| (0, analytics_js_1.track)('login_started', { method: 'microsoft_sso' }, existing?.userId); | ||
| try { | ||
| const session = await (0, login_js_1.login)({ | ||
| headless: opts.headless ?? false, | ||
| username: opts.username, | ||
| password: opts.password, | ||
| }); | ||
| (0, analytics_js_1.track)('login_success', { method: 'microsoft_sso' }, session.userId); | ||
| const ssoExpiresAt = (0, login_js_1.getSsoExpiry)(session.cookies); | ||
| const { summary, note } = (0, theme_js_1.formatSessionLifetime)(session.expiresAt, ssoExpiresAt); | ||
| console.log((0, theme_js_1.ok)(`Sesión guardada`)); | ||
| console.log(chalk_1.default.gray(` ${summary}`)); | ||
| console.log(chalk_1.default.gray(` ${note}`)); | ||
| if (session.userName) | ||
| console.log(chalk_1.default.gray(` Usuario: ${session.userName}`)); | ||
| if (session.userId) | ||
| console.log(chalk_1.default.gray(` ID: ${session.userId}`)); | ||
| (0, theme_js_1.whatNext)(); | ||
| } | ||
| catch (err) { | ||
| (0, analytics_js_1.track)('login_failed', { method: 'microsoft_sso', error_type: err?.name ?? 'LoginError' }); | ||
| console.error(chalk_1.default.red(`\n✗ Login failed: ${err.message}`)); | ||
| process.exit(1); | ||
| } | ||
| } | ||
| function loginCommand(program) { | ||
@@ -25,38 +66,11 @@ program | ||
| .action(async (opts) => { | ||
| // Check if already logged in | ||
| const existing = (0, session_js_1.loadSession)(); | ||
| if ((0, session_js_1.isSessionValid)(existing)) { | ||
| console.log(chalk_1.default.yellow(`Already logged in as ${chalk_1.default.bold(existing.userName || 'unknown')}`)); | ||
| const { relogin } = await inquirer_1.default.prompt([ | ||
| { type: 'confirm', name: 'relogin', message: 'Re-authenticate?', default: false }, | ||
| ]); | ||
| if (!relogin) | ||
| return; | ||
| } | ||
| console.log(chalk_1.default.cyan('\nOpening browser for Microsoft login...')); | ||
| console.log(chalk_1.default.gray('A browser window will open. Complete the login and it will close automatically.\n')); | ||
| (0, analytics_js_1.track)('login_started', { method: 'microsoft_sso' }, existing?.userId); | ||
| try { | ||
| const session = await (0, login_js_1.login)({ | ||
| headless: opts.headless ?? false, | ||
| username: opts.username, | ||
| password: opts.password, | ||
| }); | ||
| (0, analytics_js_1.track)('login_success', { method: 'microsoft_sso' }, session.userId); | ||
| const ssoExpiresAt = (0, login_js_1.getSsoExpiry)(session.cookies); | ||
| const { summary, note } = (0, theme_js_1.formatSessionLifetime)(session.expiresAt, ssoExpiresAt); | ||
| console.log((0, theme_js_1.ok)(`Sesión guardada`)); | ||
| console.log(chalk_1.default.gray(` ${summary}`)); | ||
| console.log(chalk_1.default.gray(` ${note}`)); | ||
| if (session.userName) | ||
| console.log(chalk_1.default.gray(` Usuario: ${session.userName}`)); | ||
| if (session.userId) | ||
| console.log(chalk_1.default.gray(` ID: ${session.userId}`)); | ||
| (0, theme_js_1.whatNext)(); | ||
| } | ||
| catch (err) { | ||
| (0, analytics_js_1.track)('login_failed', { method: 'microsoft_sso', error_type: err?.name ?? 'LoginError' }); | ||
| console.error(chalk_1.default.red(`\n✗ Login failed: ${err.message}`)); | ||
| // Blackboard SSO is gated behind the Campus account: it's the shared | ||
| // identity across Campus apps, so it comes first. | ||
| const accountSession = await (0, session_js_2.getValidAccountSession)(); | ||
| if (!accountSession) { | ||
| console.log((0, theme_js_1.fail)('Primero inicia sesión con tu cuenta Campus.')); | ||
| console.log(chalk_1.default.gray(`Ejecuta: ${chalk_1.default.cyan('campus account login')}`)); | ||
| process.exit(1); | ||
| } | ||
| await runBlackboardLogin(opts); | ||
| }); | ||
@@ -63,0 +77,0 @@ program |
+1
-1
| { | ||
| "name": "campus-cli", | ||
| "version": "1.2.0", | ||
| "version": "1.3.0", | ||
| "description": "CLI/MCP no oficial para el campus universitario (Blackboard, Canvas, Moodle...) — acceso desde la terminal y MCP para IA", | ||
@@ -5,0 +5,0 @@ "main": "run.js", |
+17
-7
@@ -47,3 +47,3 @@ # campus-cli | ||
| ```bash | ||
| npx campus-cli login | ||
| npx campus-cli account login | ||
| ``` | ||
@@ -55,3 +55,3 @@ | ||
| npm install -g campus-cli | ||
| campus login | ||
| campus account login | ||
| ``` | ||
@@ -65,3 +65,3 @@ | ||
| npm install | ||
| node run.js login | ||
| node run.js account login | ||
| ``` | ||
@@ -74,7 +74,9 @@ | ||
| ```bash | ||
| campus login | ||
| campus account login | ||
| ``` | ||
| Se abrirá una ventana con el login de Microsoft UPC. Inicia sesión con tu cuenta universitaria y completa MFA si aplica. | ||
| Se abre el navegador para iniciar sesión con tu cuenta Campus (Google) — es la identidad compartida entre las apps del ecosistema Campus, separada de tu sesión de Blackboard. Al terminar, encadena automáticamente el login de Microsoft UPC (Blackboard SSO, 100% local, sin pasar por ningún servidor propio). Si más adelante corres `campus login` por separado, te pedirá primero `campus account login` en caso de no tener una cuenta Campus activa. | ||
| Inicia sesión con tu cuenta universitaria y completa MFA si aplica. | ||
| Durante el login, Microsoft puede mostrar **"Stay signed in?"** con el checkbox **"Don't show this again"**. Marca ese checkbox y haz clic en **Yes** para que la sesión pueda mantenerse correctamente. | ||
@@ -112,6 +114,14 @@ | ||
| ### Sesión | ||
| ### Cuenta Campus | ||
| ```bash | ||
| campus login # iniciar sesión con Microsoft SSO | ||
| campus account login # iniciar sesión con Google (encadena el login de Blackboard) | ||
| campus account whoami # cuenta Campus activa | ||
| campus account logout # cerrar sesión de la cuenta Campus en este equipo | ||
| ``` | ||
| ### Sesión (Blackboard) | ||
| ```bash | ||
| campus login # iniciar sesión con Microsoft SSO (pide cuenta Campus primero) | ||
| campus logout # borrar sesión local | ||
@@ -118,0 +128,0 @@ campus whoami # usuario activo y tiempo restante |
Network access
Supply chain riskThis module accesses the network.
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 3 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 3 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
184582
10.53%52
30%3143
12.9%447
2.29%22
10%8
166.67%