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

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 (285) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/ConsoleFeatureUnavailable-D0LPL1Gy.js +1 -0
  3. package/.output/public/assets/HashValueDisplay-BmuB4FW-.js +1 -0
  4. package/.output/public/assets/InsightsInfo--v4SC07c.js +1 -0
  5. package/.output/public/assets/InsightsPageHeader-dU83CAQY.js +1 -0
  6. package/.output/public/assets/NotFoundPage-B5mhGG2a.js +1 -0
  7. package/.output/public/assets/api-D-vOVlQ4.js +1 -0
  8. package/.output/public/assets/api-keys-Csh8ARKU.js +1 -0
  9. package/.output/public/assets/api-keys-OMPUoA_A.js +1 -0
  10. package/.output/public/assets/arrow-left-Bwq9X1eS.js +1 -0
  11. package/.output/public/assets/arrow-up-right-BkOVn4if.js +1 -0
  12. package/.output/public/assets/card-C_7d6O9a.js +1 -0
  13. package/.output/public/assets/console-features-api-C1jRZTGw.js +1 -0
  14. package/.output/public/assets/dialog-DE8wFN_P.js +1 -0
  15. package/.output/public/assets/dist-C7ENGzpS.js +1 -0
  16. package/.output/public/assets/empty-CVd5NlQD.js +1 -0
  17. package/.output/public/assets/field-C06q1iC9.js +1 -0
  18. package/.output/public/assets/index-BeWlQtO6.js +11 -0
  19. package/.output/public/assets/input-group-Bi61biIA.js +1 -0
  20. package/.output/public/assets/insights-BjnAtC0f.js +2 -0
  21. package/.output/public/assets/insights-D86m11yH.js +42 -0
  22. package/.output/public/assets/insights-DpYstMoY.js +1 -0
  23. package/.output/public/assets/insights-recovery-rpc-BIm5_ALc.js +1 -0
  24. package/.output/public/assets/insights-search-KIF7mRtB.js +1 -0
  25. package/.output/public/assets/insights-usage-rpc-VHj8vyBt.js +1 -0
  26. package/.output/public/assets/insights_.distribution-ADcrX48c.js +2 -0
  27. package/.output/public/assets/insights_.distribution-CuuJUN6Y.js +1 -0
  28. package/.output/public/assets/insights_.distribution-DJRjuXsO.js +1 -0
  29. package/.output/public/assets/installations-BSBBzqWs.js +2 -0
  30. package/.output/public/assets/installations-DqIE6WcO.js +1 -0
  31. package/.output/public/assets/installations-UsELbDvw.js +1 -0
  32. package/.output/public/assets/loader-circle-CnCBxzHp.js +1 -0
  33. package/.output/public/assets/mutation-Csp7xUNh.js +1 -0
  34. package/.output/public/assets/preload-helper-B-cOmpLH.js +1 -0
  35. package/.output/public/assets/refresh-cw-FF1NtzlQ.js +1 -0
  36. package/.output/public/assets/routes-CoN9XHP_.js +2 -0
  37. package/.output/public/assets/routes-DT1jB0Zi.js +1 -0
  38. package/.output/public/assets/sidebar-D8mJRF15.js +3 -0
  39. package/.output/public/assets/styles-Bwt7v6U5.css +2 -0
  40. package/.output/public/assets/table-WBgfvgXg.js +1 -0
  41. package/.output/public/assets/useCompositeListItem-CloBmDwn.js +1 -0
  42. package/.output/public/assets/useMutation-DO8cvsTx.js +1 -0
  43. package/.output/public/assets/useOpenInteractionType-ReGr9GzJ.js +1 -0
  44. package/.output/public/assets/valueToPercent-BvPAi4bP.js +1 -0
  45. package/.output/server/{__23tanstack-start-server-fn-resolver-B7ioJhtE.mjs → __23tanstack-start-server-fn-resolver-DMRD4e4f.mjs} +51 -47
  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 +3803 -4023
  49. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +141 -98
  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__history.mjs +68 -41
  67. package/.output/server/_libs/tanstack__query-core.mjs +1618 -309
  68. package/.output/server/_libs/tanstack__react-query.mjs +262 -22
  69. package/.output/server/_libs/verkit.mjs +99 -1
  70. package/.output/server/_runtime.mjs +10 -1
  71. package/.output/server/_ssr/ConsoleFeatureUnavailable-Du7jNPNS.mjs +58 -0
  72. package/.output/server/_ssr/{HashValueDisplay-DwpGLfrl.mjs → HashValueDisplay-GQvFkXoi.mjs} +3 -3
  73. package/.output/server/_ssr/InsightsInfo-Do7QSaFR.mjs +37 -0
  74. package/.output/server/_ssr/InsightsPageHeader-DSUWUWRc.mjs +154 -0
  75. package/.output/server/_ssr/NotFoundPage-DsF4Vrhg.mjs +37 -0
  76. package/.output/server/_ssr/api-BEiVs0H7.mjs +203 -0
  77. package/.output/server/_ssr/api-keys-BrvBWGRf.mjs +5 -0
  78. package/.output/server/_ssr/{api-keys-CZeTPQys.mjs → api-keys-K5v32nf7.mjs} +49 -9
  79. package/.output/server/_ssr/api-keys-rpc-B96bBllm.mjs +52 -0
  80. package/.output/server/_ssr/api-rpc-DcUe280B.mjs +296 -0
  81. package/.output/server/_ssr/{auth-rpc-DUaeOWKK.mjs → auth-rpc-C68W56DP.mjs} +6 -6
  82. package/.output/server/_ssr/auth.server-DM1J6NwR.mjs +5712 -0
  83. package/.output/server/_ssr/bundleActivity-CpuY9CTP.mjs +19 -0
  84. package/.output/server/_ssr/card-CPAEKx0w.mjs +49 -0
  85. package/.output/server/_ssr/config-nsIcsHh8.mjs +15 -0
  86. package/.output/server/_ssr/config.server-DDCZk3Sq.mjs +36 -0
  87. package/.output/server/_ssr/console-access-Da-2Qd7q.mjs +17 -0
  88. package/.output/server/_ssr/console-features-Dzu2iUGD.mjs +55 -0
  89. package/.output/server/_ssr/console-features-api-xAsSgFa_.mjs +43 -0
  90. package/.output/server/_ssr/console-features-rpc-qARGpB24.mjs +19 -0
  91. package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-BdBf_ezN.mjs} +67 -56
  92. package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-B_y2lrwA.mjs} +2 -2
  93. package/.output/server/_ssr/{databaseJsonValue-d6I3nAS4.mjs → databaseJsonValue-Bhxciy7Z.mjs} +28 -5
  94. package/.output/server/_ssr/databaseRows-DwMAjaF8.mjs +74 -0
  95. package/.output/server/_ssr/{dialog-DS-nw-pA.mjs → dialog-q__BJ7mQ.mjs} +2 -2
  96. package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-Cxo9ghYv.mjs} +140 -6
  97. package/.output/server/_ssr/{downloadBundle-CNnYxYO2.mjs → downloadBundle-BW9I-pMQ.mjs} +10 -10
  98. package/.output/server/_ssr/empty-DcBTAooP.mjs +49 -0
  99. package/.output/server/_ssr/{empty-plugin-adapters-B5QvGpEl.mjs → empty-plugin-adapters-CYdinE-i.mjs} +1 -1
  100. package/.output/server/_ssr/{table-CnZmon_Z.mjs → field-QCDBVpXA.mjs} +63 -51
  101. package/.output/server/_ssr/getBundleChildren-DIAGCF4a.mjs +22 -0
  102. package/.output/server/_ssr/{input-group-BpsnmvZw.mjs → input-group-hCk2nUoU.mjs} +2 -2
  103. package/.output/server/_ssr/insights-BfBO0RFr.mjs +1238 -0
  104. package/.output/server/_ssr/insights-DsAU3-TB.mjs +14 -0
  105. package/.output/server/_ssr/insights-RIfg447W.mjs +5 -0
  106. package/.output/server/_ssr/insights-api-CDn-QQHU.mjs +105 -0
  107. package/.output/server/_ssr/insights-failures-BBGMhuUr.mjs +44 -0
  108. package/.output/server/_ssr/insights-failures-rpc-rA2dctK2.mjs +21 -0
  109. package/.output/server/_ssr/insights-recovery-Dpk6Mfb9.mjs +28 -0
  110. package/.output/server/_ssr/insights-recovery-rpc-BCaF-xSd.mjs +46 -0
  111. package/.output/server/_ssr/insights-recovery-rpc-CcgYyUHk.mjs +20 -0
  112. package/.output/server/_ssr/{insights-rpc-38DujK0G.mjs → insights-rpc-keycmwzw.mjs} +22 -14
  113. package/.output/server/_ssr/insights-search-BRg5VsNe.mjs +25 -0
  114. package/.output/server/_ssr/insights-usage-cbw9n2Fq.mjs +29 -0
  115. package/.output/server/_ssr/insights-usage-rpc-BKgV1QDf.mjs +29 -0
  116. package/.output/server/_ssr/insights-usage-rpc-D-JYachy.mjs +41 -0
  117. package/.output/server/_ssr/insightsRecovery-B7oMc8d8.mjs +45 -0
  118. package/.output/server/_ssr/insightsUsage-Oa7kk5ek.mjs +56 -0
  119. package/.output/server/_ssr/insights_.distribution-BIqOqRW0.mjs +335 -0
  120. package/.output/server/_ssr/insights_.distribution-CuUrDa7P.mjs +5 -0
  121. package/.output/server/_ssr/insights_.distribution-RT_6x_ud.mjs +14 -0
  122. package/.output/server/_ssr/{installations-DiVKEF5T.mjs → installations-BO5CjX2z.mjs} +517 -54
  123. package/.output/server/_ssr/installations-BjaHiIfh.mjs +5 -0
  124. package/.output/server/_ssr/installations-Dt5luA-N.mjs +69 -0
  125. package/.output/server/_ssr/listReleases-BitHzOYB.mjs +79 -0
  126. package/.output/server/_ssr/releaseManagement-B3e7I8J9.mjs +11 -0
  127. package/.output/server/_ssr/{router-B9wG_9f8.mjs → router-DfVAixxe.mjs} +135 -139
  128. package/.output/server/_ssr/{routes-D7Yh_jfj.mjs → routes-C6LstNp8.mjs} +176 -455
  129. package/.output/server/_ssr/routes-Q3Uhmwwz.mjs +292 -0
  130. package/.output/server/_ssr/runtime.server-fL0o6I2b.mjs +1459 -0
  131. package/.output/server/_ssr/schema-BKY9SqaY.mjs +25 -0
  132. package/.output/server/_ssr/select-CUlInmjk.mjs +107 -0
  133. package/.output/server/_ssr/server-C43wq9ft.mjs +8330 -0
  134. package/.output/server/_ssr/{createSsrRpc-DjWNJ0uc.mjs → sidebar-DKbx2ach.mjs} +117 -63
  135. package/.output/server/_ssr/ssr.mjs +1 -1
  136. package/.output/server/_ssr/start-BXUOv_n8.mjs +4 -0
  137. package/.output/server/_ssr/table-NGWV8B-S.mjs +53 -0
  138. package/.output/server/_ssr/updateFailures-Jwe2Baab.mjs +24 -0
  139. package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-C8bwiZr8.mjs} +2 -2
  140. package/.output/server/_tanstack-start-manifest_v-WQ5qGn8n.mjs +124 -0
  141. package/.output/server/index.mjs +267 -162
  142. package/LICENSE +13 -14
  143. package/README.md +82 -72
  144. package/lib/index.d.mts +23 -11
  145. package/lib/vite-internal.mjs +13 -3
  146. package/lib/vite.d.mts +2 -0
  147. package/package.json +14 -14
  148. package/src/components/AppSidebar.tsx +100 -71
  149. package/src/components/ConsoleAccessPage.tsx +5 -1
  150. package/src/components/ConsoleFeatureUnavailable.tsx +80 -0
  151. package/src/components/NotFoundPage.tsx +7 -7
  152. package/src/components/features/FeatureSlots.tsx +99 -0
  153. package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
  154. package/src/components/features/bundles/BundleMetadata.tsx +8 -5
  155. package/src/components/features/bundles/RolloutCohortsDialog.tsx +1 -1
  156. package/src/components/features/channels/ChannelManagementDialog.tsx +2 -5
  157. package/src/components/features/insights/AppUsage.tsx +370 -0
  158. package/src/components/features/insights/EstimatedCount.tsx +18 -0
  159. package/src/components/features/insights/EventDetails.tsx +117 -37
  160. package/src/components/features/insights/EventHistoryCard.tsx +112 -26
  161. package/src/components/features/insights/EventHistoryList.tsx +2 -2
  162. package/src/components/features/insights/InsightsControls.tsx +265 -102
  163. package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
  164. package/src/components/features/insights/InsightsInfo.tsx +48 -0
  165. package/src/components/features/insights/InsightsOverview.tsx +176 -173
  166. package/src/components/features/insights/InsightsPageHeader.tsx +35 -20
  167. package/src/components/features/insights/InsightsPeriodSelector.tsx +89 -0
  168. package/src/components/features/insights/InstallationHistoryCard.tsx +52 -10
  169. package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
  170. package/src/components/features/insights/ReleaseActivity.tsx +188 -0
  171. package/src/components/features/insights/ReportingDevicesSummary.tsx +65 -0
  172. package/src/components/features/insights/UpdateFailures.tsx +319 -0
  173. package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
  174. package/src/components/features/releases/ReleaseEditorSheet.tsx +30 -7
  175. package/src/components/ui/card.tsx +1 -1
  176. package/src/components/ui/empty.tsx +101 -0
  177. package/src/components/ui/scroll-area.tsx +52 -0
  178. package/src/components/ui/sidebar.tsx +22 -29
  179. package/src/components/ui/tabs.tsx +81 -0
  180. package/src/index.ts +26 -19
  181. package/src/lib/api-keys-api.ts +0 -28
  182. package/src/lib/api-keys-rpc.ts +27 -38
  183. package/src/lib/api-rpc.ts +189 -266
  184. package/src/lib/api.ts +19 -102
  185. package/src/lib/bundle-activity.ts +31 -0
  186. package/src/lib/console-access.ts +20 -0
  187. package/src/lib/console-features-api.ts +68 -0
  188. package/src/lib/console-features-rpc.ts +13 -0
  189. package/src/lib/console-features.ts +91 -0
  190. package/src/lib/insights-api.ts +37 -1
  191. package/src/lib/insights-failures-rpc.ts +21 -0
  192. package/src/lib/insights-failures.ts +129 -0
  193. package/src/lib/insights-recovery-rpc.ts +51 -0
  194. package/src/lib/insights-recovery.ts +68 -0
  195. package/src/lib/insights-retention.ts +21 -0
  196. package/src/lib/insights-rpc.ts +30 -12
  197. package/src/lib/insights-search.ts +58 -0
  198. package/src/lib/insights-usage-rpc.ts +29 -0
  199. package/src/lib/insights-usage.ts +74 -0
  200. package/src/lib/insights-view.ts +38 -2
  201. package/src/lib/scroll-restoration.ts +21 -0
  202. package/src/lib/server/adminInsights.ts +124 -0
  203. package/src/lib/server/auth.server.ts +13 -5
  204. package/src/lib/server/bundleActivity.ts +37 -0
  205. package/src/lib/server/config.server.ts +21 -74
  206. package/src/lib/server/console-feature-apis.ts +59 -0
  207. package/src/lib/server/console-runtime.server.ts +82 -38
  208. package/src/lib/server/downloadBundle.ts +16 -15
  209. package/src/lib/server/getBundleChildren.ts +31 -122
  210. package/src/lib/server/insightsRecovery.ts +55 -0
  211. package/src/lib/server/insightsUsage.ts +63 -0
  212. package/src/lib/server/listReleases.ts +109 -44
  213. package/src/lib/server/releaseReachability.ts +8 -7
  214. package/src/lib/server/runtime.server.ts +120 -31
  215. package/src/lib/server/updateFailures.ts +31 -0
  216. package/src/routeTree.gen.ts +44 -44
  217. package/src/router.tsx +3 -2
  218. package/src/routes/-installations-search.ts +32 -8
  219. package/src/routes/-releases-search.ts +76 -29
  220. package/src/routes/__root.tsx +10 -4
  221. package/src/routes/api/bundles/$bundleId/download.ts +2 -5
  222. package/src/routes/api-keys.tsx +5 -2
  223. package/src/routes/index.tsx +167 -104
  224. package/src/routes/insights.tsx +100 -59
  225. package/src/routes/insights_.distribution.tsx +398 -0
  226. package/src/routes/installations.tsx +98 -41
  227. package/src/vite-internal.ts +15 -3
  228. package/src/vite.ts +2 -0
  229. package/.output/public/assets/HashValueDisplay-CB0Eskit.js +0 -1
  230. package/.output/public/assets/InsightsPageHeader-Car5LNiZ.js +0 -1
  231. package/.output/public/assets/api-keys-BzKocqRy.js +0 -1
  232. package/.output/public/assets/api-keys-api-BHcODB7q.js +0 -1
  233. package/.output/public/assets/card-Dhr_eFYj.js +0 -1
  234. package/.output/public/assets/chart-no-axes-combined-Dy4WJv2U.js +0 -1
  235. package/.output/public/assets/composite-Bp98A0Ld.js +0 -1
  236. package/.output/public/assets/createServerFn-CA8fs7QD.js +0 -3
  237. package/.output/public/assets/dialog-CUTrcPlB.js +0 -1
  238. package/.output/public/assets/dist-C_NydL0e.js +0 -1
  239. package/.output/public/assets/index-DdAz-CXv.js +0 -10
  240. package/.output/public/assets/input-group-cSOG4wh_.js +0 -1
  241. package/.output/public/assets/insights-C0PBSZ-n.js +0 -1
  242. package/.output/public/assets/installations-CE33wgbY.js +0 -2
  243. package/.output/public/assets/installations-CStZja8x.js +0 -1
  244. package/.output/public/assets/lazyRouteComponent-Ccp3bwBY.js +0 -1
  245. package/.output/public/assets/link-CI8idAUi.js +0 -1
  246. package/.output/public/assets/loader-circle-BoxLrsOu.js +0 -1
  247. package/.output/public/assets/preload-helper-CQ36nxok.js +0 -1
  248. package/.output/public/assets/refresh-cw-Bqc72UZG.js +0 -1
  249. package/.output/public/assets/routes-DGVXdzpw.js +0 -2
  250. package/.output/public/assets/routes-DTlwOr8I.js +0 -1
  251. package/.output/public/assets/shield-check-uK3MHWJZ.js +0 -1
  252. package/.output/public/assets/signing-CMf4aqDO.js +0 -1
  253. package/.output/public/assets/styles-BCvbtKsm.css +0 -2
  254. package/.output/public/assets/table-2vcV89QB.js +0 -1
  255. package/.output/public/assets/useCompositeListItem-CHLITE_4.js +0 -1
  256. package/.output/public/assets/x-DCd0j8c6.js +0 -1
  257. package/.output/server/_ssr/InsightsPageHeader-Df4Brj8u.mjs +0 -484
  258. package/.output/server/_ssr/api-keys-api-BUPQVSpk.mjs +0 -60
  259. package/.output/server/_ssr/api-keys-rpc-Cs7tS5_j.mjs +0 -136
  260. package/.output/server/_ssr/api-rpc-tAupfGQm.mjs +0 -1399
  261. package/.output/server/_ssr/auth.server-DW71WzsD.mjs +0 -47
  262. package/.output/server/_ssr/bundle-signing-rpc-BhnBw0-d.mjs +0 -16
  263. package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +0 -12
  264. package/.output/server/_ssr/card-BRiPmi0C.mjs +0 -108
  265. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
  266. package/.output/server/_ssr/config.server-afDuAwaG.mjs +0 -368
  267. package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
  268. package/.output/server/_ssr/deleteBundle-BTGwBgzx.mjs +0 -66
  269. package/.output/server/_ssr/getBundleChildren-C0vh4ZvE.mjs +0 -50
  270. package/.output/server/_ssr/insights-BdS364Xu.mjs +0 -460
  271. package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +0 -303
  272. package/.output/server/_ssr/installations-CtA59MQG.mjs +0 -29
  273. package/.output/server/_ssr/routes-CKrrS2q2.mjs +0 -37
  274. package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +0 -438
  275. package/.output/server/_ssr/server-D_C2rMzf.mjs +0 -7468
  276. package/.output/server/_ssr/signing-Dz1WkZOf.mjs +0 -133
  277. package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
  278. package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +0 -97
  279. package/src/components/features/insights/InsightsBundleReports.tsx +0 -51
  280. package/src/components/features/signing/BundleSigningPage.tsx +0 -139
  281. package/src/lib/bundle-signing-api.ts +0 -14
  282. package/src/lib/bundle-signing-rpc.ts +0 -16
  283. package/src/lib/server/bundle-signing.server.ts +0 -23
  284. package/src/lib/server/deleteBundle.ts +0 -145
  285. package/src/routes/signing.tsx +0 -37
@@ -0,0 +1,1459 @@
1
+ import { i as resolveConsoleFeatures, n as consoleFeatures, t as ConsoleFeatureUnavailableError } from "./console-features-Dzu2iUGD.mjs";
2
+ import { C as isUUIDv7 } from "./dist-Cxo9ghYv.mjs";
3
+ import { i as isDatabaseJsonObject, t as DatabaseAdapterInputError } from "./databaseJsonValue-Bhxciy7Z.mjs";
4
+ import { t as createUUIDv7 } from "./uuidv7-C8bwiZr8.mjs";
5
+ import { n as defineTable, r as isRemoteDatabase, t as defineAggregate } from "./schema-BKY9SqaY.mjs";
6
+ //#region node_modules/.nitro/vite/services/ssr/assets/runtime.server-fL0o6I2b.js
7
+ var isOptional = (value, key, valid) => !Object.hasOwn(value, key) || valid(value[key]);
8
+ var isString = (value) => typeof value === "string";
9
+ /** An `UPDATE_FAILED` report's stored failure: a stage and a reason, and what else the client knew. */
10
+ var isDatabaseBundleEventFailure = (value) => isDatabaseJsonObject(value) && isString(value.stage) && isString(value.reason) && isOptional(value, "resource", isString) && isOptional(value, "http_status", (status) => Number.isSafeInteger(status)) && isOptional(value, "transport", isString) && isOptional(value, "origin_code", isString) && isOptional(value, "previous_process_exit", isString);
11
+ var isDatabaseBundleEventMetadata = (value) => isDatabaseJsonObject(value) && typeof value.cohort === "string" && (value.update_strategy === null || value.update_strategy === "fingerprint" || value.update_strategy === "appVersion") && (value.fingerprint_hash === null || typeof value.fingerprint_hash === "string") && (value.sdk_version === null || typeof value.sdk_version === "string") && isOptional(value, "failure", isDatabaseBundleEventFailure) && isOptional(value, "delivery", isString) && isOptional(value, "patch_fallback", (flag) => typeof flag === "boolean") && isOptional(value, "previous_process_exit", isString);
12
+ var isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13
+ var isIdentity$1 = (value) => typeof value === "string" && value.length > 0 && value.length <= 255;
14
+ var isTextOrNull = (value) => value === null || typeof value === "string";
15
+ /** Each bundle event field and what it holds. */
16
+ var BUNDLE_EVENT_FIELDS = {
17
+ id: (value) => typeof value === "string",
18
+ type: (value) => value === "UPDATE_DOWNLOADED" || value === "UPDATE_APPLIED" || value === "RECOVERED" || value === "UPDATE_FAILED" || value === "UNCHANGED",
19
+ install_id: isIdentity$1,
20
+ user_id: (value) => value === null || isIdentity$1(value),
21
+ from_bundle_id: isTextOrNull,
22
+ from_release_id: isTextOrNull,
23
+ to_release_id: isTextOrNull,
24
+ to_bundle_id: (value) => typeof value === "string",
25
+ platform: (value) => value === "ios" || value === "android",
26
+ app_version: (value) => typeof value === "string",
27
+ channel: (value) => typeof value === "string",
28
+ metadata: isDatabaseBundleEventMetadata,
29
+ received_at_ms: (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0
30
+ };
31
+ var updateStrategyOf = (row) => isRecord$3(row.metadata) ? row.metadata.update_strategy : void 0;
32
+ /**
33
+ * A movement, or a failed update, names the bundle it left and how the
34
+ * device updates, and a failed update says what failed; an unchanged report
35
+ * names neither.
36
+ */
37
+ var hasEventInvariants = (row) => (row.type === "UPDATE_DOWNLOADED" || row.type === "UPDATE_APPLIED" || row.type === "RECOVERED" || row.type === "UPDATE_FAILED") && typeof row.from_bundle_id === "string" && (updateStrategyOf(row) === "fingerprint" || updateStrategyOf(row) === "appVersion") && (row.type !== "UPDATE_FAILED" || isRecord$3(row.metadata) && isRecord$3(row.metadata.failure)) || row.type === "UNCHANGED" && row.from_bundle_id === null && updateStrategyOf(row) === null;
38
+ /**
39
+ * Throws unless `row` has exactly the bundle event fields, each of its type,
40
+ * and the invariants of its event type: `invalid-field` for an unknown field,
41
+ * `invalid-data` for anything else.
42
+ */
43
+ var validateBundleEventFields = (row) => {
44
+ if (!isRecord$3(row)) throw new DatabaseAdapterInputError("invalid-data");
45
+ for (const field of Object.keys(row)) if (!Object.hasOwn(BUNDLE_EVENT_FIELDS, field)) throw new DatabaseAdapterInputError("invalid-field");
46
+ for (const [field, valid] of Object.entries(BUNDLE_EVENT_FIELDS)) if (!Object.hasOwn(row, field) || !valid(row[field])) throw new DatabaseAdapterInputError("invalid-data");
47
+ if (!hasEventInvariants(row)) throw new DatabaseAdapterInputError("invalid-data");
48
+ };
49
+ var encoder = new TextEncoder();
50
+ /** Exact UTF-8 byte ordering, without case folding or Unicode normalization. */
51
+ var compareInsightsText = (left, right) => {
52
+ const a = encoder.encode(left);
53
+ const b = encoder.encode(right);
54
+ for (let index = 0; index < Math.min(a.length, b.length); index += 1) if (a[index] !== b[index]) return a[index] - b[index];
55
+ return a.length - b.length;
56
+ };
57
+ var isWellFormedText = (value) => {
58
+ for (const character of value) {
59
+ const point = character.codePointAt(0);
60
+ if (point >= 55296 && point <= 57343) return false;
61
+ }
62
+ return true;
63
+ };
64
+ var isIdentity = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 && isWellFormedText(value);
65
+ var isText = (value) => typeof value === "string" && value.length > 0 && isWellFormedText(value);
66
+ var isTimestamp = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
67
+ var isLimit = (value) => isTimestamp(value) && value >= 1 && value <= 101;
68
+ var hasOnlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
69
+ var hasScope = (value) => (value.platform === "ios" || value.platform === "android") && isText(value.channel);
70
+ var isBundleFilter = (value, withKind = false) => {
71
+ if (!isRecord$3(value) || !hasScope(value)) return false;
72
+ const keys = [
73
+ "platform",
74
+ "channel",
75
+ "type",
76
+ ...withKind ? ["kind"] : []
77
+ ];
78
+ return value.type === "RECOVERED" ? isText(value.fromBundleId) && hasOnlyKeys(value, [...keys, "fromBundleId"]) : (value.type === "UPDATE_DOWNLOADED" || value.type === "UPDATE_APPLIED" || value.type === "UPDATE_FAILED") && isText(value.toBundleId) && hasOnlyKeys(value, [...keys, "toBundleId"]);
79
+ };
80
+ var isEventFilter = (value) => {
81
+ if (!isRecord$3(value)) return false;
82
+ if (value.kind === "all") return hasOnlyKeys(value, ["kind"]);
83
+ if (value.kind === "installationMovement") return isIdentity(value.installId) && hasOnlyKeys(value, ["kind", "installId"]);
84
+ return value.kind === "bundle" && isBundleFilter(value, true);
85
+ };
86
+ var validateRow = (model, row, result = false) => {
87
+ try {
88
+ validateBundleEventFields(row);
89
+ if (!isRecord$3(row) || typeof row.id !== "string" || !isUUIDv7(row.id) || [...Object.values(row), ...isRecord$3(row.metadata) ? Object.values(row.metadata) : []].some((value) => typeof value === "string" && !isWellFormedText(value))) throw new DatabaseAdapterInputError("invalid-data");
90
+ } catch (error) {
91
+ if (result) throw new DatabaseAdapterInputError("invalid-result");
92
+ throw error;
93
+ }
94
+ };
95
+ /** Downloads, applies, recoveries, and failed updates belong in installation history. */
96
+ var isInsightsMovementEvent = (event) => event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_APPLIED" || event.type === "RECOVERED" || event.type === "UPDATE_FAILED";
97
+ var matchesInsightsEventFilter = (event, filter) => {
98
+ if (filter.kind === "all") return true;
99
+ if (filter.kind === "installationMovement") return event.install_id === filter.installId && isInsightsMovementEvent(event);
100
+ return event.type === filter.type && event.platform === filter.platform && event.channel === filter.channel && (filter.type === "RECOVERED" ? event.from_bundle_id === filter.fromBundleId : event.to_bundle_id === filter.toBundleId && event.metadata.failure?.stage !== "check");
101
+ };
102
+ var invalidQuery = () => {
103
+ throw new DatabaseAdapterInputError("invalid-query");
104
+ };
105
+ var invalidResult = () => {
106
+ throw new DatabaseAdapterInputError("invalid-result");
107
+ };
108
+ var validateCount = (count) => isTimestamp(count) ? count : invalidResult();
109
+ var isTimeRange = (value) => isRecord$3(value) && hasOnlyKeys(value, ["start", "end"]) && isTimestamp(value.start) && isTimestamp(value.end) && value.start < value.end;
110
+ var isReleaseReference = (value) => isRecord$3(value) && hasOnlyKeys(value, [
111
+ "releaseId",
112
+ "platform",
113
+ "channel"
114
+ ]) && isIdentity(value.releaseId) && hasScope(value);
115
+ var validateAggregateResult = (value) => {
116
+ const result = isRecord$3(value) ? value : invalidResult();
117
+ if (!isTimestamp(result.measuredAtMs)) invalidResult();
118
+ const coverage = result.coverage;
119
+ if (!isRecord$3(coverage) || coverage.kind !== "complete" && coverage.kind !== "partial" || coverage.sinceMs !== null && !isTimestamp(coverage.sinceMs)) invalidResult();
120
+ };
121
+ /** Validate custom and bundled providers at the same public boundary. */
122
+ var createValidatedInsightsModel = (model) => ({
123
+ async recordEvent(input) {
124
+ if (!isRecord$3(input) || !hasOnlyKeys(input, ["event"])) throw new DatabaseAdapterInputError("invalid-data");
125
+ validateRow("bundle_events", input.event);
126
+ await model.recordEvent(input);
127
+ },
128
+ async listEvents(input) {
129
+ if (!isRecord$3(input) || !hasOnlyKeys(input, [
130
+ "filter",
131
+ "sinceMs",
132
+ "beforeReceivedAtMs",
133
+ "after",
134
+ "limit"
135
+ ]) || !isEventFilter(input.filter) || !isLimit(input.limit) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs !== void 0 && !isTimestamp(input.sinceMs)) invalidQuery();
136
+ const sinceMs = input.sinceMs ?? 0;
137
+ if (sinceMs > input.beforeReceivedAtMs || input.after !== void 0 && (!isRecord$3(input.after) || !hasOnlyKeys(input.after, ["receivedAtMs", "id"]) || !isTimestamp(input.after.receivedAtMs) || typeof input.after.id !== "string" || !isUUIDv7(input.after.id) || input.after.receivedAtMs < sinceMs || input.after.receivedAtMs >= input.beforeReceivedAtMs)) invalidQuery();
138
+ const rows = await model.listEvents(input);
139
+ if (!Array.isArray(rows) || rows.length > input.limit) invalidResult();
140
+ let previous = input.after;
141
+ for (const row of rows) {
142
+ validateRow("bundle_events", row, true);
143
+ if (row.received_at_ms < sinceMs || row.received_at_ms >= input.beforeReceivedAtMs || !matchesInsightsEventFilter(row, input.filter) || previous !== void 0 && (row.received_at_ms > previous.receivedAtMs || row.received_at_ms === previous.receivedAtMs && row.id >= previous.id)) invalidResult();
144
+ previous = {
145
+ receivedAtMs: row.received_at_ms,
146
+ id: row.id
147
+ };
148
+ }
149
+ return rows;
150
+ },
151
+ async findLatestEvents(input) {
152
+ if (!isRecord$3(input)) invalidQuery();
153
+ if ("installId" in input) {
154
+ if (!hasOnlyKeys(input, ["installId"]) || !isIdentity(input.installId)) invalidQuery();
155
+ } else if (!hasOnlyKeys(input, [
156
+ "userId",
157
+ "afterInstallId",
158
+ "limit"
159
+ ]) || !isIdentity(input.userId) || !isLimit(input.limit) || input.afterInstallId !== void 0 && !isIdentity(input.afterInstallId)) invalidQuery();
160
+ const rows = await model.findLatestEvents(input);
161
+ if (!Array.isArray(rows) || rows.length > ("installId" in input ? 1 : input.limit)) invalidResult();
162
+ let previous = "installId" in input ? void 0 : input.afterInstallId;
163
+ for (const row of rows) {
164
+ validateRow("bundle_events", row, true);
165
+ if ("installId" in input ? row.install_id !== input.installId : row.user_id !== input.userId || previous !== void 0 && compareInsightsText(row.install_id, previous) <= 0) invalidResult();
166
+ previous = row.install_id;
167
+ }
168
+ return rows;
169
+ },
170
+ async countLatestEvents(input) {
171
+ if (!isRecord$3(input) || !hasOnlyKeys(input, [
172
+ "platform",
173
+ "channel",
174
+ "sinceMs",
175
+ "bundle"
176
+ ]) || !hasScope(input) || !isTimestamp(input.sinceMs) || input.bundle !== void 0 && (!Array.isArray(input.bundle) || input.bundle.length < 1 || input.bundle.length > 2 || input.bundle.some((bundle) => !isRecord$3(bundle) || !hasOnlyKeys(bundle, [
177
+ "field",
178
+ "value",
179
+ "types"
180
+ ]) || bundle.field !== "from_bundle_id" && bundle.field !== "to_bundle_id" || !isText(bundle.value) || !Array.isArray(bundle.types) || bundle.types.length === 0 || bundle.types.some((type) => ![
181
+ "UNCHANGED",
182
+ "UPDATE_DOWNLOADED",
183
+ "UPDATE_APPLIED",
184
+ "RECOVERED"
185
+ ].includes(type))))) invalidQuery();
186
+ return validateCount(await model.countLatestEvents(input));
187
+ },
188
+ async countEvents(input) {
189
+ if (!isRecord$3(input) || !hasOnlyKeys(input, [
190
+ "filter",
191
+ "sinceMs",
192
+ "beforeReceivedAtMs"
193
+ ]) || !isBundleFilter(input.filter) || !isTimestamp(input.sinceMs) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs > input.beforeReceivedAtMs) invalidQuery();
194
+ return validateCount(await model.countEvents(input));
195
+ },
196
+ async getReleaseActivity(input) {
197
+ if (!isRecord$3(input) || !hasOnlyKeys(input, [
198
+ "releases",
199
+ "scope",
200
+ "timeRange"
201
+ ])) invalidQuery();
202
+ const releases = input.releases;
203
+ const scope = input.scope;
204
+ const timeRange = input.timeRange;
205
+ const releaseQuery = Array.isArray(releases) && releases.length > 0 && releases.length <= 100 && releases.every(isReleaseReference) && scope === void 0 && (timeRange === void 0 || isTimeRange(timeRange));
206
+ const scopeQuery = releases === void 0 && isRecord$3(scope) && hasOnlyKeys(scope, ["platform", "channel"]) && hasScope(scope) && isTimeRange(timeRange);
207
+ if (!releaseQuery && !scopeQuery) invalidQuery();
208
+ const result = await model.getReleaseActivity(input);
209
+ validateAggregateResult(result);
210
+ if (!Array.isArray(result.data)) invalidResult();
211
+ return result;
212
+ },
213
+ async getAppUsage(input) {
214
+ if (!isRecord$3(input) || !hasOnlyKeys(input, [
215
+ "channel",
216
+ "platform",
217
+ "appVersion",
218
+ "timeRange",
219
+ "intervalMs"
220
+ ]) || !isText(input.channel) || ![
221
+ "all",
222
+ "ios",
223
+ "android"
224
+ ].includes(input.platform) || input.appVersion !== void 0 && !isText(input.appVersion) || !isTimeRange(input.timeRange) || !isTimestamp(input.intervalMs) || input.intervalMs < 36e5) invalidQuery();
225
+ const result = await model.getAppUsage(input);
226
+ validateAggregateResult(result);
227
+ if (!isTimestamp(result.activeInstallations) || !Array.isArray(result.points) || !Array.isArray(result.appVersions) || !Array.isArray(result.versions) || !Array.isArray(result.platforms) || !Array.isArray(result.bundleDistribution)) invalidResult();
228
+ return result;
229
+ }
230
+ });
231
+ /**
232
+ * The plugin's API as `InsightsModel`, validated at its boundary: what the
233
+ * Insights routes, the console, and the e2e harness read through.
234
+ */
235
+ var createInsightsModel = (api) => createValidatedInsightsModel({
236
+ recordEvent: ({ event }) => api.recordEvent(event),
237
+ listEvents: api.listEvents,
238
+ findLatestEvents: api.findLatestEvents,
239
+ countLatestEvents: api.countLatestEvents,
240
+ countEvents: api.countEvents,
241
+ getReleaseActivity: api.getReleaseActivity,
242
+ getAppUsage: api.getAppUsage
243
+ });
244
+ /**
245
+ * What Insights' API throws for an input it refuses, such as an unknown
246
+ * event field or a malformed cursor; its routes answer 400 for it.
247
+ */
248
+ var InsightsBadRequestError = class extends Error {
249
+ name = "InsightsBadRequestError";
250
+ };
251
+ var MAX_EVENT_STRING_LENGTH = 1024;
252
+ var MAX_IDENTITY_LENGTH$1 = 255;
253
+ function isRecord$2(value) {
254
+ return typeof value === "object" && value !== null && !Array.isArray(value);
255
+ }
256
+ function requireStringField(payload, key) {
257
+ const value = payload[key];
258
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_EVENT_STRING_LENGTH || new TextDecoder("utf-8", { ignoreBOM: true }).decode(new TextEncoder().encode(value)) !== value) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
259
+ return value;
260
+ }
261
+ function requireNullableStringField(payload, key) {
262
+ if (payload[key] === null) return null;
263
+ return requireStringField(payload, key);
264
+ }
265
+ function requireIdentityField(payload, key) {
266
+ const value = requireStringField(payload, key);
267
+ if (value.length > MAX_IDENTITY_LENGTH$1) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
268
+ return value;
269
+ }
270
+ /**
271
+ * The report's idempotency key, when the client sends one: it creates the
272
+ * UUIDv7 once and repeats it on every retry of that report.
273
+ */
274
+ function readEventId(payload) {
275
+ const value = payload.eventId;
276
+ if (value === void 0) return void 0;
277
+ if (!isUUIDv7(value)) throw new InsightsBadRequestError("Invalid event field: eventId");
278
+ return value;
279
+ }
280
+ /** One value of an open set: any other value reads as `unknown`. */
281
+ var oneOf = (values, value) => values.includes(value) ? value : "unknown";
282
+ var STAGES = [
283
+ "check",
284
+ "download",
285
+ "install"
286
+ ];
287
+ var REASONS = [
288
+ "network",
289
+ "http",
290
+ "invalid_response",
291
+ "hash_mismatch",
292
+ "signature",
293
+ "patch",
294
+ "extract",
295
+ "storage"
296
+ ];
297
+ var RESOURCES = [
298
+ "catalog",
299
+ "artifact",
300
+ "manifest",
301
+ "file",
302
+ "patch",
303
+ "archive"
304
+ ];
305
+ var TRANSPORTS = [
306
+ "timeout",
307
+ "dns",
308
+ "tls",
309
+ "connection",
310
+ "offline",
311
+ "cancelled"
312
+ ];
313
+ var DELIVERIES = [
314
+ "patch",
315
+ "manifest",
316
+ "archive"
317
+ ];
318
+ /** A storage error code or an Android exit reason: letters, digits, and `._-`. */
319
+ var CODE = /^[A-Za-z0-9._-]{1,64}$/;
320
+ var readCode = (value) => typeof value === "string" && CODE.test(value) ? value : void 0;
321
+ var readMetadata = (payload) => isRecord$2(payload.metadata) ? payload.metadata : {};
322
+ /**
323
+ * What an UPDATE_FAILED report says failed. It never refuses the report: an
324
+ * unknown value of a set reads as `unknown`, a missing or malformed failure
325
+ * as an unknown stage and reason, and a malformed optional field is left out,
326
+ * as is a null one.
327
+ */
328
+ function readFailure(value) {
329
+ const failure = isRecord$2(value) ? value : {};
330
+ const httpStatus = failure.httpStatus;
331
+ const originCode = readCode(failure.originCode);
332
+ const previousProcessExit = readCode(failure.previousProcessExit);
333
+ return {
334
+ stage: oneOf(STAGES, failure.stage),
335
+ reason: oneOf(REASONS, failure.reason),
336
+ ...failure.resource == null ? {} : { resource: oneOf(RESOURCES, failure.resource) },
337
+ ...Number.isSafeInteger(httpStatus) && httpStatus >= 100 && httpStatus <= 599 ? { httpStatus } : {},
338
+ ...failure.transport == null ? {} : { transport: oneOf(TRANSPORTS, failure.transport) },
339
+ ...originCode === void 0 ? {} : { originCode },
340
+ ...previousProcessExit === void 0 ? {} : { previousProcessExit }
341
+ };
342
+ }
343
+ function requireEvent(payload) {
344
+ if (!isRecord$2(payload)) throw new InsightsBadRequestError("Invalid event payload");
345
+ const platform = requireStringField(payload, "platform");
346
+ if (platform !== "ios" && platform !== "android") throw new InsightsBadRequestError("Invalid event field: platform");
347
+ const eventId = readEventId(payload);
348
+ const base = {
349
+ ...eventId === void 0 ? {} : { eventId },
350
+ installId: requireIdentityField(payload, "installId"),
351
+ toBundleId: requireStringField(payload, "toBundleId"),
352
+ ...payload.userId === void 0 ? {} : { userId: requireIdentityField(payload, "userId") },
353
+ platform,
354
+ appVersion: requireStringField(payload, "appVersion"),
355
+ channel: requireStringField(payload, "channel"),
356
+ cohort: requireStringField(payload, "cohort"),
357
+ fingerprintHash: requireNullableStringField(payload, "fingerprintHash"),
358
+ sdkVersion: payload.sdkVersion === void 0 ? null : requireNullableStringField(payload, "sdkVersion"),
359
+ fromReleaseId: requireNullableStringField(payload, "fromReleaseId"),
360
+ toReleaseId: requireNullableStringField(payload, "toReleaseId")
361
+ };
362
+ const type = requireStringField(payload, "type");
363
+ const movement = () => {
364
+ const updateStrategy = requireStringField(payload, "updateStrategy");
365
+ if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") throw new InsightsBadRequestError("Invalid event field: updateStrategy");
366
+ return {
367
+ ...base,
368
+ fromBundleId: requireStringField(payload, "fromBundleId"),
369
+ updateStrategy
370
+ };
371
+ };
372
+ const metadata = readMetadata(payload);
373
+ switch (type) {
374
+ case "UPDATE_DOWNLOADED": {
375
+ const delivery = metadata.delivery;
376
+ const read = {
377
+ ...delivery == null ? {} : { delivery: oneOf(DELIVERIES, delivery) },
378
+ ...metadata.patchFallback === true ? { patchFallback: true } : {}
379
+ };
380
+ return {
381
+ ...movement(),
382
+ type,
383
+ ...Object.keys(read).length === 0 ? {} : { metadata: read }
384
+ };
385
+ }
386
+ case "UPDATE_APPLIED": return {
387
+ ...movement(),
388
+ type
389
+ };
390
+ case "RECOVERED": {
391
+ const previousProcessExit = readCode(metadata.previousProcessExit);
392
+ return {
393
+ ...movement(),
394
+ type,
395
+ ...previousProcessExit === void 0 ? {} : { metadata: { previousProcessExit } }
396
+ };
397
+ }
398
+ case "UPDATE_FAILED": return {
399
+ ...movement(),
400
+ type,
401
+ metadata: { failure: readFailure(metadata.failure) }
402
+ };
403
+ case "UNCHANGED":
404
+ if (payload.fromBundleId !== null || payload.updateStrategy !== null) throw new InsightsBadRequestError("Invalid unchanged event shape");
405
+ return {
406
+ ...base,
407
+ type,
408
+ fromBundleId: null,
409
+ updateStrategy: null
410
+ };
411
+ default: throw new InsightsBadRequestError("Invalid event field: type");
412
+ }
413
+ }
414
+ function createBundleEventRow(input) {
415
+ input = requireEvent(input);
416
+ const base = {
417
+ app_version: input.appVersion,
418
+ channel: input.channel,
419
+ from_release_id: input.fromReleaseId,
420
+ id: input.eventId ?? createUUIDv7(),
421
+ install_id: input.installId,
422
+ platform: input.platform,
423
+ received_at_ms: Date.now(),
424
+ to_bundle_id: input.toBundleId,
425
+ to_release_id: input.toReleaseId,
426
+ user_id: input.userId ?? null
427
+ };
428
+ const metadata = {
429
+ cohort: input.cohort,
430
+ fingerprint_hash: input.fingerprintHash,
431
+ sdk_version: input.sdkVersion ?? null,
432
+ update_strategy: input.updateStrategy
433
+ };
434
+ switch (input.type) {
435
+ case "UPDATE_DOWNLOADED": {
436
+ const { delivery, patchFallback } = input.metadata ?? {};
437
+ return {
438
+ ...base,
439
+ from_bundle_id: input.fromBundleId,
440
+ type: input.type,
441
+ metadata: {
442
+ ...metadata,
443
+ ...delivery === void 0 ? {} : { delivery },
444
+ ...patchFallback ? { patch_fallback: true } : {}
445
+ }
446
+ };
447
+ }
448
+ case "UPDATE_APPLIED": return {
449
+ ...base,
450
+ from_bundle_id: input.fromBundleId,
451
+ type: input.type,
452
+ metadata
453
+ };
454
+ case "RECOVERED": {
455
+ const exit = input.metadata?.previousProcessExit;
456
+ return {
457
+ ...base,
458
+ from_bundle_id: input.fromBundleId,
459
+ type: input.type,
460
+ metadata: {
461
+ ...metadata,
462
+ ...exit === void 0 ? {} : { previous_process_exit: exit }
463
+ }
464
+ };
465
+ }
466
+ case "UPDATE_FAILED": {
467
+ const { failure } = input.metadata;
468
+ return {
469
+ ...base,
470
+ from_bundle_id: input.fromBundleId,
471
+ type: input.type,
472
+ metadata: {
473
+ ...metadata,
474
+ failure: {
475
+ stage: failure.stage,
476
+ reason: failure.reason,
477
+ ...failure.resource === void 0 ? {} : { resource: failure.resource },
478
+ ...failure.httpStatus === void 0 ? {} : { http_status: failure.httpStatus },
479
+ ...failure.transport === void 0 ? {} : { transport: failure.transport },
480
+ ...failure.originCode === void 0 ? {} : { origin_code: failure.originCode },
481
+ ...failure.previousProcessExit === void 0 ? {} : { previous_process_exit: failure.previousProcessExit }
482
+ }
483
+ }
484
+ };
485
+ }
486
+ case "UNCHANGED": return {
487
+ ...base,
488
+ from_bundle_id: null,
489
+ type: input.type,
490
+ metadata
491
+ };
492
+ }
493
+ }
494
+ var DEFAULT_PAGE_LIMIT = 50;
495
+ var MAX_PAGE_LIMIT = 100;
496
+ var MAX_EVENT_ID_LENGTH = 1024;
497
+ var MAX_IDENTITY_LENGTH = 255;
498
+ var MAX_CURSOR_LENGTH = 8192;
499
+ var HOUR_MS = 36e5;
500
+ var DAY_MS$1 = 24 * HOUR_MS;
501
+ var WINDOW_MS = {
502
+ "24h": 864e5,
503
+ "7d": 6048e5,
504
+ "30d": 2592e6
505
+ };
506
+ /**
507
+ * The longest receipt range, `[sinceMs, beforeReceivedAtMs)`, of a global or
508
+ * bundle event list, and the range one covers without `sinceMs`: raw events
509
+ * are listed inside a time range, as analytics products list them. It is a
510
+ * duration, so a range that ends mid-day touches 91 UTC days. Installation
511
+ * history is one index range and reads back to `sinceMs`.
512
+ */
513
+ var EVENT_LIST_RANGE_MS = 2160 * HOUR_MS;
514
+ var isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
515
+ var requireString = (value, label, maximumLength) => {
516
+ if (typeof value !== "string" || value.length === 0 || value.length > maximumLength || new TextDecoder("utf-8", { ignoreBOM: true }).decode(new TextEncoder().encode(value)) !== value) throw new InsightsBadRequestError(`Invalid ${label}.`);
517
+ return value;
518
+ };
519
+ var requireTimestamp = (value, label) => {
520
+ if (!Number.isSafeInteger(value) || Number(value) < 0) throw new InsightsBadRequestError(`Invalid ${label}.`);
521
+ return Number(value);
522
+ };
523
+ var readLimit = (value) => {
524
+ const limit = value ?? DEFAULT_PAGE_LIMIT;
525
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) throw new InsightsBadRequestError("Invalid page limit.");
526
+ return limit;
527
+ };
528
+ var encodeBase64Url = (value) => {
529
+ const bytes = new TextEncoder().encode(value);
530
+ let binary = "";
531
+ for (const byte of bytes) binary += String.fromCharCode(byte);
532
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
533
+ };
534
+ var decodeBase64Url = (value) => {
535
+ if (value.length === 0 || value.length > MAX_CURSOR_LENGTH) throw new InsightsBadRequestError("Invalid Insights cursor.");
536
+ try {
537
+ const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
538
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
539
+ const binary = atob(padded);
540
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
541
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
542
+ } catch {
543
+ throw new InsightsBadRequestError("Invalid Insights cursor.");
544
+ }
545
+ };
546
+ var encodeCursor = (value) => encodeBase64Url(JSON.stringify(value));
547
+ var decodeCursor = (value) => {
548
+ try {
549
+ return JSON.parse(decodeBase64Url(value));
550
+ } catch (error) {
551
+ if (error instanceof InsightsBadRequestError) throw error;
552
+ throw new InsightsBadRequestError("Invalid Insights cursor.");
553
+ }
554
+ };
555
+ var readScope = (input) => {
556
+ if (input.platform !== "ios" && input.platform !== "android") throw new InsightsBadRequestError("Invalid Insights platform.");
557
+ return {
558
+ platform: input.platform,
559
+ channel: requireString(input.channel, "channel", MAX_EVENT_ID_LENGTH)
560
+ };
561
+ };
562
+ var bundleFilter = (input) => {
563
+ const scope = readScope(input);
564
+ const bundleId = requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
565
+ switch (input.outcome) {
566
+ case "downloaded": return {
567
+ ...scope,
568
+ type: "UPDATE_DOWNLOADED",
569
+ toBundleId: bundleId
570
+ };
571
+ case "applied": return {
572
+ ...scope,
573
+ type: "UPDATE_APPLIED",
574
+ toBundleId: bundleId
575
+ };
576
+ case "recovered": return {
577
+ ...scope,
578
+ type: "RECOVERED",
579
+ fromBundleId: bundleId
580
+ };
581
+ case "failed": return {
582
+ ...scope,
583
+ type: "UPDATE_FAILED",
584
+ toBundleId: bundleId
585
+ };
586
+ default: throw new InsightsBadRequestError("Invalid Insights outcome.");
587
+ }
588
+ };
589
+ var sameFilter = (left, right) => {
590
+ if (!isRecord$1(left) || left.kind !== right.kind) return false;
591
+ switch (right.kind) {
592
+ case "all": return true;
593
+ case "installationMovement": return left.installId === right.installId;
594
+ case "bundle": return left.platform === right.platform && left.channel === right.channel && left.type === right.type && (right.type === "RECOVERED" ? left.fromBundleId === right.fromBundleId : left.toBundleId === right.toBundleId);
595
+ }
596
+ };
597
+ var readEventCursor = (value, filter) => {
598
+ const cursor = decodeCursor(value);
599
+ if (!isRecord$1(cursor) || cursor.version !== 2 || cursor.kind !== "events" || !isRecord$1(cursor.filter) || !isRecord$1(cursor.after)) throw new InsightsBadRequestError("Invalid Insights cursor.");
600
+ if (!sameFilter(cursor.filter, filter)) throw new InsightsBadRequestError("Insights cursor does not match the requested events.");
601
+ if (typeof cursor.after.id !== "string" || !isUUIDv7(cursor.after.id)) throw new InsightsBadRequestError("Invalid Insights event cursor ID.");
602
+ return {
603
+ after: {
604
+ id: cursor.after.id,
605
+ receivedAtMs: requireTimestamp(cursor.after.receivedAtMs, "event cursor")
606
+ },
607
+ beforeReceivedAtMs: requireTimestamp(cursor.beforeReceivedAtMs, "event cutoff"),
608
+ kind: "events",
609
+ sinceMs: requireTimestamp(cursor.sinceMs, "event start"),
610
+ filter,
611
+ version: 2
612
+ };
613
+ };
614
+ var readUserInstallationCursor = (value, userId) => {
615
+ const cursor = decodeCursor(value);
616
+ if (!isRecord$1(cursor) || cursor.version !== 1 || cursor.kind !== "user-installations" || cursor.userId !== userId) throw new InsightsBadRequestError("Insights cursor does not match the requested user ID.");
617
+ return {
618
+ afterInstallId: requireString(cursor.afterInstallId, "installation cursor", MAX_IDENTITY_LENGTH),
619
+ kind: "user-installations",
620
+ userId,
621
+ version: 1
622
+ };
623
+ };
624
+ var compareEventNewest = (left, right) => right.received_at_ms - left.received_at_ms || compareInsightsText(right.id, left.id);
625
+ var isAfterEventCursor = (row, after) => row.received_at_ms < after.receivedAtMs || row.received_at_ms === after.receivedAtMs && compareInsightsText(row.id, after.id) < 0;
626
+ var assertEventRows = (rows, input) => {
627
+ if (rows.length > input.limit) throw new Error("Insights database returned too many event rows.");
628
+ for (let index = 0; index < rows.length; index += 1) {
629
+ const row = rows[index];
630
+ const previous = rows[index - 1];
631
+ if (!row || typeof row.id !== "string" || !isUUIDv7(row.id) || !Number.isSafeInteger(row.received_at_ms) || row.received_at_ms >= input.beforeReceivedAtMs || row.received_at_ms < input.sinceMs || previous !== void 0 && compareEventNewest(previous, row) >= 0 || input.after !== void 0 && !isAfterEventCursor(row, input.after) || input.filter.kind === "installationMovement" && (row.install_id !== input.filter.installId || !isInsightsMovementEvent(row)) || input.filter.kind === "bundle" && (row.platform !== input.filter.platform || row.channel !== input.filter.channel || row.type !== input.filter.type || (input.filter.type === "RECOVERED" ? row.from_bundle_id !== input.filter.fromBundleId : row.to_bundle_id !== input.filter.toBundleId))) throw new Error("Insights database returned invalid event rows.");
632
+ }
633
+ };
634
+ /** An `UPDATE_FAILED` row's failure, as the event lists show it. */
635
+ var toFailure = (failure) => ({
636
+ stage: failure.stage,
637
+ reason: failure.reason,
638
+ ...failure.resource === void 0 ? {} : { resource: failure.resource },
639
+ ...failure.http_status === void 0 ? {} : { httpStatus: failure.http_status },
640
+ ...failure.transport === void 0 ? {} : { transport: failure.transport },
641
+ ...failure.origin_code === void 0 ? {} : { originCode: failure.origin_code },
642
+ ...failure.previous_process_exit === void 0 ? {} : { previousProcessExit: failure.previous_process_exit }
643
+ });
644
+ /** What a row's metadata adds for its type: a failure, a delivery, an exit reason. */
645
+ var eventDetails = ({ type, metadata }) => ({
646
+ ...type === "UPDATE_FAILED" && metadata.failure !== void 0 ? { failure: toFailure(metadata.failure) } : {},
647
+ ...type === "UPDATE_DOWNLOADED" && metadata.delivery !== void 0 ? { delivery: metadata.delivery } : {},
648
+ ...type === "UPDATE_DOWNLOADED" && metadata.patch_fallback === true ? { patchFallback: true } : {},
649
+ ...type === "RECOVERED" && metadata.previous_process_exit !== void 0 ? { previousProcessExit: metadata.previous_process_exit } : {}
650
+ });
651
+ var toEventHistoryRow = (row) => ({
652
+ appVersion: row.app_version,
653
+ channel: row.channel,
654
+ cohort: row.metadata.cohort,
655
+ fromBundleId: row.from_bundle_id,
656
+ id: row.id,
657
+ installId: row.install_id,
658
+ platform: row.platform,
659
+ receivedAtMs: row.received_at_ms,
660
+ toBundleId: row.to_bundle_id,
661
+ type: row.type,
662
+ userId: row.user_id,
663
+ ...eventDetails(row)
664
+ });
665
+ var toInstallationRow = (row) => ({
666
+ appVersion: row.app_version,
667
+ channel: row.channel,
668
+ cohort: row.metadata.cohort,
669
+ installId: row.install_id,
670
+ lastKnownBundleId: row.type === "UPDATE_DOWNLOADED" ? row.from_bundle_id : row.to_bundle_id,
671
+ pendingBundleId: row.type === "UPDATE_DOWNLOADED" ? row.to_bundle_id : null,
672
+ pendingReleaseId: row.type === "UPDATE_DOWNLOADED" ? row.to_release_id : null,
673
+ latestStatus: row.type,
674
+ platform: row.platform,
675
+ receivedAtMs: row.received_at_ms,
676
+ userId: row.user_id
677
+ });
678
+ /**
679
+ * Where a list without `sinceMs` starts: 90 days before its cutoff for the
680
+ * global and bundle lists, at the first event for installation history.
681
+ */
682
+ var defaultEventStart = (filter, beforeReceivedAtMs) => filter.kind === "installationMovement" ? 0 : Math.max(0, beforeReceivedAtMs - EVENT_LIST_RANGE_MS);
683
+ /** Whether a cursor's row lies inside the list's range. */
684
+ var isInsideBounds = ({ after }, bounds) => after.receivedAtMs >= bounds.sinceMs && after.receivedAtMs < bounds.beforeReceivedAtMs;
685
+ /** A page's range: its cursor's, which the request may repeat but not change. */
686
+ var readEventBounds = (input, cursor, filter) => {
687
+ const beforeReceivedAtMs = cursor?.beforeReceivedAtMs ?? (input.beforeReceivedAtMs === void 0 ? Date.now() : requireTimestamp(input.beforeReceivedAtMs, "event cutoff"));
688
+ if (cursor !== void 0 && input.beforeReceivedAtMs !== void 0 && input.beforeReceivedAtMs !== beforeReceivedAtMs) throw new InsightsBadRequestError("Insights cursor does not match the requested event cutoff.");
689
+ const sinceMs = cursor?.sinceMs ?? (input.sinceMs === void 0 ? defaultEventStart(filter, beforeReceivedAtMs) : requireTimestamp(input.sinceMs, "event start"));
690
+ const bounds = {
691
+ beforeReceivedAtMs,
692
+ sinceMs
693
+ };
694
+ if (sinceMs > beforeReceivedAtMs || input.sinceMs !== void 0 && input.sinceMs !== sinceMs || cursor !== void 0 && !isInsideBounds(cursor, bounds)) throw new InsightsBadRequestError("Insights cursor or range does not match the requested event start.");
695
+ if (filter.kind !== "installationMovement" && beforeReceivedAtMs - sinceMs > 7776e6) throw new InsightsBadRequestError("Insights event lists cover at most 90 days: send a sinceMs no more than 90 days before beforeReceivedAtMs.");
696
+ return bounds;
697
+ };
698
+ var pageEventRows = async (model, input, filter, map) => {
699
+ const limit = readLimit(input.limit);
700
+ const cursor = input.cursor === void 0 ? void 0 : readEventCursor(input.cursor, filter);
701
+ const bounds = readEventBounds(input, cursor, filter);
702
+ const query = {
703
+ filter,
704
+ ...bounds,
705
+ ...cursor === void 0 ? {} : { after: cursor.after },
706
+ limit
707
+ };
708
+ const rows = await model.listEvents(query);
709
+ assertEventRows(rows, query);
710
+ const last = rows.at(-1);
711
+ return {
712
+ beforeReceivedAtMs: bounds.beforeReceivedAtMs,
713
+ data: rows.map(map),
714
+ nextCursor: rows.length === limit && last ? encodeCursor({
715
+ after: {
716
+ id: last.id,
717
+ receivedAtMs: last.received_at_ms
718
+ },
719
+ ...bounds,
720
+ kind: "events",
721
+ filter,
722
+ version: 2
723
+ }) : null
724
+ };
725
+ };
726
+ var assertInstallationRows = (rows, input) => {
727
+ if (rows.length > input.limit) throw new Error("Insights database returned too many installation rows.");
728
+ for (let index = 0; index < rows.length; index += 1) {
729
+ const row = rows[index];
730
+ const previous = rows[index - 1];
731
+ if (!row || row.user_id !== input.userId || input.afterInstallId !== void 0 && compareInsightsText(row.install_id, input.afterInstallId) <= 0 || previous !== void 0 && compareInsightsText(previous.install_id, row.install_id) >= 0) throw new Error("Insights database returned invalid installation rows.");
732
+ }
733
+ };
734
+ var createInsightsProvider = (model) => Object.freeze({
735
+ async appendBundleEvent(input) {
736
+ const event = createBundleEventRow(input);
737
+ await model.recordEvent({ event });
738
+ },
739
+ listEvents(input) {
740
+ return pageEventRows(model, input, input.bundle === void 0 ? { kind: "all" } : {
741
+ kind: "bundle",
742
+ ...bundleFilter(input.bundle)
743
+ }, toEventHistoryRow);
744
+ },
745
+ async listInstallationEvents(input) {
746
+ if ("bundle" in input && input.bundle !== void 0) throw new InsightsBadRequestError("Installation movement queries cannot include a bundle filter.");
747
+ return pageEventRows(model, input, {
748
+ kind: "installationMovement",
749
+ installId: requireString(input.installId, "install ID", MAX_IDENTITY_LENGTH)
750
+ }, (row) => toEventHistoryRow(row));
751
+ },
752
+ async getInstallation({ installId }) {
753
+ const normalizedInstallId = requireString(installId, "install ID", MAX_IDENTITY_LENGTH);
754
+ const rows = await model.findLatestEvents({ installId: normalizedInstallId });
755
+ const row = rows[0] ?? null;
756
+ if (rows.length > 1 || row !== null && row.install_id !== normalizedInstallId) throw new Error("Insights database returned an invalid installation.");
757
+ return row === null ? null : toInstallationRow(row);
758
+ },
759
+ async pageInstallationsByCurrentUserId(input) {
760
+ const userId = requireString(input.userId, "user ID", MAX_IDENTITY_LENGTH);
761
+ const limit = readLimit(input.limit);
762
+ const cursor = input.cursor === void 0 ? void 0 : readUserInstallationCursor(input.cursor, userId);
763
+ const databaseInput = {
764
+ userId,
765
+ ...cursor === void 0 ? {} : { afterInstallId: cursor.afterInstallId },
766
+ limit
767
+ };
768
+ const rows = await model.findLatestEvents(databaseInput);
769
+ assertInstallationRows(rows, databaseInput);
770
+ const last = rows.at(-1);
771
+ return {
772
+ data: rows.map(toInstallationRow),
773
+ nextCursor: rows.length === limit && last ? encodeCursor({
774
+ afterInstallId: last.install_id,
775
+ kind: "user-installations",
776
+ userId,
777
+ version: 1
778
+ }) : null
779
+ };
780
+ },
781
+ async getReportingOverview(input) {
782
+ const scope = readScope(input);
783
+ const { window } = input;
784
+ if (!Object.hasOwn(WINDOW_MS, window)) throw new InsightsBadRequestError("Invalid reporting installation window.");
785
+ const now = Date.now();
786
+ const beforeReceivedAtMs = now - now % HOUR_MS + (now % HOUR_MS === 0 ? 0 : HOUR_MS);
787
+ const reach = Math.max(0, beforeReceivedAtMs - WINDOW_MS[window]);
788
+ const sinceMs = reach - reach % DAY_MS$1;
789
+ const measure = async (count) => {
790
+ const value = await count;
791
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error("Insights database returned an invalid count.");
792
+ return {
793
+ count: value,
794
+ measuredAtMs: Date.now()
795
+ };
796
+ };
797
+ const bundleId = input.bundleId === void 0 ? void 0 : requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
798
+ const reporting = measure(model.countLatestEvents({
799
+ ...scope,
800
+ sinceMs
801
+ }));
802
+ if (bundleId === void 0) return {
803
+ ...scope,
804
+ window,
805
+ sinceMs,
806
+ beforeReceivedAtMs,
807
+ reportingInstallations: await reporting
808
+ };
809
+ const countOutcome = (outcome) => measure(model.countEvents({
810
+ filter: bundleFilter({
811
+ ...scope,
812
+ bundleId,
813
+ outcome
814
+ }),
815
+ sinceMs,
816
+ beforeReceivedAtMs
817
+ }));
818
+ const [reportingInstallations, bundleInstallations, downloadedReports, appliedReports, recoveredReports, failedReports] = await Promise.all([
819
+ reporting,
820
+ measure(model.countLatestEvents({
821
+ ...scope,
822
+ sinceMs,
823
+ bundle: [{
824
+ field: "from_bundle_id",
825
+ value: bundleId,
826
+ types: ["UPDATE_DOWNLOADED"]
827
+ }, {
828
+ field: "to_bundle_id",
829
+ value: bundleId,
830
+ types: [
831
+ "UNCHANGED",
832
+ "UPDATE_APPLIED",
833
+ "RECOVERED"
834
+ ]
835
+ }]
836
+ })),
837
+ countOutcome("downloaded"),
838
+ countOutcome("applied"),
839
+ countOutcome("recovered"),
840
+ countOutcome("failed")
841
+ ]);
842
+ return {
843
+ ...scope,
844
+ window,
845
+ sinceMs,
846
+ beforeReceivedAtMs,
847
+ reportingInstallations,
848
+ bundle: {
849
+ bundleId,
850
+ reportingInstallations: bundleInstallations,
851
+ downloadedReports,
852
+ appliedReports,
853
+ recoveredReports,
854
+ failedReports
855
+ }
856
+ };
857
+ }
858
+ });
859
+ var DAY_MS = 864e5;
860
+ var DEFAULT_INSIGHTS_RETENTION = {
861
+ rawDays: 90,
862
+ dailyDays: 400
863
+ };
864
+ var bucketRetention = (days) => ({
865
+ field: "bucket_start_ms",
866
+ days
867
+ });
868
+ var MOVEMENTS = /* @__PURE__ */ new Set([
869
+ "UPDATE_DOWNLOADED",
870
+ "UPDATE_APPLIED",
871
+ "RECOVERED",
872
+ "UPDATE_FAILED"
873
+ ]);
874
+ /** A failed update check: it names no target bundle, so no bundle list shows it. */
875
+ var isFailedCheck = (row) => row.type === "UPDATE_FAILED" && row.metadata?.failure?.stage === "check";
876
+ /**
877
+ * Downloads, applies, recoveries, and update failures, whole. An UNCHANGED
878
+ * report counts as a launch and moves its installation's head, but no list
879
+ * shows it, so no row keeps it.
880
+ */
881
+ var bundleEvents = (days) => defineTable({
882
+ id: {
883
+ type: "string",
884
+ maxLength: 36
885
+ },
886
+ type: {
887
+ type: "string",
888
+ maxLength: 32
889
+ },
890
+ install_id: {
891
+ type: "string",
892
+ maxLength: 255
893
+ },
894
+ user_id: {
895
+ type: "string",
896
+ maxLength: 255,
897
+ required: false
898
+ },
899
+ from_release_id: {
900
+ type: "string",
901
+ maxLength: 36,
902
+ required: false
903
+ },
904
+ from_bundle_id: {
905
+ type: "string",
906
+ maxLength: 36,
907
+ required: false
908
+ },
909
+ to_release_id: {
910
+ type: "string",
911
+ maxLength: 36,
912
+ required: false
913
+ },
914
+ to_bundle_id: {
915
+ type: "string",
916
+ maxLength: 36
917
+ },
918
+ platform: {
919
+ type: "string",
920
+ maxLength: 16
921
+ },
922
+ app_version: { type: "string" },
923
+ channel: { type: "string" },
924
+ metadata: { type: "json" },
925
+ received_at_ms: { type: "integer" }
926
+ }, {
927
+ key: ["id"],
928
+ derived: {
929
+ day: {
930
+ type: "integer",
931
+ compute: (row) => row.received_at_ms - row.received_at_ms % DAY_MS
932
+ },
933
+ movement_install_id: {
934
+ type: "string",
935
+ compute: (row) => MOVEMENTS.has(row.type) ? row.install_id : null
936
+ },
937
+ bundle_ref: {
938
+ type: "string",
939
+ multi: true,
940
+ compute: (row) => isFailedCheck(row) ? [] : [row.type === "RECOVERED" && row.from_bundle_id !== null ? `from:${row.from_bundle_id}` : `to:${row.to_bundle_id}`]
941
+ }
942
+ },
943
+ indexes: {
944
+ recent: {
945
+ eq: [
946
+ "channel",
947
+ "platform",
948
+ "day"
949
+ ],
950
+ sort: ["received_at_ms"]
951
+ },
952
+ movementsByInstall: {
953
+ eq: ["movement_install_id"],
954
+ sort: ["received_at_ms"]
955
+ },
956
+ byBundle: {
957
+ eq: [
958
+ "platform",
959
+ "channel",
960
+ "type",
961
+ "bundle_ref",
962
+ "day"
963
+ ],
964
+ sort: ["received_at_ms"]
965
+ },
966
+ byDay: {
967
+ eq: ["day"],
968
+ sort: ["received_at_ms"]
969
+ }
970
+ },
971
+ retention: {
972
+ field: "day",
973
+ days
974
+ }
975
+ });
976
+ /**
977
+ * Each installation's latest event, whole, so reading it is one point read.
978
+ * Its gauges count it in the UTC day of that event. An update failure moves
979
+ * nothing, so no head holds one.
980
+ */
981
+ var bundleEventHeads = (days) => defineTable({
982
+ install_id: {
983
+ type: "string",
984
+ maxLength: 255
985
+ },
986
+ id: {
987
+ type: "string",
988
+ maxLength: 36
989
+ },
990
+ type: {
991
+ type: "string",
992
+ maxLength: 32
993
+ },
994
+ user_id: {
995
+ type: "string",
996
+ maxLength: 255,
997
+ required: false
998
+ },
999
+ from_release_id: {
1000
+ type: "string",
1001
+ maxLength: 36,
1002
+ required: false
1003
+ },
1004
+ from_bundle_id: {
1005
+ type: "string",
1006
+ maxLength: 36,
1007
+ required: false
1008
+ },
1009
+ to_release_id: {
1010
+ type: "string",
1011
+ maxLength: 36,
1012
+ required: false
1013
+ },
1014
+ to_bundle_id: {
1015
+ type: "string",
1016
+ maxLength: 36
1017
+ },
1018
+ platform: {
1019
+ type: "string",
1020
+ maxLength: 16
1021
+ },
1022
+ app_version: { type: "string" },
1023
+ channel: { type: "string" },
1024
+ metadata: { type: "json" },
1025
+ received_at_ms: { type: "integer" }
1026
+ }, {
1027
+ key: ["install_id"],
1028
+ indexes: { byUser: {
1029
+ eq: ["user_id"],
1030
+ sort: ["install_id"]
1031
+ } },
1032
+ retention: {
1033
+ field: "received_at_ms",
1034
+ days
1035
+ }
1036
+ });
1037
+ /**
1038
+ * Shards are keyed by install id. Counters are blind increments and never
1039
+ * conflict, so they keep 8. Gauges and sketches are read, merged, and written
1040
+ * back. B3's rollout gate on PostgreSQL retried 2.4% of transactions at 8
1041
+ * shards and 1.3% at 16. DynamoDB's transactions take longer, so D8's gate
1042
+ * on DynamoDB Local retried 2–24% at 16, almost all on gauge rows, and 1–5%
1043
+ * with gauges at 32. Sketches stay at 16: each shard row carries 2 KB of
1044
+ * registers that every read merges.
1045
+ *
1046
+ * Every Insights aggregate is `batched`: on DynamoDB and Firestore, which
1047
+ * bill each write, an event's changes apply after it commits, merged with
1048
+ * other events' into one write per row, on one shard per writer.
1049
+ */
1050
+ var COUNTER_SHARDS = 8;
1051
+ var GAUGE_SHARDS = 32;
1052
+ var SKETCH_SHARDS = 16;
1053
+ /** Rows keyed by a hashed identity (scope and period) and a bucket. */
1054
+ var identityFields = {
1055
+ identity: {
1056
+ type: "string",
1057
+ maxLength: 32
1058
+ },
1059
+ bucket_start_ms: { type: "integer" }
1060
+ };
1061
+ var window = {
1062
+ eq: ["identity"],
1063
+ sort: ["bucket_start_ms"]
1064
+ };
1065
+ /**
1066
+ * Counters of one period kind: hourly, daily, or lifetime at bucket 0. A
1067
+ * release or channel row counts downloads, launches, and failed launches,
1068
+ * and downloads a patch delivered and those that fell back from one. A
1069
+ * release's lifetime row counts its failed updates (`failed_updates`), since
1070
+ * no breakdown row outlives the raw period; windowed reads sum
1071
+ * `insights_failures` instead.
1072
+ */
1073
+ var counters = (retention) => defineAggregate(identityFields, {
1074
+ key: ["identity", "bucket_start_ms"],
1075
+ counters: [
1076
+ "downloads",
1077
+ "launches",
1078
+ "failed_launches",
1079
+ "failed_updates",
1080
+ "patch_downloads",
1081
+ "patch_fallbacks"
1082
+ ],
1083
+ shards: COUNTER_SHARDS,
1084
+ batched: true,
1085
+ indexes: { window },
1086
+ ...retention === void 0 ? {} : { retention }
1087
+ });
1088
+ /**
1089
+ * Unique-installation sketches of one period kind. Rows of a failure
1090
+ * identity (`failure` and `check` scopes) keep only `failed_users`, so a
1091
+ * release's launch rows never grow by a failure's registers.
1092
+ */
1093
+ var sketches = (retention) => defineAggregate(identityFields, {
1094
+ key: ["identity", "bucket_start_ms"],
1095
+ distinct: [
1096
+ "launch_users",
1097
+ "activity_users",
1098
+ "failed_users"
1099
+ ],
1100
+ shards: SKETCH_SHARDS,
1101
+ batched: true,
1102
+ indexes: { window },
1103
+ retention
1104
+ });
1105
+ /** Installations whose update to a release failed, since its first failure: kept, at bucket 0. */
1106
+ var lifetimeSketches = () => defineAggregate(identityFields, {
1107
+ key: ["identity", "bucket_start_ms"],
1108
+ distinct: ["failed_users"],
1109
+ shards: SKETCH_SHARDS,
1110
+ batched: true,
1111
+ indexes: { window }
1112
+ });
1113
+ /**
1114
+ * Update failures by what failed, and recoveries by why the crashed process
1115
+ * exited, a row an hour. `release_id` is the failed update's target release,
1116
+ * or empty for a failed check or an unknown target, and the recovered
1117
+ * release for a recovery. A failure's `stage` and `reason` say where and why
1118
+ * it failed, and `detail` what else the client knew, as the JSON array
1119
+ * `[resource, httpStatus, originCode, transport]`. A recovery's stage is
1120
+ * `launch` and its reason the exit reason, with an empty detail. Every index
1121
+ * is in key order, so its counters stay blind increments.
1122
+ */
1123
+ var insightsFailures = (retention) => defineAggregate({
1124
+ platform: {
1125
+ type: "string",
1126
+ maxLength: 16
1127
+ },
1128
+ channel: { type: "string" },
1129
+ bucket_start_ms: { type: "integer" },
1130
+ release_id: {
1131
+ type: "string",
1132
+ maxLength: 36
1133
+ },
1134
+ stage: {
1135
+ type: "string",
1136
+ maxLength: 16
1137
+ },
1138
+ reason: {
1139
+ type: "string",
1140
+ maxLength: 64
1141
+ },
1142
+ detail: {
1143
+ type: "string",
1144
+ maxLength: 160
1145
+ }
1146
+ }, {
1147
+ key: [
1148
+ "platform",
1149
+ "channel",
1150
+ "bucket_start_ms",
1151
+ "release_id",
1152
+ "stage",
1153
+ "reason",
1154
+ "detail"
1155
+ ],
1156
+ counters: ["events"],
1157
+ shards: COUNTER_SHARDS,
1158
+ batched: true,
1159
+ indexes: { byScope: {
1160
+ eq: ["platform", "channel"],
1161
+ sort: ["bucket_start_ms"]
1162
+ } },
1163
+ retention
1164
+ });
1165
+ /** Installations counted in the UTC day of their latest event, so reads sum whole UTC days. */
1166
+ var insightsDistribution = (retention) => defineAggregate({
1167
+ channel: { type: "string" },
1168
+ platform: {
1169
+ type: "string",
1170
+ maxLength: 16
1171
+ },
1172
+ app_version: { type: "string" },
1173
+ release_id: {
1174
+ type: "string",
1175
+ maxLength: 36
1176
+ },
1177
+ bucket_start_ms: { type: "integer" }
1178
+ }, {
1179
+ key: [
1180
+ "channel",
1181
+ "platform",
1182
+ "app_version",
1183
+ "release_id",
1184
+ "bucket_start_ms"
1185
+ ],
1186
+ gauges: ["latest_installations"],
1187
+ shards: GAUGE_SHARDS,
1188
+ batched: true,
1189
+ indexes: {
1190
+ byScope: {
1191
+ eq: ["channel", "platform"],
1192
+ sort: ["bucket_start_ms"]
1193
+ },
1194
+ byVersion: {
1195
+ eq: [
1196
+ "channel",
1197
+ "platform",
1198
+ "app_version"
1199
+ ],
1200
+ sort: ["bucket_start_ms"]
1201
+ }
1202
+ },
1203
+ retention
1204
+ });
1205
+ /** Latest events by the bundle they came from or went to, bucketed like the distribution. */
1206
+ var insightsLatestByBundle = (retention) => defineAggregate({
1207
+ platform: {
1208
+ type: "string",
1209
+ maxLength: 16
1210
+ },
1211
+ channel: { type: "string" },
1212
+ bundle_field: {
1213
+ type: "string",
1214
+ maxLength: 16
1215
+ },
1216
+ bundle_id: {
1217
+ type: "string",
1218
+ maxLength: 36
1219
+ },
1220
+ type: {
1221
+ type: "string",
1222
+ maxLength: 32
1223
+ },
1224
+ bucket_start_ms: { type: "integer" }
1225
+ }, {
1226
+ key: [
1227
+ "platform",
1228
+ "channel",
1229
+ "bundle_field",
1230
+ "bundle_id",
1231
+ "type",
1232
+ "bucket_start_ms"
1233
+ ],
1234
+ gauges: ["installations"],
1235
+ shards: GAUGE_SHARDS,
1236
+ batched: true,
1237
+ indexes: { byBundle: {
1238
+ eq: [
1239
+ "platform",
1240
+ "channel",
1241
+ "bundle_field",
1242
+ "bundle_id",
1243
+ "type"
1244
+ ],
1245
+ sort: ["bucket_start_ms"]
1246
+ } },
1247
+ retention
1248
+ });
1249
+ /**
1250
+ * Stored events by outcome and the bundle a bundle filter matches, a row an
1251
+ * hour, which a failed check has none of; and every stored event, a row a UTC
1252
+ * day (`DAILY_EVENTS`). Bundle counts sum the hourly rows, and event lists
1253
+ * read either to skip days without events.
1254
+ */
1255
+ var insightsOutcomes = (retention) => defineAggregate({
1256
+ platform: {
1257
+ type: "string",
1258
+ maxLength: 16
1259
+ },
1260
+ channel: { type: "string" },
1261
+ type: {
1262
+ type: "string",
1263
+ maxLength: 32
1264
+ },
1265
+ bundle_ref: {
1266
+ type: "string",
1267
+ maxLength: 41
1268
+ },
1269
+ bucket_start_ms: { type: "integer" }
1270
+ }, {
1271
+ key: [
1272
+ "platform",
1273
+ "channel",
1274
+ "type",
1275
+ "bundle_ref",
1276
+ "bucket_start_ms"
1277
+ ],
1278
+ counters: ["events"],
1279
+ shards: COUNTER_SHARDS,
1280
+ batched: true,
1281
+ indexes: { byRef: {
1282
+ eq: [
1283
+ "platform",
1284
+ "channel",
1285
+ "type",
1286
+ "bundle_ref"
1287
+ ],
1288
+ sort: ["bucket_start_ms"]
1289
+ } },
1290
+ retention
1291
+ });
1292
+ /**
1293
+ * Insights' tables, each expiring after its period. The periods are values
1294
+ * the engine applies at runtime; the tables and indexes are the same for
1295
+ * any periods.
1296
+ */
1297
+ var createInsightsSchema = ({ rawDays, dailyDays } = DEFAULT_INSIGHTS_RETENTION) => {
1298
+ const hourly = bucketRetention(rawDays);
1299
+ const daily = bucketRetention(dailyDays);
1300
+ return {
1301
+ bundle_events: bundleEvents(rawDays),
1302
+ bundle_event_heads: bundleEventHeads(dailyDays),
1303
+ insights_overview: counters(hourly),
1304
+ insights_sketches: sketches(hourly),
1305
+ insights_overview_daily: counters(daily),
1306
+ insights_sketches_daily: sketches(daily),
1307
+ insights_overview_lifetime: counters(),
1308
+ insights_sketches_lifetime: lifetimeSketches(),
1309
+ insights_distribution: insightsDistribution(daily),
1310
+ insights_latest_by_bundle: insightsLatestByBundle(daily),
1311
+ insights_outcomes: insightsOutcomes(hourly),
1312
+ insights_failures: insightsFailures(hourly)
1313
+ };
1314
+ };
1315
+ createInsightsSchema();
1316
+ 30 * DAY_MS;
1317
+ /** The `error` a failed admin route answered with, or a message naming its status. */
1318
+ var errorOf = async (response, path) => {
1319
+ const body = await response.json().catch(() => null);
1320
+ return new Error(typeof body === "object" && body !== null && typeof Reflect.get(body, "error") === "string" ? String(Reflect.get(body, "error")) : `The server answered ${path.split("?")[0]} with ${response.status}.`);
1321
+ };
1322
+ var withQuery = (path, input) => {
1323
+ const query = new URLSearchParams();
1324
+ for (const [key, value] of Object.entries(input)) if (value !== void 0) query.set(key, String(value));
1325
+ const value = query.toString();
1326
+ return value.length === 0 ? path : `${path}?${value}`;
1327
+ };
1328
+ var eventQuery = ({ beforeReceivedAtMs, sinceMs, cursor, limit }) => ({
1329
+ beforeReceivedAtMs,
1330
+ sinceMs,
1331
+ cursor,
1332
+ limit
1333
+ });
1334
+ /**
1335
+ * A self-hosted server's Insights, read through the admin routes its
1336
+ * `insights()` plugin serves.
1337
+ */
1338
+ var createAdminInsightsReads = (fetchAdmin) => {
1339
+ const read = async (path) => {
1340
+ const response = await fetchAdmin(path);
1341
+ if (response.status === 404) return null;
1342
+ if (!response.ok) throw await errorOf(response, path);
1343
+ return await response.json();
1344
+ };
1345
+ const required = async (path) => {
1346
+ const value = await read(path);
1347
+ if (value === null) throw new ConsoleFeatureUnavailableError("insights", { remote: true });
1348
+ return value;
1349
+ };
1350
+ return {
1351
+ getReportingOverview: ({ platform, channel, window, bundleId }) => required(withQuery("/overview", {
1352
+ platform,
1353
+ channel,
1354
+ window,
1355
+ bundleId
1356
+ })),
1357
+ listEvents: ({ bundle, ...input }) => required(withQuery("/events", {
1358
+ ...eventQuery(input),
1359
+ ...bundle === void 0 ? {} : {
1360
+ platform: bundle.platform,
1361
+ channel: bundle.channel,
1362
+ bundleId: bundle.bundleId,
1363
+ outcome: bundle.outcome
1364
+ }
1365
+ })),
1366
+ listInstallationEvents: ({ installId, ...input }) => required(withQuery(`/installations/${encodeURIComponent(installId)}/events`, eventQuery(input))),
1367
+ getInstallation: ({ installId }) => read(`/installations/${encodeURIComponent(installId)}`),
1368
+ pageInstallationsByCurrentUserId: ({ userId, cursor, limit }) => required(withQuery("/installations", {
1369
+ userId,
1370
+ cursor,
1371
+ limit
1372
+ })),
1373
+ getUpdateFailures: ({ platform, channel, releaseId, timeRange }) => required(withQuery("/failures", {
1374
+ platform,
1375
+ channel,
1376
+ releaseId,
1377
+ start: timeRange?.start,
1378
+ end: timeRange?.end
1379
+ })),
1380
+ getRetention: () => required("/retention")
1381
+ };
1382
+ };
1383
+ var featureApi = (api) => api;
1384
+ /** What serves each console feature, keyed like `consoleFeatures`. */
1385
+ var consoleFeatureApis = {
1386
+ insights: featureApi({
1387
+ local: (api) => ({
1388
+ ...createInsightsProvider(createInsightsModel(api)),
1389
+ getRetention: async () => api.retention,
1390
+ getUpdateFailures: api.getUpdateFailures
1391
+ }),
1392
+ remote: createAdminInsightsReads
1393
+ }),
1394
+ insightsAnalytics: featureApi({ local: (api) => createInsightsModel(api) }),
1395
+ apiKeys: featureApi({ local: (api) => api })
1396
+ };
1397
+ var isRecord = (value) => typeof value === "object" && value !== null;
1398
+ /**
1399
+ * Whether one of Hot Updater's own factories made `plugin`: the brand they
1400
+ * set under a registered symbol, read here with a copy of the key so the
1401
+ * console imports no package's internals. Assembly refuses any other plugin
1402
+ * that takes one of their ids, so features key on it.
1403
+ */
1404
+ var isOfficialPlugin = (plugin) => isRecord(plugin) && Reflect.get(plugin, Symbol.for("@hot-updater/server/official-plugin")) === true;
1405
+ var featureIds = Object.keys(consoleFeatureApis);
1406
+ /**
1407
+ * What serves each feature, built by its factory: over the plugins' APIs the
1408
+ * console assembled, or over a self-hosted server's admin API. A feature
1409
+ * without its plugin, or without a factory for the admin API, has none.
1410
+ */
1411
+ var featureApis = (build) => Object.fromEntries(featureIds.flatMap((feature) => {
1412
+ const api = build(feature);
1413
+ return api === void 0 ? [] : [[feature, api]];
1414
+ }));
1415
+ /**
1416
+ * The console's features and what serves them, for the plugins its config
1417
+ * lists, the plugins the server runs:
1418
+ * - a self-hosted server (`standaloneRepository`) runs them itself; the
1419
+ * console serves what that server's admin API serves of their features,
1420
+ * and nothing that needs the database;
1421
+ * - otherwise the console runs them over the database, as the server does,
1422
+ * and serves their features through `api`, their APIs.
1423
+ */
1424
+ var createConsoleRuntime = (config) => {
1425
+ const { database } = config;
1426
+ const official = new Set(config.plugins.filter(isOfficialPlugin).map((plugin) => plugin.id));
1427
+ if (isRemoteDatabase(database)) {
1428
+ const { fetchAdmin } = database;
1429
+ const features = resolveConsoleFeatures([...official], { remote: true });
1430
+ return {
1431
+ remote: true,
1432
+ features: async () => features,
1433
+ apis: featureApis((feature) => consoleFeatureApis[feature].remote?.(fetchAdmin))
1434
+ };
1435
+ }
1436
+ const api = config.api ?? {};
1437
+ const features = resolveConsoleFeatures([...official], { remote: false });
1438
+ return {
1439
+ remote: false,
1440
+ features: async () => features,
1441
+ apis: featureApis((feature) => {
1442
+ const { plugin } = consoleFeatures[feature];
1443
+ const pluginApi = official.has(plugin) ? api[plugin] : void 0;
1444
+ return pluginApi === void 0 ? void 0 : consoleFeatureApis[feature].local(pluginApi);
1445
+ })
1446
+ };
1447
+ };
1448
+ /**
1449
+ * What serves `feature`. Every feature's server function asks for it first,
1450
+ * so a feature the console does not serve is refused with one error the
1451
+ * client recognizes, before anything is read.
1452
+ */
1453
+ var requireFeature = async (runtime, feature) => {
1454
+ const api = runtime.apis[feature];
1455
+ if (api === void 0 || !(await runtime.features())[feature]) throw new ConsoleFeatureUnavailableError(feature, { remote: runtime.remote });
1456
+ return api;
1457
+ };
1458
+ //#endregion
1459
+ export { createConsoleRuntime, requireFeature };