azdo-cli 0.20.0-feature-043-dpapi-credential-store.744 → 0.20.0-feature-043-dpapi-credential-store.746
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
CHANGED
|
@@ -19,7 +19,7 @@ Azure DevOps CLI focused on work item read/write workflows.
|
|
|
19
19
|
- Feed long PR titles, descriptions and comment bodies from a file or a pipe instead of the shell (`--title-file`, `--description-file`, `--file`; `-` reads standard input)
|
|
20
20
|
- Persist org/project/default fields in local config (`config`)
|
|
21
21
|
- List all fields of a work item (`list-fields`)
|
|
22
|
-
- Authenticate per Azure DevOps organization with `azdo auth login` — OAuth (Microsoft Entra) by default, or a Personal Access Token via `--use-pat` (or the `AZDO_PAT` env var). Credentials are stored in the OS credential store. Inspect with `azdo auth status`, remove with `azdo auth logout`. Diagnose auth problems with `azdo auth diagnose`. Print the credential the CLI itself would use, for API calls the CLI does not yet wrap, with `azdo auth token`. See [docs/authentication.md](docs/authentication.md).
|
|
22
|
+
- Authenticate per Azure DevOps organization with `azdo auth login` — OAuth (Microsoft Entra) by default, or a Personal Access Token via `--use-pat` (or the `AZDO_PAT` env var). Credentials are stored in the OS credential store — or, on Windows over SSH where Credential Manager is unreachable, in DPAPI-encrypted files under `~/.azdo` (`azdo config set credentialStore dpapi`, which offers to copy existing Credential Manager credentials; `--copy-credentials` / `--no-copy-credentials` skip the prompt). Inspect with `azdo auth status`, remove with `azdo auth logout`. Diagnose auth problems with `azdo auth diagnose`. Print the credential the CLI itself would use, for API calls the CLI does not yet wrap, with `azdo auth token`. See [docs/authentication.md](docs/authentication.md).
|
|
23
23
|
- Trace all HTTP requests to a local file with `--trace <filepath>` (sensitive headers and tokens are automatically redacted).
|
|
24
24
|
|
|
25
25
|
## Installation
|
|
@@ -38,6 +38,10 @@ azdo auth login --org myorg
|
|
|
38
38
|
azdo config set org myorg
|
|
39
39
|
azdo config set project myproject
|
|
40
40
|
|
|
41
|
+
# Windows over SSH: keep credentials in DPAPI files instead of Credential Manager
|
|
42
|
+
# (run once from a console to copy the existing credentials across)
|
|
43
|
+
azdo config set credentialStore dpapi --copy-credentials
|
|
44
|
+
|
|
41
45
|
# Read a work item
|
|
42
46
|
azdo get-item 12345
|
|
43
47
|
|
|
@@ -947,8 +947,12 @@ function requestedStore() {
|
|
|
947
947
|
let fromConfig;
|
|
948
948
|
try {
|
|
949
949
|
fromConfig = loadConfig()?.credentialStore;
|
|
950
|
-
} catch {
|
|
951
|
-
|
|
950
|
+
} catch (err) {
|
|
951
|
+
throw new CredentialStoreUnavailableError(
|
|
952
|
+
"unknown",
|
|
953
|
+
err,
|
|
954
|
+
`Could not read the azdo config file to determine the credential store: ${err.message}. Fix the file, or set AZDO_CREDENTIAL_STORE=<keyring|dpapi> for this session.`
|
|
955
|
+
);
|
|
952
956
|
}
|
|
953
957
|
return fromConfig?.trim().toLowerCase() || "keyring";
|
|
954
958
|
}
|
|
@@ -966,7 +970,13 @@ function activeCredentialStore() {
|
|
|
966
970
|
}
|
|
967
971
|
}
|
|
968
972
|
function probeBackend() {
|
|
969
|
-
|
|
973
|
+
let requested;
|
|
974
|
+
try {
|
|
975
|
+
requested = requestedStore();
|
|
976
|
+
} catch {
|
|
977
|
+
return "unknown";
|
|
978
|
+
}
|
|
979
|
+
if (requested === "dpapi") return "windows-dpapi";
|
|
970
980
|
switch (process.platform) {
|
|
971
981
|
case "win32":
|
|
972
982
|
return "windows-credential-manager";
|
|
@@ -1168,7 +1178,7 @@ async function deletePat(org) {
|
|
|
1168
1178
|
}
|
|
1169
1179
|
try {
|
|
1170
1180
|
const { unlinkSync: unlinkSync2 } = await import("fs");
|
|
1171
|
-
const { lockPath: lockPath2 } = await import("./oauth-token-refresh-
|
|
1181
|
+
const { lockPath: lockPath2 } = await import("./oauth-token-refresh-RUT4XIMM.js");
|
|
1172
1182
|
unlinkSync2(lockPath2(org));
|
|
1173
1183
|
} catch {
|
|
1174
1184
|
}
|
package/dist/index.js
CHANGED
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
tokenResponseToCredential,
|
|
40
40
|
unsetConfigValue,
|
|
41
41
|
unsetOrgScopedValue
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-W4EVKARR.js";
|
|
43
43
|
|
|
44
44
|
// src/program.ts
|
|
45
45
|
import { Command as Command19 } from "commander";
|
|
@@ -143,6 +143,14 @@ function getActiveTraceWriter() {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
// src/services/command-helpers.ts
|
|
146
|
+
var EXIT_CREDENTIAL_STORE_UNAVAILABLE = 4;
|
|
147
|
+
function reportCredentialStoreUnavailable(err) {
|
|
148
|
+
if (!(err instanceof CredentialStoreUnavailableError)) return false;
|
|
149
|
+
process.stderr.write(`Error: ${err.message}
|
|
150
|
+
`);
|
|
151
|
+
process.exitCode = EXIT_CREDENTIAL_STORE_UNAVAILABLE;
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
146
154
|
function isSentinel(message, sentinel) {
|
|
147
155
|
return message === sentinel || message.startsWith(`${sentinel}: `);
|
|
148
156
|
}
|
|
@@ -221,6 +229,10 @@ function formatCreateError(err) {
|
|
|
221
229
|
return `Create rejected: ${message}`;
|
|
222
230
|
}
|
|
223
231
|
function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
232
|
+
if (reportCredentialStoreUnavailable(err)) {
|
|
233
|
+
if (exit) process.exit(EXIT_CREDENTIAL_STORE_UNAVAILABLE);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
224
236
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
225
237
|
const msg = error.message;
|
|
226
238
|
const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
|
|
@@ -2679,7 +2691,8 @@ function buildConfigListEntries(cfg) {
|
|
|
2679
2691
|
return entries;
|
|
2680
2692
|
}
|
|
2681
2693
|
function writeConfigList(cfg) {
|
|
2682
|
-
const
|
|
2694
|
+
const orgKeys = Object.values(cfg.organizations ?? {}).flatMap((scope) => Object.keys(scope ?? {}));
|
|
2695
|
+
const keyWidth = Math.max(10, ...SETTINGS.map((s) => s.key.length + 1), ...orgKeys.map((k) => k.length + 1));
|
|
2683
2696
|
const valueWidth = 30;
|
|
2684
2697
|
const scopeWidth = 12;
|
|
2685
2698
|
process.stdout.write(
|
|
@@ -3461,6 +3474,7 @@ function isCreateWriteError(err) {
|
|
|
3461
3474
|
return isSentinel(err.message, "AUTH_FAILED") || isSentinel(err.message, "PERMISSION_DENIED") || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("HTTP_");
|
|
3462
3475
|
}
|
|
3463
3476
|
function handleUpsertError(err, id, context) {
|
|
3477
|
+
if (reportCredentialStoreUnavailable(err)) process.exit(EXIT_CREDENTIAL_STORE_UNAVAILABLE);
|
|
3464
3478
|
if (!(err instanceof Error)) {
|
|
3465
3479
|
process.stderr.write(`Error: ${String(err)}
|
|
3466
3480
|
`);
|
|
@@ -4455,6 +4469,7 @@ function writeError(message, exitCode = 1) {
|
|
|
4455
4469
|
process.exitCode = exitCode;
|
|
4456
4470
|
}
|
|
4457
4471
|
function handlePrCommandError(err, context, mode = "read") {
|
|
4472
|
+
if (reportCredentialStoreUnavailable(err)) return;
|
|
4458
4473
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
4459
4474
|
if (isSentinel(error.message, "AUTH_FAILED")) {
|
|
4460
4475
|
const scopeLabel = mode === "write" ? "Code (Read & Write)" : "Code (Read)";
|
|
@@ -6165,6 +6180,7 @@ function writeError2(message) {
|
|
|
6165
6180
|
process.exitCode = 1;
|
|
6166
6181
|
}
|
|
6167
6182
|
function handlePipelineError(err, context) {
|
|
6183
|
+
if (reportCredentialStoreUnavailable(err)) return;
|
|
6168
6184
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
6169
6185
|
if (isSentinel(error.message, "AUTH_FAILED")) {
|
|
6170
6186
|
writeError2('Authentication failed. Check that your credential is valid and has the "Build (Read)" scope.');
|
|
@@ -7224,6 +7240,7 @@ function formatRelationsList(workItemId, relations) {
|
|
|
7224
7240
|
return lines.join("\n");
|
|
7225
7241
|
}
|
|
7226
7242
|
function handleRelationError(err, id1) {
|
|
7243
|
+
if (reportCredentialStoreUnavailable(err)) process.exit(EXIT_CREDENTIAL_STORE_UNAVAILABLE);
|
|
7227
7244
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7228
7245
|
if (msg === "SELF_RELATION") {
|
|
7229
7246
|
process.stderr.write(`Error: cannot relate a work item to itself (#${id1}).
|