@agent-team-foundation/first-tree-hub 0.10.12 → 0.10.14

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.
@@ -563,6 +563,11 @@ const serverConfigSchema = defineConfig({
563
563
  secret: true
564
564
  })
565
565
  },
566
+ auth: {
567
+ accessTokenExpiry: field(z.string().default("30m"), { env: "FIRST_TREE_HUB_AUTH_ACCESS_TOKEN_EXPIRY" }),
568
+ refreshTokenExpiry: field(z.string().default("30d"), { env: "FIRST_TREE_HUB_AUTH_REFRESH_TOKEN_EXPIRY" }),
569
+ connectTokenExpiry: field(z.string().default("10m"), { env: "FIRST_TREE_HUB_AUTH_CONNECT_TOKEN_EXPIRY" })
570
+ },
566
571
  contextTree: optional({
567
572
  repo: field(z.string(), {
568
573
  env: "FIRST_TREE_HUB_CONTEXT_TREE_REPO",
@@ -644,6 +649,7 @@ const serverConfigSchema = defineConfig({
644
649
  //#endregion
645
650
  //#region src/core/bootstrap.ts
646
651
  var bootstrap_exports = /* @__PURE__ */ __exportAll({
652
+ AuthRefreshFailedError: () => AuthRefreshFailedError,
647
653
  ensureFreshAccessToken: () => ensureFreshAccessToken,
648
654
  ensureFreshAdminToken: () => ensureFreshAdminToken,
649
655
  loadCredentials: () => loadCredentials,
@@ -686,6 +692,20 @@ function resolveAccessToken() {
686
692
  return creds.accessToken;
687
693
  }
688
694
  /**
695
+ * Thrown when `/auth/refresh` returns 401 — i.e. the persisted refresh
696
+ * token has expired or been revoked, so no amount of retrying will get
697
+ * us back online without operator action. Callers (the WS reconnect
698
+ * loop in particular) catch this distinctly from generic network/HTTP
699
+ * errors so they can stop the 1Hz reconnect-and-fail thrash and ask
700
+ * systemd/launchd to back off.
701
+ */
702
+ var AuthRefreshFailedError = class extends Error {
703
+ constructor(message) {
704
+ super(message);
705
+ this.name = "AuthRefreshFailedError";
706
+ }
707
+ };
708
+ /**
689
709
  * In-flight refresh promise. Multiple callers (WS handshake, proactive
690
710
  * refresh timer, every SDK request) can see an expired token within the same
691
711
  * millisecond — without dedupe each would fire an independent `/auth/refresh`
@@ -704,6 +724,14 @@ const DEFAULT_MIN_VALIDITY_MS = 3e4;
704
724
  * token has less than that much life left. The WS proactive-refresh path
705
725
  * passes a value that overlaps its lead window so it never receives a
706
726
  * token already inside the "about to expire" zone.
727
+ *
728
+ * Sliding-window note: the server now rotates the refresh token on every
729
+ * successful `/auth/refresh`. We persist the rotated token alongside the
730
+ * new access token so an actively-used client never hits the absolute
731
+ * `refreshTokenExpiry` ceiling. If the response omits `refreshToken`
732
+ * (i.e. an older server) we keep the existing one — the cost is just
733
+ * losing the sliding behaviour against that backend, not a correctness
734
+ * regression.
707
735
  */
708
736
  async function ensureFreshAccessToken(opts) {
709
737
  const minValidityMs = opts?.minValidityMs ?? DEFAULT_MIN_VALIDITY_MS;
@@ -718,11 +746,13 @@ async function ensureFreshAccessToken(opts) {
718
746
  body: JSON.stringify({ refreshToken: creds.refreshToken }),
719
747
  signal: AbortSignal.timeout(1e4)
720
748
  });
721
- if (!res.ok) throw new Error("Access token expired and refresh failed. Run `first-tree-hub client connect <server-url>`.");
749
+ if (res.status === 401) throw new AuthRefreshFailedError("Refresh token rejected by server. Re-run `first-tree-hub connect <token>` (get a fresh token from the Web Computers page → New Connection).");
750
+ if (!res.ok) throw new Error(`Refresh request failed with status ${res.status}.`);
722
751
  const data = await res.json();
723
752
  saveCredentials({
724
753
  ...creds,
725
- accessToken: data.accessToken
754
+ accessToken: data.accessToken,
755
+ refreshToken: data.refreshToken ?? creds.refreshToken
726
756
  });
727
757
  return data.accessToken;
728
758
  })();
@@ -778,4 +808,4 @@ function saveAgentConfig(agentName, agentId, runtime) {
778
808
  return agentDir;
779
809
  }
780
810
  //#endregion
781
- export { serverConfigSchema as C, resolveConfigReadonly as S, loadAgents as _, resolveAccessToken as a, resetConfig as b, saveCredentials as c, DEFAULT_HOME_DIR as d, agentConfigSchema as f, initConfig as g, getConfigValue as h, loadCredentials as i, DEFAULT_CONFIG_DIR as l, collectMissingPrompts as m, ensureFreshAccessToken as n, resolveServerUrl as o, clientConfigSchema as p, ensureFreshAdminToken as r, saveAgentConfig as s, bootstrap_exports as t, DEFAULT_DATA_DIR as u, migrateLegacyHome as v, setConfigValue as w, resetConfigMeta as x, readConfigFile as y };
811
+ export { resolveConfigReadonly as C, resetConfigMeta as S, setConfigValue as T, initConfig as _, loadCredentials as a, readConfigFile as b, saveAgentConfig as c, DEFAULT_DATA_DIR as d, DEFAULT_HOME_DIR as f, getConfigValue as g, collectMissingPrompts as h, ensureFreshAdminToken as i, saveCredentials as l, clientConfigSchema as m, bootstrap_exports as n, resolveAccessToken as o, agentConfigSchema as p, ensureFreshAccessToken as r, resolveServerUrl as s, AuthRefreshFailedError as t, DEFAULT_CONFIG_DIR as u, loadAgents as v, serverConfigSchema as w, resetConfig as x, migrateLegacyHome as y };
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import "../observability-DPyf745N-BSc8QNcR.mjs";
3
- import { $ as findStaleAliases, A as checkDatabase, B as installClientService, C as runHomeMigration, D as checkAgentConfigs, E as runMigrations, F as checkServerReachable, G as stopClientService, I as checkWebSocket, L as printResults, M as checkNodeVersion, N as checkServerConfig, O as checkBackgroundService, P as checkServerHealth, R as reconcileAgentConfigs, S as saveOnboardState, T as migrateLocalAgentDirs, U as restartClientService, V as isServiceSupported, W as startClientService, X as ClientRuntime, Y as stopPostgres, Z as handleClientOrgMismatch, _ as promptMissingFields, _t as probeCapabilities, a as declineUpdate, at as fail, b as onboardCheck, c as detectInstallMode, ct as print, d as startServer, dt as ClientOrgMismatchError, et as formatStaleReason, f as COMMAND_VERSION, ft as ClientUserMismatchError, g as promptAddAgent, gt as cleanWorkspaces, h as isInteractive, ht as SessionRegistry, i as createExecuteUpdate, it as resolveReplyToFromEnv, j as checkDocker, k as checkClientConfig, l as fetchLatestVersion, lt as setJsonMode, m as uploadClientCapabilities, mt as SdkError, nt as createOwner, o as promptUpdate, ot as success, p as reconcileLocalRuntimeProviders, pt as FirstTreeHubSDK, r as registerSaaSConnectCommand, s as PACKAGE_NAME, tt as removeLocalAgent, u as installGlobalLatest, v as formatCheckReport, vt as applyClientLoggerConfig, w as createApiNameResolver, x as onboardCreate, y as loadOnboardState, yt as configureClientLoggerForService, z as getClientServiceStatus } from "../saas-connect-DLVGb8OH.mjs";
3
+ import { $ as findStaleAliases, A as checkDatabase, B as installClientService, C as runHomeMigration, D as checkAgentConfigs, E as runMigrations, F as checkServerReachable, G as stopClientService, I as checkWebSocket, L as printResults, M as checkNodeVersion, N as checkServerConfig, O as checkBackgroundService, P as checkServerHealth, R as reconcileAgentConfigs, S as saveOnboardState, T as migrateLocalAgentDirs, U as restartClientService, V as isServiceSupported, W as startClientService, X as ClientRuntime, Y as stopPostgres, Z as handleClientOrgMismatch, _ as promptMissingFields, _t as probeCapabilities, a as declineUpdate, at as fail, b as onboardCheck, c as detectInstallMode, ct as print, d as startServer, dt as ClientOrgMismatchError, et as formatStaleReason, f as COMMAND_VERSION, ft as ClientUserMismatchError, g as promptAddAgent, gt as cleanWorkspaces, h as isInteractive, ht as SessionRegistry, i as createExecuteUpdate, it as resolveReplyToFromEnv, j as checkDocker, k as checkClientConfig, l as fetchLatestVersion, lt as setJsonMode, m as uploadClientCapabilities, mt as SdkError, nt as createOwner, o as promptUpdate, ot as success, p as reconcileLocalRuntimeProviders, pt as FirstTreeHubSDK, r as registerSaaSConnectCommand, s as PACKAGE_NAME, tt as removeLocalAgent, u as installGlobalLatest, v as formatCheckReport, vt as applyClientLoggerConfig, w as createApiNameResolver, x as onboardCreate, y as loadOnboardState, yt as configureClientLoggerForService, z as getClientServiceStatus } from "../saas-connect-DWcxHtjX.mjs";
4
4
  import "../logger-core-BTmvdflj-DjW8FM4T.mjs";
5
- import { C as serverConfigSchema, S as resolveConfigReadonly, _ as loadAgents, b as resetConfig, c as saveCredentials, d as DEFAULT_HOME_DIR, f as agentConfigSchema, g as initConfig, h as getConfigValue, i as loadCredentials, l as DEFAULT_CONFIG_DIR, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, r as ensureFreshAdminToken, s as saveAgentConfig, u as DEFAULT_DATA_DIR, w as setConfigValue, x as resetConfigMeta, y as readConfigFile } from "../bootstrap-CDeXqhkQ.mjs";
6
- import "../dist-DwbhZyGi.mjs";
7
- import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-viiZmwcn.mjs";
5
+ import { C as resolveConfigReadonly, S as resetConfigMeta, T as setConfigValue, _ as initConfig, a as loadCredentials, b as readConfigFile, c as saveAgentConfig, d as DEFAULT_DATA_DIR, f as DEFAULT_HOME_DIR, g as getConfigValue, i as ensureFreshAdminToken, l as saveCredentials, m as clientConfigSchema, p as agentConfigSchema, r as ensureFreshAccessToken, s as resolveServerUrl, u as DEFAULT_CONFIG_DIR, v as loadAgents, w as serverConfigSchema, x as resetConfig } from "../bootstrap-B2x4TTyJ.mjs";
6
+ import "../dist-D6AOiyNg.mjs";
7
+ import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-DQ1l18Ah.mjs";
8
8
  import "../invitation-B1pjAyOz-BaCA9PII.mjs";
9
9
  import { join } from "node:path";
10
10
  import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
@@ -1308,6 +1308,24 @@ function registerClientCommands(program) {
1308
1308
  print.line(` Hub: (could not read ${clientYaml}: ${msg.slice(0, 60)})\n`);
1309
1309
  }
1310
1310
  else print.line(" Hub: (not configured — run `first-tree-hub client connect <url>`)\n");
1311
+ const creds = loadCredentials();
1312
+ if (creds) {
1313
+ const exp = decodeJwtExpSeconds(creds.refreshToken);
1314
+ if (exp == null) print.line(" Auth: ⚠ could not parse refresh token (corrupt credentials)\n");
1315
+ else {
1316
+ const remainingSec = exp - Math.floor(Date.now() / 1e3);
1317
+ if (remainingSec <= 0) {
1318
+ print.line(" Auth: ✗ refresh token EXPIRED — re-run `first-tree-hub connect <token>`\n");
1319
+ print.line(" (get a fresh token from your Hub's Web admin → Computers → New Connection)\n");
1320
+ } else if (remainingSec < 2 * 86400) {
1321
+ const hours = Math.floor(remainingSec / 3600);
1322
+ print.line(` Auth: ⚠ refresh token expires in ~${hours}h — reclaim soon to stay online\n`);
1323
+ } else {
1324
+ const days = Math.floor(remainingSec / 86400);
1325
+ print.line(` Auth: ✓ refresh token valid for ~${days}d\n`);
1326
+ }
1327
+ }
1328
+ } else print.line(" Auth: (no credentials — run `first-tree-hub client connect <url>`)\n");
1311
1329
  const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
1312
1330
  try {
1313
1331
  const agents = loadAgents({
@@ -1474,6 +1492,25 @@ function getNested(obj, path) {
1474
1492
  }
1475
1493
  return typeof cur === "string" ? cur : null;
1476
1494
  }
1495
+ /**
1496
+ * Pull the `exp` claim (in seconds since epoch) out of a JWT without
1497
+ * verifying the signature — the `client status` auth-health line just
1498
+ * needs the wall-clock countdown, not a trust decision. Returns null
1499
+ * for malformed tokens so the caller can render a friendly fallback
1500
+ * instead of crashing.
1501
+ */
1502
+ function decodeJwtExpSeconds(token) {
1503
+ const parts = token.split(".");
1504
+ if (parts.length < 2 || !parts[1]) return null;
1505
+ try {
1506
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
1507
+ const pad = b64.length % 4 === 0 ? "" : "=".repeat(4 - b64.length % 4);
1508
+ const payload = JSON.parse(Buffer.from(b64 + pad, "base64").toString("utf-8"));
1509
+ return typeof payload.exp === "number" ? payload.exp : null;
1510
+ } catch {
1511
+ return null;
1512
+ }
1513
+ }
1477
1514
  //#endregion
1478
1515
  //#region src/commands/config.ts
1479
1516
  function resolveConfigPath(flags) {
@@ -1571,13 +1608,13 @@ function isSecretField(schema, dotPath) {
1571
1608
  //#region src/commands/onboard.ts
1572
1609
  async function promptMissing(args) {
1573
1610
  if (!args.server) try {
1574
- const { resolveServerUrl } = await import("../bootstrap-CDeXqhkQ.mjs").then((n) => n.t);
1611
+ const { resolveServerUrl } = await import("../bootstrap-B2x4TTyJ.mjs").then((n) => n.n);
1575
1612
  resolveServerUrl();
1576
1613
  } catch {
1577
1614
  args.server = await input({ message: "Hub server URL:" });
1578
1615
  saveOnboardState(args);
1579
1616
  }
1580
- const { loadCredentials } = await import("../bootstrap-CDeXqhkQ.mjs").then((n) => n.t);
1617
+ const { loadCredentials } = await import("../bootstrap-B2x4TTyJ.mjs").then((n) => n.n);
1581
1618
  if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
1582
1619
  if (!args.id) {
1583
1620
  args.id = await input({ message: "Agent ID:" });
@@ -585,10 +585,25 @@ const addParticipantSchema = z.object({
585
585
  });
586
586
  z.object({ agentId: z.string().min(1) });
587
587
  const clientStatusSchema = z.enum(["connected", "disconnected"]);
588
+ /**
589
+ * Auth health channel surfaced to the Web admin dashboard. Computed
590
+ * server-side per request from the row's offline duration vs the
591
+ * configured refresh-token TTL — there is no DB column. See
592
+ * `deriveAuthState` server-side.
593
+ *
594
+ * - `ok` — online, or recently offline (cached refresh token can
595
+ * plausibly still mint access tokens).
596
+ * - `expired` — offline longer than the refresh-token TTL; the client
597
+ * cannot recover on its own. The operator mints a fresh
598
+ * connect token via the Web "+ New Connection" button
599
+ * (or the inline Reconnect button on the row).
600
+ */
601
+ const clientAuthStateSchema = z.enum(["ok", "expired"]);
588
602
  z.object({
589
603
  id: z.string(),
590
604
  userId: z.string().nullable(),
591
605
  status: clientStatusSchema,
606
+ authState: clientAuthStateSchema,
592
607
  sdkVersion: z.string().max(50).nullable(),
593
608
  hostname: z.string().max(100).nullable(),
594
609
  os: z.string().max(50).nullable(),
@@ -1,5 +1,5 @@
1
1
  import { d as __exportAll } from "./esm-CYu4tXXn.mjs";
2
- import { r as AGENT_SELECTOR_HEADER } from "./dist-DwbhZyGi.mjs";
2
+ import { r as AGENT_SELECTOR_HEADER } from "./dist-D6AOiyNg.mjs";
3
3
  //#region src/core/feishu.ts
4
4
  var feishu_exports = /* @__PURE__ */ __exportAll({
5
5
  bindFeishuBot: () => bindFeishuBot,
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import "./observability-DPyf745N-BSc8QNcR.mjs";
2
- import { A as checkDatabase, B as installClientService, C as runHomeMigration, D as checkAgentConfigs, E as runMigrations, F as checkServerReachable, G as stopClientService, H as resolveCliInvocation, I as checkWebSocket, J as isDockerAvailable, K as uninstallClientService, L as printResults, M as checkNodeVersion, N as checkServerConfig, P as checkServerHealth, Q as rotateClientIdWithBackup, U as restartClientService, V as isServiceSupported, W as startClientService, X as ClientRuntime, Y as stopPostgres, Z as handleClientOrgMismatch, _ as promptMissingFields, b as onboardCheck, d as startServer, g as promptAddAgent, h as isInteractive, j as checkDocker, k as checkClientConfig, mt as SdkError, n as deriveHubUrlFromToken, nt as createOwner, pt as FirstTreeHubSDK, q as ensurePostgres, rt as hasUser, st as blank, t as HubUrlDerivationError, ut as status, v as formatCheckReport, x as onboardCreate, z as getClientServiceStatus } from "./saas-connect-DLVGb8OH.mjs";
2
+ import { A as checkDatabase, B as installClientService, C as runHomeMigration, D as checkAgentConfigs, E as runMigrations, F as checkServerReachable, G as stopClientService, H as resolveCliInvocation, I as checkWebSocket, J as isDockerAvailable, K as uninstallClientService, L as printResults, M as checkNodeVersion, N as checkServerConfig, P as checkServerHealth, Q as rotateClientIdWithBackup, U as restartClientService, V as isServiceSupported, W as startClientService, X as ClientRuntime, Y as stopPostgres, Z as handleClientOrgMismatch, _ as promptMissingFields, b as onboardCheck, d as startServer, g as promptAddAgent, h as isInteractive, j as checkDocker, k as checkClientConfig, mt as SdkError, n as deriveHubUrlFromToken, nt as createOwner, pt as FirstTreeHubSDK, q as ensurePostgres, rt as hasUser, st as blank, t as HubUrlDerivationError, ut as status, v as formatCheckReport, x as onboardCreate, z as getClientServiceStatus } from "./saas-connect-DWcxHtjX.mjs";
3
3
  import "./logger-core-BTmvdflj-DjW8FM4T.mjs";
4
- import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-CDeXqhkQ.mjs";
5
- import "./dist-DwbhZyGi.mjs";
6
- import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-viiZmwcn.mjs";
4
+ import { i as ensureFreshAdminToken, o as resolveAccessToken, r as ensureFreshAccessToken, s as resolveServerUrl, t as AuthRefreshFailedError } from "./bootstrap-B2x4TTyJ.mjs";
5
+ import "./dist-D6AOiyNg.mjs";
6
+ import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-DQ1l18Ah.mjs";
7
7
  import "./invitation-B1pjAyOz-BaCA9PII.mjs";
8
- export { ClientRuntime, FirstTreeHubSDK, HubUrlDerivationError, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, deriveHubUrlFromToken, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, handleClientOrgMismatch, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, restartClientService, rotateClientIdWithBackup, runHomeMigration, runMigrations, startClientService, startServer, status, stopClientService, stopPostgres, uninstallClientService };
8
+ export { AuthRefreshFailedError, ClientRuntime, FirstTreeHubSDK, HubUrlDerivationError, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, deriveHubUrlFromToken, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, handleClientOrgMismatch, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, restartClientService, rotateClientIdWithBackup, runHomeMigration, runMigrations, startClientService, startServer, status, stopClientService, stopPostgres, uninstallClientService };
@@ -1,3 +1,3 @@
1
- import "./dist-DwbhZyGi.mjs";
1
+ import "./dist-D6AOiyNg.mjs";
2
2
  import { g as previewInvitation, v as rotateInvitation } from "./invitation-B1pjAyOz-BaCA9PII.mjs";
3
3
  export { previewInvitation, rotateInvitation };