@metabase/cli 0.1.11 → 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.
- package/README.md +34 -22
- package/dist/{add-collection-DKULSPwT.mjs → add-collection-BQsx2Q-S.mjs} +5 -5
- package/dist/add-collection-BiBKEHwz.mjs +10 -0
- package/dist/{archive-y-_m3XID.mjs → archive-BP53zrzs.mjs} +6 -6
- package/dist/{archive-BLbaqc-5.mjs → archive-BS4U2_gi.mjs} +7 -7
- package/dist/{archive-BL_0fXhf.mjs → archive-BrYMa87Z.mjs} +6 -6
- package/dist/{archive-ibJVYsBC.mjs → archive-D7qtYihp.mjs} +6 -6
- package/dist/{archive-BLxX3J6I.mjs → archive-Dt5zuiwu.mjs} +6 -6
- package/dist/{archive-BJ6uL3hh.mjs → archive-FLrOqODN.mjs} +7 -7
- package/dist/{archive-DQsV2uwy.mjs → archive-H8wBIt8q.mjs} +7 -7
- package/dist/auth-DWA7DSo9.mjs +19 -0
- package/dist/{body-DCQg78AF.mjs → body-3vzTm8Jj.mjs} +3 -3
- package/dist/{branches-CJVscsZe.mjs → branches-Lr1cCUdg.mjs} +6 -6
- package/dist/{cancel-C8W0sPix.mjs → cancel-Br3FHyDR.mjs} +5 -5
- package/dist/{cancel-task-dDgQDswT.mjs → cancel-task-BXgeq3MH.mjs} +6 -6
- package/dist/{capabilities-7L9GVMd_.mjs → capabilities-BX1rnVuH.mjs} +2 -14
- package/dist/card-DQV8WOIa.mjs +20 -0
- package/dist/{card-DnIeMmUn.mjs → card-wCPcuKSi.mjs} +1 -1
- package/dist/{cards-B7HE8BT6.mjs → cards-D-AkjCxr.mjs} +6 -6
- package/dist/cli.mjs +25 -25
- package/dist/collection-DIUWkVFY.mjs +20 -0
- package/dist/{collection-namespace-CBCApZJf.mjs → collection-namespace-BQ_ebqOP.mjs} +2 -2
- package/dist/{command-augment-BH9qgQ5u.mjs → command-augment-CAur0XOQ.mjs} +13 -2
- package/dist/{create-84jcfY4N.mjs → create-BpAak62U.mjs} +9 -9
- package/dist/{create-VjvMdgM5.mjs → create-C1xYECl-.mjs} +8 -8
- package/dist/{create-vNcD4jfq.mjs → create-CDL9WHiR.mjs} +10 -10
- package/dist/{create-Di9NY7kK.mjs → create-CcZEV9xx.mjs} +8 -8
- package/dist/{create-DN8us8si.mjs → create-DHUhRzcV.mjs} +11 -11
- package/dist/{create-BQA6nM5n.mjs → create-DUdkitlQ.mjs} +12 -12
- package/dist/{create-B6mzc8UK.mjs → create-Dx1SDXyQ.mjs} +8 -8
- package/dist/{create-branch-CCoDX2Qf.mjs → create-branch-BvZKizC4.mjs} +6 -6
- package/dist/{create-BWaBYzhC.mjs → create-ux0yPpq1.mjs} +10 -10
- package/dist/{create-DPASPFGi.mjs → create-y4j2I1aH.mjs} +11 -11
- package/dist/{current-task-QdxA3mSX.mjs → current-task-DaWn0DYt.mjs} +6 -6
- package/dist/dashboard-CohHWhkV.mjs +21 -0
- package/dist/{database-Dsv4QBs0.mjs → database-BmsxuRmO.mjs} +1 -1
- package/dist/db-BD8JZUDw.mjs +22 -0
- package/dist/{delete-CE_Jfjni.mjs → delete-CW3cEHlr.mjs} +7 -7
- package/dist/{delete-ByJ6guaH.mjs → delete-DFWjBpzl.mjs} +7 -7
- package/dist/{delete-runtime-CjFc69tS.mjs → delete-runtime-BhZAAvjc.mjs} +3 -3
- package/dist/{delete-table-D5LDUkOk.mjs → delete-table-Wa0yRsnM.mjs} +7 -7
- package/dist/{dirty-C6yzkIPn.mjs → dirty-CKcGYIpv.mjs} +6 -6
- package/dist/document-CRuag6F5.mjs +19 -0
- package/dist/{eid-ZOyix96k.mjs → eid-m44hs7Ul.mjs} +7 -7
- package/dist/{error-C1bUYXn7.mjs → error-DR_i1hxJ.mjs} +40 -5
- package/dist/{export-BhbBX-pl.mjs → export-8C4F9pYn.mjs} +8 -8
- package/dist/field-CuabjsR7.mjs +18 -0
- package/dist/{fields-CZ6IK8Qf.mjs → fields-hXrRjf7e.mjs} +7 -7
- package/dist/{get-D49EdHgH.mjs → get-3zFOkcl_.mjs} +7 -7
- package/dist/{get-CqKttHWW.mjs → get-8r-_QuSr.mjs} +6 -6
- package/dist/{get-DlzC0xCg.mjs → get-9MCrsgTf.mjs} +8 -8
- package/dist/{get-B03gdAun.mjs → get-BjCQctuS.mjs} +6 -6
- package/dist/{get-C0emTO1r.mjs → get-BlNLGR-F.mjs} +6 -6
- package/dist/{get-9b0ZEWh1.mjs → get-BxgSKzca.mjs} +6 -6
- package/dist/{get-mR01pCNG.mjs → get-C7EKJGTP.mjs} +6 -6
- package/dist/{get-Bbo2Y-aL.mjs → get-Cit5liJv.mjs} +7 -7
- package/dist/{get-BtqQ98nb.mjs → get-CqU4xVSg.mjs} +5 -5
- package/dist/{get-157SWRte.mjs → get-CvLECgtm.mjs} +6 -6
- package/dist/{get-nl41R-2q.mjs → get-D7WUNQYY.mjs} +6 -6
- package/dist/{get-BKWZA_22.mjs → get-DWg3riaY.mjs} +6 -6
- package/dist/{get-DIyv2a7h.mjs → get-cjqlMKF5.mjs} +6 -6
- package/dist/{get-run-_dlMCGp5.mjs → get-run-CTDPEDcU.mjs} +6 -6
- package/dist/{get-DclzgC5G.mjs → get-xVPjvf7O.mjs} +6 -6
- package/dist/git-sync-CKsGGVOm.mjs +28 -0
- package/dist/{has-remote-changes-CToITRyl.mjs → has-remote-changes-DYo4PQfC.mjs} +6 -6
- package/dist/{import-xnYHb4QX.mjs → import-Cwd-_wYv.mjs} +8 -8
- package/dist/{input-xewHccej.mjs → input-BXWgdKiS.mjs} +26 -4
- package/dist/{is-dirty-BHyMmUVV.mjs → is-dirty-HynZex-f.mjs} +4 -4
- package/dist/is-dirty-Mkgi21bq.mjs +9 -0
- package/dist/{items-D3AmbIOO.mjs → items-D4pSBGxR.mjs} +8 -8
- package/dist/{key-y4TtxKwU.mjs → key-C8q5oIr5.mjs} +1 -1
- package/dist/{list-CIu1zfpU.mjs → list-B2QX8BZ3.mjs} +5 -5
- package/dist/{list-D7Np4fwk.mjs → list-Bqy3rN6k.mjs} +5 -5
- package/dist/{list-BhZYkbc9.mjs → list-CI1WQ8U3.mjs} +7 -7
- package/dist/{list-DKPhFATP.mjs → list-Ctze3nfU.mjs} +5 -5
- package/dist/{list-BtTRJyrR.mjs → list-D-Ms9fhd.mjs} +7 -7
- package/dist/{list-BdStkg8G.mjs → list-D3BBrpoQ.mjs} +5 -5
- package/dist/{list-BkzrFyJO.mjs → list-D7o0DOZe.mjs} +5 -5
- package/dist/{list-DSJwMHXB.mjs → list-DBjOFesD.mjs} +6 -6
- package/dist/{list-C5NywUBH.mjs → list-DMZumpNv.mjs} +5 -5
- package/dist/{list-Byn-dRW4.mjs → list-De2qX1w8.mjs} +7 -7
- package/dist/{list-CA6MlByL.mjs → list-ET066OdN.mjs} +5 -5
- package/dist/{list-C_d7ppOJ.mjs → list-F0BQNbaZ.mjs} +25 -18
- package/dist/{list-BHCVZh7Q.mjs → list-XgdM17ix.mjs} +6 -6
- package/dist/{list-5ZZfaOzm.mjs → list-_xdr1WKh.mjs} +5 -5
- package/dist/login-BHDdLJzw.mjs +453 -0
- package/dist/{logout-Bltlsm6u.mjs → logout-ClizU0OL.mjs} +15 -6
- package/dist/{manifest-BPHqF5LY.mjs → manifest-DYeLri80.mjs} +2 -2
- package/dist/measure-SmyQt9Lg.mjs +19 -0
- package/dist/{metadata-BKcyjsoA.mjs → metadata-D23RdbEZ.mjs} +8 -8
- package/dist/{metadata-Bf2TfG8F.mjs → metadata-a_N6Sx30.mjs} +7 -7
- package/dist/{parse-enum-Dr7ACrTR.mjs → parse-enum-BatHQ-Gs.mjs} +1 -1
- package/dist/{parse-id-CWRJlQVK.mjs → parse-id-B1BlAunl.mjs} +1 -1
- package/dist/{parse-ref-BiETXmvm.mjs → parse-ref-CZr1bYIl.mjs} +1 -1
- package/dist/{path-Djw9IARu.mjs → path-FSfPscAQ.mjs} +5 -5
- package/dist/{poll-hJrV0cPm.mjs → poll-Dl5Zv4Ed.mjs} +2 -2
- package/dist/{poll-task-DK6X9Juu.mjs → poll-task-BoRK94WQ.mjs} +2 -2
- package/dist/{preflight-DjSPp3Ct.mjs → preflight-CVlo39xa.mjs} +4 -4
- package/dist/process-DsGf7Mg5.mjs +73 -0
- package/dist/{prompt-u4WhE4T5.mjs → prompt-Bc_bHSD0.mjs} +13 -3
- package/dist/{query-Cc-9oPsW.mjs → query-CSvpJssD.mjs} +8 -8
- package/dist/{query-result-B4kTRid7.mjs → query-result-L5_NrwQR.mjs} +1 -1
- package/dist/{query-Do5n5SZc.mjs → query-yZ9HeaGp.mjs} +12 -12
- package/dist/{remove-collection-DVvnmU06.mjs → remove-collection-CV7ssevw.mjs} +8 -8
- package/dist/{render-BQHJhSP1.mjs → render-CkuFkWlQ.mjs} +6 -1
- package/dist/{rescan-values-CHluR3JC.mjs → rescan-values-CMeOv6yx.mjs} +8 -8
- package/dist/{run-CcJbcaw8.mjs → run-CIJOYXor.mjs} +8 -8
- package/dist/{runs-Dc8qZBhl.mjs → runs--CzbmfPg.mjs} +7 -7
- package/dist/{runtime-jWkzCPlL.mjs → runtime-DJTGfiKO.mjs} +591 -123
- package/dist/{schema-tables-CZ6EMHUZ.mjs → schema-tables-BII9pHR8.mjs} +7 -7
- package/dist/{schemas-Br1cpOZc.mjs → schemas-BzxVz71I.mjs} +5 -5
- package/dist/{search-DB2joPgt.mjs → search-CjJZdaM0.mjs} +5 -5
- package/dist/segment-dkO4Ualw.mjs +19 -0
- package/dist/{set-BRhVzEWd.mjs → set-Ds_brNWu.mjs} +8 -8
- package/dist/{setting-CuxO4T1n.mjs → setting-YDJRWd1m.mjs} +3 -3
- package/dist/{setup-Bn4jwxdl.mjs → setup-DJb-NgSM.mjs} +7 -7
- package/dist/{skills-CY_FmaPo.mjs → skills-CJ8g8fhQ.mjs} +1 -1
- package/dist/{skills-Db1r65S5.mjs → skills-Dn-kn8Wj.mjs} +3 -3
- package/dist/snippet-DqTiC73e.mjs +19 -0
- package/dist/{stash-Dp2JFgmp.mjs → stash-DGujowmx.mjs} +8 -8
- package/dist/{status-BxGBwALE.mjs → status-3-Tl07gu.mjs} +14 -6
- package/dist/{status-j09luNai.mjs → status-Cx0TIn1I.mjs} +7 -7
- package/dist/{summary-DVrj7DSa.mjs → summary-CT-xFvnX.mjs} +6 -6
- package/dist/{sync-schema-BVHvjzGX.mjs → sync-schema-BTF7O3FM.mjs} +10 -10
- package/dist/table-DNGPtV4P.mjs +19 -0
- package/dist/{table-CkWja2UH.mjs → table-qDD2kApF.mjs} +1 -1
- package/dist/transform-DEbvouxo.mjs +24 -0
- package/dist/transform-job-Du8npb7F.mjs +19 -0
- package/dist/{tree-BEPw6xbY.mjs → tree-CLUxXw-Z.mjs} +5 -5
- package/dist/{update-CAoOJRwz.mjs → update-6IuvkFCk.mjs} +11 -11
- package/dist/{update-DiG6SSGm.mjs → update-BeH_S7h9.mjs} +9 -9
- package/dist/{update-Bd0ASKUG.mjs → update-BfNmqvuN.mjs} +13 -13
- package/dist/{update-B5VBJFU9.mjs → update-C_Ui5qPU.mjs} +12 -12
- package/dist/{update-BXaT0Faw.mjs → update-DD5Q37bz.mjs} +9 -9
- package/dist/{update-DzKPxRBE.mjs → update-DdqzxPIr.mjs} +12 -12
- package/dist/{update-fVqLtZ_t.mjs → update-DpCv5Mdo.mjs} +9 -9
- package/dist/{update-C5vlBTII.mjs → update-I0t_6Mni.mjs} +9 -9
- package/dist/{update-C875_WGf.mjs → update-Z_LMzsiz.mjs} +11 -11
- package/dist/{update-dashcard-7bZtw04E.mjs → update-dashcard-Bl3wbB8-.mjs} +9 -9
- package/dist/{update-Bn5wUyTQ.mjs → update-tQvEYuzD.mjs} +10 -10
- package/dist/{upgrade-DdCxE9Ml.mjs → upgrade-r2ZlfstE.mjs} +6 -34
- package/dist/{uuid-DywghRXO.mjs → uuid-BduDonqb.mjs} +4 -4
- package/dist/{validate-B62TRDGV.mjs → validate-VawhJ5Sc.mjs} +2 -2
- package/dist/{validate-query-BpiN1CFu.mjs → validate-query-CSV-TTnd.mjs} +3 -3
- package/dist/{values-DSul3siC.mjs → values-DuNE6Jxc.mjs} +6 -6
- package/dist/{verify-DzDyKXOa.mjs → verify-D_yq68-R.mjs} +5 -5
- package/dist/{wait-CacFZI5Y.mjs → wait-CoDzO37B.mjs} +7 -7
- package/dist/{wait-flags-DV419apK.mjs → wait-flags-BxUfF5zy.mjs} +2 -2
- package/package.json +1 -1
- package/dist/add-collection-DyEe_xRO.mjs +0 -10
- package/dist/auth-BKaQjcmP.mjs +0 -19
- package/dist/card-CGBvUmCW.mjs +0 -20
- package/dist/collection-Ch7pPKHD.mjs +0 -20
- package/dist/dashboard-BDb2krMr.mjs +0 -21
- package/dist/db-CoVdkdVm.mjs +0 -22
- package/dist/document-BA2S1ev3.mjs +0 -19
- package/dist/field-B5bptFqL.mjs +0 -18
- package/dist/git-sync-DqJmONiB.mjs +0 -28
- package/dist/is-dirty-BvA3yzRc.mjs +0 -9
- package/dist/login-jWvDDvEm.mjs +0 -180
- package/dist/measure-DBX4k13t.mjs +0 -19
- package/dist/segment-BWcyA1P-.mjs +0 -19
- package/dist/snippet-C9UM6NwC.mjs +0 -19
- package/dist/table-DYI8K12W.mjs +0 -19
- package/dist/transform-CvbMbJtI.mjs +0 -24
- package/dist/transform-job-CtzgvccZ.mjs +0 -19
- /package/dist/{body-flags-D78h_-Ua.mjs → body-flags-D7q87Btw.mjs} +0 -0
- /package/dist/{collection-BPTlcFw5.mjs → collection-Deiziuu2.mjs} +0 -0
- /package/dist/{dashboard-FY5UzJ_Z.mjs → dashboard-B4bn3z6t.mjs} +0 -0
- /package/dist/{document-CcfiiV3b.mjs → document-qfwR0r63.mjs} +0 -0
- /package/dist/{field-MGxpNQUH.mjs → field-E0IBy4Uw.mjs} +0 -0
- /package/dist/{measure-COGAoZpo.mjs → measure-JJAdFoqK.mjs} +0 -0
- /package/dist/{paginate-Dfm9eO9A.mjs → paginate-BexjkjbY.mjs} +0 -0
- /package/dist/{revision-message-flag-Bo-Kvv6F.mjs → revision-message-flag-C7zBeWEt.mjs} +0 -0
- /package/dist/{segment-d-708KKO.mjs → segment-B9SEv9V_.mjs} +0 -0
- /package/dist/{setting-CSJvi1-6.mjs → setting-i0pugNHl.mjs} +0 -0
- /package/dist/{snippet-BZvo05ua.mjs → snippet-NNzqlksR.mjs} +0 -0
- /package/dist/{transform-job-n8W6tLqF.mjs → transform-job-B9pXBOwr.mjs} +0 -0
- /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-
|
|
2
|
-
import { DEFAULT_MAX_BYTES, package_default, reportError } from "./error-
|
|
3
|
-
import { BASELINE_CAPABILITIES, isPlainObject
|
|
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
|
|
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
|
|
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
|
|
170
|
-
if (!keyringEnabled()) return
|
|
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
|
|
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
|
-
|
|
223
|
-
|
|
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" ?
|
|
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
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
949
|
+
return (await executeWithAuthRefresh(path, opts ?? {})).response;
|
|
713
950
|
},
|
|
714
951
|
async requestParsed(schema, path, opts) {
|
|
715
|
-
const prepared =
|
|
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 =
|
|
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
|
|
1050
|
+
function displayUrl(input) {
|
|
860
1051
|
const parsed = new URL(input);
|
|
861
|
-
parsed.
|
|
862
|
-
parsed.
|
|
863
|
-
|
|
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
|
|
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 ||
|
|
1280
|
+
return profileFlag || readEnv(ENV_PROFILE) || null;
|
|
880
1281
|
}
|
|
881
1282
|
function readEnvCredentials() {
|
|
882
1283
|
return {
|
|
883
|
-
url:
|
|
884
|
-
apiKey:
|
|
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
|
|
891
|
-
const
|
|
892
|
-
const
|
|
893
|
-
const
|
|
894
|
-
const
|
|
895
|
-
|
|
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:
|
|
902
|
-
|
|
1304
|
+
url: normalizedUrl,
|
|
1305
|
+
credential: fresh,
|
|
903
1306
|
profile,
|
|
904
|
-
source:
|
|
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
|
|
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
|
-
|
|
1136
|
-
}, {
|
|
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
|
-
|
|
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
|
|
1171
|
-
const message
|
|
1172
|
-
|
|
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,
|
|
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 };
|