azdo-cli 0.20.0-feature-043-dpapi-credential-store.742 → 0.20.0-feature-043-dpapi-credential-store.744

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.
@@ -978,14 +978,17 @@ function probeBackend() {
978
978
  return "unknown";
979
979
  }
980
980
  }
981
- function wrapUnavailable(fn) {
981
+ function wrapUnavailableAs(backend, fn) {
982
982
  try {
983
983
  return fn();
984
984
  } catch (err) {
985
985
  if (err instanceof CredentialStoreUnavailableError) throw err;
986
- throw new CredentialStoreUnavailableError(probeBackend(), err);
986
+ throw new CredentialStoreUnavailableError(backend, err);
987
987
  }
988
988
  }
989
+ function wrapUnavailable(fn) {
990
+ return wrapUnavailableAs(probeBackend(), fn);
991
+ }
989
992
  function entryFor(account) {
990
993
  if (activeCredentialStore() === "dpapi") return new DpapiEntry(account);
991
994
  return wrapUnavailable(() => new Entry(SERVICE, account));
@@ -1165,7 +1168,7 @@ async function deletePat(org) {
1165
1168
  }
1166
1169
  try {
1167
1170
  const { unlinkSync: unlinkSync2 } = await import("fs");
1168
- const { lockPath: lockPath2 } = await import("./oauth-token-refresh-5O2DYWFR.js");
1171
+ const { lockPath: lockPath2 } = await import("./oauth-token-refresh-M6L5EUZW.js");
1169
1172
  unlinkSync2(lockPath2(org));
1170
1173
  } catch {
1171
1174
  }
@@ -1191,6 +1194,62 @@ async function listOrgsWithStoredPat() {
1191
1194
  present.sort((a, b) => a.localeCompare(b));
1192
1195
  return present;
1193
1196
  }
1197
+ function keyringEntry(account) {
1198
+ return new Entry(SERVICE, account);
1199
+ }
1200
+ function knownOrgs() {
1201
+ const orgs = /* @__PURE__ */ new Set();
1202
+ for (const ev of readAuditEvents()) {
1203
+ if (ev.event === "auth.store" || ev.event === "oauth-login-success") orgs.add(ev.org);
1204
+ }
1205
+ try {
1206
+ const config = loadConfig();
1207
+ if (config.org) orgs.add(config.org);
1208
+ for (const name of Object.keys(config.organizations ?? {})) orgs.add(name);
1209
+ } catch {
1210
+ }
1211
+ return [...orgs].sort((a, b) => a.localeCompare(b));
1212
+ }
1213
+ function listKeyringCredentials() {
1214
+ const present = [];
1215
+ for (const org of knownOrgs()) {
1216
+ const value = wrapUnavailableAs(
1217
+ "windows-credential-manager",
1218
+ () => keyringEntry(accountFor(org)).getPassword()
1219
+ );
1220
+ if (value !== null) present.push(org);
1221
+ }
1222
+ return present;
1223
+ }
1224
+ function copyKeyringCredentialsToDpapi(orgs) {
1225
+ const result = { copied: [], skipped: [], failed: [] };
1226
+ for (const org of orgs) {
1227
+ try {
1228
+ const raw = wrapUnavailableAs(
1229
+ "windows-credential-manager",
1230
+ () => keyringEntry(accountFor(org)).getPassword()
1231
+ );
1232
+ if (raw === null) continue;
1233
+ const target = new DpapiEntry(accountFor(org));
1234
+ if (target.getPassword() !== null) {
1235
+ result.skipped.push(org);
1236
+ continue;
1237
+ }
1238
+ const cred = parseStoredValue(raw);
1239
+ target.setPassword(raw);
1240
+ appendAuthAuditEvent({
1241
+ event: "auth.store",
1242
+ org,
1243
+ backend: "windows-dpapi",
1244
+ ...cred.kind === "pat" ? { masked_pat: maskedDisplay(cred.token) } : { accountId: cred.accountId }
1245
+ });
1246
+ result.copied.push(org);
1247
+ } catch (err) {
1248
+ result.failed.push({ org, message: err.message });
1249
+ }
1250
+ }
1251
+ return result;
1252
+ }
1194
1253
 
1195
1254
  // src/services/oauth-token-refresh.ts
1196
1255
  var DEFAULT_LOCK_WAIT_MS = 5e3;
@@ -1393,5 +1452,7 @@ export {
1393
1452
  storePat,
1394
1453
  storeOAuthCredential,
1395
1454
  deletePat,
1396
- listOrgsWithStoredPat
1455
+ listOrgsWithStoredPat,
1456
+ listKeyringCredentials,
1457
+ copyKeyringCredentialsToDpapi
1397
1458
  };
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  SETTINGS,
8
8
  appendAuthAuditEvent,
9
9
  buildScopeString,
10
+ copyKeyringCredentialsToDpapi,
10
11
  copyOrgScope,
11
12
  defaultScopes,
12
13
  deleteOrgScope,
@@ -16,6 +17,7 @@ import {
16
17
  getOrgScopedValue,
17
18
  getPat,
18
19
  getStoredCredential,
20
+ listKeyringCredentials,
19
21
  listOrgsWithStoredPat,
20
22
  loadConfig,
21
23
  maskedDisplay,
@@ -37,7 +39,7 @@ import {
37
39
  tokenResponseToCredential,
38
40
  unsetConfigValue,
39
41
  unsetOrgScopedValue
40
- } from "./chunk-B76UX3CR.js";
42
+ } from "./chunk-RG45CRKX.js";
41
43
 
42
44
  // src/program.ts
43
45
  import { Command as Command19 } from "commander";
@@ -2607,6 +2609,56 @@ not be resolved, 4 OS credential store unavailable. stdout stays empty on all.
2607
2609
  // src/commands/config.ts
2608
2610
  import { Command as Command4 } from "commander";
2609
2611
  import { createInterface as createInterface2 } from "readline";
2612
+
2613
+ // src/services/dpapi-copy.ts
2614
+ var RERUN = "azdo config set credentialStore dpapi --copy-credentials";
2615
+ function isYes(answer) {
2616
+ const a = answer.trim().toLowerCase();
2617
+ return a === "" || a === "y" || a === "yes";
2618
+ }
2619
+ function empty(found, keyringUnavailable) {
2620
+ return { found, keyringUnavailable, copied: [], skipped: [], failed: [] };
2621
+ }
2622
+ async function offerKeyringToDpapiCopy(choice, io) {
2623
+ if (choice === "no") return empty([], false);
2624
+ let found;
2625
+ try {
2626
+ found = listKeyringCredentials();
2627
+ } catch {
2628
+ io.notice(
2629
+ `Credential Manager is not reachable from this session (typical over SSH), so no existing credentials were copied. To bring them over, run \`${RERUN}\` once from a console session.
2630
+ `
2631
+ );
2632
+ return empty([], true);
2633
+ }
2634
+ if (found.length === 0) return empty(found, false);
2635
+ const list = found.join(", ");
2636
+ if (choice === "ask") {
2637
+ if (!io.interactive) {
2638
+ io.notice(`Credential Manager holds credentials for ${list}. Run \`${RERUN}\` to copy them to the DPAPI store.
2639
+ `);
2640
+ return empty(found, false);
2641
+ }
2642
+ const answer = await io.ask(`Copy the credentials for ${list} from Credential Manager to the DPAPI store? [Y/n] `);
2643
+ if (!isYes(answer)) {
2644
+ io.notice("Nothing copied. Run `azdo auth login` for each org, or re-run with --copy-credentials.\n");
2645
+ return empty(found, false);
2646
+ }
2647
+ }
2648
+ const result = copyKeyringCredentialsToDpapi(found);
2649
+ for (const org of result.copied) io.notice(`Copied the credential for ${org} to the DPAPI store.
2650
+ `);
2651
+ for (const org of result.skipped) io.notice(`Kept the existing DPAPI credential for ${org} (not overwritten).
2652
+ `);
2653
+ for (const { org, message } of result.failed) io.notice(`Could not copy the credential for ${org}: ${message}
2654
+ `);
2655
+ if (result.copied.length > 0) {
2656
+ io.notice("The Credential Manager entries were left in place; `credentialStore keyring` still finds them.\n");
2657
+ }
2658
+ return { found, keyringUnavailable: false, ...result };
2659
+ }
2660
+
2661
+ // src/commands/config.ts
2610
2662
  function formatConfigValue(value, unsetFallback = "") {
2611
2663
  if (value === void 0) {
2612
2664
  return unsetFallback;
@@ -2661,6 +2713,25 @@ function writeConfigList(cfg) {
2661
2713
  );
2662
2714
  }
2663
2715
  }
2716
+ async function offerCopy(options) {
2717
+ let choice = "ask";
2718
+ if (options.copyCredentials === true) choice = "yes";
2719
+ else if (options.copyCredentials === false) choice = "no";
2720
+ const interactive = !options.json && Boolean(process.stdin.isTTY) && Boolean(process.stderr.isTTY);
2721
+ let rl;
2722
+ try {
2723
+ return await offerKeyringToDpapiCopy(choice, {
2724
+ interactive,
2725
+ ask: (prompt) => {
2726
+ rl ??= createInterface2({ input: process.stdin, output: process.stderr });
2727
+ return createAsk(rl)(prompt);
2728
+ },
2729
+ notice: (message) => process.stderr.write(message)
2730
+ });
2731
+ } finally {
2732
+ rl?.close();
2733
+ }
2734
+ }
2664
2735
  function createAsk(rl) {
2665
2736
  return (prompt) => new Promise((resolve2) => rl.question(prompt, resolve2));
2666
2737
  }
@@ -2713,30 +2784,38 @@ function createConfigCommand() {
2713
2784
  ].join("\n")
2714
2785
  );
2715
2786
  const set = new Command4("set");
2716
- set.description("Set a configuration value").argument("<key>", "setting key (org, project, fields, markdown, credentialStore)").argument("<value>", "setting value").option("--org <org>", "set value in an org-scoped configuration").option("--json", "output in JSON format").action((key, value, options) => {
2787
+ set.description("Set a configuration value").argument("<key>", "setting key (org, project, fields, markdown, credentialStore)").argument("<value>", "setting value").option("--org <org>", "set value in an org-scoped configuration").option("--json", "output in JSON format").option("--copy-credentials", "with credentialStore dpapi: copy Credential Manager credentials without asking").option("--no-copy-credentials", "with credentialStore dpapi: do not offer to copy Credential Manager credentials").action((key, value, options) => {
2717
2788
  try {
2718
2789
  if (options.org) {
2719
2790
  setOrgScopedValue(options.org, key, value);
2720
2791
  } else {
2721
2792
  setConfigValue(key, value);
2722
2793
  }
2723
- if (options.json) {
2724
- const output = { key, value, scope: options.org ?? "default" };
2725
- if (key === "fields") {
2726
- output.value = value.split(",").map((s) => s.trim());
2727
- }
2728
- process.stdout.write(JSON.stringify(output) + "\n");
2729
- } else {
2730
- const scopeTag = options.org ? ` (org: ${options.org})` : "";
2731
- process.stdout.write(`Set "${key}" to "${value}"${scopeTag}
2732
- `);
2733
- }
2734
2794
  } catch (err) {
2735
2795
  const message = err instanceof Error ? err.message : String(err);
2736
2796
  process.stderr.write(`Error: ${message}
2737
2797
  `);
2738
2798
  process.exit(1);
2739
2799
  }
2800
+ const output = { key, value, scope: options.org ?? "default" };
2801
+ if (key === "fields") {
2802
+ output.value = value.split(",").map((s) => s.trim());
2803
+ }
2804
+ if (!options.json) {
2805
+ const scopeTag = options.org ? ` (org: ${options.org})` : "";
2806
+ process.stdout.write(`Set "${key}" to "${value}"${scopeTag}
2807
+ `);
2808
+ }
2809
+ const writeJson = () => {
2810
+ if (options.json) process.stdout.write(JSON.stringify(output) + "\n");
2811
+ };
2812
+ if (!options.org && key === "credentialStore" && loadConfig().credentialStore === "dpapi") {
2813
+ return offerCopy(options).then((outcome) => {
2814
+ output.credentialsCopied = outcome.copied;
2815
+ writeJson();
2816
+ });
2817
+ }
2818
+ writeJson();
2740
2819
  });
2741
2820
  const get = new Command4("get");
2742
2821
  get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields, markdown, credentialStore)").option("--org <org>", "read from an org-scoped configuration").option("--json", "output in JSON format").action((key, options) => {
@@ -5,7 +5,7 @@ import {
5
5
  lockPath,
6
6
  locksDir,
7
7
  refreshIfNeeded
8
- } from "./chunk-B76UX3CR.js";
8
+ } from "./chunk-RG45CRKX.js";
9
9
  export {
10
10
  _resetInFlight,
11
11
  classifyRefreshFailure,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.20.0-feature-043-dpapi-credential-store.742",
3
+ "version": "0.20.0-feature-043-dpapi-credential-store.744",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {