@metabase/cli 0.2.0 → 0.2.1-alpha.bcm-ghy-4075-spawn-default-profile.eb120d1

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 (210) hide show
  1. package/README.md +222 -10
  2. package/dist/add-collection-CCpC-GUz.mjs +9 -0
  3. package/dist/{add-collection-BTSGp4f9.mjs → add-collection-CLXfrgp4.mjs} +4 -4
  4. package/dist/{append-DY1-O789.mjs → append-DMcxP1LM.mjs} +5 -5
  5. package/dist/archive-4WqMsPWB.mjs +38 -0
  6. package/dist/{archive-CWtUzlHO.mjs → archive-5inmbMDL.mjs} +4 -4
  7. package/dist/{archive-CJ9rElKn.mjs → archive-BggM_AuL.mjs} +5 -5
  8. package/dist/{archive-pHsa4dL0.mjs → archive-BmhehbS0.mjs} +3 -3
  9. package/dist/{archive-DCPw3df6.mjs → archive-BwIZzWGe.mjs} +4 -4
  10. package/dist/{archive-DOa1u1aJ.mjs → archive-DErhQn8A.mjs} +4 -4
  11. package/dist/archive-DTnz1suV.mjs +38 -0
  12. package/dist/{archive-w6TX-zEk.mjs → archive-Q1387APe.mjs} +4 -4
  13. package/dist/{archive-BOEEMrYL.mjs → archive-h-3H5b4K.mjs} +5 -5
  14. package/dist/{auth-BrQtY0pO.mjs → auth-DzCAiArV.mjs} +4 -4
  15. package/dist/{body-C1_AaTS4.mjs → body-5snfmiwR.mjs} +3 -3
  16. package/dist/{branches-BsBzaGIf.mjs → branches-PL170CYS.mjs} +4 -4
  17. package/dist/{cancel-BNM0yyx-.mjs → cancel-EFKcF5J_.mjs} +3 -3
  18. package/dist/{cancel-task-Bggzn4JL.mjs → cancel-task-mFFx0d5m.mjs} +4 -4
  19. package/dist/{capabilities-DrDJo_ng.mjs → capabilities-CiaAfUgX.mjs} +1 -1
  20. package/dist/{card-DHCcmpgr.mjs → card-BsfdXK0O.mjs} +6 -6
  21. package/dist/{cards-D0hvA13L.mjs → cards-BI7zKb9M.mjs} +4 -4
  22. package/dist/cli.mjs +29 -26
  23. package/dist/{collection-D0O9gp3S.mjs → collection-2h_by9Ue.mjs} +6 -6
  24. package/dist/{collection-CFoLPrY5.mjs → collection-C52Ndi_O.mjs} +4 -2
  25. package/dist/{collection-namespace-DqPNrCdQ.mjs → collection-namespace-uNA5WhLB.mjs} +2 -2
  26. package/dist/{create-Cii_utzV.mjs → create-BLD0YniB.mjs} +5 -4
  27. package/dist/create-BP8EIWrP.mjs +161 -0
  28. package/dist/{create-Cyv7vHV_.mjs → create-BpizGzgQ.mjs} +5 -5
  29. package/dist/create-BxCsAdqc.mjs +45 -0
  30. package/dist/{create-CZ9KFnbi.mjs → create-CBKT7SE_.mjs} +5 -5
  31. package/dist/{create-C17Fh1_X.mjs → create-CQ32FwVe.mjs} +8 -7
  32. package/dist/{create-DKuixGJ2.mjs → create-CU3bYpsT.mjs} +5 -5
  33. package/dist/create-DAH24ltu.mjs +45 -0
  34. package/dist/{create-a3s1S2Dw.mjs → create-DutkaD7P.mjs} +7 -7
  35. package/dist/{create-E6L3WMqV.mjs → create-FuZa4j1N.mjs} +5 -5
  36. package/dist/{create-BR7x66KW.mjs → create-KveE3j3p.mjs} +8 -8
  37. package/dist/{create-C8ndg3g4.mjs → create-VrSowEbj.mjs} +6 -6
  38. package/dist/{create-oLb0E-ZP.mjs → create-acWKw4Y1.mjs} +7 -7
  39. package/dist/{create-branch-Dxn6UnV_.mjs → create-branch-CP1Y4_El.mjs} +4 -4
  40. package/dist/{create-05rX4oL-.mjs → create-xPfmI1Tu.mjs} +7 -7
  41. package/dist/{csv-T6_vlLJS.mjs → csv-CZyYi0vi.mjs} +5 -5
  42. package/dist/{current-task-DxWbw-bU.mjs → current-task-BR4koo9z.mjs} +4 -4
  43. package/dist/{dashboard-BpSPoeAn.mjs → dashboard-ARgJfamb.mjs} +8 -8
  44. package/dist/{database-CIbVSfua.mjs → database-BOqzcexD.mjs} +1 -1
  45. package/dist/{db-DBRijnh4.mjs → db-dbnxMPSR.mjs} +6 -6
  46. package/dist/{delete--P1xFX37.mjs → delete-BpL3vaaT.mjs} +5 -5
  47. package/dist/{delete-Ce2EBQYA.mjs → delete-C5DqDwev.mjs} +5 -5
  48. package/dist/delete-DCDFLHFa.mjs +49 -0
  49. package/dist/delete-P2Qlsr_s.mjs +49 -0
  50. package/dist/{delete-BBOIUl0D.mjs → delete-ea_ns1uM.mjs} +5 -5
  51. package/dist/{delete-runtime-B-8I9V1g.mjs → delete-runtime-DdBn6poA.mjs} +2 -2
  52. package/dist/{delete-table-Bj32NsXn.mjs → delete-table-tM048btr.mjs} +5 -5
  53. package/dist/{dependencies-ixzjVjUz.mjs → dependencies-DdOTClO2.mjs} +4 -4
  54. package/dist/destroy-B51EPvOJ.mjs +168 -0
  55. package/dist/{dirty-CH70h5m5.mjs → dirty-CEQYmhNF.mjs} +4 -4
  56. package/dist/{document-dZpuFu3I.mjs → document-DR1FLOn9.mjs} +5 -5
  57. package/dist/{eid-XjkBbyb_.mjs → eid-CtKGjAkz.mjs} +4 -4
  58. package/dist/events-C26DZ6yZ.mjs +44 -0
  59. package/dist/{export-CChMg-mL.mjs → export-CippuWZJ.mjs} +5 -8
  60. package/dist/export-Cy2DFtR2.mjs +10 -0
  61. package/dist/{field-B1heFeCz.mjs → field-CXDndCbB.mjs} +4 -4
  62. package/dist/{fields-C7h2QwUp.mjs → fields-Xm_HVNcX.mjs} +4 -4
  63. package/dist/{get-RlP_xiUd.mjs → get-3O7hv4zv.mjs} +5 -5
  64. package/dist/{get-Cj90oyxG.mjs → get-B0cq1uGy.mjs} +4 -4
  65. package/dist/{get-BWHJDa0B.mjs → get-B90bXbpw.mjs} +4 -4
  66. package/dist/{get-DWWmpQ87.mjs → get-BQBu2c94.mjs} +4 -4
  67. package/dist/{get-BsyccuCH.mjs → get-BQz45IBF.mjs} +5 -4
  68. package/dist/get-BSfKWZNJ.mjs +35 -0
  69. package/dist/get-BdNC8WM8.mjs +38 -0
  70. package/dist/{get-D9r2znrn.mjs → get-Bj66bcuU.mjs} +4 -4
  71. package/dist/{get-DRZENkCj.mjs → get-BrKqMM1d.mjs} +4 -4
  72. package/dist/{get-cyR67Lmq.mjs → get-CjZWjRa5.mjs} +4 -4
  73. package/dist/{get-kqY2lwhO.mjs → get-CwGqARAf.mjs} +4 -4
  74. package/dist/{get-B6ZJCJ8H.mjs → get-D5crrFSf.mjs} +4 -4
  75. package/dist/{get-AvGH_oph.mjs → get-DTkvGu_J.mjs} +3 -3
  76. package/dist/{get-KVN9ANVh.mjs → get-DX2fp6BB.mjs} +3 -3
  77. package/dist/get-DYuJVlP-.mjs +35 -0
  78. package/dist/{get-DrWYvefc.mjs → get-FIE88hj9.mjs} +4 -4
  79. package/dist/{get-C98IFgFi.mjs → get-Ihx_XaaI.mjs} +3 -3
  80. package/dist/{get-run-C4C_QQqO.mjs → get-run-lVwhQVFx.mjs} +4 -4
  81. package/dist/{get-Dg1J0wxb.mjs → get-z0BKkiRX.mjs} +4 -4
  82. package/dist/git-sync-CmB4YBmS.mjs +31 -0
  83. package/dist/{has-remote-changes-dLUGr0MD.mjs → has-remote-changes-DHrYSnTg.mjs} +4 -4
  84. package/dist/{import-Byull6jF.mjs → import-BU40xXSF.mjs} +6 -6
  85. package/dist/{input-m9ctfLxZ.mjs → input-BGyHn0Kr.mjs} +1 -1
  86. package/dist/{is-dirty-CU3ovgdG.mjs → is-dirty-BPluqPPP.mjs} +3 -3
  87. package/dist/is-dirty-DOy1TRcf.mjs +8 -0
  88. package/dist/{items-CEGDYWuF.mjs → items-BZpYkImP.mjs} +6 -6
  89. package/dist/{key-Ge4VwWvS.mjs → key-B8UAsQmB.mjs} +1 -1
  90. package/dist/{library-DHWWa4Tt.mjs → library-BGWDIfEM.mjs} +4 -4
  91. package/dist/list-B037kJtH.mjs +39 -0
  92. package/dist/{list-BuyRG3GB.mjs → list-B7Ihu6XI.mjs} +4 -4
  93. package/dist/{list-BtcpNl4-.mjs → list-BCeZlfpQ.mjs} +3 -3
  94. package/dist/{list-Bpv06V24.mjs → list-Bct00qku.mjs} +4 -4
  95. package/dist/{list-D9FY30xH.mjs → list-Be8xMtS3.mjs} +3 -3
  96. package/dist/{list-3z1ZChTG.mjs → list-CjBPVJqd.mjs} +3 -3
  97. package/dist/{list-CmCJwaGT.mjs → list-CoqHnusK.mjs} +3 -3
  98. package/dist/{list-O2w5xd1q.mjs → list-CvaoHnl-.mjs} +3 -3
  99. package/dist/{list-BliCJAjt.mjs → list-DamRE5Ds.mjs} +3 -3
  100. package/dist/{list-Br48kL09.mjs → list-DmopauvJ.mjs} +3 -3
  101. package/dist/{list-th_Ym6Cv.mjs → list-M_1sdYNH.mjs} +4 -4
  102. package/dist/{list-Cur_72xZ.mjs → list-Ov6bYzT9.mjs} +6 -5
  103. package/dist/{list-BFm4iEBt.mjs → list-SfpASSFx.mjs} +3 -3
  104. package/dist/{list-DEkPxVZu.mjs → list-fPfgG0PY.mjs} +3 -3
  105. package/dist/list-lsPeumLP.mjs +34 -0
  106. package/dist/{list-B9TyELob.mjs → list-sn9M7QQX.mjs} +3 -3
  107. package/dist/{list-CMb03cv3.mjs → list-wxIVl45_.mjs} +3 -3
  108. package/dist/listing-wFZuS5li.mjs +10 -0
  109. package/dist/{login-hKlYsAa1.mjs → login-CRlmR4op.mjs} +59 -19
  110. package/dist/{logout-DO8GVC6b.mjs → logout-DSZEUxSQ.mjs} +4 -4
  111. package/dist/{measure-R3T20wnB.mjs → measure-Ka0n3426.mjs} +5 -5
  112. package/dist/{parameter-values-BJb2GuLz.mjs → parameter-values-CwW8Xes7.mjs} +3 -3
  113. package/dist/{parse-enum-Cfne2suT.mjs → parse-enum-ooCsJWn6.mjs} +1 -1
  114. package/dist/parse-id-xS1tWJAq.mjs +18 -0
  115. package/dist/{parse-ref-C_MsXYI6.mjs → parse-ref-j4lxaQmd.mjs} +1 -1
  116. package/dist/{path-DzhQVCH8.mjs → path-CVmg-bmf.mjs} +3 -3
  117. package/dist/{poll-D1Yg78c3.mjs → poll-CuTlWBwI.mjs} +2 -2
  118. package/dist/{poll-task-DLUZaglY.mjs → poll-task-DhsYs8Hx.mjs} +2 -2
  119. package/dist/{preflight-Cy2jUSca.mjs → preflight-D3VgjEg7.mjs} +2 -2
  120. package/dist/{process-D5HN1wpT.mjs → process-7c8BcQEi.mjs} +1 -1
  121. package/dist/profile-name-CZ1-Vtrh.mjs +7 -0
  122. package/dist/{prompt-ZG9Drin_.mjs → prompt-jQnNKtlx.mjs} +1 -1
  123. package/dist/{publish-3hmHoaX-.mjs → publish-DFTCsjHU.mjs} +12 -7
  124. package/dist/{query-DkZViD_3.mjs → query-CPq-dhFV.mjs} +7 -7
  125. package/dist/{query-DTASj9Zu.mjs → query-Dfx69iJ4.mjs} +4 -4
  126. package/dist/{query-result-CbTFdlSf.mjs → query-result-Cg7odyR_.mjs} +1 -1
  127. package/dist/{remove-collection-D3wH9H6R.mjs → remove-collection-DUAMhx5L.mjs} +6 -6
  128. package/dist/{replace-CFzqkZXs.mjs → replace-DZWd6avZ.mjs} +5 -5
  129. package/dist/{requests-KBiY9UYy.mjs → requests-DYrDe-D0.mjs} +2 -2
  130. package/dist/{rescan-values-B1WycObS.mjs → rescan-values-DZSnEFA0.mjs} +5 -5
  131. package/dist/{resolve-Ct084wx3.mjs → resolve-BTZuO3Yq.mjs} +22 -15
  132. package/dist/{run-B8qEEdC8.mjs → run-B2b3Bjmv.mjs} +3 -3
  133. package/dist/{run-cw0qTEQ7.mjs → run-ryZ4sXK5.mjs} +6 -6
  134. package/dist/{runs-BOA4kK_X.mjs → runs-F82HVSie.mjs} +5 -5
  135. package/dist/{runtime-B-c-Fc2i.mjs → runtime-C6ASrgVA.mjs} +942 -906
  136. package/dist/{schema-tables-DRYzSVcW.mjs → schema-tables-CCOKb715.mjs} +4 -4
  137. package/dist/{schemas-BiMjD25L.mjs → schemas-LtVrNECH.mjs} +3 -3
  138. package/dist/{search-DjKxRXO5.mjs → search-9GSZHgJQ.mjs} +3 -3
  139. package/dist/{segment-CZkk0g79.mjs → segment-C40bORNZ.mjs} +5 -5
  140. package/dist/{selectors-B7MJUn3I.mjs → selectors-B2nDmrIV.mjs} +3 -3
  141. package/dist/{set-kyoORFG3.mjs → set-C57KDMGM.mjs} +6 -6
  142. package/dist/{set-active-CF868qdW.mjs → set-active-BDHGHtj2.mjs} +3 -3
  143. package/dist/{setting-BZgjURQ_.mjs → setting-zrJwy48C.mjs} +3 -3
  144. package/dist/{setup-D9joJYPb.mjs → setup-Doq8atTG.mjs} +4 -4
  145. package/dist/{skills-BqI05u72.mjs → skills-y-KsUqT9.mjs} +1 -1
  146. package/dist/{skills-D8NrJxjp.mjs → skills-yuzH_o8s.mjs} +3 -3
  147. package/dist/{snippet-y_FzKxaQ.mjs → snippet-ScFM8re8.mjs} +5 -5
  148. package/dist/{stash-BVMerTaF.mjs → stash-D_u6z4q6.mjs} +6 -6
  149. package/dist/{status-C-0jFjFn.mjs → status--Dm2Jv6k.mjs} +3 -3
  150. package/dist/{status-x8bDOa1i.mjs → status-CDPhDX5I.mjs} +26 -10
  151. package/dist/{summary-B-R9SdMF.mjs → summary-C9a2cBTE.mjs} +3 -3
  152. package/dist/{sync-schema-wne2KXIW.mjs → sync-schema-CDIWNCGI.mjs} +7 -7
  153. package/dist/sync-scope-DW2QpGwv.mjs +55 -0
  154. package/dist/{table-CS2-2INi.mjs → table-BKehBm_E.mjs} +4 -4
  155. package/dist/timeline-DEaiuB5V.mjs +157 -0
  156. package/dist/timeline-event-CLWMxvE0.mjs +22 -0
  157. package/dist/timeline-n3Nw9Egt.mjs +24 -0
  158. package/dist/transform-CrOOqKen.mjs +31 -0
  159. package/dist/transform-job-J-E0Mec7.mjs +25 -0
  160. package/dist/{transform-tag-DaNKreDj.mjs → transform-tag-Bkyasy_s.mjs} +4 -4
  161. package/dist/{transforms-DB2WNP2R.mjs → transforms-w2a5qsjJ.mjs} +4 -4
  162. package/dist/{tree-BIOphGVs.mjs → tree-JZC6vD9q.mjs} +3 -3
  163. package/dist/{unpublish-BWM36ZTg.mjs → unpublish-BsfIH46y.mjs} +4 -4
  164. package/dist/{update-BCbv880u.mjs → update-B8hn6VxO.mjs} +8 -8
  165. package/dist/{update-BlzcZwRQ.mjs → update-BSNK36fk.mjs} +8 -8
  166. package/dist/update-BTFj6WHo.mjs +52 -0
  167. package/dist/{update-6umfSz5t.mjs → update-BfIGjAVA.mjs} +5 -5
  168. package/dist/{update-HxYcvpSq.mjs → update-C1_aIL6X.mjs} +7 -7
  169. package/dist/{update-BchtHlbH.mjs → update-CXKWHOIN.mjs} +6 -6
  170. package/dist/{update-CTZg2Lia.mjs → update-Cg09lBb5.mjs} +9 -9
  171. package/dist/update-D01S8hnw.mjs +52 -0
  172. package/dist/{update-7Ts7KrB8.mjs → update-DKQjMsuE.mjs} +6 -6
  173. package/dist/{update-Sz0QRGWB.mjs → update-DSlSr7Ky.mjs} +6 -6
  174. package/dist/{update-DJDuIhYG.mjs → update-QaBVvlz9.mjs} +6 -6
  175. package/dist/{update-BiUeEGU-.mjs → update-WLb9099y.mjs} +8 -8
  176. package/dist/{update-zfLbmC-H.mjs → update-bFNKpay8.mjs} +6 -6
  177. package/dist/{update-dashcard-CfDpy7Lz.mjs → update-dashcard-CKZC7scy.mjs} +6 -6
  178. package/dist/{upgrade-BWl0VjMK.mjs → upgrade-DkK1ZbEY.mjs} +4 -4
  179. package/dist/{upload-CPzOhGeX.mjs → upload-DOMoaR9f.mjs} +3 -3
  180. package/dist/{uuid-D_YeqDui.mjs → uuid-DDbLKnbe.mjs} +2 -2
  181. package/dist/{validate-clAWO3X-.mjs → validate-DqQ7DQnP.mjs} +1 -1
  182. package/dist/{validate-query-CsK2_2_u.mjs → validate-query-BYG7lyaY.mjs} +2 -2
  183. package/dist/{values-DSk8ZJ3B.mjs → values-Bc99Kkuv.mjs} +3 -3
  184. package/dist/{verify-DkQdLjf3.mjs → verify-WvMbaT8W.mjs} +2 -2
  185. package/dist/{wait-DvSsMjea.mjs → wait-DmgEPnHW.mjs} +5 -5
  186. package/dist/{wait-flags-dXGThljd.mjs → wait-flags-BpzyrpAL.mjs} +2 -2
  187. package/dist/workspace-BgH_4xFO.mjs +75 -0
  188. package/dist/workspace-LFtHxcw9.mjs +19 -0
  189. package/package.json +1 -1
  190. package/skill-data/core/SKILL.md +5 -3
  191. package/skill-data/git-sync/SKILL.md +20 -5
  192. package/skill-data/visualization/SKILL.md +1 -0
  193. package/dist/add-collection-DlAwt2Z3.mjs +0 -9
  194. package/dist/git-sync-JHa7bIyY.mjs +0 -31
  195. package/dist/is-dirty-Bb2dfEYs.mjs +0 -8
  196. package/dist/parse-id-DfY0Lugw.mjs +0 -12
  197. package/dist/transform-6hqM1DXA.mjs +0 -31
  198. package/dist/transform-job-DuERhLaj.mjs +0 -25
  199. /package/dist/{dashboard-B-MTpfeS.mjs → dashboard-BLihjDXg.mjs} +0 -0
  200. /package/dist/{document-CL4ML5FN.mjs → document-D4zB-5a0.mjs} +0 -0
  201. /package/dist/{measure-S9WszHre.mjs → measure-C3G1srWb.mjs} +0 -0
  202. /package/dist/{paginate-CMqbqhMC.mjs → paginate-FVZUxL4J.mjs} +0 -0
  203. /package/dist/{revision-message-flag-CHzX0cqV.mjs → revision-message-flag-CHrJgFFx.mjs} +0 -0
  204. /package/dist/{segment-BPb1H6DW.mjs → segment-CxRYEXeO.mjs} +0 -0
  205. /package/dist/{setting-Cko6bE_w.mjs → setting-m46MUtW5.mjs} +0 -0
  206. /package/dist/{snippet-DWiXNy4T.mjs → snippet-C8PVaPY9.mjs} +0 -0
  207. /package/dist/{table-C3RIKiYM.mjs → table-3onHpwcb.mjs} +0 -0
  208. /package/dist/{transform-JQel7e5H.mjs → transform-DP6J2ber.mjs} +0 -0
  209. /package/dist/{transform-job-CtVziW85.mjs → transform-job-B5x8aYfV.mjs} +0 -0
  210. /package/dist/{transform-tag-wFiWmiyO.mjs → transform-tag-BRwfWeHf.mjs} +0 -0
@@ -1,4 +1,4 @@
1
- import { AbortError, BASELINE_CAPABILITIES, ConfigError, DEFAULT_MAX_BYTES, ENV_API_KEY, ENV_DISABLE_KEYRING, ENV_PROFILE, ENV_SKIP_PREFLIGHT, ENV_URL, MetabaseError, NetworkError, ResponseShapeError, TimeoutError, ValidationError, consumeLegacyEnvWarnings, errorMessage, flagConsumesValue, isNotFoundError, isPlainObject, normalizeFlag, package_default, readEnv, reportError, toAliasArray, warn } from "./capabilities-DrDJo_ng.mjs";
1
+ import { AbortError, BASELINE_CAPABILITIES, ConfigError, DEFAULT_MAX_BYTES, ENV_API_KEY, ENV_DISABLE_KEYRING, ENV_PROFILE, ENV_SKIP_PREFLIGHT, ENV_URL, MetabaseError, NetworkError, ResponseShapeError, TimeoutError, ValidationError, consumeLegacyEnvWarnings, errorMessage, flagConsumesValue, isNotFoundError, isPlainObject, normalizeFlag, package_default, readEnv, reportError, toAliasArray, warn } from "./capabilities-CiaAfUgX.mjs";
2
2
  import { setMetabaseAugment } from "./command-augment-D9pI9Vbh.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { z } from "zod";
@@ -6,8 +6,8 @@ import { promises } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
7
  import { Entry } from "@napi-rs/keyring";
8
8
  import { homedir } from "node:os";
9
- import { parse } from "semver";
10
9
  import { setTimeout } from "node:timers/promises";
10
+ import { parse } from "semver";
11
11
 
12
12
  //#region src/runtime/json.ts
13
13
  const JSON_CONTENT_TYPE = "application/json";
@@ -77,720 +77,372 @@ const SessionProperties = z.object({
77
77
  }).loose();
78
78
 
79
79
  //#endregion
80
- //#region src/core/version/tag.ts
81
- const ParsedVersionSchema = z.object({
82
- tag: z.string(),
83
- major: z.number().int().nonnegative(),
84
- patch: z.number().int().nonnegative()
85
- });
86
- function tryParseTag(tag) {
87
- const parsed = parse(tag);
88
- if (parsed === null || parsed.major !== 0 && parsed.major !== 1) return null;
89
- return {
90
- tag,
91
- major: parsed.minor,
92
- patch: parsed.patch
93
- };
80
+ //#region src/core/url.ts
81
+ function normalizeUrl(input) {
82
+ const trimmed = input.trim().replace(/\/+$/, "");
83
+ if (!/^https?:\/\//i.test(trimmed)) throw new ConfigError("URL must start with http:// or https://");
84
+ return trimmed;
94
85
  }
95
-
96
- //#endregion
97
- //#region src/core/auth/profile-record.ts
98
- const ProbedUser = z.object({
99
- id: z.number().int(),
100
- name: z.string(),
101
- isAdmin: z.boolean()
102
- });
103
- const ProfileLastProbe = z.object({
104
- at: z.iso.datetime(),
105
- version: ParsedVersionSchema.nullable(),
106
- tokenFeatures: TokenFeatures.nullable(),
107
- user: ProbedUser
108
- });
109
- const ProfileFailureKind = z.enum([
110
- "auth",
111
- "network",
112
- "server"
86
+ function displayUrl(input) {
87
+ const parsed = new URL(input);
88
+ const path = parsed.pathname.replace(/\/+$/, "");
89
+ return `${parsed.origin}${path}`;
90
+ }
91
+ const LOOPBACK_HOSTS = new Set([
92
+ "localhost",
93
+ "127.0.0.1",
94
+ "::1",
95
+ "[::1]"
113
96
  ]);
114
- const ProfileLastFailure = z.object({
115
- at: z.iso.datetime(),
116
- kind: ProfileFailureKind,
117
- reason: z.string()
118
- });
119
- const ProfileOAuth = z.object({
120
- accessToken: z.string().nullable(),
121
- refreshToken: z.string().nullable(),
122
- expiresAt: z.iso.datetime(),
123
- clientId: z.string()
124
- }).loose();
125
- const ProfileAuthMethod = z.enum(["oauth", "apiKey"]);
126
- const ProfileRecord = z.object({
127
- name: z.string(),
128
- url: z.string(),
129
- apiKey: z.string().nullable(),
130
- oauth: ProfileOAuth.nullable().default(null),
131
- lastProbe: ProfileLastProbe.nullable(),
132
- lastFailure: ProfileLastFailure.nullable()
133
- }).loose();
134
- const ProfilesFile = z.object({ profiles: z.array(ProfileRecord) }).loose();
135
- function profileAuthMethod(record) {
136
- return record.oauth !== null ? "oauth" : "apiKey";
97
+ function isLoopbackHost(hostname) {
98
+ return LOOPBACK_HOSTS.has(hostname.toLowerCase());
99
+ }
100
+ function assertEndpointOrigin(endpoint, baseUrl, label) {
101
+ let parsed;
102
+ try {
103
+ parsed = new URL(endpoint);
104
+ } catch {
105
+ throw new ConfigError(`OAuth ${label} is not a valid URL: ${endpoint}`);
106
+ }
107
+ const base = new URL(baseUrl);
108
+ if (parsed.protocol !== "https:" && !isLoopbackHost(parsed.hostname)) throw new ConfigError(`OAuth ${label} must use https: ${endpoint}`);
109
+ if (parsed.origin !== base.origin) throw new ConfigError(`OAuth ${label} origin (${parsed.origin}) does not match the Metabase URL (${base.origin})`);
137
110
  }
138
111
 
139
112
  //#endregion
140
- //#region src/core/auth/storage.ts
141
- const KEYRING_SERVICE = "metabase-cli";
142
- const PROFILES_FILE = "profiles.json";
143
- const LEGACY_CREDENTIALS_FILE = "credentials.json";
144
- const LEGACY_REJECTIONS_FILE = "rejections.json";
145
- const PROFILES_FILE_MODE = 384;
146
- const PROFILES_DIR_MODE = 448;
147
- const DEFAULT_PROFILE = "default";
148
- const LEGACY_STORAGE_NOTICE = "Old profile storage detected and ignored; re-run `mb auth login` for each profile.";
149
- const KEYCHAIN_RESIDUAL_NOTICE = "warning: could not remove one or more secrets from the OS keychain; a stored token may remain — remove it manually or retry.";
150
- const account = {
151
- profileApiKey: (profile) => `profile:${profile}:apiKey`,
152
- profileOAuthAccess: (profile) => `profile:${profile}:oauthAccess`,
153
- profileOAuthRefresh: (profile) => `profile:${profile}:oauthRefresh`
154
- };
155
- let legacyWarningPending = false;
156
- let keychainResidualPending = false;
157
- let keyringDowngradeNotice = null;
158
- function profilesFilePath() {
159
- return join(configDir(), PROFILES_FILE);
113
+ //#region src/runtime/signal.ts
114
+ function createProcessAbortHandler() {
115
+ const controller = new AbortController();
116
+ const handler = () => {
117
+ if (!controller.signal.aborted) controller.abort(new AbortError("interrupted"));
118
+ process.off("SIGINT", handler);
119
+ };
120
+ process.on("SIGINT", handler);
121
+ return {
122
+ signal: controller.signal,
123
+ uninstall: () => process.off("SIGINT", handler)
124
+ };
160
125
  }
161
- function consumeLegacyStorageWarning() {
162
- if (!legacyWarningPending) return null;
163
- legacyWarningPending = false;
164
- return LEGACY_STORAGE_NOTICE;
126
+ let singleton = null;
127
+ function getProcessAbortSignal() {
128
+ if (singleton === null) singleton = createProcessAbortHandler().signal;
129
+ return singleton;
165
130
  }
166
- function consumeKeychainResidualWarning() {
167
- if (!keychainResidualPending) return null;
168
- keychainResidualPending = false;
169
- return KEYCHAIN_RESIDUAL_NOTICE;
131
+ function combineAborts(timeoutSignal, callerSignal) {
132
+ const processSignal = getProcessAbortSignal();
133
+ const signals = [timeoutSignal, processSignal];
134
+ if (callerSignal) signals.push(callerSignal);
135
+ return {
136
+ combined: AbortSignal.any(signals),
137
+ processSignal
138
+ };
170
139
  }
171
- function consumeKeyringDowngradeWarning() {
172
- const message = keyringDowngradeNotice;
173
- keyringDowngradeNotice = null;
174
- return message;
140
+ function throwIfAborted(...signals) {
141
+ for (const signal of signals) if (signal?.aborted) throw abortReason(signal);
175
142
  }
176
- function keyringEnabled() {
177
- return readEnv(ENV_DISABLE_KEYRING) !== "1";
143
+ function abortReason(signal) {
144
+ const reason = signal.reason;
145
+ if (reason instanceof MetabaseError) return reason;
146
+ if (reason instanceof Error || typeof reason === "string") return new AbortError(errorMessage(reason) || "aborted");
147
+ return new AbortError("aborted");
178
148
  }
179
- function trySetKeyring(key, value) {
180
- if (!keyringEnabled()) return false;
181
- try {
182
- new Entry(KEYRING_SERVICE, key).setPassword(value);
183
- return true;
184
- } catch {
185
- return false;
186
- }
149
+
150
+ //#endregion
151
+ //#region src/core/auth/credential.ts
152
+ const API_KEY_HEADER = "x-api-key";
153
+ const AUTHORIZATION_HEADER = "authorization";
154
+ const BEARER_PREFIX = "Bearer ";
155
+ const EXPIRY_SKEW_MS = 6e4;
156
+ const DEFAULT_EXPIRES_IN_SECONDS = 3600;
157
+ function credentialAuthHeader(credential) {
158
+ if (credential.kind === "apiKey") return {
159
+ name: API_KEY_HEADER,
160
+ value: credential.apiKey
161
+ };
162
+ return {
163
+ name: AUTHORIZATION_HEADER,
164
+ value: BEARER_PREFIX + credential.accessToken
165
+ };
187
166
  }
188
- function tryReadKeyring(key) {
189
- if (!keyringEnabled()) return void 0;
190
- try {
191
- return new Entry(KEYRING_SERVICE, key).getPassword();
192
- } catch {
193
- return void 0;
194
- }
167
+ function credentialSecrets(credential) {
168
+ if (credential.kind === "apiKey") return [credential.apiKey];
169
+ return [credential.accessToken, credential.refreshToken];
195
170
  }
196
- function removeKeyringEntry(key) {
197
- if (!keyringEnabled()) return "skipped";
198
- try {
199
- return new Entry(KEYRING_SERVICE, key).deletePassword() ? "removed" : "absent";
200
- } catch {
201
- return "failed";
202
- }
171
+ function isOAuthExpired(credential, nowMs) {
172
+ const expiryMs = Date.parse(credential.expiresAt);
173
+ if (Number.isNaN(expiryMs)) return true;
174
+ return expiryMs - EXPIRY_SKEW_MS <= nowMs;
203
175
  }
204
- async function readProfilesFile() {
205
- const path = profilesFilePath();
206
- let raw;
207
- try {
208
- raw = await promises.readFile(path, "utf8");
209
- } catch (error) {
210
- if (isNotFoundError(error)) {
211
- await detectLegacyArtifacts();
212
- return { profiles: [] };
213
- }
214
- throw error;
215
- }
216
- const parsed = parseJsonResult(raw, ProfilesFile, { source: path });
217
- if (parsed.ok) return parsed.value;
218
- if (parsed.error instanceof ValidationError) {
219
- legacyWarningPending = true;
220
- return { profiles: [] };
221
- }
222
- throw parsed.error;
176
+ function expiresAtFromNow(expiresInSeconds, nowMs) {
177
+ return new Date(nowMs + expiresInSeconds * 1e3).toISOString();
223
178
  }
224
- async function detectLegacyArtifacts() {
225
- const legacyCredentials = join(configDir(), LEGACY_CREDENTIALS_FILE);
226
- const legacyRejections = join(configDir(), LEGACY_REJECTIONS_FILE);
227
- const [credentialsExists, rejectionsExists] = await Promise.all([fileExists(legacyCredentials), fileExists(legacyRejections)]);
228
- if (credentialsExists || rejectionsExists) legacyWarningPending = true;
179
+ function oauthCredentialFromTokens(tokens, refreshToken, clientId, scope, nowMs) {
180
+ return {
181
+ kind: "oauth",
182
+ accessToken: tokens.access_token,
183
+ refreshToken,
184
+ expiresAt: expiresAtFromNow(tokens.expires_in ?? DEFAULT_EXPIRES_IN_SECONDS, nowMs),
185
+ clientId,
186
+ scope
187
+ };
229
188
  }
230
- async function fileExists(path) {
231
- try {
232
- await promises.access(path);
233
- return true;
234
- } catch {
235
- return false;
189
+
190
+ //#endregion
191
+ //#region src/core/http/sanitize.ts
192
+ const SECRET_HEADER_NAMES = new Set([
193
+ "authorization",
194
+ "x-api-key",
195
+ "x-metabase-session",
196
+ "cookie",
197
+ "set-cookie",
198
+ "proxy-authorization"
199
+ ]);
200
+ const REDACTED = "[REDACTED]";
201
+ function redactHeaders(headers) {
202
+ const result = {};
203
+ const entries = headers instanceof Headers ? headers.entries() : Object.entries(headers);
204
+ for (const [key, value] of entries) result[key] = SECRET_HEADER_NAMES.has(key.toLowerCase()) ? REDACTED : value;
205
+ return result;
206
+ }
207
+ function redactBody(body, ctx) {
208
+ let result = body;
209
+ for (const secret of ctx.knownSecrets) {
210
+ if (secret.length === 0) continue;
211
+ result = result.replaceAll(secret, REDACTED);
236
212
  }
213
+ return result;
237
214
  }
238
- async function writeProfilesFile(file) {
239
- const path = profilesFilePath();
240
- if (file.profiles.length === 0) {
241
- await promises.unlink(path).catch(() => void 0);
242
- await cleanupLegacyFiles();
243
- return;
215
+
216
+ //#endregion
217
+ //#region src/core/http/errors.ts
218
+ const ROUTE_MISSING_LITERAL = "API endpoint does not exist.";
219
+ const RESOURCE_MISSING_LITERAL = "Not found.";
220
+ const STATUS_CLASSIFICATIONS = {
221
+ 401: { retryable: false },
222
+ 403: { retryable: false },
223
+ 404: { retryable: false },
224
+ 408: {
225
+ retryable: true,
226
+ message: "Metabase timed out responding."
227
+ },
228
+ 425: { retryable: true },
229
+ 429: {
230
+ retryable: true,
231
+ message: "Metabase rate-limited the request."
232
+ },
233
+ 500: { retryable: true },
234
+ 502: { retryable: true },
235
+ 503: { retryable: true },
236
+ 504: {
237
+ retryable: true,
238
+ message: "Metabase timed out responding."
244
239
  }
245
- await promises.mkdir(dirname(path), {
246
- recursive: true,
247
- mode: PROFILES_DIR_MODE
248
- });
249
- const tmpPath = `${path}.${process.pid}.tmp`;
250
- await promises.writeFile(tmpPath, JSON.stringify(file, null, 2) + "\n", { mode: PROFILES_FILE_MODE });
251
- if (process.platform !== "win32") await promises.chmod(tmpPath, PROFILES_FILE_MODE);
252
- try {
253
- await promises.rename(tmpPath, path);
254
- } catch (error) {
255
- await promises.unlink(tmpPath).catch(() => void 0);
256
- throw error;
240
+ };
241
+ const ErrorEnvelope = z.object({
242
+ message: z.string().optional(),
243
+ error: z.string().optional(),
244
+ "error-message": z.string().optional(),
245
+ via: z.array(z.object({ message: z.string().optional() }).loose()).optional(),
246
+ "specific-errors": z.unknown().optional(),
247
+ errors: z.unknown().optional()
248
+ }).loose();
249
+ const MAX_EXTRACTED_MESSAGE_LEN = 500;
250
+ const ELLIPSIS = "…";
251
+ var HttpError = class extends MetabaseError {
252
+ category = "http";
253
+ exitCode = 1;
254
+ status;
255
+ kind;
256
+ developerDetail;
257
+ constructor(input) {
258
+ const sanitizedBody = sanitizeBody(input.rawBody, input.redactionContext);
259
+ const redactedHeaders = redactHeaders(input.responseHeaders);
260
+ const kind = classifyKind(input.status, sanitizedBody, redactedHeaders);
261
+ super(input.overrideUserMessage ?? buildUserMessage(kind, input, sanitizedBody));
262
+ this.name = "HttpError";
263
+ this.status = input.status;
264
+ this.kind = kind;
265
+ this.developerDetail = {
266
+ status: input.status,
267
+ statusText: input.statusText,
268
+ method: input.method,
269
+ url: input.url,
270
+ responseHeaders: redactedHeaders,
271
+ body: sanitizedBody
272
+ };
257
273
  }
258
- await cleanupLegacyFiles();
274
+ get isRetryable() {
275
+ return isRetryableStatus(this.status);
276
+ }
277
+ };
278
+ function isRetryableStatus(status) {
279
+ return STATUS_CLASSIFICATIONS[status]?.retryable === true;
259
280
  }
260
- async function cleanupLegacyFiles() {
261
- await Promise.all([promises.unlink(join(configDir(), LEGACY_CREDENTIALS_FILE)).catch(() => void 0), promises.unlink(join(configDir(), LEGACY_REJECTIONS_FILE)).catch(() => void 0)]);
281
+ function sanitizeBody(rawBody, ctx) {
282
+ if (rawBody === null) return null;
283
+ if (ctx === void 0) return rawBody;
284
+ return redactBody(rawBody, ctx);
262
285
  }
263
- function findRecord(file, name) {
264
- return file.profiles.find((entry) => entry.name === name) ?? null;
286
+ function classifyKind(status, sanitizedBody, redactedHeaders) {
287
+ if (status === 401 || status === 403) return "auth";
288
+ if (status === 404) return isRouteMissingResponse(sanitizedBody, redactedHeaders) ? "route-missing" : "resource-missing";
289
+ if (status === 429) return "rate-limit";
290
+ if (status >= 500 && status < 600) return "server-error";
291
+ return "generic";
265
292
  }
266
- function sideIsKeyringBacked(record, side) {
267
- return side === "oauth" ? record.oauth !== null && record.oauth.accessToken === null : record.oauth === null && record.apiKey === null;
293
+ function isRouteMissingResponse(sanitizedBody, redactedHeaders) {
294
+ if (sanitizedBody?.includes(ROUTE_MISSING_LITERAL)) return true;
295
+ if (sanitizedBody?.includes(RESOURCE_MISSING_LITERAL)) return false;
296
+ if (redactedHeaders["content-type"]?.includes(JSON_CONTENT_TYPE)) return false;
297
+ if (sanitizedBody === null || sanitizedBody.trim() === "") return true;
298
+ return !parseJsonResult(sanitizedBody, ErrorEnvelope).ok;
268
299
  }
269
- function flagResidualIfUnconfirmed(existing, cleared, removals) {
270
- if (existing === null || !removals.includes("failed")) return;
271
- if (sideIsKeyringBacked(existing, cleared)) keychainResidualPending = true;
300
+ function buildUserMessage(kind, input, sanitizedBody) {
301
+ if (kind === "route-missing") return buildRouteMissingMessage(input);
302
+ if (kind === "resource-missing") return `Not found: ${input.method} ${pathFromUrl(input.url)}.`;
303
+ const fromBody = parseEnvelopeMessage(sanitizedBody);
304
+ if (fromBody !== null) return fromBody;
305
+ if (kind === "auth") return `Invalid or unauthorized API key (host: ${hostFromUrl(input.url)}).`;
306
+ return defaultMessageForStatus(input.status);
272
307
  }
273
- function fileLocation(key) {
274
- return {
275
- backend: "file",
276
- path: profilesFilePath(),
277
- account: key,
278
- reason: keyringEnabled() ? "unavailable" : "disabled"
279
- };
308
+ function buildRouteMissingMessage(input) {
309
+ const path = pathFromUrl(input.url);
310
+ if (!input.serverTag) return `This endpoint is not available on the connected Metabase: ${input.method} ${path}.`;
311
+ return `This endpoint is not available on Metabase ${input.serverTag}: ${input.method} ${path}. The command may require a newer Metabase major version. Run 'mb auth list' to see this server's version.`;
280
312
  }
281
- function keyringFallbackWarning(location) {
282
- const cause = location.reason === "disabled" ? `OS keychain disabled via ${ENV_DISABLE_KEYRING}` : "OS keychain unavailable";
283
- return `warning: ${cause}; credentials stored as plaintext at ${location.path}`;
313
+ function pathFromUrl(url) {
314
+ const parsed = new URL(url);
315
+ return parsed.pathname + parsed.search;
284
316
  }
285
- function persistSecret(key, value) {
286
- if (trySetKeyring(key, value)) return {
287
- backend: "keyring",
288
- service: KEYRING_SERVICE,
289
- account: key
290
- };
291
- removeKeyringEntry(key);
292
- return fileLocation(key);
317
+ function hostFromUrl(url) {
318
+ return new URL(url).host;
293
319
  }
294
- function resolveSecret(key, inline) {
295
- if (inline !== null) return inline;
296
- return tryReadKeyring(key) ?? null;
320
+ function parseEnvelopeMessage(sanitizedBody) {
321
+ if (!sanitizedBody) return null;
322
+ const result = parseJsonResult(sanitizedBody, ErrorEnvelope);
323
+ if (!result.ok) return null;
324
+ const envelope = result.value;
325
+ const topLevel = envelope.message ?? envelope.error ?? envelope["error-message"];
326
+ if (topLevel) return capLength(topLevel);
327
+ const viaMessage = envelope.via?.find((entry) => entry.message)?.message;
328
+ if (viaMessage) return capLength(viaMessage);
329
+ const specific = formatErrorTree(envelope["specific-errors"]);
330
+ if (specific) return capLength(specific);
331
+ const generic = formatErrorTree(envelope.errors);
332
+ if (generic) return capLength(generic);
333
+ return null;
297
334
  }
298
- async function readProfileCredential(name = DEFAULT_PROFILE) {
299
- const file = await readProfilesFile();
300
- const record = findRecord(file, name);
301
- if (record === null) return null;
302
- return resolveRecordCredential(record);
335
+ function formatErrorTree(value) {
336
+ const entries = collectLeafEntries(value, []);
337
+ if (entries.length === 0) return null;
338
+ return entries.map(formatLeafEntry).join("; ");
303
339
  }
304
- function resolveRecordCredential(record) {
305
- if (record.oauth !== null) {
306
- const accessToken = resolveSecret(account.profileOAuthAccess(record.name), record.oauth.accessToken);
307
- const refreshToken = resolveSecret(account.profileOAuthRefresh(record.name), record.oauth.refreshToken);
308
- if (accessToken === null || refreshToken === null) return null;
309
- return {
310
- url: record.url,
311
- credential: {
312
- kind: "oauth",
313
- accessToken,
314
- refreshToken,
315
- expiresAt: record.oauth.expiresAt,
316
- clientId: record.oauth.clientId
317
- }
318
- };
340
+ function formatLeafEntry(entry) {
341
+ return entry.path === "" ? entry.message : `${entry.path}: ${entry.message}`;
342
+ }
343
+ function collectLeafEntries(value, path) {
344
+ if (typeof value === "string") {
345
+ const trimmed = value.trim();
346
+ return trimmed === "" ? [] : [{
347
+ path: path.join("."),
348
+ message: trimmed
349
+ }];
319
350
  }
320
- const apiKey = resolveSecret(account.profileApiKey(record.name), record.apiKey);
321
- if (apiKey === null) return null;
322
- return {
323
- url: record.url,
324
- credential: {
325
- kind: "apiKey",
326
- apiKey
327
- }
328
- };
351
+ if (Array.isArray(value)) {
352
+ const messages = value.filter((entry) => typeof entry === "string" && entry.trim() !== "");
353
+ if (messages.length === 0) return [];
354
+ return [{
355
+ path: path.join("."),
356
+ message: messages.join("; ")
357
+ }];
358
+ }
359
+ if (isPlainObject(value)) return Object.entries(value).flatMap(([key, child]) => collectLeafEntries(child, [...path, key]));
360
+ return [];
329
361
  }
330
- async function readProfileRecord(name = DEFAULT_PROFILE) {
331
- const file = await readProfilesFile();
332
- return findRecord(file, name);
362
+ function capLength(message) {
363
+ if (message.length <= MAX_EXTRACTED_MESSAGE_LEN) return message;
364
+ return message.slice(0, MAX_EXTRACTED_MESSAGE_LEN - ELLIPSIS.length) + ELLIPSIS;
333
365
  }
334
- async function listProfileRecords() {
335
- const file = await readProfilesFile();
336
- return file.profiles;
366
+ function defaultMessageForStatus(status) {
367
+ return STATUS_CLASSIFICATIONS[status]?.message ?? `Metabase returned ${status}.`;
337
368
  }
338
- async function upsertRecord(file, name, updated) {
339
- const exists = findRecord(file, name) !== null;
340
- const profiles = exists ? file.profiles.map((entry) => entry.name === name ? updated : entry) : [...file.profiles, updated];
341
- await writeProfilesFile({
342
- ...file,
343
- profiles
369
+
370
+ //#endregion
371
+ //#region src/core/http/network-error.ts
372
+ const NETWORK_HINTS = {
373
+ ECONNREFUSED: (t) => `Connection refused by ${t.host} — is Metabase running and is the port correct?`,
374
+ ENOTFOUND: (t) => `Host not found: ${t.hostname} — check the URL.`,
375
+ EAI_AGAIN: (t) => `Could not resolve ${t.hostname} — check your network connection and the URL.`,
376
+ ECONNRESET: (t) => `Connection to ${t.host} was reset — the server may have closed it, or http/https may be mismatched.`,
377
+ ETIMEDOUT: (t) => `Connection to ${t.host} timed out — check the host, port, and your network.`
378
+ };
379
+ const TLS_ERROR_CODES = new Set([
380
+ "EPROTO",
381
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
382
+ "SELF_SIGNED_CERT_IN_CHAIN",
383
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
384
+ "CERT_HAS_EXPIRED",
385
+ "ERR_TLS_CERT_ALTNAME_INVALID"
386
+ ]);
387
+ function buildNetworkError(error, method, url) {
388
+ const fallback = errorMessage(error);
389
+ const code = causeCode(error);
390
+ const target = networkTarget(url);
391
+ return new NetworkError(networkMessage(code, target, fallback), {
392
+ method,
393
+ url,
394
+ cause: code ?? fallback
344
395
  });
345
396
  }
346
- async function writeProfile(profile, name = DEFAULT_PROFILE) {
347
- const location = persistSecret(account.profileApiKey(name), profile.apiKey);
348
- const inlineApiKey = location.backend === "file" ? profile.apiKey : null;
349
- const file = await readProfilesFile();
350
- const existing = findRecord(file, name);
351
- flagResidualIfUnconfirmed(existing, "oauth", [removeKeyringEntry(account.profileOAuthAccess(name)), removeKeyringEntry(account.profileOAuthRefresh(name))]);
352
- const updated = existing === null ? {
353
- name,
354
- url: profile.url,
355
- apiKey: inlineApiKey,
356
- oauth: null,
357
- lastProbe: null,
358
- lastFailure: null
359
- } : {
360
- ...existing,
361
- url: profile.url,
362
- apiKey: inlineApiKey,
363
- oauth: null
364
- };
365
- await upsertRecord(file, name, updated);
366
- return location;
367
- }
368
- async function writeOAuthProfile(url, credential, name = DEFAULT_PROFILE) {
369
- const accessKey = account.profileOAuthAccess(name);
370
- const refreshKey = account.profileOAuthRefresh(name);
371
- const accessLocation = persistSecret(accessKey, credential.accessToken);
372
- const refreshLocation = persistSecret(refreshKey, credential.refreshToken);
373
- const onFile = accessLocation.backend === "file" || refreshLocation.backend === "file";
374
- const oauth = {
375
- accessToken: onFile ? credential.accessToken : null,
376
- refreshToken: onFile ? credential.refreshToken : null,
377
- expiresAt: credential.expiresAt,
378
- clientId: credential.clientId
379
- };
380
- const file = await readProfilesFile();
381
- const existing = findRecord(file, name);
382
- if (onFile && existing !== null && sideIsKeyringBacked(existing, "oauth")) keyringDowngradeNotice = keyringFallbackWarning(fileLocation(accessKey));
383
- flagResidualIfUnconfirmed(existing, "apiKey", [removeKeyringEntry(account.profileApiKey(name))]);
384
- const updated = existing === null ? {
385
- name,
386
- url,
387
- apiKey: null,
388
- oauth,
389
- lastProbe: null,
390
- lastFailure: null
391
- } : {
392
- ...existing,
393
- url,
394
- apiKey: null,
395
- oauth
396
- };
397
- await upsertRecord(file, name, updated);
398
- return onFile ? fileLocation(accessKey) : {
399
- backend: "keyring",
400
- service: KEYRING_SERVICE,
401
- account: accessKey
402
- };
403
- }
404
- async function writeProbeResult(name, input) {
405
- const probe = ProfileLastProbe.parse({
406
- at: new Date().toISOString(),
407
- version: input.server.version,
408
- tokenFeatures: input.server.tokenFeatures,
409
- user: input.user
410
- });
411
- const file = await readProfilesFile();
412
- const existing = findRecord(file, name);
413
- if (existing === null) return null;
414
- const profiles = file.profiles.map((entry) => entry.name === name ? {
415
- ...entry,
416
- lastProbe: probe,
417
- lastFailure: null
418
- } : entry);
419
- await writeProfilesFile({
420
- ...file,
421
- profiles
422
- });
423
- return probe;
424
- }
425
- async function writeProbeFailure(name, input) {
426
- const failure$1 = ProfileLastFailure.parse({
427
- at: new Date().toISOString(),
428
- kind: input.kind,
429
- reason: input.reason
430
- });
431
- const file = await readProfilesFile();
432
- const existing = findRecord(file, name);
433
- if (existing === null) return null;
434
- const profiles = file.profiles.map((entry) => entry.name === name ? {
435
- ...entry,
436
- lastFailure: failure$1
437
- } : entry);
438
- await writeProfilesFile({
439
- ...file,
440
- profiles
441
- });
442
- return failure$1;
443
- }
444
- async function clearProfile(name = DEFAULT_PROFILE) {
445
- const file = await readProfilesFile();
446
- const existing = findRecord(file, name);
447
- const removals = [
448
- removeKeyringEntry(account.profileApiKey(name)),
449
- removeKeyringEntry(account.profileOAuthAccess(name)),
450
- removeKeyringEntry(account.profileOAuthRefresh(name))
451
- ];
452
- if (existing !== null) flagResidualIfUnconfirmed(existing, existing.oauth !== null ? "oauth" : "apiKey", removals);
453
- if (existing === null) return false;
454
- await writeProfilesFile({
455
- ...file,
456
- profiles: file.profiles.filter((entry) => entry.name !== name)
457
- });
458
- return true;
459
- }
460
-
461
- //#endregion
462
- //#region src/runtime/signal.ts
463
- function createProcessAbortHandler() {
464
- const controller = new AbortController();
465
- const handler = () => {
466
- if (!controller.signal.aborted) controller.abort(new AbortError("interrupted"));
467
- process.off("SIGINT", handler);
468
- };
469
- process.on("SIGINT", handler);
470
- return {
471
- signal: controller.signal,
472
- uninstall: () => process.off("SIGINT", handler)
473
- };
397
+ function networkMessage(code, target, fallback) {
398
+ if (code === null) return `Could not reach Metabase: ${fallback}`;
399
+ if (TLS_ERROR_CODES.has(code)) return `Could not reach Metabase: TLS error contacting ${target.host} (${code}) — the certificate could not be verified, or https:// was used against a plain-HTTP server.`;
400
+ const hint = NETWORK_HINTS[code];
401
+ if (hint === void 0) return `Could not reach Metabase: ${fallback} (${code})`;
402
+ return `Could not reach Metabase: ${hint(target)}`;
474
403
  }
475
- let singleton = null;
476
- function getProcessAbortSignal() {
477
- if (singleton === null) singleton = createProcessAbortHandler().signal;
478
- return singleton;
404
+ function causeCode(error) {
405
+ const cause = error instanceof Error ? error.cause : void 0;
406
+ if (cause instanceof Error && "code" in cause && typeof cause.code === "string") return cause.code;
407
+ return null;
479
408
  }
480
- function combineAborts(timeoutSignal, callerSignal) {
481
- const processSignal = getProcessAbortSignal();
482
- const signals = [timeoutSignal, processSignal];
483
- if (callerSignal) signals.push(callerSignal);
409
+ function networkTarget(url) {
410
+ const parsed = new URL(url);
484
411
  return {
485
- combined: AbortSignal.any(signals),
486
- processSignal
412
+ host: parsed.host,
413
+ hostname: parsed.hostname
487
414
  };
488
415
  }
489
- function throwIfAborted(...signals) {
490
- for (const signal of signals) if (signal?.aborted) throw abortReason(signal);
491
- }
492
- function abortReason(signal) {
493
- const reason = signal.reason;
494
- if (reason instanceof MetabaseError) return reason;
495
- if (reason instanceof Error || typeof reason === "string") return new AbortError(errorMessage(reason) || "aborted");
496
- return new AbortError("aborted");
497
- }
498
416
 
499
417
  //#endregion
500
- //#region src/core/auth/credential.ts
501
- const API_KEY_HEADER = "x-api-key";
502
- const AUTHORIZATION_HEADER = "authorization";
503
- const BEARER_PREFIX = "Bearer ";
504
- const EXPIRY_SKEW_MS = 6e4;
505
- const DEFAULT_EXPIRES_IN_SECONDS = 3600;
506
- function credentialAuthHeader(credential) {
507
- if (credential.kind === "apiKey") return {
508
- name: API_KEY_HEADER,
509
- value: credential.apiKey
510
- };
511
- return {
512
- name: AUTHORIZATION_HEADER,
513
- value: BEARER_PREFIX + credential.accessToken
514
- };
515
- }
516
- function credentialSecrets(credential) {
517
- if (credential.kind === "apiKey") return [credential.apiKey];
518
- return [credential.accessToken, credential.refreshToken];
519
- }
520
- function isOAuthExpired(credential, nowMs) {
521
- const expiryMs = Date.parse(credential.expiresAt);
522
- if (Number.isNaN(expiryMs)) return true;
523
- return expiryMs - EXPIRY_SKEW_MS <= nowMs;
524
- }
525
- function expiresAtFromNow(expiresInSeconds, nowMs) {
526
- return new Date(nowMs + expiresInSeconds * 1e3).toISOString();
418
+ //#region src/core/http/retry.ts
419
+ const DEFAULT_MAX_RETRIES = 3;
420
+ const BASE_DELAY_MS = 250;
421
+ const MAX_DELAY_MS = 8e3;
422
+ const MS_PER_SECOND = 1e3;
423
+ function backoffDelay({ attempt, retryAfterHeader }) {
424
+ const fromHeader = parseRetryAfter(retryAfterHeader);
425
+ if (fromHeader !== null) return fromHeader;
426
+ return Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** attempt);
527
427
  }
528
- function oauthCredentialFromTokens(tokens, refreshToken, clientId, nowMs) {
529
- return {
530
- kind: "oauth",
531
- accessToken: tokens.access_token,
532
- refreshToken,
533
- expiresAt: expiresAtFromNow(tokens.expires_in ?? DEFAULT_EXPIRES_IN_SECONDS, nowMs),
534
- clientId
535
- };
428
+ function parseRetryAfter(header) {
429
+ if (!header) return null;
430
+ const numeric = Number(header);
431
+ if (Number.isFinite(numeric) && numeric >= 0) return Math.round(numeric * MS_PER_SECOND);
432
+ const dateMs = Date.parse(header);
433
+ if (Number.isFinite(dateMs)) return Math.max(0, dateMs - Date.now());
434
+ return null;
536
435
  }
537
-
538
- //#endregion
539
- //#region src/core/http/sanitize.ts
540
- const SECRET_HEADER_NAMES = new Set([
541
- "authorization",
542
- "x-api-key",
543
- "x-metabase-session",
544
- "cookie",
545
- "set-cookie",
546
- "proxy-authorization"
547
- ]);
548
- const REDACTED = "[REDACTED]";
549
- function redactHeaders(headers) {
550
- const result = {};
551
- const entries = headers instanceof Headers ? headers.entries() : Object.entries(headers);
552
- for (const [key, value] of entries) result[key] = SECRET_HEADER_NAMES.has(key.toLowerCase()) ? REDACTED : value;
553
- return result;
436
+ function sleep(ms, signal) {
437
+ return setTimeout(ms, void 0, { signal });
554
438
  }
555
- function redactBody(body, ctx) {
556
- let result = body;
557
- for (const secret of ctx.knownSecrets) {
558
- if (secret.length === 0) continue;
559
- result = result.replaceAll(secret, REDACTED);
560
- }
561
- return result;
562
- }
563
-
564
- //#endregion
565
- //#region src/core/http/errors.ts
566
- const ROUTE_MISSING_LITERAL = "API endpoint does not exist.";
567
- const RESOURCE_MISSING_LITERAL = "Not found.";
568
- const STATUS_CLASSIFICATIONS = {
569
- 401: { retryable: false },
570
- 403: { retryable: false },
571
- 404: { retryable: false },
572
- 408: {
573
- retryable: true,
574
- message: "Metabase timed out responding."
575
- },
576
- 425: { retryable: true },
577
- 429: {
578
- retryable: true,
579
- message: "Metabase rate-limited the request."
580
- },
581
- 500: { retryable: true },
582
- 502: { retryable: true },
583
- 503: { retryable: true },
584
- 504: {
585
- retryable: true,
586
- message: "Metabase timed out responding."
587
- }
588
- };
589
- const ErrorEnvelope = z.object({
590
- message: z.string().optional(),
591
- error: z.string().optional(),
592
- "error-message": z.string().optional(),
593
- via: z.array(z.object({ message: z.string().optional() }).loose()).optional(),
594
- "specific-errors": z.unknown().optional(),
595
- errors: z.unknown().optional()
596
- }).loose();
597
- const MAX_EXTRACTED_MESSAGE_LEN = 500;
598
- const ELLIPSIS = "…";
599
- var HttpError = class extends MetabaseError {
600
- category = "http";
601
- exitCode = 1;
602
- status;
603
- kind;
604
- developerDetail;
605
- constructor(input) {
606
- const sanitizedBody = sanitizeBody(input.rawBody, input.redactionContext);
607
- const redactedHeaders = redactHeaders(input.responseHeaders);
608
- const kind = classifyKind(input.status, sanitizedBody, redactedHeaders);
609
- super(input.overrideUserMessage ?? buildUserMessage(kind, input, sanitizedBody));
610
- this.name = "HttpError";
611
- this.status = input.status;
612
- this.kind = kind;
613
- this.developerDetail = {
614
- status: input.status,
615
- statusText: input.statusText,
616
- method: input.method,
617
- url: input.url,
618
- responseHeaders: redactedHeaders,
619
- body: sanitizedBody
620
- };
621
- }
622
- get isRetryable() {
623
- return isRetryableStatus(this.status);
624
- }
625
- };
626
- function isRetryableStatus(status) {
627
- return STATUS_CLASSIFICATIONS[status]?.retryable === true;
628
- }
629
- function sanitizeBody(rawBody, ctx) {
630
- if (rawBody === null) return null;
631
- if (ctx === void 0) return rawBody;
632
- return redactBody(rawBody, ctx);
633
- }
634
- function classifyKind(status, sanitizedBody, redactedHeaders) {
635
- if (status === 401 || status === 403) return "auth";
636
- if (status === 404) return isRouteMissingResponse(sanitizedBody, redactedHeaders) ? "route-missing" : "resource-missing";
637
- if (status === 429) return "rate-limit";
638
- if (status >= 500 && status < 600) return "server-error";
639
- return "generic";
640
- }
641
- function isRouteMissingResponse(sanitizedBody, redactedHeaders) {
642
- if (sanitizedBody?.includes(ROUTE_MISSING_LITERAL)) return true;
643
- if (sanitizedBody?.includes(RESOURCE_MISSING_LITERAL)) return false;
644
- if (redactedHeaders["content-type"]?.includes(JSON_CONTENT_TYPE)) return false;
645
- if (sanitizedBody === null || sanitizedBody.trim() === "") return true;
646
- return !parseJsonResult(sanitizedBody, ErrorEnvelope).ok;
647
- }
648
- function buildUserMessage(kind, input, sanitizedBody) {
649
- if (kind === "route-missing") return buildRouteMissingMessage(input);
650
- if (kind === "resource-missing") return `Not found: ${input.method} ${pathFromUrl(input.url)}.`;
651
- const fromBody = parseEnvelopeMessage(sanitizedBody);
652
- if (fromBody !== null) return fromBody;
653
- if (kind === "auth") return `Invalid or unauthorized API key (host: ${hostFromUrl(input.url)}).`;
654
- return defaultMessageForStatus(input.status);
655
- }
656
- function buildRouteMissingMessage(input) {
657
- const path = pathFromUrl(input.url);
658
- if (!input.serverTag) return `This endpoint is not available on the connected Metabase: ${input.method} ${path}.`;
659
- return `This endpoint is not available on Metabase ${input.serverTag}: ${input.method} ${path}. The command may require a newer Metabase major version. Run 'mb auth list' to see this server's version.`;
660
- }
661
- function pathFromUrl(url) {
662
- const parsed = new URL(url);
663
- return parsed.pathname + parsed.search;
664
- }
665
- function hostFromUrl(url) {
666
- return new URL(url).host;
667
- }
668
- function parseEnvelopeMessage(sanitizedBody) {
669
- if (!sanitizedBody) return null;
670
- const result = parseJsonResult(sanitizedBody, ErrorEnvelope);
671
- if (!result.ok) return null;
672
- const envelope = result.value;
673
- const topLevel = envelope.message ?? envelope.error ?? envelope["error-message"];
674
- if (topLevel) return capLength(topLevel);
675
- const viaMessage = envelope.via?.find((entry) => entry.message)?.message;
676
- if (viaMessage) return capLength(viaMessage);
677
- const specific = formatErrorTree(envelope["specific-errors"]);
678
- if (specific) return capLength(specific);
679
- const generic = formatErrorTree(envelope.errors);
680
- if (generic) return capLength(generic);
681
- return null;
682
- }
683
- function formatErrorTree(value) {
684
- const entries = collectLeafEntries(value, []);
685
- if (entries.length === 0) return null;
686
- return entries.map(formatLeafEntry).join("; ");
687
- }
688
- function formatLeafEntry(entry) {
689
- return entry.path === "" ? entry.message : `${entry.path}: ${entry.message}`;
690
- }
691
- function collectLeafEntries(value, path) {
692
- if (typeof value === "string") {
693
- const trimmed = value.trim();
694
- return trimmed === "" ? [] : [{
695
- path: path.join("."),
696
- message: trimmed
697
- }];
698
- }
699
- if (Array.isArray(value)) {
700
- const messages = value.filter((entry) => typeof entry === "string" && entry.trim() !== "");
701
- if (messages.length === 0) return [];
702
- return [{
703
- path: path.join("."),
704
- message: messages.join("; ")
705
- }];
706
- }
707
- if (isPlainObject(value)) return Object.entries(value).flatMap(([key, child]) => collectLeafEntries(child, [...path, key]));
708
- return [];
709
- }
710
- function capLength(message) {
711
- if (message.length <= MAX_EXTRACTED_MESSAGE_LEN) return message;
712
- return message.slice(0, MAX_EXTRACTED_MESSAGE_LEN - ELLIPSIS.length) + ELLIPSIS;
713
- }
714
- function defaultMessageForStatus(status) {
715
- return STATUS_CLASSIFICATIONS[status]?.message ?? `Metabase returned ${status}.`;
716
- }
717
-
718
- //#endregion
719
- //#region src/core/http/network-error.ts
720
- const NETWORK_HINTS = {
721
- ECONNREFUSED: (t) => `Connection refused by ${t.host} — is Metabase running and is the port correct?`,
722
- ENOTFOUND: (t) => `Host not found: ${t.hostname} — check the URL.`,
723
- EAI_AGAIN: (t) => `Could not resolve ${t.hostname} — check your network connection and the URL.`,
724
- ECONNRESET: (t) => `Connection to ${t.host} was reset — the server may have closed it, or http/https may be mismatched.`,
725
- ETIMEDOUT: (t) => `Connection to ${t.host} timed out — check the host, port, and your network.`
726
- };
727
- const TLS_ERROR_CODES = new Set([
728
- "EPROTO",
729
- "DEPTH_ZERO_SELF_SIGNED_CERT",
730
- "SELF_SIGNED_CERT_IN_CHAIN",
731
- "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
732
- "CERT_HAS_EXPIRED",
733
- "ERR_TLS_CERT_ALTNAME_INVALID"
734
- ]);
735
- function buildNetworkError(error, method, url) {
736
- const fallback = errorMessage(error);
737
- const code = causeCode(error);
738
- const target = networkTarget(url);
739
- return new NetworkError(networkMessage(code, target, fallback), {
740
- method,
741
- url,
742
- cause: code ?? fallback
743
- });
744
- }
745
- function networkMessage(code, target, fallback) {
746
- if (code === null) return `Could not reach Metabase: ${fallback}`;
747
- if (TLS_ERROR_CODES.has(code)) return `Could not reach Metabase: TLS error contacting ${target.host} (${code}) — the certificate could not be verified, or https:// was used against a plain-HTTP server.`;
748
- const hint = NETWORK_HINTS[code];
749
- if (hint === void 0) return `Could not reach Metabase: ${fallback} (${code})`;
750
- return `Could not reach Metabase: ${hint(target)}`;
751
- }
752
- function causeCode(error) {
753
- const cause = error instanceof Error ? error.cause : void 0;
754
- if (cause instanceof Error && "code" in cause && typeof cause.code === "string") return cause.code;
755
- return null;
756
- }
757
- function networkTarget(url) {
758
- const parsed = new URL(url);
759
- return {
760
- host: parsed.host,
761
- hostname: parsed.hostname
762
- };
763
- }
764
-
765
- //#endregion
766
- //#region src/core/http/retry.ts
767
- const DEFAULT_MAX_RETRIES = 3;
768
- const BASE_DELAY_MS = 250;
769
- const MAX_DELAY_MS = 8e3;
770
- const MS_PER_SECOND = 1e3;
771
- function backoffDelay({ attempt, retryAfterHeader }) {
772
- const fromHeader = parseRetryAfter(retryAfterHeader);
773
- if (fromHeader !== null) return fromHeader;
774
- return Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** attempt);
775
- }
776
- function parseRetryAfter(header) {
777
- if (!header) return null;
778
- const numeric = Number(header);
779
- if (Number.isFinite(numeric) && numeric >= 0) return Math.round(numeric * MS_PER_SECOND);
780
- const dateMs = Date.parse(header);
781
- if (Number.isFinite(dateMs)) return Math.max(0, dateMs - Date.now());
782
- return null;
783
- }
784
- function sleep(ms, signal) {
785
- return setTimeout(ms, void 0, { signal });
786
- }
787
- async function runWithRetries(attempt, signal) {
788
- let attemptIndex = 0;
789
- while (true) {
790
- const outcome = await attempt(attemptIndex);
791
- if (outcome.kind === "success") return outcome.response;
792
- await sleep(outcome.delayMs, signal);
793
- attemptIndex += 1;
439
+ async function runWithRetries(attempt, signal) {
440
+ let attemptIndex = 0;
441
+ while (true) {
442
+ const outcome = await attempt(attemptIndex);
443
+ if (outcome.kind === "success") return outcome.response;
444
+ await sleep(outcome.delayMs, signal);
445
+ attemptIndex += 1;
794
446
  }
795
447
  }
796
448
 
@@ -990,38 +642,612 @@ function buildUrl(baseUrl, path, query) {
990
642
  }
991
643
  return target.toString();
992
644
  }
993
- function acceptHeader(expected) {
994
- if (expected === "json") return JSON_CONTENT_TYPE;
995
- if (expected === "text") return "text/*";
996
- return "*/*";
645
+ function acceptHeader(expected) {
646
+ if (expected === "json") return JSON_CONTENT_TYPE;
647
+ if (expected === "text") return "text/*";
648
+ return "*/*";
649
+ }
650
+ function assertContentType(response, prepared) {
651
+ if (prepared.expectContentType === "binary") return;
652
+ const contentType = response.headers.get("content-type");
653
+ if (contentType === null) throwContentTypeMismatch(response, prepared, prepared.expectContentType);
654
+ if (prepared.expectContentType === "json" && !contentType.includes(JSON_CONTENT_TYPE)) throwContentTypeMismatch(response, prepared, "json");
655
+ if (prepared.expectContentType === "text" && !contentType.startsWith(TEXT_CONTENT_TYPE_PREFIX)) throwContentTypeMismatch(response, prepared, "text");
656
+ }
657
+ function throwContentTypeMismatch(response, prepared, expected) {
658
+ const actual = response.headers.get("content-type") ?? "no content-type";
659
+ throw new HttpError({
660
+ status: response.status,
661
+ statusText: response.statusText,
662
+ method: prepared.method,
663
+ url: prepared.url,
664
+ responseHeaders: response.headers,
665
+ rawBody: null,
666
+ overrideUserMessage: `Expected ${expected} response but got ${actual}`
667
+ });
668
+ }
669
+ async function readBodyForError(response) {
670
+ try {
671
+ const buffer = Buffer.from(await response.arrayBuffer());
672
+ return buffer.subarray(0, ERROR_BODY_BYTE_CAP).toString("utf8");
673
+ } catch (error) {
674
+ return `[body read failed: ${errorMessage(error)}]`;
675
+ }
676
+ }
677
+
678
+ //#endregion
679
+ //#region src/core/http/oauth.ts
680
+ const DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
681
+ const FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
682
+ const OAUTH_TIMEOUT_MS = 3e4;
683
+ async function oauthFetch(request) {
684
+ const timeoutSignal = AbortSignal.timeout(OAUTH_TIMEOUT_MS);
685
+ try {
686
+ return await fetch(request.url, {
687
+ method: request.method,
688
+ headers: {
689
+ ...request.headers,
690
+ "user-agent": USER_AGENT
691
+ },
692
+ ...request.body !== void 0 ? { body: request.body } : {},
693
+ signal: timeoutSignal
694
+ });
695
+ } catch (error) {
696
+ if (timeoutSignal.aborted) throw new TimeoutError(`OAuth request timed out after ${OAUTH_TIMEOUT_MS}ms`, {
697
+ kind: "http",
698
+ method: request.method,
699
+ url: request.url,
700
+ timeoutMs: OAUTH_TIMEOUT_MS
701
+ });
702
+ throw buildNetworkError(error, request.method, request.url);
703
+ }
704
+ }
705
+ const OAUTH_SCOPE = "mb:full";
706
+ const WORKSPACE_MANAGER_SCOPE = "mb:workspace-manager";
707
+ const OAuthServerMetadata = z.object({
708
+ issuer: z.string(),
709
+ authorization_endpoint: z.string(),
710
+ token_endpoint: z.string(),
711
+ registration_endpoint: z.string().optional(),
712
+ revocation_endpoint: z.string().optional(),
713
+ scopes_supported: z.array(z.string()).optional()
714
+ }).loose();
715
+ const RegisteredClient = z.object({ client_id: z.string() }).loose();
716
+ const OAuthTokens = z.object({
717
+ access_token: z.string(),
718
+ token_type: z.string(),
719
+ expires_in: z.number().int().optional(),
720
+ refresh_token: z.string().optional()
721
+ }).loose();
722
+ const OAuthErrorBody = z.object({
723
+ error: z.string(),
724
+ error_description: z.string().optional()
725
+ }).loose();
726
+ async function readJson(response, schema, source) {
727
+ let raw;
728
+ try {
729
+ raw = await response.json();
730
+ } catch (error) {
731
+ throw new ConfigError(`${source}: invalid JSON response (${errorMessage(error)})`);
732
+ }
733
+ const parsed = schema.safeParse(raw);
734
+ if (!parsed.success) throw new ConfigError(`${source}: unexpected response shape`);
735
+ return parsed.data;
736
+ }
737
+ async function failure(response, source, method, url) {
738
+ const rawBody = await response.text().catch(() => "");
739
+ if (isRetryableStatus(response.status)) return new HttpError({
740
+ status: response.status,
741
+ statusText: response.statusText,
742
+ method,
743
+ url,
744
+ responseHeaders: response.headers,
745
+ rawBody
746
+ });
747
+ const parsed = parseJsonResult(rawBody, OAuthErrorBody);
748
+ if (parsed.ok) {
749
+ const detail = parsed.value.error_description ?? parsed.value.error;
750
+ return new ConfigError(`${source} failed (${response.status}): ${detail}`);
751
+ }
752
+ return new ConfigError(`${source} failed (${response.status})`);
753
+ }
754
+ async function postForm(url, params, schema, source) {
755
+ const response = await oauthFetch({
756
+ url,
757
+ method: "POST",
758
+ headers: {
759
+ "content-type": FORM_CONTENT_TYPE,
760
+ accept: JSON_CONTENT_TYPE
761
+ },
762
+ body: new URLSearchParams(params)
763
+ });
764
+ if (!response.ok) throw await failure(response, source, "POST", url);
765
+ return readJson(response, schema, source);
766
+ }
767
+ const OAUTH_UNSUPPORTED_MESSAGE = "this Metabase does not support OAuth login (requires Metabase v63 or newer)";
768
+ async function tryDiscoverMetadata(baseUrl, requiredScope = OAUTH_SCOPE) {
769
+ const url = `${baseUrl}${DISCOVERY_PATH}`;
770
+ const response = await oauthFetch({
771
+ url,
772
+ method: "GET",
773
+ headers: { accept: JSON_CONTENT_TYPE }
774
+ });
775
+ const contentType = response.headers.get("content-type") ?? "";
776
+ if (!response.ok || !contentType.includes("json")) {
777
+ await response.body?.cancel().catch(() => void 0);
778
+ return null;
779
+ }
780
+ const metadata = await readJson(response, OAuthServerMetadata, "OAuth discovery");
781
+ const scopes = metadata.scopes_supported;
782
+ if (scopes !== void 0 && !scopes.includes(requiredScope)) return null;
783
+ assertEndpointOrigin(metadata.issuer, baseUrl, "issuer");
784
+ assertEndpointOrigin(metadata.authorization_endpoint, baseUrl, "authorization endpoint");
785
+ assertEndpointOrigin(metadata.token_endpoint, baseUrl, "token endpoint");
786
+ if (metadata.registration_endpoint !== void 0) assertEndpointOrigin(metadata.registration_endpoint, baseUrl, "registration endpoint");
787
+ if (metadata.revocation_endpoint !== void 0) assertEndpointOrigin(metadata.revocation_endpoint, baseUrl, "revocation endpoint");
788
+ return metadata;
789
+ }
790
+ async function discoverMetadata(baseUrl, requiredScope = OAUTH_SCOPE) {
791
+ const metadata = await tryDiscoverMetadata(baseUrl, requiredScope);
792
+ if (metadata === null) throw new ConfigError(OAUTH_UNSUPPORTED_MESSAGE);
793
+ return metadata;
794
+ }
795
+ async function registerClient(input) {
796
+ const response = await oauthFetch({
797
+ url: input.registrationEndpoint,
798
+ method: "POST",
799
+ headers: {
800
+ "content-type": JSON_CONTENT_TYPE,
801
+ accept: JSON_CONTENT_TYPE
802
+ },
803
+ body: JSON.stringify({
804
+ application_type: "native",
805
+ client_name: input.clientName,
806
+ redirect_uris: [input.redirectUri],
807
+ grant_types: ["authorization_code", "refresh_token"],
808
+ response_types: ["code"],
809
+ token_endpoint_auth_method: "none",
810
+ scope: input.scope
811
+ })
812
+ });
813
+ if (!response.ok) throw await failure(response, "OAuth client registration", "POST", input.registrationEndpoint);
814
+ return readJson(response, RegisteredClient, "OAuth client registration");
815
+ }
816
+ function exchangeCode(input) {
817
+ return postForm(input.tokenEndpoint, {
818
+ grant_type: "authorization_code",
819
+ code: input.code,
820
+ redirect_uri: input.redirectUri,
821
+ client_id: input.clientId,
822
+ code_verifier: input.codeVerifier
823
+ }, OAuthTokens, "OAuth token exchange");
824
+ }
825
+ function refreshTokens(input) {
826
+ return postForm(input.tokenEndpoint, {
827
+ grant_type: "refresh_token",
828
+ refresh_token: input.refreshToken,
829
+ client_id: input.clientId
830
+ }, OAuthTokens, "OAuth token refresh");
831
+ }
832
+ async function revokeToken(input) {
833
+ const response = await oauthFetch({
834
+ url: input.revocationEndpoint,
835
+ method: "POST",
836
+ headers: { "content-type": FORM_CONTENT_TYPE },
837
+ body: new URLSearchParams({
838
+ token: input.token,
839
+ client_id: input.clientId
840
+ })
841
+ });
842
+ if (!response.ok) throw await failure(response, "OAuth token revocation", "POST", input.revocationEndpoint);
843
+ await response.body?.cancel().catch(() => void 0);
844
+ }
845
+
846
+ //#endregion
847
+ //#region src/core/version/tag.ts
848
+ const ParsedVersionSchema = z.object({
849
+ tag: z.string(),
850
+ major: z.number().int().nonnegative(),
851
+ patch: z.number().int().nonnegative()
852
+ });
853
+ function tryParseTag(tag) {
854
+ const parsed = parse(tag);
855
+ if (parsed === null || parsed.major !== 0 && parsed.major !== 1) return null;
856
+ return {
857
+ tag,
858
+ major: parsed.minor,
859
+ patch: parsed.patch
860
+ };
861
+ }
862
+
863
+ //#endregion
864
+ //#region src/core/auth/profile-record.ts
865
+ const ProbedUser = z.object({
866
+ id: z.number().int(),
867
+ name: z.string(),
868
+ isAdmin: z.boolean()
869
+ });
870
+ const ProfileLastProbe = z.object({
871
+ at: z.iso.datetime(),
872
+ version: ParsedVersionSchema.nullable(),
873
+ tokenFeatures: TokenFeatures.nullable(),
874
+ user: ProbedUser
875
+ });
876
+ const ProfileFailureKind = z.enum([
877
+ "auth",
878
+ "network",
879
+ "server"
880
+ ]);
881
+ const ProfileLastFailure = z.object({
882
+ at: z.iso.datetime(),
883
+ kind: ProfileFailureKind,
884
+ reason: z.string()
885
+ });
886
+ const ProfileOAuth = z.object({
887
+ accessToken: z.string().nullable(),
888
+ refreshToken: z.string().nullable(),
889
+ expiresAt: z.iso.datetime(),
890
+ clientId: z.string(),
891
+ scope: z.string().default(OAUTH_SCOPE)
892
+ }).loose();
893
+ const ProfileAuthMethod = z.enum(["oauth", "apiKey"]);
894
+ const ProfileRecord = z.object({
895
+ name: z.string(),
896
+ url: z.string(),
897
+ apiKey: z.string().nullable(),
898
+ oauth: ProfileOAuth.nullable().default(null),
899
+ lastProbe: ProfileLastProbe.nullable(),
900
+ lastFailure: ProfileLastFailure.nullable()
901
+ }).loose();
902
+ const ProfilesFile = z.object({
903
+ profiles: z.array(ProfileRecord),
904
+ defaultProfile: z.string().nullable().default(null)
905
+ }).loose();
906
+ function profileAuthMethod(record) {
907
+ return record.oauth !== null ? "oauth" : "apiKey";
908
+ }
909
+
910
+ //#endregion
911
+ //#region src/core/auth/storage.ts
912
+ const KEYRING_SERVICE = "metabase-cli";
913
+ const PROFILES_FILE = "profiles.json";
914
+ const LEGACY_CREDENTIALS_FILE = "credentials.json";
915
+ const LEGACY_REJECTIONS_FILE = "rejections.json";
916
+ const PROFILES_FILE_MODE = 384;
917
+ const PROFILES_DIR_MODE = 448;
918
+ const DEFAULT_PROFILE = "default";
919
+ const LEGACY_STORAGE_NOTICE = "Old profile storage detected and ignored; re-run `mb auth login` for each profile.";
920
+ const KEYCHAIN_RESIDUAL_NOTICE = "warning: could not remove one or more secrets from the OS keychain; a stored token may remain — remove it manually or retry.";
921
+ const account = {
922
+ profileApiKey: (profile) => `profile:${profile}:apiKey`,
923
+ profileOAuthAccess: (profile) => `profile:${profile}:oauthAccess`,
924
+ profileOAuthRefresh: (profile) => `profile:${profile}:oauthRefresh`
925
+ };
926
+ let legacyWarningPending = false;
927
+ let keychainResidualPending = false;
928
+ let keyringDowngradeNotice = null;
929
+ function profilesFilePath() {
930
+ return join(configDir(), PROFILES_FILE);
931
+ }
932
+ function consumeLegacyStorageWarning() {
933
+ if (!legacyWarningPending) return null;
934
+ legacyWarningPending = false;
935
+ return LEGACY_STORAGE_NOTICE;
936
+ }
937
+ function consumeKeychainResidualWarning() {
938
+ if (!keychainResidualPending) return null;
939
+ keychainResidualPending = false;
940
+ return KEYCHAIN_RESIDUAL_NOTICE;
941
+ }
942
+ function consumeKeyringDowngradeWarning() {
943
+ const message = keyringDowngradeNotice;
944
+ keyringDowngradeNotice = null;
945
+ return message;
946
+ }
947
+ function keyringEnabled() {
948
+ return readEnv(ENV_DISABLE_KEYRING) !== "1";
949
+ }
950
+ function trySetKeyring(key, value) {
951
+ if (!keyringEnabled()) return false;
952
+ try {
953
+ new Entry(KEYRING_SERVICE, key).setPassword(value);
954
+ return true;
955
+ } catch {
956
+ return false;
957
+ }
958
+ }
959
+ function tryReadKeyring(key) {
960
+ if (!keyringEnabled()) return void 0;
961
+ try {
962
+ return new Entry(KEYRING_SERVICE, key).getPassword();
963
+ } catch {
964
+ return void 0;
965
+ }
966
+ }
967
+ function removeKeyringEntry(key) {
968
+ if (!keyringEnabled()) return "skipped";
969
+ try {
970
+ return new Entry(KEYRING_SERVICE, key).deletePassword() ? "removed" : "absent";
971
+ } catch {
972
+ return "failed";
973
+ }
974
+ }
975
+ async function readProfilesFile() {
976
+ const path = profilesFilePath();
977
+ let raw;
978
+ try {
979
+ raw = await promises.readFile(path, "utf8");
980
+ } catch (error) {
981
+ if (isNotFoundError(error)) {
982
+ await detectLegacyArtifacts();
983
+ return {
984
+ profiles: [],
985
+ defaultProfile: null
986
+ };
987
+ }
988
+ throw error;
989
+ }
990
+ const parsed = parseJsonResult(raw, ProfilesFile, { source: path });
991
+ if (parsed.ok) return parsed.value;
992
+ if (parsed.error instanceof ValidationError) {
993
+ legacyWarningPending = true;
994
+ return {
995
+ profiles: [],
996
+ defaultProfile: null
997
+ };
998
+ }
999
+ throw parsed.error;
1000
+ }
1001
+ async function detectLegacyArtifacts() {
1002
+ const legacyCredentials = join(configDir(), LEGACY_CREDENTIALS_FILE);
1003
+ const legacyRejections = join(configDir(), LEGACY_REJECTIONS_FILE);
1004
+ const [credentialsExists, rejectionsExists] = await Promise.all([fileExists(legacyCredentials), fileExists(legacyRejections)]);
1005
+ if (credentialsExists || rejectionsExists) legacyWarningPending = true;
1006
+ }
1007
+ async function fileExists(path) {
1008
+ try {
1009
+ await promises.access(path);
1010
+ return true;
1011
+ } catch {
1012
+ return false;
1013
+ }
1014
+ }
1015
+ async function writeProfilesFile(file) {
1016
+ const path = profilesFilePath();
1017
+ if (file.profiles.length === 0) {
1018
+ await promises.unlink(path).catch(() => void 0);
1019
+ await cleanupLegacyFiles();
1020
+ return;
1021
+ }
1022
+ await promises.mkdir(dirname(path), {
1023
+ recursive: true,
1024
+ mode: PROFILES_DIR_MODE
1025
+ });
1026
+ const tmpPath = `${path}.${process.pid}.tmp`;
1027
+ await promises.writeFile(tmpPath, JSON.stringify(file, null, 2) + "\n", { mode: PROFILES_FILE_MODE });
1028
+ if (process.platform !== "win32") await promises.chmod(tmpPath, PROFILES_FILE_MODE);
1029
+ try {
1030
+ await promises.rename(tmpPath, path);
1031
+ } catch (error) {
1032
+ await promises.unlink(tmpPath).catch(() => void 0);
1033
+ throw error;
1034
+ }
1035
+ await cleanupLegacyFiles();
1036
+ }
1037
+ async function cleanupLegacyFiles() {
1038
+ await Promise.all([promises.unlink(join(configDir(), LEGACY_CREDENTIALS_FILE)).catch(() => void 0), promises.unlink(join(configDir(), LEGACY_REJECTIONS_FILE)).catch(() => void 0)]);
1039
+ }
1040
+ function findRecord(file, name) {
1041
+ return file.profiles.find((entry) => entry.name === name) ?? null;
1042
+ }
1043
+ function sideIsKeyringBacked(record, side) {
1044
+ return side === "oauth" ? record.oauth !== null && record.oauth.accessToken === null : record.oauth === null && record.apiKey === null;
1045
+ }
1046
+ function flagResidualIfUnconfirmed(existing, cleared, removals) {
1047
+ if (existing === null || !removals.includes("failed")) return;
1048
+ if (sideIsKeyringBacked(existing, cleared)) keychainResidualPending = true;
1049
+ }
1050
+ function fileLocation(key) {
1051
+ return {
1052
+ backend: "file",
1053
+ path: profilesFilePath(),
1054
+ account: key,
1055
+ reason: keyringEnabled() ? "unavailable" : "disabled"
1056
+ };
1057
+ }
1058
+ function keyringFallbackWarning(location) {
1059
+ const cause = location.reason === "disabled" ? `OS keychain disabled via ${ENV_DISABLE_KEYRING}` : "OS keychain unavailable";
1060
+ return `warning: ${cause}; credentials stored as plaintext at ${location.path}`;
1061
+ }
1062
+ function persistSecret(key, value) {
1063
+ if (trySetKeyring(key, value)) return {
1064
+ backend: "keyring",
1065
+ service: KEYRING_SERVICE,
1066
+ account: key
1067
+ };
1068
+ removeKeyringEntry(key);
1069
+ return fileLocation(key);
1070
+ }
1071
+ function resolveSecret(key, inline) {
1072
+ if (inline !== null) return inline;
1073
+ return tryReadKeyring(key) ?? null;
1074
+ }
1075
+ async function readProfileCredential(name = DEFAULT_PROFILE) {
1076
+ const file = await readProfilesFile();
1077
+ const record = findRecord(file, name);
1078
+ if (record === null) return null;
1079
+ return resolveRecordCredential(record);
1080
+ }
1081
+ function resolveRecordCredential(record) {
1082
+ if (record.oauth !== null) {
1083
+ const accessToken = resolveSecret(account.profileOAuthAccess(record.name), record.oauth.accessToken);
1084
+ const refreshToken = resolveSecret(account.profileOAuthRefresh(record.name), record.oauth.refreshToken);
1085
+ if (accessToken === null || refreshToken === null) return null;
1086
+ return {
1087
+ url: record.url,
1088
+ credential: {
1089
+ kind: "oauth",
1090
+ accessToken,
1091
+ refreshToken,
1092
+ expiresAt: record.oauth.expiresAt,
1093
+ clientId: record.oauth.clientId,
1094
+ scope: record.oauth.scope
1095
+ }
1096
+ };
1097
+ }
1098
+ const apiKey = resolveSecret(account.profileApiKey(record.name), record.apiKey);
1099
+ if (apiKey === null) return null;
1100
+ return {
1101
+ url: record.url,
1102
+ credential: {
1103
+ kind: "apiKey",
1104
+ apiKey
1105
+ }
1106
+ };
1107
+ }
1108
+ async function readProfileRecord(name = DEFAULT_PROFILE) {
1109
+ const file = await readProfilesFile();
1110
+ return findRecord(file, name);
1111
+ }
1112
+ async function listProfileRecords() {
1113
+ const file = await readProfilesFile();
1114
+ return file.profiles;
1115
+ }
1116
+ async function upsertRecord(file, name, updated) {
1117
+ const exists = findRecord(file, name) !== null;
1118
+ const profiles = exists ? file.profiles.map((entry) => entry.name === name ? updated : entry) : [...file.profiles, updated];
1119
+ await writeProfilesFile({
1120
+ ...file,
1121
+ profiles
1122
+ });
1123
+ }
1124
+ async function writeProfile(profile, name = DEFAULT_PROFILE) {
1125
+ const location = persistSecret(account.profileApiKey(name), profile.apiKey);
1126
+ const inlineApiKey = location.backend === "file" ? profile.apiKey : null;
1127
+ const file = await readProfilesFile();
1128
+ const existing = findRecord(file, name);
1129
+ flagResidualIfUnconfirmed(existing, "oauth", [removeKeyringEntry(account.profileOAuthAccess(name)), removeKeyringEntry(account.profileOAuthRefresh(name))]);
1130
+ const updated = existing === null ? {
1131
+ name,
1132
+ url: profile.url,
1133
+ apiKey: inlineApiKey,
1134
+ oauth: null,
1135
+ lastProbe: null,
1136
+ lastFailure: null
1137
+ } : {
1138
+ ...existing,
1139
+ url: profile.url,
1140
+ apiKey: inlineApiKey,
1141
+ oauth: null
1142
+ };
1143
+ await upsertRecord(file, name, updated);
1144
+ return location;
1145
+ }
1146
+ async function writeOAuthProfile(url, credential, name = DEFAULT_PROFILE) {
1147
+ const accessKey = account.profileOAuthAccess(name);
1148
+ const refreshKey = account.profileOAuthRefresh(name);
1149
+ const accessLocation = persistSecret(accessKey, credential.accessToken);
1150
+ const refreshLocation = persistSecret(refreshKey, credential.refreshToken);
1151
+ const onFile = accessLocation.backend === "file" || refreshLocation.backend === "file";
1152
+ const oauth = {
1153
+ accessToken: onFile ? credential.accessToken : null,
1154
+ refreshToken: onFile ? credential.refreshToken : null,
1155
+ expiresAt: credential.expiresAt,
1156
+ clientId: credential.clientId,
1157
+ scope: credential.scope
1158
+ };
1159
+ const file = await readProfilesFile();
1160
+ const existing = findRecord(file, name);
1161
+ if (onFile && existing !== null && sideIsKeyringBacked(existing, "oauth")) keyringDowngradeNotice = keyringFallbackWarning(fileLocation(accessKey));
1162
+ flagResidualIfUnconfirmed(existing, "apiKey", [removeKeyringEntry(account.profileApiKey(name))]);
1163
+ const updated = existing === null ? {
1164
+ name,
1165
+ url,
1166
+ apiKey: null,
1167
+ oauth,
1168
+ lastProbe: null,
1169
+ lastFailure: null
1170
+ } : {
1171
+ ...existing,
1172
+ url,
1173
+ apiKey: null,
1174
+ oauth
1175
+ };
1176
+ await upsertRecord(file, name, updated);
1177
+ return onFile ? fileLocation(accessKey) : {
1178
+ backend: "keyring",
1179
+ service: KEYRING_SERVICE,
1180
+ account: accessKey
1181
+ };
1182
+ }
1183
+ async function writeProbeResult(name, input) {
1184
+ const probe = ProfileLastProbe.parse({
1185
+ at: new Date().toISOString(),
1186
+ version: input.server.version,
1187
+ tokenFeatures: input.server.tokenFeatures,
1188
+ user: input.user
1189
+ });
1190
+ const file = await readProfilesFile();
1191
+ const existing = findRecord(file, name);
1192
+ if (existing === null) return null;
1193
+ const profiles = file.profiles.map((entry) => entry.name === name ? {
1194
+ ...entry,
1195
+ lastProbe: probe,
1196
+ lastFailure: null
1197
+ } : entry);
1198
+ await writeProfilesFile({
1199
+ ...file,
1200
+ profiles
1201
+ });
1202
+ return probe;
1203
+ }
1204
+ async function writeProbeFailure(name, input) {
1205
+ const failure$1 = ProfileLastFailure.parse({
1206
+ at: new Date().toISOString(),
1207
+ kind: input.kind,
1208
+ reason: input.reason
1209
+ });
1210
+ const file = await readProfilesFile();
1211
+ const existing = findRecord(file, name);
1212
+ if (existing === null) return null;
1213
+ const profiles = file.profiles.map((entry) => entry.name === name ? {
1214
+ ...entry,
1215
+ lastFailure: failure$1
1216
+ } : entry);
1217
+ await writeProfilesFile({
1218
+ ...file,
1219
+ profiles
1220
+ });
1221
+ return failure$1;
1222
+ }
1223
+ async function clearProfile(name = DEFAULT_PROFILE) {
1224
+ const file = await readProfilesFile();
1225
+ const existing = findRecord(file, name);
1226
+ const removals = [
1227
+ removeKeyringEntry(account.profileApiKey(name)),
1228
+ removeKeyringEntry(account.profileOAuthAccess(name)),
1229
+ removeKeyringEntry(account.profileOAuthRefresh(name))
1230
+ ];
1231
+ if (existing !== null) flagResidualIfUnconfirmed(existing, existing.oauth !== null ? "oauth" : "apiKey", removals);
1232
+ if (existing === null) return false;
1233
+ await writeProfilesFile({
1234
+ ...file,
1235
+ profiles: file.profiles.filter((entry) => entry.name !== name),
1236
+ defaultProfile: file.defaultProfile === name ? null : file.defaultProfile
1237
+ });
1238
+ return true;
997
1239
  }
998
- function assertContentType(response, prepared) {
999
- if (prepared.expectContentType === "binary") return;
1000
- const contentType = response.headers.get("content-type");
1001
- if (contentType === null) throwContentTypeMismatch(response, prepared, prepared.expectContentType);
1002
- if (prepared.expectContentType === "json" && !contentType.includes(JSON_CONTENT_TYPE)) throwContentTypeMismatch(response, prepared, "json");
1003
- if (prepared.expectContentType === "text" && !contentType.startsWith(TEXT_CONTENT_TYPE_PREFIX)) throwContentTypeMismatch(response, prepared, "text");
1240
+ async function readDefaultProfileName() {
1241
+ const file = await readProfilesFile();
1242
+ return file.defaultProfile;
1004
1243
  }
1005
- function throwContentTypeMismatch(response, prepared, expected) {
1006
- const actual = response.headers.get("content-type") ?? "no content-type";
1007
- throw new HttpError({
1008
- status: response.status,
1009
- statusText: response.statusText,
1010
- method: prepared.method,
1011
- url: prepared.url,
1012
- responseHeaders: response.headers,
1013
- rawBody: null,
1014
- overrideUserMessage: `Expected ${expected} response but got ${actual}`
1244
+ async function setDefaultProfile(name) {
1245
+ const file = await readProfilesFile();
1246
+ await writeProfilesFile({
1247
+ ...file,
1248
+ defaultProfile: name
1015
1249
  });
1016
1250
  }
1017
- async function readBodyForError(response) {
1018
- try {
1019
- const buffer = Buffer.from(await response.arrayBuffer());
1020
- return buffer.subarray(0, ERROR_BODY_BYTE_CAP).toString("utf8");
1021
- } catch (error) {
1022
- return `[body read failed: ${errorMessage(error)}]`;
1023
- }
1024
- }
1025
1251
 
1026
1252
  //#endregion
1027
1253
  //#region src/core/version/probe.ts
@@ -1039,219 +1265,19 @@ async function probeServer(client, opts = {}) {
1039
1265
  };
1040
1266
  }
1041
1267
 
1042
- //#endregion
1043
- //#region src/core/url.ts
1044
- function normalizeUrl(input) {
1045
- const trimmed = input.trim().replace(/\/+$/, "");
1046
- if (!/^https?:\/\//i.test(trimmed)) throw new ConfigError("URL must start with http:// or https://");
1047
- return trimmed;
1048
- }
1049
- function displayUrl(input) {
1050
- const parsed = new URL(input);
1051
- const path = parsed.pathname.replace(/\/+$/, "");
1052
- return `${parsed.origin}${path}`;
1053
- }
1054
- const LOOPBACK_HOSTS = new Set([
1055
- "localhost",
1056
- "127.0.0.1",
1057
- "::1",
1058
- "[::1]"
1059
- ]);
1060
- function isLoopbackHost(hostname) {
1061
- return LOOPBACK_HOSTS.has(hostname.toLowerCase());
1062
- }
1063
- function assertEndpointOrigin(endpoint, baseUrl, label) {
1064
- let parsed;
1065
- try {
1066
- parsed = new URL(endpoint);
1067
- } catch {
1068
- throw new ConfigError(`OAuth ${label} is not a valid URL: ${endpoint}`);
1069
- }
1070
- const base = new URL(baseUrl);
1071
- if (parsed.protocol !== "https:" && !isLoopbackHost(parsed.hostname)) throw new ConfigError(`OAuth ${label} must use https: ${endpoint}`);
1072
- if (parsed.origin !== base.origin) throw new ConfigError(`OAuth ${label} origin (${parsed.origin}) does not match the Metabase URL (${base.origin})`);
1073
- }
1074
-
1075
- //#endregion
1076
- //#region src/core/http/oauth.ts
1077
- const DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
1078
- const FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
1079
- const OAUTH_TIMEOUT_MS = 3e4;
1080
- async function oauthFetch(request) {
1081
- const timeoutSignal = AbortSignal.timeout(OAUTH_TIMEOUT_MS);
1082
- try {
1083
- return await fetch(request.url, {
1084
- method: request.method,
1085
- headers: {
1086
- ...request.headers,
1087
- "user-agent": USER_AGENT
1088
- },
1089
- ...request.body !== void 0 ? { body: request.body } : {},
1090
- signal: timeoutSignal
1091
- });
1092
- } catch (error) {
1093
- if (timeoutSignal.aborted) throw new TimeoutError(`OAuth request timed out after ${OAUTH_TIMEOUT_MS}ms`, {
1094
- kind: "http",
1095
- method: request.method,
1096
- url: request.url,
1097
- timeoutMs: OAUTH_TIMEOUT_MS
1098
- });
1099
- throw buildNetworkError(error, request.method, request.url);
1100
- }
1101
- }
1102
- const OAUTH_SCOPE = "mb:full";
1103
- const OAuthServerMetadata = z.object({
1104
- issuer: z.string(),
1105
- authorization_endpoint: z.string(),
1106
- token_endpoint: z.string(),
1107
- registration_endpoint: z.string().optional(),
1108
- revocation_endpoint: z.string().optional(),
1109
- scopes_supported: z.array(z.string()).optional()
1110
- }).loose();
1111
- const RegisteredClient = z.object({ client_id: z.string() }).loose();
1112
- const OAuthTokens = z.object({
1113
- access_token: z.string(),
1114
- token_type: z.string(),
1115
- expires_in: z.number().int().optional(),
1116
- refresh_token: z.string().optional()
1117
- }).loose();
1118
- const OAuthErrorBody = z.object({
1119
- error: z.string(),
1120
- error_description: z.string().optional()
1121
- }).loose();
1122
- async function readJson(response, schema, source) {
1123
- let raw;
1124
- try {
1125
- raw = await response.json();
1126
- } catch (error) {
1127
- throw new ConfigError(`${source}: invalid JSON response (${errorMessage(error)})`);
1128
- }
1129
- const parsed = schema.safeParse(raw);
1130
- if (!parsed.success) throw new ConfigError(`${source}: unexpected response shape`);
1131
- return parsed.data;
1132
- }
1133
- async function failure(response, source, method, url) {
1134
- const rawBody = await response.text().catch(() => "");
1135
- if (isRetryableStatus(response.status)) return new HttpError({
1136
- status: response.status,
1137
- statusText: response.statusText,
1138
- method,
1139
- url,
1140
- responseHeaders: response.headers,
1141
- rawBody
1142
- });
1143
- const parsed = parseJsonResult(rawBody, OAuthErrorBody);
1144
- if (parsed.ok) {
1145
- const detail = parsed.value.error_description ?? parsed.value.error;
1146
- return new ConfigError(`${source} failed (${response.status}): ${detail}`);
1147
- }
1148
- return new ConfigError(`${source} failed (${response.status})`);
1149
- }
1150
- async function postForm(url, params, schema, source) {
1151
- const response = await oauthFetch({
1152
- url,
1153
- method: "POST",
1154
- headers: {
1155
- "content-type": FORM_CONTENT_TYPE,
1156
- accept: JSON_CONTENT_TYPE
1157
- },
1158
- body: new URLSearchParams(params)
1159
- });
1160
- if (!response.ok) throw await failure(response, source, "POST", url);
1161
- return readJson(response, schema, source);
1162
- }
1163
- const OAUTH_UNSUPPORTED_MESSAGE = "this Metabase does not support OAuth login (requires Metabase v63 or newer)";
1164
- async function tryDiscoverMetadata(baseUrl) {
1165
- const url = `${baseUrl}${DISCOVERY_PATH}`;
1166
- const response = await oauthFetch({
1167
- url,
1168
- method: "GET",
1169
- headers: { accept: JSON_CONTENT_TYPE }
1170
- });
1171
- const contentType = response.headers.get("content-type") ?? "";
1172
- if (!response.ok || !contentType.includes("json")) {
1173
- await response.body?.cancel().catch(() => void 0);
1174
- return null;
1175
- }
1176
- const metadata = await readJson(response, OAuthServerMetadata, "OAuth discovery");
1177
- const scopes = metadata.scopes_supported;
1178
- if (scopes !== void 0 && !scopes.includes(OAUTH_SCOPE)) return null;
1179
- assertEndpointOrigin(metadata.issuer, baseUrl, "issuer");
1180
- assertEndpointOrigin(metadata.authorization_endpoint, baseUrl, "authorization endpoint");
1181
- assertEndpointOrigin(metadata.token_endpoint, baseUrl, "token endpoint");
1182
- if (metadata.registration_endpoint !== void 0) assertEndpointOrigin(metadata.registration_endpoint, baseUrl, "registration endpoint");
1183
- if (metadata.revocation_endpoint !== void 0) assertEndpointOrigin(metadata.revocation_endpoint, baseUrl, "revocation endpoint");
1184
- return metadata;
1185
- }
1186
- async function discoverMetadata(baseUrl) {
1187
- const metadata = await tryDiscoverMetadata(baseUrl);
1188
- if (metadata === null) throw new ConfigError(OAUTH_UNSUPPORTED_MESSAGE);
1189
- return metadata;
1190
- }
1191
- async function registerClient(input) {
1192
- const response = await oauthFetch({
1193
- url: input.registrationEndpoint,
1194
- method: "POST",
1195
- headers: {
1196
- "content-type": JSON_CONTENT_TYPE,
1197
- accept: JSON_CONTENT_TYPE
1198
- },
1199
- body: JSON.stringify({
1200
- application_type: "native",
1201
- client_name: input.clientName,
1202
- redirect_uris: [input.redirectUri],
1203
- grant_types: ["authorization_code", "refresh_token"],
1204
- response_types: ["code"],
1205
- token_endpoint_auth_method: "none",
1206
- scope: OAUTH_SCOPE
1207
- })
1208
- });
1209
- if (!response.ok) throw await failure(response, "OAuth client registration", "POST", input.registrationEndpoint);
1210
- return readJson(response, RegisteredClient, "OAuth client registration");
1211
- }
1212
- function exchangeCode(input) {
1213
- return postForm(input.tokenEndpoint, {
1214
- grant_type: "authorization_code",
1215
- code: input.code,
1216
- redirect_uri: input.redirectUri,
1217
- client_id: input.clientId,
1218
- code_verifier: input.codeVerifier
1219
- }, OAuthTokens, "OAuth token exchange");
1220
- }
1221
- function refreshTokens(input) {
1222
- return postForm(input.tokenEndpoint, {
1223
- grant_type: "refresh_token",
1224
- refresh_token: input.refreshToken,
1225
- client_id: input.clientId
1226
- }, OAuthTokens, "OAuth token refresh");
1227
- }
1228
- async function revokeToken(input) {
1229
- const response = await oauthFetch({
1230
- url: input.revocationEndpoint,
1231
- method: "POST",
1232
- headers: { "content-type": FORM_CONTENT_TYPE },
1233
- body: new URLSearchParams({
1234
- token: input.token,
1235
- client_id: input.clientId
1236
- })
1237
- });
1238
- if (!response.ok) throw await failure(response, "OAuth token revocation", "POST", input.revocationEndpoint);
1239
- await response.body?.cancel().catch(() => void 0);
1240
- }
1241
-
1242
1268
  //#endregion
1243
1269
  //#region src/core/auth/oauth-session.ts
1244
1270
  async function refreshOAuthCredential(baseUrl, credential, nowMs) {
1245
- const metadata = await discoverMetadata(baseUrl);
1271
+ const metadata = await discoverMetadata(baseUrl, credential.scope);
1246
1272
  const tokens = await refreshTokens({
1247
1273
  tokenEndpoint: metadata.token_endpoint,
1248
1274
  refreshToken: credential.refreshToken,
1249
1275
  clientId: credential.clientId
1250
1276
  });
1251
- return oauthCredentialFromTokens(tokens, tokens.refresh_token ?? credential.refreshToken, credential.clientId, nowMs);
1277
+ return oauthCredentialFromTokens(tokens, tokens.refresh_token ?? credential.refreshToken, credential.clientId, credential.scope, nowMs);
1252
1278
  }
1253
1279
  async function revokeOAuthCredential(baseUrl, credential) {
1254
- const metadata = await discoverMetadata(baseUrl);
1280
+ const metadata = await discoverMetadata(baseUrl, credential.scope);
1255
1281
  const revocationEndpoint = metadata.revocation_endpoint;
1256
1282
  if (revocationEndpoint === void 0) return false;
1257
1283
  await revokeToken({
@@ -1272,8 +1298,10 @@ async function revokeOAuthCredential(baseUrl, credential) {
1272
1298
  function isPreflightSkipped() {
1273
1299
  return readEnv(ENV_SKIP_PREFLIGHT) === "1";
1274
1300
  }
1275
- function resolveProfileName(profileFlag) {
1276
- return explicitProfileName(profileFlag) ?? DEFAULT_PROFILE;
1301
+ async function resolveActiveProfileName(profileFlag) {
1302
+ const explicit = explicitProfileName(profileFlag);
1303
+ if (explicit !== null) return explicit;
1304
+ return await readDefaultProfileName() ?? DEFAULT_PROFILE;
1277
1305
  }
1278
1306
  function explicitProfileName(profileFlag) {
1279
1307
  return profileFlag || readEnv(ENV_PROFILE) || null;
@@ -1285,7 +1313,7 @@ function readEnvCredentials() {
1285
1313
  };
1286
1314
  }
1287
1315
  async function resolveConfig(flags) {
1288
- const profile = resolveProfileName(flags.profile);
1316
+ const profile = await resolveActiveProfileName(flags.profile);
1289
1317
  const env = readEnvCredentials();
1290
1318
  const hasUrl = Boolean(flags.url ?? env.url);
1291
1319
  const hasKey = Boolean(flags.apiKey ?? env.apiKey);
@@ -1615,6 +1643,8 @@ function defineMetabaseCommand(def) {
1615
1643
  getResolvedConfig,
1616
1644
  getServerInfo
1617
1645
  });
1646
+ } catch (error) {
1647
+ throw enrichScopeForbiddenError(error, cachedConfig);
1618
1648
  } finally {
1619
1649
  emitPendingWarnings();
1620
1650
  }
@@ -1633,6 +1663,12 @@ function defineMetabaseCommand(def) {
1633
1663
  });
1634
1664
  return cmd;
1635
1665
  }
1666
+ function enrichScopeForbiddenError(error, config) {
1667
+ if (!(error instanceof HttpError) || error.status !== 403 || config === null) return error;
1668
+ const { credential } = config;
1669
+ if (credential.kind !== "oauth" || credential.scope === OAUTH_SCOPE) return error;
1670
+ return new ConfigError(`${error.userMessage} This profile's login is scoped to ${credential.scope}, which only allows workspace commands against this server. Run \`mb auth login\` for a full-access login, or run content commands against the workspace itself via its ws-<id> profile.`);
1671
+ }
1636
1672
  function emitPendingWarnings() {
1637
1673
  for (const message of consumeLegacyEnvWarnings()) warn(message);
1638
1674
  const legacy = consumeLegacyStorageWarning();
@@ -1692,4 +1728,4 @@ function buildConfigFlags(ctx) {
1692
1728
  }
1693
1729
 
1694
1730
  //#endregion
1695
- export { DEFAULT_PROFILE, HttpError, OAUTH_SCOPE, ParsedVersionSchema, ProbedUser, ProfileAuthMethod, ProfileLastFailure, TokenFeatures, USER_AGENT, clearProfile, combineAborts, consumeKeychainResidualWarning, consumeKeyringDowngradeWarning, createClient, createCredentialRefresher, defineMetabaseCommand, discoverMetadata, displayUrl, exchangeCode, explicitProfileName, keyringFallbackWarning, listProfileRecords, normalizeUrl, oauthCredentialFromTokens, parseCsv, parseEnum, parseEnumCsv, parseInteger, parseJson, parseJsonOrPlain, probeServer, profileAuthMethod, readEnvCredentials, readProfileCredential, readProfileRecord, registerClient, resolveProfileName, resolveRecordCredential, revokeOAuthCredential, throwIfAborted, tryDiscoverMetadata, writeOAuthProfile, writeProbeFailure, writeProbeResult, writeProfile };
1731
+ export { DEFAULT_PROFILE, HttpError, OAUTH_SCOPE, ParsedVersionSchema, ProbedUser, ProfileAuthMethod, ProfileLastFailure, TokenFeatures, USER_AGENT, WORKSPACE_MANAGER_SCOPE, clearProfile, combineAborts, consumeKeychainResidualWarning, consumeKeyringDowngradeWarning, createClient, createCredentialRefresher, defineMetabaseCommand, discoverMetadata, displayUrl, exchangeCode, explicitProfileName, keyringFallbackWarning, listProfileRecords, normalizeUrl, oauthCredentialFromTokens, parseCsv, parseEnum, parseEnumCsv, parseInteger, parseJson, parseJsonOrPlain, probeServer, profileAuthMethod, readEnvCredentials, readProfileCredential, readProfileRecord, registerClient, resolveActiveProfileName, resolveRecordCredential, revokeOAuthCredential, setDefaultProfile, throwIfAborted, tryDiscoverMetadata, writeOAuthProfile, writeProbeFailure, writeProbeResult, writeProfile };