@pensar/apex 0.0.93 → 0.0.96-canary.11229b88

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/bin/pensar.js CHANGED
@@ -98,7 +98,9 @@ if (command === "benchmark") {
98
98
  console.log(
99
99
  " pensar swarm Run parallel pentests on multiple targets"
100
100
  );
101
- console.log(" pensar auth Authenticate to a target application");
101
+ console.log(
102
+ " pensar auth Connect to Pensar Console for managed inference"
103
+ );
102
104
  console.log();
103
105
  console.log("Options:");
104
106
  console.log(" -h, --help Show this help message");
@@ -172,24 +174,12 @@ if (command === "benchmark") {
172
174
  );
173
175
  console.log();
174
176
  console.log("Auth Usage:");
175
- console.log(" pensar auth --target <url> [options]");
176
- console.log();
177
- console.log("Auth Options:");
178
- console.log(
179
- " --target <url> Target URL to authenticate against (required)"
180
- );
181
- console.log(" --username <user> Username for login");
182
- console.log(" --password <pass> Password for login");
183
- console.log(" --api-key <key> API key for authentication");
184
- console.log(" --bearer <token> Bearer token to verify");
185
- console.log(" --cookies <string> Existing cookies to verify");
186
- console.log(
187
- " --model <model> AI model to use (default: claude-sonnet-4-5)"
188
- );
189
- console.log(" --no-browser Disable browser tools");
190
177
  console.log(
191
- " --discover-only Only discover auth requirements, don't authenticate"
178
+ " pensar auth Login to Pensar Console (or show status if connected)"
192
179
  );
180
+ console.log(" pensar auth login Login to Pensar Console");
181
+ console.log(" pensar auth logout Disconnect from Pensar Console");
182
+ console.log(" pensar auth status Show connection status");
193
183
  console.log();
194
184
  console.log("Header Modes (for quicktest, pentest, swarm):");
195
185
  console.log(" none No custom headers added to requests");
@@ -217,15 +207,9 @@ if (command === "benchmark") {
217
207
  );
218
208
  console.log(" pensar swarm targets.json");
219
209
  console.log(" pensar swarm targets.json --headers none");
220
- console.log(
221
- " pensar auth --target http://localhost:3000 --discover-only"
222
- );
223
- console.log(
224
- " pensar auth --target http://localhost:3000 --username admin --password admin123"
225
- );
226
- console.log(
227
- ' pensar auth --target http://localhost:3000 --bearer "eyJ..."'
228
- );
210
+ console.log(" pensar auth");
211
+ console.log(" pensar auth status");
212
+ console.log(" pensar auth logout");
229
213
  } else if (args.length === 0) {
230
214
  // No command specified, run the TUI
231
215
  const appPath = join(__dirname, "..", "build", "index.js");
package/build/auth.js ADDED
@@ -0,0 +1,603 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/core/config/config.ts
5
+ import os from "os";
6
+ import path from "path";
7
+ import fs from "fs/promises";
8
+ // package.json
9
+ var package_default = {
10
+ name: "@pensar/apex",
11
+ version: "0.0.96-canary.11229b88",
12
+ description: "AI-powered penetration testing CLI tool with terminal UI",
13
+ module: "src/tui/index.tsx",
14
+ main: "build/index.js",
15
+ type: "module",
16
+ repository: {
17
+ type: "git",
18
+ url: "https://github.com/pensarai/apex.git"
19
+ },
20
+ bin: {
21
+ pensar: "./bin/pensar.js"
22
+ },
23
+ files: [
24
+ "build",
25
+ "bin",
26
+ "src/core/installation",
27
+ "pensar.svg",
28
+ "LICENSE"
29
+ ],
30
+ scripts: {
31
+ build: "bun build src/tui/index.tsx --outdir build --target node --format esm --external sharp && bun build src/cli/auth.ts --outdir build --target node --format esm",
32
+ "generate:ascii": "bun run scripts/generate-ascii-art.ts",
33
+ "generate:models": "bun run scripts/generate-models.ts",
34
+ "build:binary": "bun run generate:ascii && bun build src/cli.ts --compile --outfile pensar",
35
+ "build:binary:macos-arm64": "bun build src/cli.ts --compile --target=bun-darwin-arm64 --outfile dist/pensar-darwin-arm64",
36
+ "build:binary:macos-x64": "bun build src/cli.ts --compile --target=bun-darwin-x64 --outfile dist/pensar-darwin-x64",
37
+ "build:binary:linux-x64": "bun build src/cli.ts --compile --target=bun-linux-x64 --outfile dist/pensar-linux-x64",
38
+ "build:binary:linux-arm64": "bun build src/cli.ts --compile --target=bun-linux-arm64 --outfile dist/pensar-linux-arm64",
39
+ "build:binaries": "bun run generate:ascii && mkdir -p dist && bun run build:binary:macos-arm64 && bun run build:binary:macos-x64 && bun run build:binary:linux-x64 && bun run build:binary:linux-arm64",
40
+ dev: "bun run scripts/watch.ts",
41
+ "dev:debug": "SHOW_CONSOLE=true bun run scripts/watch.ts",
42
+ start: "bun run src/tui/index.tsx",
43
+ pensar: "node bin/pensar.js",
44
+ tsc: "tsc --noEmit",
45
+ "daytona-benchmark": "bun run scripts/daytona-benchmark.ts",
46
+ "local-benchmark": "bun run scripts/local-benchmark.ts",
47
+ test: "vitest run",
48
+ "test:watch": "vitest",
49
+ lint: "eslint src/",
50
+ format: "prettier --write .",
51
+ "format:check": "prettier --check .",
52
+ prepublishOnly: "npm run build"
53
+ },
54
+ keywords: [
55
+ "penetration-testing",
56
+ "security",
57
+ "pentesting",
58
+ "ai",
59
+ "cli",
60
+ "terminal",
61
+ "tui"
62
+ ],
63
+ author: "Pensar",
64
+ license: "MIT",
65
+ engines: {
66
+ node: ">=18.0.0",
67
+ bun: ">=1.0.0"
68
+ },
69
+ devDependencies: {
70
+ "@eslint/js": "^10.0.1",
71
+ "@playwright/mcp": "^0.0.54",
72
+ "@types/bun": "^1.3.0",
73
+ "@types/mailparser": "^3.4.6",
74
+ "@types/react": "^19.2.6",
75
+ "@typescript-eslint/eslint-plugin": "^8.55.0",
76
+ "@typescript-eslint/parser": "^8.55.0",
77
+ dotenv: "^17.2.3",
78
+ eslint: "^10.0.0",
79
+ "eslint-config-prettier": "^10.1.8",
80
+ "eslint-plugin-unused-imports": "^4.4.1",
81
+ prettier: "^3.8.1",
82
+ "typescript-eslint": "^8.55.0",
83
+ vitest: "^2.1.8"
84
+ },
85
+ peerDependencies: {
86
+ typescript: "^5.9.3"
87
+ },
88
+ dependencies: {
89
+ "@ai-sdk/amazon-bedrock": "^4.0.69",
90
+ "@ai-sdk/anthropic": "^3.0.50",
91
+ "@ai-sdk/google": "^3.0.37",
92
+ "@ai-sdk/openai": "^3.0.37",
93
+ "@ai-sdk/openai-compatible": "^2.0.35",
94
+ "@daytonaio/sdk": "^0.112.1",
95
+ "@googleapis/gmail": "^16.1.1",
96
+ "@microsoft/microsoft-graph-client": "^3.0.7",
97
+ "@modelcontextprotocol/sdk": "^1.0.0",
98
+ "@openrouter/ai-sdk-provider": "^2.2.3",
99
+ "@opentui/core": "^0.1.80",
100
+ "@opentui/react": "^0.1.80",
101
+ ai: "^6.0.105",
102
+ glob: "^13.0.0",
103
+ "google-auth-library": "^10.6.1",
104
+ "highlight.js": "^11.11.1",
105
+ ignore: "^7.0.5",
106
+ imapflow: "^1.2.10",
107
+ mailparser: "^3.9.3",
108
+ marked: "^16.4.0",
109
+ nanoid: "^5.1.6",
110
+ "p-limit": "^7.2.0",
111
+ react: "^19.2.0",
112
+ sharp: "^0.34.4",
113
+ yaml: "^2.8.2",
114
+ zod: "^3.25.76"
115
+ },
116
+ packageManager: "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
117
+ };
118
+
119
+ // src/core/installation/index.ts
120
+ function getCurrentVersion() {
121
+ return package_default.version;
122
+ }
123
+
124
+ // src/core/config/config.ts
125
+ var DEFAULT_CONFIG = {
126
+ responsibleUseAccepted: false
127
+ };
128
+ async function init() {
129
+ const folder = path.join(os.homedir(), ".pensar");
130
+ const file = path.join(folder, "config.json");
131
+ const dirExists = await fs.access(folder).then(() => true).catch(() => false);
132
+ if (!dirExists) {
133
+ await fs.mkdir(folder, { recursive: true });
134
+ }
135
+ const fileExists = await fs.access(file).then(() => true).catch(() => false);
136
+ if (!fileExists) {
137
+ await fs.writeFile(file, JSON.stringify(DEFAULT_CONFIG));
138
+ }
139
+ const version = getCurrentVersion();
140
+ return { ...DEFAULT_CONFIG, version };
141
+ }
142
+ async function get() {
143
+ const folder = path.join(os.homedir(), ".pensar");
144
+ const file = path.join(folder, "config.json");
145
+ const exists = await fs.access(file).then(() => true).catch(() => false);
146
+ if (!exists) {
147
+ return await init();
148
+ }
149
+ const config = await fs.readFile(file, "utf8");
150
+ const parsedConfig = JSON.parse(config);
151
+ const version = getCurrentVersion();
152
+ return {
153
+ ...parsedConfig,
154
+ version,
155
+ openAiAPIKey: process.env.OPENAI_API_KEY ?? parsedConfig.openAiAPIKey,
156
+ anthropicAPIKey: process.env.ANTHROPIC_API_KEY ?? parsedConfig.anthropicAPIKey,
157
+ googleAPIKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY ?? parsedConfig.googleAPIKey,
158
+ openRouterAPIKey: process.env.OPENROUTER_API_KEY ?? parsedConfig.openRouterAPIKey,
159
+ inceptionAPIKey: process.env.INCEPTION_API_KEY ?? parsedConfig.inceptionAPIKey,
160
+ bedrockAPIKey: process.env.BEDROCK_API_KEY ?? parsedConfig.bedrockAPIKey,
161
+ pensarAPIKey: process.env.PENSAR_API_KEY ?? parsedConfig.pensarAPIKey,
162
+ daytonaAPIKey: process.env.DAYTONA_API_KEY ?? parsedConfig.daytonaAPIKey,
163
+ daytonaOrgId: process.env.DAYTONA_ORG_ID ?? parsedConfig.daytonaOrgId,
164
+ runloopAPIKey: process.env.RUNLOOP_API_KEY ?? parsedConfig.runloopAPIKey
165
+ };
166
+ }
167
+ async function update(config) {
168
+ const currentConfig = await get();
169
+ const newConfig = { ...currentConfig, ...config };
170
+ const folder = path.join(os.homedir(), ".pensar");
171
+ const file = path.join(folder, "config.json");
172
+ await fs.writeFile(file, JSON.stringify(newConfig));
173
+ }
174
+
175
+ // src/core/config/index.ts
176
+ var config = {
177
+ get,
178
+ init,
179
+ update
180
+ };
181
+
182
+ // src/core/api/constants.ts
183
+ var PENSAR_API_BASE_URL = "https://api.pensar.dev";
184
+ var PENSAR_CONSOLE_BASE_URL = "https://console.pensar.dev";
185
+ function getPensarApiUrl() {
186
+ return PENSAR_API_BASE_URL;
187
+ }
188
+ function getPensarConsoleUrl() {
189
+ return process.env.PENSAR_CONSOLE_URL || PENSAR_CONSOLE_BASE_URL;
190
+ }
191
+ // src/core/config/index.ts
192
+ var config2 = {
193
+ get,
194
+ init,
195
+ update
196
+ };
197
+
198
+ // src/core/api/constants.ts
199
+ var PENSAR_API_BASE_URL2 = "https://api.pensar.dev";
200
+ function getPensarApiUrl2() {
201
+ return PENSAR_API_BASE_URL2;
202
+ }
203
+ // src/core/auth/device-flow.ts
204
+ function sleep(ms) {
205
+ return new Promise((resolve) => setTimeout(resolve, ms));
206
+ }
207
+ async function startDeviceFlow(apiUrl) {
208
+ const url = apiUrl ?? getPensarApiUrl2();
209
+ try {
210
+ const configResponse = await fetch(`${url}/api/cli/config`);
211
+ if (configResponse.ok) {
212
+ const cliConfig = await configResponse.json();
213
+ const response2 = await fetch("https://api.workos.com/user_management/authorize/device", {
214
+ method: "POST",
215
+ headers: { "Content-Type": "application/json" },
216
+ body: JSON.stringify({ client_id: cliConfig.workosClientId })
217
+ });
218
+ if (response2.ok) {
219
+ const deviceInfo2 = await response2.json();
220
+ return {
221
+ mode: "workos",
222
+ clientId: cliConfig.workosClientId,
223
+ deviceInfo: deviceInfo2
224
+ };
225
+ }
226
+ }
227
+ } catch {}
228
+ const response = await fetch(`${url}/auth/device/code`, {
229
+ method: "POST",
230
+ headers: { "Content-Type": "application/json" }
231
+ });
232
+ if (!response.ok) {
233
+ throw new Error("Failed to start device authorization");
234
+ }
235
+ const deviceInfo = await response.json();
236
+ return { mode: "legacy", deviceInfo };
237
+ }
238
+ async function pollWorkOSToken(params) {
239
+ const { clientId, deviceCode, interval, expiresIn, signal } = params;
240
+ const deadline = Date.now() + expiresIn * 1000;
241
+ while (Date.now() < deadline) {
242
+ if (signal?.aborted)
243
+ throw new Error("Authorization cancelled");
244
+ await sleep(interval * 1000);
245
+ if (signal?.aborted)
246
+ throw new Error("Authorization cancelled");
247
+ try {
248
+ const response = await fetch("https://api.workos.com/user_management/authenticate", {
249
+ method: "POST",
250
+ headers: { "Content-Type": "application/json" },
251
+ body: JSON.stringify({
252
+ client_id: clientId,
253
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
254
+ device_code: deviceCode
255
+ })
256
+ });
257
+ if (response.status === 400) {
258
+ continue;
259
+ }
260
+ if (!response.ok) {
261
+ throw new Error("Authentication failed");
262
+ }
263
+ const data = await response.json();
264
+ return {
265
+ accessToken: data.access_token,
266
+ refreshToken: data.refresh_token
267
+ };
268
+ } catch (err) {
269
+ if (err instanceof Error && (err.message === "Authentication failed" || err.message === "Authorization cancelled")) {
270
+ throw err;
271
+ }
272
+ }
273
+ }
274
+ throw new Error("Authorization timed out. Please try again.");
275
+ }
276
+ async function pollLegacyToken(params) {
277
+ const { apiUrl, deviceCode, interval, expiresIn, signal } = params;
278
+ const deadline = Date.now() + expiresIn * 1000;
279
+ while (Date.now() < deadline) {
280
+ if (signal?.aborted)
281
+ throw new Error("Authorization cancelled");
282
+ await sleep(interval * 1000);
283
+ if (signal?.aborted)
284
+ throw new Error("Authorization cancelled");
285
+ try {
286
+ const response = await fetch(`${apiUrl}/auth/device/token`, {
287
+ method: "POST",
288
+ headers: { "Content-Type": "application/json" },
289
+ body: JSON.stringify({ deviceCode })
290
+ });
291
+ if (!response.ok)
292
+ continue;
293
+ const data = await response.json();
294
+ if (data.status === "complete" && data.apiKey) {
295
+ return data;
296
+ }
297
+ if (data.status === "expired") {
298
+ throw new Error("Authorization expired. Please try again.");
299
+ }
300
+ if (data.status === "not_found") {
301
+ throw new Error("Invalid authorization session. Please try again.");
302
+ }
303
+ } catch (err) {
304
+ if (signal?.aborted) {
305
+ throw new Error("Authorization cancelled", { cause: err });
306
+ }
307
+ if (err instanceof Error && err.message.includes("Please try again")) {
308
+ throw err;
309
+ }
310
+ }
311
+ }
312
+ throw new Error("Authorization timed out. Please try again.");
313
+ }
314
+ // src/core/auth/workspaces.ts
315
+ function sleep2(ms) {
316
+ return new Promise((resolve) => setTimeout(resolve, ms));
317
+ }
318
+ async function fetchWorkspaces(apiUrl, accessToken) {
319
+ const response = await fetch(`${apiUrl}/api/cli/workspaces`, {
320
+ headers: { Authorization: `Bearer ${accessToken}` }
321
+ });
322
+ if (!response.ok) {
323
+ throw new Error(`Failed to fetch workspaces (${response.status})`);
324
+ }
325
+ const data = await response.json();
326
+ return data.workspaces;
327
+ }
328
+ async function pollForWorkspaceCreation(apiUrl, accessToken, signal) {
329
+ const POLL_INTERVAL = 3000;
330
+ const TIMEOUT = 5 * 60 * 1000;
331
+ const deadline = Date.now() + TIMEOUT;
332
+ while (Date.now() < deadline) {
333
+ if (signal?.aborted)
334
+ throw new Error("Cancelled");
335
+ await sleep2(POLL_INTERVAL);
336
+ if (signal?.aborted)
337
+ throw new Error("Cancelled");
338
+ try {
339
+ const response = await fetch(`${apiUrl}/api/cli/workspaces`, {
340
+ headers: { Authorization: `Bearer ${accessToken}` }
341
+ });
342
+ if (!response.ok)
343
+ continue;
344
+ const data = await response.json();
345
+ if (data.workspaces.length > 0) {
346
+ return data.workspaces;
347
+ }
348
+ } catch {}
349
+ }
350
+ throw new Error("Workspace creation timed out. Please try again.");
351
+ }
352
+ async function selectWorkspace(apiUrl, accessToken, workspaceId) {
353
+ const response = await fetch(`${apiUrl}/api/cli/select-workspace`, {
354
+ method: "POST",
355
+ headers: {
356
+ "Content-Type": "application/json",
357
+ Authorization: `Bearer ${accessToken}`
358
+ },
359
+ body: JSON.stringify({ workspaceId })
360
+ });
361
+ if (!response.ok) {
362
+ throw new Error("Failed to select workspace");
363
+ }
364
+ return await response.json();
365
+ }
366
+ // src/core/auth/connection.ts
367
+ function isConnected(cfg) {
368
+ return !!(cfg.accessToken || cfg.pensarAPIKey);
369
+ }
370
+ async function disconnect() {
371
+ await config2.update({
372
+ pensarAPIKey: null,
373
+ accessToken: null,
374
+ refreshToken: null,
375
+ workspaceId: null,
376
+ workspaceSlug: null,
377
+ gatewaySigningKey: null
378
+ });
379
+ }
380
+ // src/cli/auth.ts
381
+ import * as readline from "readline";
382
+ function openUrl(url) {
383
+ try {
384
+ const platform = process.platform;
385
+ if (platform === "darwin") {
386
+ Bun.spawn(["open", url]);
387
+ } else if (platform === "win32") {
388
+ Bun.spawn(["cmd", "/c", "start", url]);
389
+ } else {
390
+ Bun.spawn(["xdg-open", url]);
391
+ }
392
+ } catch {}
393
+ }
394
+ function prompt(question) {
395
+ const rl = readline.createInterface({
396
+ input: process.stdin,
397
+ output: process.stdout
398
+ });
399
+ return new Promise((resolve) => {
400
+ rl.question(question, (answer) => {
401
+ rl.close();
402
+ resolve(answer.trim());
403
+ });
404
+ });
405
+ }
406
+ async function promptWorkspaceSelection(workspaces) {
407
+ console.log(`
408
+ Select a workspace:
409
+ `);
410
+ workspaces.forEach((ws, i) => {
411
+ console.log(` ${i + 1}. ${ws.name} (${ws.slug}) \u2014 $${ws.balance.toFixed(2)}`);
412
+ });
413
+ const answer = await prompt(`
414
+ Enter number (1-${workspaces.length}): `);
415
+ const index = parseInt(answer, 10) - 1;
416
+ if (isNaN(index) || index < 0 || index >= workspaces.length) {
417
+ console.error("Invalid selection.");
418
+ process.exit(1);
419
+ }
420
+ return workspaces[index];
421
+ }
422
+ async function login() {
423
+ const appConfig = await config.get();
424
+ if (isConnected(appConfig)) {
425
+ console.log("Already connected to Pensar Console.");
426
+ if (appConfig.workspaceSlug) {
427
+ console.log(` Workspace: ${appConfig.workspaceSlug}`);
428
+ }
429
+ const answer = await prompt(`
430
+ Reconnect? (y/N): `);
431
+ if (answer.toLowerCase() !== "y") {
432
+ return;
433
+ }
434
+ }
435
+ console.log(`
436
+ Pensar Console \u2014 Managed Inference`);
437
+ console.log(`Connect for usage-based AI inference. No API keys needed.
438
+ `);
439
+ const apiUrl = getPensarApiUrl();
440
+ const flowInfo = await startDeviceFlow(apiUrl);
441
+ if (flowInfo.mode === "workos") {
442
+ const { clientId, deviceInfo } = flowInfo;
443
+ openUrl(deviceInfo.verification_uri_complete);
444
+ console.log("A browser window should have opened.");
445
+ console.log(`If not, open this URL:
446
+ ${deviceInfo.verification_uri_complete}
447
+ `);
448
+ console.log(`Your code: ${deviceInfo.user_code}
449
+ `);
450
+ console.log("Waiting for browser authorization...");
451
+ const tokens = await pollWorkOSToken({
452
+ clientId,
453
+ deviceCode: deviceInfo.device_code,
454
+ interval: deviceInfo.interval,
455
+ expiresIn: deviceInfo.expires_in
456
+ });
457
+ await config.update({
458
+ accessToken: tokens.accessToken,
459
+ refreshToken: tokens.refreshToken
460
+ });
461
+ console.log(`
462
+ Authenticated successfully. Fetching workspaces...`);
463
+ await handleWorkspaces(apiUrl, tokens.accessToken);
464
+ } else {
465
+ const { deviceInfo } = flowInfo;
466
+ openUrl(deviceInfo.verificationUriComplete);
467
+ console.log("A browser window should have opened.");
468
+ console.log(`If not, open this URL:
469
+ ${deviceInfo.verificationUriComplete}
470
+ `);
471
+ console.log(`Your code: ${deviceInfo.userCode}
472
+ `);
473
+ console.log("Waiting for browser authorization...");
474
+ const data = await pollLegacyToken({
475
+ apiUrl,
476
+ deviceCode: deviceInfo.deviceCode,
477
+ interval: deviceInfo.interval,
478
+ expiresIn: deviceInfo.expiresIn
479
+ });
480
+ await config.update({
481
+ pensarAPIKey: data.apiKey,
482
+ gatewaySigningKey: data.signingKey ?? null
483
+ });
484
+ if (data.workspace) {
485
+ await config.update({
486
+ workspaceId: data.workspace.id,
487
+ workspaceSlug: data.workspace.slug
488
+ });
489
+ }
490
+ console.log(`
491
+ \u2713 Connected to Pensar Console`);
492
+ if (data.workspace) {
493
+ console.log(` Workspace: ${data.workspace.name} (${data.workspace.slug})`);
494
+ }
495
+ if (data.credits) {
496
+ console.log(` Credits: $${data.credits.balance.toFixed(2)}`);
497
+ }
498
+ }
499
+ }
500
+ async function handleWorkspaces(apiUrl, accessToken) {
501
+ let workspaces = await fetchWorkspaces(apiUrl, accessToken);
502
+ if (workspaces.length === 0) {
503
+ const consoleUrl = getPensarConsoleUrl();
504
+ console.log(`
505
+ No workspaces found. Opening browser to create one...`);
506
+ console.log(`If the browser didn't open, visit: ${consoleUrl}/credits
507
+ `);
508
+ openUrl(`${consoleUrl}/credits`);
509
+ console.log("Waiting for workspace creation...");
510
+ workspaces = await pollForWorkspaceCreation(apiUrl, accessToken);
511
+ }
512
+ let workspace;
513
+ if (workspaces.length === 1) {
514
+ workspace = workspaces[0];
515
+ } else {
516
+ workspace = await promptWorkspaceSelection(workspaces);
517
+ }
518
+ const result = await selectWorkspace(apiUrl, accessToken, workspace.id);
519
+ await config.update({
520
+ workspaceId: workspace.id,
521
+ workspaceSlug: workspace.slug,
522
+ gatewaySigningKey: result.signingKey ?? null
523
+ });
524
+ console.log(`
525
+ \u2713 Connected to Pensar Console`);
526
+ console.log(` Workspace: ${workspace.name} (${workspace.slug})`);
527
+ console.log(` Credits: $${result.billing.balance.toFixed(2)}`);
528
+ if (!result.confirmed && result.billingUrl) {
529
+ console.log(`
530
+ \u26A0 Your workspace needs credits. Add them at:
531
+ ${result.billingUrl}`);
532
+ } else if (result.billing.balance < 1) {
533
+ const billingUrl = `${getPensarConsoleUrl()}/${workspace.slug}/settings/billing`;
534
+ console.log(`
535
+ \u26A0 Low credit balance. We recommend at least $30 for uninterrupted pentests.
536
+ Add credits: ${billingUrl}`);
537
+ }
538
+ console.log("\nPensar models are now available. Run `pensar` to get started.");
539
+ }
540
+ async function logout() {
541
+ const appConfig = await config.get();
542
+ if (!isConnected(appConfig)) {
543
+ console.log("Not currently connected to Pensar Console.");
544
+ return;
545
+ }
546
+ await disconnect();
547
+ console.log("\u2713 Disconnected from Pensar Console.");
548
+ }
549
+ async function status() {
550
+ const appConfig = await config.get();
551
+ if (!isConnected(appConfig)) {
552
+ console.log("Not connected to Pensar Console.");
553
+ console.log("\nRun `pensar auth login` to connect.");
554
+ return;
555
+ }
556
+ console.log("\u2713 Connected to Pensar Console");
557
+ if (appConfig.workspaceSlug) {
558
+ console.log(` Workspace: ${appConfig.workspaceSlug}`);
559
+ }
560
+ if (appConfig.accessToken) {
561
+ console.log(" Auth: WorkOS (modern)");
562
+ } else {
563
+ console.log(" Auth: API key (legacy)");
564
+ }
565
+ }
566
+ function showHelp() {
567
+ console.log(`Pensar Auth \u2014 Connect to Pensar Console
568
+ `);
569
+ console.log("Usage:");
570
+ console.log(" pensar auth Login to Pensar Console (or show status if connected)");
571
+ console.log(" pensar auth login Login to Pensar Console");
572
+ console.log(" pensar auth logout Disconnect from Pensar Console");
573
+ console.log(" pensar auth status Show connection status");
574
+ console.log();
575
+ console.log("Options:");
576
+ console.log(" -h, --help Show this help message");
577
+ }
578
+ async function main() {
579
+ const args = process.argv.slice(2);
580
+ const subcommand = args[0];
581
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
582
+ showHelp();
583
+ return;
584
+ }
585
+ try {
586
+ if (!subcommand || subcommand === "login") {
587
+ await login();
588
+ } else if (subcommand === "logout") {
589
+ await logout();
590
+ } else if (subcommand === "status") {
591
+ await status();
592
+ } else {
593
+ console.error(`Unknown auth subcommand: ${subcommand}`);
594
+ console.error("Run 'pensar auth --help' for usage information");
595
+ process.exit(1);
596
+ }
597
+ } catch (err) {
598
+ console.error(`
599
+ Error: ${err instanceof Error ? err.message : String(err)}`);
600
+ process.exit(1);
601
+ }
602
+ }
603
+ main();