@oxygen-agent/cli 1.46.0 → 1.50.37
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/README.md +13 -1
- package/dist/credentials.js +114 -43
- package/dist/index.js +371 -91
- package/dist/local-custom-http-column.js +1 -1
- package/dist/update.d.ts +31 -0
- package/dist/update.js +116 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/billing.js +2 -1
- package/node_modules/@oxygen/shared/dist/cell-format.d.ts +60 -0
- package/node_modules/@oxygen/shared/dist/cell-format.js +278 -0
- package/node_modules/@oxygen/shared/dist/column-types.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/column-types.js +3 -2
- package/node_modules/@oxygen/shared/dist/file-import.js +1 -1
- package/node_modules/@oxygen/shared/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/index.js +1 -0
- package/node_modules/@oxygen/shared/dist/log.js +1 -1
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/workflows/dist/index.d.ts +145 -143
- package/node_modules/@oxygen/workflows/dist/index.js +30 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,12 @@ OXYGEN is the CLI/API-first GTM engineering workspace for early-stage B2B SaaS t
|
|
|
7
7
|
- Node.js >=22.22.0
|
|
8
8
|
- npm >=10.9.0
|
|
9
9
|
|
|
10
|
+
Supported OSes:
|
|
11
|
+
|
|
12
|
+
- macOS: credentials are stored in Keychain.
|
|
13
|
+
- Windows: credentials are stored in Windows Credential Manager.
|
|
14
|
+
- Linux: credentials are stored in ~/.config/oxygen/credentials.json with user-only file permissions.
|
|
15
|
+
|
|
10
16
|
## Install
|
|
11
17
|
|
|
12
18
|
```sh
|
|
@@ -14,12 +20,18 @@ npm install -g @oxygen-agent/cli
|
|
|
14
20
|
oxygen login
|
|
15
21
|
```
|
|
16
22
|
|
|
23
|
+
Copy both lines: the login command runs after npm has installed the `oxygen` binary.
|
|
24
|
+
|
|
25
|
+
On Windows, run the same commands from PowerShell, Windows Terminal, or Command Prompt after installing Node.js with npm added to PATH.
|
|
26
|
+
|
|
17
27
|
## Update
|
|
18
28
|
|
|
19
29
|
```sh
|
|
20
30
|
oxygen update
|
|
21
31
|
```
|
|
22
32
|
|
|
33
|
+
`oxygen update` runs `npm install -g @oxygen-agent/cli@latest`, preserves npm prefixes on macOS, Linux, and Windows, and runs npm through `cmd.exe` on Windows.
|
|
34
|
+
|
|
23
35
|
For product documentation and support, visit https://oxygen-agent.com.
|
|
24
36
|
|
|
25
|
-
Version: 1.
|
|
37
|
+
Version: 1.50.37
|
package/dist/credentials.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, chmodSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
@@ -9,6 +9,7 @@ const SERVICE_NAME = "oxygen-cli";
|
|
|
9
9
|
const ACCOUNT_NAME = "default";
|
|
10
10
|
const DEFAULT_API_URL = "https://oxygen-agent.com";
|
|
11
11
|
const DEFAULT_PROFILE_NAME = "default";
|
|
12
|
+
const WINDOWS_CREDENTIAL_PAYLOAD_ENV = "OXYGEN_CLI_WINDOWS_CREDENTIAL_PAYLOAD";
|
|
12
13
|
export function defaultApiUrl(env = process.env) {
|
|
13
14
|
return normalizeApiUrl(env.OXYGEN_API_URL ?? DEFAULT_API_URL);
|
|
14
15
|
}
|
|
@@ -68,6 +69,7 @@ export async function clearCredentials(env = process.env, options = {}) {
|
|
|
68
69
|
exitCode: 1,
|
|
69
70
|
});
|
|
70
71
|
}
|
|
72
|
+
// skipcq: JS-0320
|
|
71
73
|
delete profiles[profile];
|
|
72
74
|
const names = Object.keys(profiles).sort((a, b) => a.localeCompare(b));
|
|
73
75
|
if (names.length === 0) {
|
|
@@ -186,19 +188,19 @@ async function readSystemCredentialDocument(env) {
|
|
|
186
188
|
async function readSystemCredentialPayload(env) {
|
|
187
189
|
const store = credentialStore(env);
|
|
188
190
|
if (store === "keychain")
|
|
189
|
-
return readMacOSKeychainPayload();
|
|
191
|
+
return readMacOSKeychainPayload(env);
|
|
190
192
|
if (store === "credential-manager")
|
|
191
|
-
return readWindowsCredentialManagerPayload();
|
|
193
|
+
return readWindowsCredentialManagerPayload(env);
|
|
192
194
|
return readFileCredentialPayload(env);
|
|
193
195
|
}
|
|
194
196
|
async function writeSystemCredentialPayload(payload, env) {
|
|
195
197
|
const store = credentialStore(env);
|
|
196
198
|
if (store === "keychain") {
|
|
197
|
-
await writeMacOSKeychainPayload(payload);
|
|
199
|
+
await writeMacOSKeychainPayload(payload, env);
|
|
198
200
|
return;
|
|
199
201
|
}
|
|
200
202
|
if (store === "credential-manager") {
|
|
201
|
-
await writeWindowsCredentialManagerPayload(payload);
|
|
203
|
+
await writeWindowsCredentialManagerPayload(payload, env);
|
|
202
204
|
return;
|
|
203
205
|
}
|
|
204
206
|
writeFileCredentialPayload(payload, env);
|
|
@@ -206,11 +208,11 @@ async function writeSystemCredentialPayload(payload, env) {
|
|
|
206
208
|
async function deleteSystemCredentialPayload(env) {
|
|
207
209
|
const store = credentialStore(env);
|
|
208
210
|
if (store === "keychain") {
|
|
209
|
-
await deleteMacOSKeychain();
|
|
211
|
+
await deleteMacOSKeychain(env);
|
|
210
212
|
return;
|
|
211
213
|
}
|
|
212
214
|
if (store === "credential-manager") {
|
|
213
|
-
await deleteWindowsCredentialManager();
|
|
215
|
+
await deleteWindowsCredentialManager(env);
|
|
214
216
|
return;
|
|
215
217
|
}
|
|
216
218
|
deleteFileCredentials(env);
|
|
@@ -271,63 +273,97 @@ function deleteFileCredentials(env) {
|
|
|
271
273
|
const path = credentialsPath(env);
|
|
272
274
|
rmSync(path, { force: true });
|
|
273
275
|
}
|
|
274
|
-
async function readMacOSKeychainPayload() {
|
|
276
|
+
async function readMacOSKeychainPayload(env) {
|
|
275
277
|
try {
|
|
276
|
-
const { stdout } = await execFileAsync(
|
|
278
|
+
const { stdout } = await execFileAsync(macOSSecurityCommand(env), [
|
|
277
279
|
"find-generic-password",
|
|
278
280
|
"-a",
|
|
279
281
|
ACCOUNT_NAME,
|
|
280
282
|
"-s",
|
|
281
283
|
SERVICE_NAME,
|
|
282
284
|
"-w",
|
|
283
|
-
]);
|
|
285
|
+
], { env });
|
|
284
286
|
return stdout;
|
|
285
287
|
}
|
|
286
288
|
catch {
|
|
287
289
|
return null;
|
|
288
290
|
}
|
|
289
291
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
292
|
+
// `security -i` reads commands with a line buffer that traditionally caps at
|
|
293
|
+
// 4096 bytes. Beyond that, the input is silently truncated, which would
|
|
294
|
+
// corrupt the stored credential and break re-auth. Reject early with a clear
|
|
295
|
+
// error so a user with too many profiles gets actionable feedback instead of
|
|
296
|
+
// a partial-write that only surfaces on the next CLI invocation.
|
|
297
|
+
const MACOS_SECURITY_LINE_LIMIT_BYTES = 4096;
|
|
298
|
+
async function writeMacOSKeychainPayload(payload, env) {
|
|
299
|
+
// `security add-generic-password -w PAYLOAD` would put the token in argv
|
|
300
|
+
// (visible to any same-UID `ps`). Use `security -i` instead: the subcommand
|
|
301
|
+
// is written on stdin, so the only argv `ps` ever sees is `security -i`.
|
|
302
|
+
const command = macOSSecurityCommand(env);
|
|
303
|
+
const escaped = payload.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
304
|
+
const line = `add-generic-password -a "${ACCOUNT_NAME}" -s "${SERVICE_NAME}" -w "${escaped}" -U\n`;
|
|
305
|
+
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
306
|
+
if (lineBytes >= MACOS_SECURITY_LINE_LIMIT_BYTES) {
|
|
307
|
+
throw credentialStoreError("write", "macOS Keychain", {
|
|
308
|
+
code: "payload_too_large",
|
|
309
|
+
stderr: `Credential document is ${lineBytes} bytes; \`security -i\` truncates lines at ${MACOS_SECURITY_LINE_LIMIT_BYTES} bytes. Reduce stored profiles (oxygen logout <profile>) and retry.`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
await new Promise((resolve, reject) => {
|
|
313
|
+
const child = spawn(command, ["-i"], { env, stdio: ["pipe", "pipe", "pipe"] });
|
|
314
|
+
let stderr = "";
|
|
315
|
+
child.stderr.on("data", (chunk) => {
|
|
316
|
+
stderr += chunk.toString("utf8");
|
|
317
|
+
});
|
|
318
|
+
child.once("error", (error) => {
|
|
319
|
+
reject(credentialStoreError("write", "macOS Keychain", error));
|
|
320
|
+
});
|
|
321
|
+
child.once("close", (code, signal) => {
|
|
322
|
+
const trimmedStderr = stderr.trim();
|
|
323
|
+
if (code === 0 && !trimmedStderr) {
|
|
324
|
+
resolve();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
reject(credentialStoreError("write", "macOS Keychain", { code, signal, stderr: trimmedStderr }));
|
|
328
|
+
});
|
|
329
|
+
child.stdin.end(line);
|
|
330
|
+
});
|
|
301
331
|
}
|
|
302
|
-
async function deleteMacOSKeychain() {
|
|
303
|
-
await execFileAsync(
|
|
332
|
+
async function deleteMacOSKeychain(env) {
|
|
333
|
+
await execFileAsync(macOSSecurityCommand(env), [
|
|
304
334
|
"delete-generic-password",
|
|
305
335
|
"-a",
|
|
306
336
|
ACCOUNT_NAME,
|
|
307
337
|
"-s",
|
|
308
338
|
SERVICE_NAME,
|
|
309
|
-
]).catch(() => undefined);
|
|
339
|
+
], { env }).catch(() => undefined);
|
|
310
340
|
}
|
|
311
|
-
async function readWindowsCredentialManagerPayload() {
|
|
341
|
+
async function readWindowsCredentialManagerPayload(env) {
|
|
312
342
|
try {
|
|
313
343
|
const { stdout } = await runPowerShell(`${windowsCredentialManagerScript()}
|
|
314
344
|
$ptr = [IntPtr]::Zero
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
$
|
|
318
|
-
[
|
|
319
|
-
[Console]::Out.Write($secret)
|
|
320
|
-
|
|
345
|
+
try {
|
|
346
|
+
if (-not [OxygenCredMan]::CredRead("${SERVICE_NAME}", 1, 0, [ref]$ptr)) { exit 2 }
|
|
347
|
+
$cred = [Runtime.InteropServices.Marshal]::PtrToStructure($ptr, [type][OxygenCredMan+CREDENTIAL])
|
|
348
|
+
$secret = [Runtime.InteropServices.Marshal]::PtrToStringUni($cred.CredentialBlob, [int]($cred.CredentialBlobSize / 2))
|
|
349
|
+
[Console]::Out.Write($secret)
|
|
350
|
+
} finally {
|
|
351
|
+
if ($ptr -ne [IntPtr]::Zero) { [OxygenCredMan]::CredFree($ptr) }
|
|
352
|
+
}
|
|
353
|
+
`, env);
|
|
321
354
|
return stdout;
|
|
322
355
|
}
|
|
323
356
|
catch {
|
|
324
357
|
return null;
|
|
325
358
|
}
|
|
326
359
|
}
|
|
327
|
-
async function writeWindowsCredentialManagerPayload(value) {
|
|
360
|
+
async function writeWindowsCredentialManagerPayload(value, env) {
|
|
328
361
|
const payload = Buffer.from(value, "utf16le").toString("base64");
|
|
329
|
-
|
|
330
|
-
|
|
362
|
+
try {
|
|
363
|
+
await runPowerShell(`${windowsCredentialManagerScript()}
|
|
364
|
+
$payload = $env:${WINDOWS_CREDENTIAL_PAYLOAD_ENV}
|
|
365
|
+
if ([string]::IsNullOrWhiteSpace($payload)) { exit 1 }
|
|
366
|
+
$secret = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($payload))
|
|
331
367
|
$bytes = [Text.Encoding]::Unicode.GetBytes($secret)
|
|
332
368
|
$blob = [Runtime.InteropServices.Marshal]::StringToCoTaskMemUni($secret)
|
|
333
369
|
try {
|
|
@@ -342,23 +378,58 @@ try {
|
|
|
342
378
|
} finally {
|
|
343
379
|
[Runtime.InteropServices.Marshal]::FreeCoTaskMem($blob)
|
|
344
380
|
}
|
|
345
|
-
`,
|
|
381
|
+
`, {
|
|
382
|
+
...env,
|
|
383
|
+
[WINDOWS_CREDENTIAL_PAYLOAD_ENV]: payload,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
throw credentialStoreError("write", "Windows Credential Manager", error);
|
|
388
|
+
}
|
|
346
389
|
}
|
|
347
|
-
async function deleteWindowsCredentialManager() {
|
|
390
|
+
async function deleteWindowsCredentialManager(env) {
|
|
348
391
|
await runPowerShell(`${windowsCredentialManagerScript()}
|
|
349
392
|
[void][OxygenCredMan]::CredDelete("${SERVICE_NAME}", 1, 0)
|
|
350
|
-
|
|
393
|
+
`, env).catch(() => undefined);
|
|
351
394
|
}
|
|
352
|
-
async function runPowerShell(script,
|
|
353
|
-
|
|
395
|
+
async function runPowerShell(script, env) {
|
|
396
|
+
const encodedCommand = Buffer.from(script, "utf16le").toString("base64");
|
|
397
|
+
return execFileAsync(windowsPowerShellCommand(env), [
|
|
354
398
|
"-NoProfile",
|
|
355
399
|
"-NonInteractive",
|
|
356
400
|
"-ExecutionPolicy",
|
|
357
401
|
"Bypass",
|
|
358
|
-
"-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
402
|
+
"-EncodedCommand",
|
|
403
|
+
encodedCommand,
|
|
404
|
+
], { env });
|
|
405
|
+
}
|
|
406
|
+
function credentialStoreError(operation, store, error) {
|
|
407
|
+
const child = error;
|
|
408
|
+
const details = {
|
|
409
|
+
operation,
|
|
410
|
+
store,
|
|
411
|
+
};
|
|
412
|
+
if (typeof child.code === "number") {
|
|
413
|
+
details.exit_code = child.code;
|
|
414
|
+
}
|
|
415
|
+
else if (typeof child.code === "string") {
|
|
416
|
+
details.reason = child.code;
|
|
417
|
+
}
|
|
418
|
+
if (typeof child.signal === "string")
|
|
419
|
+
details.signal = child.signal;
|
|
420
|
+
if (typeof child.stderr === "string" && child.stderr.trim()) {
|
|
421
|
+
details.stderr = sanitizeCredentialErrorText(child.stderr.trim()).slice(0, 4000);
|
|
422
|
+
}
|
|
423
|
+
return new OxygenError("credential_store_failed", `Unable to ${operation} Oxygen CLI credentials in ${store}.`, { details, exitCode: 1 });
|
|
424
|
+
}
|
|
425
|
+
function sanitizeCredentialErrorText(value) {
|
|
426
|
+
return value.replace(/\boxy_live_[A-Za-z0-9_-]+\b/g, "[redacted_token]");
|
|
427
|
+
}
|
|
428
|
+
function macOSSecurityCommand(env) {
|
|
429
|
+
return env.OXYGEN_SECURITY_COMMAND?.trim() || "security";
|
|
430
|
+
}
|
|
431
|
+
function windowsPowerShellCommand(env) {
|
|
432
|
+
return env.OXYGEN_POWERSHELL_COMMAND?.trim() || "powershell.exe";
|
|
362
433
|
}
|
|
363
434
|
function windowsCredentialManagerScript() {
|
|
364
435
|
return `
|