@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 +10 -26
- package/build/auth.js +603 -0
- package/build/index.js +1656 -995
- package/package.json +2 -2
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(
|
|
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
|
-
"
|
|
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
|
-
|
|
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();
|