@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,20 @@
1
+ import { createMiddleware } from "@tanstack/react-start";
2
+
3
+ /**
4
+ * Refuses a caller the console does not authorize before a server function
5
+ * reads its input. TanStack Start runs a function's middleware before the
6
+ * function's own validator, so an anonymous or unlisted caller gets the access
7
+ * check's 401 or 403, never an input error, whatever it sends. Every server
8
+ * function except the sign-in reads in `auth-rpc.ts` uses it, and
9
+ * `console-access.spec.ts` checks that.
10
+ */
11
+ export const consoleAccess = createMiddleware({ type: "function" }).server(
12
+ async ({ next }) => {
13
+ const [{ getRequest }, { requireConsoleAccess }] = await Promise.all([
14
+ import("@tanstack/react-start/server"),
15
+ import("./server/auth.server"),
16
+ ]);
17
+ await requireConsoleAccess(getRequest());
18
+ return next();
19
+ },
20
+ );
@@ -0,0 +1,68 @@
1
+ import {
2
+ type QueryClient,
3
+ queryOptions,
4
+ useQuery,
5
+ } from "@tanstack/react-query";
6
+ import { notFound } from "@tanstack/react-router";
7
+
8
+ import { type ConsoleFeature, consoleFeatures } from "./console-features";
9
+ import { getConsoleFeaturesRpc } from "./console-features-rpc";
10
+
11
+ export const consoleFeaturesQueryKey = ["console", "features"] as const;
12
+
13
+ export const consoleFeaturesQueryOptions = () =>
14
+ queryOptions({
15
+ queryKey: consoleFeaturesQueryKey,
16
+ queryFn: () => getConsoleFeaturesRpc(),
17
+ // A server's plugins change only when it restarts: one read serves a visit.
18
+ staleTime: Infinity,
19
+ });
20
+
21
+ /** The features the console serves; the authorized layout loads them first. */
22
+ export const useConsoleFeatures = () => useQuery(consoleFeaturesQueryOptions());
23
+
24
+ /** Whether `feature` is on; false until the features load. */
25
+ export const useConsoleFeature = (feature: ConsoleFeature): boolean =>
26
+ useConsoleFeatures().data?.features[feature] === true;
27
+
28
+ /** A feature route's not-found data: the feature its guard found off. */
29
+ type ConsoleFeatureNotFoundData = { readonly feature: ConsoleFeature };
30
+
31
+ /** The feature in a route's not-found data, when a feature guard threw it. */
32
+ export const notFoundFeature = (data: unknown): ConsoleFeature | undefined => {
33
+ const feature =
34
+ typeof data === "object" && data !== null
35
+ ? Reflect.get(data, "feature")
36
+ : undefined;
37
+ return typeof feature === "string" && Object.hasOwn(consoleFeatures, feature)
38
+ ? (feature as ConsoleFeature)
39
+ : undefined;
40
+ };
41
+
42
+ /**
43
+ * A feature route's `beforeLoad`: not found, naming the feature, when the
44
+ * console does not serve it. A failed features read lets the route load: the
45
+ * root layout answers a visitor it does not authorize, and the route's own
46
+ * reads report any other failure. The failure leaves the cache, which a
47
+ * server render sends to the browser: a refused visitor's failure is the 401
48
+ * or 403 Response, which the render cannot serialize.
49
+ */
50
+ export const requireConsoleFeature = async (
51
+ queryClient: QueryClient,
52
+ feature: ConsoleFeature,
53
+ ): Promise<void> => {
54
+ const featureSet = await queryClient
55
+ .ensureQueryData(consoleFeaturesQueryOptions())
56
+ .catch(() => {
57
+ queryClient.removeQueries({
58
+ queryKey: consoleFeaturesQueryKey,
59
+ exact: true,
60
+ });
61
+ return undefined;
62
+ });
63
+ if (featureSet !== undefined && !featureSet.features[feature]) {
64
+ throw notFound({
65
+ data: { feature } satisfies ConsoleFeatureNotFoundData,
66
+ });
67
+ }
68
+ };
@@ -0,0 +1,13 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+
3
+ import { consoleAccess } from "./console-access";
4
+ import type { ConsoleFeatureSet } from "./console-features";
5
+
6
+ /** The features this console serves, for navigation and route guards. */
7
+ export const getConsoleFeaturesRpc = createServerFn({ method: "GET" })
8
+ .middleware([consoleAccess])
9
+ .handler(async (): Promise<ConsoleFeatureSet> => {
10
+ const { prepareConfig } = await import("./server/config.server");
11
+ const { runtime } = await prepareConfig();
12
+ return { features: await runtime.features(), remote: runtime.remote };
13
+ });
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The console's features and the server plugin each one needs. The server is
3
+ * the source of truth: the console serves a feature only when the server runs
4
+ * its plugin, and against a self-hosted server (`standaloneRepository`) only
5
+ * the features its admin API serves (`remote`); the rest read the database.
6
+ * Navigation, route guards, and server functions all read this registry. Its
7
+ * other halves, keyed by the same features, hold what serves each feature on
8
+ * the server (`server/console-feature-apis.ts`) and what each adds to pages
9
+ * (`components/features/console-feature-ui.tsx`).
10
+ */
11
+ export const consoleFeatures = {
12
+ /** Event history and installation lookups, which the admin API serves too. */
13
+ insights: { plugin: "insights", label: "Insights", remote: true },
14
+ /** App usage, distribution, and release activity, read from the database. */
15
+ insightsAnalytics: { plugin: "insights", label: "Insights", remote: false },
16
+ /** API key management: `apiKeys()` mounts no admin routes to manage them. */
17
+ apiKeys: { plugin: "apiKeys", label: "API keys", remote: false },
18
+ } as const satisfies Readonly<
19
+ Record<
20
+ string,
21
+ {
22
+ readonly plugin: string;
23
+ /** How navigation and the not-installed page name the feature. */
24
+ readonly label: string;
25
+ readonly remote: boolean;
26
+ }
27
+ >
28
+ >;
29
+
30
+ export type ConsoleFeature = keyof typeof consoleFeatures;
31
+
32
+ /** Whether each feature is on. */
33
+ export type ConsoleFeatures = Readonly<Record<ConsoleFeature, boolean>>;
34
+
35
+ /** What the console serves, as its client reads it. */
36
+ export type ConsoleFeatureSet = {
37
+ readonly features: ConsoleFeatures;
38
+ /** Whether the console reaches a self-hosted server through its admin API. */
39
+ readonly remote: boolean;
40
+ };
41
+
42
+ const featureIds = Object.keys(consoleFeatures) as ConsoleFeature[];
43
+
44
+ /** The features on for the plugins a server runs, from where the console reads them. */
45
+ export const resolveConsoleFeatures = (
46
+ plugins: readonly string[],
47
+ { remote }: { readonly remote: boolean },
48
+ ): ConsoleFeatures => {
49
+ const installed = new Set(plugins);
50
+ return Object.fromEntries(
51
+ featureIds.map((feature) => {
52
+ const { plugin, remote: servedRemotely } = consoleFeatures[feature];
53
+ return [feature, installed.has(plugin) && (servedRemotely || !remote)];
54
+ }),
55
+ ) as Record<ConsoleFeature, boolean>;
56
+ };
57
+
58
+ /** Why a feature is off: its plugin is missing, or it reads a database the console does not open. */
59
+ export const unavailableReason = (
60
+ feature: ConsoleFeature,
61
+ { remote }: { readonly remote: boolean },
62
+ ): "plugin" | "remote" =>
63
+ remote && !consoleFeatures[feature].remote ? "remote" : "plugin";
64
+
65
+ /**
66
+ * A server function refused a feature the console does not serve: the
67
+ * features' 404. The client recognizes it by `name`, which survives
68
+ * serialization where the class does not.
69
+ */
70
+ export class ConsoleFeatureUnavailableError extends Error {
71
+ override readonly name = "ConsoleFeatureUnavailableError";
72
+ readonly status = 404;
73
+
74
+ constructor(
75
+ readonly feature: ConsoleFeature,
76
+ { remote }: { readonly remote: boolean },
77
+ ) {
78
+ super(
79
+ unavailableReason(feature, { remote }) === "remote"
80
+ ? "The console reads this from the database, and it reaches a self-hosted server through its admin API instead."
81
+ : `The server runs without the ${consoleFeatures[feature].plugin}() plugin.`,
82
+ );
83
+ }
84
+ }
85
+
86
+ export const isConsoleFeatureUnavailableError = (
87
+ error: unknown,
88
+ ): error is Error & { readonly feature: ConsoleFeature } =>
89
+ error instanceof Error &&
90
+ error.name === "ConsoleFeatureUnavailableError" &&
91
+ featureIds.includes(Reflect.get(error, "feature") as ConsoleFeature);
@@ -1,9 +1,19 @@
1
- import type { InsightsEventPageInput } from "@hot-updater/server";
1
+ import type { InsightsEventPageInput } from "@hot-updater/server/plugins/insights";
2
2
  import { useQuery } from "@tanstack/react-query";
3
3
 
4
+ import {
5
+ readUpdateFailuresInput,
6
+ type UpdateFailuresInput,
7
+ } from "./insights-failures";
8
+ import { getUpdateFailuresRpc } from "./insights-failures-rpc";
9
+ import {
10
+ DEFAULT_INSIGHTS_RETENTION,
11
+ type InsightsRetentionDays,
12
+ } from "./insights-retention";
4
13
  import {
5
14
  findInsightsInstallationsRpc,
6
15
  getInsightsInstallationRpc,
16
+ getInsightsRetentionRpc,
7
17
  getReportingInstallationsRpc,
8
18
  listInsightsEventsRpc,
9
19
  listInsightsInstallationEventsRpc,
@@ -98,3 +108,29 @@ export const useInsightsInstallationEventsQuery = (
98
108
  refetchOnWindowFocus: true,
99
109
  staleTime: STALE_TIME_MS,
100
110
  });
111
+
112
+ /** A release's or a channel's update failures; lifetime for a release without a window. */
113
+ export const useUpdateFailuresQuery = (
114
+ input: UpdateFailuresInput,
115
+ enabled = true,
116
+ ) =>
117
+ useQuery({
118
+ queryKey: ["insights", "update-failures", input],
119
+ queryFn: () =>
120
+ getUpdateFailuresRpc({ data: readUpdateFailuresInput(input) }),
121
+ enabled,
122
+ refetchOnWindowFocus: true,
123
+ staleTime: STALE_TIME_MS,
124
+ });
125
+
126
+ /**
127
+ * How long the server keeps Insights rows; the defaults until it answers, or
128
+ * when it does not report them.
129
+ */
130
+ export const useInsightsRetention = (): InsightsRetentionDays =>
131
+ useQuery({
132
+ queryKey: ["insights", "retention"],
133
+ queryFn: () => getInsightsRetentionRpc(),
134
+ // A server's periods change only when it restarts.
135
+ staleTime: Infinity,
136
+ }).data ?? DEFAULT_INSIGHTS_RETENTION;
@@ -0,0 +1,21 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+
3
+ import { consoleAccess } from "./console-access";
4
+ import { readUpdateFailuresInput } from "./insights-failures";
5
+
6
+ export const getUpdateFailuresRpc = createServerFn({ method: "GET" })
7
+ .middleware([consoleAccess])
8
+ .validator(readUpdateFailuresInput)
9
+ .handler(async ({ data }) => {
10
+ const [{ prepareConfig }, { getUpdateFailuresReport }, { requireFeature }] =
11
+ await Promise.all([
12
+ import("./server/config.server"),
13
+ import("./server/updateFailures"),
14
+ import("./server/runtime.server"),
15
+ ]);
16
+ const { runtime } = await prepareConfig();
17
+ return getUpdateFailuresReport(
18
+ await requireFeature(runtime, "insights"),
19
+ data,
20
+ );
21
+ });
@@ -0,0 +1,129 @@
1
+ import type {
2
+ InsightsFailureBreakdown,
3
+ InsightsUpdateFailures,
4
+ } from "@hot-updater/server/plugins/insights";
5
+
6
+ import { recoveryWindows } from "./insights-recovery";
7
+ import type { InsightsWindow } from "./insights-rpc";
8
+ import type { InsightsEventRow } from "./insights-view";
9
+
10
+ export type { InsightsFailureBreakdown };
11
+
12
+ export type UpdateFailuresInput = {
13
+ readonly platform: "ios" | "android";
14
+ readonly channel: string;
15
+ /** A release's failures; the channel's without it. */
16
+ readonly releaseId?: string;
17
+ /** The period that ends with the current hour; a release's since its first report without it. */
18
+ readonly window?: InsightsWindow;
19
+ };
20
+
21
+ export type UpdateFailuresReport = InsightsUpdateFailures & {
22
+ /** Where the period starts, or null since the release's first report. */
23
+ readonly startMs: number | null;
24
+ readonly endMs: number;
25
+ };
26
+
27
+ export function readUpdateFailuresInput(
28
+ input: UpdateFailuresInput,
29
+ ): UpdateFailuresInput {
30
+ if (
31
+ !input ||
32
+ (input.platform !== "ios" && input.platform !== "android") ||
33
+ typeof input.channel !== "string" ||
34
+ !input.channel.trim() ||
35
+ input.channel.length > 1_024 ||
36
+ (input.releaseId !== undefined &&
37
+ (typeof input.releaseId !== "string" ||
38
+ !input.releaseId.trim() ||
39
+ input.releaseId.length > 36)) ||
40
+ (input.window === undefined
41
+ ? input.releaseId === undefined
42
+ : !Object.hasOwn(recoveryWindows, input.window))
43
+ ) {
44
+ throw new Error("Choose a platform, channel, and time window.");
45
+ }
46
+ return input;
47
+ }
48
+
49
+ /**
50
+ * The failure rate of update attempts: failed download and install reports,
51
+ * over those plus download reports. Null without either.
52
+ */
53
+ export const failureRate = ({
54
+ failedUpdates,
55
+ downloads,
56
+ }: Pick<InsightsUpdateFailures, "failedUpdates" | "downloads">):
57
+ | number
58
+ | null =>
59
+ failedUpdates + downloads === 0
60
+ ? null
61
+ : failedUpdates / (failedUpdates + downloads);
62
+
63
+ /** Of the downloads that tried a patch, the share that fell back to files or the archive. */
64
+ export const patchFallbackRate = ({
65
+ patchDownloads,
66
+ patchFallbacks,
67
+ }: Pick<InsightsUpdateFailures, "patchDownloads" | "patchFallbacks">):
68
+ | number
69
+ | null =>
70
+ patchDownloads + patchFallbacks === 0
71
+ ? null
72
+ : patchFallbacks / (patchDownloads + patchFallbacks);
73
+
74
+ /** Installations whose update check failed, over the active ones; estimates, so at most 100%. */
75
+ export const checkFailureRate = ({
76
+ failedInstallations,
77
+ activeInstallations,
78
+ }: NonNullable<InsightsUpdateFailures["checks"]>): number | null =>
79
+ activeInstallations === 0
80
+ ? null
81
+ : Math.min(1, failedInstallations / activeInstallations);
82
+
83
+ export const formatRate = (rate: number | null): string =>
84
+ rate === null ? "—" : `${(rate * 100).toFixed(2)}%`;
85
+
86
+ const STAGES: Readonly<Record<string, string>> = {
87
+ check: "Update check",
88
+ download: "Download",
89
+ install: "Install",
90
+ };
91
+
92
+ const REASONS: Readonly<Record<string, string>> = {
93
+ network: "Network",
94
+ http: "HTTP error",
95
+ invalid_response: "Invalid response",
96
+ hash_mismatch: "Hash mismatch",
97
+ signature: "Signature",
98
+ patch: "Patch",
99
+ extract: "Extract",
100
+ storage: "Storage",
101
+ };
102
+
103
+ export const failureStageLabel = (stage: string): string =>
104
+ STAGES[stage] ?? "Unknown stage";
105
+
106
+ export const failureReasonLabel = (reason: string): string =>
107
+ REASONS[reason] ?? "Unknown reason";
108
+
109
+ /** What else the client knew: its resource, HTTP status, origin code, and transport. */
110
+ export const failureDetailParts = (detail: {
111
+ readonly resource?: string | null;
112
+ readonly httpStatus?: number | null;
113
+ readonly originCode?: string | null;
114
+ readonly transport?: string | null;
115
+ }): string[] => [
116
+ ...(detail.httpStatus == null ? [] : [`HTTP ${detail.httpStatus}`]),
117
+ ...(detail.originCode == null ? [] : [detail.originCode]),
118
+ ...(detail.transport == null ? [] : [detail.transport]),
119
+ ...(detail.resource == null ? [] : [detail.resource]),
120
+ ];
121
+
122
+ /** An update failure as one line: where, why, and what else the client knew. */
123
+ export const describeFailure = (
124
+ failure: NonNullable<InsightsEventRow["failure"]>,
125
+ ): string =>
126
+ [
127
+ `${failureStageLabel(failure.stage)} failed: ${failureReasonLabel(failure.reason)}`,
128
+ ...failureDetailParts(failure),
129
+ ].join(" · ");
@@ -0,0 +1,51 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+
3
+ import type { BundleActivityInput } from "./bundle-activity";
4
+ import { consoleAccess } from "./console-access";
5
+ import { readRecoveryInput } from "./insights-recovery";
6
+
7
+ export function readBundleActivityInput(
8
+ inputs: readonly BundleActivityInput[],
9
+ ) {
10
+ if (!Array.isArray(inputs) || inputs.length > 20)
11
+ throw new Error("Choose up to 20 bundles.");
12
+ return inputs.map((input) => {
13
+ readRecoveryInput({ ...input, window: "30d" });
14
+ if (!input.releaseId) throw new Error("Choose a bundle ID.");
15
+ return input;
16
+ });
17
+ }
18
+
19
+ export const getBundleActivityRpc = createServerFn({ method: "GET" })
20
+ .middleware([consoleAccess])
21
+ .validator(readBundleActivityInput)
22
+ .handler(async ({ data }) => {
23
+ const [{ prepareConfig }, { getBundleActivity }, { requireFeature }] =
24
+ await Promise.all([
25
+ import("./server/config.server"),
26
+ import("./server/bundleActivity"),
27
+ import("./server/runtime.server"),
28
+ ]);
29
+ const { runtime } = await prepareConfig();
30
+ return getBundleActivity(
31
+ await requireFeature(runtime, "insightsAnalytics"),
32
+ data,
33
+ );
34
+ });
35
+
36
+ export const getRecoveryReportRpc = createServerFn({ method: "GET" })
37
+ .middleware([consoleAccess])
38
+ .validator(readRecoveryInput)
39
+ .handler(async ({ data }) => {
40
+ const [{ prepareConfig }, { getRecoveryReport }, { requireFeature }] =
41
+ await Promise.all([
42
+ import("./server/config.server"),
43
+ import("./server/insightsRecovery"),
44
+ import("./server/runtime.server"),
45
+ ]);
46
+ const { runtime } = await prepareConfig();
47
+ return getRecoveryReport(
48
+ await requireFeature(runtime, "insightsAnalytics"),
49
+ data,
50
+ );
51
+ });
@@ -0,0 +1,68 @@
1
+ import type { InsightsCoverage } from "@hot-updater/server/plugins/insights";
2
+
3
+ import type { InsightsWindow } from "./insights-rpc";
4
+
5
+ export type RecoveryInput = {
6
+ readonly platform: "ios" | "android";
7
+ readonly channel: string;
8
+ readonly window: InsightsWindow;
9
+ readonly releaseId?: string;
10
+ };
11
+
12
+ export type RecoveryReport = {
13
+ readonly downloads: number;
14
+ /**
15
+ * Distinct installations that reported in the scope and period, or that
16
+ * launched the release; estimated.
17
+ */
18
+ readonly activeInstallations: number;
19
+ /**
20
+ * Installation-days: each installation once for each UTC day it launched,
21
+ * and once more on a day an update applied or recovered.
22
+ */
23
+ readonly activeDays: number;
24
+ readonly failedLaunches: number;
25
+ readonly points: readonly {
26
+ readonly startMs: number;
27
+ /** That UTC day's daily active installations. */
28
+ readonly dailyActiveInstallations: number;
29
+ readonly failedLaunches: number;
30
+ }[];
31
+ readonly startMs: number;
32
+ readonly endMs: number;
33
+ readonly measuredAtMs: number;
34
+ readonly coverage: InsightsCoverage;
35
+ };
36
+
37
+ export const recoveryWindows = {
38
+ "24h": { durationMs: 86_400_000, intervalMs: 3_600_000 },
39
+ "7d": { durationMs: 7 * 86_400_000, intervalMs: 6 * 3_600_000 },
40
+ "30d": { durationMs: 30 * 86_400_000, intervalMs: 86_400_000 },
41
+ } as const;
42
+
43
+ /**
44
+ * Where a period ends: the end of the current UTC hour, as the server's
45
+ * reporting overview ends. The counters keep whole hours, so this is the
46
+ * latest end that still counts a report received just now; every Insights
47
+ * card ends here, so their numbers cover the same hours.
48
+ */
49
+ export const insightsPeriodEnd = (now: number): number =>
50
+ Math.ceil(now / 3_600_000) * 3_600_000;
51
+
52
+ export function readRecoveryInput(input: RecoveryInput): RecoveryInput {
53
+ if (
54
+ !input ||
55
+ (input.platform !== "ios" && input.platform !== "android") ||
56
+ typeof input.channel !== "string" ||
57
+ !input.channel.trim() ||
58
+ input.channel.length > 1_024 ||
59
+ !Object.hasOwn(recoveryWindows, input.window) ||
60
+ (input.releaseId !== undefined &&
61
+ (typeof input.releaseId !== "string" ||
62
+ !input.releaseId.trim() ||
63
+ input.releaseId.length > 1_024))
64
+ ) {
65
+ throw new Error("Choose a platform, channel, and time window.");
66
+ }
67
+ return input;
68
+ }
@@ -0,0 +1,21 @@
1
+ /** How long the server's `insights()` keeps rows, in days. */
2
+ export interface InsightsRetentionDays {
3
+ /** Raw events and hourly totals. */
4
+ readonly rawDays: number;
5
+ /** Daily totals, distributions, and each installation's latest report. */
6
+ readonly dailyDays: number;
7
+ }
8
+
9
+ /** `insights()`'s defaults, for a server that does not report its periods. */
10
+ export const DEFAULT_INSIGHTS_RETENTION: InsightsRetentionDays = {
11
+ rawDays: 90,
12
+ dailyDays: 400,
13
+ };
14
+
15
+ export const formatDays = (days: number): string =>
16
+ `${days.toLocaleString("en")} ${days === 1 ? "day" : "days"}`;
17
+
18
+ /** Whether daily totals cover App usage's 12-month period: 52 weeks. */
19
+ export const keepsTwelveMonths = ({
20
+ dailyDays,
21
+ }: InsightsRetentionDays): boolean => dailyDays >= 364;
@@ -1,13 +1,13 @@
1
1
  import type {
2
- ReportingOverview,
3
2
  InsightsScope,
3
+ ReportingOverview,
4
4
  InsightsEventPageInput,
5
5
  InsightsInstallationEventPageInput,
6
6
  ActiveInstallationWindow,
7
- InsightsProvider,
8
- } from "@hot-updater/server";
7
+ } from "@hot-updater/server/plugins/insights";
9
8
  import { createServerFn } from "@tanstack/react-start";
10
9
 
10
+ import { consoleAccess } from "./console-access";
11
11
  import type {
12
12
  InsightsInstallationViewRow,
13
13
  InsightsViewPage,
@@ -30,10 +30,14 @@ type InstallationsPageInput = {
30
30
  readonly limit: number;
31
31
  };
32
32
 
33
- const runtime = async (): Promise<InsightsProvider> => {
34
- const { prepareConfig } = await import("./server/config.server");
35
- const { hotUpdater } = await prepareConfig();
36
- return hotUpdater;
33
+ /** The Insights reads, once the console's access check and the feature guard pass. */
34
+ const insightsReads = async () => {
35
+ const [{ prepareConfig }, { requireFeature }] = await Promise.all([
36
+ import("./server/config.server"),
37
+ import("./server/runtime.server"),
38
+ ]);
39
+ const { runtime } = await prepareConfig();
40
+ return requireFeature(runtime, "insights");
37
41
  };
38
42
 
39
43
  const readOverview = (input: InsightsOverviewInput) => input;
@@ -44,27 +48,41 @@ const readInstallationPage = (input: InstallationsPageInput) => input;
44
48
  const readInstallId = (input: { readonly installId: string }) => input;
45
49
 
46
50
  export const getReportingInstallationsRpc = createServerFn({ method: "GET" })
51
+ .middleware([consoleAccess])
47
52
  .validator(readOverview)
48
- .handler(async ({ data }) => (await runtime()).getReportingOverview(data));
53
+ .handler(async ({ data }) =>
54
+ (await insightsReads()).getReportingOverview(data),
55
+ );
49
56
 
50
57
  export const listInsightsEventsRpc = createServerFn({ method: "GET" })
58
+ .middleware([consoleAccess])
51
59
  .validator(readEventsPage)
52
- .handler(async ({ data }) => (await runtime()).listEvents(data));
60
+ .handler(async ({ data }) => (await insightsReads()).listEvents(data));
53
61
 
54
62
  export const listInsightsInstallationEventsRpc = createServerFn({
55
63
  method: "GET",
56
64
  })
65
+ .middleware([consoleAccess])
57
66
  .validator(readInstallationEventsPage)
58
- .handler(async ({ data }) => (await runtime()).listInstallationEvents(data));
67
+ .handler(async ({ data }) =>
68
+ (await insightsReads()).listInstallationEvents(data),
69
+ );
59
70
 
60
71
  export const getInsightsInstallationRpc = createServerFn({ method: "GET" })
72
+ .middleware([consoleAccess])
61
73
  .validator(readInstallId)
62
- .handler(async ({ data }) => (await runtime()).getInstallation(data));
74
+ .handler(async ({ data }) => (await insightsReads()).getInstallation(data));
75
+
76
+ /** How long the server's `insights()` keeps rows. */
77
+ export const getInsightsRetentionRpc = createServerFn({ method: "GET" })
78
+ .middleware([consoleAccess])
79
+ .handler(async () => (await insightsReads()).getRetention());
63
80
 
64
81
  export const findInsightsInstallationsRpc = createServerFn({ method: "GET" })
82
+ .middleware([consoleAccess])
65
83
  .validator(readInstallationPage)
66
84
  .handler(async ({ data }) => {
67
- const insights = await runtime();
85
+ const insights = await insightsReads();
68
86
  const install = data.cursor
69
87
  ? null
70
88
  : await insights.getInstallation({ installId: data.identity });
@@ -0,0 +1,58 @@
1
+ import type { InsightsWindow } from "./insights-api";
2
+ import type { AppUsageScope, UsageWindow } from "./insights-usage";
3
+
4
+ const readText = (value: unknown) =>
5
+ typeof value === "string" && value.trim().length > 0 && value.length <= 1024
6
+ ? value
7
+ : undefined;
8
+ const readWindow = (value: unknown): InsightsWindow | undefined =>
9
+ value === "24h" || value === "7d" || value === "30d" ? value : undefined;
10
+ const readUsageWindow = (value: unknown): UsageWindow | undefined =>
11
+ value === "12m" ? value : readWindow(value);
12
+
13
+ export type InsightsSearch = {
14
+ platform?: AppUsageScope["platform"];
15
+ channel?: string;
16
+ appVersion?: string;
17
+ window?: UsageWindow;
18
+ bundleWindow?: InsightsWindow;
19
+ healthPlatform?: "ios" | "android";
20
+ healthChannel?: string;
21
+ releaseId?: string;
22
+ };
23
+
24
+ export function validateInsightsSearch(
25
+ search: Record<string, unknown>,
26
+ ): InsightsSearch {
27
+ return {
28
+ platform:
29
+ search.platform === "all" ||
30
+ search.platform === "ios" ||
31
+ search.platform === "android"
32
+ ? search.platform
33
+ : undefined,
34
+ channel: readText(search.channel),
35
+ appVersion: readText(search.appVersion),
36
+ window: readUsageWindow(search.window),
37
+ bundleWindow: readWindow(search.bundleWindow),
38
+ healthPlatform:
39
+ search.healthPlatform === "ios" || search.healthPlatform === "android"
40
+ ? search.healthPlatform
41
+ : undefined,
42
+ healthChannel: readText(search.healthChannel),
43
+ releaseId: readText(search.releaseId),
44
+ };
45
+ }
46
+
47
+ export function validateDistributionSearch(search: Record<string, unknown>) {
48
+ return {
49
+ ...validateInsightsSearch(search),
50
+ version: readText(search.version),
51
+ page:
52
+ typeof search.page === "number" &&
53
+ Number.isSafeInteger(search.page) &&
54
+ search.page > 0
55
+ ? search.page
56
+ : undefined,
57
+ };
58
+ }