@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
@@ -0,0 +1,29 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+
3
+ import { consoleAccess } from "./console-access";
4
+ import { readAppUsageInput, type AppUsageInput } from "./insights-usage";
5
+
6
+ export const getAppUsageReportRpc = createServerFn({ method: "GET" })
7
+ .middleware([consoleAccess])
8
+ .validator((input: AppUsageInput) => {
9
+ readAppUsageInput(input);
10
+ return {
11
+ platform: input.platform,
12
+ channel: input.channel,
13
+ appVersion: input.appVersion,
14
+ window: input.window,
15
+ };
16
+ })
17
+ .handler(async ({ data }) => {
18
+ const [{ prepareConfig }, { getAppUsageReport }, { requireFeature }] =
19
+ await Promise.all([
20
+ import("./server/config.server"),
21
+ import("./server/insightsUsage"),
22
+ import("./server/runtime.server"),
23
+ ]);
24
+ const { runtime } = await prepareConfig();
25
+ return getAppUsageReport(
26
+ await requireFeature(runtime, "insightsAnalytics"),
27
+ data,
28
+ );
29
+ });
@@ -0,0 +1,74 @@
1
+ export type AppUsageInput = {
2
+ readonly platform: "all" | "ios" | "android";
3
+ readonly channel: string;
4
+ readonly appVersion?: string;
5
+ readonly window: UsageWindow;
6
+ };
7
+ export type AppUsageScope = Omit<AppUsageInput, "window">;
8
+
9
+ export type UsageDistribution = {
10
+ readonly name: string;
11
+ readonly installations: number;
12
+ };
13
+
14
+ export type BundleDistribution = {
15
+ readonly appVersion: string;
16
+ readonly platform: "ios" | "android";
17
+ readonly releaseId: string | null;
18
+ readonly installations: number;
19
+ };
20
+
21
+ export type AppUsageReport = {
22
+ readonly bundleDistribution: readonly BundleDistribution[];
23
+ readonly activeInstallations: number;
24
+ readonly sinceMs: number;
25
+ /**
26
+ * Where the distribution starts: the start of the UTC day that contains the
27
+ * period's start. Latest reports are counted by UTC day, so the
28
+ * distribution covers every UTC day the period touches.
29
+ */
30
+ readonly distributionSinceMs: number;
31
+ readonly beforeReceivedAtMs: number;
32
+ readonly intervalMs: number;
33
+ readonly truncated: boolean;
34
+ readonly appVersions: readonly string[];
35
+ readonly versions: readonly UsageDistribution[];
36
+ readonly platforms: readonly UsageDistribution[];
37
+ readonly points: readonly {
38
+ readonly startMs: number;
39
+ readonly installations: number | null;
40
+ }[];
41
+ };
42
+
43
+ export const usageMetrics = {
44
+ "24h": { label: "DAU", period: "24 hours", interval: "hour" },
45
+ "7d": { label: "WAU", period: "7 days", interval: "6 hours" },
46
+ "30d": { label: "MAU", period: "30 days", interval: "day" },
47
+ "12m": { label: "YAU", period: "12 months", interval: "week" },
48
+ } as const;
49
+
50
+ /**
51
+ * An App usage period. Hourly counts are kept 90 days, so the 12-month
52
+ * period counts whole UTC days, from the daily counts kept 13 months.
53
+ */
54
+ export type UsageWindow = keyof typeof usageMetrics;
55
+
56
+ export function readAppUsageInput(input: AppUsageInput): AppUsageInput {
57
+ if (
58
+ !input ||
59
+ (input.platform !== "all" &&
60
+ input.platform !== "ios" &&
61
+ input.platform !== "android") ||
62
+ typeof input.channel !== "string" ||
63
+ !input.channel.trim() ||
64
+ input.channel.length > 1_024 ||
65
+ !Object.hasOwn(usageMetrics, input.window) ||
66
+ (input.appVersion !== undefined &&
67
+ (typeof input.appVersion !== "string" ||
68
+ !input.appVersion.trim() ||
69
+ input.appVersion.length > 1_024))
70
+ ) {
71
+ throw new Error("Choose App usage filters and a time window.");
72
+ }
73
+ return input;
74
+ }
@@ -1,4 +1,7 @@
1
- import type { EventHistoryRow, InstallationRow } from "@hot-updater/server";
1
+ import type {
2
+ EventHistoryRow,
3
+ InstallationRow,
4
+ } from "@hot-updater/server/plugins/insights";
2
5
 
3
6
  export type InsightsEventRow = EventHistoryRow;
4
7
  export type InsightsInstallationViewRow = InstallationRow;
@@ -9,7 +12,40 @@ export type InsightsViewPage<TRow> = {
9
12
  };
10
13
 
11
14
  export const outcomeLabels = {
15
+ downloaded: "Downloaded reports",
12
16
  applied: "Applied reports",
13
17
  recovered: "Recovered-from reports",
14
- adopted: "Adopted reports",
18
+ failed: "Failed update reports",
15
19
  } as const;
20
+
21
+ const DAY_MS = 86_400_000;
22
+
23
+ /** The time ranges an event list reads, shortest first. */
24
+ export const eventRanges = {
25
+ "24h": { period: "24 hours", durationMs: DAY_MS },
26
+ "7d": { period: "7 days", durationMs: 7 * DAY_MS },
27
+ "30d": { period: "30 days", durationMs: 30 * DAY_MS },
28
+ "90d": { period: "90 days", durationMs: 90 * DAY_MS },
29
+ } as const;
30
+
31
+ export type EventRange = keyof typeof eventRanges;
32
+
33
+ export const EVENT_RANGES = Object.keys(eventRanges) as readonly EventRange[];
34
+
35
+ export const DEFAULT_EVENT_RANGE: EventRange = "7d";
36
+
37
+ /** The next longer range, or undefined for the longest. */
38
+ export const widerEventRange = (range: EventRange): EventRange | undefined =>
39
+ EVENT_RANGES[EVENT_RANGES.indexOf(range) + 1];
40
+
41
+ /**
42
+ * The receipt interval [sinceMs, beforeReceivedAtMs) a range covers. The end
43
+ * stays fixed while the list pages, so every page reads the same interval.
44
+ */
45
+ export const eventRangeBounds = (
46
+ range: EventRange,
47
+ beforeReceivedAtMs: number,
48
+ ) => ({
49
+ beforeReceivedAtMs,
50
+ sinceMs: Math.max(0, beforeReceivedAtMs - eventRanges[range].durationMs),
51
+ });
@@ -0,0 +1,21 @@
1
+ import type { AnyRouter, ParsedLocation } from "@tanstack/react-router";
2
+
3
+ declare module "@tanstack/react-router" {
4
+ interface StaticDataRouteOption {
5
+ /**
6
+ * The route's scroll-restoration key for a location; without one, each
7
+ * history entry keeps its own scroll position.
8
+ */
9
+ readonly scrollRestorationKey?: (location: ParsedLocation) => string;
10
+ }
11
+ }
12
+
13
+ /** A location's scroll-restoration key: its route's own, else its history entry's. */
14
+ export const routeScrollRestorationKey = (
15
+ router: Pick<AnyRouter, "getMatchedRoutes">,
16
+ location: ParsedLocation,
17
+ ): string => {
18
+ const { foundRoute } = router.getMatchedRoutes(location.pathname);
19
+ const routeKey = foundRoute?.options.staticData?.scrollRestorationKey;
20
+ return routeKey?.(location) ?? location.state.__TSR_key!;
21
+ };
@@ -0,0 +1,124 @@
1
+ import type {
2
+ InsightsEventPageInput,
3
+ InsightsProvider,
4
+ InsightsUpdateFailures,
5
+ InsightsUpdateFailuresInput,
6
+ } from "@hot-updater/server/plugins/insights";
7
+
8
+ import { ConsoleFeatureUnavailableError } from "../console-features";
9
+ import type { InsightsRetentionDays } from "../insights-retention";
10
+
11
+ /** The Insights reads the console pages use, update failures, and how long rows are kept. */
12
+ export type ConsoleInsightsReads = Omit<
13
+ InsightsProvider,
14
+ "appendBundleEvent"
15
+ > & {
16
+ getRetention(): Promise<InsightsRetentionDays>;
17
+ getUpdateFailures(
18
+ input: InsightsUpdateFailuresInput,
19
+ ): Promise<InsightsUpdateFailures>;
20
+ };
21
+
22
+ /** A GET on a self-hosted server's admin handler. */
23
+ export type FetchAdmin = (path: string) => Promise<Response>;
24
+
25
+ /** The `error` a failed admin route answered with, or a message naming its status. */
26
+ const errorOf = async (response: Response, path: string): Promise<Error> => {
27
+ const body: unknown = await response.json().catch(() => null);
28
+ return new Error(
29
+ typeof body === "object" &&
30
+ body !== null &&
31
+ typeof Reflect.get(body, "error") === "string"
32
+ ? String(Reflect.get(body, "error"))
33
+ : `The server answered ${path.split("?")[0]} with ${response.status}.`,
34
+ );
35
+ };
36
+
37
+ const withQuery = (
38
+ path: string,
39
+ input: Readonly<Record<string, number | string | undefined>>,
40
+ ): string => {
41
+ const query = new URLSearchParams();
42
+ for (const [key, value] of Object.entries(input)) {
43
+ if (value !== undefined) query.set(key, String(value));
44
+ }
45
+ const value = query.toString();
46
+ return value.length === 0 ? path : `${path}?${value}`;
47
+ };
48
+
49
+ const eventQuery = ({
50
+ beforeReceivedAtMs,
51
+ sinceMs,
52
+ cursor,
53
+ limit,
54
+ }: Omit<InsightsEventPageInput, "bundle">) => ({
55
+ beforeReceivedAtMs,
56
+ sinceMs,
57
+ cursor,
58
+ limit,
59
+ });
60
+
61
+ /**
62
+ * A self-hosted server's Insights, read through the admin routes its
63
+ * `insights()` plugin serves.
64
+ */
65
+ export const createAdminInsightsReads = (
66
+ fetchAdmin: FetchAdmin,
67
+ ): ConsoleInsightsReads => {
68
+ const read = async <T>(path: string): Promise<T | null> => {
69
+ const response = await fetchAdmin(path);
70
+ if (response.status === 404) return null;
71
+ if (!response.ok) throw await errorOf(response, path);
72
+ return (await response.json()) as T;
73
+ };
74
+ // A route no plugin serves answers 404: the server runs without insights(),
75
+ // as after a restart without it once the console read its plugins.
76
+ const required = async <T>(path: string): Promise<T> => {
77
+ const value = await read<T>(path);
78
+ if (value === null) {
79
+ throw new ConsoleFeatureUnavailableError("insights", { remote: true });
80
+ }
81
+ return value;
82
+ };
83
+
84
+ return {
85
+ getReportingOverview: ({ platform, channel, window, bundleId }) =>
86
+ required(withQuery("/overview", { platform, channel, window, bundleId })),
87
+ listEvents: ({ bundle, ...input }) =>
88
+ required(
89
+ withQuery("/events", {
90
+ ...eventQuery(input),
91
+ ...(bundle === undefined
92
+ ? {}
93
+ : {
94
+ platform: bundle.platform,
95
+ channel: bundle.channel,
96
+ bundleId: bundle.bundleId,
97
+ outcome: bundle.outcome,
98
+ }),
99
+ }),
100
+ ),
101
+ listInstallationEvents: ({ installId, ...input }) =>
102
+ required(
103
+ withQuery(
104
+ `/installations/${encodeURIComponent(installId)}/events`,
105
+ eventQuery(input),
106
+ ),
107
+ ),
108
+ getInstallation: ({ installId }) =>
109
+ read(`/installations/${encodeURIComponent(installId)}`),
110
+ pageInstallationsByCurrentUserId: ({ userId, cursor, limit }) =>
111
+ required(withQuery("/installations", { userId, cursor, limit })),
112
+ getUpdateFailures: ({ platform, channel, releaseId, timeRange }) =>
113
+ required(
114
+ withQuery("/failures", {
115
+ platform,
116
+ channel,
117
+ releaseId,
118
+ start: timeRange?.start,
119
+ end: timeRange?.end,
120
+ }),
121
+ ),
122
+ getRetention: () => required("/retention"),
123
+ };
124
+ };
@@ -13,11 +13,19 @@ const accessError = (
13
13
  { status: access.status === "unauthenticated" ? 401 : 403 },
14
14
  );
15
15
 
16
- export const getConsoleAccess = async (
17
- request: Request,
18
- ): Promise<ConsoleAccess> => {
19
- const adapter = await getConsoleAuthAdapter();
20
- return adapter.getAccess(request);
16
+ // Server function middleware checks access before input, and prepareConfig
17
+ // checks again for its callers: one request asks the adapter once.
18
+ const accessByRequest = new WeakMap<Request, Promise<ConsoleAccess>>();
19
+
20
+ export const getConsoleAccess = (request: Request): Promise<ConsoleAccess> => {
21
+ let access = accessByRequest.get(request);
22
+ if (!access) {
23
+ access = getConsoleAuthAdapter().then((adapter) =>
24
+ adapter.getAccess(request),
25
+ );
26
+ accessByRequest.set(request, access);
27
+ }
28
+ return access;
21
29
  };
22
30
 
23
31
  export const getConsoleAuthProviders = async (
@@ -0,0 +1,37 @@
1
+ import type {
2
+ InsightsModel,
3
+ ReleaseReference,
4
+ } from "@hot-updater/server/plugins/insights";
5
+
6
+ import type { BundleActivityInput } from "../bundle-activity";
7
+
8
+ export async function getBundleActivity(
9
+ model: InsightsModel,
10
+ inputs: readonly BundleActivityInput[],
11
+ ) {
12
+ if (inputs.length === 0) return {};
13
+ const releases: ReleaseReference[] = inputs.map((input) => ({
14
+ releaseId: input.releaseId,
15
+ platform: input.platform,
16
+ channel: input.channel,
17
+ }));
18
+ const activity = await model.getReleaseActivity({ releases });
19
+ return Object.fromEntries(
20
+ activity.data.flatMap((item) =>
21
+ item.release === undefined
22
+ ? []
23
+ : [
24
+ [
25
+ item.release.releaseId,
26
+ {
27
+ downloads: item.metrics.downloads,
28
+ activeDays: item.metrics.launches,
29
+ failedLaunches: item.metrics.failedLaunches,
30
+ measuredAtMs: activity.measuredAtMs,
31
+ coverage: activity.coverage,
32
+ },
33
+ ],
34
+ ],
35
+ ),
36
+ );
37
+ }
@@ -1,28 +1,23 @@
1
1
  import {
2
2
  assertStorageOperations,
3
- createDatabaseClient,
4
- type DatabaseClient,
3
+ type HotUpdaterCoreApi,
5
4
  type StoragePluginWith,
6
5
  } from "@hot-updater/plugin-core";
6
+ import { createDatabaseCoreApi } from "@hot-updater/server/db";
7
7
  import { getRequest } from "@tanstack/react-start/server";
8
8
 
9
- import type { ResolvedHotUpdaterConsoleConfig } from "../../index";
9
+ import type { HotUpdaterConsoleConfig } from "../../index";
10
10
  import { requireConsoleAccess } from "./auth.server";
11
11
  import { resolveConsoleConfig } from "./console-runtime.server";
12
+ import type { ConsoleRuntime } from "./runtime.server";
12
13
 
13
- type ResolvedConsoleConfig = ResolvedHotUpdaterConsoleConfig & {
14
- readonly console: NonNullable<ResolvedHotUpdaterConsoleConfig["console"]>;
14
+ type ResolvedConsoleConfig = HotUpdaterConsoleConfig & {
15
+ readonly console: NonNullable<HotUpdaterConsoleConfig["console"]>;
15
16
  };
16
17
 
17
18
  let configPromise: Promise<ResolvedConsoleConfig> | null = null;
18
- let databaseClient: DatabaseClient | null = null;
19
- let hotUpdater: ReturnType<
20
- typeof import("./runtime.server").createRuntimeHotUpdater
21
- > | null = null;
22
- let apiKeyStore: ReturnType<
23
- typeof import("./runtime.server").createApiKeyStore
24
- > | null = null;
25
- let apiKeyStoreResolved = false;
19
+ let core: HotUpdaterCoreApi | null = null;
20
+ let runtime: ConsoleRuntime | null = null;
26
21
  let storagePluginPromise: Promise<
27
22
  StoragePluginWith<"get" | "put" | "exists" | "delete">
28
23
  > | null = null;
@@ -69,30 +64,19 @@ export const prepareConfig = async (request: Request = getRequest()) => {
69
64
  await requireConsoleAccess(request);
70
65
  const config = await loadCachedConfig(request);
71
66
 
72
- if (!databaseClient) {
73
- databaseClient = createDatabaseClient(config.database);
74
- }
75
-
76
- if (!hotUpdater) {
77
- const { createRuntimeHotUpdater } = await import("./runtime.server");
78
- hotUpdater = createRuntimeHotUpdater(config);
79
- }
67
+ // Bundles, releases, catalogs, and channels: core's API, in process or
68
+ // over a self-hosted server's admin API.
69
+ core ??= createDatabaseCoreApi(config.database);
80
70
 
81
- if (!apiKeyStoreResolved) {
82
- const { createApiKeyStore } = await import("./runtime.server");
83
- apiKeyStore = createApiKeyStore(config);
84
- apiKeyStoreResolved = true;
71
+ // The console's features: those of the plugins the server runs.
72
+ if (!runtime) {
73
+ const { createConsoleRuntime } = await import("./runtime.server");
74
+ runtime = createConsoleRuntime(config);
85
75
  }
86
76
 
87
77
  const storagePlugin = await loadCachedStoragePlugin(config);
88
78
 
89
- return {
90
- config,
91
- databaseClient,
92
- hotUpdater,
93
- apiKeyStore,
94
- storagePlugin,
95
- };
79
+ return { config, core, runtime, storagePlugin };
96
80
  } catch (error) {
97
81
  if (
98
82
  !(
@@ -0,0 +1,59 @@
1
+ import type { RemoteDatabase } from "@hot-updater/plugin-core";
2
+ import type { ApiKeyManagementAPI } from "@hot-updater/server/plugins/api-keys";
3
+ import {
4
+ createInsightsModel,
5
+ createInsightsProvider,
6
+ type InsightsApi,
7
+ type InsightsModel,
8
+ } from "@hot-updater/server/plugins/insights";
9
+
10
+ import type { ConsoleFeature } from "../console-features";
11
+ import {
12
+ type ConsoleInsightsReads,
13
+ createAdminInsightsReads,
14
+ } from "./adminInsights";
15
+
16
+ /** A GET on a self-hosted server's admin handler. */
17
+ export type FetchAdmin = RemoteDatabase["fetchAdmin"];
18
+
19
+ /**
20
+ * What serves one console feature: `local` builds it from the API of the
21
+ * feature's plugin, which the console assembles over the database as the
22
+ * server does; `remote` builds it over a self-hosted server's admin API, for
23
+ * a feature that API serves.
24
+ */
25
+ export interface ConsoleFeatureApi<TPluginApi, TApi> {
26
+ readonly local: (pluginApi: TPluginApi) => TApi;
27
+ readonly remote?: (fetchAdmin: FetchAdmin) => TApi;
28
+ }
29
+
30
+ const featureApi = <TPluginApi, TApi>(
31
+ api: ConsoleFeatureApi<TPluginApi, TApi>,
32
+ ): ConsoleFeatureApi<TPluginApi, TApi> => api;
33
+
34
+ /** What serves each console feature, keyed like `consoleFeatures`. */
35
+ export const consoleFeatureApis = {
36
+ insights: featureApi({
37
+ local: (api: InsightsApi): ConsoleInsightsReads => ({
38
+ ...createInsightsProvider(createInsightsModel(api)),
39
+ getRetention: async () => api.retention,
40
+ getUpdateFailures: api.getUpdateFailures,
41
+ }),
42
+ remote: createAdminInsightsReads,
43
+ }),
44
+ insightsAnalytics: featureApi({
45
+ local: (api: InsightsApi): InsightsModel => createInsightsModel(api),
46
+ }),
47
+ apiKeys: featureApi({
48
+ local: (api: ApiKeyManagementAPI) => api,
49
+ }),
50
+ } satisfies {
51
+ readonly [F in ConsoleFeature]: ConsoleFeatureApi<never, unknown>;
52
+ };
53
+
54
+ /** What serves each console feature, as its server functions use it. */
55
+ export type ConsoleFeatureApis = {
56
+ readonly [F in ConsoleFeature]: ReturnType<
57
+ (typeof consoleFeatureApis)[F]["local"]
58
+ >;
59
+ };
@@ -1,43 +1,9 @@
1
1
  import type {
2
2
  ConsoleAuthAdapter,
3
- ConsoleSigningConfig,
3
+ HotUpdaterConsoleConfig,
4
4
  HotUpdaterConsoleConfigSource,
5
- ResolvedHotUpdaterConsoleConfig,
6
5
  } from "../../index";
7
6
 
8
- const readString = (value: unknown, key: string): string | undefined => {
9
- if (typeof value !== "object" || value === null) return undefined;
10
- const member = Reflect.get(value, key);
11
- return typeof member === "string" && member.trim().length > 0
12
- ? member.trim()
13
- : undefined;
14
- };
15
-
16
- const getProviderName = (signing: unknown): string => {
17
- return (
18
- readString(signing, "name")?.slice(0, 80) ??
19
- (readString(signing, "privateKeyPath") ? "localSigning" : undefined) ??
20
- "Configured provider"
21
- );
22
- };
23
-
24
- export const sanitizeConsoleSigningConfig = (
25
- signing: unknown,
26
- ): ConsoleSigningConfig | undefined => {
27
- if (typeof signing !== "object" || signing === null) return undefined;
28
- const privateKeyPath = readString(signing, "privateKeyPath");
29
- if (
30
- Reflect.get(signing, "enabled") === false ||
31
- (privateKeyPath && Reflect.get(signing, "enabled") !== true)
32
- )
33
- return undefined;
34
-
35
- return {
36
- enabled: true,
37
- provider: getProviderName(signing),
38
- };
39
- };
40
-
41
7
  export const getConsoleAuthAdapter = async (): Promise<ConsoleAuthAdapter> => {
42
8
  const module = await import("virtual:hot-updater-console/auth");
43
9
  return module.default;
@@ -45,17 +11,17 @@ export const getConsoleAuthAdapter = async (): Promise<ConsoleAuthAdapter> => {
45
11
 
46
12
  export const resolveConsoleConfig = async (
47
13
  request: Request,
48
- ): Promise<ResolvedHotUpdaterConsoleConfig> => {
14
+ ): Promise<HotUpdaterConsoleConfig> => {
49
15
  const { default: source } =
50
16
  (await import("virtual:hot-updater-console/config")) as {
51
17
  readonly default: HotUpdaterConsoleConfigSource;
52
18
  };
53
19
 
54
20
  const config = typeof source === "function" ? await source(request) : source;
55
- const { signing: signingSource, ...configWithoutSigning } = config;
56
- const signing = sanitizeConsoleSigningConfig(signingSource);
57
21
  return {
58
- ...configWithoutSigning,
59
- ...(signing === undefined ? {} : { signing }),
22
+ console: config.console,
23
+ database: config.database,
24
+ storage: config.storage,
25
+ ...(config.plugins === undefined ? {} : { plugins: config.plugins }),
60
26
  };
61
27
  };
@@ -3,10 +3,11 @@ import {
3
3
  getManifestStorageUri,
4
4
  getPatchStorageUri,
5
5
  } from "@hot-updater/core";
6
- import type {
7
- Bundle,
8
- DatabaseClient,
9
- StoragePluginWith,
6
+ import {
7
+ type Bundle,
8
+ type HotUpdaterCoreApi,
9
+ rowToBundle,
10
+ type StoragePluginWith,
10
11
  } from "@hot-updater/plugin-core";
11
12
 
12
13
  interface DeleteBundleInput {
@@ -18,7 +19,7 @@ interface DeleteBundlesInput {
18
19
  }
19
20
 
20
21
  interface DeleteBundleDependencies {
21
- databaseClient: DatabaseClient;
22
+ core: Pick<HotUpdaterCoreApi, "getBundle" | "deleteBundles">;
22
23
  storagePlugin: StoragePluginWith<"delete">;
23
24
  waitForStorageCleanup?: boolean;
24
25
  }
@@ -54,8 +55,7 @@ async function cleanupBundleStorage(
54
55
  if (resolvedStorageUri) cleanupUris.add(resolvedStorageUri);
55
56
  };
56
57
 
57
- addCleanupUri(bundle.storageUri);
58
- addCleanupUri(getManifestStorageUri(bundle) ?? undefined);
58
+ addCleanupUri(getManifestStorageUri(bundle));
59
59
  addCleanupUri(getPatchStorageUri(bundle) ?? undefined);
60
60
  for (const patch of getBundlePatches(bundle)) {
61
61
  addCleanupUri(patch.patchStorageUri);
@@ -73,30 +73,24 @@ async function cleanupBundleStorage(
73
73
  export async function deleteBundles(
74
74
  { bundleIds }: DeleteBundlesInput,
75
75
  {
76
- databaseClient,
76
+ core,
77
77
  storagePlugin,
78
78
  waitForStorageCleanup = true,
79
79
  }: DeleteBundleDependencies,
80
80
  ) {
81
81
  const uniqueBundleIds = [...new Set(bundleIds)];
82
- const { data: matchedBundles } = await databaseClient.getBundles({
83
- where: { id: { in: uniqueBundleIds } },
84
- limit: uniqueBundleIds.length,
85
- });
86
- const matchedById = new Map(
87
- matchedBundles.map((bundle) => [bundle.id, bundle]),
82
+ const details = await Promise.all(
83
+ uniqueBundleIds.map((bundleId) => core.getBundle(bundleId)),
84
+ );
85
+ const bundles = details.flatMap((detail) =>
86
+ detail === null ? [] : [rowToBundle(detail.bundle, detail.patches)],
88
87
  );
89
- const bundles = uniqueBundleIds.flatMap((bundleId) => {
90
- const bundle = matchedById.get(bundleId);
91
- return bundle ? [bundle] : [];
92
- });
93
88
  const missingBundleIds = uniqueBundleIds.filter(
94
- (bundleId) => !matchedById.has(bundleId),
89
+ (_bundleId, position) => details[position] === null,
95
90
  );
96
91
 
97
92
  for (const bundle of bundles) {
98
93
  const cleanupCandidates = [
99
- bundle.storageUri,
100
94
  getManifestStorageUri(bundle),
101
95
  getPatchStorageUri(bundle),
102
96
  ...getBundlePatches(bundle).map((patch) => patch.patchStorageUri),
@@ -106,12 +100,9 @@ export async function deleteBundles(
106
100
  }
107
101
  }
108
102
 
103
+ // One core write; a bundle a release still uses refuses the whole batch.
109
104
  if (bundles.length > 0) {
110
- await databaseClient.mutate(async (mutation) => {
111
- for (const bundle of bundles) {
112
- await mutation.deleteBundleById(bundle.id);
113
- }
114
- });
105
+ await core.deleteBundles(bundles.map((bundle) => bundle.id));
115
106
  }
116
107
 
117
108
  const cleanupStorage = async () => {