@metabase/cli 0.1.12 → 0.1.13

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 (179) hide show
  1. package/README.md +34 -22
  2. package/dist/{add-collection-DEME4IOy.mjs → add-collection-BQsx2Q-S.mjs} +5 -5
  3. package/dist/add-collection-BiBKEHwz.mjs +10 -0
  4. package/dist/{archive-BtDvBsr8.mjs → archive-BP53zrzs.mjs} +6 -6
  5. package/dist/{archive-Dc-FwNZm.mjs → archive-BS4U2_gi.mjs} +7 -7
  6. package/dist/{archive-DUlrUlok.mjs → archive-BrYMa87Z.mjs} +6 -6
  7. package/dist/{archive-CMeTr8jv.mjs → archive-D7qtYihp.mjs} +6 -6
  8. package/dist/{archive-BFk-oupo.mjs → archive-Dt5zuiwu.mjs} +6 -6
  9. package/dist/{archive-BNioNJtG.mjs → archive-FLrOqODN.mjs} +7 -7
  10. package/dist/{archive-MTTQv3wR.mjs → archive-H8wBIt8q.mjs} +7 -7
  11. package/dist/auth-DWA7DSo9.mjs +19 -0
  12. package/dist/{body-89w3r7In.mjs → body-3vzTm8Jj.mjs} +3 -3
  13. package/dist/{branches-DuVKH8Ye.mjs → branches-Lr1cCUdg.mjs} +6 -6
  14. package/dist/{cancel-BEMQKVuR.mjs → cancel-Br3FHyDR.mjs} +5 -5
  15. package/dist/{cancel-task-D2mSPFXO.mjs → cancel-task-BXgeq3MH.mjs} +6 -6
  16. package/dist/{capabilities-7L9GVMd_.mjs → capabilities-BX1rnVuH.mjs} +2 -14
  17. package/dist/card-DQV8WOIa.mjs +20 -0
  18. package/dist/{card-DnIeMmUn.mjs → card-wCPcuKSi.mjs} +1 -1
  19. package/dist/{cards-L9jksUMD.mjs → cards-D-AkjCxr.mjs} +6 -6
  20. package/dist/cli.mjs +25 -25
  21. package/dist/collection-DIUWkVFY.mjs +20 -0
  22. package/dist/{collection-namespace-w26SavJH.mjs → collection-namespace-BQ_ebqOP.mjs} +2 -2
  23. package/dist/{command-augment-BH9qgQ5u.mjs → command-augment-CAur0XOQ.mjs} +13 -2
  24. package/dist/{create-D6QqOX6u.mjs → create-BpAak62U.mjs} +9 -9
  25. package/dist/{create-Dew3rKsR.mjs → create-C1xYECl-.mjs} +8 -8
  26. package/dist/{create-Cxg_h8Kf.mjs → create-CDL9WHiR.mjs} +10 -10
  27. package/dist/{create-8hMjPz7j.mjs → create-CcZEV9xx.mjs} +8 -8
  28. package/dist/{create-DoeqRVmx.mjs → create-DHUhRzcV.mjs} +11 -11
  29. package/dist/{create-Bu9Jayck.mjs → create-DUdkitlQ.mjs} +12 -12
  30. package/dist/{create-zEq4ZJAT.mjs → create-Dx1SDXyQ.mjs} +8 -8
  31. package/dist/{create-branch-C5P2y8U5.mjs → create-branch-BvZKizC4.mjs} +6 -6
  32. package/dist/{create-C3b3MoIN.mjs → create-ux0yPpq1.mjs} +10 -10
  33. package/dist/{create-DCYc032c.mjs → create-y4j2I1aH.mjs} +11 -11
  34. package/dist/{current-task-uD4QO5bM.mjs → current-task-DaWn0DYt.mjs} +6 -6
  35. package/dist/dashboard-CohHWhkV.mjs +21 -0
  36. package/dist/{database-Dsv4QBs0.mjs → database-BmsxuRmO.mjs} +1 -1
  37. package/dist/db-BD8JZUDw.mjs +22 -0
  38. package/dist/{delete-DGaNAFdQ.mjs → delete-CW3cEHlr.mjs} +7 -7
  39. package/dist/{delete-Bftif2R3.mjs → delete-DFWjBpzl.mjs} +7 -7
  40. package/dist/{delete-runtime-CjFc69tS.mjs → delete-runtime-BhZAAvjc.mjs} +3 -3
  41. package/dist/{delete-table-C0qxP6FE.mjs → delete-table-Wa0yRsnM.mjs} +7 -7
  42. package/dist/{dirty-CZZLpWt1.mjs → dirty-CKcGYIpv.mjs} +6 -6
  43. package/dist/document-CRuag6F5.mjs +19 -0
  44. package/dist/{eid-C8xTmR8e.mjs → eid-m44hs7Ul.mjs} +7 -7
  45. package/dist/{error-BGH4gxGN.mjs → error-DR_i1hxJ.mjs} +40 -5
  46. package/dist/{export-DWdjnBKo.mjs → export-8C4F9pYn.mjs} +8 -8
  47. package/dist/field-CuabjsR7.mjs +18 -0
  48. package/dist/{fields-Cq08PQec.mjs → fields-hXrRjf7e.mjs} +7 -7
  49. package/dist/{get-8nAjRL2H.mjs → get-3zFOkcl_.mjs} +7 -7
  50. package/dist/{get-BaKKF50e.mjs → get-8r-_QuSr.mjs} +6 -6
  51. package/dist/{get-ug26gcOI.mjs → get-9MCrsgTf.mjs} +8 -8
  52. package/dist/{get-Bbij6RjC.mjs → get-BjCQctuS.mjs} +6 -6
  53. package/dist/{get-Bcr_5fAb.mjs → get-BlNLGR-F.mjs} +6 -6
  54. package/dist/{get-Rqz7UHv1.mjs → get-BxgSKzca.mjs} +6 -6
  55. package/dist/{get-f5ENeUNw.mjs → get-C7EKJGTP.mjs} +6 -6
  56. package/dist/{get-DHPN1716.mjs → get-Cit5liJv.mjs} +7 -7
  57. package/dist/{get-DzLAmSKd.mjs → get-CqU4xVSg.mjs} +5 -5
  58. package/dist/{get-B2vGW7Aw.mjs → get-CvLECgtm.mjs} +6 -6
  59. package/dist/{get-DtKv-gl3.mjs → get-D7WUNQYY.mjs} +6 -6
  60. package/dist/{get-7IhxPyE_.mjs → get-DWg3riaY.mjs} +6 -6
  61. package/dist/{get-Cr8oz_jr.mjs → get-cjqlMKF5.mjs} +6 -6
  62. package/dist/{get-run-DhbHXaZE.mjs → get-run-CTDPEDcU.mjs} +6 -6
  63. package/dist/{get-B_BJ10pV.mjs → get-xVPjvf7O.mjs} +6 -6
  64. package/dist/git-sync-CKsGGVOm.mjs +28 -0
  65. package/dist/{has-remote-changes-CTLh3WjZ.mjs → has-remote-changes-DYo4PQfC.mjs} +6 -6
  66. package/dist/{import-XakIQfRp.mjs → import-Cwd-_wYv.mjs} +8 -8
  67. package/dist/{input-BZwm4yby.mjs → input-BXWgdKiS.mjs} +1 -1
  68. package/dist/{is-dirty-DkOOyjsZ.mjs → is-dirty-HynZex-f.mjs} +4 -4
  69. package/dist/is-dirty-Mkgi21bq.mjs +9 -0
  70. package/dist/{items-DC985V8K.mjs → items-D4pSBGxR.mjs} +8 -8
  71. package/dist/{key-y4TtxKwU.mjs → key-C8q5oIr5.mjs} +1 -1
  72. package/dist/{list-CEbMS6g3.mjs → list-B2QX8BZ3.mjs} +5 -5
  73. package/dist/{list-CIEI8XK_.mjs → list-Bqy3rN6k.mjs} +5 -5
  74. package/dist/{list-Df4MhR2A.mjs → list-CI1WQ8U3.mjs} +7 -7
  75. package/dist/{list-xfmTxHNu.mjs → list-Ctze3nfU.mjs} +5 -5
  76. package/dist/{list-Cnb9mQdd.mjs → list-D-Ms9fhd.mjs} +7 -7
  77. package/dist/{list-gy64AAWO.mjs → list-D3BBrpoQ.mjs} +5 -5
  78. package/dist/{list-STlBcPs8.mjs → list-D7o0DOZe.mjs} +5 -5
  79. package/dist/{list-CyIgwqFB.mjs → list-DBjOFesD.mjs} +6 -6
  80. package/dist/{list-B83-hnq5.mjs → list-DMZumpNv.mjs} +5 -5
  81. package/dist/{list-PYNFVJGM.mjs → list-De2qX1w8.mjs} +7 -7
  82. package/dist/{list-CtAFlkLe.mjs → list-ET066OdN.mjs} +5 -5
  83. package/dist/{list-BtNboYpZ.mjs → list-F0BQNbaZ.mjs} +25 -18
  84. package/dist/{list-OGkVAG6b.mjs → list-XgdM17ix.mjs} +6 -6
  85. package/dist/{list-DfPwBQrZ.mjs → list-_xdr1WKh.mjs} +5 -5
  86. package/dist/login-BHDdLJzw.mjs +453 -0
  87. package/dist/{logout-BUgUTFol.mjs → logout-ClizU0OL.mjs} +15 -6
  88. package/dist/{manifest-BPHqF5LY.mjs → manifest-DYeLri80.mjs} +2 -2
  89. package/dist/measure-SmyQt9Lg.mjs +19 -0
  90. package/dist/{metadata-Dwkcsa1c.mjs → metadata-D23RdbEZ.mjs} +8 -8
  91. package/dist/{metadata-B1EmV1l5.mjs → metadata-a_N6Sx30.mjs} +7 -7
  92. package/dist/{parse-enum-Dr7ACrTR.mjs → parse-enum-BatHQ-Gs.mjs} +1 -1
  93. package/dist/{parse-id-sKNP6yu5.mjs → parse-id-B1BlAunl.mjs} +1 -1
  94. package/dist/{parse-ref-BiETXmvm.mjs → parse-ref-CZr1bYIl.mjs} +1 -1
  95. package/dist/{path-BZJR7YLR.mjs → path-FSfPscAQ.mjs} +5 -5
  96. package/dist/{poll-Cyw-dQi4.mjs → poll-Dl5Zv4Ed.mjs} +2 -2
  97. package/dist/{poll-task-BG0oVnfZ.mjs → poll-task-BoRK94WQ.mjs} +2 -2
  98. package/dist/{preflight-BjR63JnX.mjs → preflight-CVlo39xa.mjs} +4 -4
  99. package/dist/process-DsGf7Mg5.mjs +73 -0
  100. package/dist/{prompt-u4WhE4T5.mjs → prompt-Bc_bHSD0.mjs} +13 -3
  101. package/dist/{query-C2xqnz24.mjs → query-CSvpJssD.mjs} +8 -8
  102. package/dist/{query-result-B4kTRid7.mjs → query-result-L5_NrwQR.mjs} +1 -1
  103. package/dist/{query-Dk55Wp2p.mjs → query-yZ9HeaGp.mjs} +12 -12
  104. package/dist/{remove-collection-DbVNWGyb.mjs → remove-collection-CV7ssevw.mjs} +8 -8
  105. package/dist/{render-BQHJhSP1.mjs → render-CkuFkWlQ.mjs} +6 -1
  106. package/dist/{rescan-values-CuLQ9UHP.mjs → rescan-values-CMeOv6yx.mjs} +8 -8
  107. package/dist/{run-PDN33QjN.mjs → run-CIJOYXor.mjs} +8 -8
  108. package/dist/{runs-B9T-7lei.mjs → runs--CzbmfPg.mjs} +7 -7
  109. package/dist/{runtime-DgHh4T6t.mjs → runtime-DJTGfiKO.mjs} +591 -123
  110. package/dist/{schema-tables-DjmNwrlT.mjs → schema-tables-BII9pHR8.mjs} +7 -7
  111. package/dist/{schemas-CWL8gv1C.mjs → schemas-BzxVz71I.mjs} +5 -5
  112. package/dist/{search-D7JzmvfR.mjs → search-CjJZdaM0.mjs} +5 -5
  113. package/dist/segment-dkO4Ualw.mjs +19 -0
  114. package/dist/{set-C0dCLnUA.mjs → set-Ds_brNWu.mjs} +8 -8
  115. package/dist/{setting-Licy7ZSs.mjs → setting-YDJRWd1m.mjs} +3 -3
  116. package/dist/{setup-BL4fqxoU.mjs → setup-DJb-NgSM.mjs} +7 -7
  117. package/dist/{skills-CY_FmaPo.mjs → skills-CJ8g8fhQ.mjs} +1 -1
  118. package/dist/{skills-Cn8FMCxG.mjs → skills-Dn-kn8Wj.mjs} +3 -3
  119. package/dist/snippet-DqTiC73e.mjs +19 -0
  120. package/dist/{stash-SGod4B8-.mjs → stash-DGujowmx.mjs} +8 -8
  121. package/dist/{status-CvUuZVvC.mjs → status-3-Tl07gu.mjs} +14 -6
  122. package/dist/{status-BCb5iCsQ.mjs → status-Cx0TIn1I.mjs} +7 -7
  123. package/dist/{summary-qaTPTLWs.mjs → summary-CT-xFvnX.mjs} +6 -6
  124. package/dist/{sync-schema-CjjAYTbO.mjs → sync-schema-BTF7O3FM.mjs} +10 -10
  125. package/dist/table-DNGPtV4P.mjs +19 -0
  126. package/dist/{table-CkWja2UH.mjs → table-qDD2kApF.mjs} +1 -1
  127. package/dist/transform-DEbvouxo.mjs +24 -0
  128. package/dist/transform-job-Du8npb7F.mjs +19 -0
  129. package/dist/{tree-C0MFcweC.mjs → tree-CLUxXw-Z.mjs} +5 -5
  130. package/dist/{update-DCR0wFNl.mjs → update-6IuvkFCk.mjs} +11 -11
  131. package/dist/{update-CHUUY-IB.mjs → update-BeH_S7h9.mjs} +9 -9
  132. package/dist/{update-BqsGGrzx.mjs → update-BfNmqvuN.mjs} +13 -13
  133. package/dist/{update-BImaz9vj.mjs → update-C_Ui5qPU.mjs} +12 -12
  134. package/dist/{update-CJASCsG4.mjs → update-DD5Q37bz.mjs} +9 -9
  135. package/dist/{update-Dty6LvU2.mjs → update-DdqzxPIr.mjs} +12 -12
  136. package/dist/{update-BTzsE8GU.mjs → update-DpCv5Mdo.mjs} +9 -9
  137. package/dist/{update-BUoWwmQV.mjs → update-I0t_6Mni.mjs} +9 -9
  138. package/dist/{update-vk_M0eOR.mjs → update-Z_LMzsiz.mjs} +11 -11
  139. package/dist/{update-dashcard-BkJaVVvf.mjs → update-dashcard-Bl3wbB8-.mjs} +9 -9
  140. package/dist/{update-qoP7hZTg.mjs → update-tQvEYuzD.mjs} +10 -10
  141. package/dist/{upgrade-TdaiAYbH.mjs → upgrade-r2ZlfstE.mjs} +6 -34
  142. package/dist/{uuid-BeUz8VpP.mjs → uuid-BduDonqb.mjs} +4 -4
  143. package/dist/{validate-B62TRDGV.mjs → validate-VawhJ5Sc.mjs} +2 -2
  144. package/dist/{validate-query-BpiN1CFu.mjs → validate-query-CSV-TTnd.mjs} +3 -3
  145. package/dist/{values-DR-3HcK6.mjs → values-DuNE6Jxc.mjs} +6 -6
  146. package/dist/{verify-D2uBlvmt.mjs → verify-D_yq68-R.mjs} +5 -5
  147. package/dist/{wait-FAnqO-LT.mjs → wait-CoDzO37B.mjs} +7 -7
  148. package/dist/{wait-flags-B5BI_xob.mjs → wait-flags-BxUfF5zy.mjs} +2 -2
  149. package/package.json +1 -1
  150. package/dist/add-collection-BPtBMh8Y.mjs +0 -10
  151. package/dist/auth-CjFnuPe9.mjs +0 -19
  152. package/dist/card-DoDpwR4t.mjs +0 -20
  153. package/dist/collection-CF-1RIPY.mjs +0 -20
  154. package/dist/dashboard-C5zTX345.mjs +0 -21
  155. package/dist/db-4y5Z1CQw.mjs +0 -22
  156. package/dist/document-97l9Non_.mjs +0 -19
  157. package/dist/field-BwUTrelF.mjs +0 -18
  158. package/dist/git-sync-JIMEYW-E.mjs +0 -28
  159. package/dist/is-dirty-4gf8ujws.mjs +0 -9
  160. package/dist/login-C0VFlX8y.mjs +0 -180
  161. package/dist/measure-BmcmxRQ3.mjs +0 -19
  162. package/dist/segment-DrZNuxsM.mjs +0 -19
  163. package/dist/snippet-C5_4nPGj.mjs +0 -19
  164. package/dist/table-DK1B8Hzq.mjs +0 -19
  165. package/dist/transform-Dh5746zH.mjs +0 -24
  166. package/dist/transform-job-D0nRGPYN.mjs +0 -19
  167. /package/dist/{body-flags-D78h_-Ua.mjs → body-flags-D7q87Btw.mjs} +0 -0
  168. /package/dist/{collection-BPTlcFw5.mjs → collection-Deiziuu2.mjs} +0 -0
  169. /package/dist/{dashboard-FY5UzJ_Z.mjs → dashboard-B4bn3z6t.mjs} +0 -0
  170. /package/dist/{document-CcfiiV3b.mjs → document-qfwR0r63.mjs} +0 -0
  171. /package/dist/{field-MGxpNQUH.mjs → field-E0IBy4Uw.mjs} +0 -0
  172. /package/dist/{measure-COGAoZpo.mjs → measure-JJAdFoqK.mjs} +0 -0
  173. /package/dist/{paginate-Dfm9eO9A.mjs → paginate-BexjkjbY.mjs} +0 -0
  174. /package/dist/{revision-message-flag-Bo-Kvv6F.mjs → revision-message-flag-C7zBeWEt.mjs} +0 -0
  175. /package/dist/{segment-d-708KKO.mjs → segment-B9SEv9V_.mjs} +0 -0
  176. /package/dist/{setting-CSJvi1-6.mjs → setting-i0pugNHl.mjs} +0 -0
  177. /package/dist/{snippet-BZvo05ua.mjs → snippet-NNzqlksR.mjs} +0 -0
  178. /package/dist/{transform-job-n8W6tLqF.mjs → transform-job-B9pXBOwr.mjs} +0 -0
  179. /package/dist/{transform--dviwMA1.mjs → transform-qczxOAzn.mjs} +0 -0
@@ -1,6 +1,6 @@
1
- import { AbortError, ConfigError, MetabaseError, NetworkError, ResponseShapeError, TimeoutError, ValidationError, errorMessage, flagConsumesValue, isNotFoundError, normalizeFlag, setMetabaseAugment, toAliasArray } from "./command-augment-BH9qgQ5u.mjs";
2
- import { DEFAULT_MAX_BYTES, package_default, reportError } from "./error-BGH4gxGN.mjs";
3
- import { BASELINE_CAPABILITIES, isPlainObject, warn } from "./capabilities-7L9GVMd_.mjs";
1
+ import { AbortError, ConfigError, MetabaseError, NetworkError, ResponseShapeError, TimeoutError, ValidationError, errorMessage, flagConsumesValue, isNotFoundError, normalizeFlag, setMetabaseAugment, toAliasArray, warn } from "./command-augment-CAur0XOQ.mjs";
2
+ import { DEFAULT_MAX_BYTES, ENV_API_KEY, ENV_DISABLE_KEYRING, ENV_PROFILE, ENV_SKIP_PREFLIGHT, ENV_URL, consumeLegacyEnvWarnings, package_default, readEnv, reportError } from "./error-DR_i1hxJ.mjs";
3
+ import { BASELINE_CAPABILITIES, isPlainObject } from "./capabilities-BX1rnVuH.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { z } from "zod";
6
6
  import { promises } from "node:fs";
@@ -117,14 +117,25 @@ const ProfileLastFailure = z.object({
117
117
  kind: ProfileFailureKind,
118
118
  reason: z.string()
119
119
  });
120
+ const ProfileOAuth = z.object({
121
+ accessToken: z.string().nullable(),
122
+ refreshToken: z.string().nullable(),
123
+ expiresAt: z.iso.datetime(),
124
+ clientId: z.string()
125
+ }).loose();
126
+ const ProfileAuthMethod = z.enum(["oauth", "apiKey"]);
120
127
  const ProfileRecord = z.object({
121
128
  name: z.string(),
122
129
  url: z.string(),
123
130
  apiKey: z.string().nullable(),
131
+ oauth: ProfileOAuth.nullable().default(null),
124
132
  lastProbe: ProfileLastProbe.nullable(),
125
133
  lastFailure: ProfileLastFailure.nullable()
126
- });
127
- const ProfilesFile = z.object({ profiles: z.array(ProfileRecord) });
134
+ }).loose();
135
+ const ProfilesFile = z.object({ profiles: z.array(ProfileRecord) }).loose();
136
+ function profileAuthMethod(record) {
137
+ return record.oauth !== null ? "oauth" : "apiKey";
138
+ }
128
139
 
129
140
  //#endregion
130
141
  //#region src/core/auth/storage.ts
@@ -136,8 +147,15 @@ const PROFILES_FILE_MODE = 384;
136
147
  const PROFILES_DIR_MODE = 448;
137
148
  const DEFAULT_PROFILE = "default";
138
149
  const LEGACY_STORAGE_NOTICE = "Old profile storage detected and ignored; re-run `mb auth login` for each profile.";
139
- const account = { profileApiKey: (profile) => `profile:${profile}:apiKey` };
150
+ 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.";
151
+ const account = {
152
+ profileApiKey: (profile) => `profile:${profile}:apiKey`,
153
+ profileOAuthAccess: (profile) => `profile:${profile}:oauthAccess`,
154
+ profileOAuthRefresh: (profile) => `profile:${profile}:oauthRefresh`
155
+ };
140
156
  let legacyWarningPending = false;
157
+ let keychainResidualPending = false;
158
+ let keyringDowngradeNotice = null;
141
159
  function profilesFilePath() {
142
160
  return join(configDir(), PROFILES_FILE);
143
161
  }
@@ -146,8 +164,18 @@ function consumeLegacyStorageWarning() {
146
164
  legacyWarningPending = false;
147
165
  return LEGACY_STORAGE_NOTICE;
148
166
  }
167
+ function consumeKeychainResidualWarning() {
168
+ if (!keychainResidualPending) return null;
169
+ keychainResidualPending = false;
170
+ return KEYCHAIN_RESIDUAL_NOTICE;
171
+ }
172
+ function consumeKeyringDowngradeWarning() {
173
+ const message = keyringDowngradeNotice;
174
+ keyringDowngradeNotice = null;
175
+ return message;
176
+ }
149
177
  function keyringEnabled() {
150
- return process.env["METABASE_CLI_DISABLE_KEYRING"] !== "1";
178
+ return readEnv(ENV_DISABLE_KEYRING) !== "1";
151
179
  }
152
180
  function trySetKeyring(key, value) {
153
181
  if (!keyringEnabled()) return false;
@@ -166,12 +194,12 @@ function tryReadKeyring(key) {
166
194
  return void 0;
167
195
  }
168
196
  }
169
- function tryRemoveKeyring(key) {
170
- if (!keyringEnabled()) return void 0;
197
+ function removeKeyringEntry(key) {
198
+ if (!keyringEnabled()) return "skipped";
171
199
  try {
172
- return new Entry(KEYRING_SERVICE, key).deletePassword();
200
+ return new Entry(KEYRING_SERVICE, key).deletePassword() ? "removed" : "absent";
173
201
  } catch {
174
- return void 0;
202
+ return "failed";
175
203
  }
176
204
  }
177
205
  async function readProfilesFile() {
@@ -219,8 +247,15 @@ async function writeProfilesFile(file) {
219
247
  recursive: true,
220
248
  mode: PROFILES_DIR_MODE
221
249
  });
222
- await promises.writeFile(path, JSON.stringify(file, null, 2) + "\n", { mode: PROFILES_FILE_MODE });
223
- if (process.platform !== "win32") await promises.chmod(path, PROFILES_FILE_MODE);
250
+ const tmpPath = `${path}.${process.pid}.tmp`;
251
+ await promises.writeFile(tmpPath, JSON.stringify(file, null, 2) + "\n", { mode: PROFILES_FILE_MODE });
252
+ if (process.platform !== "win32") await promises.chmod(tmpPath, PROFILES_FILE_MODE);
253
+ try {
254
+ await promises.rename(tmpPath, path);
255
+ } catch (error) {
256
+ await promises.unlink(tmpPath).catch(() => void 0);
257
+ throw error;
258
+ }
224
259
  await cleanupLegacyFiles();
225
260
  }
226
261
  async function cleanupLegacyFiles() {
@@ -229,6 +264,13 @@ async function cleanupLegacyFiles() {
229
264
  function findRecord(file, name) {
230
265
  return file.profiles.find((entry) => entry.name === name) ?? null;
231
266
  }
267
+ function sideIsKeyringBacked(record, side) {
268
+ return side === "oauth" ? record.oauth !== null && record.oauth.accessToken === null : record.oauth === null && record.apiKey === null;
269
+ }
270
+ function flagResidualIfUnconfirmed(existing, cleared, removals) {
271
+ if (existing === null || !removals.includes("failed")) return;
272
+ if (sideIsKeyringBacked(existing, cleared)) keychainResidualPending = true;
273
+ }
232
274
  function fileLocation(key) {
233
275
  return {
234
276
  backend: "file",
@@ -238,34 +280,54 @@ function fileLocation(key) {
238
280
  };
239
281
  }
240
282
  function keyringFallbackWarning(location) {
241
- const cause = location.reason === "disabled" ? "OS keychain disabled via METABASE_CLI_DISABLE_KEYRING" : "OS keychain unavailable";
283
+ const cause = location.reason === "disabled" ? `OS keychain disabled via ${ENV_DISABLE_KEYRING}` : "OS keychain unavailable";
242
284
  return `warning: ${cause}; credentials stored as plaintext at ${location.path}`;
243
285
  }
244
- async function persistApiKey(name, apiKey) {
245
- const key = account.profileApiKey(name);
246
- if (trySetKeyring(key, apiKey)) return {
286
+ function persistSecret(key, value) {
287
+ if (trySetKeyring(key, value)) return {
247
288
  backend: "keyring",
248
289
  service: KEYRING_SERVICE,
249
290
  account: key
250
291
  };
292
+ removeKeyringEntry(key);
251
293
  return fileLocation(key);
252
294
  }
253
- async function readProfile(name = DEFAULT_PROFILE) {
295
+ function resolveSecret(key, inline) {
296
+ if (inline !== null) return inline;
297
+ return tryReadKeyring(key) ?? null;
298
+ }
299
+ async function readProfileCredential(name = DEFAULT_PROFILE) {
254
300
  const file = await readProfilesFile();
255
301
  const record = findRecord(file, name);
256
302
  if (record === null) return null;
257
- const apiKey = await resolveApiKey(record);
303
+ return resolveRecordCredential(record);
304
+ }
305
+ function resolveRecordCredential(record) {
306
+ if (record.oauth !== null) {
307
+ const accessToken = resolveSecret(account.profileOAuthAccess(record.name), record.oauth.accessToken);
308
+ const refreshToken = resolveSecret(account.profileOAuthRefresh(record.name), record.oauth.refreshToken);
309
+ if (accessToken === null || refreshToken === null) return null;
310
+ return {
311
+ url: record.url,
312
+ credential: {
313
+ kind: "oauth",
314
+ accessToken,
315
+ refreshToken,
316
+ expiresAt: record.oauth.expiresAt,
317
+ clientId: record.oauth.clientId
318
+ }
319
+ };
320
+ }
321
+ const apiKey = resolveSecret(account.profileApiKey(record.name), record.apiKey);
258
322
  if (apiKey === null) return null;
259
323
  return {
260
324
  url: record.url,
261
- apiKey
325
+ credential: {
326
+ kind: "apiKey",
327
+ apiKey
328
+ }
262
329
  };
263
330
  }
264
- async function resolveApiKey(record) {
265
- const fromKeyring = tryReadKeyring(account.profileApiKey(record.name));
266
- if (typeof fromKeyring === "string") return fromKeyring;
267
- return record.apiKey;
268
- }
269
331
  async function readProfileRecord(name = DEFAULT_PROFILE) {
270
332
  const file = await readProfilesFile();
271
333
  return findRecord(file, name);
@@ -274,29 +336,72 @@ async function listProfileRecords() {
274
336
  const file = await readProfilesFile();
275
337
  return file.profiles;
276
338
  }
339
+ async function upsertRecord(file, name, updated) {
340
+ const exists = findRecord(file, name) !== null;
341
+ const profiles = exists ? file.profiles.map((entry) => entry.name === name ? updated : entry) : [...file.profiles, updated];
342
+ await writeProfilesFile({
343
+ ...file,
344
+ profiles
345
+ });
346
+ }
277
347
  async function writeProfile(profile, name = DEFAULT_PROFILE) {
278
- const location = await persistApiKey(name, profile.apiKey);
348
+ const location = persistSecret(account.profileApiKey(name), profile.apiKey);
279
349
  const inlineApiKey = location.backend === "file" ? profile.apiKey : null;
280
350
  const file = await readProfilesFile();
281
351
  const existing = findRecord(file, name);
352
+ flagResidualIfUnconfirmed(existing, "oauth", [removeKeyringEntry(account.profileOAuthAccess(name)), removeKeyringEntry(account.profileOAuthRefresh(name))]);
282
353
  const updated = existing === null ? {
283
354
  name,
284
355
  url: profile.url,
285
356
  apiKey: inlineApiKey,
357
+ oauth: null,
286
358
  lastProbe: null,
287
359
  lastFailure: null
288
360
  } : {
289
361
  ...existing,
290
362
  url: profile.url,
291
- apiKey: inlineApiKey
363
+ apiKey: inlineApiKey,
364
+ oauth: null
292
365
  };
293
- const profiles = existing === null ? [...file.profiles, updated] : file.profiles.map((entry) => entry.name === name ? updated : entry);
294
- await writeProfilesFile({
295
- ...file,
296
- profiles
297
- });
366
+ await upsertRecord(file, name, updated);
298
367
  return location;
299
368
  }
369
+ async function writeOAuthProfile(url, credential, name = DEFAULT_PROFILE) {
370
+ const accessKey = account.profileOAuthAccess(name);
371
+ const refreshKey = account.profileOAuthRefresh(name);
372
+ const accessLocation = persistSecret(accessKey, credential.accessToken);
373
+ const refreshLocation = persistSecret(refreshKey, credential.refreshToken);
374
+ const onFile = accessLocation.backend === "file" || refreshLocation.backend === "file";
375
+ const oauth = {
376
+ accessToken: onFile ? credential.accessToken : null,
377
+ refreshToken: onFile ? credential.refreshToken : null,
378
+ expiresAt: credential.expiresAt,
379
+ clientId: credential.clientId
380
+ };
381
+ const file = await readProfilesFile();
382
+ const existing = findRecord(file, name);
383
+ if (onFile && existing !== null && sideIsKeyringBacked(existing, "oauth")) keyringDowngradeNotice = keyringFallbackWarning(fileLocation(accessKey));
384
+ flagResidualIfUnconfirmed(existing, "apiKey", [removeKeyringEntry(account.profileApiKey(name))]);
385
+ const updated = existing === null ? {
386
+ name,
387
+ url,
388
+ apiKey: null,
389
+ oauth,
390
+ lastProbe: null,
391
+ lastFailure: null
392
+ } : {
393
+ ...existing,
394
+ url,
395
+ apiKey: null,
396
+ oauth
397
+ };
398
+ await upsertRecord(file, name, updated);
399
+ return onFile ? fileLocation(accessKey) : {
400
+ backend: "keyring",
401
+ service: KEYRING_SERVICE,
402
+ account: accessKey
403
+ };
404
+ }
300
405
  async function writeProbeResult(name, input) {
301
406
  const probe = ProfileLastProbe.parse({
302
407
  at: new Date().toISOString(),
@@ -319,7 +424,7 @@ async function writeProbeResult(name, input) {
319
424
  return probe;
320
425
  }
321
426
  async function writeProbeFailure(name, input) {
322
- const failure = ProfileLastFailure.parse({
427
+ const failure$1 = ProfileLastFailure.parse({
323
428
  at: new Date().toISOString(),
324
429
  kind: input.kind,
325
430
  reason: input.reason
@@ -329,18 +434,23 @@ async function writeProbeFailure(name, input) {
329
434
  if (existing === null) return null;
330
435
  const profiles = file.profiles.map((entry) => entry.name === name ? {
331
436
  ...entry,
332
- lastFailure: failure
437
+ lastFailure: failure$1
333
438
  } : entry);
334
439
  await writeProfilesFile({
335
440
  ...file,
336
441
  profiles
337
442
  });
338
- return failure;
443
+ return failure$1;
339
444
  }
340
445
  async function clearProfile(name = DEFAULT_PROFILE) {
341
- tryRemoveKeyring(account.profileApiKey(name));
342
446
  const file = await readProfilesFile();
343
447
  const existing = findRecord(file, name);
448
+ const removals = [
449
+ removeKeyringEntry(account.profileApiKey(name)),
450
+ removeKeyringEntry(account.profileOAuthAccess(name)),
451
+ removeKeyringEntry(account.profileOAuthRefresh(name))
452
+ ];
453
+ if (existing !== null) flagResidualIfUnconfirmed(existing, existing.oauth !== null ? "oauth" : "apiKey", removals);
344
454
  if (existing === null) return false;
345
455
  await writeProfilesFile({
346
456
  ...file,
@@ -387,6 +497,45 @@ function abortReason(signal) {
387
497
  return new AbortError("aborted");
388
498
  }
389
499
 
500
+ //#endregion
501
+ //#region src/core/auth/credential.ts
502
+ const API_KEY_HEADER = "x-api-key";
503
+ const AUTHORIZATION_HEADER = "authorization";
504
+ const BEARER_PREFIX = "Bearer ";
505
+ const EXPIRY_SKEW_MS = 6e4;
506
+ const DEFAULT_EXPIRES_IN_SECONDS = 3600;
507
+ function credentialAuthHeader(credential) {
508
+ if (credential.kind === "apiKey") return {
509
+ name: API_KEY_HEADER,
510
+ value: credential.apiKey
511
+ };
512
+ return {
513
+ name: AUTHORIZATION_HEADER,
514
+ value: BEARER_PREFIX + credential.accessToken
515
+ };
516
+ }
517
+ function credentialSecrets(credential) {
518
+ if (credential.kind === "apiKey") return [credential.apiKey];
519
+ return [credential.accessToken, credential.refreshToken];
520
+ }
521
+ function isOAuthExpired(credential, nowMs) {
522
+ const expiryMs = Date.parse(credential.expiresAt);
523
+ if (Number.isNaN(expiryMs)) return true;
524
+ return expiryMs - EXPIRY_SKEW_MS <= nowMs;
525
+ }
526
+ function expiresAtFromNow(expiresInSeconds, nowMs) {
527
+ return new Date(nowMs + expiresInSeconds * 1e3).toISOString();
528
+ }
529
+ function oauthCredentialFromTokens(tokens, refreshToken, clientId, nowMs) {
530
+ return {
531
+ kind: "oauth",
532
+ accessToken: tokens.access_token,
533
+ refreshToken,
534
+ expiresAt: expiresAtFromNow(tokens.expires_in ?? DEFAULT_EXPIRES_IN_SECONDS, nowMs),
535
+ clientId
536
+ };
537
+ }
538
+
390
539
  //#endregion
391
540
  //#region src/core/http/sanitize.ts
392
541
  const SECRET_HEADER_NAMES = new Set([
@@ -567,6 +716,53 @@ function defaultMessageForStatus(status) {
567
716
  return STATUS_CLASSIFICATIONS[status]?.message ?? `Metabase returned ${status}.`;
568
717
  }
569
718
 
719
+ //#endregion
720
+ //#region src/core/http/network-error.ts
721
+ const NETWORK_HINTS = {
722
+ ECONNREFUSED: (t) => `Connection refused by ${t.host} — is Metabase running and is the port correct?`,
723
+ ENOTFOUND: (t) => `Host not found: ${t.hostname} — check the URL.`,
724
+ EAI_AGAIN: (t) => `Could not resolve ${t.hostname} — check your network connection and the URL.`,
725
+ ECONNRESET: (t) => `Connection to ${t.host} was reset — the server may have closed it, or http/https may be mismatched.`,
726
+ ETIMEDOUT: (t) => `Connection to ${t.host} timed out — check the host, port, and your network.`
727
+ };
728
+ const TLS_ERROR_CODES = new Set([
729
+ "EPROTO",
730
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
731
+ "SELF_SIGNED_CERT_IN_CHAIN",
732
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
733
+ "CERT_HAS_EXPIRED",
734
+ "ERR_TLS_CERT_ALTNAME_INVALID"
735
+ ]);
736
+ function buildNetworkError(error, method, url) {
737
+ const fallback = errorMessage(error);
738
+ const code = causeCode(error);
739
+ const target = networkTarget(url);
740
+ return new NetworkError(networkMessage(code, target, fallback), {
741
+ method,
742
+ url,
743
+ cause: code ?? fallback
744
+ });
745
+ }
746
+ function networkMessage(code, target, fallback) {
747
+ if (code === null) return `Could not reach Metabase: ${fallback}`;
748
+ 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.`;
749
+ const hint = NETWORK_HINTS[code];
750
+ if (hint === void 0) return `Could not reach Metabase: ${fallback} (${code})`;
751
+ return `Could not reach Metabase: ${hint(target)}`;
752
+ }
753
+ function causeCode(error) {
754
+ const cause = error instanceof Error ? error.cause : void 0;
755
+ if (cause instanceof Error && "code" in cause && typeof cause.code === "string") return cause.code;
756
+ return null;
757
+ }
758
+ function networkTarget(url) {
759
+ const parsed = new URL(url);
760
+ return {
761
+ host: parsed.host,
762
+ hostname: parsed.hostname
763
+ };
764
+ }
765
+
570
766
  //#endregion
571
767
  //#region src/core/http/retry.ts
572
768
  const DEFAULT_MAX_RETRIES = 3;
@@ -601,6 +797,7 @@ async function runWithRetries(attempt, signal) {
601
797
 
602
798
  //#endregion
603
799
  //#region src/core/http/client.ts
800
+ const UNAUTHORIZED_STATUS = 401;
604
801
  const DEFAULT_TIMEOUT_MS = 3e4;
605
802
  const OCTET_STREAM_CONTENT_TYPE = "application/octet-stream";
606
803
  const TEXT_CONTENT_TYPE_PREFIX = "text/";
@@ -615,7 +812,26 @@ const NO_SERVER_TAG = async () => null;
615
812
  function createClient(config, overrides = {}) {
616
813
  const fetchImpl = overrides.fetchImpl ?? globalThis.fetch.bind(globalThis);
617
814
  const getServerTag = overrides.getServerTag ?? NO_SERVER_TAG;
618
- const redactionContext = { knownSecrets: new Set([config.apiKey]) };
815
+ const refreshCredential = overrides.refreshCredential;
816
+ let credential = config.credential;
817
+ const knownSecrets = new Set(credentialSecrets(credential));
818
+ const redactionContext = { knownSecrets };
819
+ let refreshInFlight = null;
820
+ function tryRefreshCredential() {
821
+ const refresh = refreshCredential;
822
+ if (credential.kind !== "oauth" || refresh === void 0) return Promise.resolve(false);
823
+ refreshInFlight ??= refreshOnce(refresh).finally(() => {
824
+ refreshInFlight = null;
825
+ });
826
+ return refreshInFlight;
827
+ }
828
+ async function refreshOnce(refresh) {
829
+ const next = await refresh();
830
+ if (next === null) return false;
831
+ credential = next;
832
+ for (const secret of credentialSecrets(next)) knownSecrets.add(secret);
833
+ return true;
834
+ }
619
835
  async function attemptOnce(prepared, attempt) {
620
836
  const hasRetriesLeft = attempt < prepared.retries;
621
837
  const timeoutSignal = AbortSignal.timeout(prepared.timeoutMs);
@@ -677,12 +893,33 @@ function createClient(config, overrides = {}) {
677
893
  async function executeRaw(prepared) {
678
894
  return runWithRetries((attempt) => attemptOnce(prepared, attempt), prepared.callerSignal);
679
895
  }
896
+ async function executeWithAuthRefresh(path, opts) {
897
+ const prepared = prepare(path, opts);
898
+ try {
899
+ return {
900
+ response: await executeRaw(prepared),
901
+ prepared
902
+ };
903
+ } catch (error) {
904
+ if (error instanceof HttpError && error.status === UNAUTHORIZED_STATUS) {
905
+ if (await tryRefreshCredential()) {
906
+ const retried = prepare(path, opts);
907
+ return {
908
+ response: await executeRaw(retried),
909
+ prepared: retried
910
+ };
911
+ }
912
+ }
913
+ throw error;
914
+ }
915
+ }
680
916
  function prepare(path, opts = {}) {
681
917
  const method = opts.method ?? "GET";
682
918
  const expectContentType = opts.expectContentType ?? "json";
683
919
  const url = buildUrl(config.url, path, opts.query);
684
920
  const headers = new Headers();
685
- headers.set("x-api-key", config.apiKey);
921
+ const auth = credentialAuthHeader(credential);
922
+ headers.set(auth.name, auth.value);
686
923
  headers.set("accept", acceptHeader(expectContentType));
687
924
  headers.set("user-agent", USER_AGENT);
688
925
  let body = null;
@@ -709,14 +946,13 @@ function createClient(config, overrides = {}) {
709
946
  }
710
947
  return {
711
948
  async requestRaw(path, opts) {
712
- return executeRaw(prepare(path, opts));
949
+ return (await executeWithAuthRefresh(path, opts ?? {})).response;
713
950
  },
714
951
  async requestParsed(schema, path, opts) {
715
- const prepared = prepare(path, {
952
+ const { response, prepared } = await executeWithAuthRefresh(path, {
716
953
  ...opts,
717
954
  expectContentType: "json"
718
955
  });
719
- const response = await executeRaw(prepared);
720
956
  const text = await response.text();
721
957
  try {
722
958
  return parseJson(text, schema, { source: prepared.url });
@@ -732,11 +968,10 @@ function createClient(config, overrides = {}) {
732
968
  }
733
969
  },
734
970
  async requestStream(path, opts) {
735
- const prepared = prepare(path, {
971
+ const { response, prepared } = await executeWithAuthRefresh(path, {
736
972
  ...opts,
737
973
  expectContentType: opts?.expectContentType ?? "binary"
738
974
  });
739
- const response = await executeRaw(prepared);
740
975
  if (!response.body) throw new NetworkError("Response had no body to stream", {
741
976
  method: prepared.method,
742
977
  url: prepared.url,
@@ -780,50 +1015,6 @@ function throwContentTypeMismatch(response, prepared, expected) {
780
1015
  overrideUserMessage: `Expected ${expected} response but got ${actual}`
781
1016
  });
782
1017
  }
783
- const NETWORK_HINTS = {
784
- ECONNREFUSED: (t) => `Connection refused by ${t.host} — is Metabase running and is the port correct?`,
785
- ENOTFOUND: (t) => `Host not found: ${t.hostname} — check the URL.`,
786
- EAI_AGAIN: (t) => `Could not resolve ${t.hostname} — check your network connection and the URL.`,
787
- ECONNRESET: (t) => `Connection to ${t.host} was reset — the server may have closed it, or http/https may be mismatched.`,
788
- ETIMEDOUT: (t) => `Connection to ${t.host} timed out — check the host, port, and your network.`
789
- };
790
- const TLS_ERROR_CODES = new Set([
791
- "EPROTO",
792
- "DEPTH_ZERO_SELF_SIGNED_CERT",
793
- "SELF_SIGNED_CERT_IN_CHAIN",
794
- "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
795
- "CERT_HAS_EXPIRED",
796
- "ERR_TLS_CERT_ALTNAME_INVALID"
797
- ]);
798
- function buildNetworkError(error, method, url) {
799
- const fallback = errorMessage(error);
800
- const code = causeCode(error);
801
- const target = networkTarget(url);
802
- return new NetworkError(networkMessage(code, target, fallback), {
803
- method,
804
- url,
805
- cause: code ?? fallback
806
- });
807
- }
808
- function networkMessage(code, target, fallback) {
809
- if (code === null) return `Could not reach Metabase: ${fallback}`;
810
- 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.`;
811
- const hint = NETWORK_HINTS[code];
812
- if (hint === void 0) return `Could not reach Metabase: ${fallback} (${code})`;
813
- return `Could not reach Metabase: ${hint(target)}`;
814
- }
815
- function causeCode(error) {
816
- const cause = error instanceof Error ? error.cause : void 0;
817
- if (cause instanceof Error && "code" in cause && typeof cause.code === "string") return cause.code;
818
- return null;
819
- }
820
- function networkTarget(url) {
821
- const parsed = new URL(url);
822
- return {
823
- host: parsed.host,
824
- hostname: parsed.hostname
825
- };
826
- }
827
1018
  async function readBodyForError(response) {
828
1019
  try {
829
1020
  const buffer = Buffer.from(await response.arrayBuffer());
@@ -856,52 +1047,302 @@ function normalizeUrl(input) {
856
1047
  if (!/^https?:\/\//i.test(trimmed)) throw new ConfigError("URL must start with http:// or https://");
857
1048
  return trimmed;
858
1049
  }
859
- function originOnly(input) {
1050
+ function displayUrl(input) {
860
1051
  const parsed = new URL(input);
861
- parsed.username = "";
862
- parsed.password = "";
863
- return parsed.origin;
1052
+ const path = parsed.pathname.replace(/\/+$/, "");
1053
+ return `${parsed.origin}${path}`;
1054
+ }
1055
+ const LOOPBACK_HOSTS = new Set([
1056
+ "localhost",
1057
+ "127.0.0.1",
1058
+ "::1",
1059
+ "[::1]"
1060
+ ]);
1061
+ function isLoopbackHost(hostname) {
1062
+ return LOOPBACK_HOSTS.has(hostname.toLowerCase());
1063
+ }
1064
+ function assertEndpointOrigin(endpoint, baseUrl, label) {
1065
+ let parsed;
1066
+ try {
1067
+ parsed = new URL(endpoint);
1068
+ } catch {
1069
+ throw new ConfigError(`OAuth ${label} is not a valid URL: ${endpoint}`);
1070
+ }
1071
+ const base = new URL(baseUrl);
1072
+ if (parsed.protocol !== "https:" && !isLoopbackHost(parsed.hostname)) throw new ConfigError(`OAuth ${label} must use https: ${endpoint}`);
1073
+ if (parsed.origin !== base.origin) throw new ConfigError(`OAuth ${label} origin (${parsed.origin}) does not match the Metabase URL (${base.origin})`);
1074
+ }
1075
+
1076
+ //#endregion
1077
+ //#region src/core/http/oauth.ts
1078
+ const DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
1079
+ const FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
1080
+ const OAUTH_TIMEOUT_MS = 3e4;
1081
+ async function oauthFetch(request) {
1082
+ const timeoutSignal = AbortSignal.timeout(OAUTH_TIMEOUT_MS);
1083
+ try {
1084
+ return await fetch(request.url, {
1085
+ method: request.method,
1086
+ headers: {
1087
+ ...request.headers,
1088
+ "user-agent": USER_AGENT
1089
+ },
1090
+ ...request.body !== void 0 ? { body: request.body } : {},
1091
+ signal: timeoutSignal
1092
+ });
1093
+ } catch (error) {
1094
+ if (timeoutSignal.aborted) throw new TimeoutError(`OAuth request timed out after ${OAUTH_TIMEOUT_MS}ms`, {
1095
+ kind: "http",
1096
+ method: request.method,
1097
+ url: request.url,
1098
+ timeoutMs: OAUTH_TIMEOUT_MS
1099
+ });
1100
+ throw buildNetworkError(error, request.method, request.url);
1101
+ }
1102
+ }
1103
+ const OAUTH_SCOPE = "mb:full";
1104
+ const OAuthServerMetadata = z.object({
1105
+ issuer: z.string(),
1106
+ authorization_endpoint: z.string(),
1107
+ token_endpoint: z.string(),
1108
+ registration_endpoint: z.string().optional(),
1109
+ revocation_endpoint: z.string().optional(),
1110
+ scopes_supported: z.array(z.string()).optional()
1111
+ }).loose();
1112
+ const RegisteredClient = z.object({ client_id: z.string() }).loose();
1113
+ const OAuthTokens = z.object({
1114
+ access_token: z.string(),
1115
+ token_type: z.string(),
1116
+ expires_in: z.number().int().optional(),
1117
+ refresh_token: z.string().optional()
1118
+ }).loose();
1119
+ const OAuthErrorBody = z.object({
1120
+ error: z.string(),
1121
+ error_description: z.string().optional()
1122
+ }).loose();
1123
+ async function readJson(response, schema, source) {
1124
+ let raw;
1125
+ try {
1126
+ raw = await response.json();
1127
+ } catch (error) {
1128
+ throw new ConfigError(`${source}: invalid JSON response (${errorMessage(error)})`);
1129
+ }
1130
+ const parsed = schema.safeParse(raw);
1131
+ if (!parsed.success) throw new ConfigError(`${source}: unexpected response shape`);
1132
+ return parsed.data;
1133
+ }
1134
+ async function failure(response, source, method, url) {
1135
+ const rawBody = await response.text().catch(() => "");
1136
+ if (isRetryableStatus(response.status)) return new HttpError({
1137
+ status: response.status,
1138
+ statusText: response.statusText,
1139
+ method,
1140
+ url,
1141
+ responseHeaders: response.headers,
1142
+ rawBody
1143
+ });
1144
+ const parsed = parseJsonResult(rawBody, OAuthErrorBody);
1145
+ if (parsed.ok) {
1146
+ const detail = parsed.value.error_description ?? parsed.value.error;
1147
+ return new ConfigError(`${source} failed (${response.status}): ${detail}`);
1148
+ }
1149
+ return new ConfigError(`${source} failed (${response.status})`);
1150
+ }
1151
+ async function postForm(url, params, schema, source) {
1152
+ const response = await oauthFetch({
1153
+ url,
1154
+ method: "POST",
1155
+ headers: {
1156
+ "content-type": FORM_CONTENT_TYPE,
1157
+ accept: JSON_CONTENT_TYPE
1158
+ },
1159
+ body: new URLSearchParams(params)
1160
+ });
1161
+ if (!response.ok) throw await failure(response, source, "POST", url);
1162
+ return readJson(response, schema, source);
1163
+ }
1164
+ const OAUTH_UNSUPPORTED_MESSAGE = "this Metabase does not support OAuth login (requires Metabase v63 or newer)";
1165
+ async function tryDiscoverMetadata(baseUrl) {
1166
+ const url = `${baseUrl}${DISCOVERY_PATH}`;
1167
+ const response = await oauthFetch({
1168
+ url,
1169
+ method: "GET",
1170
+ headers: { accept: JSON_CONTENT_TYPE }
1171
+ });
1172
+ const contentType = response.headers.get("content-type") ?? "";
1173
+ if (!response.ok || !contentType.includes("json")) {
1174
+ await response.body?.cancel().catch(() => void 0);
1175
+ return null;
1176
+ }
1177
+ const metadata = await readJson(response, OAuthServerMetadata, "OAuth discovery");
1178
+ const scopes = metadata.scopes_supported;
1179
+ if (scopes !== void 0 && !scopes.includes(OAUTH_SCOPE)) return null;
1180
+ assertEndpointOrigin(metadata.issuer, baseUrl, "issuer");
1181
+ assertEndpointOrigin(metadata.authorization_endpoint, baseUrl, "authorization endpoint");
1182
+ assertEndpointOrigin(metadata.token_endpoint, baseUrl, "token endpoint");
1183
+ if (metadata.registration_endpoint !== void 0) assertEndpointOrigin(metadata.registration_endpoint, baseUrl, "registration endpoint");
1184
+ if (metadata.revocation_endpoint !== void 0) assertEndpointOrigin(metadata.revocation_endpoint, baseUrl, "revocation endpoint");
1185
+ return metadata;
1186
+ }
1187
+ async function discoverMetadata(baseUrl) {
1188
+ const metadata = await tryDiscoverMetadata(baseUrl);
1189
+ if (metadata === null) throw new ConfigError(OAUTH_UNSUPPORTED_MESSAGE);
1190
+ return metadata;
1191
+ }
1192
+ async function registerClient(input) {
1193
+ const response = await oauthFetch({
1194
+ url: input.registrationEndpoint,
1195
+ method: "POST",
1196
+ headers: {
1197
+ "content-type": JSON_CONTENT_TYPE,
1198
+ accept: JSON_CONTENT_TYPE
1199
+ },
1200
+ body: JSON.stringify({
1201
+ application_type: "native",
1202
+ client_name: input.clientName,
1203
+ redirect_uris: [input.redirectUri],
1204
+ grant_types: ["authorization_code", "refresh_token"],
1205
+ response_types: ["code"],
1206
+ token_endpoint_auth_method: "none",
1207
+ scope: OAUTH_SCOPE
1208
+ })
1209
+ });
1210
+ if (!response.ok) throw await failure(response, "OAuth client registration", "POST", input.registrationEndpoint);
1211
+ return readJson(response, RegisteredClient, "OAuth client registration");
1212
+ }
1213
+ function exchangeCode(input) {
1214
+ return postForm(input.tokenEndpoint, {
1215
+ grant_type: "authorization_code",
1216
+ code: input.code,
1217
+ redirect_uri: input.redirectUri,
1218
+ client_id: input.clientId,
1219
+ code_verifier: input.codeVerifier
1220
+ }, OAuthTokens, "OAuth token exchange");
1221
+ }
1222
+ function refreshTokens(input) {
1223
+ return postForm(input.tokenEndpoint, {
1224
+ grant_type: "refresh_token",
1225
+ refresh_token: input.refreshToken,
1226
+ client_id: input.clientId
1227
+ }, OAuthTokens, "OAuth token refresh");
1228
+ }
1229
+ async function revokeToken(input) {
1230
+ const response = await oauthFetch({
1231
+ url: input.revocationEndpoint,
1232
+ method: "POST",
1233
+ headers: { "content-type": FORM_CONTENT_TYPE },
1234
+ body: new URLSearchParams({
1235
+ token: input.token,
1236
+ client_id: input.clientId
1237
+ })
1238
+ });
1239
+ if (!response.ok) throw await failure(response, "OAuth token revocation", "POST", input.revocationEndpoint);
1240
+ await response.body?.cancel().catch(() => void 0);
1241
+ }
1242
+
1243
+ //#endregion
1244
+ //#region src/core/auth/oauth-session.ts
1245
+ async function refreshOAuthCredential(baseUrl, credential, nowMs) {
1246
+ const metadata = await discoverMetadata(baseUrl);
1247
+ const tokens = await refreshTokens({
1248
+ tokenEndpoint: metadata.token_endpoint,
1249
+ refreshToken: credential.refreshToken,
1250
+ clientId: credential.clientId
1251
+ });
1252
+ return oauthCredentialFromTokens(tokens, tokens.refresh_token ?? credential.refreshToken, credential.clientId, nowMs);
1253
+ }
1254
+ async function revokeOAuthCredential(baseUrl, credential) {
1255
+ const metadata = await discoverMetadata(baseUrl);
1256
+ const revocationEndpoint = metadata.revocation_endpoint;
1257
+ if (revocationEndpoint === void 0) return false;
1258
+ await revokeToken({
1259
+ revocationEndpoint,
1260
+ token: credential.refreshToken,
1261
+ clientId: credential.clientId
1262
+ });
1263
+ await revokeToken({
1264
+ revocationEndpoint,
1265
+ token: credential.accessToken,
1266
+ clientId: credential.clientId
1267
+ });
1268
+ return true;
864
1269
  }
865
1270
 
866
1271
  //#endregion
867
1272
  //#region src/core/config.ts
868
- const ENV_URL = "METABASE_URL";
869
- const ENV_API_KEY = "METABASE_API_KEY";
870
- const ENV_PROFILE = "METABASE_PROFILE";
871
- const ENV_SKIP_PREFLIGHT = "METABASE_CLI_SKIP_PREFLIGHT";
872
1273
  function isPreflightSkipped() {
873
- return process.env[ENV_SKIP_PREFLIGHT] === "1";
1274
+ return readEnv(ENV_SKIP_PREFLIGHT) === "1";
874
1275
  }
875
1276
  function resolveProfileName(profileFlag) {
876
1277
  return explicitProfileName(profileFlag) ?? DEFAULT_PROFILE;
877
1278
  }
878
1279
  function explicitProfileName(profileFlag) {
879
- return profileFlag || process.env[ENV_PROFILE] || null;
1280
+ return profileFlag || readEnv(ENV_PROFILE) || null;
880
1281
  }
881
1282
  function readEnvCredentials() {
882
1283
  return {
883
- url: process.env[ENV_URL] ?? null,
884
- apiKey: process.env[ENV_API_KEY] ?? null
1284
+ url: readEnv(ENV_URL) ?? null,
1285
+ apiKey: readEnv(ENV_API_KEY) ?? null
885
1286
  };
886
1287
  }
887
1288
  async function resolveConfig(flags) {
888
1289
  const profile = resolveProfileName(flags.profile);
889
1290
  const env = readEnvCredentials();
890
- const flagUrl = flags.url;
891
- const flagKey = flags.apiKey;
892
- const needsStored = !flagUrl && !env.url || !flagKey && !env.apiKey;
893
- const stored = needsStored ? await readProfile(profile) : null;
894
- const urlField = pickField(flagUrl, env.url, stored?.url);
895
- const keyField = pickField(flagKey, env.apiKey, stored?.apiKey);
896
- if (urlField === null || keyField === null) {
1291
+ const hasUrl = Boolean(flags.url ?? env.url);
1292
+ const hasKey = Boolean(flags.apiKey ?? env.apiKey);
1293
+ const stored = hasUrl && hasKey ? null : await readProfileCredential(profile);
1294
+ const url = resolveUrl(flags.url, env.url, stored?.url);
1295
+ const credential = resolveCredential(flags.apiKey, env.apiKey, stored?.credential);
1296
+ if (url === null || credential === null) {
897
1297
  const hint = await failureHintForProfile(profile);
898
1298
  throw new ConfigError(`Not authenticated for profile "${profile}". Run \`mb auth login\`, set ${ENV_URL}/${ENV_API_KEY}, or pass --url/--api-key.${hint}`);
899
1299
  }
1300
+ const normalizedUrl = normalizeUrl(url.value);
1301
+ assertOAuthUrlMatchesIssuer(credential.credential, stored?.url, normalizedUrl, profile);
1302
+ const fresh = await ensureFreshCredential(profile, credential.credential, stored?.url);
900
1303
  return {
901
- url: normalizeUrl(urlField.value),
902
- apiKey: keyField.value,
1304
+ url: normalizedUrl,
1305
+ credential: fresh,
903
1306
  profile,
904
- source: urlField.source === keyField.source ? urlField.source : "mixed"
1307
+ source: url.source === credential.source ? url.source : "mixed"
1308
+ };
1309
+ }
1310
+ function assertOAuthUrlMatchesIssuer(credential, issuerUrl, requestUrl, profile) {
1311
+ if (credential.kind !== "oauth" || issuerUrl === void 0) return;
1312
+ const issuer = normalizeUrl(issuerUrl);
1313
+ if (issuer === requestUrl) return;
1314
+ throw new ConfigError(`profile "${profile}" is a browser-login (OAuth) profile bound to ${issuer}, but the request URL is ${requestUrl}. Drop --url/${ENV_URL} to use the profile's own URL, or run \`mb auth login --url ${requestUrl}\` to authenticate there.`);
1315
+ }
1316
+ async function ensureFreshCredential(profile, credential, issuerUrl) {
1317
+ if (credential.kind !== "oauth" || issuerUrl === void 0) return credential;
1318
+ if (!isOAuthExpired(credential, Date.now())) return credential;
1319
+ let refreshed;
1320
+ try {
1321
+ refreshed = await refreshOAuthCredential(issuerUrl, credential, Date.now());
1322
+ } catch {
1323
+ return credential;
1324
+ }
1325
+ await persistRefreshed(profile, issuerUrl, refreshed);
1326
+ return refreshed;
1327
+ }
1328
+ async function refreshAndPersist(profile, issuerUrl, credential) {
1329
+ const refreshed = await refreshOAuthCredential(issuerUrl, credential, Date.now());
1330
+ await persistRefreshed(profile, issuerUrl, refreshed);
1331
+ return refreshed;
1332
+ }
1333
+ async function persistRefreshed(profile, issuerUrl, refreshed) {
1334
+ await writeOAuthProfile(issuerUrl, refreshed, profile);
1335
+ }
1336
+ function createCredentialRefresher(profile) {
1337
+ return async () => {
1338
+ const stored = await readProfileCredential(profile);
1339
+ if (stored === null || stored.credential.kind !== "oauth") return null;
1340
+ try {
1341
+ return await refreshAndPersist(profile, stored.url, stored.credential);
1342
+ } catch (error) {
1343
+ if (error instanceof ConfigError) throw new ConfigError(`${error.message} — run \`mb auth login --profile ${profile}\` to log in again`);
1344
+ throw error;
1345
+ }
905
1346
  };
906
1347
  }
907
1348
  async function failureHintForProfile(profile) {
@@ -910,7 +1351,7 @@ async function failureHintForProfile(profile) {
910
1351
  if (record.lastProbe !== null && record.lastProbe.at >= record.lastFailure.at) return "";
911
1352
  return ` profile "${profile}" last verify failed: ${record.lastFailure.reason}. Run \`mb auth login --profile ${profile}\` to update the token.`;
912
1353
  }
913
- function pickField(flag, env, stored) {
1354
+ function resolveUrl(flag, env, stored) {
914
1355
  if (flag) return {
915
1356
  value: flag,
916
1357
  source: "flag"
@@ -925,6 +1366,27 @@ function pickField(flag, env, stored) {
925
1366
  };
926
1367
  return null;
927
1368
  }
1369
+ function resolveCredential(flagKey, envKey, stored) {
1370
+ if (flagKey) return {
1371
+ credential: {
1372
+ kind: "apiKey",
1373
+ apiKey: flagKey
1374
+ },
1375
+ source: "flag"
1376
+ };
1377
+ if (envKey) return {
1378
+ credential: {
1379
+ kind: "apiKey",
1380
+ apiKey: envKey
1381
+ },
1382
+ source: "env"
1383
+ };
1384
+ if (stored) return {
1385
+ credential: stored,
1386
+ source: "stored"
1387
+ };
1388
+ return null;
1389
+ }
928
1390
 
929
1391
  //#endregion
930
1392
  //#region src/core/version/capabilities.ts
@@ -961,10 +1423,10 @@ var CapabilityError = class extends MetabaseError {
961
1423
  isRetryable = false;
962
1424
  exitCode = 2;
963
1425
  developerDetail;
964
- constructor(failure) {
965
- super(failure.detail);
1426
+ constructor(failure$1) {
1427
+ super(failure$1.detail);
966
1428
  this.name = "CapabilityError";
967
- this.developerDetail = failure;
1429
+ this.developerDetail = failure$1;
968
1430
  }
969
1431
  };
970
1432
 
@@ -1132,8 +1594,11 @@ function defineMetabaseCommand(def) {
1132
1594
  const resolved = await getResolvedConfig();
1133
1595
  cachedClient = createClient({
1134
1596
  url: resolved.url,
1135
- apiKey: resolved.apiKey
1136
- }, { getServerTag: async () => (await getServerInfo())?.version?.tag ?? null });
1597
+ credential: resolved.credential
1598
+ }, {
1599
+ getServerTag: async () => (await getServerInfo())?.version?.tag ?? null,
1600
+ refreshCredential: createCredentialRefresher(resolved.profile)
1601
+ });
1137
1602
  }
1138
1603
  return cachedClient;
1139
1604
  };
@@ -1152,7 +1617,7 @@ function defineMetabaseCommand(def) {
1152
1617
  getServerInfo
1153
1618
  });
1154
1619
  } finally {
1155
- emitLegacyStorageWarningIfPending();
1620
+ emitPendingWarnings();
1156
1621
  }
1157
1622
  } catch (error) {
1158
1623
  reportError(error, reportFormat);
@@ -1167,9 +1632,12 @@ function defineMetabaseCommand(def) {
1167
1632
  });
1168
1633
  return cmd;
1169
1634
  }
1170
- function emitLegacyStorageWarningIfPending() {
1171
- const message = consumeLegacyStorageWarning();
1172
- if (message !== null) warn(message);
1635
+ function emitPendingWarnings() {
1636
+ for (const message of consumeLegacyEnvWarnings()) warn(message);
1637
+ const legacy = consumeLegacyStorageWarning();
1638
+ if (legacy !== null) warn(legacy);
1639
+ const downgrade = consumeKeyringDowngradeWarning();
1640
+ if (downgrade !== null) warn(downgrade);
1173
1641
  }
1174
1642
  async function loadServerInfo(getResolvedConfig) {
1175
1643
  const resolved = await getResolvedConfig();
@@ -1193,9 +1661,9 @@ function createPreflightEnforcer(required, getServerInfo, skip) {
1193
1661
  warn(NO_PROBE_DATA_WARNING);
1194
1662
  return;
1195
1663
  }
1196
- const failure = checkCapabilities(info, required);
1197
- if (failure === null || failure.reason === "unknown-version") return;
1198
- throw new CapabilityError(failure);
1664
+ const failure$1 = checkCapabilities(info, required);
1665
+ if (failure$1 === null || failure$1.reason === "unknown-version") return;
1666
+ throw new CapabilityError(failure$1);
1199
1667
  };
1200
1668
  }
1201
1669
  function isBaseline(caps) {
@@ -1223,4 +1691,4 @@ function buildConfigFlags(ctx) {
1223
1691
  }
1224
1692
 
1225
1693
  //#endregion
1226
- export { DEFAULT_PROFILE, HttpError, ParsedVersionSchema, ProbedUser, ProfileLastFailure, TokenFeatures, USER_AGENT, clearProfile, combineAborts, createClient, defineMetabaseCommand, explicitProfileName, keyringFallbackWarning, listProfileRecords, normalizeUrl, originOnly, parseCsv, parseEnum, parseEnumCsv, parseInteger, parseJson, parseJsonOrPlain, probeServer, readEnvCredentials, readProfile, readProfileRecord, resolveProfileName, throwIfAborted, writeProbeFailure, writeProbeResult, writeProfile };
1694
+ 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 };