@voxli/cli 0.6.0 → 0.6.2

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.
@@ -1,6 +1,7 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import { stdin, stdout } from "node:process";
3
- import { writeConfig } from "../lib/config.js";
3
+ import { join } from "node:path";
4
+ import { findLocalConfigDir, writeConfig } from "../lib/config.js";
4
5
  import { register, ApiError } from "../lib/api.js";
5
6
  import { getStableHostname } from "../lib/hostname.js";
6
7
  import { browserAuth } from "../lib/browser-auth.js";
@@ -37,12 +38,30 @@ async function validateAndSave(token, extra, opts) {
37
38
  // Network error or other — warn but still save
38
39
  console.warn("Warning: could not validate token (network error). Saving anyway.");
39
40
  }
40
- const target = opts?.local ? "local" : "global";
41
+ // Target selection:
42
+ // - --local: force cwd/.voxli (creates if missing).
43
+ // - default: reuse an existing local config in the cwd ancestor chain so
44
+ // the listener (which reads local first) picks up the new credentials.
45
+ // Falls back to the global config if no local one exists.
46
+ let writeOpts;
47
+ if (opts?.local) {
48
+ writeOpts = { configDir: join(process.cwd(), ".voxli") };
49
+ }
50
+ else {
51
+ const existingLocal = await findLocalConfigDir();
52
+ if (existingLocal) {
53
+ console.log(`Detected local config at ${existingLocal}; saving there.`);
54
+ writeOpts = { configDir: existingLocal };
55
+ }
56
+ else {
57
+ writeOpts = { target: "global" };
58
+ }
59
+ }
41
60
  const savedPath = await writeConfig({
42
61
  accessToken: token,
43
62
  refreshToken: extra?.refreshToken,
44
63
  clientId: extra?.clientId,
45
- }, { target });
64
+ }, writeOpts);
46
65
  console.log(`${label} saved to ${savedPath}`);
47
66
  }
48
67
  export async function authCommand(opts) {
@@ -3,7 +3,9 @@ import { spawn } from "node:child_process";
3
3
  import { resolveApiKey, resolveConfig, attemptTokenRefresh, } from "../lib/config.js";
4
4
  import { buildAgentIdentifier, getStableHostname } from "../lib/hostname.js";
5
5
  import { register, ApiError } from "../lib/api.js";
6
+ import { getJwtExpiry } from "../lib/oauth.js";
6
7
  const POLL_INTERVAL = 5_000;
8
+ const REFRESH_BUFFER_SECONDS = 30 * 60;
7
9
  export async function listenCommand(options) {
8
10
  const isEnvToken = !!resolveApiKey();
9
11
  let apiKey = null;
@@ -40,6 +42,19 @@ export async function listenCommand(options) {
40
42
  console.log(`Listening as ${displayName} (${uniqueIdentifier}) using credentials from ${credentialSource}`);
41
43
  while (true) {
42
44
  try {
45
+ // Proactively refresh if the token expires within the buffer window so
46
+ // newly-spawned subprocesses inherit a token that will outlast the test.
47
+ if (!isEnvToken) {
48
+ const exp = getJwtExpiry(apiKey);
49
+ const nowSec = Math.floor(Date.now() / 1000);
50
+ if (exp !== null && exp - nowSec < REFRESH_BUFFER_SECONDS) {
51
+ const newToken = await attemptTokenRefresh(apiKey);
52
+ if (newToken && newToken !== apiKey) {
53
+ apiKey = newToken;
54
+ console.log("Access token refreshed proactively.");
55
+ }
56
+ }
57
+ }
43
58
  const data = await register(apiKey, {
44
59
  name: displayName,
45
60
  unique_identifier: uniqueIdentifier,
@@ -13,6 +13,11 @@ export declare function refreshAccessToken(baseUrl: string, refreshToken: string
13
13
  accessToken: string;
14
14
  refreshToken?: string;
15
15
  }>;
16
+ /**
17
+ * Read the `exp` claim (seconds since epoch) from a JWT access token.
18
+ * Returns null if the token isn't a parseable JWT or has no `exp`.
19
+ */
20
+ export declare function getJwtExpiry(token: string): number | null;
16
21
  export declare function buildAuthorizeUrl(baseUrl: string, params: {
17
22
  clientId: string;
18
23
  redirectUri: string;
package/dist/lib/oauth.js CHANGED
@@ -66,6 +66,22 @@ export async function refreshAccessToken(baseUrl, refreshToken, clientId) {
66
66
  refreshToken: data.refresh_token,
67
67
  };
68
68
  }
69
+ /**
70
+ * Read the `exp` claim (seconds since epoch) from a JWT access token.
71
+ * Returns null if the token isn't a parseable JWT or has no `exp`.
72
+ */
73
+ export function getJwtExpiry(token) {
74
+ const parts = token.split(".");
75
+ if (parts.length !== 3)
76
+ return null;
77
+ try {
78
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
79
+ return typeof payload.exp === "number" ? payload.exp : null;
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
69
85
  export function buildAuthorizeUrl(baseUrl, params) {
70
86
  const url = new URL("/oauth/authorize", baseUrl);
71
87
  url.searchParams.set("response_type", "code");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voxli/cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "CLI agent for running Voxli test scenarios locally",
5
5
  "type": "module",
6
6
  "bin": {