@simonfestl/husky-cli 1.38.4 → 1.38.6

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.
@@ -74,79 +74,49 @@ export function saveConfig(config) {
74
74
  }
75
75
  /**
76
76
  * Fetch role and permissions from /api/auth/whoami
77
- * Uses session token (Bearer) if available, otherwise falls back to API key.
77
+ * Uses session token (Bearer) only.
78
78
  * Caches the result in config for 1 hour.
79
79
  */
80
80
  export async function fetchAndCacheRole() {
81
81
  const config = getConfig();
82
- // Check if there's an active session - if so, use session role directly
83
- // Session roles are already validated by the server, no need to re-fetch
84
- if (config.sessionToken && config.sessionRole && config.sessionExpiresAt) {
85
- const expiresAt = new Date(config.sessionExpiresAt);
86
- if (expiresAt > new Date()) {
87
- // Session is active - fetch permissions for this session role if needed
88
- // Check if we have fresh permissions for this session
89
- const needsPermissionsFetch = !config.roleLastChecked || !config.permissions;
90
- if (needsPermissionsFetch && config.apiUrl && config.apiKey) {
91
- try {
92
- // First try the role-specific permissions endpoint
93
- let url = new URL(`/api/auth/permissions/${encodeURIComponent(config.sessionRole)}`, config.apiUrl);
94
- let res = await fetch(url.toString(), {
95
- headers: { "x-api-key": config.apiKey },
96
- });
97
- // If role-specific endpoint fails, fall back to whoami with session token
98
- if (!res.ok && config.sessionToken) {
99
- url = new URL("/api/auth/whoami", config.apiUrl);
100
- res = await fetch(url.toString(), {
101
- headers: { "Authorization": `Bearer ${config.sessionToken}` },
102
- });
103
- }
104
- if (res.ok) {
105
- const data = await res.json();
106
- // Update permissions cache (keep sessionRole as source of truth for role)
107
- config.permissions = data.permissions;
108
- config.roleLastChecked = new Date().toISOString();
109
- saveConfig(config);
110
- return { role: config.sessionRole, permissions: data.permissions };
111
- }
112
- }
113
- catch {
114
- // Fall through to use cached permissions
115
- }
116
- }
117
- // Return session role with cached permissions
118
- return { role: config.sessionRole, permissions: config.permissions };
119
- }
82
+ // Session token is the only supported auth mode for runtime requests.
83
+ if (!config.sessionToken || !config.sessionRole || !config.sessionExpiresAt) {
84
+ return {};
85
+ }
86
+ if (!isValidRole(config.sessionRole)) {
87
+ return {};
88
+ }
89
+ const expiresAt = new Date(config.sessionExpiresAt);
90
+ if (expiresAt <= new Date()) {
91
+ return {};
120
92
  }
121
- // No active session - use API key auth
122
93
  // Check if we have cached role that's less than 5 minutes old
123
94
  // Short TTL ensures revoked permissions are detected quickly
124
95
  const PERMISSION_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
125
- if (config.role && config.roleLastChecked) {
96
+ if (config.permissions && config.roleLastChecked) {
126
97
  const lastChecked = new Date(config.roleLastChecked);
127
98
  const cacheExpiry = new Date(Date.now() - PERMISSION_CACHE_TTL_MS);
128
99
  if (lastChecked > cacheExpiry) {
129
- return { role: config.role, permissions: config.permissions };
100
+ return { role: config.sessionRole, permissions: config.permissions };
130
101
  }
131
102
  }
132
- // Fetch fresh role/permissions using API key
133
- if (!config.apiUrl || !config.apiKey) {
134
- return {};
103
+ // If API URL is unavailable, return the session role with cached permissions.
104
+ if (!config.apiUrl) {
105
+ return { role: config.sessionRole, permissions: config.permissions };
135
106
  }
136
107
  try {
137
108
  const api = getApiClient();
138
109
  const data = await api.get("/api/auth/whoami");
139
- // Update config cache
140
- config.role = data.role;
110
+ // Keep sessionRole as source of truth; refresh permissions from whoami.
141
111
  config.permissions = data.permissions;
142
112
  config.roleLastChecked = new Date().toISOString();
143
113
  saveConfig(config);
144
- return { role: data.role, permissions: data.permissions };
114
+ return { role: config.sessionRole, permissions: data.permissions };
145
115
  }
146
116
  catch {
147
117
  // Ignore fetch errors, return cached or empty
148
118
  }
149
- return { role: config.role, permissions: config.permissions };
119
+ return { role: config.sessionRole, permissions: config.permissions };
150
120
  }
151
121
  /**
152
122
  * Check if current config has a specific permission
@@ -166,7 +136,7 @@ export function hasPermission(permission) {
166
136
  }
167
137
  /**
168
138
  * Get current role from config.
169
- * Prefers sessionRole (from auth login) over role (from API key) when session is active.
139
+ * Uses sessionRole (from auth login) when session is active.
170
140
  * Validates that the role is a known valid role before returning.
171
141
  */
172
142
  export function getRole() {
@@ -184,10 +154,6 @@ export function getRole() {
184
154
  return undefined;
185
155
  }
186
156
  }
187
- // Fall back to API key role (also validate)
188
- if (config.role && isValidRole(config.role)) {
189
- return config.role;
190
- }
191
157
  return undefined;
192
158
  }
193
159
  /**
@@ -250,15 +216,15 @@ export function isSessionActive() {
250
216
  }
251
217
  /**
252
218
  * Check if the API is properly configured for making requests.
253
- * Returns true if we have either an active session or an API key.
219
+ * Returns true if we have an API URL and an active session.
254
220
  */
255
221
  export function isApiConfigured() {
256
222
  const config = getConfig();
257
- return Boolean(config.apiUrl && (config.apiKey || isSessionActive()));
223
+ return Boolean(config.apiUrl && isSessionActive());
258
224
  }
259
225
  /**
260
226
  * Get authentication headers for API requests.
261
- * Returns Bearer token if session is active, otherwise x-api-key.
227
+ * Returns Bearer token if session is active.
262
228
  * Use this for all API calls to ensure consistent auth.
263
229
  */
264
230
  export function getAuthHeaders() {
@@ -271,56 +237,70 @@ export function getAuthHeaders() {
271
237
  return { Authorization: `Bearer ${config.sessionToken}` };
272
238
  }
273
239
  }
274
- // Fall back to API key
275
- if (config.apiKey) {
276
- return { "x-api-key": config.apiKey };
277
- }
278
240
  return {};
279
241
  }
280
242
  /**
281
243
  * Ensure the session is valid, refreshing if needed.
282
- * Call this before long-running operations (like watch modes).
283
- * Returns true if session is valid (or was refreshed), false if no session/refresh failed.
244
+ * Returns true if session is active, false otherwise.
284
245
  */
285
246
  export async function ensureValidSession() {
286
247
  const config = getConfig();
248
+ // No session at all
287
249
  if (!config.sessionToken || !config.sessionExpiresAt) {
288
- return false; // No session, will use API key
250
+ return false;
289
251
  }
290
252
  const expiresAt = new Date(config.sessionExpiresAt);
291
- const now = new Date();
292
- const fiveMinutes = 5 * 60 * 1000;
293
- // Refresh if expiring within 5 minutes
294
- if (expiresAt.getTime() - now.getTime() < fiveMinutes) {
295
- if (!config.apiUrl || !config.apiKey || !config.sessionAgent) {
296
- return false; // Can't refresh without these
253
+ const msLeft = expiresAt.getTime() - Date.now();
254
+ // Refresh if expiring soon (5 min) to avoid 401 mid-command.
255
+ const REFRESH_THRESHOLD_MS = 5 * 60 * 1000;
256
+ const shouldRefresh = msLeft <= REFRESH_THRESHOLD_MS;
257
+ if (!shouldRefresh) {
258
+ return true;
259
+ }
260
+ if (!config.apiUrl) {
261
+ // Can't refresh without API URL. If the token is already expired, fail.
262
+ if (msLeft <= 0) {
263
+ clearSessionConfig();
264
+ return false;
297
265
  }
298
- try {
299
- const url = new URL("/api/auth/session", config.apiUrl);
300
- const res = await fetch(url.toString(), {
301
- method: "POST",
302
- headers: {
303
- "x-api-key": config.apiKey,
304
- "Content-Type": "application/json",
305
- },
306
- body: JSON.stringify({ agent: config.sessionAgent }),
307
- });
308
- if (res.ok) {
309
- const data = await res.json();
310
- setSessionConfig({
311
- token: data.token,
312
- expiresAt: data.expiresAt,
313
- agent: data.agent,
314
- role: data.role,
315
- });
316
- return true;
266
+ return true;
267
+ }
268
+ try {
269
+ const res = await fetch(new URL("/api/auth/refresh", config.apiUrl).toString(), {
270
+ method: "POST",
271
+ headers: { Authorization: `Bearer ${config.sessionToken}` },
272
+ });
273
+ if (!res.ok) {
274
+ if (msLeft <= 0) {
275
+ clearSessionConfig();
276
+ return false;
317
277
  }
278
+ // Keep the current token (it is still valid but close to expiry).
279
+ return true;
318
280
  }
319
- catch {
320
- // Refresh failed, continue with current token if not expired
281
+ const data = await res.json();
282
+ if (!data.token || !data.expiresAt) {
283
+ if (msLeft <= 0) {
284
+ clearSessionConfig();
285
+ return false;
286
+ }
287
+ return true;
321
288
  }
289
+ setSessionConfig({
290
+ token: data.token,
291
+ expiresAt: data.expiresAt,
292
+ agent: data.agent?.id || config.sessionAgent || "unknown",
293
+ role: data.role || config.sessionRole || "worker",
294
+ });
295
+ return true;
296
+ }
297
+ catch {
298
+ if (msLeft <= 0) {
299
+ clearSessionConfig();
300
+ return false;
301
+ }
302
+ return true;
322
303
  }
323
- return expiresAt > now;
324
304
  }
325
305
  export function getSessionConfig() {
326
306
  const config = getConfig();
@@ -386,6 +366,10 @@ configCommand
386
366
  "wattiz-base-url": "wattizBaseUrl",
387
367
  "wattiz-language": "wattizLanguage",
388
368
  "gtasks-subject": "gtasksSubject",
369
+ "reddit-client-id": "redditClientId",
370
+ "reddit-client-secret": "redditClientSecret",
371
+ "youtube-api-key": "youtubeApiKey",
372
+ "x-bearer-token": "xBearerToken",
389
373
  };
390
374
  const configKey = keyMappings[key];
391
375
  if (!configKey) {
@@ -404,6 +388,7 @@ configCommand
404
388
  console.log(" Emove: emove-username, emove-password, emove-base-url");
405
389
  console.log(" Wattiz: wattiz-username, wattiz-password, wattiz-base-url, wattiz-language");
406
390
  console.log(" GTasks: gtasks-subject");
391
+ console.log(" Research: reddit-client-id, reddit-client-secret, youtube-api-key, x-bearer-token");
407
392
  console.log(" Brain: agent-type");
408
393
  console.error("\nšŸ’” For configuration help: husky explain config");
409
394
  process.exit(1);
@@ -425,7 +410,7 @@ configCommand
425
410
  config[configKey] = value;
426
411
  saveConfig(config);
427
412
  // Mask sensitive values in output
428
- const sensitiveKeys = ["api-key", "billbee-api-key", "billbee-password", "zendesk-api-token", "seatable-api-token", "gotess-token", "gemini-api-key", "nocodb-api-token", "skuterzone-username", "skuterzone-password", "emove-username", "emove-password", "wattiz-username", "wattiz-password"];
413
+ const sensitiveKeys = ["api-key", "billbee-api-key", "billbee-password", "zendesk-api-token", "seatable-api-token", "gotess-token", "gemini-api-key", "nocodb-api-token", "skuterzone-username", "skuterzone-password", "emove-username", "emove-password", "wattiz-username", "wattiz-password", "reddit-client-secret", "youtube-api-key", "x-bearer-token"];
429
414
  const displayValue = sensitiveKeys.includes(key) ? "***" : value;
430
415
  console.log(`āœ“ Set ${key} = ${displayValue}`);
431
416
  });
@@ -466,8 +451,9 @@ configCommand
466
451
  if (!config.apiUrl) {
467
452
  ErrorHelpers.missingApiUrl();
468
453
  }
469
- if (!config.apiKey) {
470
- ErrorHelpers.missingApiKey();
454
+ if (!isSessionActive()) {
455
+ console.error("No active session. Run: husky auth login --agent <name>");
456
+ process.exit(1);
471
457
  }
472
458
  console.log("Testing API connection...");
473
459
  try {
@@ -475,51 +461,41 @@ configCommand
475
461
  // First test basic connectivity with /api/tasks
476
462
  await api.get("/api/tasks");
477
463
  console.log(`API connection successful (API URL: ${config.apiUrl})`);
478
- // Check if there's an active session
479
- const hasActiveSession = isSessionActive();
480
- if (hasActiveSession) {
481
- // Show session info instead of fetching from API
482
- console.log(`\nSession Info:`);
483
- console.log(` Agent: ${config.sessionAgent || "(unknown)"}`);
484
- console.log(` Role: ${config.sessionRole || "(unknown)"}`);
485
- console.log(` Expires: ${config.sessionExpiresAt ? new Date(config.sessionExpiresAt).toLocaleString() : "(unknown)"}`);
486
- console.log(`\n Use 'husky auth permissions' to see full permissions.`);
487
- }
488
- else {
489
- // No session - fetch API key role from whoami
490
- try {
491
- const data = await api.get("/api/auth/whoami");
492
- // Cache the role/permissions (only if no session)
493
- const updatedConfig = getConfig();
494
- updatedConfig.role = data.role;
495
- updatedConfig.permissions = data.permissions;
496
- updatedConfig.roleLastChecked = new Date().toISOString();
497
- saveConfig(updatedConfig);
498
- console.log(`\nRBAC Info (API Key):`);
499
- console.log(` Role: ${data.role || "(not assigned)"}`);
500
- if (data.permissions && data.permissions.length > 0) {
501
- console.log(` Permissions: ${data.permissions.join(", ")}`);
502
- }
503
- if (data.agentId) {
504
- console.log(` Agent ID: ${data.agentId}`);
505
- }
464
+ // Show session info and refresh permission cache from whoami.
465
+ console.log(`\nSession Info:`);
466
+ console.log(` Agent: ${config.sessionAgent || "(unknown)"}`);
467
+ console.log(` Role: ${config.sessionRole || "(unknown)"}`);
468
+ console.log(` Expires: ${config.sessionExpiresAt ? new Date(config.sessionExpiresAt).toLocaleString() : "(unknown)"}`);
469
+ try {
470
+ const data = await api.get("/api/auth/whoami");
471
+ const updatedConfig = getConfig();
472
+ updatedConfig.permissions = data.permissions;
473
+ updatedConfig.roleLastChecked = new Date().toISOString();
474
+ saveConfig(updatedConfig);
475
+ console.log(`\nRBAC Info (Session):`);
476
+ console.log(` Role: ${data.role || config.sessionRole || "(unknown)"}`);
477
+ if (data.permissions && data.permissions.length > 0) {
478
+ console.log(` Permissions: ${data.permissions.join(", ")}`);
506
479
  }
507
- catch {
508
- // whoami failed, but tasks worked - connection is fine
480
+ if (data.agentId) {
481
+ console.log(` Agent ID: ${data.agentId}`);
509
482
  }
510
483
  }
484
+ catch {
485
+ // whoami failed, but tasks worked - connection is fine
486
+ }
511
487
  }
512
488
  catch (error) {
513
489
  const errorMsg = error instanceof Error ? error.message : "Unknown error";
514
490
  if (errorMsg.includes("401")) {
515
491
  console.error(`API connection failed: Unauthorized (HTTP 401)`);
516
- console.error(" Check your API key with: husky config set api-key <key>");
492
+ console.error(" Session is missing or expired. Run: husky auth login --agent <name>");
517
493
  console.error("\n For configuration help: husky explain config");
518
494
  process.exit(1);
519
495
  }
520
496
  else if (errorMsg.includes("403")) {
521
497
  console.error(`API connection failed: Forbidden (HTTP 403)`);
522
- console.error(" Your API key may not have the required permissions");
498
+ console.error(" Your session role may not have the required permissions");
523
499
  console.error("\n For configuration help: husky explain config");
524
500
  process.exit(1);
525
501
  }
@@ -4,7 +4,7 @@
4
4
  * Provides menu-based authentication and API key management.
5
5
  */
6
6
  import { select, input, confirm } from "@inquirer/prompts";
7
- import { getConfig, setConfig, setSessionConfig, clearSessionConfig, getAuthHeaders } from "../config.js";
7
+ import { getConfig, setConfig, setSessionConfig, clearSessionConfig } from "../config.js";
8
8
  import { pressEnterToContinue } from "./utils.js";
9
9
  export async function authMenu() {
10
10
  console.log("\n AUTH");
@@ -55,6 +55,11 @@ async function login() {
55
55
  await pressEnterToContinue();
56
56
  return;
57
57
  }
58
+ if (!config.apiKey) {
59
+ console.error("\n Error: API key not configured. Run 'husky config set api-key <key>' first.\n");
60
+ await pressEnterToContinue();
61
+ return;
62
+ }
58
63
  const agentName = await input({
59
64
  message: "Agent name:",
60
65
  default: "interactive-cli",
@@ -65,7 +70,7 @@ async function login() {
65
70
  method: "POST",
66
71
  headers: {
67
72
  "Content-Type": "application/json",
68
- ...getAuthHeaders(),
73
+ "x-api-key": config.apiKey,
69
74
  },
70
75
  body: JSON.stringify({ agent: agentName }),
71
76
  });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const researchCommand: Command;