@hot-updater/console 1.0.0-rc.2 → 1.0.0-rc.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/ConsoleFeatureUnavailable-B9VYGA3N.js +1 -0
  3. package/.output/public/assets/HashValueDisplay-D880zntR.js +1 -0
  4. package/.output/public/assets/InsightsInfo-By_sPjTE.js +1 -0
  5. package/.output/public/assets/InsightsPageHeader-9_kx_LL7.js +1 -0
  6. package/.output/public/assets/NotFoundPage-Dys8C0cW.js +1 -0
  7. package/.output/public/assets/api-CtarlV0l.js +1 -0
  8. package/.output/public/assets/api-keys-DFF_QqtN.js +1 -0
  9. package/.output/public/assets/api-keys-evfsQ4sN.js +1 -0
  10. package/.output/public/assets/arrow-left-BP9D-ta_.js +1 -0
  11. package/.output/public/assets/arrow-up-right-CDP9Tevw.js +1 -0
  12. package/.output/public/assets/card-DzCPSRsh.js +1 -0
  13. package/.output/public/assets/console-features-api-yv7rm3DX.js +1 -0
  14. package/.output/public/assets/dialog-BDvCy_QO.js +1 -0
  15. package/.output/public/assets/dist-Clb6cQ9l.js +1 -0
  16. package/.output/public/assets/empty-C5x9mn_s.js +1 -0
  17. package/.output/public/assets/field-Bou7shoO.js +1 -0
  18. package/.output/public/assets/index-0eg3l8aC.js +10 -0
  19. package/.output/public/assets/input-group-DVtWFUUF.js +1 -0
  20. package/.output/public/assets/insights-2MbdiSNv.js +1 -0
  21. package/.output/public/assets/insights-C6tmCZHk.js +2 -0
  22. package/.output/public/assets/insights-CadS_aIA.js +42 -0
  23. package/.output/public/assets/insights-recovery-rpc-wvdyM4LD.js +1 -0
  24. package/.output/public/assets/insights-search-KIF7mRtB.js +1 -0
  25. package/.output/public/assets/insights-usage-rpc-qUkNtrWr.js +1 -0
  26. package/.output/public/assets/insights_.distribution-BhBs6PNA.js +1 -0
  27. package/.output/public/assets/insights_.distribution-CItZw3-8.js +1 -0
  28. package/.output/public/assets/insights_.distribution-CJUH_wAS.js +2 -0
  29. package/.output/public/assets/installations-CH2H7JQt.js +2 -0
  30. package/.output/public/assets/installations-DMeMuwsW.js +1 -0
  31. package/.output/public/assets/installations-DnvxI9Dt.js +1 -0
  32. package/.output/public/assets/loader-circle-Ca7C9YY1.js +1 -0
  33. package/.output/public/assets/mutation-x9L28_CD.js +1 -0
  34. package/.output/public/assets/preload-helper-BN-UkjOE.js +1 -0
  35. package/.output/public/assets/refresh-cw-CVGqvy8Z.js +1 -0
  36. package/.output/public/assets/routes-BhK8DuZA.js +1 -0
  37. package/.output/public/assets/routes-veKtDW39.js +2 -0
  38. package/.output/public/assets/sidebar-B45aMqRm.js +3 -0
  39. package/.output/public/assets/styles-Dn0pK1EX.css +2 -0
  40. package/.output/public/assets/table-CwLnN6KX.js +1 -0
  41. package/.output/public/assets/useCompositeListItem-CF7vsfcA.js +1 -0
  42. package/.output/public/assets/useMutation-CN1ygYEr.js +1 -0
  43. package/.output/public/assets/useOpenInteractionType-AnAMWYIY.js +1 -0
  44. package/.output/public/assets/valueToPercent-C-t8FT0y.js +1 -0
  45. package/.output/server/{__23tanstack-start-server-fn-resolver-B7ioJhtE.mjs → __23tanstack-start-server-fn-resolver-CUQLz7hs.mjs} +52 -40
  46. package/.output/server/_libs/@base-ui/react+[...].mjs +7044 -3622
  47. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
  48. package/.output/server/_libs/@tanstack/react-router+[...].mjs +49 -3
  49. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +4 -4
  50. package/.output/server/_libs/d3-array.mjs +231 -0
  51. package/.output/server/_libs/d3-color.mjs +330 -0
  52. package/.output/server/_libs/d3-format.mjs +252 -0
  53. package/.output/server/_libs/d3-interpolate.mjs +207 -0
  54. package/.output/server/_libs/d3-path.mjs +77 -0
  55. package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
  56. package/.output/server/_libs/d3-shape.mjs +818 -0
  57. package/.output/server/_libs/decimal.js-light.mjs +884 -0
  58. package/.output/server/_libs/es-toolkit.mjs +1159 -0
  59. package/.output/server/_libs/eventemitter3.mjs +236 -0
  60. package/.output/server/_libs/immer.mjs +532 -0
  61. package/.output/server/_libs/lucide-react.mjs +46 -32
  62. package/.output/server/_libs/next-themes.mjs +2 -2
  63. package/.output/server/_libs/react-redux+[...].mjs +205 -0
  64. package/.output/server/_libs/recharts+[...].mjs +14350 -0
  65. package/.output/server/_libs/sonner.mjs +2 -2
  66. package/.output/server/_libs/tanstack__react-query.mjs +8 -3
  67. package/.output/server/_libs/verkit.mjs +99 -1
  68. package/.output/server/_runtime.mjs +10 -1
  69. package/.output/server/_ssr/ConsoleFeatureUnavailable-DoxET2AT.mjs +58 -0
  70. package/.output/server/_ssr/{HashValueDisplay-DwpGLfrl.mjs → HashValueDisplay-BplboHEb.mjs} +3 -3
  71. package/.output/server/_ssr/InsightsInfo-o2YPd5Qc.mjs +37 -0
  72. package/.output/server/_ssr/InsightsPageHeader-NExCJL1I.mjs +154 -0
  73. package/.output/server/_ssr/NotFoundPage-Cd-A8ock.mjs +37 -0
  74. package/.output/server/_ssr/api-B1Ro0uys.mjs +202 -0
  75. package/.output/server/_ssr/api-C0G9MTMI.mjs +4205 -0
  76. package/.output/server/_ssr/{api-keys-CZeTPQys.mjs → api-keys-CT3HG7KT.mjs} +49 -9
  77. package/.output/server/_ssr/api-keys-Dvvy1sem.mjs +5 -0
  78. package/.output/server/_ssr/api-keys-rpc-b1qTTFfG.mjs +52 -0
  79. package/.output/server/_ssr/api-rpc-Cbns2MGx.mjs +338 -0
  80. package/.output/server/_ssr/{auth-rpc-DUaeOWKK.mjs → auth-rpc-DChS7m9y.mjs} +6 -6
  81. package/.output/server/_ssr/auth.server-Ccqz2iVv.mjs +37 -0
  82. package/.output/server/_ssr/bundleActivity-D17Pb_h5.mjs +19 -0
  83. package/.output/server/_ssr/card-DMbcvlPz.mjs +49 -0
  84. package/.output/server/_ssr/config-DMEJdhoW.mjs +13 -0
  85. package/.output/server/_ssr/config.server-DyypN6cQ.mjs +60 -0
  86. package/.output/server/_ssr/console-access-KpPv5lin.mjs +17 -0
  87. package/.output/server/_ssr/console-features-CsbyHYfK.mjs +55 -0
  88. package/.output/server/_ssr/console-features-api-B-KVuo4p.mjs +43 -0
  89. package/.output/server/_ssr/console-features-rpc-Djyo5740.mjs +19 -0
  90. package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-B_I3Ll4M.mjs} +28 -2
  91. package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-DuNm5mmo.mjs} +2 -2
  92. package/.output/server/_ssr/databaseRows-CXxf_VIB.mjs +132 -0
  93. package/.output/server/_ssr/{deleteBundle-BTGwBgzx.mjs → deleteBundle-Dcc21Lbd.mjs} +9 -19
  94. package/.output/server/_ssr/{dialog-DS-nw-pA.mjs → dialog-cOmLTUV2.mjs} +2 -2
  95. package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-B-r4u3Lg.mjs} +28 -5
  96. package/.output/server/_ssr/{downloadBundle-CNnYxYO2.mjs → downloadBundle-CexOFoZ4.mjs} +6 -7
  97. package/.output/server/_ssr/empty-BHoY_5jt.mjs +49 -0
  98. package/.output/server/_ssr/{table-CnZmon_Z.mjs → field-mjhcNEXD.mjs} +63 -51
  99. package/.output/server/_ssr/getBundleChildren-DbDXe9wB.mjs +22 -0
  100. package/.output/server/_ssr/{input-group-BpsnmvZw.mjs → input-group-96H30HWC.mjs} +2 -2
  101. package/.output/server/_ssr/insights-D25VDVaz.mjs +5 -0
  102. package/.output/server/_ssr/insights-D359GJFL.mjs +14 -0
  103. package/.output/server/_ssr/insights-DgFv1NsO.mjs +1238 -0
  104. package/.output/server/_ssr/insights-api-ChAKrbhG.mjs +105 -0
  105. package/.output/server/_ssr/insights-failures-WQ-ttjOx.mjs +44 -0
  106. package/.output/server/_ssr/insights-failures-rpc-BFsqlltN.mjs +21 -0
  107. package/.output/server/_ssr/insights-recovery-DkL6bxMW.mjs +28 -0
  108. package/.output/server/_ssr/insights-recovery-rpc-2P5m8F62.mjs +46 -0
  109. package/.output/server/_ssr/insights-recovery-rpc-Tj8vNC19.mjs +20 -0
  110. package/.output/server/_ssr/{insights-rpc-38DujK0G.mjs → insights-rpc-DrkVmV9o.mjs} +22 -14
  111. package/.output/server/_ssr/insights-search-BsalnNw-.mjs +25 -0
  112. package/.output/server/_ssr/insights-usage-1nVTQrLy.mjs +29 -0
  113. package/.output/server/_ssr/insights-usage-rpc-Ct_4xHhZ.mjs +29 -0
  114. package/.output/server/_ssr/insights-usage-rpc-DaYIRtFK.mjs +41 -0
  115. package/.output/server/_ssr/insightsRecovery-CpGucWN1.mjs +45 -0
  116. package/.output/server/_ssr/insightsUsage-JYpKZAF7.mjs +56 -0
  117. package/.output/server/_ssr/insights_.distribution-Bcu6uYs_.mjs +5 -0
  118. package/.output/server/_ssr/insights_.distribution-D3Csuyjw.mjs +14 -0
  119. package/.output/server/_ssr/insights_.distribution-jQZEF07J.mjs +335 -0
  120. package/.output/server/_ssr/installations-B8Vh54lH.mjs +69 -0
  121. package/.output/server/_ssr/installations-BI5kelHv.mjs +5 -0
  122. package/.output/server/_ssr/{installations-DiVKEF5T.mjs → installations-Cxikllmx.mjs} +517 -54
  123. package/.output/server/_ssr/listReleases-DqU9t1l3.mjs +79 -0
  124. package/.output/server/_ssr/releaseManagement-B3e7I8J9.mjs +11 -0
  125. package/.output/server/_ssr/{router-B9wG_9f8.mjs → router-CRkm-wHD.mjs} +117 -124
  126. package/.output/server/_ssr/routes-B3IJo_jL.mjs +292 -0
  127. package/.output/server/_ssr/{routes-D7Yh_jfj.mjs → routes-CNAi0Mmv.mjs} +176 -455
  128. package/.output/server/_ssr/runtime.server-NucbYjc7.mjs +1372 -0
  129. package/.output/server/_ssr/select-BkPAJFL7.mjs +107 -0
  130. package/.output/server/_ssr/{server-D_C2rMzf.mjs → server-DKJYsaCP.mjs} +6 -32
  131. package/.output/server/_ssr/{createSsrRpc-DjWNJ0uc.mjs → sidebar-Cw1VuC7d.mjs} +117 -63
  132. package/.output/server/_ssr/ssr.mjs +1 -1
  133. package/.output/server/_ssr/table-q0P9wz7x.mjs +53 -0
  134. package/.output/server/_ssr/updateFailures-DMU9f0pK.mjs +24 -0
  135. package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-BzSO-fr-.mjs} +2 -2
  136. package/.output/server/_tanstack-start-manifest_v-Bb0gKDbb.mjs +124 -0
  137. package/.output/server/index.mjs +273 -168
  138. package/README.md +71 -65
  139. package/lib/index.d.mts +13 -11
  140. package/lib/vite-internal.mjs +6 -3
  141. package/package.json +8 -8
  142. package/src/components/AppSidebar.tsx +91 -71
  143. package/src/components/ConsoleAccessPage.tsx +5 -1
  144. package/src/components/ConsoleFeatureUnavailable.tsx +82 -0
  145. package/src/components/NotFoundPage.tsx +7 -7
  146. package/src/components/features/FeatureSlots.tsx +99 -0
  147. package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
  148. package/src/components/features/bundles/BundleMetadata.tsx +7 -4
  149. package/src/components/features/channels/ChannelManagementDialog.tsx +2 -5
  150. package/src/components/features/insights/AppUsage.tsx +370 -0
  151. package/src/components/features/insights/EstimatedCount.tsx +18 -0
  152. package/src/components/features/insights/EventDetails.tsx +117 -37
  153. package/src/components/features/insights/EventHistoryCard.tsx +112 -26
  154. package/src/components/features/insights/EventHistoryList.tsx +2 -2
  155. package/src/components/features/insights/InsightsControls.tsx +265 -102
  156. package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
  157. package/src/components/features/insights/InsightsInfo.tsx +48 -0
  158. package/src/components/features/insights/InsightsOverview.tsx +176 -173
  159. package/src/components/features/insights/InsightsPageHeader.tsx +35 -20
  160. package/src/components/features/insights/InsightsPeriodSelector.tsx +89 -0
  161. package/src/components/features/insights/InstallationHistoryCard.tsx +52 -10
  162. package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
  163. package/src/components/features/insights/ReleaseActivity.tsx +188 -0
  164. package/src/components/features/insights/ReportingDevicesSummary.tsx +65 -0
  165. package/src/components/features/insights/UpdateFailures.tsx +319 -0
  166. package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
  167. package/src/components/features/releases/ReleaseEditorSheet.tsx +24 -1
  168. package/src/components/ui/card.tsx +1 -1
  169. package/src/components/ui/empty.tsx +101 -0
  170. package/src/components/ui/scroll-area.tsx +52 -0
  171. package/src/components/ui/sidebar.tsx +22 -29
  172. package/src/components/ui/tabs.tsx +81 -0
  173. package/src/index.ts +12 -17
  174. package/src/lib/api-keys-api.ts +0 -28
  175. package/src/lib/api-keys-rpc.ts +27 -38
  176. package/src/lib/api-rpc.ts +197 -221
  177. package/src/lib/api.ts +11 -13
  178. package/src/lib/bundle-activity.ts +31 -0
  179. package/src/lib/console-access.ts +20 -0
  180. package/src/lib/console-features-api.ts +68 -0
  181. package/src/lib/console-features-rpc.ts +13 -0
  182. package/src/lib/console-features.ts +91 -0
  183. package/src/lib/insights-api.ts +37 -1
  184. package/src/lib/insights-failures-rpc.ts +21 -0
  185. package/src/lib/insights-failures.ts +129 -0
  186. package/src/lib/insights-recovery-rpc.ts +51 -0
  187. package/src/lib/insights-recovery.ts +68 -0
  188. package/src/lib/insights-retention.ts +21 -0
  189. package/src/lib/insights-rpc.ts +30 -12
  190. package/src/lib/insights-search.ts +58 -0
  191. package/src/lib/insights-usage-rpc.ts +29 -0
  192. package/src/lib/insights-usage.ts +74 -0
  193. package/src/lib/insights-view.ts +38 -2
  194. package/src/lib/scroll-restoration.ts +21 -0
  195. package/src/lib/server/adminInsights.ts +124 -0
  196. package/src/lib/server/auth.server.ts +13 -5
  197. package/src/lib/server/bundleActivity.ts +37 -0
  198. package/src/lib/server/config.server.ts +16 -32
  199. package/src/lib/server/console-feature-apis.ts +59 -0
  200. package/src/lib/server/console-runtime.server.ts +6 -40
  201. package/src/lib/server/deleteBundle.ts +16 -25
  202. package/src/lib/server/downloadBundle.ts +7 -10
  203. package/src/lib/server/getBundleChildren.ts +31 -122
  204. package/src/lib/server/insightsRecovery.ts +55 -0
  205. package/src/lib/server/insightsUsage.ts +63 -0
  206. package/src/lib/server/listReleases.ts +109 -44
  207. package/src/lib/server/releaseReachability.ts +4 -3
  208. package/src/lib/server/runtime.server.ts +132 -31
  209. package/src/lib/server/updateFailures.ts +31 -0
  210. package/src/routeTree.gen.ts +21 -21
  211. package/src/router.tsx +3 -2
  212. package/src/routes/-installations-search.ts +32 -8
  213. package/src/routes/-releases-search.ts +76 -29
  214. package/src/routes/__root.tsx +10 -4
  215. package/src/routes/api/bundles/$bundleId/download.ts +2 -5
  216. package/src/routes/api-keys.tsx +5 -2
  217. package/src/routes/index.tsx +167 -104
  218. package/src/routes/insights.tsx +100 -59
  219. package/src/routes/insights_.distribution.tsx +398 -0
  220. package/src/routes/installations.tsx +98 -41
  221. package/src/vite-internal.ts +6 -3
  222. package/.output/public/assets/HashValueDisplay-CB0Eskit.js +0 -1
  223. package/.output/public/assets/InsightsPageHeader-Car5LNiZ.js +0 -1
  224. package/.output/public/assets/api-keys-BzKocqRy.js +0 -1
  225. package/.output/public/assets/api-keys-api-BHcODB7q.js +0 -1
  226. package/.output/public/assets/card-Dhr_eFYj.js +0 -1
  227. package/.output/public/assets/chart-no-axes-combined-Dy4WJv2U.js +0 -1
  228. package/.output/public/assets/composite-Bp98A0Ld.js +0 -1
  229. package/.output/public/assets/createServerFn-CA8fs7QD.js +0 -3
  230. package/.output/public/assets/dialog-CUTrcPlB.js +0 -1
  231. package/.output/public/assets/dist-C_NydL0e.js +0 -1
  232. package/.output/public/assets/index-DdAz-CXv.js +0 -10
  233. package/.output/public/assets/input-group-cSOG4wh_.js +0 -1
  234. package/.output/public/assets/insights-C0PBSZ-n.js +0 -1
  235. package/.output/public/assets/installations-CE33wgbY.js +0 -2
  236. package/.output/public/assets/installations-CStZja8x.js +0 -1
  237. package/.output/public/assets/lazyRouteComponent-Ccp3bwBY.js +0 -1
  238. package/.output/public/assets/link-CI8idAUi.js +0 -1
  239. package/.output/public/assets/loader-circle-BoxLrsOu.js +0 -1
  240. package/.output/public/assets/preload-helper-CQ36nxok.js +0 -1
  241. package/.output/public/assets/refresh-cw-Bqc72UZG.js +0 -1
  242. package/.output/public/assets/routes-DGVXdzpw.js +0 -2
  243. package/.output/public/assets/routes-DTlwOr8I.js +0 -1
  244. package/.output/public/assets/shield-check-uK3MHWJZ.js +0 -1
  245. package/.output/public/assets/signing-CMf4aqDO.js +0 -1
  246. package/.output/public/assets/styles-BCvbtKsm.css +0 -2
  247. package/.output/public/assets/table-2vcV89QB.js +0 -1
  248. package/.output/public/assets/useCompositeListItem-CHLITE_4.js +0 -1
  249. package/.output/public/assets/x-DCd0j8c6.js +0 -1
  250. package/.output/server/_ssr/InsightsPageHeader-Df4Brj8u.mjs +0 -484
  251. package/.output/server/_ssr/api-keys-api-BUPQVSpk.mjs +0 -60
  252. package/.output/server/_ssr/api-keys-rpc-Cs7tS5_j.mjs +0 -136
  253. package/.output/server/_ssr/api-rpc-tAupfGQm.mjs +0 -1399
  254. package/.output/server/_ssr/auth.server-DW71WzsD.mjs +0 -47
  255. package/.output/server/_ssr/bundle-signing-rpc-BhnBw0-d.mjs +0 -16
  256. package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +0 -12
  257. package/.output/server/_ssr/card-BRiPmi0C.mjs +0 -108
  258. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
  259. package/.output/server/_ssr/config.server-afDuAwaG.mjs +0 -368
  260. package/.output/server/_ssr/databaseJsonValue-d6I3nAS4.mjs +0 -45
  261. package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
  262. package/.output/server/_ssr/getBundleChildren-C0vh4ZvE.mjs +0 -50
  263. package/.output/server/_ssr/insights-BdS364Xu.mjs +0 -460
  264. package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +0 -303
  265. package/.output/server/_ssr/installations-CtA59MQG.mjs +0 -29
  266. package/.output/server/_ssr/routes-CKrrS2q2.mjs +0 -37
  267. package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +0 -438
  268. package/.output/server/_ssr/signing-Dz1WkZOf.mjs +0 -133
  269. package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +0 -97
  270. package/src/components/features/insights/InsightsBundleReports.tsx +0 -51
  271. package/src/components/features/signing/BundleSigningPage.tsx +0 -139
  272. package/src/lib/bundle-signing-api.ts +0 -14
  273. package/src/lib/bundle-signing-rpc.ts +0 -16
  274. package/src/lib/server/bundle-signing.server.ts +0 -23
  275. package/src/routes/signing.tsx +0 -37
@@ -1,24 +1,21 @@
1
1
  import type {
2
- DatabaseClient,
2
+ HotUpdaterCoreApi,
3
3
  StoragePluginWith,
4
4
  } from "@hot-updater/plugin-core";
5
5
 
6
6
  interface DownloadBundleDependencies {
7
- readonly databaseClient: DatabaseClient;
7
+ readonly core: Pick<HotUpdaterCoreApi, "getBundle">;
8
8
  readonly storagePlugin?: StoragePluginWith<"get">;
9
9
  }
10
10
 
11
11
  export const downloadBundle = async (
12
12
  bundleId: string,
13
- { databaseClient, storagePlugin }: DownloadBundleDependencies,
13
+ { core, storagePlugin }: DownloadBundleDependencies,
14
14
  ): Promise<Response> => {
15
- const bundle = await databaseClient.getBundleById(bundleId);
16
- if (!bundle) return new Response("Bundle not found", { status: 404 });
15
+ const detail = await core.getBundle(bundleId);
16
+ if (!detail) return new Response("Bundle not found", { status: 404 });
17
17
 
18
- const storageUri = bundle.storageUri;
19
- if (!storageUri) {
20
- return new Response("Bundle has no storage URI", { status: 404 });
21
- }
18
+ const storageUri = detail.bundle.manifest_storage_uri;
22
19
 
23
20
  const protocol = new URL(storageUri).protocol.replace(":", "");
24
21
  if (storagePlugin?.protocol === protocol) {
@@ -28,7 +25,7 @@ export const downloadBundle = async (
28
25
 
29
26
  const headers = new Headers(response.headers);
30
27
  headers.set("cache-control", "private, no-store");
31
- headers.set("content-disposition", "attachment");
28
+ headers.set("content-disposition", 'attachment; filename="manifest.json"');
32
29
  return new Response(response.body, {
33
30
  headers,
34
31
  status: response.status,
@@ -1,130 +1,39 @@
1
- import { getBundlePatches } from "@hot-updater/core";
2
- import type { Bundle, DatabaseClient } from "@hot-updater/plugin-core";
3
-
4
- const CHILDREN_QUERY_LIMIT = 100;
5
-
6
- interface GetBundleChildrenInput {
7
- baseBundleId: string;
8
- }
9
-
10
- interface GetBundleChildrenDeps {
11
- databaseClient: DatabaseClient;
12
- }
13
-
14
- type CursorPaginationInfo = {
15
- hasNextPage: boolean;
16
- nextCursor?: string | null;
17
- };
18
-
19
- async function collectBundleChildrenByBaseIds(
20
- baseBundleIds: string[],
21
- deps: GetBundleChildrenDeps,
22
- ): Promise<Record<string, Bundle[]>> {
23
- const uniqueBaseBundleIds = [...new Set(baseBundleIds.filter(Boolean))];
24
- const bundlesByBaseId = Object.fromEntries(
25
- uniqueBaseBundleIds.map((bundleId) => [bundleId, [] as Bundle[]]),
26
- );
27
-
28
- const baseBundles = (
29
- await Promise.all(
30
- uniqueBaseBundleIds.map((bundleId) =>
31
- deps.databaseClient.getBundleById(bundleId),
32
- ),
33
- )
34
- ).filter((bundle): bundle is Bundle => Boolean(bundle));
35
-
36
- const groupMap = new Map<Bundle["platform"], Set<string>>();
37
-
38
- for (const baseBundle of baseBundles) {
39
- const existingGroup = groupMap.get(baseBundle.platform);
40
-
41
- if (existingGroup) {
42
- existingGroup.add(baseBundle.id);
43
- continue;
44
- }
45
-
46
- groupMap.set(baseBundle.platform, new Set([baseBundle.id]));
47
- }
48
-
49
- for (const [platform, bundleIds] of groupMap) {
50
- const seenBundleIds = new Set<string>();
51
- const seenCursors = new Set<string>();
52
- let after: string | undefined;
53
-
54
- while (true) {
55
- const page = await deps.databaseClient.getBundles({
56
- where: {
57
- platform,
58
- },
59
- limit: CHILDREN_QUERY_LIMIT,
60
- cursor: after ? { after } : undefined,
61
- });
62
-
63
- for (const bundle of page.data) {
64
- const parentBundleIds = getBundlePatches(bundle).map(
65
- (patch) => patch.baseBundleId,
66
- );
67
- const matchedParentBundleIds = parentBundleIds.filter((bundleId) =>
68
- bundleIds.has(bundleId),
69
- );
70
-
71
- if (
72
- matchedParentBundleIds.length === 0 ||
73
- seenBundleIds.has(bundle.id)
74
- ) {
75
- continue;
76
- }
77
-
78
- seenBundleIds.add(bundle.id);
79
- for (const parentBundleId of matchedParentBundleIds) {
80
- bundlesByBaseId[parentBundleId]?.push(bundle);
81
- }
82
- }
83
-
84
- const pagination = page.pagination as CursorPaginationInfo;
85
- const nextCursor = pagination.nextCursor ?? undefined;
86
-
87
- if (
88
- !pagination.hasNextPage ||
89
- !nextCursor ||
90
- seenCursors.has(nextCursor)
91
- ) {
92
- break;
93
- }
94
-
95
- seenCursors.add(nextCursor);
96
- after = nextCursor;
97
- }
98
- }
99
-
100
- return bundlesByBaseId;
101
- }
102
-
1
+ import {
2
+ type Bundle,
3
+ type HotUpdaterCoreApi,
4
+ rowToBundle,
5
+ } from "@hot-updater/plugin-core";
6
+
7
+ /** The most children the lineage panel lists; the count comes from the base bundle's counter. */
8
+ export const BUNDLE_CHILDREN_LIMIT = 100;
9
+
10
+ /**
11
+ * The bundles with a patch from this base, newest first: one read of the
12
+ * patches index, then each child bundle.
13
+ */
103
14
  export async function getBundleChildren(
104
- { baseBundleId }: GetBundleChildrenInput,
105
- deps: GetBundleChildrenDeps,
15
+ core: Pick<HotUpdaterCoreApi, "getBundle" | "listPatchesFromBase">,
16
+ baseBundleId: string,
106
17
  ): Promise<Bundle[]> {
107
- const childrenByBaseId = await collectBundleChildrenByBaseIds(
108
- [baseBundleId],
109
- deps,
18
+ const patches = await core.listPatchesFromBase(baseBundleId, {
19
+ order: "desc",
20
+ limit: BUNDLE_CHILDREN_LIMIT,
21
+ });
22
+ const details = await Promise.all(
23
+ [...new Set(patches.map(({ bundle_id }) => bundle_id))].map((id) =>
24
+ core.getBundle(id),
25
+ ),
26
+ );
27
+ return details.flatMap((detail) =>
28
+ detail === null ? [] : [rowToBundle(detail.bundle, detail.patches)],
110
29
  );
111
-
112
- return childrenByBaseId[baseBundleId] ?? [];
113
30
  }
114
31
 
32
+ /** How many bundles patch from each base: each base bundle's reference counter, in one batch read. */
115
33
  export async function getBundleChildCounts(
116
- baseBundleIds: string[],
117
- deps: GetBundleChildrenDeps,
34
+ core: Pick<HotUpdaterCoreApi, "countBundleChildren">,
35
+ baseBundleIds: readonly string[],
118
36
  ): Promise<Record<string, number>> {
119
- const childrenByBaseId = await collectBundleChildrenByBaseIds(
120
- baseBundleIds,
121
- deps,
122
- );
123
-
124
- return Object.fromEntries(
125
- Object.entries(childrenByBaseId).map(([bundleId, bundles]) => [
126
- bundleId,
127
- bundles.length,
128
- ]),
129
- );
37
+ const ids = [...new Set(baseBundleIds.filter(Boolean))];
38
+ return ids.length === 0 ? {} : core.countBundleChildren(ids);
130
39
  }
@@ -0,0 +1,55 @@
1
+ import type { InsightsModel } from "@hot-updater/server/plugins/insights";
2
+
3
+ import {
4
+ insightsPeriodEnd,
5
+ readRecoveryInput,
6
+ recoveryWindows,
7
+ type RecoveryInput,
8
+ type RecoveryReport,
9
+ } from "../insights-recovery";
10
+
11
+ export async function getRecoveryReport(
12
+ model: InsightsModel,
13
+ input: RecoveryInput,
14
+ now = Date.now(),
15
+ ): Promise<RecoveryReport> {
16
+ readRecoveryInput(input);
17
+ const end = insightsPeriodEnd(now);
18
+ const start = Math.max(0, end - recoveryWindows[input.window].durationMs);
19
+ const result = await model.getReleaseActivity(
20
+ input.releaseId === undefined
21
+ ? {
22
+ scope: { platform: input.platform, channel: input.channel },
23
+ timeRange: { start, end },
24
+ }
25
+ : {
26
+ releases: [
27
+ {
28
+ releaseId: input.releaseId,
29
+ platform: input.platform,
30
+ channel: input.channel,
31
+ },
32
+ ],
33
+ timeRange: { start, end },
34
+ },
35
+ );
36
+ const metrics = result.data[0]?.metrics;
37
+ // The launches counter counts each installation once per UTC day it
38
+ // launched, so it reads as active days, and per day as daily active
39
+ // installations.
40
+ return {
41
+ downloads: metrics?.downloads ?? 0,
42
+ activeInstallations: metrics?.uniqueUsers ?? 0,
43
+ activeDays: metrics?.launches ?? 0,
44
+ failedLaunches: metrics?.failedLaunches ?? 0,
45
+ points: (metrics?.series ?? []).map((point) => ({
46
+ startMs: point.startMs,
47
+ dailyActiveInstallations: point.launches,
48
+ failedLaunches: point.failedLaunches,
49
+ })),
50
+ startMs: start,
51
+ endMs: end,
52
+ measuredAtMs: result.measuredAtMs,
53
+ coverage: result.coverage,
54
+ };
55
+ }
@@ -0,0 +1,63 @@
1
+ import type { InsightsModel } from "@hot-updater/server/plugins/insights";
2
+
3
+ import { insightsPeriodEnd, recoveryWindows } from "../insights-recovery";
4
+ import {
5
+ readAppUsageInput,
6
+ type AppUsageInput,
7
+ type AppUsageReport,
8
+ type UsageWindow,
9
+ } from "../insights-usage";
10
+
11
+ const DAY_MS = 86_400_000;
12
+ const WEEK_MS = 7 * DAY_MS;
13
+
14
+ /**
15
+ * What a period reads, ending with the current hour. The 12-month period is
16
+ * 52 weeks of whole UTC days, the last one today, so it reads the daily
17
+ * counts: the hourly ones are kept 90 days.
18
+ */
19
+ const usageRange = (window: UsageWindow, now: number) => {
20
+ const end = insightsPeriodEnd(now);
21
+ if (window === "12m") {
22
+ const lastDay = Math.ceil(end / DAY_MS) * DAY_MS;
23
+ return {
24
+ start: Math.max(0, lastDay - 52 * WEEK_MS),
25
+ end,
26
+ intervalMs: WEEK_MS,
27
+ };
28
+ }
29
+ const { durationMs, intervalMs } = recoveryWindows[window];
30
+ return { start: Math.max(0, end - durationMs), end, intervalMs };
31
+ };
32
+
33
+ export async function getAppUsageReport(
34
+ model: InsightsModel,
35
+ input: AppUsageInput,
36
+ now = Date.now(),
37
+ ): Promise<AppUsageReport> {
38
+ readAppUsageInput(input);
39
+ const { start, end, intervalMs } = usageRange(input.window, now);
40
+ const report = await model.getAppUsage({
41
+ channel: input.channel,
42
+ platform: input.platform,
43
+ ...(input.appVersion === undefined ? {} : { appVersion: input.appVersion }),
44
+ timeRange: { start, end },
45
+ intervalMs,
46
+ });
47
+ return {
48
+ bundleDistribution: report.bundleDistribution,
49
+ activeInstallations: report.activeInstallations,
50
+ sinceMs:
51
+ report.coverage.sinceMs === null
52
+ ? start
53
+ : Math.max(start, report.coverage.sinceMs),
54
+ distributionSinceMs: start - (start % DAY_MS),
55
+ beforeReceivedAtMs: end,
56
+ intervalMs,
57
+ truncated: report.coverage.kind === "partial",
58
+ appVersions: report.appVersions,
59
+ versions: report.versions,
60
+ platforms: report.platforms,
61
+ points: report.points,
62
+ };
63
+ }
@@ -1,61 +1,126 @@
1
- import type { ReleaseModel, ReleaseRow } from "@hot-updater/plugin-core";
1
+ import type {
2
+ HotUpdaterCoreApi,
3
+ ReleaseFilter,
4
+ ReleaseRow,
5
+ } from "@hot-updater/plugin-core";
2
6
 
3
7
  export interface ReleaseListInput {
4
- afterReleaseId?: string;
5
- beforeReleaseId?: string;
6
- bundleId?: string;
7
- channelId?: string;
8
- enabled?: boolean;
9
- limit: number;
10
- page?: number;
11
- platform?: "ios" | "android";
12
- targetAppVersion?: string;
8
+ /** One of the filter sets the release indexes serve. */
9
+ readonly filter: ReleaseFilter;
10
+ /** Releases older than this id: the next page. */
11
+ readonly beforeReleaseId?: string;
12
+ /** Releases newer than this id: the previous page. */
13
+ readonly afterReleaseId?: string;
14
+ readonly limit: number;
13
15
  }
14
16
 
15
17
  export interface ReleaseListResult {
16
- data: readonly ReleaseRow[];
17
- pagination: {
18
- currentPage: number;
19
- hasNextPage: boolean;
20
- hasPreviousPage: boolean;
21
- };
18
+ /** Newest first. */
19
+ readonly data: readonly ReleaseRow[];
20
+ /** `beforeReleaseId` of the next page, when there may be older releases. */
21
+ readonly next?: string;
22
+ /** `afterReleaseId` of the previous page, when there may be newer releases. */
23
+ readonly previous?: string;
22
24
  }
23
25
 
26
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
27
+ typeof value === "object" && value !== null;
28
+
29
+ const text = (value: unknown, name: string): string => {
30
+ if (typeof value !== "string" || value.length === 0) {
31
+ throw new Error(`Release filter ${name} is required.`);
32
+ }
33
+ return value;
34
+ };
35
+
36
+ const enabledOf = (value: unknown) => {
37
+ if (value === undefined) return {};
38
+ if (typeof value !== "boolean") {
39
+ throw new Error("Release filter enabled must be true or false.");
40
+ }
41
+ return { enabled: value };
42
+ };
43
+
44
+ /**
45
+ * A release list's filter: none, a channel with its platform (and enabled),
46
+ * a bundle, or a scope (and enabled). Those are the sets the release indexes
47
+ * serve, so any other combination is refused.
48
+ */
49
+ export const readReleaseFilter = (value: unknown): ReleaseFilter => {
50
+ if (value === undefined) return { kind: "all" };
51
+ if (!isRecord(value)) throw new Error("Invalid release filter.");
52
+ switch (value.kind) {
53
+ case "all":
54
+ return { kind: "all" };
55
+ case "channelPlatform": {
56
+ if (value.platform !== "ios" && value.platform !== "android") {
57
+ throw new Error("Release filter platform must be ios or android.");
58
+ }
59
+ return {
60
+ kind: "channelPlatform",
61
+ channelId: text(value.channelId, "channelId"),
62
+ platform: value.platform,
63
+ ...enabledOf(value.enabled),
64
+ };
65
+ }
66
+ case "bundle":
67
+ return { kind: "bundle", bundleId: text(value.bundleId, "bundleId") };
68
+ case "scope":
69
+ return {
70
+ kind: "scope",
71
+ scopeKey: text(value.scopeKey, "scopeKey"),
72
+ ...enabledOf(value.enabled),
73
+ };
74
+ default:
75
+ throw new Error("Invalid release filter.");
76
+ }
77
+ };
78
+
79
+ /**
80
+ * One page of releases, newest first, through the index the filter names:
81
+ * each page reads only its own rows. `next` is set for a full page, so the
82
+ * last page may lead to an empty one.
83
+ */
24
84
  export async function listReleases(
25
- releases: ReleaseModel,
85
+ core: Pick<HotUpdaterCoreApi, "listReleases">,
26
86
  input: ReleaseListInput,
27
87
  ): Promise<ReleaseListResult> {
28
- const page = input.page ?? 1;
29
- const rows = await releases.findMany({
30
- ...(input.afterReleaseId === undefined
31
- ? {}
32
- : { afterReleaseId: input.afterReleaseId }),
88
+ const { filter, limit } = input;
89
+ if (input.afterReleaseId !== undefined) {
90
+ // Newer releases, read oldest first from the cursor, then turned around.
91
+ const data = (
92
+ await core.listReleases({
93
+ filter,
94
+ order: "asc",
95
+ after: input.afterReleaseId,
96
+ limit,
97
+ })
98
+ ).reverse();
99
+ const first = data[0]?.id;
100
+ const last = data.at(-1)?.id;
101
+ return {
102
+ data,
103
+ ...(last === undefined ? {} : { next: last }),
104
+ ...(first !== undefined && data.length === limit
105
+ ? { previous: first }
106
+ : {}),
107
+ };
108
+ }
109
+ const data = await core.listReleases({
110
+ filter,
111
+ order: "desc",
112
+ limit,
33
113
  ...(input.beforeReleaseId === undefined
34
114
  ? {}
35
- : { beforeReleaseId: input.beforeReleaseId }),
36
- ...(input.bundleId === undefined ? {} : { bundleId: input.bundleId }),
37
- ...(input.channelId === undefined ? {} : { channelId: input.channelId }),
38
- ...(input.enabled === undefined ? {} : { enabled: input.enabled }),
39
- ...(input.platform === undefined ? {} : { platform: input.platform }),
40
- ...(input.targetAppVersion === undefined
41
- ? {}
42
- : { targetAppVersion: input.targetAppVersion }),
43
- limit: input.limit + 1,
115
+ : { after: input.beforeReleaseId }),
44
116
  });
45
- const isPreviousRequest = input.afterReleaseId !== undefined;
46
- const hasExtraRow = rows.length > input.limit;
47
- const data = isPreviousRequest
48
- ? hasExtraRow
49
- ? rows.slice(1)
50
- : rows
51
- : rows.slice(0, input.limit);
52
-
117
+ const first = data[0]?.id;
118
+ const last = data.at(-1)?.id;
53
119
  return {
54
120
  data,
55
- pagination: {
56
- currentPage: page,
57
- hasNextPage: isPreviousRequest || hasExtraRow,
58
- hasPreviousPage: page > 1,
59
- },
121
+ ...(last !== undefined && data.length === limit ? { next: last } : {}),
122
+ ...(first !== undefined && input.beforeReleaseId !== undefined
123
+ ? { previous: first }
124
+ : {}),
60
125
  };
61
126
  }
@@ -4,7 +4,7 @@ import {
4
4
  } from "@hot-updater/core";
5
5
  import type {
6
6
  CompiledReleaseCatalog,
7
- ReleaseCatalogModel,
7
+ HotUpdaterCoreApi,
8
8
  ReleaseRow,
9
9
  } from "@hot-updater/plugin-core";
10
10
 
@@ -63,15 +63,16 @@ export function collectCurrentlyReachableReleaseIds(
63
63
  return reachableReleaseIds;
64
64
  }
65
65
 
66
+ /** Marks each release its scope's compiled catalog no longer serves: one catalog read per scope. */
66
67
  export async function addReleaseReachability(
67
- releaseCatalogs: Pick<ReleaseCatalogModel, "findByScopeKey">,
68
+ core: Pick<HotUpdaterCoreApi, "getReleaseCatalogRow">,
68
69
  releases: readonly ReleaseRow[],
69
70
  ): Promise<readonly ReleaseReachabilityRow[]> {
70
71
  const scopeKeys = [...new Set(releases.map(({ scope_key }) => scope_key))];
71
72
  const reachableByScope = new Map(
72
73
  await Promise.all(
73
74
  scopeKeys.map(async (scopeKey) => {
74
- const row = await releaseCatalogs.findByScopeKey(scopeKey);
75
+ const row = await core.getReleaseCatalogRow(scopeKey);
75
76
  const reachableReleaseIds =
76
77
  row === null
77
78
  ? new Set<string>()
@@ -1,36 +1,137 @@
1
- import type {
2
- ApiKeyModel,
3
- BundleRepository,
4
- DatabasePlugin,
1
+ import {
2
+ type ConfiguredDatabase,
3
+ isRemoteDatabase,
5
4
  } from "@hot-updater/plugin-core";
5
+ import { createDatabasePluginApis } from "@hot-updater/server/db";
6
+
6
7
  import {
7
- createInsightsProvider,
8
- type InsightsProvider,
9
- } from "@hot-updater/server";
10
-
11
- export function createRuntimeHotUpdater(config: {
12
- readonly database: DatabasePlugin;
13
- }): InsightsProvider {
14
- return createInsightsProvider(config.database.models.insights);
8
+ type ConsoleFeature,
9
+ ConsoleFeatureUnavailableError,
10
+ type ConsoleFeatures,
11
+ consoleFeatures,
12
+ resolveConsoleFeatures,
13
+ } from "../console-features";
14
+ import {
15
+ type ConsoleFeatureApis,
16
+ consoleFeatureApis,
17
+ type FetchAdmin,
18
+ } from "./console-feature-apis";
19
+
20
+ export type { ConsoleFeatureApis } from "./console-feature-apis";
21
+
22
+ export interface ConsoleRuntime {
23
+ /** Whether the console reaches a self-hosted server through its admin API. */
24
+ readonly remote: boolean;
25
+ /** The features on here, resolved once from the plugins the server runs. */
26
+ features(): Promise<ConsoleFeatures>;
27
+ /** What serves the features the console can serve here; {@link requireFeature} hands it out. */
28
+ readonly apis: Partial<ConsoleFeatureApis>;
15
29
  }
16
30
 
17
- export function createApiKeyStore(config: {
18
- readonly database: BundleRepository;
19
- }): ApiKeyModel | null {
20
- const models: unknown = Reflect.get(config.database, "models");
21
- const apiKeys: unknown =
22
- typeof models === "object" && models !== null
23
- ? Reflect.get(models, "apiKeys")
24
- : undefined;
25
- if (
26
- typeof apiKeys !== "object" ||
27
- apiKeys === null ||
28
- typeof Reflect.get(apiKeys, "create") !== "function" ||
29
- typeof Reflect.get(apiKeys, "findByHash") !== "function" ||
30
- typeof Reflect.get(apiKeys, "list") !== "function" ||
31
- typeof Reflect.get(apiKeys, "revoke") !== "function"
32
- ) {
33
- return null;
31
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
32
+ typeof value === "object" && value !== null;
33
+
34
+ /** The ids of the plugins a self-hosted server lists on its admin `/version`. */
35
+ const readServerPlugins = async (
36
+ fetchAdmin: FetchAdmin,
37
+ ): Promise<readonly string[]> => {
38
+ const response = await fetchAdmin("/version");
39
+ if (!response.ok) {
40
+ throw new Error(`The server answered /version with ${response.status}.`);
34
41
  }
35
- return apiKeys as ApiKeyModel;
36
- }
42
+ const body: unknown = await response.json();
43
+ const plugins = isRecord(body) ? body.plugins : undefined;
44
+ // A server from before `/version` listed its plugins reports none, so the
45
+ // console serves none of their features until the server is upgraded.
46
+ return Array.isArray(plugins)
47
+ ? plugins.filter((id): id is string => typeof id === "string")
48
+ : [];
49
+ };
50
+
51
+ /** Runs `load` once and keeps its result; a failure is not kept, so the next call retries. */
52
+ const once = <T>(load: () => Promise<T>): (() => Promise<T>) => {
53
+ let pending: Promise<T> | undefined;
54
+ return () =>
55
+ (pending ??= load().catch((error: unknown) => {
56
+ pending = undefined;
57
+ throw error;
58
+ }));
59
+ };
60
+
61
+ const featureIds = Object.keys(consoleFeatureApis) as ConsoleFeature[];
62
+
63
+ /**
64
+ * What serves each feature, built by its factory: over the plugins' APIs the
65
+ * console assembled, or over a self-hosted server's admin API. A feature
66
+ * without its plugin, or without a factory for the admin API, has none.
67
+ */
68
+ const featureApis = (
69
+ build: (feature: ConsoleFeature) => unknown,
70
+ ): Partial<ConsoleFeatureApis> =>
71
+ Object.fromEntries(
72
+ featureIds.flatMap((feature) => {
73
+ const api = build(feature);
74
+ return api === undefined ? [] : [[feature, api]];
75
+ }),
76
+ );
77
+
78
+ /**
79
+ * The console's features and what serves them, for a console config:
80
+ * - a self-hosted server (`standaloneRepository`) runs its plugins and lists
81
+ * them on its admin `/version`; the console serves what that server's admin
82
+ * API serves, and nothing that needs the database;
83
+ * - otherwise the console assembles `plugins` over the database, as the
84
+ * server does, and serves the features of the plugins it assembled.
85
+ */
86
+ export const createConsoleRuntime = (config: {
87
+ readonly database: ConfiguredDatabase;
88
+ readonly plugins?: readonly unknown[];
89
+ }): ConsoleRuntime => {
90
+ const { database } = config;
91
+ if (isRemoteDatabase(database)) {
92
+ const { fetchAdmin } = database;
93
+ return {
94
+ remote: true,
95
+ features: once(async () =>
96
+ resolveConsoleFeatures(await readServerPlugins(fetchAdmin), {
97
+ remote: true,
98
+ }),
99
+ ),
100
+ apis: featureApis((feature) =>
101
+ consoleFeatureApis[feature].remote?.(fetchAdmin),
102
+ ),
103
+ };
104
+ }
105
+ const api = createDatabasePluginApis(database, config.plugins ?? []);
106
+ const features = resolveConsoleFeatures(Object.keys(api), { remote: false });
107
+ return {
108
+ remote: false,
109
+ features: async () => features,
110
+ // A feature reads the plugin with its plugin's id, taken to be Hot
111
+ // Updater's own, as a self-hosted server's /version list is.
112
+ apis: featureApis((feature) => {
113
+ const pluginApi = api[consoleFeatures[feature].plugin];
114
+ return pluginApi === undefined
115
+ ? undefined
116
+ : consoleFeatureApis[feature].local(pluginApi as never);
117
+ }),
118
+ };
119
+ };
120
+
121
+ /**
122
+ * What serves `feature`. Every feature's server function asks for it first,
123
+ * so a feature the console does not serve is refused with one error the
124
+ * client recognizes, before anything is read.
125
+ */
126
+ export const requireFeature = async <F extends ConsoleFeature>(
127
+ runtime: ConsoleRuntime,
128
+ feature: F,
129
+ ): Promise<ConsoleFeatureApis[F]> => {
130
+ const api = runtime.apis[feature];
131
+ if (api === undefined || !(await runtime.features())[feature]) {
132
+ throw new ConsoleFeatureUnavailableError(feature, {
133
+ remote: runtime.remote,
134
+ });
135
+ }
136
+ return api as ConsoleFeatureApis[F];
137
+ };