alegra-data-agent 0.5.0 → 0.6.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/dist/api.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
- import { readAuth } from "./auth.js";
3
+ import { authRecordFromToken, authRemainingMs, readAuth, writeAuth } from "./auth.js";
4
4
  import { COMMAND_NAME, GATEWAY_URL, getManifestCachePath, getPackageVersion, readLockFile, writeLockFile, } from "./lib.js";
5
5
  import { manifestTimestamp, verifyManifestSignature } from "./signature.js";
6
6
  export class ManifestSignatureError extends Error {
@@ -37,8 +37,55 @@ function sleep(ms) {
37
37
  return new Promise((r) => setTimeout(r, ms));
38
38
  }
39
39
  let rateLimitCooldownUntil = 0;
40
- async function authedFetch(path, opts = {}) {
40
+ const REFRESH_AHEAD_MS = 2 * 24 * 3600_000;
41
+ let refreshTriedFor = null;
42
+ export function shouldRefreshAuth() {
43
+ const remaining = authRemainingMs(readAuth());
44
+ return remaining !== null && remaining < REFRESH_AHEAD_MS;
45
+ }
46
+ export async function refreshAuth() {
41
47
  const auth = readAuth();
48
+ if (!auth)
49
+ return false;
50
+ refreshTriedFor = auth.token;
51
+ try {
52
+ const res = await fetch(`${GATEWAY_URL}/api/cli/refresh`, {
53
+ method: "POST",
54
+ headers: {
55
+ Authorization: `Bearer ${auth.token}`,
56
+ Accept: "application/json",
57
+ "User-Agent": userAgent(),
58
+ "X-Request-Id": newRequestId(),
59
+ },
60
+ signal: AbortSignal.timeout(10_000),
61
+ });
62
+ if (!res.ok)
63
+ return false;
64
+ const body = (await res.json());
65
+ if (typeof body.token !== "string")
66
+ return false;
67
+ const record = authRecordFromToken(body.token);
68
+ if (!record)
69
+ return false;
70
+ writeAuth(record);
71
+ return true;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ export async function ensureFreshAuth() {
78
+ if (readAuth()?.token !== refreshTriedFor && shouldRefreshAuth())
79
+ await refreshAuth();
80
+ const auth = readAuth();
81
+ if (!auth)
82
+ return false;
83
+ const remaining = authRemainingMs(auth);
84
+ return remaining === null || remaining > 60_000;
85
+ }
86
+ async function authedFetch(path, opts = {}) {
87
+ await ensureFreshAuth();
88
+ let auth = readAuth();
42
89
  if (!auth) {
43
90
  if (opts.requireAuth === false) {
44
91
  return new Response(null, { status: 401 });
@@ -84,6 +131,10 @@ async function authedFetch(path, opts = {}) {
84
131
  clearTimeout(timer);
85
132
  }
86
133
  if (res.status === 401) {
134
+ if (auth.token !== refreshTriedFor && (await refreshAuth())) {
135
+ auth = readAuth() ?? auth;
136
+ continue;
137
+ }
87
138
  if (opts.requireAuth === false)
88
139
  return res;
89
140
  throw new AuthRequiredError(`Sesión expirada. Agente: ejecutá vos mismo \`pnpm dlx ${COMMAND_NAME} login\` — al usuario solo le toca el click del navegador.`);
package/dist/auth.js CHANGED
@@ -83,6 +83,18 @@ export function isAuthExpired(record) {
83
83
  return false;
84
84
  return Date.now() >= exp * 1000 - 60_000;
85
85
  }
86
+ export function authRemainingMs(record) {
87
+ if (!record)
88
+ return null;
89
+ const exp = decodeJwtExp(record.token);
90
+ return exp === null ? null : exp * 1000 - Date.now();
91
+ }
92
+ export function authRecordFromToken(token) {
93
+ const email = decodeJwtEmail(token);
94
+ if (!email)
95
+ return null;
96
+ return { email, token, obtainedAt: new Date().toISOString() };
97
+ }
86
98
  export function hasValidAuth() {
87
99
  const record = readAuth();
88
100
  return record !== null && !isAuthExpired(record);
@@ -1,3 +1,4 @@
1
+ import { refreshAuth } from "../api.js";
1
2
  import { isAuthExpired, performBrowserLogin, readAuth, writeAuth } from "../auth.js";
2
3
  import { bold, dim, green, log, logError, logWarn } from "../colors.js";
3
4
  import { GATEWAY_URL, getAuthFilePath } from "../lib.js";
@@ -13,6 +14,13 @@ export async function runLogin(opts = {}) {
13
14
  log("");
14
15
  return 0;
15
16
  }
17
+ if (existing && (await refreshAuth())) {
18
+ const renewed = readAuth();
19
+ log(` ${green("✓")} Sesión renovada como ${bold(renewed?.email ?? existing.email)}.`);
20
+ log(dim(` Token: ${getAuthFilePath()}`));
21
+ log("");
22
+ return 0;
23
+ }
16
24
  if (existing) {
17
25
  logWarn(`Tu sesión (${existing.email}) expiró — reabro el login.`);
18
26
  }
@@ -1,11 +1,11 @@
1
1
  import { appendFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
- import { AuthRequiredError, fetchManifest } from "../api.js";
3
+ import { AuthRequiredError, ensureFreshAuth, fetchManifest } from "../api.js";
4
4
  import { hasValidAuth } from "../auth.js";
5
5
  import { refreshAutoUpdate } from "../autoupdate.js";
6
6
  import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
7
7
  import { detectDrift, installAll, summarize } from "../installer.js";
8
- import { COMMAND_NAME, getAutoUpdateLogPath, getLastCheckPath } from "../lib.js";
8
+ import { COMMAND_NAME, getAutoUpdateLogPath, getLastCheckPath, getPackageVersion, } from "../lib.js";
9
9
  import { banner } from "../ui.js";
10
10
  import { runLogin } from "./login.js";
11
11
  const BACKGROUND_COOLDOWN_MS = 15 * 60_000;
@@ -71,14 +71,18 @@ async function loginFromBackground(reason, force = false) {
71
71
  markAuthFailed();
72
72
  return false;
73
73
  }
74
+ function warnStaleFromBackground() {
75
+ log(yellow(` ⚠ ${COMMAND_NAME} v${getPackageVersion()}: no pude actualizar las skills porque la sesión venció. Agente: corré vos mismo \`pnpm dlx ${COMMAND_NAME} login\` y después \`pnpm dlx ${COMMAND_NAME} update\` — al usuario solo le toca el clic del navegador.`));
76
+ }
74
77
  export async function runUpdate(opts = {}) {
75
78
  if (opts.background && !opts.dryRun && checkedRecently())
76
79
  return 0;
77
- if (opts.background && !hasValidAuth()) {
78
- if (authFailedRecently())
79
- return 0;
80
- if (!(await loginFromBackground("sesión vencida o ausente")))
80
+ const fresh = await ensureFreshAuth();
81
+ if (opts.background && !fresh) {
82
+ if (authFailedRecently() || !(await loginFromBackground("sesión vencida o ausente"))) {
83
+ warnStaleFromBackground();
81
84
  return 0;
85
+ }
82
86
  }
83
87
  banner();
84
88
  if (!hasValidAuth()) {
@@ -94,8 +98,10 @@ export async function runUpdate(opts = {}) {
94
98
  catch (err) {
95
99
  if (err instanceof AuthRequiredError && opts.background && !reauthed) {
96
100
  reauthed = true;
97
- if (!(await loginFromBackground("el gateway rechazó la sesión", true)))
101
+ if (!(await loginFromBackground("el gateway rechazó la sesión", true))) {
102
+ warnStaleFromBackground();
98
103
  return 0;
104
+ }
99
105
  continue;
100
106
  }
101
107
  if (err instanceof AuthRequiredError && !reauthed) {
package/dist/main.js CHANGED
@@ -83,9 +83,13 @@ async function main() {
83
83
  log(helpText());
84
84
  exitCode = 1;
85
85
  }
86
- process.exit(exitCode);
86
+ exitGracefully(exitCode);
87
+ }
88
+ function exitGracefully(code) {
89
+ process.exitCode = code;
90
+ setTimeout(() => process.exit(code), 2000).unref();
87
91
  }
88
92
  main().catch((err) => {
89
93
  logError(err instanceof Error ? err.message : String(err));
90
- process.exit(1);
94
+ exitGracefully(1);
91
95
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alegra-data-agent",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "description": "Agente de Data interno de Alegra. Instala skills + MCPs en tu cliente de IA con un comando.",
6
6
  "license": "UNLICENSED",