@metabase/cli 0.3.1 → 0.3.2-alpha.fix-transform-test-ee-path.041c793
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/dist/{add-collection-CYDUm_XO.mjs → add-collection-73X1DmCM.mjs} +6 -9
- package/dist/{alert-ClyhtxIz.mjs → alert-BB38lWSL.mjs} +7 -7
- package/dist/{alerts-Dt3D1gcz.mjs → alerts-DxyzqLSU.mjs} +7 -7
- package/dist/{append-Bl_Ensmr.mjs → append-CHme1Enm.mjs} +5 -5
- package/dist/{archive-DSMqHRr1.mjs → archive-BGiDxu8O.mjs} +4 -4
- package/dist/{archive-D3a3Wgz0.mjs → archive-BMyvyf62.mjs} +4 -4
- package/dist/{archive-BdHXQg4I.mjs → archive-C4Y2B58q.mjs} +4 -4
- package/dist/{archive-CWceyy8x.mjs → archive-CP5Hlqjw.mjs} +5 -5
- package/dist/{archive-BOyKhNOz.mjs → archive-Cq3fnc0v2.mjs} +6 -6
- package/dist/{archive-DVGDg8mP.mjs → archive-CqKqNdUh.mjs} +6 -6
- package/dist/{archive-DDru3vdy.mjs → archive-Czo5T0Lp.mjs} +4 -4
- package/dist/{archive-nKD3mOB8.mjs → archive-DBcxZwLd.mjs} +6 -6
- package/dist/{archive-CNblz55j.mjs → archive-DKFPJ9j3.mjs} +6 -6
- package/dist/{archive-BJZ7Dace.mjs → archive-DPL1qOuL.mjs} +6 -6
- package/dist/{archive-DiGxCojL.mjs → archive-eS-XA74p.mjs} +4 -4
- package/dist/{auth-aCksg8q2.mjs → auth-6YyB0HNB.mjs} +5 -5
- package/dist/{body-DxRSAMNC.mjs → body-OjAWKVaU.mjs} +3 -3
- package/dist/{branches-_0VWR2Zm.mjs → branches-D5YLVFDK.mjs} +4 -7
- package/dist/{cancel-ctpVE8uC.mjs → cancel-DeRIq1YJ.mjs} +4 -4
- package/dist/{cancel-task-FtxBka-M.mjs → cancel-task-CqExODVm.mjs} +5 -8
- package/dist/card-BMSqofd7.mjs +24 -0
- package/dist/{card-zyUkr2nS.mjs → card-CEkp1mcI.mjs} +1 -1
- package/dist/{card-DZxRmver.mjs → card-voh1k4hl.mjs} +14 -11
- package/dist/{cards-DHOWAvfv.mjs → cards-B813G9I5.mjs} +7 -7
- package/dist/cli.mjs +572 -69
- package/dist/client-cJ7aw0Gt.mjs +4092 -0
- package/dist/collection-BfNg7tEg.mjs +20 -0
- package/dist/{collection-CfulNRBG.mjs → collection-BxdPxD38.mjs} +1 -1
- package/dist/{collection-CGoVLvcD.mjs → collection-DYM0p3Xl.mjs} +1 -1
- package/dist/{collection-namespace-CEoQ7G08.mjs → collection-namespace-CVzDgJV0.mjs} +2 -2
- package/dist/{content-translation-DNKerjS3.mjs → content-translation-QKWt9l76.mjs} +3 -3
- package/dist/{create-e650zxzX.mjs → create-4UnoQvsy.mjs} +5 -5
- package/dist/{create-DaYRnK5P.mjs → create-BJad6MR82.mjs} +8 -8
- package/dist/{create-CcLchJAN.mjs → create-BLiB2xqo.mjs} +7 -7
- package/dist/{create-C9vy4ZG6.mjs → create-BWp6L_dW.mjs} +5 -5
- package/dist/{create-C78g8Crb.mjs → create-Bef5eBCA.mjs} +5 -5
- package/dist/{create-F4Lf09xq.mjs → create-CMV3qlzJ.mjs} +4 -4
- package/dist/{create-DmvoibX8.mjs → create-CWGsiFvU.mjs} +4 -4
- package/dist/{create-C3IaXXty.mjs → create-CoFg1p6m.mjs} +4 -4
- package/dist/{create-CEydRMWS.mjs → create-CvIeb1NU.mjs} +7 -7
- package/dist/{create-vlrlY-2E.mjs → create-CvpeAcFc.mjs} +4 -4
- package/dist/{create-wJR4CxHA.mjs → create-D50TxVTk.mjs} +5 -8
- package/dist/{create-CLG2-bso.mjs → create-DEBgYRlM.mjs} +6 -6
- package/dist/{create-7ddYK4TA.mjs → create-DJm0YuLW.mjs} +6 -6
- package/dist/{create-BdPVWYSi.mjs → create-DmKwDQCY2.mjs} +6 -6
- package/dist/{create-branch-B8C7jl-J.mjs → create-branch-BN5iH-GM.mjs} +5 -9
- package/dist/{create-DIVhBIyE.mjs → create-bvpl6Fn5.mjs} +11 -7
- package/dist/create-uanpKsPo.mjs +34 -0
- package/dist/{csv-BLPJtlSC.mjs → csv-BhuX9fwh.mjs} +5 -5
- package/dist/{current-task-C-ldJbWE.mjs → current-task-BMt2NeqV.mjs} +5 -8
- package/dist/{dashboard-CtO_3EfC.mjs → dashboard-Ct_ajSYI.mjs} +10 -10
- package/dist/{dashboard-CKUR8ZpQ.mjs → dashboard-D0hwTC-f.mjs} +11 -2
- package/dist/{dashboard-BlbNszi8.mjs → dashboard-DgK81azj.mjs} +1 -1
- package/dist/{database-B19qlLKZ.mjs → database-C3E9PZSx.mjs} +1 -1
- package/dist/{database-C8G6ZKG0.mjs → database-JtHTqSpq.mjs} +1 -1
- package/dist/{db-xXselnBq.mjs → db-B_LW2VE-.mjs} +7 -7
- package/dist/{delete-ZJJyqsRh.mjs → delete-BOcPK_H9.mjs} +5 -5
- package/dist/{delete-BZZHw6pm.mjs → delete-CowGO4rh.mjs} +5 -5
- package/dist/{delete-D6WxgWRp.mjs → delete-D0f7VatX.mjs} +5 -5
- package/dist/{delete-BwSPQy7m.mjs → delete-D2bUV6uo.mjs} +5 -5
- package/dist/delete-DZ_ICQug.mjs +44 -0
- package/dist/{delete-BZwAOQxG.mjs → delete-Ur7b11no.mjs} +5 -5
- package/dist/{delete-runtime-D74s6vhu.mjs → delete-runtime-BKEzJn22.mjs} +1 -2
- package/dist/{delete-table-CJktZE3w.mjs → delete-table-Dd8X573t.mjs} +5 -5
- package/dist/{dependencies-BdqIqEIy.mjs → dependencies-XDVLBwko.mjs} +7 -7
- package/dist/{dirty-BW2As2wS.mjs → dirty-k-JmGLsS.mjs} +6 -9
- package/dist/{document-Cd2mmqCp.mjs → document-BHpLJbKU.mjs} +5 -0
- package/dist/document-BW9iow8R.mjs +19 -0
- package/dist/{document-D6OybtP2.mjs → document-CA0Q6lqu.mjs} +1 -1
- package/dist/{download-mfFJ9oRS.mjs → download-CQjPIYDO.mjs} +3 -6
- package/dist/{eid-DZ-4t2xe.mjs → eid-CNTKDgwj.mjs} +4 -5
- package/dist/{events-D6yFmXP1.mjs → events-BshWlkDp.mjs} +5 -5
- package/dist/{export-DYZjz1WP.mjs → export-Dq6ecU8h.mjs} +7 -10
- package/dist/{field-BJ54wmIX.mjs → field-Bo2ISMO4.mjs} +5 -5
- package/dist/{field-Cr2DPUG9.mjs → field-CcN4oZy6.mjs} +16 -1
- package/dist/{field-BPsNa-TO.mjs → field-fDtIrY6D.mjs} +1 -1
- package/dist/{fields-FUWCgPOa.mjs → fields-CVctqn5G.mjs} +7 -7
- package/dist/{get-BraVQ1FJ.mjs → get-7xTeNGAo.mjs} +6 -6
- package/dist/{get-LAew34ID.mjs → get-8b5Sh50q.mjs} +6 -6
- package/dist/{get-CWnriw33.mjs → get-B4RgW-VT.mjs} +6 -6
- package/dist/{get-C_5AbfpO.mjs → get-BD81MHl_.mjs} +4 -4
- package/dist/{get-ByZJf9nO.mjs → get-Bl3OmlBP.mjs} +5 -5
- package/dist/get-BtTetWzm.mjs +32 -0
- package/dist/{get-_ggLr1tY.mjs → get-BvSA_mmf.mjs} +6 -6
- package/dist/{get-DhkltKIV.mjs → get-BvgEF--n.mjs} +4 -4
- package/dist/{get-DYaQpixt.mjs → get-C5sWciaE.mjs} +6 -6
- package/dist/get-CO6X4nZH.mjs +93 -0
- package/dist/{get-DVyesErX.mjs → get-CTPvdcud.mjs} +6 -6
- package/dist/{get-DMECsioX.mjs → get-CrpwxWOV.mjs} +4 -4
- package/dist/{get-Ba2HRdrx.mjs → get-D4oGhZWX.mjs} +6 -6
- package/dist/{get-CsN7NBI0.mjs → get-Df6cGHCM.mjs} +4 -4
- package/dist/{get-BWl3vzsf.mjs → get-DfMVlz64.mjs} +6 -6
- package/dist/{get-C5q49Dgx.mjs → get-Dr-U321T2.mjs} +6 -6
- package/dist/{get-DAOSGt5Z.mjs → get-Dw7jAno3.mjs} +6 -6
- package/dist/{get-DRJtsS1_.mjs → get-PWhOdz1z.mjs} +4 -4
- package/dist/{get-CQ0-fFWP.mjs → get-RslzBaXx.mjs} +5 -8
- package/dist/{get-run-oH4dRCBx.mjs → get-run-CZdDqfUj.mjs} +6 -6
- package/dist/{get-DCutQeiV.mjs → get-wjIgUlNl.mjs} +4 -4
- package/dist/git-sync-BmoRk0UV.mjs +28 -0
- package/dist/{git-sync-BDnxPrqU.mjs → git-sync-BwBRmEhE.mjs} +18 -1
- package/dist/{git-sync-DxD_ng48.mjs → git-sync-TjX1r_wF.mjs} +1 -1
- package/dist/{group-DBM87IgC.mjs → group-YQQ5GIcP.mjs} +2 -2
- package/dist/{has-remote-changes-CnmyIw_7.mjs → has-remote-changes-i-3MVxat.mjs} +5 -8
- package/dist/{import-B7Si-bSI.mjs → import-DWEKejya.mjs} +7 -10
- package/dist/{input-DH9i67x_.mjs → input-DEvkIjGr.mjs} +1 -1
- package/dist/{is-dirty-BOH_VEA8.mjs → is-dirty-C5ipqd4v.mjs} +3 -6
- package/dist/{items-DouHkWev.mjs → items-hKJ2ZDwy.mjs} +7 -7
- package/dist/{key-BEhqIzIQ.mjs → key-PYetuQ13.mjs} +1 -1
- package/dist/{library-1J_4Dicw.mjs → library-B7_04UVK.mjs} +5 -5
- package/dist/library-BPIzb3K_.mjs +81 -0
- package/dist/{library-DfYAT2Ep.mjs → library-PGCfm0UA.mjs} +1 -1
- package/dist/{list-BK_6VNm_.mjs → list-1vdClKSE.mjs} +4 -4
- package/dist/{list-DWLGpMvr.mjs → list-6myofqMv.mjs} +7 -7
- package/dist/{list-DhViDVx_.mjs → list-B-9QOX9Y.mjs} +6 -6
- package/dist/{list-aKUk-a1Y.mjs → list-Bs7_Rysg.mjs} +7 -7
- package/dist/list-CHuhMXet.mjs +35 -0
- package/dist/{list-DptEWB5Y.mjs → list-CLZU_EOf.mjs} +4 -4
- package/dist/{list-B2Ddf4W0.mjs → list-Cen-7uTo.mjs} +5 -5
- package/dist/{list-D2IYZ6lk.mjs → list-CgH-H-O6.mjs} +6 -6
- package/dist/{list-BeMYIeiI.mjs → list-CjNXskB4.mjs} +7 -7
- package/dist/{list-C1bAtgO1.mjs → list-CmFh1GQ_.mjs} +4 -4
- package/dist/{list-Cy0w4Fap.mjs → list-D6tTvYUP.mjs} +4 -4
- package/dist/{list-C7UvO3Uw.mjs → list-DI820W6w.mjs} +6 -6
- package/dist/{list-D25ofn4v.mjs → list-DKZnQ4Ts.mjs} +7 -7
- package/dist/{list-C0zJ2ehz.mjs → list-DKcC4cm0.mjs} +6 -6
- package/dist/{list-qs9wAtC3.mjs → list-DfRqNH1N.mjs} +4 -4
- package/dist/list-DjwnsyXM.mjs +60 -0
- package/dist/{list-sDpy4nDU.mjs → list-VNC-_E59.mjs} +6 -6
- package/dist/{list-DJK9_BEF.mjs → list-eKDT1XXk.mjs} +14 -13
- package/dist/{list-BXlTYsYA.mjs → list-oX2SMzxZ.mjs} +4 -4
- package/dist/{login-BmyCAfRV.mjs → login-DumZaWXK.mjs} +18 -15
- package/dist/{logout-DUR32q0V.mjs → logout-BsXPRc-3.mjs} +3 -4
- package/dist/{measure-DdVp-8jw.mjs → measure-BFxkbAoa.mjs} +6 -6
- package/dist/{notification-Fcq1fIPw.mjs → notification-DVrLkjFT.mjs} +1 -1
- package/dist/{parameter-DUKR8NXo.mjs → parameter-Celg0wZa.mjs} +1 -1
- package/dist/{parameter-values-CBR-cBev.mjs → parameter-values-CoF9Hx1p.mjs} +5 -6
- package/dist/{parse-enum-D0Jfm_Bz.mjs → parse-enum-Dc0dMZ3P.mjs} +1 -1
- package/dist/{parse-id-B3YwNojT.mjs → parse-id-DmFJgpY6.mjs} +1 -1
- package/dist/{parse-ref-ibKVB0J4.mjs → parse-ref-DTDld_LJ.mjs} +1 -1
- package/dist/{path-D8BQmEhL.mjs → path-BQ4lRDyN.mjs} +5 -5
- package/dist/{poll-75SvT7Zv.mjs → poll-CZeXepUQ.mjs} +2 -2
- package/dist/{preflight-CoF1Em99.mjs → preflight-BTQe4swo.mjs} +3 -4
- package/dist/{process-DBOOrmET.mjs → process-CTrfxxaC.mjs} +1 -1
- package/dist/{publish-Cb7XrQ3X.mjs → publish-CFB2Cc5A.mjs} +9 -9
- package/dist/{pulse-BmsDenXm.mjs → pulse-Dz0ocv0l.mjs} +1 -1
- package/dist/{pulse-DhG6I9mI.mjs → pulse-x0gFxWfG.mjs} +2 -2
- package/dist/{query-D35B588l.mjs → query-BJbUnAHf.mjs} +10 -12
- package/dist/query-CV2KCS5O.mjs +114 -0
- package/dist/{query-result-DBDwk8TS.mjs → query-result-E4Q-Gra6.mjs} +1 -1
- package/dist/{remove-collection-DJem6V26.mjs → remove-collection-D3yvKH3e.mjs} +6 -9
- package/dist/{render-CI93iP0I.mjs → render-BJRCs7rA.mjs} +13 -2
- package/dist/{replace-CtsWICrD.mjs → replace-uj4WTqx_.mjs} +5 -5
- package/dist/{rescan-values-CaXUL6z5.mjs → rescan-values-CzxQU318.mjs} +6 -6
- package/dist/{run-CM-Amtbf.mjs → run-B2f_omHu.mjs} +26 -14
- package/dist/run-DlO1J1Sx.mjs +50 -0
- package/dist/{run-BeLm17F4.mjs → run-yX2dtmE5.mjs} +6 -6
- package/dist/{runs-DFG63zgr.mjs → runs-DzUO45wU.mjs} +7 -7
- package/dist/{runtime-z8rra6-I.mjs → runtime-arKqJqIT.mjs} +994 -252
- package/dist/{schema-tables-COAHkIcy.mjs → schema-tables-DZCNzuQ6.mjs} +7 -7
- package/dist/{schemas-BX5BATiV.mjs → schemas-2ejqaVsG.mjs} +5 -5
- package/dist/{search-DFBwUc6d.mjs → search-BAXg-EJw.mjs} +2 -1
- package/dist/{search-CPvvmhdt.mjs → search-DcihauDZ.mjs} +6 -6
- package/dist/{segment-C62L6BI3.mjs → segment-BQT2poPG.mjs} +6 -6
- package/dist/{selectors-P4QnrORk.mjs → selectors-BzestDld.mjs} +3 -3
- package/dist/{send-PZh5IKF8.mjs → send-DMxoLFo7.mjs} +4 -4
- package/dist/{set-DbCtUoFd.mjs → set-CDmiD0vv.mjs} +5 -5
- package/dist/{set-active-DL4QzWDO.mjs → set-active-RrvWR9NY.mjs} +5 -5
- package/dist/{setting-BlRcZbhC.mjs → setting-BiBLCcxz.mjs} +4 -4
- package/dist/{setup-D_SLaC-3.mjs → setup-DYsTzkWO.mjs} +4 -4
- package/dist/{signal-CMd0EFUa.mjs → signal-k7cm5N6h.mjs} +11 -2
- package/dist/skill-list-D9_1Pmt7.mjs +76 -0
- package/dist/skills-BPHXUfeL.mjs +351 -0
- package/dist/{skills-D991LB1M.mjs → skills-C3bxxC6a.mjs} +3 -3
- package/dist/snippet-BtfxE60H.mjs +19 -0
- package/dist/{stash-DyBxFoP6.mjs → stash-DdheS0Ad.mjs} +7 -11
- package/dist/{status-cfDHqK2Q.mjs → status-BuIzXRX1.mjs} +12 -11
- package/dist/{status-DUsiogz5.mjs → status-CXouQJ31.mjs} +11 -9
- package/dist/{subscription-Bn0AU7GN.mjs → subscription-CJ8Igs1U.mjs} +6 -6
- package/dist/{subscriptions-DwqjCVlc.mjs → subscriptions-D0NpGvCQ.mjs} +7 -7
- package/dist/{summary-BGwSNzf4.mjs → summary-D04S8gGj.mjs} +6 -6
- package/dist/{sync-schema-dtkDOAKe.mjs → sync-schema-DrA5zkdl.mjs} +7 -7
- package/dist/{sync-task-BACcrs0i.mjs → sync-task-BXeWuARG.mjs} +1 -1
- package/dist/{table-CsIJNPFf.mjs → table-C6h4tIRw.mjs} +1 -1
- package/dist/{table-CeeTJgQb.mjs → table-DfuY0N8e.mjs} +51 -4
- package/dist/{table-BJoEAoCU.mjs → table-Dntbz7Q2.mjs} +5 -5
- package/dist/timeline-8jwsldo3.mjs +21 -0
- package/dist/{timeline-event-BsF7YqzZ.mjs → timeline-event-BbB1xEdI.mjs} +6 -6
- package/dist/{transform-DoTyyXAu.mjs → transform-CtqB1WYo.mjs} +2 -2
- package/dist/{transform-D3QX0jxA.mjs → transform-DiW7bREn.mjs} +68 -5
- package/dist/{transform-job-50GZHhKH.mjs → transform-job-AaMZRxAq.mjs} +1 -1
- package/dist/transform-job-B-1YAbvy.mjs +22 -0
- package/dist/{transform-job-DRrK28IH.mjs → transform-job-CCguL6LP.mjs} +41 -4
- package/dist/transform-m6s4kkWU.mjs +28 -0
- package/dist/{transform-tag-VM9R7Q_M.mjs → transform-tag-PjryheLF.mjs} +5 -5
- package/dist/transform-test-BjjCQ2xm.mjs +169 -0
- package/dist/transform-test-DSTkKKNC.mjs +16 -0
- package/dist/transform-test-gzDcbypy.mjs +25 -0
- package/dist/{transforms-BjACGbBp.mjs → transforms-CijX3VE7.mjs} +7 -7
- package/dist/{tree-BwS6JAmk.mjs → tree-Cmb3O6rH.mjs} +4 -5
- package/dist/{unpublish-DWdPnENY.mjs → unpublish-DkP_AcK5.mjs} +4 -7
- package/dist/{update-DKEHY-gt.mjs → update-8UmD1prT.mjs} +7 -7
- package/dist/{update-C1FBVLfC.mjs → update-BFyRBc-H.mjs} +8 -8
- package/dist/{update-tLIhVIPp.mjs → update-BrXGg3QO.mjs} +8 -8
- package/dist/{update-DCjAjihm.mjs → update-C91m9YAR.mjs} +6 -6
- package/dist/{update-BpPaacCF.mjs → update-COcx61Ur.mjs} +6 -6
- package/dist/update-Ci8YL4_2.mjs +41 -0
- package/dist/{update-BR_vkkd1.mjs → update-CtLbFCd_.mjs} +5 -5
- package/dist/{update-DTmRHBik.mjs → update-D6Zzvc7F.mjs} +7 -7
- package/dist/{update-cqR1U6DU.mjs → update-D8htjzNJ.mjs} +5 -5
- package/dist/{update-B2CF2DK4.mjs → update-D9kOTr0n.mjs} +6 -6
- package/dist/{update-C4y3iQsv.mjs → update-DPTio6i1.mjs} +7 -7
- package/dist/{update-DsZttiSC.mjs → update-DlWB9s0v.mjs} +5 -5
- package/dist/{update-BCT9qpVr.mjs → update-DubrpPbz.mjs} +5 -5
- package/dist/{update-3idz42uD.mjs → update-Dz47cKBk.mjs} +7 -7
- package/dist/{update-DkviXwSg.mjs → update-FCP5Lop2.mjs} +7 -7
- package/dist/{update-DlTaD1x3.mjs → update-KSHnb6vB.mjs} +9 -9
- package/dist/{update-dashcard-DlE_bb2F.mjs → update-dashcard-BlibHa5o.mjs} +7 -7
- package/dist/{upgrade-mNV7XuJG.mjs → upgrade-EwL6wPSY.mjs} +6 -8
- package/dist/upload-BZshRJIM.mjs +13 -0
- package/dist/{upload-CEGVxmYB.mjs → upload-D0KaTqUE.mjs} +4 -7
- package/dist/{upload-CD555ui1.mjs → upload-Do2PBSeH.mjs} +2 -2
- package/dist/{uuid-C4hqn3kA.mjs → uuid-DjKLDIVE.mjs} +10 -7
- package/dist/{validate-query-DaGYSmpQ.mjs → validate-query-D6_ad-Gi.mjs} +3 -4
- package/dist/{validate-BWchyW0V.mjs → validate-xR_MHBNR.mjs} +69 -23
- package/dist/{values-C0O3eNaW.mjs → values-Dc9612_L.mjs} +6 -6
- package/dist/{verify-DfTjYF7s.mjs → verify-DVN1eqXZ.mjs} +3 -18
- package/dist/{wait-WZmFenN_.mjs → wait-C-ovOTAZ.mjs} +7 -10
- package/dist/{wait-flags-BbfDSP8R.mjs → wait-flags-BQNmuvIj.mjs} +2 -2
- package/dist/{window-BkvrP0gB.mjs → window-DSb1tqSe.mjs} +1 -1
- package/package.json +1 -1
- package/skill-data/core/SKILL.md +31 -15
- package/skill-data/dashboard/SKILL.md +1 -1
- package/skill-data/data-workflow/references/building-clean-tables.md +6 -2
- package/skill-data/data-workflow/references/reusable-definitions.md +11 -3
- package/skill-data/document/SKILL.md +4 -1
- package/skill-data/git-sync/SKILL.md +5 -0
- package/skill-data/mbql/SKILL.md +84 -115
- package/skill-data/mbql/references/operators.md +77 -231
- package/skill-data/metadata/SKILL.md +1 -1
- package/skill-data/metadata/references/semantic-types.md +7 -1
- package/skill-data/native-sql/SKILL.md +8 -9
- package/skill-data/native-sql/references/template-tags.md +27 -27
- package/skill-data/notification/SKILL.md +7 -1
- package/skill-data/transform/SKILL.md +76 -4
- package/skill-data/transform-test-plan/SKILL.md +171 -0
- package/skill-data/transform-test-plan/references/checklist.md +81 -0
- package/skill-data/transform-test-plan/references/checks.md +260 -0
- package/skill-data/visualization/SKILL.md +13 -6
- package/skill-data/visualization/references/settings.md +13 -11
- package/skills/metabase-cli/SKILL.md +1 -1
- package/dist/card-CGCXbdmD.mjs +0 -24
- package/dist/client-mKuHg_sc.mjs +0 -2218
- package/dist/collection-D8Tk8i3a.mjs +0 -20
- package/dist/document-KbcHhzFz.mjs +0 -19
- package/dist/get-BC8J8BXB.mjs +0 -80
- package/dist/git-sync-Dik8nKfw.mjs +0 -28
- package/dist/library-ClLgrAXy.mjs +0 -19
- package/dist/list-CW5PdYCX.mjs +0 -98
- package/dist/network-error-D6CiOEBw.mjs +0 -431
- package/dist/predicates-Bkm2IoeX.mjs +0 -170
- package/dist/query-C256WBYG.mjs +0 -87
- package/dist/skills-DTCrMcix.mjs +0 -216
- package/dist/snippet-DTDO55Cg.mjs +0 -19
- package/dist/timeline-DcXPshcT.mjs +0 -21
- package/dist/transform-Dsf_wHAj.mjs +0 -28
- package/dist/transform-job-B1yLv5l-.mjs +0 -22
- package/dist/upload-DsYCf5kB.mjs +0 -13
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { B as normalizeFlag, D as consumeLegacyEnvWarnings, E as ENV_URL, O as readEnv, S as ENV_DISABLE_KEYRING, T as ENV_SKIP_PREFLIGHT, V as toAliasArray, a as reportError, b as warn, i as mergeCapabilities, j as version, k as setMetabaseAugment, n as BASELINE_CAPABILITIES, r as checkCapabilities, t as interruptSignal, x as ENV_API_KEY, z as flagConsumesValue } from "./cli.mjs";
|
|
3
|
-
import { _ as parseJsonResult, a as isRetryableStatus, c as normalizeUrl, f as isOAuthExpired, m as JSON_CONTENT_TYPE, o as assertEndpointOrigin, p as oauthCredentialFromTokens, r as HttpError, t as buildNetworkError } from "./network-error-D6CiOEBw.mjs";
|
|
1
|
+
import { D as ENV_API_KEY, E as warn, F as ProfileRefreshedError, I as setMetabaseAugment, J as normalizeFlag, M as ENV_URL, N as consumeLegacyEnvWarnings, O as ENV_DISABLE_KEYRING, P as readEnv, Q as ConfigError, R as version, Y as toAliasArray, Z as ChainedRequestError, a as Features, at as ValidationError, b as isPlainObject, c as ruleGap, ct as KNOWN_RANGE, et as MetabaseError, i as FEATURE_RULES, j as ENV_SKIP_PREFLIGHT, l as reportError, n as methodRequirements, nt as ResponseShapeError, o as evaluateFeatures, ot as errorMessage, q as flagConsumesValue, r as FEATURE_NAMES, rt as TimeoutError, st as isFileNotFoundError, t as interruptSignal, tt as NetworkError } from "./cli.mjs";
|
|
4
2
|
import { defineCommand } from "citty";
|
|
5
3
|
import { z } from "zod";
|
|
6
|
-
import { parse } from "semver";
|
|
7
4
|
import { Entry } from "@napi-rs/keyring";
|
|
8
5
|
import { promises } from "node:fs";
|
|
9
6
|
import { dirname, join } from "node:path";
|
|
@@ -23,6 +20,21 @@ function parseOptionalInteger(value, options) {
|
|
|
23
20
|
}
|
|
24
21
|
//#endregion
|
|
25
22
|
//#region ../client/src/version/preflight-error.ts
|
|
23
|
+
const RequirementReason = z.enum(["version-too-old", "missing-token-feature"]);
|
|
24
|
+
const RequirementFailure = z.object({
|
|
25
|
+
reason: RequirementReason,
|
|
26
|
+
detail: z.string(),
|
|
27
|
+
feature: z.enum(FEATURE_NAMES),
|
|
28
|
+
since: z.number().int(),
|
|
29
|
+
tokenFeature: z.string().nullable(),
|
|
30
|
+
serverVersion: z.string()
|
|
31
|
+
});
|
|
32
|
+
function versionTooOldMessage(since, serverVersion) {
|
|
33
|
+
return `This operation requires Metabase v${since}+ (this server is ${serverVersion}). Upgrade Metabase to use it.`;
|
|
34
|
+
}
|
|
35
|
+
function missingTokenFeatureMessage(tokenFeature) {
|
|
36
|
+
return `This operation requires the '${tokenFeature}' premium feature (not enabled on this server).`;
|
|
37
|
+
}
|
|
26
38
|
var CapabilityError = class extends MetabaseError {
|
|
27
39
|
category = "capability";
|
|
28
40
|
isRetryable = false;
|
|
@@ -35,35 +47,900 @@ var CapabilityError = class extends MetabaseError {
|
|
|
35
47
|
};
|
|
36
48
|
//#endregion
|
|
37
49
|
//#region ../client/src/domain/session-properties.ts
|
|
38
|
-
const ServerVersion = z.object({
|
|
50
|
+
const ServerVersion$1 = z.object({
|
|
39
51
|
tag: z.string(),
|
|
40
52
|
date: z.string().optional(),
|
|
41
53
|
hash: z.string().optional()
|
|
42
54
|
}).loose();
|
|
43
55
|
const TokenFeatures = z.record(z.string(), z.boolean());
|
|
44
56
|
const SessionProperties = z.object({
|
|
45
|
-
version: ServerVersion,
|
|
57
|
+
version: ServerVersion$1,
|
|
46
58
|
"token-features": TokenFeatures.optional()
|
|
47
59
|
}).loose();
|
|
48
60
|
//#endregion
|
|
49
61
|
//#region ../client/src/version/tag.ts
|
|
50
|
-
const
|
|
62
|
+
const ReleaseVersion = z.object({
|
|
63
|
+
kind: z.literal("release"),
|
|
51
64
|
tag: z.string(),
|
|
52
65
|
major: z.number().int().nonnegative(),
|
|
53
66
|
patch: z.number().int().nonnegative()
|
|
54
67
|
});
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
const DevelopmentBuild = z.object({
|
|
69
|
+
kind: z.literal("development"),
|
|
70
|
+
tag: z.string()
|
|
71
|
+
});
|
|
72
|
+
const ServerVersion = z.discriminatedUnion("kind", [ReleaseVersion, DevelopmentBuild]);
|
|
73
|
+
const Edition = z.enum(["oss", "ee"]);
|
|
74
|
+
const TAG = /^v?([01])\.(\d+)\.(\d+)(?:\.\d+)?(?:-([0-9A-Za-z.-]+))?$/;
|
|
75
|
+
const OSS_EDITION_NUMBER = "0";
|
|
76
|
+
const DEV_BUILD_SUFFIX = "SNAPSHOT";
|
|
77
|
+
function tagParts(tag) {
|
|
78
|
+
const match = TAG.exec(tag);
|
|
79
|
+
if (match === null) return null;
|
|
80
|
+
const [, editionNumber, major, patch, prerelease] = match;
|
|
81
|
+
if (editionNumber === void 0 || major === void 0 || patch === void 0) return null;
|
|
60
82
|
return {
|
|
83
|
+
edition: editionNumber === OSS_EDITION_NUMBER ? "oss" : "ee",
|
|
84
|
+
major: Number(major),
|
|
85
|
+
patch: Number(patch),
|
|
86
|
+
prerelease: prerelease ?? null
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function parseTag(tag) {
|
|
90
|
+
const parts = tagParts(tag);
|
|
91
|
+
if (parts === null || parts.prerelease === DEV_BUILD_SUFFIX) return {
|
|
92
|
+
kind: "development",
|
|
93
|
+
tag
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
kind: "release",
|
|
61
97
|
tag,
|
|
62
|
-
major:
|
|
63
|
-
patch:
|
|
98
|
+
major: parts.major,
|
|
99
|
+
patch: parts.patch
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function editionFromTag(tag) {
|
|
103
|
+
const parts = tagParts(tag);
|
|
104
|
+
return parts === null ? null : parts.edition;
|
|
105
|
+
}
|
|
106
|
+
function describeVersion(version) {
|
|
107
|
+
return version.kind === "release" ? version.tag : `development build ${version.tag}`;
|
|
108
|
+
}
|
|
109
|
+
const StoredServerVersion = z.object({ tag: z.string() }).loose().transform(({ tag }) => parseTag(tag));
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region ../client/src/version/probe.ts
|
|
112
|
+
const PROBE_PATH = "/api/session/properties";
|
|
113
|
+
const PROBE_TIMEOUT_MS = 1e4;
|
|
114
|
+
async function probeServer(client, opts = {}) {
|
|
115
|
+
return serverInfoFromProperties(await client.requestParsed(SessionProperties, PROBE_PATH, {
|
|
116
|
+
timeoutMs: PROBE_TIMEOUT_MS,
|
|
117
|
+
retries: opts.retries ?? 0
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
function serverInfoFromProperties(properties) {
|
|
121
|
+
const { tag } = properties.version;
|
|
122
|
+
return {
|
|
123
|
+
version: parseTag(tag),
|
|
124
|
+
edition: editionFromTag(tag),
|
|
125
|
+
date: properties.version.date ?? null,
|
|
126
|
+
hash: properties.version.hash ?? null,
|
|
127
|
+
tokenFeatures: properties["token-features"] ?? null
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region ../client/src/version/profile.ts
|
|
132
|
+
const Skew = z.enum([
|
|
133
|
+
"supported",
|
|
134
|
+
"older-than-known",
|
|
135
|
+
"newer-than-known",
|
|
136
|
+
"development"
|
|
137
|
+
]);
|
|
138
|
+
z.object({
|
|
139
|
+
version: ServerVersion,
|
|
140
|
+
buildDate: z.string().nullable(),
|
|
141
|
+
hash: z.string().nullable(),
|
|
142
|
+
edition: Edition,
|
|
143
|
+
tokenFeatures: TokenFeatures.nullable(),
|
|
144
|
+
features: Features,
|
|
145
|
+
skew: Skew
|
|
146
|
+
});
|
|
147
|
+
function createServerProfile(info) {
|
|
148
|
+
const placement = place(info.version);
|
|
149
|
+
return {
|
|
150
|
+
version: info.version,
|
|
151
|
+
buildDate: info.date,
|
|
152
|
+
hash: info.hash,
|
|
153
|
+
edition: detectEdition(info),
|
|
154
|
+
tokenFeatures: info.tokenFeatures,
|
|
155
|
+
features: evaluateFeatures(placement.major, info.tokenFeatures),
|
|
156
|
+
skew: placement.skew
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function featureGap(profile, feature) {
|
|
160
|
+
const placement = place(profile.version);
|
|
161
|
+
return ruleGap(FEATURE_RULES[feature], placement.major, profile.tokenFeatures);
|
|
162
|
+
}
|
|
163
|
+
function place(version) {
|
|
164
|
+
if (version.kind === "development") return {
|
|
165
|
+
major: "development",
|
|
166
|
+
skew: "development"
|
|
167
|
+
};
|
|
168
|
+
if (version.major > KNOWN_RANGE.max) return {
|
|
169
|
+
major: KNOWN_RANGE.max + 1,
|
|
170
|
+
skew: "newer-than-known"
|
|
171
|
+
};
|
|
172
|
+
if (version.major < KNOWN_RANGE.min) return {
|
|
173
|
+
major: version.major,
|
|
174
|
+
skew: "older-than-known"
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
major: version.major,
|
|
178
|
+
skew: "supported"
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function detectEdition(info) {
|
|
182
|
+
if (info.edition !== null) return info.edition;
|
|
183
|
+
return grantsAnyFeature(info.tokenFeatures) ? "ee" : "oss";
|
|
184
|
+
}
|
|
185
|
+
function grantsAnyFeature(tokenFeatures) {
|
|
186
|
+
return tokenFeatures !== null && Object.values(tokenFeatures).some((enabled) => enabled);
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region ../client/src/version/requirement-check.ts
|
|
190
|
+
/** The first of `features` the profile lacks, or `null` when it has every one. */
|
|
191
|
+
function checkFeatures(features, profile) {
|
|
192
|
+
for (const feature of features) {
|
|
193
|
+
const gap = featureGap(profile, feature);
|
|
194
|
+
if (gap !== null) return describeGap(gap, feature, profile);
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
function describeGap(gap, feature, profile) {
|
|
199
|
+
const rule = FEATURE_RULES[feature];
|
|
200
|
+
const serverVersion = profile.version.tag;
|
|
201
|
+
const tokenFeature = rule.tokenFeature ?? null;
|
|
202
|
+
if (gap.kind === "token") return {
|
|
203
|
+
reason: "missing-token-feature",
|
|
204
|
+
detail: missingTokenFeatureMessage(gap.tokenFeature),
|
|
205
|
+
feature,
|
|
206
|
+
since: rule.since,
|
|
207
|
+
tokenFeature,
|
|
208
|
+
serverVersion
|
|
209
|
+
};
|
|
210
|
+
return {
|
|
211
|
+
reason: "version-too-old",
|
|
212
|
+
detail: versionTooOldMessage(rule.since, describeVersion(profile.version)),
|
|
213
|
+
feature,
|
|
214
|
+
since: rule.since,
|
|
215
|
+
tokenFeature,
|
|
216
|
+
serverVersion
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/core/auth/server-summary.ts
|
|
221
|
+
const KnownRange = z.object({
|
|
222
|
+
min: z.number().int(),
|
|
223
|
+
max: z.number().int()
|
|
224
|
+
});
|
|
225
|
+
const ServerSummary = z.object({
|
|
226
|
+
version: ServerVersion.nullable(),
|
|
227
|
+
edition: Edition.nullable(),
|
|
228
|
+
skew: Skew.nullable(),
|
|
229
|
+
knownRange: KnownRange,
|
|
230
|
+
tokenFeatures: TokenFeatures.nullable(),
|
|
231
|
+
features: Features.nullable()
|
|
232
|
+
});
|
|
233
|
+
function summarizeServer(info) {
|
|
234
|
+
if (info === null) return {
|
|
235
|
+
version: null,
|
|
236
|
+
edition: null,
|
|
237
|
+
skew: null,
|
|
238
|
+
knownRange: KNOWN_RANGE,
|
|
239
|
+
tokenFeatures: null,
|
|
240
|
+
features: null
|
|
241
|
+
};
|
|
242
|
+
const profile = createServerProfile(info);
|
|
243
|
+
return {
|
|
244
|
+
version: profile.version,
|
|
245
|
+
edition: profile.edition,
|
|
246
|
+
skew: profile.skew,
|
|
247
|
+
knownRange: KNOWN_RANGE,
|
|
248
|
+
tokenFeatures: profile.tokenFeatures,
|
|
249
|
+
features: profile.features
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function skewNotice(profile) {
|
|
253
|
+
const { min, max } = KNOWN_RANGE;
|
|
254
|
+
switch (profile.skew) {
|
|
255
|
+
case "supported":
|
|
256
|
+
case "development": return null;
|
|
257
|
+
case "older-than-known": return `Metabase ${describeVersion(profile.version)} is older than this CLI supports (v${min}+); commands needing a newer feature are refused by name. Upgrade Metabase to v${min} or later.`;
|
|
258
|
+
case "newer-than-known": return `Metabase ${describeVersion(profile.version)} is newer than this CLI supports (up to v${max}); commands run as if it were v${max + 1}. Run \`mb upgrade\` for a newer CLI.`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const PROFILE_REFRESHED_REMEDY = "the profile was refreshed — retry the command.";
|
|
262
|
+
function serverChangeNote(cached, fresh) {
|
|
263
|
+
const before = describeVersion(cached.version);
|
|
264
|
+
const after = describeVersion(fresh.version);
|
|
265
|
+
if (before !== after) return `The server's version changed since the last probe (was ${before}, now ${after}); ${PROFILE_REFRESHED_REMEDY}`;
|
|
266
|
+
if (!sameTokenFeatures(cached.tokenFeatures, fresh.tokenFeatures)) return `The server's premium features changed since the last probe; ${PROFILE_REFRESHED_REMEDY}`;
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
function sameTokenFeatures(a, b) {
|
|
270
|
+
const grantedA = grantedFeatures(a);
|
|
271
|
+
const grantedB = grantedFeatures(b);
|
|
272
|
+
return grantedA.length === grantedB.length && grantedA.every((key, i) => key === grantedB[i]);
|
|
273
|
+
}
|
|
274
|
+
function grantedFeatures(features) {
|
|
275
|
+
if (features === null) return [];
|
|
276
|
+
return Object.keys(features).filter((key) => features[key] === true).toSorted();
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region ../client/src/url.ts
|
|
280
|
+
function normalizeUrl(input) {
|
|
281
|
+
const trimmed = input.trim().replace(/\/+$/, "");
|
|
282
|
+
if (!/^https?:\/\//i.test(trimmed)) throw new ConfigError("URL must start with http:// or https://");
|
|
283
|
+
return trimmed;
|
|
284
|
+
}
|
|
285
|
+
function displayUrl(input) {
|
|
286
|
+
const parsed = new URL(input);
|
|
287
|
+
const path = parsed.pathname.replace(/\/+$/, "");
|
|
288
|
+
return `${parsed.origin}${path}`;
|
|
289
|
+
}
|
|
290
|
+
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
291
|
+
"localhost",
|
|
292
|
+
"127.0.0.1",
|
|
293
|
+
"::1",
|
|
294
|
+
"[::1]"
|
|
295
|
+
]);
|
|
296
|
+
function isLoopbackHost(hostname) {
|
|
297
|
+
return LOOPBACK_HOSTS.has(hostname.toLowerCase());
|
|
298
|
+
}
|
|
299
|
+
function assertEndpointOrigin(endpoint, baseUrl, label) {
|
|
300
|
+
let parsed;
|
|
301
|
+
try {
|
|
302
|
+
parsed = new URL(endpoint);
|
|
303
|
+
} catch {
|
|
304
|
+
throw new ConfigError(`OAuth ${label} is not a valid URL: ${endpoint}`);
|
|
305
|
+
}
|
|
306
|
+
const base = new URL(baseUrl);
|
|
307
|
+
if (parsed.protocol !== "https:" && !isLoopbackHost(parsed.hostname)) throw new ConfigError(`OAuth ${label} must use https: ${endpoint}`);
|
|
308
|
+
if (parsed.origin !== base.origin) throw new ConfigError(`OAuth ${label} origin (${parsed.origin}) does not match the Metabase URL (${base.origin})`);
|
|
309
|
+
}
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region ../client/src/auth/credential.ts
|
|
312
|
+
const API_KEY_HEADER = "x-api-key";
|
|
313
|
+
const AUTHORIZATION_HEADER = "authorization";
|
|
314
|
+
const BEARER_PREFIX = "Bearer ";
|
|
315
|
+
const EXPIRY_SKEW_MS = 6e4;
|
|
316
|
+
function credentialAuthHeader(credential) {
|
|
317
|
+
if (credential.kind === "apiKey") return {
|
|
318
|
+
name: API_KEY_HEADER,
|
|
319
|
+
value: credential.apiKey
|
|
320
|
+
};
|
|
321
|
+
return {
|
|
322
|
+
name: AUTHORIZATION_HEADER,
|
|
323
|
+
value: BEARER_PREFIX + credential.accessToken
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const MAX_HEADER_CODE_UNIT = 255;
|
|
327
|
+
function headerBearingSecret(credential) {
|
|
328
|
+
if (credential.kind === "apiKey") return {
|
|
329
|
+
field: "apiKey",
|
|
330
|
+
value: credential.apiKey
|
|
331
|
+
};
|
|
332
|
+
return {
|
|
333
|
+
field: "accessToken",
|
|
334
|
+
value: credential.accessToken
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function assertCredentialHeaderSafe(credential) {
|
|
338
|
+
const { field, value } = headerBearingSecret(credential);
|
|
339
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
340
|
+
const codeUnit = value.charCodeAt(index);
|
|
341
|
+
if (codeUnit > MAX_HEADER_CODE_UNIT) {
|
|
342
|
+
const codePoint = codeUnit.toString(16).toUpperCase().padStart(4, "0");
|
|
343
|
+
throw new ConfigError(`Credential field "${field}" has a character at index ${index} (U+${codePoint}) that cannot be sent in an HTTP header. Header values are limited to Latin-1; a smart quote, ellipsis, or dash substituted during copy-paste is the usual cause.`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function credentialSecrets(credential) {
|
|
348
|
+
if (credential.kind === "apiKey") return [credential.apiKey];
|
|
349
|
+
return [credential.accessToken, credential.refreshToken];
|
|
350
|
+
}
|
|
351
|
+
function isOAuthExpired(credential, nowMs) {
|
|
352
|
+
const expiryMs = Date.parse(credential.expiresAt);
|
|
353
|
+
if (Number.isNaN(expiryMs)) return true;
|
|
354
|
+
return expiryMs - EXPIRY_SKEW_MS <= nowMs;
|
|
355
|
+
}
|
|
356
|
+
function expiresAtFromNow(expiresInSeconds, nowMs) {
|
|
357
|
+
return new Date(nowMs + expiresInSeconds * 1e3).toISOString();
|
|
358
|
+
}
|
|
359
|
+
function oauthCredentialFromTokens(tokens, refreshToken, clientId, nowMs) {
|
|
360
|
+
return {
|
|
361
|
+
kind: "oauth",
|
|
362
|
+
accessToken: tokens.access_token,
|
|
363
|
+
refreshToken,
|
|
364
|
+
expiresAt: expiresAtFromNow(tokens.expires_in ?? 3600, nowMs),
|
|
365
|
+
clientId
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region ../client/src/json.ts
|
|
370
|
+
const JSON_CONTENT_TYPE = "application/json";
|
|
371
|
+
function parseJson(input, schema, opts = {}) {
|
|
372
|
+
const result = parseJsonResult(input, schema, opts);
|
|
373
|
+
if (!result.ok) throw result.error;
|
|
374
|
+
return result.value;
|
|
375
|
+
}
|
|
376
|
+
function parseJsonResult(input, schema, opts = {}) {
|
|
377
|
+
const sourcePrefix = opts.source === void 0 ? "" : `${opts.source}: `;
|
|
378
|
+
let raw;
|
|
379
|
+
try {
|
|
380
|
+
raw = JSON.parse(input);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
return {
|
|
383
|
+
ok: false,
|
|
384
|
+
error: new ConfigError(`${sourcePrefix}invalid JSON: ${errorMessage(error)}`)
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const parsed = schema.safeParse(raw);
|
|
388
|
+
if (!parsed.success) return {
|
|
389
|
+
ok: false,
|
|
390
|
+
error: new ValidationError(`${sourcePrefix}value did not match expected schema`, {
|
|
391
|
+
source: opts.source ?? "<input>",
|
|
392
|
+
zodIssues: parsed.error.issues
|
|
393
|
+
})
|
|
394
|
+
};
|
|
395
|
+
return {
|
|
396
|
+
ok: true,
|
|
397
|
+
value: parsed.data
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function parseJsonOrPlain(text, contentType, schema, opts = {}) {
|
|
401
|
+
if (!isJsonContentType(contentType)) return parseJson(JSON.stringify(text), schema, opts);
|
|
402
|
+
const attempt = parseJsonResult(text, schema, opts);
|
|
403
|
+
if (attempt.ok) return attempt.value;
|
|
404
|
+
if (attempt.error instanceof ValidationError) throw attempt.error;
|
|
405
|
+
return parseJson(JSON.stringify(text), schema, opts);
|
|
406
|
+
}
|
|
407
|
+
function isJsonContentType(contentType) {
|
|
408
|
+
return contentType !== null && contentType.includes("application/json");
|
|
409
|
+
}
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region ../client/src/http/sanitize.ts
|
|
412
|
+
const SECRET_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
413
|
+
"authorization",
|
|
414
|
+
"x-api-key",
|
|
415
|
+
"x-metabase-session",
|
|
416
|
+
"cookie",
|
|
417
|
+
"set-cookie",
|
|
418
|
+
"proxy-authorization"
|
|
419
|
+
]);
|
|
420
|
+
const REDACTED = "[REDACTED]";
|
|
421
|
+
function redactHeaders(headers) {
|
|
422
|
+
const result = {};
|
|
423
|
+
const entries = headers instanceof Headers ? headers.entries() : Object.entries(headers);
|
|
424
|
+
for (const [key, value] of entries) result[key] = SECRET_HEADER_NAMES.has(key.toLowerCase()) ? REDACTED : value;
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
function redactBody(body, ctx) {
|
|
428
|
+
let result = body;
|
|
429
|
+
for (const secret of ctx.knownSecrets) {
|
|
430
|
+
if (secret.length === 0) continue;
|
|
431
|
+
result = result.replaceAll(secret, REDACTED);
|
|
432
|
+
}
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region ../client/src/http/errors.ts
|
|
437
|
+
const NOT_FOUND_STATUS = 404;
|
|
438
|
+
const TEXT_CONTENT_TYPE = "text/plain";
|
|
439
|
+
const ROUTE_MISSING_LITERAL = "API endpoint does not exist.";
|
|
440
|
+
const RESOURCE_MISSING_LITERAL = "Not found.";
|
|
441
|
+
const STATUS_CLASSIFICATIONS = {
|
|
442
|
+
401: { retryable: false },
|
|
443
|
+
403: { retryable: false },
|
|
444
|
+
404: { retryable: false },
|
|
445
|
+
408: {
|
|
446
|
+
retryable: true,
|
|
447
|
+
message: "Metabase timed out responding."
|
|
448
|
+
},
|
|
449
|
+
425: { retryable: true },
|
|
450
|
+
429: {
|
|
451
|
+
retryable: true,
|
|
452
|
+
message: "Metabase rate-limited the request."
|
|
453
|
+
},
|
|
454
|
+
500: { retryable: true },
|
|
455
|
+
502: { retryable: true },
|
|
456
|
+
503: { retryable: true },
|
|
457
|
+
504: {
|
|
458
|
+
retryable: true,
|
|
459
|
+
message: "Metabase timed out responding."
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
const ErrorEnvelope = z.object({
|
|
463
|
+
message: z.string().optional(),
|
|
464
|
+
error: z.string().optional(),
|
|
465
|
+
"error-message": z.string().optional(),
|
|
466
|
+
via: z.array(z.object({ message: z.string().optional() }).loose()).optional(),
|
|
467
|
+
"specific-errors": z.unknown().optional(),
|
|
468
|
+
errors: z.unknown().optional(),
|
|
469
|
+
"error-code": z.string().optional()
|
|
470
|
+
}).loose();
|
|
471
|
+
const FieldErrorNodeSchema = z.lazy(() => z.union([
|
|
472
|
+
z.string(),
|
|
473
|
+
z.array(z.string()),
|
|
474
|
+
z.record(z.string(), FieldErrorNodeSchema)
|
|
475
|
+
]));
|
|
476
|
+
const FieldErrorTree = z.record(z.string(), FieldErrorNodeSchema);
|
|
477
|
+
const FIELD_MESSAGE_SEPARATOR = "; ";
|
|
478
|
+
const MAX_EXTRACTED_MESSAGE_LEN = 500;
|
|
479
|
+
const ELLIPSIS = "…";
|
|
480
|
+
const GOT_VALUE_PATTERN = /got: ((?:[^"\\]|\\.)*)/g;
|
|
481
|
+
const MAX_GOT_VALUE_LEN = 60;
|
|
482
|
+
var HttpError = class extends MetabaseError {
|
|
483
|
+
category = "http";
|
|
484
|
+
status;
|
|
485
|
+
kind;
|
|
486
|
+
developerDetail;
|
|
487
|
+
constructor(input) {
|
|
488
|
+
const sanitizedBody = sanitizeBody(input.rawBody, input.redactionContext);
|
|
489
|
+
const redactedHeaders = redactHeaders(input.responseHeaders);
|
|
490
|
+
const kind = classifyKind(input.status, sanitizedBody, redactedHeaders);
|
|
491
|
+
super(input.overrideUserMessage ?? buildUserMessage(kind, input, sanitizedBody, redactedHeaders));
|
|
492
|
+
const fields = extractEnvelopeViews(sanitizedBody);
|
|
493
|
+
this.name = "HttpError";
|
|
494
|
+
this.status = input.status;
|
|
495
|
+
this.kind = kind;
|
|
496
|
+
this.developerDetail = {
|
|
497
|
+
status: input.status,
|
|
498
|
+
statusText: input.statusText,
|
|
499
|
+
method: input.method,
|
|
500
|
+
url: input.url,
|
|
501
|
+
responseHeaders: redactedHeaders,
|
|
502
|
+
body: sanitizedBody,
|
|
503
|
+
fieldErrors: fields.fieldErrors,
|
|
504
|
+
specificFieldErrors: fields.specificFieldErrors,
|
|
505
|
+
errorCode: fields.errorCode
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
get isRetryable() {
|
|
509
|
+
return isRetryableStatus(this.status);
|
|
510
|
+
}
|
|
511
|
+
get fieldErrors() {
|
|
512
|
+
return this.developerDetail.fieldErrors;
|
|
513
|
+
}
|
|
514
|
+
get specificFieldErrors() {
|
|
515
|
+
return this.developerDetail.specificFieldErrors;
|
|
516
|
+
}
|
|
517
|
+
get errorCode() {
|
|
518
|
+
return this.developerDetail.errorCode;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
function chainRequestFailure(cause, message) {
|
|
522
|
+
if (cause instanceof HttpError) return new HttpError({
|
|
523
|
+
status: cause.status,
|
|
524
|
+
statusText: cause.developerDetail.statusText,
|
|
525
|
+
method: cause.developerDetail.method,
|
|
526
|
+
url: cause.developerDetail.url,
|
|
527
|
+
responseHeaders: cause.developerDetail.responseHeaders,
|
|
528
|
+
rawBody: cause.developerDetail.body,
|
|
529
|
+
overrideUserMessage: message
|
|
530
|
+
});
|
|
531
|
+
return new ChainedRequestError(message, cause);
|
|
532
|
+
}
|
|
533
|
+
function isRetryableStatus(status) {
|
|
534
|
+
return STATUS_CLASSIFICATIONS[status]?.retryable === true;
|
|
535
|
+
}
|
|
536
|
+
function sanitizeBody(rawBody, ctx) {
|
|
537
|
+
if (rawBody === null) return null;
|
|
538
|
+
if (ctx === void 0) return rawBody;
|
|
539
|
+
return redactBody(rawBody, ctx);
|
|
540
|
+
}
|
|
541
|
+
function classifyKind(status, sanitizedBody, redactedHeaders) {
|
|
542
|
+
if (status === 401 || status === 403) return "auth";
|
|
543
|
+
if (status === NOT_FOUND_STATUS) return isRouteMissingResponse(sanitizedBody, redactedHeaders) ? "route-missing" : "resource-missing";
|
|
544
|
+
if (status === 429) return "rate-limit";
|
|
545
|
+
if (status >= 500 && status < 600) return "server-error";
|
|
546
|
+
return "generic";
|
|
547
|
+
}
|
|
548
|
+
function isRouteMissingResponse(sanitizedBody, redactedHeaders) {
|
|
549
|
+
if (sanitizedBody?.includes(ROUTE_MISSING_LITERAL)) return true;
|
|
550
|
+
if (sanitizedBody?.includes(RESOURCE_MISSING_LITERAL)) return false;
|
|
551
|
+
if (redactedHeaders["content-type"]?.includes("application/json")) return false;
|
|
552
|
+
if (sanitizedBody === null || sanitizedBody.trim() === "") return true;
|
|
553
|
+
return parseEnvelope(sanitizedBody) === null;
|
|
554
|
+
}
|
|
555
|
+
function buildUserMessage(kind, input, sanitizedBody, redactedHeaders) {
|
|
556
|
+
if (kind === "route-missing") return buildRouteMissingMessage(input);
|
|
557
|
+
if (kind === "resource-missing") return `Not found: ${input.method} ${pathFromUrl(input.url)}.`;
|
|
558
|
+
const fromBody = parseEnvelopeMessage(sanitizedBody);
|
|
559
|
+
if (fromBody !== null) return fromBody;
|
|
560
|
+
if (kind === "auth") return `Invalid or unauthorized API key (host: ${hostFromUrl(input.url)}).`;
|
|
561
|
+
const fromText = plainTextMessage(sanitizedBody, redactedHeaders);
|
|
562
|
+
if (fromText !== null) return fromText;
|
|
563
|
+
return defaultMessageForStatus(input.status);
|
|
564
|
+
}
|
|
565
|
+
function plainTextMessage(sanitizedBody, redactedHeaders) {
|
|
566
|
+
if (sanitizedBody === null || !redactedHeaders["content-type"]?.includes(TEXT_CONTENT_TYPE)) return null;
|
|
567
|
+
const trimmed = sanitizedBody.trim();
|
|
568
|
+
return trimmed === "" ? null : condenseMessage(trimmed);
|
|
569
|
+
}
|
|
570
|
+
function buildRouteMissingMessage(input) {
|
|
571
|
+
const path = pathFromUrl(input.url);
|
|
572
|
+
if (!input.serverTag) return `This endpoint is not available on the connected Metabase: ${input.method} ${path}.`;
|
|
573
|
+
return `This endpoint is not available on Metabase ${input.serverTag}: ${input.method} ${path}. It may require a newer Metabase major version.`;
|
|
574
|
+
}
|
|
575
|
+
function pathFromUrl(url) {
|
|
576
|
+
const parsed = new URL(url);
|
|
577
|
+
return parsed.pathname + parsed.search;
|
|
578
|
+
}
|
|
579
|
+
function hostFromUrl(url) {
|
|
580
|
+
return new URL(url).host;
|
|
581
|
+
}
|
|
582
|
+
function parseEnvelope(sanitizedBody) {
|
|
583
|
+
if (!sanitizedBody) return null;
|
|
584
|
+
const result = parseJsonResult(sanitizedBody, ErrorEnvelope);
|
|
585
|
+
return result.ok ? result.value : null;
|
|
586
|
+
}
|
|
587
|
+
const NO_ENVELOPE_VIEWS = {
|
|
588
|
+
fieldErrors: null,
|
|
589
|
+
specificFieldErrors: null,
|
|
590
|
+
errorCode: null
|
|
591
|
+
};
|
|
592
|
+
function extractEnvelopeViews(sanitizedBody) {
|
|
593
|
+
const envelope = parseEnvelope(sanitizedBody);
|
|
594
|
+
if (envelope === null) return NO_ENVELOPE_VIEWS;
|
|
595
|
+
return {
|
|
596
|
+
fieldErrors: parseFieldErrors(envelope.errors),
|
|
597
|
+
specificFieldErrors: parseFieldErrors(envelope["specific-errors"]),
|
|
598
|
+
errorCode: envelope["error-code"] ?? null
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function parseFieldErrors(value) {
|
|
602
|
+
const parsed = FieldErrorTree.safeParse(value);
|
|
603
|
+
if (!parsed.success) return null;
|
|
604
|
+
const entries = collectLeafEntries(parsed.data, []);
|
|
605
|
+
if (entries.length === 0) return null;
|
|
606
|
+
return Object.fromEntries(entries.map((entry) => [entry.path, entry.message]));
|
|
607
|
+
}
|
|
608
|
+
function parseEnvelopeMessage(sanitizedBody) {
|
|
609
|
+
const envelope = parseEnvelope(sanitizedBody);
|
|
610
|
+
if (envelope === null) return null;
|
|
611
|
+
const topLevel = envelope.message ?? envelope.error ?? envelope["error-message"];
|
|
612
|
+
if (topLevel) return condenseMessage(topLevel);
|
|
613
|
+
const viaMessage = envelope.via?.find((entry) => entry.message)?.message;
|
|
614
|
+
if (viaMessage) return condenseMessage(viaMessage);
|
|
615
|
+
const specific = formatErrorTree(envelope["specific-errors"]);
|
|
616
|
+
if (specific) return condenseMessage(specific);
|
|
617
|
+
const generic = formatErrorTree(envelope.errors);
|
|
618
|
+
if (generic) return condenseMessage(generic);
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
function formatErrorTree(value) {
|
|
622
|
+
const entries = collectLeafEntries(value, []);
|
|
623
|
+
if (entries.length === 0) return null;
|
|
624
|
+
return entries.map(formatLeafEntry).join(FIELD_MESSAGE_SEPARATOR);
|
|
625
|
+
}
|
|
626
|
+
function formatLeafEntry(entry) {
|
|
627
|
+
return entry.path === "" ? entry.message : `${entry.path}: ${entry.message}`;
|
|
628
|
+
}
|
|
629
|
+
function collectLeafEntries(value, path) {
|
|
630
|
+
if (typeof value === "string") {
|
|
631
|
+
const trimmed = value.trim();
|
|
632
|
+
return trimmed === "" ? [] : [{
|
|
633
|
+
path: path.join("."),
|
|
634
|
+
message: trimmed
|
|
635
|
+
}];
|
|
636
|
+
}
|
|
637
|
+
if (Array.isArray(value)) {
|
|
638
|
+
const messages = value.filter((entry) => typeof entry === "string" && entry.trim() !== "");
|
|
639
|
+
if (messages.length === 0) return [];
|
|
640
|
+
return [{
|
|
641
|
+
path: path.join("."),
|
|
642
|
+
message: messages.join(FIELD_MESSAGE_SEPARATOR)
|
|
643
|
+
}];
|
|
644
|
+
}
|
|
645
|
+
if (isPlainObject(value)) return Object.entries(value).flatMap(([key, child]) => collectLeafEntries(child, [...path, key]));
|
|
646
|
+
return [];
|
|
647
|
+
}
|
|
648
|
+
function condenseMessage(message) {
|
|
649
|
+
const trimmed = message.replace(GOT_VALUE_PATTERN, trimGotValue);
|
|
650
|
+
if (trimmed.length <= MAX_EXTRACTED_MESSAGE_LEN) return trimmed;
|
|
651
|
+
return trimmed.slice(0, MAX_EXTRACTED_MESSAGE_LEN - 1) + ELLIPSIS;
|
|
652
|
+
}
|
|
653
|
+
function trimGotValue(match, value) {
|
|
654
|
+
if (value.length <= MAX_GOT_VALUE_LEN) return match;
|
|
655
|
+
return `got: ${value.slice(0, MAX_GOT_VALUE_LEN)}${ELLIPSIS}`;
|
|
656
|
+
}
|
|
657
|
+
function defaultMessageForStatus(status) {
|
|
658
|
+
return STATUS_CLASSIFICATIONS[status]?.message ?? `Metabase returned ${status}.`;
|
|
659
|
+
}
|
|
660
|
+
//#endregion
|
|
661
|
+
//#region ../client/src/http/network-error.ts
|
|
662
|
+
const NETWORK_HINTS = {
|
|
663
|
+
ECONNREFUSED: (t) => `Connection refused by ${t.host} — is Metabase running and is the port correct?`,
|
|
664
|
+
ENOTFOUND: (t) => `Host not found: ${t.hostname} — check the URL.`,
|
|
665
|
+
EAI_AGAIN: (t) => `Could not resolve ${t.hostname} — check your network connection and the URL.`,
|
|
666
|
+
ECONNRESET: (t) => `Connection to ${t.host} was reset — the server may have closed it, or http/https may be mismatched.`,
|
|
667
|
+
UND_ERR_SOCKET: (t) => `Connection to ${t.host} was closed by the other side — a pooled connection or a proxy may have dropped it.`,
|
|
668
|
+
EPIPE: (t) => `Connection to ${t.host} closed while the request was still being written — the server or a proxy dropped it.`,
|
|
669
|
+
ETIMEDOUT: (t) => `Connection to ${t.host} timed out — check the host, port, and your network.`
|
|
670
|
+
};
|
|
671
|
+
const CONNECTION_CLOSED_CODES = /* @__PURE__ */ new Set([
|
|
672
|
+
"ECONNRESET",
|
|
673
|
+
"UND_ERR_SOCKET",
|
|
674
|
+
"EPIPE"
|
|
675
|
+
]);
|
|
676
|
+
const TLS_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
677
|
+
"EPROTO",
|
|
678
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
679
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
680
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
681
|
+
"CERT_HAS_EXPIRED",
|
|
682
|
+
"ERR_TLS_CERT_ALTNAME_INVALID"
|
|
683
|
+
]);
|
|
684
|
+
function buildNetworkError(error, method, url) {
|
|
685
|
+
const fallback = errorMessage(error);
|
|
686
|
+
const code = failureCode(error);
|
|
687
|
+
return new NetworkError(networkMessage(code, networkTarget(url), fallback), {
|
|
688
|
+
method,
|
|
689
|
+
url,
|
|
690
|
+
cause: code ?? fallback
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
function isConnectionClosed(error) {
|
|
694
|
+
const code = failureCode(error);
|
|
695
|
+
return code !== null && CONNECTION_CLOSED_CODES.has(code);
|
|
696
|
+
}
|
|
697
|
+
function networkMessage(code, target, fallback) {
|
|
698
|
+
if (code === null) return `Could not reach Metabase: ${fallback}`;
|
|
699
|
+
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. For a certificate the OS does not trust either, set NODE_EXTRA_CA_CERTS to its CA bundle.`;
|
|
700
|
+
const hint = NETWORK_HINTS[code];
|
|
701
|
+
if (hint === void 0) return `Could not reach Metabase: ${fallback} (${code})`;
|
|
702
|
+
return `Could not reach Metabase: ${hint(target)}`;
|
|
703
|
+
}
|
|
704
|
+
function failureCode(error) {
|
|
705
|
+
const own = errorCode(error);
|
|
706
|
+
if (own !== null) return own;
|
|
707
|
+
return errorCode(error instanceof Error ? error.cause : void 0);
|
|
708
|
+
}
|
|
709
|
+
function errorCode(value) {
|
|
710
|
+
if (value instanceof Error && "code" in value && typeof value.code === "string") return value.code;
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
function networkTarget(url) {
|
|
714
|
+
const parsed = new URL(url);
|
|
715
|
+
return {
|
|
716
|
+
host: parsed.host,
|
|
717
|
+
hostname: parsed.hostname
|
|
64
718
|
};
|
|
65
719
|
}
|
|
66
720
|
//#endregion
|
|
721
|
+
//#region ../client/src/http/oauth.ts
|
|
722
|
+
const DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
|
|
723
|
+
const FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
|
|
724
|
+
const OAUTH_TIMEOUT_MS = 3e4;
|
|
725
|
+
async function oauthFetch(request) {
|
|
726
|
+
const timeoutSignal = AbortSignal.timeout(OAUTH_TIMEOUT_MS);
|
|
727
|
+
try {
|
|
728
|
+
return await fetch(request.url, {
|
|
729
|
+
method: request.method,
|
|
730
|
+
headers: {
|
|
731
|
+
...request.headers,
|
|
732
|
+
"user-agent": request.userAgent
|
|
733
|
+
},
|
|
734
|
+
...request.body !== void 0 ? { body: request.body } : {},
|
|
735
|
+
signal: timeoutSignal
|
|
736
|
+
});
|
|
737
|
+
} catch (error) {
|
|
738
|
+
if (timeoutSignal.aborted) throw new TimeoutError(`OAuth request timed out after ${OAUTH_TIMEOUT_MS}ms`, {
|
|
739
|
+
kind: "http",
|
|
740
|
+
method: request.method,
|
|
741
|
+
url: request.url,
|
|
742
|
+
timeoutMs: OAUTH_TIMEOUT_MS
|
|
743
|
+
});
|
|
744
|
+
throw buildNetworkError(error, request.method, request.url);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
const OAUTH_SCOPE = "mb:full";
|
|
748
|
+
const OAuthServerMetadata = z.object({
|
|
749
|
+
issuer: z.string(),
|
|
750
|
+
authorization_endpoint: z.string(),
|
|
751
|
+
token_endpoint: z.string(),
|
|
752
|
+
registration_endpoint: z.string().optional(),
|
|
753
|
+
revocation_endpoint: z.string().optional(),
|
|
754
|
+
scopes_supported: z.array(z.string()).optional()
|
|
755
|
+
}).loose();
|
|
756
|
+
const RegisteredClient = z.object({ client_id: z.string() }).loose();
|
|
757
|
+
const OAuthTokens = z.object({
|
|
758
|
+
access_token: z.string(),
|
|
759
|
+
token_type: z.string(),
|
|
760
|
+
expires_in: z.number().int().optional(),
|
|
761
|
+
refresh_token: z.string().optional()
|
|
762
|
+
}).loose();
|
|
763
|
+
const OAuthErrorBody = z.object({
|
|
764
|
+
error: z.string(),
|
|
765
|
+
error_description: z.string().optional()
|
|
766
|
+
}).loose();
|
|
767
|
+
async function readJson(response, schema, source) {
|
|
768
|
+
let raw;
|
|
769
|
+
try {
|
|
770
|
+
raw = await response.json();
|
|
771
|
+
} catch (error) {
|
|
772
|
+
throw new ConfigError(`${source}: invalid JSON response (${errorMessage(error)})`);
|
|
773
|
+
}
|
|
774
|
+
const parsed = schema.safeParse(raw);
|
|
775
|
+
if (!parsed.success) throw new ConfigError(`${source}: unexpected response shape`);
|
|
776
|
+
return parsed.data;
|
|
777
|
+
}
|
|
778
|
+
async function failure(response, source, method, url) {
|
|
779
|
+
const rawBody = await response.text().catch(() => "");
|
|
780
|
+
if (isRetryableStatus(response.status)) return new HttpError({
|
|
781
|
+
status: response.status,
|
|
782
|
+
statusText: response.statusText,
|
|
783
|
+
method,
|
|
784
|
+
url,
|
|
785
|
+
responseHeaders: response.headers,
|
|
786
|
+
rawBody
|
|
787
|
+
});
|
|
788
|
+
const parsed = parseJsonResult(rawBody, OAuthErrorBody);
|
|
789
|
+
if (parsed.ok) {
|
|
790
|
+
const detail = parsed.value.error_description ?? parsed.value.error;
|
|
791
|
+
return new ConfigError(`${source} failed (${response.status}): ${detail}`);
|
|
792
|
+
}
|
|
793
|
+
return new ConfigError(`${source} failed (${response.status})`);
|
|
794
|
+
}
|
|
795
|
+
async function postForm(url, userAgent, params, schema, source) {
|
|
796
|
+
const response = await oauthFetch({
|
|
797
|
+
url,
|
|
798
|
+
method: "POST",
|
|
799
|
+
userAgent,
|
|
800
|
+
headers: {
|
|
801
|
+
"content-type": FORM_CONTENT_TYPE,
|
|
802
|
+
accept: JSON_CONTENT_TYPE
|
|
803
|
+
},
|
|
804
|
+
body: new URLSearchParams(params)
|
|
805
|
+
});
|
|
806
|
+
if (!response.ok) throw await failure(response, source, "POST", url);
|
|
807
|
+
return readJson(response, schema, source);
|
|
808
|
+
}
|
|
809
|
+
const OAUTH_UNSUPPORTED_MESSAGE = "this Metabase does not support OAuth login (requires Metabase v63 or newer)";
|
|
810
|
+
async function fetchMetadata(baseUrl, userAgent) {
|
|
811
|
+
const response = await oauthFetch({
|
|
812
|
+
url: `${baseUrl}${DISCOVERY_PATH}`,
|
|
813
|
+
method: "GET",
|
|
814
|
+
userAgent,
|
|
815
|
+
headers: { accept: JSON_CONTENT_TYPE }
|
|
816
|
+
});
|
|
817
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
818
|
+
if (!response.ok || !contentType.includes("json")) {
|
|
819
|
+
await response.body?.cancel().catch(() => void 0);
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
return readJson(response, OAuthServerMetadata, "OAuth discovery");
|
|
823
|
+
}
|
|
824
|
+
function pinEndpoints(metadata, baseUrl) {
|
|
825
|
+
assertEndpointOrigin(metadata.issuer, baseUrl, "issuer");
|
|
826
|
+
assertEndpointOrigin(metadata.authorization_endpoint, baseUrl, "authorization endpoint");
|
|
827
|
+
assertEndpointOrigin(metadata.token_endpoint, baseUrl, "token endpoint");
|
|
828
|
+
if (metadata.registration_endpoint !== void 0) assertEndpointOrigin(metadata.registration_endpoint, baseUrl, "registration endpoint");
|
|
829
|
+
if (metadata.revocation_endpoint !== void 0) assertEndpointOrigin(metadata.revocation_endpoint, baseUrl, "revocation endpoint");
|
|
830
|
+
return metadata;
|
|
831
|
+
}
|
|
832
|
+
async function grantsFullAccessScope(metadata, baseUrl, userAgent) {
|
|
833
|
+
if (metadata.scopes_supported?.includes("mb:full") === true) return true;
|
|
834
|
+
const url = `${baseUrl}${PROBE_PATH}`;
|
|
835
|
+
const response = await oauthFetch({
|
|
836
|
+
url,
|
|
837
|
+
method: "GET",
|
|
838
|
+
userAgent,
|
|
839
|
+
headers: { accept: JSON_CONTENT_TYPE }
|
|
840
|
+
});
|
|
841
|
+
if (!response.ok) throw await failure(response, "Server properties", "GET", url);
|
|
842
|
+
return createServerProfile(serverInfoFromProperties(await readJson(response, SessionProperties, "Server properties"))).features.oauthFullAccessScope;
|
|
843
|
+
}
|
|
844
|
+
async function tryDiscoverMetadata(baseUrl, userAgent) {
|
|
845
|
+
const metadata = await fetchMetadata(baseUrl, userAgent);
|
|
846
|
+
if (metadata === null || !await grantsFullAccessScope(metadata, baseUrl, userAgent)) return null;
|
|
847
|
+
return pinEndpoints(metadata, baseUrl);
|
|
848
|
+
}
|
|
849
|
+
async function discoverLoginMetadata(baseUrl, userAgent) {
|
|
850
|
+
const metadata = await tryDiscoverMetadata(baseUrl, userAgent);
|
|
851
|
+
if (metadata === null) throw new ConfigError(OAUTH_UNSUPPORTED_MESSAGE);
|
|
852
|
+
return metadata;
|
|
853
|
+
}
|
|
854
|
+
async function discoverMetadata(baseUrl, userAgent) {
|
|
855
|
+
const metadata = await fetchMetadata(baseUrl, userAgent);
|
|
856
|
+
if (metadata === null) throw new ConfigError(OAUTH_UNSUPPORTED_MESSAGE);
|
|
857
|
+
return pinEndpoints(metadata, baseUrl);
|
|
858
|
+
}
|
|
859
|
+
async function registerClient(input) {
|
|
860
|
+
const response = await oauthFetch({
|
|
861
|
+
url: input.registrationEndpoint,
|
|
862
|
+
method: "POST",
|
|
863
|
+
userAgent: input.userAgent,
|
|
864
|
+
headers: {
|
|
865
|
+
"content-type": JSON_CONTENT_TYPE,
|
|
866
|
+
accept: JSON_CONTENT_TYPE
|
|
867
|
+
},
|
|
868
|
+
body: JSON.stringify({
|
|
869
|
+
application_type: "native",
|
|
870
|
+
client_name: input.clientName,
|
|
871
|
+
redirect_uris: [input.redirectUri],
|
|
872
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
873
|
+
response_types: ["code"],
|
|
874
|
+
token_endpoint_auth_method: "none",
|
|
875
|
+
scope: OAUTH_SCOPE
|
|
876
|
+
})
|
|
877
|
+
});
|
|
878
|
+
if (!response.ok) throw await failure(response, "OAuth client registration", "POST", input.registrationEndpoint);
|
|
879
|
+
return readJson(response, RegisteredClient, "OAuth client registration");
|
|
880
|
+
}
|
|
881
|
+
function exchangeCode(input) {
|
|
882
|
+
return postForm(input.tokenEndpoint, input.userAgent, {
|
|
883
|
+
grant_type: "authorization_code",
|
|
884
|
+
code: input.code,
|
|
885
|
+
redirect_uri: input.redirectUri,
|
|
886
|
+
client_id: input.clientId,
|
|
887
|
+
code_verifier: input.codeVerifier
|
|
888
|
+
}, OAuthTokens, "OAuth token exchange");
|
|
889
|
+
}
|
|
890
|
+
function refreshTokens(input) {
|
|
891
|
+
return postForm(input.tokenEndpoint, input.userAgent, {
|
|
892
|
+
grant_type: "refresh_token",
|
|
893
|
+
refresh_token: input.refreshToken,
|
|
894
|
+
client_id: input.clientId
|
|
895
|
+
}, OAuthTokens, "OAuth token refresh");
|
|
896
|
+
}
|
|
897
|
+
async function revokeToken(input) {
|
|
898
|
+
const response = await oauthFetch({
|
|
899
|
+
url: input.revocationEndpoint,
|
|
900
|
+
method: "POST",
|
|
901
|
+
userAgent: input.userAgent,
|
|
902
|
+
headers: { "content-type": FORM_CONTENT_TYPE },
|
|
903
|
+
body: new URLSearchParams({
|
|
904
|
+
token: input.token,
|
|
905
|
+
client_id: input.clientId
|
|
906
|
+
})
|
|
907
|
+
});
|
|
908
|
+
if (!response.ok) throw await failure(response, "OAuth token revocation", "POST", input.revocationEndpoint);
|
|
909
|
+
await response.body?.cancel().catch(() => void 0);
|
|
910
|
+
}
|
|
911
|
+
//#endregion
|
|
912
|
+
//#region ../client/src/auth/oauth-session.ts
|
|
913
|
+
async function refreshOAuthCredential(baseUrl, credential, nowMs, userAgent) {
|
|
914
|
+
const tokens = await refreshTokens({
|
|
915
|
+
tokenEndpoint: (await discoverMetadata(baseUrl, userAgent)).token_endpoint,
|
|
916
|
+
refreshToken: credential.refreshToken,
|
|
917
|
+
clientId: credential.clientId,
|
|
918
|
+
userAgent
|
|
919
|
+
});
|
|
920
|
+
return oauthCredentialFromTokens(tokens, tokens.refresh_token ?? credential.refreshToken, credential.clientId, nowMs);
|
|
921
|
+
}
|
|
922
|
+
async function revokeOAuthCredential(baseUrl, credential, userAgent) {
|
|
923
|
+
const revocationEndpoint = (await discoverMetadata(baseUrl, userAgent)).revocation_endpoint;
|
|
924
|
+
if (revocationEndpoint === void 0) return false;
|
|
925
|
+
await revokeToken({
|
|
926
|
+
revocationEndpoint,
|
|
927
|
+
token: credential.refreshToken,
|
|
928
|
+
clientId: credential.clientId,
|
|
929
|
+
userAgent
|
|
930
|
+
});
|
|
931
|
+
await revokeToken({
|
|
932
|
+
revocationEndpoint,
|
|
933
|
+
token: credential.accessToken,
|
|
934
|
+
clientId: credential.clientId,
|
|
935
|
+
userAgent
|
|
936
|
+
});
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
//#endregion
|
|
940
|
+
//#region src/core/user-agent.ts
|
|
941
|
+
const USER_AGENT = `metabase-cli/${version}`;
|
|
942
|
+
const OAUTH_CLIENT_NAME = "Metabase CLI";
|
|
943
|
+
//#endregion
|
|
67
944
|
//#region src/core/paths.ts
|
|
68
945
|
const APP_DIR_NAME = "metabase-cli";
|
|
69
946
|
function configDir() {
|
|
@@ -79,10 +956,14 @@ const ProbedUser = z.object({
|
|
|
79
956
|
});
|
|
80
957
|
const ProfileLastProbe = z.object({
|
|
81
958
|
at: z.iso.datetime(),
|
|
82
|
-
version:
|
|
959
|
+
version: StoredServerVersion,
|
|
960
|
+
edition: Edition.nullable().default(null),
|
|
961
|
+
date: z.string().nullable().default(null),
|
|
962
|
+
hash: z.string().nullable().default(null),
|
|
83
963
|
tokenFeatures: TokenFeatures.nullable(),
|
|
84
964
|
user: ProbedUser
|
|
85
965
|
});
|
|
966
|
+
const UntaggedLastProbe = z.object({ version: z.null() }).loose().transform(() => null);
|
|
86
967
|
const ProfileFailureKind = z.enum([
|
|
87
968
|
"auth",
|
|
88
969
|
"network",
|
|
@@ -105,7 +986,7 @@ const ProfileRecord = z.object({
|
|
|
105
986
|
url: z.string(),
|
|
106
987
|
apiKey: z.string().nullable(),
|
|
107
988
|
oauth: ProfileOAuth.nullable().default(null),
|
|
108
|
-
lastProbe: ProfileLastProbe.nullable(),
|
|
989
|
+
lastProbe: z.union([ProfileLastProbe, UntaggedLastProbe]).nullable(),
|
|
109
990
|
lastFailure: ProfileLastFailure.nullable()
|
|
110
991
|
}).loose();
|
|
111
992
|
const ProfilesFile = z.object({ profiles: z.array(ProfileRecord) }).loose();
|
|
@@ -374,6 +1255,9 @@ async function writeProbeResult(name, input) {
|
|
|
374
1255
|
const probe = ProfileLastProbe.parse({
|
|
375
1256
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
376
1257
|
version: input.server.version,
|
|
1258
|
+
edition: input.server.edition,
|
|
1259
|
+
date: input.server.date,
|
|
1260
|
+
hash: input.server.hash,
|
|
377
1261
|
tokenFeatures: input.server.tokenFeatures,
|
|
378
1262
|
user: input.user
|
|
379
1263
|
});
|
|
@@ -425,207 +1309,6 @@ async function clearProfile(name = DEFAULT_PROFILE) {
|
|
|
425
1309
|
return true;
|
|
426
1310
|
}
|
|
427
1311
|
//#endregion
|
|
428
|
-
//#region ../client/src/http/oauth.ts
|
|
429
|
-
const DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
|
|
430
|
-
const FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
|
|
431
|
-
const OAUTH_TIMEOUT_MS = 3e4;
|
|
432
|
-
async function oauthFetch(request) {
|
|
433
|
-
const timeoutSignal = AbortSignal.timeout(OAUTH_TIMEOUT_MS);
|
|
434
|
-
try {
|
|
435
|
-
return await fetch(request.url, {
|
|
436
|
-
method: request.method,
|
|
437
|
-
headers: {
|
|
438
|
-
...request.headers,
|
|
439
|
-
"user-agent": request.userAgent
|
|
440
|
-
},
|
|
441
|
-
...request.body !== void 0 ? { body: request.body } : {},
|
|
442
|
-
signal: timeoutSignal
|
|
443
|
-
});
|
|
444
|
-
} catch (error) {
|
|
445
|
-
if (timeoutSignal.aborted) throw new TimeoutError(`OAuth request timed out after ${OAUTH_TIMEOUT_MS}ms`, {
|
|
446
|
-
kind: "http",
|
|
447
|
-
method: request.method,
|
|
448
|
-
url: request.url,
|
|
449
|
-
timeoutMs: OAUTH_TIMEOUT_MS
|
|
450
|
-
});
|
|
451
|
-
throw buildNetworkError(error, request.method, request.url);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
const OAUTH_SCOPE = "mb:full";
|
|
455
|
-
const OAuthServerMetadata = z.object({
|
|
456
|
-
issuer: z.string(),
|
|
457
|
-
authorization_endpoint: z.string(),
|
|
458
|
-
token_endpoint: z.string(),
|
|
459
|
-
registration_endpoint: z.string().optional(),
|
|
460
|
-
revocation_endpoint: z.string().optional(),
|
|
461
|
-
scopes_supported: z.array(z.string()).optional()
|
|
462
|
-
}).loose();
|
|
463
|
-
const RegisteredClient = z.object({ client_id: z.string() }).loose();
|
|
464
|
-
const OAuthTokens = z.object({
|
|
465
|
-
access_token: z.string(),
|
|
466
|
-
token_type: z.string(),
|
|
467
|
-
expires_in: z.number().int().optional(),
|
|
468
|
-
refresh_token: z.string().optional()
|
|
469
|
-
}).loose();
|
|
470
|
-
const OAuthErrorBody = z.object({
|
|
471
|
-
error: z.string(),
|
|
472
|
-
error_description: z.string().optional()
|
|
473
|
-
}).loose();
|
|
474
|
-
async function readJson(response, schema, source) {
|
|
475
|
-
let raw;
|
|
476
|
-
try {
|
|
477
|
-
raw = await response.json();
|
|
478
|
-
} catch (error) {
|
|
479
|
-
throw new ConfigError(`${source}: invalid JSON response (${errorMessage(error)})`);
|
|
480
|
-
}
|
|
481
|
-
const parsed = schema.safeParse(raw);
|
|
482
|
-
if (!parsed.success) throw new ConfigError(`${source}: unexpected response shape`);
|
|
483
|
-
return parsed.data;
|
|
484
|
-
}
|
|
485
|
-
async function failure(response, source, method, url) {
|
|
486
|
-
const rawBody = await response.text().catch(() => "");
|
|
487
|
-
if (isRetryableStatus(response.status)) return new HttpError({
|
|
488
|
-
status: response.status,
|
|
489
|
-
statusText: response.statusText,
|
|
490
|
-
method,
|
|
491
|
-
url,
|
|
492
|
-
responseHeaders: response.headers,
|
|
493
|
-
rawBody
|
|
494
|
-
});
|
|
495
|
-
const parsed = parseJsonResult(rawBody, OAuthErrorBody);
|
|
496
|
-
if (parsed.ok) {
|
|
497
|
-
const detail = parsed.value.error_description ?? parsed.value.error;
|
|
498
|
-
return new ConfigError(`${source} failed (${response.status}): ${detail}`);
|
|
499
|
-
}
|
|
500
|
-
return new ConfigError(`${source} failed (${response.status})`);
|
|
501
|
-
}
|
|
502
|
-
async function postForm(url, userAgent, params, schema, source) {
|
|
503
|
-
const response = await oauthFetch({
|
|
504
|
-
url,
|
|
505
|
-
method: "POST",
|
|
506
|
-
userAgent,
|
|
507
|
-
headers: {
|
|
508
|
-
"content-type": FORM_CONTENT_TYPE,
|
|
509
|
-
accept: JSON_CONTENT_TYPE
|
|
510
|
-
},
|
|
511
|
-
body: new URLSearchParams(params)
|
|
512
|
-
});
|
|
513
|
-
if (!response.ok) throw await failure(response, source, "POST", url);
|
|
514
|
-
return readJson(response, schema, source);
|
|
515
|
-
}
|
|
516
|
-
const OAUTH_UNSUPPORTED_MESSAGE = "this Metabase does not support OAuth login (requires Metabase v63 or newer)";
|
|
517
|
-
async function tryDiscoverMetadata(baseUrl, userAgent) {
|
|
518
|
-
const response = await oauthFetch({
|
|
519
|
-
url: `${baseUrl}${DISCOVERY_PATH}`,
|
|
520
|
-
method: "GET",
|
|
521
|
-
userAgent,
|
|
522
|
-
headers: { accept: JSON_CONTENT_TYPE }
|
|
523
|
-
});
|
|
524
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
525
|
-
if (!response.ok || !contentType.includes("json")) {
|
|
526
|
-
await response.body?.cancel().catch(() => void 0);
|
|
527
|
-
return null;
|
|
528
|
-
}
|
|
529
|
-
const metadata = await readJson(response, OAuthServerMetadata, "OAuth discovery");
|
|
530
|
-
const scopes = metadata.scopes_supported;
|
|
531
|
-
if (scopes !== void 0 && !scopes.includes("mb:full")) return null;
|
|
532
|
-
assertEndpointOrigin(metadata.issuer, baseUrl, "issuer");
|
|
533
|
-
assertEndpointOrigin(metadata.authorization_endpoint, baseUrl, "authorization endpoint");
|
|
534
|
-
assertEndpointOrigin(metadata.token_endpoint, baseUrl, "token endpoint");
|
|
535
|
-
if (metadata.registration_endpoint !== void 0) assertEndpointOrigin(metadata.registration_endpoint, baseUrl, "registration endpoint");
|
|
536
|
-
if (metadata.revocation_endpoint !== void 0) assertEndpointOrigin(metadata.revocation_endpoint, baseUrl, "revocation endpoint");
|
|
537
|
-
return metadata;
|
|
538
|
-
}
|
|
539
|
-
async function discoverMetadata(baseUrl, userAgent) {
|
|
540
|
-
const metadata = await tryDiscoverMetadata(baseUrl, userAgent);
|
|
541
|
-
if (metadata === null) throw new ConfigError(OAUTH_UNSUPPORTED_MESSAGE);
|
|
542
|
-
return metadata;
|
|
543
|
-
}
|
|
544
|
-
async function registerClient(input) {
|
|
545
|
-
const response = await oauthFetch({
|
|
546
|
-
url: input.registrationEndpoint,
|
|
547
|
-
method: "POST",
|
|
548
|
-
userAgent: input.userAgent,
|
|
549
|
-
headers: {
|
|
550
|
-
"content-type": JSON_CONTENT_TYPE,
|
|
551
|
-
accept: JSON_CONTENT_TYPE
|
|
552
|
-
},
|
|
553
|
-
body: JSON.stringify({
|
|
554
|
-
application_type: "native",
|
|
555
|
-
client_name: input.clientName,
|
|
556
|
-
redirect_uris: [input.redirectUri],
|
|
557
|
-
grant_types: ["authorization_code", "refresh_token"],
|
|
558
|
-
response_types: ["code"],
|
|
559
|
-
token_endpoint_auth_method: "none",
|
|
560
|
-
scope: OAUTH_SCOPE
|
|
561
|
-
})
|
|
562
|
-
});
|
|
563
|
-
if (!response.ok) throw await failure(response, "OAuth client registration", "POST", input.registrationEndpoint);
|
|
564
|
-
return readJson(response, RegisteredClient, "OAuth client registration");
|
|
565
|
-
}
|
|
566
|
-
function exchangeCode(input) {
|
|
567
|
-
return postForm(input.tokenEndpoint, input.userAgent, {
|
|
568
|
-
grant_type: "authorization_code",
|
|
569
|
-
code: input.code,
|
|
570
|
-
redirect_uri: input.redirectUri,
|
|
571
|
-
client_id: input.clientId,
|
|
572
|
-
code_verifier: input.codeVerifier
|
|
573
|
-
}, OAuthTokens, "OAuth token exchange");
|
|
574
|
-
}
|
|
575
|
-
function refreshTokens(input) {
|
|
576
|
-
return postForm(input.tokenEndpoint, input.userAgent, {
|
|
577
|
-
grant_type: "refresh_token",
|
|
578
|
-
refresh_token: input.refreshToken,
|
|
579
|
-
client_id: input.clientId
|
|
580
|
-
}, OAuthTokens, "OAuth token refresh");
|
|
581
|
-
}
|
|
582
|
-
async function revokeToken(input) {
|
|
583
|
-
const response = await oauthFetch({
|
|
584
|
-
url: input.revocationEndpoint,
|
|
585
|
-
method: "POST",
|
|
586
|
-
userAgent: input.userAgent,
|
|
587
|
-
headers: { "content-type": FORM_CONTENT_TYPE },
|
|
588
|
-
body: new URLSearchParams({
|
|
589
|
-
token: input.token,
|
|
590
|
-
client_id: input.clientId
|
|
591
|
-
})
|
|
592
|
-
});
|
|
593
|
-
if (!response.ok) throw await failure(response, "OAuth token revocation", "POST", input.revocationEndpoint);
|
|
594
|
-
await response.body?.cancel().catch(() => void 0);
|
|
595
|
-
}
|
|
596
|
-
//#endregion
|
|
597
|
-
//#region ../client/src/auth/oauth-session.ts
|
|
598
|
-
async function refreshOAuthCredential(baseUrl, credential, nowMs, userAgent) {
|
|
599
|
-
const tokens = await refreshTokens({
|
|
600
|
-
tokenEndpoint: (await discoverMetadata(baseUrl, userAgent)).token_endpoint,
|
|
601
|
-
refreshToken: credential.refreshToken,
|
|
602
|
-
clientId: credential.clientId,
|
|
603
|
-
userAgent
|
|
604
|
-
});
|
|
605
|
-
return oauthCredentialFromTokens(tokens, tokens.refresh_token ?? credential.refreshToken, credential.clientId, nowMs);
|
|
606
|
-
}
|
|
607
|
-
async function revokeOAuthCredential(baseUrl, credential, userAgent) {
|
|
608
|
-
const revocationEndpoint = (await discoverMetadata(baseUrl, userAgent)).revocation_endpoint;
|
|
609
|
-
if (revocationEndpoint === void 0) return false;
|
|
610
|
-
await revokeToken({
|
|
611
|
-
revocationEndpoint,
|
|
612
|
-
token: credential.refreshToken,
|
|
613
|
-
clientId: credential.clientId,
|
|
614
|
-
userAgent
|
|
615
|
-
});
|
|
616
|
-
await revokeToken({
|
|
617
|
-
revocationEndpoint,
|
|
618
|
-
token: credential.accessToken,
|
|
619
|
-
clientId: credential.clientId,
|
|
620
|
-
userAgent
|
|
621
|
-
});
|
|
622
|
-
return true;
|
|
623
|
-
}
|
|
624
|
-
//#endregion
|
|
625
|
-
//#region src/core/user-agent.ts
|
|
626
|
-
const USER_AGENT = `metabase-cli/${version}`;
|
|
627
|
-
const OAUTH_CLIENT_NAME = "Metabase CLI";
|
|
628
|
-
//#endregion
|
|
629
1312
|
//#region src/core/config.ts
|
|
630
1313
|
function isPreflightSkipped() {
|
|
631
1314
|
return readEnv(ENV_SKIP_PREFLIGHT) === "1";
|
|
@@ -741,6 +1424,34 @@ function resolveCredential(flagKey, envKey, stored) {
|
|
|
741
1424
|
return null;
|
|
742
1425
|
}
|
|
743
1426
|
//#endregion
|
|
1427
|
+
//#region src/core/auth/cached-server.ts
|
|
1428
|
+
async function lookupCachedProbe(profileName, url) {
|
|
1429
|
+
const record = await readProfileRecord(profileName);
|
|
1430
|
+
if (record === null) return { kind: "no-profile" };
|
|
1431
|
+
if (record.lastProbe === null) return { kind: "never-probed" };
|
|
1432
|
+
if (url !== null && normalizeUrl(record.url) !== url) return {
|
|
1433
|
+
kind: "other-url",
|
|
1434
|
+
url
|
|
1435
|
+
};
|
|
1436
|
+
return {
|
|
1437
|
+
kind: "found",
|
|
1438
|
+
probe: record.lastProbe
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
async function readCachedProbe(profileName, url) {
|
|
1442
|
+
const hit = await lookupCachedProbe(profileName, url);
|
|
1443
|
+
return hit.kind === "found" ? hit.probe : null;
|
|
1444
|
+
}
|
|
1445
|
+
async function readCachedServerProfile(profileName) {
|
|
1446
|
+
const envUrl = readEnvCredentials().url;
|
|
1447
|
+
const hit = await lookupCachedProbe(profileName, envUrl === null ? null : normalizeUrl(envUrl));
|
|
1448
|
+
if (hit.kind !== "found") return hit;
|
|
1449
|
+
return {
|
|
1450
|
+
kind: "found",
|
|
1451
|
+
profile: createServerProfile(hit.probe)
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
//#endregion
|
|
744
1455
|
//#region src/output/format.ts
|
|
745
1456
|
function resolveFormat({ json, format, isTty }) {
|
|
746
1457
|
const explicit = format !== void 0 && format !== "auto" ? format : null;
|
|
@@ -875,7 +1586,7 @@ function flagCandidates(token) {
|
|
|
875
1586
|
//#endregion
|
|
876
1587
|
//#region src/commands/runtime.ts
|
|
877
1588
|
function defineMetabaseCommand(def) {
|
|
878
|
-
const
|
|
1589
|
+
const requirements = def.requires === null ? null : deriveRequirements(def.requires);
|
|
879
1590
|
const cmd = defineCommand({
|
|
880
1591
|
meta: def.meta,
|
|
881
1592
|
args: def.args,
|
|
@@ -888,35 +1599,44 @@ function defineMetabaseCommand(def) {
|
|
|
888
1599
|
assertKnownFlags(rawArgs, def.args);
|
|
889
1600
|
let cachedConfig = null;
|
|
890
1601
|
let cachedClient = null;
|
|
891
|
-
let cachedServerInfo = null;
|
|
892
1602
|
const getResolvedConfig = async () => {
|
|
893
1603
|
if (cachedConfig === null) cachedConfig = await resolveConfig(buildConfigFlags(ctx));
|
|
894
1604
|
return cachedConfig;
|
|
895
1605
|
};
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
};
|
|
1606
|
+
const preflightSkipped = ctx.skipPreflight || isPreflightSkipped();
|
|
1607
|
+
let cachedServer = null;
|
|
1608
|
+
const noticeSkew = createSkewNotifier();
|
|
900
1609
|
const rawGetClient = async () => {
|
|
901
1610
|
if (cachedClient === null) {
|
|
902
1611
|
const resolved = await getResolvedConfig();
|
|
903
|
-
const
|
|
1612
|
+
const probe = await readCachedProbe(resolved.profile, resolved.url);
|
|
1613
|
+
const server = probe === null ? null : createServerProfile(probe);
|
|
1614
|
+
const { createClient } = await import("./client-cJ7aw0Gt.mjs").then((n) => n.t);
|
|
904
1615
|
cachedClient = createClient({
|
|
905
1616
|
url: resolved.url,
|
|
906
1617
|
credential: resolved.credential
|
|
907
1618
|
}, {
|
|
908
1619
|
userAgent: USER_AGENT,
|
|
909
|
-
|
|
1620
|
+
...server !== null && { server },
|
|
910
1621
|
refreshCredential: createCredentialRefresher(resolved.profile),
|
|
911
|
-
signal: interruptSignal
|
|
1622
|
+
signal: interruptSignal,
|
|
1623
|
+
enforceRequirements: !preflightSkipped
|
|
912
1624
|
});
|
|
1625
|
+
if (probe !== null && server !== null) {
|
|
1626
|
+
cachedServer = {
|
|
1627
|
+
client: cachedClient,
|
|
1628
|
+
profileName: resolved.profile,
|
|
1629
|
+
probe
|
|
1630
|
+
};
|
|
1631
|
+
noticeSkew(server);
|
|
1632
|
+
}
|
|
913
1633
|
}
|
|
914
1634
|
return cachedClient;
|
|
915
1635
|
};
|
|
916
|
-
const enforcePreflight = createPreflightEnforcer(
|
|
1636
|
+
const enforcePreflight = createPreflightEnforcer(requirements === null ? null : requirements.features, preflightSkipped, noticeSkew);
|
|
917
1637
|
const getClient = async () => {
|
|
918
1638
|
const client = await rawGetClient();
|
|
919
|
-
await enforcePreflight();
|
|
1639
|
+
await enforcePreflight(client);
|
|
920
1640
|
return client;
|
|
921
1641
|
};
|
|
922
1642
|
try {
|
|
@@ -924,9 +1644,10 @@ function defineMetabaseCommand(def) {
|
|
|
924
1644
|
args,
|
|
925
1645
|
ctx,
|
|
926
1646
|
getClient,
|
|
927
|
-
getResolvedConfig
|
|
928
|
-
getServerInfo
|
|
1647
|
+
getResolvedConfig
|
|
929
1648
|
});
|
|
1649
|
+
} catch (error) {
|
|
1650
|
+
throw await refreshProfileOnStaleError(error, cachedServer);
|
|
930
1651
|
} finally {
|
|
931
1652
|
emitPendingWarnings();
|
|
932
1653
|
}
|
|
@@ -941,10 +1662,16 @@ function defineMetabaseCommand(def) {
|
|
|
941
1662
|
skills: def.skills ?? [],
|
|
942
1663
|
inputSchema: def.inputSchema ?? null,
|
|
943
1664
|
outputSchema: def.outputSchema ?? null,
|
|
944
|
-
|
|
1665
|
+
requires: requirements
|
|
945
1666
|
});
|
|
946
1667
|
return cmd;
|
|
947
1668
|
}
|
|
1669
|
+
function deriveRequirements(methods) {
|
|
1670
|
+
return {
|
|
1671
|
+
methods,
|
|
1672
|
+
features: [...new Set(methods.flatMap((key) => methodRequirements(key)))]
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
948
1675
|
function emitPendingWarnings() {
|
|
949
1676
|
for (const message of consumeLegacyEnvWarnings()) warn(message);
|
|
950
1677
|
const legacy = consumeLegacyStorageWarning();
|
|
@@ -952,34 +1679,49 @@ function emitPendingWarnings() {
|
|
|
952
1679
|
const downgrade = consumeKeyringDowngradeWarning();
|
|
953
1680
|
if (downgrade !== null) warn(downgrade);
|
|
954
1681
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1682
|
+
function createSkewNotifier() {
|
|
1683
|
+
let noticed = false;
|
|
1684
|
+
return (profile) => {
|
|
1685
|
+
if (noticed) return;
|
|
1686
|
+
noticed = true;
|
|
1687
|
+
const notice = skewNotice(profile);
|
|
1688
|
+
if (notice !== null) warn(notice);
|
|
961
1689
|
};
|
|
962
1690
|
}
|
|
963
1691
|
const NO_OP_ENFORCER = async () => {};
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
if (required === null || skip || isPreflightSkipped() || isBaseline(required)) return NO_OP_ENFORCER;
|
|
1692
|
+
function createPreflightEnforcer(features, skip, noticeSkew) {
|
|
1693
|
+
if (features === null || skip || features.length === 0) return NO_OP_ENFORCER;
|
|
967
1694
|
let done = false;
|
|
968
|
-
return async () => {
|
|
1695
|
+
return async (client) => {
|
|
969
1696
|
if (done) return;
|
|
970
1697
|
done = true;
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
}
|
|
976
|
-
const failure = checkCapabilities(info, required);
|
|
977
|
-
if (failure === null || failure.reason === "unknown-version") return;
|
|
978
|
-
throw new CapabilityError(failure);
|
|
1698
|
+
const profile = await client.server();
|
|
1699
|
+
noticeSkew(profile);
|
|
1700
|
+
const failure = checkFeatures(features, profile);
|
|
1701
|
+
if (failure !== null) throw new CapabilityError(failure);
|
|
979
1702
|
};
|
|
980
1703
|
}
|
|
981
|
-
function
|
|
982
|
-
|
|
1704
|
+
async function refreshProfileOnStaleError(error, cached) {
|
|
1705
|
+
if (cached === null) return error;
|
|
1706
|
+
if (!(error instanceof ResponseShapeError) && !(error instanceof CapabilityError)) return error;
|
|
1707
|
+
const note = await refreshChangedProbe(cached);
|
|
1708
|
+
return note === null ? error : new ProfileRefreshedError(error, note);
|
|
1709
|
+
}
|
|
1710
|
+
async function refreshChangedProbe(cached) {
|
|
1711
|
+
let fresh;
|
|
1712
|
+
try {
|
|
1713
|
+
fresh = await probeServer(cached.client);
|
|
1714
|
+
} catch (error) {
|
|
1715
|
+
if (error instanceof MetabaseError) return null;
|
|
1716
|
+
throw error;
|
|
1717
|
+
}
|
|
1718
|
+
const note = serverChangeNote(cached.probe, fresh);
|
|
1719
|
+
if (note === null) return null;
|
|
1720
|
+
await writeProbeResult(cached.profileName, {
|
|
1721
|
+
user: cached.probe.user,
|
|
1722
|
+
server: fresh
|
|
1723
|
+
});
|
|
1724
|
+
return note;
|
|
983
1725
|
}
|
|
984
1726
|
function pickCommonArgs(args) {
|
|
985
1727
|
const out = {};
|
|
@@ -1004,4 +1746,4 @@ function buildConfigFlags(ctx) {
|
|
|
1004
1746
|
return flags;
|
|
1005
1747
|
}
|
|
1006
1748
|
//#endregion
|
|
1007
|
-
export {
|
|
1749
|
+
export { createServerProfile as $, OAUTH_SCOPE as A, JSON_CONTENT_TYPE as B, ProbedUser as C, OAUTH_CLIENT_NAME as D, profileAuthMethod as E, buildNetworkError as F, credentialSecrets as G, parseJsonOrPlain as H, isConnectionClosed as I, normalizeUrl as J, oauthCredentialFromTokens as K, HttpError as L, exchangeCode as M, registerClient as N, USER_AGENT as O, tryDiscoverMetadata as P, Skew as Q, chainRequestFailure as R, writeProfile as S, ProfileLastFailure as T, assertCredentialHeaderSafe as U, parseJson as V, credentialAuthHeader as W, summarizeServer as X, ServerSummary as Y, checkFeatures as Z, readProfileRecord as _, readCachedServerProfile as a, writeProbeFailure as b, readEnvCredentials as c, clearProfile as d, probeServer as et, consumeKeychainResidualWarning as f, readProfileCredential as g, listProfileRecords as h, parseEnumCsv as i, parseOptionalInteger as it, discoverLoginMetadata as j, revokeOAuthCredential as k, resolveProfileName as l, keyringFallbackWarning as m, parseCsv as n, RequirementFailure as nt, createCredentialRefresher as o, consumeKeyringDowngradeWarning as p, displayUrl as q, parseEnum as r, parseInteger as rt, explicitProfileName as s, defineMetabaseCommand as t, CapabilityError as tt, DEFAULT_PROFILE as u, resolveRecordCredential as v, ProfileAuthMethod as w, writeProbeResult as x, writeOAuthProfile as y, isRetryableStatus as z };
|