datex-studio-cli 0.4.11__tar.gz → 0.4.12__tar.gz

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.
Files changed (176) hide show
  1. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/PKG-INFO +1 -1
  2. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/pyproject.toml +2 -1
  3. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/__init__.py +1 -1
  4. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/cli.py +24 -1
  5. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/auth.py +34 -19
  6. datex_studio_cli-0.4.12/src/dxs/core/auth/datex_tenant.py +265 -0
  7. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/auth/msal_client.py +13 -3
  8. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/devops/client.py +40 -68
  9. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/dynamics/client.py +39 -67
  10. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/.gitignore +0 -0
  11. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/README.md +0 -0
  12. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/report-creator.skill +0 -0
  13. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/scripts/analyze_corpus.py +0 -0
  14. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/scripts/generate_rdlx_schema.py +0 -0
  15. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/__main__.py +0 -0
  16. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/__init__.py +0 -0
  17. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/api.py +0 -0
  18. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/branch.py +0 -0
  19. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/config.py +0 -0
  20. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/configuration.py +0 -0
  21. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/crm.py +0 -0
  22. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/datasource.py +0 -0
  23. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/devops.py +0 -0
  24. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/document.py +0 -0
  25. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/endpoint.py +0 -0
  26. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/env.py +0 -0
  27. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/explore.py +0 -0
  28. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/function.py +0 -0
  29. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/init.py +0 -0
  30. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/marketplace.py +0 -0
  31. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/odata.py +0 -0
  32. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/organization.py +0 -0
  33. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/proxy.py +0 -0
  34. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/release_notes.py +0 -0
  35. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/repo.py +0 -0
  36. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/__init__.py +0 -0
  37. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/add_cmds.py +0 -0
  38. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/api.py +0 -0
  39. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/batch.py +0 -0
  40. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/create.py +0 -0
  41. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/data.py +0 -0
  42. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/dataset.py +0 -0
  43. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/edit.py +0 -0
  44. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/folder.py +0 -0
  45. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/preview.py +0 -0
  46. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/schema_cmds.py +0 -0
  47. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/schema.py +0 -0
  48. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/servicepack.py +0 -0
  49. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/source.py +0 -0
  50. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/studio.py +0 -0
  51. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/telemetry.py +0 -0
  52. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/user.py +0 -0
  53. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/context.py +0 -0
  54. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/__init__.py +0 -0
  55. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/api/__init__.py +0 -0
  56. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/api/app_config.py +0 -0
  57. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/api/client.py +0 -0
  58. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/api/endpoints.py +0 -0
  59. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/api/metadata_models.py +0 -0
  60. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/api/models.py +0 -0
  61. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/auth/__init__.py +0 -0
  62. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/auth/decorators.py +0 -0
  63. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/auth/token_cache.py +0 -0
  64. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/cache.py +0 -0
  65. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/__init__.py +0 -0
  66. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/flow_generator.py +0 -0
  67. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/flow_models.py +0 -0
  68. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/generator.py +0 -0
  69. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/models.py +0 -0
  70. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/parsers.py +0 -0
  71. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/resolver.py +0 -0
  72. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/type_def_parser.py +0 -0
  73. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/validator.py +0 -0
  74. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/designer/__init__.py +0 -0
  75. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/designer/flow_models.py +0 -0
  76. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/designer/models.py +0 -0
  77. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/designer/parsers.py +0 -0
  78. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/devops/__init__.py +0 -0
  79. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/devops/helpers.py +0 -0
  80. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/devops/models.py +0 -0
  81. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/dynamics/__init__.py +0 -0
  82. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/dynamics/metadata_cache.py +0 -0
  83. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/footprint/__init__.py +0 -0
  84. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/footprint/client.py +0 -0
  85. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/footprint/edmx_parser.py +0 -0
  86. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/footprint/metadata.py +0 -0
  87. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/function/__init__.py +0 -0
  88. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/function/generator.py +0 -0
  89. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/function/models.py +0 -0
  90. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/function/validator.py +0 -0
  91. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/graph.py +0 -0
  92. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/onboarding.py +0 -0
  93. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/output/__init__.py +0 -0
  94. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/output/csv_fmt.py +0 -0
  95. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/output/formatter.py +0 -0
  96. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/output/json_fmt.py +0 -0
  97. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/output/redaction.py +0 -0
  98. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/output/yaml_fmt.py +0 -0
  99. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/release_notes.py +0 -0
  100. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/responses.py +0 -0
  101. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/telemetry/__init__.py +0 -0
  102. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/telemetry/config.py +0 -0
  103. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/telemetry/identity.py +0 -0
  104. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/telemetry/recorder.py +0 -0
  105. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/telemetry/scrubber.py +0 -0
  106. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/telemetry/sender.py +0 -0
  107. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/models/__init__.py +0 -0
  108. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/py.typed +0 -0
  109. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/__init__.py +0 -0
  110. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/datasource_binding.py +0 -0
  111. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/engine.py +0 -0
  112. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/field_parser.py +0 -0
  113. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/folder.py +0 -0
  114. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/manifest.py +0 -0
  115. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/models.py +0 -0
  116. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/preview.py +0 -0
  117. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/preview_arjs.py +0 -0
  118. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/schema.py +0 -0
  119. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/svg_renderer.py +0 -0
  120. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/templates/__init__.py +0 -0
  121. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
  122. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
  123. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/validate_arjs.py +0 -0
  124. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/validator.py +0 -0
  125. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/wrapper.py +0 -0
  126. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/__init__.py +0 -0
  127. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/click_options.py +0 -0
  128. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/clipboard.py +0 -0
  129. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/config.py +0 -0
  130. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/errors.py +0 -0
  131. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/filtering.py +0 -0
  132. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/image.py +0 -0
  133. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/paths.py +0 -0
  134. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/resolvers.py +0 -0
  135. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/responses.py +0 -0
  136. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/restricted.py +0 -0
  137. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/sorting.py +0 -0
  138. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/update_check.py +0 -0
  139. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/__init__.py +0 -0
  140. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/app.py +0 -0
  141. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/design.py +0 -0
  142. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/proxy_app.py +0 -0
  143. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/404/index.html +0 -0
  144. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/404.html +0 -0
  145. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_buildManifest.js +0 -0
  146. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_ssgManifest.js +0 -0
  147. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
  148. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
  149. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
  150. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/538-84e2e111415f1fda.js +0 -0
  151. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
  152. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
  153. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
  154. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
  155. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/app/_not-found/page-ea1be7001c230704.js +0 -0
  156. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/app/design/capture/page-a5e129c2d223432c.js +0 -0
  157. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/app/design/page-21ba66a3dd3b03f0.js +0 -0
  158. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/app/layout-2cc6eac09e476c91.js +0 -0
  159. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/app/page-20c358395882cdac.js +0 -0
  160. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
  161. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
  162. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/main-app-b69998d8941231d8.js +0 -0
  163. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +0 -0
  164. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +0 -0
  165. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
  166. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
  167. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
  168. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/design/capture/index.html +0 -0
  169. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/design/capture/index.txt +0 -0
  170. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/design/index.html +0 -0
  171. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/design/index.txt +0 -0
  172. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/index.html +0 -0
  173. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/index.txt +0 -0
  174. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/render-cli.js +0 -0
  175. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/report-preview.html +0 -0
  176. {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/report-validate.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datex-studio-cli
3
- Version: 0.4.11
3
+ Version: 0.4.12
4
4
  Summary: CLI for Datex Studio low-code platform, designed for LLM-based AI agents
5
5
  Project-URL: Homepage, https://github.com/datex/datex-studio-cli
6
6
  Project-URL: Documentation, https://github.com/datex/datex-studio-cli
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "datex-studio-cli"
7
- version = "0.4.11"
7
+ version = "0.4.12"
8
8
  description = "CLI for Datex Studio low-code platform, designed for LLM-based AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -152,6 +152,7 @@ python_functions = ["test_*"]
152
152
  addopts = "-v --tb=short"
153
153
  markers = [
154
154
  "fidelity: cross-engine SSIM fidelity tests (require agent-browser + ARJS)",
155
+ "internal_commands_gated: exercise the devops/crm visibility gate (no DXS_INTERNAL_COMMANDS override)",
155
156
  ]
156
157
 
157
158
  [tool.coverage.run]
@@ -1,4 +1,4 @@
1
1
  """Datex Studio CLI - Command-line interface for Datex Studio platform."""
2
2
 
3
- __version__ = "0.4.11"
3
+ __version__ = "0.4.12"
4
4
  __app_name__ = "dxs"
@@ -8,6 +8,7 @@ import click
8
8
 
9
9
  from dxs import __app_name__, __version__
10
10
  from dxs.context import DxsContext, pass_context # re-exported for backward compat
11
+ from dxs.core.auth.datex_tenant import DATEX_INTERNAL_COMMANDS, internal_commands_available
11
12
  from dxs.core.output import OutputFormat, format_error
12
13
  from dxs.core.output.redaction import (
13
14
  AUTHOR_VERBOSE_FIELDS as _AUTHOR_VERBOSE_FIELDS, # noqa: F401 — re-exported
@@ -77,7 +78,29 @@ CONTEXT_SETTINGS = {
77
78
 
78
79
 
79
80
  class DxsGroup(click.Group):
80
- """Custom Group that shows global options in subcommand help."""
81
+ """Custom Group that shows global options in subcommand help.
82
+
83
+ Also gates the Datex-internal command groups (``devops``, ``crm``). The gate
84
+ is applied at lookup time rather than at registration so it reflects the
85
+ identity cache as of *this* invocation — registration happens once at import.
86
+ """
87
+
88
+ def _is_gated(self, cmd_name: str) -> bool:
89
+ """Whether ``cmd_name`` is Datex-internal and unavailable to this user."""
90
+ return cmd_name in DATEX_INTERNAL_COMMANDS and not internal_commands_available()
91
+
92
+ def list_commands(self, ctx: click.Context) -> list[str]:
93
+ """List visible subcommands, omitting unavailable Datex-internal groups."""
94
+ return [name for name in super().list_commands(ctx) if not self._is_gated(name)]
95
+
96
+ def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
97
+ """Resolve a subcommand, refusing unavailable Datex-internal groups."""
98
+ if self._is_gated(cmd_name):
99
+ ctx.fail(
100
+ f"'{cmd_name}' is a Datex-internal command group and requires a "
101
+ "Datex tenant identity. Run 'dxs auth login' with your Datex account."
102
+ )
103
+ return super().get_command(ctx, cmd_name)
81
104
 
82
105
  def format_options(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
83
106
  """Write options to the formatter, including global options."""
@@ -8,6 +8,7 @@ import click
8
8
 
9
9
  from dxs.context import DxsContext, pass_context
10
10
  from dxs.core.auth import MSALClient, MultiIdentityTokenCache
11
+ from dxs.core.auth.datex_tenant import claims_are_datex, is_datex_identity
11
12
  from dxs.core.auth.decorators import get_access_token
12
13
  from dxs.core.auth.token_cache import CachedIdentity, get_identity_override
13
14
  from dxs.core.footprint import get_footprint_token
@@ -78,23 +79,33 @@ def run_standard_device_login(
78
79
  consent_cb = on_additional_consent_needed or _on_consent
79
80
 
80
81
  ctx.log("Initiating device code authentication...")
82
+ additional_scope_sets: Any
81
83
  if tenant_id is not None:
82
84
  client = MSALClient.for_tenant(tenant_id, scopes=settings.azure_scopes)
83
85
  # External tenants only need Studio API + Footprint; DevOps/CRM are not relevant.
84
86
  additional_scope_sets = [[settings.footprint_scope]]
85
87
  else:
86
88
  client = MSALClient(scopes=settings.azure_scopes)
87
- additional_scope_sets = [
88
- scopes
89
- for scopes in [
90
- settings.azure_devops_scopes,
91
- settings.dynamics_crm_scopes,
92
- settings.dynamics_crm_admin_scopes,
93
- settings.data_export_service_scopes,
94
- [settings.footprint_scope],
95
- ]
96
- if scopes
97
- ]
89
+
90
+ def additional_scope_sets(token_response: dict[str, Any]) -> list[list[str]]:
91
+ """Choose consent scopes from the claims of whoever just signed in.
92
+
93
+ The home-tenant path is also what customers hit before their own
94
+ tenant is known, so DevOps/CRM consent is requested only for Datex
95
+ staff — a customer must never be prompted for Datex-internal
96
+ resources by name.
97
+ """
98
+ internal = (
99
+ [
100
+ settings.azure_devops_scopes,
101
+ settings.dynamics_crm_scopes,
102
+ settings.dynamics_crm_admin_scopes,
103
+ settings.data_export_service_scopes,
104
+ ]
105
+ if claims_are_datex(token_response.get("id_token_claims") or {})
106
+ else []
107
+ )
108
+ return [scopes for scopes in [*internal, [settings.footprint_scope]] if scopes]
98
109
 
99
110
  result = client.authenticate_device_code_multi_resource(
100
111
  primary_scopes=settings.azure_scopes,
@@ -638,17 +649,21 @@ def status(ctx: DxsContext) -> None:
638
649
  "name": "Datex Studio API",
639
650
  "scopes": settings.azure_scopes,
640
651
  },
641
- "azure_devops": {
642
- "name": "Azure DevOps API",
643
- "scopes": settings.azure_devops_scopes,
644
- },
645
652
  }
646
653
 
647
- if settings.dynamics_crm_scopes:
648
- resources["dynamics_crm"] = {
649
- "name": "Dynamics CRM API",
650
- "scopes": settings.dynamics_crm_scopes,
654
+ # DevOps and CRM are Datex-internal. Probing them with a customer identity
655
+ # only ever yields noise, so omit those rows entirely unless the identity
656
+ # being reported is a Datex one.
657
+ if is_datex_identity(active_identity):
658
+ resources["azure_devops"] = {
659
+ "name": "Azure DevOps API",
660
+ "scopes": settings.azure_devops_scopes,
651
661
  }
662
+ if settings.dynamics_crm_scopes:
663
+ resources["dynamics_crm"] = {
664
+ "name": "Dynamics CRM API",
665
+ "scopes": settings.dynamics_crm_scopes,
666
+ }
652
667
 
653
668
  resources["footprint"] = {
654
669
  "name": "Footprint API",
@@ -0,0 +1,265 @@
1
+ """Datex-tenant (staff-only) identity resolution.
2
+
3
+ Azure DevOps and Dynamics CRM are Datex-internal resources: they live in the
4
+ Datex Entra tenant and only Datex staff can reach them. Two consequences drive
5
+ this module.
6
+
7
+ **Customers must never see them.** A customer authenticates either against their
8
+ own tenant (``external_entra_id``) or, for organizations that do not require
9
+ external auth, as a B2B guest inside the Datex tenant. Neither can use
10
+ ``dxs devops`` or ``dxs crm``, so those groups are hidden from ``--help`` and
11
+ their token rows are omitted from ``dxs auth status``.
12
+
13
+ **Staff must keep them across an identity switch.** ``dxs auth switch CAG``
14
+ makes a customer identity active, but the cached Datex identity is still valid;
15
+ DevOps/CRM tokens are therefore minted from *that* identity rather than from
16
+ whichever one happens to be active. The rotated refresh token is written back
17
+ onto the Datex identity so it stays alive while work happens elsewhere.
18
+ """
19
+
20
+ import os
21
+ from collections.abc import Mapping
22
+ from typing import Any
23
+
24
+ from dxs.core.auth.msal_client import MSALClient
25
+ from dxs.core.auth.token_cache import (
26
+ CachedIdentity,
27
+ MultiIdentityTokenCache,
28
+ get_identity_override,
29
+ )
30
+ from dxs.utils.config import get_settings
31
+ from dxs.utils.errors import AuthenticationError
32
+
33
+ # Command groups that require a Datex-tenant identity.
34
+ DATEX_INTERNAL_COMMANDS = frozenset({"crm", "devops"})
35
+
36
+ # Entra stamps B2B guest UPNs with this marker (``user_theirdomain.com#EXT#@…``).
37
+ # It is the signal that separates a customer invited into the Datex tenant from
38
+ # an actual Datex staff member, since both carry the Datex ``tid``.
39
+ GUEST_UPN_MARKER = "#EXT#"
40
+
41
+ # Escape hatch for support and CI: force the internal command groups on or off
42
+ # regardless of the cached identities.
43
+ INTERNAL_COMMANDS_ENV = "DXS_INTERNAL_COMMANDS"
44
+
45
+ # Pre-acquired resource tokens injected by an external host (e.g. Sidekick).
46
+ # Their presence means the caller has already arranged internal access.
47
+ INJECTED_RESOURCE_TOKEN_ENV = ("DXS_DEVOPS_TOKEN", "DXS_DYNAMICS_TOKEN")
48
+
49
+ _TRUTHY = {"1", "true", "yes", "on"}
50
+ _FALSY = {"0", "false", "no", "off"}
51
+
52
+
53
+ def _home_tenant_id() -> str:
54
+ """The Datex tenant ID (``azure_tenant_id``, the CLI's configured home tenant)."""
55
+ return get_settings().azure_tenant_id
56
+
57
+
58
+ def _is_guest_upn(username: str | None) -> bool:
59
+ """Whether a username is a B2B guest UPN rather than a native account."""
60
+ return GUEST_UPN_MARKER in (username or "").upper()
61
+
62
+
63
+ def is_datex_identity(identity: CachedIdentity) -> bool:
64
+ """Whether ``identity`` is a Datex staff identity.
65
+
66
+ Requires the Datex tenant *and* a native (non-guest) account. The tenant
67
+ check alone is not enough: organizations with ``externalEntraId=false``
68
+ authenticate their users against the Datex tenant as guests, so they would
69
+ otherwise be mistaken for staff.
70
+ """
71
+ if identity.external_entra_id:
72
+ return False
73
+ if not identity.tenant_id or identity.tenant_id.lower() != _home_tenant_id().lower():
74
+ return False
75
+ return not _is_guest_upn(identity.account_username)
76
+
77
+
78
+ def resolve_datex_identity(
79
+ cache: MultiIdentityTokenCache | None = None,
80
+ ) -> tuple[str, CachedIdentity] | None:
81
+ """Find the cached Datex identity, whatever identity is currently active.
82
+
83
+ Prefers the explicitly selected identity (``--tenant`` override, else the
84
+ persisted active one) when it is itself a Datex identity, so staff with more
85
+ than one Datex account keep control over which is used. Otherwise picks a
86
+ usable one, favouring identities that still hold a refresh token.
87
+
88
+ Args:
89
+ cache: Token cache to read. Defaults to the real credentials file.
90
+
91
+ Returns:
92
+ ``(storage_key, identity)`` for the chosen identity, or None when the
93
+ cache holds no Datex identity.
94
+ """
95
+ cache = cache or MultiIdentityTokenCache()
96
+
97
+ candidates = [
98
+ (key, identity)
99
+ for key, identity, _is_active in cache.list_identities()
100
+ if is_datex_identity(identity)
101
+ ]
102
+ if not candidates:
103
+ return None
104
+
105
+ selected_key = get_identity_override()
106
+ if selected_key is None:
107
+ selected_key = next(
108
+ (key for key, _identity, is_active in cache.list_identities() if is_active), None
109
+ )
110
+ for key, identity in candidates:
111
+ if key == selected_key:
112
+ return (key, identity)
113
+
114
+ # No selected Datex identity — prefer one that can still mint tokens.
115
+ candidates.sort(key=lambda c: (not bool(c[1].refresh_token), c[1].is_expired(), c[0]))
116
+ return candidates[0]
117
+
118
+
119
+ def has_datex_identity(cache: MultiIdentityTokenCache | None = None) -> bool:
120
+ """Whether any cached identity belongs to the Datex tenant."""
121
+ return resolve_datex_identity(cache) is not None
122
+
123
+
124
+ def claims_are_datex(claims: Mapping[str, Any]) -> bool:
125
+ """Whether a set of token claims describes a Datex staff account.
126
+
127
+ The claims equivalent of :func:`is_datex_identity`, for deciding before an
128
+ identity has been persisted — notably at login, where the consent prompts a
129
+ user sees must not mention Datex-internal resources.
130
+ """
131
+ tenant_id = claims.get("tid")
132
+ if not tenant_id or str(tenant_id).lower() != _home_tenant_id().lower():
133
+ return False
134
+ username = claims.get("upn") or claims.get("preferred_username") or claims.get("email")
135
+ return not _is_guest_upn(username)
136
+
137
+
138
+ def _injected_identity_is_datex() -> bool | None:
139
+ """Classify an injected ``DXS_ACCESS_TOKEN`` from its own claims.
140
+
141
+ Returns None when no access token is injected, so the caller can fall back
142
+ to inspecting the credentials file.
143
+ """
144
+ token = os.environ.get("DXS_ACCESS_TOKEN")
145
+ if not token:
146
+ return None
147
+
148
+ from dxs.core.auth.decorators import _decode_jwt_payload
149
+
150
+ return claims_are_datex(_decode_jwt_payload(token) or {})
151
+
152
+
153
+ def internal_commands_available() -> bool:
154
+ """Whether the Datex-internal command groups should be exposed.
155
+
156
+ Resolution order: the ``DXS_INTERNAL_COMMANDS`` override, an explicitly
157
+ injected resource token, the claims of an injected ``DXS_ACCESS_TOKEN``,
158
+ then the cached identities.
159
+ """
160
+ override = os.environ.get(INTERNAL_COMMANDS_ENV, "").strip().lower()
161
+ if override in _TRUTHY:
162
+ return True
163
+ if override in _FALSY:
164
+ return False
165
+
166
+ if any(os.environ.get(var) for var in INJECTED_RESOURCE_TOKEN_ENV):
167
+ return True
168
+
169
+ injected = _injected_identity_is_datex()
170
+ if injected is not None:
171
+ return injected
172
+
173
+ try:
174
+ return has_datex_identity()
175
+ except Exception: # noqa: BLE001 - visibility must never break help rendering
176
+ return False
177
+
178
+
179
+ def _no_datex_identity_error(resource: str) -> AuthenticationError:
180
+ """Build the 'this resource needs a Datex login' error."""
181
+ return AuthenticationError(
182
+ message=(
183
+ f"{resource} requires a Datex tenant identity, and none is cached. "
184
+ "This command group is only available to Datex staff."
185
+ ),
186
+ code="DXS-AUTH-020",
187
+ suggestions=[
188
+ "Run 'dxs auth login' with your Datex account",
189
+ "Your active identity does not need to change — the Datex identity is "
190
+ "used for this resource automatically",
191
+ ],
192
+ )
193
+
194
+
195
+ def acquire_datex_resource_token(
196
+ scopes: list[str],
197
+ *,
198
+ resource: str,
199
+ error_code: str,
200
+ extra_suggestions: list[str] | None = None,
201
+ ) -> str:
202
+ """Mint an access token for a Datex-internal resource.
203
+
204
+ Redeems the cached Datex identity's refresh token against the Datex tenant
205
+ authority, independently of which identity is active, and persists the
206
+ rotated refresh token back onto that identity.
207
+
208
+ Args:
209
+ scopes: OAuth scopes for the target resource.
210
+ resource: Human-readable resource name, used in error messages.
211
+ error_code: Error code to attach when acquisition fails.
212
+ extra_suggestions: Additional suggestions for the failure path.
213
+
214
+ Returns:
215
+ An access token for ``scopes``.
216
+
217
+ Raises:
218
+ AuthenticationError: If no Datex identity is cached, or the refresh
219
+ token could not be redeemed.
220
+ """
221
+ cache = MultiIdentityTokenCache()
222
+ resolved = resolve_datex_identity(cache)
223
+ if resolved is None:
224
+ raise _no_datex_identity_error(resource)
225
+
226
+ identity_key, identity = resolved
227
+ if not identity.refresh_token:
228
+ raise AuthenticationError(
229
+ message=(
230
+ f"Could not acquire {resource} token: the cached Datex identity "
231
+ f"({identity_key}) has no refresh token."
232
+ ),
233
+ code=error_code,
234
+ details={"error": "no_refresh_token", "identity": identity_key},
235
+ suggestions=["Run 'dxs auth login' with your Datex account"],
236
+ )
237
+
238
+ # Datex identities are always home-tenant, so the default authority applies.
239
+ client = MSALClient(scopes=scopes)
240
+ result: dict[str, Any] = client.refresh_access_token_detailed(
241
+ refresh_token=identity.refresh_token, scopes=scopes
242
+ )
243
+
244
+ if "access_token" in result:
245
+ rotated = result.get("refresh_token")
246
+ if rotated:
247
+ # Keep the Datex identity alive even though it is not the active one.
248
+ cache.update_refresh_token(rotated, username=identity_key)
249
+ return str(result["access_token"])
250
+
251
+ error = result.get("error", "unknown_error")
252
+ error_description = result.get("error_description") or "Token acquisition failed."
253
+ raise AuthenticationError(
254
+ message=f"Could not acquire {resource} token: {error_description}",
255
+ code=error_code,
256
+ details={
257
+ "error": error,
258
+ "error_description": error_description,
259
+ "identity": identity_key,
260
+ },
261
+ suggestions=[
262
+ "Run 'dxs auth login' with your Datex account to refresh credentials",
263
+ *(extra_suggestions or []),
264
+ ],
265
+ )
@@ -151,7 +151,9 @@ class MSALClient:
151
151
  self,
152
152
  primary_scopes: list[str],
153
153
  additional_scopes: list[str] | None = None,
154
- additional_scope_sets: list[list[str]] | None = None,
154
+ additional_scope_sets: list[list[str]]
155
+ | Callable[[dict[str, Any]], list[list[str]]]
156
+ | None = None,
155
157
  on_device_code: Callable[[dict[str, Any]], None] | None = None,
156
158
  on_additional_consent_needed: Callable[[str, list[str]], None] | None = None,
157
159
  ) -> dict[str, Any]:
@@ -164,7 +166,11 @@ class MSALClient:
164
166
  Args:
165
167
  primary_scopes: Scopes for primary resource (e.g., Datex API)
166
168
  additional_scopes: Scopes for a single additional resource (deprecated, use additional_scope_sets)
167
- additional_scope_sets: List of scope lists for multiple additional resources
169
+ additional_scope_sets: List of scope lists for multiple additional
170
+ resources, or a callable receiving the primary token response and
171
+ returning that list. The callable form lets the caller choose
172
+ which resources to request consent for based on *who* just
173
+ authenticated — the identity is not known until then.
168
174
  on_device_code: Callback to handle the device code response
169
175
  on_additional_consent_needed: Callback when additional consent is needed (resource_name, scopes)
170
176
 
@@ -222,7 +228,11 @@ class MSALClient:
222
228
  details={"error": error, "error_description": error_desc},
223
229
  )
224
230
 
225
- # Build list of all additional scope sets to acquire
231
+ # Build list of all additional scope sets to acquire. A callable is
232
+ # resolved now that the primary identity's claims are known.
233
+ if callable(additional_scope_sets):
234
+ additional_scope_sets = additional_scope_sets(result)
235
+
226
236
  all_additional_scopes: list[list[str]] = []
227
237
  if additional_scopes:
228
238
  all_additional_scopes.append(additional_scopes)
@@ -16,14 +16,14 @@ DEVOPS_RESOURCE_ID = "499b84ac-1321-427f-aa17-267ca6975798"
16
16
  def get_devops_token() -> str:
17
17
  """Get an OAuth token for Azure DevOps API.
18
18
 
19
- Uses the existing MSAL client to acquire a token with the
20
- Azure DevOps user_impersonation scope.
19
+ Azure DevOps is a Datex-internal resource, so the token is minted from the
20
+ cached Datex-tenant identity rather than from whichever identity is active.
21
+ That keeps ``dxs devops`` working after ``dxs auth switch <customer org>``.
21
22
 
22
23
  Priority:
23
24
  0. DXS_DEVOPS_TOKEN environment variable (direct token from external auth)
24
25
  1. DXS_REFRESH_TOKEN environment variable (OBO flow)
25
- 2. MSAL silent acquisition (cached token)
26
- 3. File-based refresh token
26
+ 2. The cached Datex identity's refresh token
27
27
 
28
28
  Returns:
29
29
  Access token for Azure DevOps API
@@ -33,8 +33,8 @@ def get_devops_token() -> str:
33
33
  """
34
34
  import os
35
35
 
36
+ from dxs.core.auth.datex_tenant import acquire_datex_resource_token
36
37
  from dxs.core.auth.msal_client import MSALClient
37
- from dxs.core.auth.token_cache import MultiIdentityTokenCache
38
38
 
39
39
  # Priority 0: DXS_DEVOPS_TOKEN environment variable (direct token)
40
40
  # Used when an external tool (e.g., Sidekick) provides a pre-acquired DevOps token
@@ -48,13 +48,10 @@ def get_devops_token() -> str:
48
48
  if not devops_scopes:
49
49
  devops_scopes = [f"{DEVOPS_RESOURCE_ID}/user_impersonation"]
50
50
 
51
- # Track the error from env token attempt for better error reporting
52
- env_token_error: str | None = None
53
- env_token_error_desc: str | None = None
54
-
55
51
  # Priority 1: DXS_REFRESH_TOKEN environment variable (OBO flow)
56
52
  env_refresh_token = os.environ.get("DXS_REFRESH_TOKEN")
57
53
  if env_refresh_token:
54
+ env_error: dict[str, Any]
58
55
  try:
59
56
  msal_client = MSALClient(scopes=devops_scopes)
60
57
  result = msal_client._app.acquire_token_by_refresh_token(
@@ -65,67 +62,42 @@ def get_devops_token() -> str:
65
62
  return cast(str, result["access_token"])
66
63
  # Capture MSAL error response for reporting
67
64
  if result:
68
- env_token_error = result.get("error", "unknown_error")
69
- env_token_error_desc = result.get(
70
- "error_description", "OBO token acquisition failed"
71
- )
65
+ env_error = {
66
+ "error": result.get("error", "unknown_error"),
67
+ "error_description": result.get(
68
+ "error_description", "OBO token acquisition failed"
69
+ ),
70
+ }
72
71
  else:
73
- env_token_error = "no_result"
74
- env_token_error_desc = "No response from OBO token refresh"
72
+ env_error = {
73
+ "error": "no_result",
74
+ "error_description": "No response from OBO token refresh",
75
+ }
75
76
  except Exception as e:
76
- env_token_error = "exception"
77
- env_token_error_desc = f"OBO token acquisition failed: {e}"
78
-
79
- # Priority 2: Try to get token from MSAL silent acquisition.
80
- # External-tenant identities must use their own tenant authority; the home
81
- # authority returns invalid_grant for their refresh tokens.
82
- cache = MultiIdentityTokenCache()
83
- identity = cache.get_active_identity()
84
- if identity and identity.external_entra_id and identity.tenant_id:
85
- msal_client = MSALClient.for_tenant(tenant_id=identity.tenant_id, scopes=devops_scopes)
86
- else:
87
- msal_client = MSALClient(scopes=devops_scopes)
88
- result = msal_client.acquire_token_silent()
89
- if result and "access_token" in result:
90
- return cast(str, result["access_token"])
91
-
92
- # Priority 3: Try using the stored refresh token from file cache
93
- if identity and identity.refresh_token:
94
- try:
95
- result = msal_client._app.acquire_token_by_refresh_token(
96
- refresh_token=identity.refresh_token,
97
- scopes=devops_scopes,
98
- )
99
- if result and "access_token" in result:
100
- return cast(str, result["access_token"])
101
-
102
- error = result.get("error") if result else "no_result"
103
- error_desc = (
104
- result.get("error_description") if result else "No response from token refresh"
105
- )
106
- except Exception as e:
107
- error = "exception"
108
- error_desc = str(e)
109
- else:
110
- # If env token was tried but failed, report that error instead
111
- if env_token_error:
112
- error = env_token_error
113
- error_desc = env_token_error_desc
114
- else:
115
- error = "no_refresh_token"
116
- error_desc = (
117
- "No refresh token available (set DXS_REFRESH_TOKEN or run 'dxs auth login')"
118
- )
119
-
120
- raise AuthenticationError(
121
- message=f"Could not acquire Azure DevOps token: {error_desc}",
122
- code="DXS-DEVOPS-AUTH-001",
123
- details={"error": error, "error_description": error_desc},
124
- suggestions=[
125
- "Set DXS_REFRESH_TOKEN environment variable for OBO flow",
126
- "Run 'dxs auth login' to authenticate with refresh token",
127
- "Ensure your account has Azure DevOps access",
128
- ],
77
+ env_error = {
78
+ "error": "exception",
79
+ "error_description": f"OBO token acquisition failed: {e}",
80
+ }
81
+
82
+ # An injected refresh token is an explicit instruction about which
83
+ # credential to use; do not silently fall back to a cached identity.
84
+ raise AuthenticationError(
85
+ message=f"Could not acquire Azure DevOps token: {env_error['error_description']}",
86
+ code="DXS-DEVOPS-AUTH-001",
87
+ details=env_error,
88
+ suggestions=[
89
+ "Verify DXS_REFRESH_TOKEN is current and consented for Azure DevOps",
90
+ "Run 'dxs auth login' to authenticate with refresh token",
91
+ "Ensure your account has Azure DevOps access",
92
+ ],
93
+ )
94
+
95
+ # Priority 2: The cached Datex identity, whichever identity is active.
96
+ return acquire_datex_resource_token(
97
+ devops_scopes,
98
+ resource="Azure DevOps",
99
+ error_code="DXS-DEVOPS-AUTH-001",
100
+ extra_suggestions=["Ensure your account has Azure DevOps access"],
129
101
  )
130
102
 
131
103