@hot-updater/console 0.36.7 → 1.0.0-rc.1

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 (262) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/BundleIdDisplay-D4ErdU5_.js +1 -0
  3. package/.output/public/assets/InsightsCapabilityContext-B42VqvP6.js +1 -0
  4. package/.output/public/assets/api-keys-api-DGVTTvTJ.js +1 -0
  5. package/.output/public/assets/api-keys-z16OZIP4.js +1 -0
  6. package/.output/public/assets/badge-Br_KDO0J.js +1 -0
  7. package/.output/public/assets/card-DzuEXvyJ.js +1 -0
  8. package/.output/public/assets/chart-no-axes-combined-FaYMGmQS.js +1 -0
  9. package/.output/public/assets/createServerFn-gM2-LFBy.js +3 -0
  10. package/.output/public/assets/dialog-BeAENgez.js +1 -0
  11. package/.output/public/assets/dist-Bssh_cv0.js +1 -0
  12. package/.output/public/assets/field-Buq148-A.js +1 -0
  13. package/.output/public/assets/index-uOpQAIC4.js +10 -0
  14. package/.output/public/assets/input-group-B3p5UpyW.js +1 -0
  15. package/.output/public/assets/insights-DJTr7Ozn.js +1 -0
  16. package/.output/public/assets/insights-api-DVMOoRMS.js +1 -0
  17. package/.output/public/assets/installations-BY64iIAK.js +2 -0
  18. package/.output/public/assets/installations-D5v9i3AL.js +1 -0
  19. package/.output/public/assets/lazyRouteComponent-DKYu5cZJ.js +1 -0
  20. package/.output/public/assets/preload-helper-CQ36nxok.js +1 -0
  21. package/.output/public/assets/refresh-cw-BkbfZ62O.js +1 -0
  22. package/.output/public/assets/routes-BuoI5-Cr.js +2 -0
  23. package/.output/public/assets/routes-C8fvgETO.js +1 -0
  24. package/.output/public/assets/search-qsIDvS2I.js +1 -0
  25. package/.output/public/assets/shield-check-DE_hVuHu.js +1 -0
  26. package/.output/public/assets/signing-mJD88Y4C.js +1 -0
  27. package/.output/public/assets/styles-BZIG7t42.css +2 -0
  28. package/.output/public/assets/table-RamFEclP.js +1 -0
  29. package/.output/public/assets/useCompositeListItem-ChzDqdpc.js +42 -0
  30. package/.output/public/assets/useMutation-JsGVjQMY.js +1 -0
  31. package/.output/public/assets/useOpenInteractionType-Dwfy-SpA.js +1 -0
  32. package/.output/server/__23tanstack-start-server-fn-resolver-Dftxlm2E.mjs +146 -0
  33. package/.output/server/_libs/@base-ui/react+[...].mjs +16124 -0
  34. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
  35. package/.output/server/_libs/@tanstack/react-router+[...].mjs +41 -10
  36. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +132 -0
  37. package/.output/server/_libs/d3-array.mjs +231 -0
  38. package/.output/server/_libs/d3-color.mjs +330 -0
  39. package/.output/server/_libs/d3-format.mjs +252 -0
  40. package/.output/server/_libs/d3-interpolate.mjs +207 -0
  41. package/.output/server/_libs/d3-path.mjs +77 -0
  42. package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
  43. package/.output/server/_libs/d3-shape.mjs +818 -0
  44. package/.output/server/_libs/decimal.js-light.mjs +884 -0
  45. package/.output/server/_libs/es-toolkit.mjs +1134 -0
  46. package/.output/server/_libs/eventemitter3.mjs +236 -0
  47. package/.output/server/_libs/immer.mjs +532 -0
  48. package/.output/server/_libs/lucide-react.mjs +141 -13
  49. package/.output/server/_libs/next-themes.mjs +2 -2
  50. package/.output/server/_libs/react-redux+[...].mjs +205 -0
  51. package/.output/server/_libs/recharts+[...].mjs +14117 -0
  52. package/.output/server/_libs/sonner.mjs +2 -2
  53. package/.output/server/_libs/tanstack__query-core.mjs +114 -1
  54. package/.output/server/_libs/tanstack__react-query.mjs +3 -4
  55. package/.output/server/_runtime.mjs +10 -1
  56. package/.output/server/_ssr/BundleActivityChart-CWcdPuUn.mjs +288 -0
  57. package/.output/server/_ssr/BundleIdDisplay-BEvB3MvA.mjs +13 -0
  58. package/.output/server/_ssr/InsightsCapabilityContext-Cx8fjm2s.mjs +15 -0
  59. package/.output/server/_ssr/api-keys-CpBqHpuC.mjs +300 -0
  60. package/.output/server/_ssr/api-keys-api-Czkp57UV.mjs +60 -0
  61. package/.output/server/_ssr/api-keys-rpc-BTtMDSoR.mjs +136 -0
  62. package/.output/server/_ssr/api-rpc-BEX8ISai.mjs +1468 -0
  63. package/.output/server/_ssr/auth-BF8mbAgF.mjs +21 -0
  64. package/.output/server/_ssr/auth-rpc-BFMEC-TT.mjs +24 -0
  65. package/.output/server/_ssr/auth.server-DW71WzsD.mjs +47 -0
  66. package/.output/server/_ssr/badge-CpWXQwyF.mjs +26 -0
  67. package/.output/server/_ssr/bundle-signing-rpc-BbCFUQSu.mjs +16 -0
  68. package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +12 -0
  69. package/.output/server/_ssr/card-Bsc8mRis.mjs +86 -0
  70. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +13 -0
  71. package/.output/server/_ssr/config.server-DQYEr_5X.mjs +361 -0
  72. package/.output/server/_ssr/{createServerFn-DzU0k62V.mjs → createServerFn-B2hcXoPM.mjs} +1 -1
  73. package/.output/server/_ssr/createServerRpc-DB29C_5p.mjs +12 -0
  74. package/.output/server/_ssr/{sidebar-Cv_H0_Jv.mjs → createSsrRpc-DhfrxEjg.mjs} +68 -50
  75. package/.output/server/_ssr/databaseRows-QlCYLi0H.mjs +166 -0
  76. package/.output/server/_ssr/deleteBundle-Cu_XOMt0.mjs +66 -0
  77. package/.output/server/_ssr/dialog-C0vKUp8R.mjs +170 -0
  78. package/.output/server/_ssr/dist-BVSLTpoP.mjs +250 -0
  79. package/.output/server/_ssr/downloadBundle-CNnYxYO2.mjs +24 -0
  80. package/.output/server/_ssr/{empty-plugin-adapters-B4HCzgmM.mjs → empty-plugin-adapters-B5QvGpEl.mjs} +1 -1
  81. package/.output/server/_ssr/field-DASCh-5d.mjs +74 -0
  82. package/.output/server/_ssr/{getBundleChildren-CVtSiH5t.mjs → getBundleChildren-BcRMFoDF.mjs} +10 -18
  83. package/.output/server/_ssr/input-group-etIsBUqv.mjs +288 -0
  84. package/.output/server/_ssr/insights-api-C1F7UreR.mjs +71 -0
  85. package/.output/server/_ssr/insights-input-9fEJF-ZG.mjs +96 -0
  86. package/.output/server/_ssr/insights-rpc-C-8R6kED.mjs +155 -0
  87. package/.output/server/_ssr/insights-ygT_aIfo.mjs +858 -0
  88. package/.output/server/_ssr/installations-DqRT1QTd.mjs +535 -0
  89. package/.output/server/_ssr/installations-K8TCYTYh.mjs +19 -0
  90. package/.output/server/_ssr/router-BxBEvtF1.mjs +513 -0
  91. package/.output/server/_ssr/routes-BC7rn-tf.mjs +37 -0
  92. package/.output/server/_ssr/routes-CM5KAwpB.mjs +2285 -0
  93. package/.output/server/_ssr/runtime.server-la7TRAH_.mjs +527 -0
  94. package/.output/server/_ssr/server-CpzW--lT.mjs +7468 -0
  95. package/.output/server/_ssr/signing-jk4HANnl.mjs +134 -0
  96. package/.output/server/_ssr/ssr.mjs +4 -5740
  97. package/.output/server/_ssr/start-DpViQGnt.mjs +4 -0
  98. package/.output/server/_ssr/table-CU58HeI7.mjs +53 -0
  99. package/.output/server/_ssr/uuidv7-CaKuKXrV.mjs +31 -0
  100. package/.output/server/_tanstack-start-manifest_v-Mz9MmSlD.mjs +103 -0
  101. package/.output/server/index.mjs +229 -47
  102. package/README.md +29 -8
  103. package/lib/index.d.mts +35 -0
  104. package/lib/index.mjs +4 -0
  105. package/lib/vite-internal.mjs +127 -0
  106. package/lib/vite.d.mts +10 -0
  107. package/lib/vite.mjs +5 -0
  108. package/package.json +51 -29
  109. package/public/apple-touch-icon.png +0 -0
  110. package/public/favicon-16x16.png +0 -0
  111. package/public/favicon-32x32.png +0 -0
  112. package/public/favicon.ico +0 -0
  113. package/public/logo.svg +1 -0
  114. package/public/manifest.json +30 -0
  115. package/public/robots.txt +3 -0
  116. package/src/components/AppSidebar.tsx +159 -0
  117. package/src/components/BundleIdDisplay.tsx +18 -0
  118. package/src/components/ChannelBadge.tsx +31 -0
  119. package/src/components/ConsoleAccessPage.tsx +122 -0
  120. package/src/components/EnabledStatusIcon.tsx +54 -0
  121. package/src/components/HashValueDisplay.tsx +85 -0
  122. package/src/components/HotUpdaterLogo.tsx +30 -0
  123. package/src/components/NotFoundPage.tsx +33 -0
  124. package/src/components/PlatformIcon.tsx +39 -0
  125. package/src/components/RolloutPercentageBadge.tsx +28 -0
  126. package/src/components/ThemeProvider.tsx +116 -0
  127. package/src/components/TimestampDisplay.tsx +41 -0
  128. package/src/components/features/api-keys/ApiKeysPage.tsx +377 -0
  129. package/src/components/features/bundles/BundleActivityChart.tsx +193 -0
  130. package/src/components/features/bundles/BundleChildrenPanel.tsx +201 -0
  131. package/src/components/features/bundles/BundleInsightsSummary.tsx +123 -0
  132. package/src/components/features/bundles/BundleMetadata.tsx +136 -0
  133. package/src/components/features/bundles/RolloutCohortsDialog.tsx +201 -0
  134. package/src/components/features/channels/ChannelManagementDialog.tsx +228 -0
  135. package/src/components/features/insights/ActivityChart.tsx +150 -0
  136. package/src/components/features/insights/BundleDistribution.tsx +133 -0
  137. package/src/components/features/insights/BundleSelector.tsx +114 -0
  138. package/src/components/features/insights/InsightsCapabilityContext.tsx +24 -0
  139. package/src/components/features/insights/InsightsControls.tsx +115 -0
  140. package/src/components/features/insights/InsightsErrorAlert.tsx +21 -0
  141. package/src/components/features/insights/InsightsOverview.tsx +208 -0
  142. package/src/components/features/insights/InstallationHistoryCard.tsx +248 -0
  143. package/src/components/features/insights/InstallationMatchesCard.tsx +132 -0
  144. package/src/components/features/insights/InstallationPageHeader.tsx +138 -0
  145. package/src/components/features/insights/InstallationPagination.tsx +48 -0
  146. package/src/components/features/insights/InstallationSearch.tsx +230 -0
  147. package/src/components/features/insights/UpdateOutcomes.tsx +147 -0
  148. package/src/components/features/releases/ReleaseEditorSheet.tsx +879 -0
  149. package/src/components/features/releases/ReleaseStateBadge.tsx +20 -0
  150. package/src/components/features/signing/BundleSigningPage.tsx +139 -0
  151. package/src/components/ui/alert-dialog.tsx +185 -0
  152. package/src/components/ui/alert.tsx +76 -0
  153. package/src/components/ui/badge.tsx +49 -0
  154. package/src/components/ui/button.tsx +58 -0
  155. package/src/components/ui/card.tsx +89 -0
  156. package/src/components/ui/chart.tsx +371 -0
  157. package/src/components/ui/combobox.tsx +303 -0
  158. package/src/components/ui/dialog.tsx +156 -0
  159. package/src/components/ui/dropdown-menu.tsx +270 -0
  160. package/src/components/ui/field.tsx +235 -0
  161. package/src/components/ui/input-group.tsx +147 -0
  162. package/src/components/ui/input.tsx +19 -0
  163. package/src/components/ui/label.tsx +25 -0
  164. package/src/components/ui/progress.tsx +84 -0
  165. package/src/components/ui/select.tsx +205 -0
  166. package/src/components/ui/separator.tsx +23 -0
  167. package/src/components/ui/sheet.tsx +132 -0
  168. package/src/components/ui/sidebar.tsx +779 -0
  169. package/src/components/ui/skeleton.tsx +13 -0
  170. package/src/components/ui/slider.tsx +56 -0
  171. package/src/components/ui/sonner.tsx +45 -0
  172. package/src/components/ui/switch.tsx +25 -0
  173. package/src/components/ui/table.tsx +114 -0
  174. package/src/components/ui/textarea.tsx +18 -0
  175. package/src/components/ui/toggle-group.tsx +89 -0
  176. package/src/components/ui/toggle.tsx +43 -0
  177. package/src/components/ui/tooltip.tsx +66 -0
  178. package/src/hooks/use-mobile.ts +30 -0
  179. package/src/index.ts +47 -0
  180. package/src/lib/api-keys-api.ts +69 -0
  181. package/src/lib/api-keys-rpc.ts +87 -0
  182. package/src/lib/api-rpc.ts +478 -0
  183. package/src/lib/api.ts +444 -0
  184. package/src/lib/auth-rpc.ts +16 -0
  185. package/src/lib/bundle-signing-api.ts +14 -0
  186. package/src/lib/bundle-signing-rpc.ts +16 -0
  187. package/src/lib/constants.ts +1 -0
  188. package/src/lib/git.ts +51 -0
  189. package/src/lib/insights-api.ts +144 -0
  190. package/src/lib/insights-error.ts +19 -0
  191. package/src/lib/insights-input.ts +187 -0
  192. package/src/lib/insights-overview.ts +170 -0
  193. package/src/lib/insights-rpc.ts +218 -0
  194. package/src/lib/server/auth.server.ts +43 -0
  195. package/src/lib/server/bundle-signing.server.ts +23 -0
  196. package/src/lib/server/config.server.ts +109 -0
  197. package/src/lib/server/console-runtime.server.ts +61 -0
  198. package/src/lib/server/deleteBundle.ts +145 -0
  199. package/src/lib/server/downloadBundle.ts +46 -0
  200. package/src/lib/server/getBundleChildren.ts +130 -0
  201. package/src/lib/server/listReleases.ts +61 -0
  202. package/src/lib/server/releaseActivity.ts +27 -0
  203. package/src/lib/server/releaseReachability.ts +95 -0
  204. package/src/lib/server/runtime.server.ts +172 -0
  205. package/src/lib/utils.ts +9 -0
  206. package/src/logo.svg +1 -0
  207. package/src/routeTree.gen.ts +199 -0
  208. package/src/router.tsx +21 -0
  209. package/src/routes/-installations-search.ts +19 -0
  210. package/src/routes/-releases-search.ts +74 -0
  211. package/src/routes/__root.tsx +174 -0
  212. package/src/routes/api/auth/$.ts +19 -0
  213. package/src/routes/api/bundles/$bundleId/download.ts +17 -0
  214. package/src/routes/api-keys.tsx +37 -0
  215. package/src/routes/index.tsx +982 -0
  216. package/src/routes/insights.tsx +139 -0
  217. package/src/routes/installations.tsx +194 -0
  218. package/src/routes/signing.tsx +37 -0
  219. package/src/styles.css +152 -0
  220. package/src/virtual-modules.d.ts +21 -0
  221. package/src/vite-env.d.ts +1 -0
  222. package/src/vite-internal.ts +195 -0
  223. package/src/vite.ts +12 -0
  224. package/.output/public/assets/dist-DJX53faP.js +0 -3
  225. package/.output/public/assets/index-Iylis5kY.js +0 -10
  226. package/.output/public/assets/routes-HT21hs0C.js +0 -54
  227. package/.output/public/assets/styles-C0Re4quB.css +0 -2
  228. package/.output/server/__23tanstack-start-server-fn-resolver-CgwgZ0-i.mjs +0 -66
  229. package/.output/server/_libs/@floating-ui/core+[...].mjs +0 -698
  230. package/.output/server/_libs/@floating-ui/dom+[...].mjs +0 -644
  231. package/.output/server/_libs/@floating-ui/react-dom+[...].mjs +0 -839
  232. package/.output/server/_libs/@radix-ui/react-alert-dialog+[...].mjs +0 -2138
  233. package/.output/server/_libs/@radix-ui/react-popper+[...].mjs +0 -308
  234. package/.output/server/_libs/@radix-ui/react-select+[...].mjs +0 -1038
  235. package/.output/server/_libs/@tanstack/devtools-event-client+[...].mjs +0 -196
  236. package/.output/server/_libs/@tanstack/form-core+[...].mjs +0 -3228
  237. package/.output/server/_libs/@tanstack/react-form+[...].mjs +0 -430
  238. package/.output/server/_libs/@tanstack/react-table+[...].mjs +0 -2382
  239. package/.output/server/_libs/radix-ui__number.mjs +0 -6
  240. package/.output/server/_libs/radix-ui__primitive.mjs +0 -9
  241. package/.output/server/_libs/radix-ui__react-arrow.mjs +0 -23
  242. package/.output/server/_libs/radix-ui__react-collection.mjs +0 -78
  243. package/.output/server/_libs/radix-ui__react-direction.mjs +0 -13
  244. package/.output/server/_libs/radix-ui__react-label.mjs +0 -22
  245. package/.output/server/_libs/radix-ui__react-separator.mjs +0 -30
  246. package/.output/server/_libs/radix-ui__react-slider.mjs +0 -510
  247. package/.output/server/_libs/radix-ui__react-switch.mjs +0 -157
  248. package/.output/server/_libs/radix-ui__react-tooltip.mjs +0 -493
  249. package/.output/server/_ssr/api-rpc-BIbDUKSz.mjs +0 -292
  250. package/.output/server/_ssr/config.server-BSS366KT.mjs +0 -41
  251. package/.output/server/_ssr/deleteBundle-BgbZTtpW.mjs +0 -141
  252. package/.output/server/_ssr/dist-KboydRVk.mjs +0 -120
  253. package/.output/server/_ssr/router-C3hqEy3k.mjs +0 -307
  254. package/.output/server/_ssr/routes-B0lQvvlC.mjs +0 -3048
  255. package/.output/server/_ssr/start-02sLteYG.mjs +0 -4
  256. package/.output/server/_ssr/storageProfile-FVkjPoGb.mjs +0 -9
  257. package/.output/server/_tanstack-start-manifest_v-Bpx52WOU.mjs +0 -20
  258. package/.output/server/node_modules/tslib/modules/index.js +0 -70
  259. package/.output/server/node_modules/tslib/modules/package.json +0 -3
  260. package/.output/server/node_modules/tslib/package.json +0 -47
  261. package/.output/server/node_modules/tslib/tslib.js +0 -484
  262. package/.output/server/package.json +0 -9
@@ -0,0 +1,187 @@
1
+ import type { ActiveInstallationWindow } from "@hot-updater/server";
2
+ import type { BundleEventInsightsWindow } from "@hot-updater/server";
3
+
4
+ const MAX_INSIGHTS_STRING_LENGTH = 1024;
5
+ const MAX_INSIGHTS_LIMIT = 100;
6
+
7
+ export type BundleEventSummaryInput = {
8
+ readonly bundleId: string;
9
+ };
10
+
11
+ export type BundleEventInsightsInput = BundleEventSummaryInput & {
12
+ readonly window: BundleEventInsightsWindow;
13
+ readonly limit?: number;
14
+ readonly offset?: number;
15
+ };
16
+
17
+ export type SearchInstallationsInput = {
18
+ readonly query: string;
19
+ readonly limit?: number;
20
+ readonly offset?: number;
21
+ };
22
+
23
+ export type InstallationHistoryInput = {
24
+ readonly installId: string;
25
+ readonly limit?: number;
26
+ readonly offset?: number;
27
+ };
28
+
29
+ export type ActiveInstallationInput = {
30
+ readonly window: ActiveInstallationWindow;
31
+ readonly userId?: string;
32
+ };
33
+
34
+ type InsightsPagination = {
35
+ readonly limit?: number;
36
+ readonly offset?: number;
37
+ };
38
+
39
+ export class InsightsInputValidationError extends Error {
40
+ readonly name = "InsightsInputValidationError";
41
+
42
+ constructor(readonly field: string) {
43
+ super(`Invalid insights input: ${field}`);
44
+ }
45
+ }
46
+
47
+ const isRecord = (input: unknown): input is Record<string, unknown> =>
48
+ typeof input === "object" && input !== null && !Array.isArray(input);
49
+
50
+ const parseRecord = (input: unknown): Record<string, unknown> => {
51
+ if (!isRecord(input)) {
52
+ throw new InsightsInputValidationError("input");
53
+ }
54
+ return input;
55
+ };
56
+
57
+ const parseString = (input: Record<string, unknown>, field: string): string => {
58
+ const value = input[field];
59
+ if (typeof value !== "string") {
60
+ throw new InsightsInputValidationError(field);
61
+ }
62
+ const trimmed = value.trim();
63
+ if (trimmed.length === 0 || trimmed.length > MAX_INSIGHTS_STRING_LENGTH) {
64
+ throw new InsightsInputValidationError(field);
65
+ }
66
+ return trimmed;
67
+ };
68
+
69
+ const parseLimit = (input: Record<string, unknown>): number | undefined => {
70
+ const value = input.limit;
71
+ if (value === undefined) {
72
+ return undefined;
73
+ }
74
+ if (
75
+ typeof value !== "number" ||
76
+ !Number.isInteger(value) ||
77
+ value < 1 ||
78
+ value > MAX_INSIGHTS_LIMIT
79
+ ) {
80
+ throw new InsightsInputValidationError("limit");
81
+ }
82
+ return value;
83
+ };
84
+
85
+ const parseOffset = (input: Record<string, unknown>): number | undefined => {
86
+ const value = input.offset;
87
+ if (value === undefined) {
88
+ return undefined;
89
+ }
90
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
91
+ throw new InsightsInputValidationError("offset");
92
+ }
93
+ return value;
94
+ };
95
+
96
+ const parsePagination = (
97
+ input: Record<string, unknown>,
98
+ ): InsightsPagination => {
99
+ const limit = parseLimit(input);
100
+ const offset = parseOffset(input);
101
+ return {
102
+ ...(limit === undefined ? {} : { limit }),
103
+ ...(offset === undefined ? {} : { offset }),
104
+ };
105
+ };
106
+
107
+ const parseWindow = (
108
+ input: Record<string, unknown>,
109
+ ): BundleEventInsightsWindow => {
110
+ const value = input.window;
111
+ if (value === "24h" || value === "7d" || value === "30d" || value === "all") {
112
+ return value;
113
+ }
114
+ throw new InsightsInputValidationError("window");
115
+ };
116
+
117
+ const parseActiveWindow = (
118
+ input: Record<string, unknown>,
119
+ ): ActiveInstallationWindow => {
120
+ const value = input.window;
121
+ if (value === "24h" || value === "7d" || value === "30d") {
122
+ return value;
123
+ }
124
+ throw new InsightsInputValidationError("window");
125
+ };
126
+
127
+ const parseOptionalUserId = (
128
+ input: Record<string, unknown>,
129
+ ): string | undefined => {
130
+ if (input.userId === undefined) return undefined;
131
+ if (typeof input.userId !== "string") {
132
+ throw new InsightsInputValidationError("userId");
133
+ }
134
+ const userId = input.userId.trim();
135
+ if (userId.length === 0) return undefined;
136
+ if (userId.length > MAX_INSIGHTS_STRING_LENGTH) {
137
+ throw new InsightsInputValidationError("userId");
138
+ }
139
+ return userId;
140
+ };
141
+
142
+ export const parseActiveInstallationInput = (
143
+ input: unknown,
144
+ ): ActiveInstallationInput => {
145
+ const record = parseRecord(input);
146
+ const window = parseActiveWindow(record);
147
+ const userId = parseOptionalUserId(record);
148
+ return userId === undefined ? { window } : { window, userId };
149
+ };
150
+
151
+ export const parseBundleEventSummaryInput = (
152
+ input: unknown,
153
+ ): BundleEventSummaryInput => {
154
+ const record = parseRecord(input);
155
+ return { bundleId: parseString(record, "bundleId") };
156
+ };
157
+
158
+ export const parseBundleEventInsightsInput = (
159
+ input: unknown,
160
+ ): BundleEventInsightsInput => {
161
+ const record = parseRecord(input);
162
+ return {
163
+ bundleId: parseString(record, "bundleId"),
164
+ window: parseWindow(record),
165
+ ...parsePagination(record),
166
+ };
167
+ };
168
+
169
+ export const parseSearchInstallationsInput = (
170
+ input: unknown,
171
+ ): SearchInstallationsInput => {
172
+ const record = parseRecord(input);
173
+ return {
174
+ query: parseString(record, "query"),
175
+ ...parsePagination(record),
176
+ };
177
+ };
178
+
179
+ export const parseInstallationHistoryInput = (
180
+ input: unknown,
181
+ ): InstallationHistoryInput => {
182
+ const record = parseRecord(input);
183
+ return {
184
+ installId: parseString(record, "installId"),
185
+ ...parsePagination(record),
186
+ };
187
+ };
@@ -0,0 +1,170 @@
1
+ import { normalizeRolloutCohortCount } from "@hot-updater/core";
2
+ import type { Bundle, ChannelRow, ReleaseRow } from "@hot-updater/plugin-core";
3
+
4
+ export type InsightsBundleMetadata = {
5
+ readonly platform: "ios" | "android";
6
+ readonly channel: string | null;
7
+ readonly targetAppVersion: string | null;
8
+ readonly fingerprintHash: string | null;
9
+ };
10
+
11
+ export type LatestReportedBundle = {
12
+ readonly bundleId: string;
13
+ readonly trackedInstallations: number;
14
+ readonly observedShare: number;
15
+ readonly bundle: InsightsBundleMetadata | null;
16
+ };
17
+
18
+ export type ConfiguredRollout = {
19
+ readonly releaseId: string;
20
+ readonly bundleId: string;
21
+ readonly configuredPercentage: number;
22
+ readonly trackedInstallations: number;
23
+ readonly bundle: InsightsBundleMetadata;
24
+ };
25
+
26
+ export type InsightsOverview = {
27
+ readonly trackedInstallations: number;
28
+ readonly mostCommonLatestReportedBundle: LatestReportedBundle | null;
29
+ readonly latestReportedBundles: readonly LatestReportedBundle[];
30
+ readonly configuredRollouts: readonly ConfiguredRollout[];
31
+ };
32
+
33
+ export type LatestInstallationBundle = {
34
+ readonly lastKnownBundleId: string | null;
35
+ };
36
+
37
+ export type InstallationBundleCount = {
38
+ readonly bundleId: string;
39
+ readonly installations: number;
40
+ };
41
+
42
+ const toBundleMetadata = (
43
+ bundle: Bundle,
44
+ release: ReleaseRow | undefined,
45
+ channelById: ReadonlyMap<string, string>,
46
+ ): InsightsBundleMetadata => ({
47
+ platform: bundle.platform,
48
+ channel: release ? (channelById.get(release.channel_id) ?? null) : null,
49
+ targetAppVersion: release?.target_app_version ?? null,
50
+ fingerprintHash: release?.fingerprint_hash ?? null,
51
+ });
52
+
53
+ const compareCodePoints = (left: string, right: string): number =>
54
+ left < right ? -1 : left > right ? 1 : 0;
55
+
56
+ const createOverview = (
57
+ bundles: readonly Bundle[],
58
+ releases: readonly ReleaseRow[],
59
+ channels: readonly ChannelRow[],
60
+ trackedInstallations: number,
61
+ latestReportedCounts: ReadonlyMap<string, number>,
62
+ ): InsightsOverview => {
63
+ const bundleById = new Map(bundles.map((bundle) => [bundle.id, bundle]));
64
+ const channelById = new Map(
65
+ channels.map((channel) => [channel.id, channel.name]),
66
+ );
67
+ const bundleReleases = releases
68
+ .filter(
69
+ (release): release is ReleaseRow & { readonly bundle_id: string } =>
70
+ release.kind === "BUNDLE" && release.bundle_id !== null,
71
+ )
72
+ .sort((left, right) => compareCodePoints(right.id, left.id));
73
+ const latestReleaseByBundleId = new Map<string, ReleaseRow>();
74
+ for (const release of bundleReleases) {
75
+ if (!latestReleaseByBundleId.has(release.bundle_id)) {
76
+ latestReleaseByBundleId.set(release.bundle_id, release);
77
+ }
78
+ }
79
+ const latestReportedBundles = [...latestReportedCounts]
80
+ .map(([bundleId, count]): LatestReportedBundle => {
81
+ const bundle = bundleById.get(bundleId);
82
+ return {
83
+ bundleId,
84
+ trackedInstallations: count,
85
+ observedShare:
86
+ trackedInstallations === 0 ? 0 : count / trackedInstallations,
87
+ bundle: bundle
88
+ ? toBundleMetadata(
89
+ bundle,
90
+ latestReleaseByBundleId.get(bundle.id),
91
+ channelById,
92
+ )
93
+ : null,
94
+ };
95
+ })
96
+ .sort(
97
+ (left, right) =>
98
+ right.trackedInstallations - left.trackedInstallations ||
99
+ compareCodePoints(left.bundleId, right.bundleId),
100
+ );
101
+ const configuredRollouts = bundleReleases
102
+ .filter((release) => bundleById.has(release.bundle_id))
103
+ .map(
104
+ (release): ConfiguredRollout => ({
105
+ releaseId: release.id,
106
+ bundleId: release.bundle_id,
107
+ configuredPercentage:
108
+ normalizeRolloutCohortCount(release.rollout_cohort_count) / 10,
109
+ trackedInstallations: latestReportedCounts.get(release.bundle_id) ?? 0,
110
+ bundle: toBundleMetadata(
111
+ bundleById.get(release.bundle_id)!,
112
+ release,
113
+ channelById,
114
+ ),
115
+ }),
116
+ )
117
+ .sort((left, right) => compareCodePoints(left.releaseId, right.releaseId));
118
+
119
+ return {
120
+ trackedInstallations,
121
+ mostCommonLatestReportedBundle: latestReportedBundles[0] ?? null,
122
+ latestReportedBundles,
123
+ configuredRollouts,
124
+ };
125
+ };
126
+
127
+ const countLatestInstallationBundles = (
128
+ rows: readonly LatestInstallationBundle[],
129
+ ): ReadonlyMap<string, number> => {
130
+ const counts = new Map<string, number>();
131
+ for (const row of rows) {
132
+ if (row.lastKnownBundleId === null) continue;
133
+ counts.set(
134
+ row.lastKnownBundleId,
135
+ (counts.get(row.lastKnownBundleId) ?? 0) + 1,
136
+ );
137
+ }
138
+ return counts;
139
+ };
140
+
141
+ export const createInsightsOverviewFromCounts = (
142
+ bundles: readonly Bundle[],
143
+ releases: readonly ReleaseRow[],
144
+ channels: readonly ChannelRow[],
145
+ trackedInstallations: number,
146
+ counts: readonly InstallationBundleCount[],
147
+ ): InsightsOverview =>
148
+ createOverview(
149
+ bundles,
150
+ releases,
151
+ channels,
152
+ trackedInstallations,
153
+ new Map(
154
+ counts.map(({ bundleId, installations }) => [bundleId, installations]),
155
+ ),
156
+ );
157
+
158
+ export const createInsightsOverview = (
159
+ bundles: readonly Bundle[],
160
+ releases: readonly ReleaseRow[],
161
+ channels: readonly ChannelRow[],
162
+ latestInstallations: readonly LatestInstallationBundle[],
163
+ ): InsightsOverview =>
164
+ createOverview(
165
+ bundles,
166
+ releases,
167
+ channels,
168
+ latestInstallations.length,
169
+ countLatestInstallationBundles(latestInstallations),
170
+ );
@@ -0,0 +1,218 @@
1
+ import type { Bundle, ChannelRow, ReleaseRow } from "@hot-updater/plugin-core";
2
+ import { createServerFn } from "@tanstack/react-start";
3
+
4
+ import { parseActiveInstallationInput } from "./insights-input";
5
+ import {
6
+ type InsightsOverview,
7
+ createInsightsOverviewFromCounts,
8
+ } from "./insights-overview";
9
+
10
+ const DEFAULT_INSIGHTS_PAGE_SIZE = 100;
11
+
12
+ export type InsightsCapabilities = {
13
+ readonly capabilities:
14
+ | { readonly insights: false }
15
+ | {
16
+ readonly insights: true;
17
+ readonly mode: "bounded";
18
+ readonly maxMatchingRows: number;
19
+ };
20
+ };
21
+
22
+ type BundlePage = {
23
+ readonly data: readonly Bundle[];
24
+ readonly pagination: {
25
+ readonly hasNextPage: boolean;
26
+ readonly currentPage: number;
27
+ readonly totalPages: number;
28
+ };
29
+ };
30
+
31
+ type InsightsOverviewDependencies = {
32
+ readonly runtime: unknown;
33
+ readonly getBundles: (options: {
34
+ readonly limit: number;
35
+ readonly page: number;
36
+ }) => Promise<BundlePage>;
37
+ readonly getReleases: (options: {
38
+ readonly beforeReleaseId?: string;
39
+ readonly limit: number;
40
+ }) => Promise<readonly ReleaseRow[]>;
41
+ readonly getChannels: () => Promise<readonly ChannelRow[]>;
42
+ readonly pageSize?: number;
43
+ };
44
+
45
+ export class InsightsNotSupportedError extends Error {
46
+ constructor() {
47
+ super("Insights are not supported by the configured database plugin.");
48
+ this.name = "InsightsNotSupportedError";
49
+ }
50
+ }
51
+
52
+ export class InsightsBundlePaginationError extends Error {
53
+ readonly name = "InsightsBundlePaginationError";
54
+
55
+ constructor(
56
+ readonly requestedPage: number,
57
+ readonly reason: string,
58
+ ) {
59
+ super(`Invalid bundle pagination for page ${requestedPage}: ${reason}`);
60
+ }
61
+ }
62
+
63
+ export const getInsightsCapabilities = async (
64
+ runtime: unknown,
65
+ ): Promise<InsightsCapabilities> => {
66
+ const { getInsightsCapability } = await import("./server/runtime.server");
67
+ const capability = await getInsightsCapability(runtime);
68
+ if (!capability.insights || !capability.insightsQueries) {
69
+ return { capabilities: { insights: false } };
70
+ }
71
+ return {
72
+ capabilities: {
73
+ insights: true,
74
+ mode: "bounded",
75
+ maxMatchingRows: capability.maxMatchingRows,
76
+ },
77
+ };
78
+ };
79
+
80
+ const collectBundles = async (
81
+ getBundles: InsightsOverviewDependencies["getBundles"],
82
+ pageSize: number,
83
+ ): Promise<readonly Bundle[]> => {
84
+ const bundles: Bundle[] = [];
85
+ let page = 1;
86
+ while (true) {
87
+ const result = await getBundles({ limit: pageSize, page });
88
+ const { currentPage, hasNextPage, totalPages } = result.pagination;
89
+ const isEmptyFirstPage =
90
+ page === 1 &&
91
+ currentPage === 1 &&
92
+ totalPages === 0 &&
93
+ result.data.length === 0 &&
94
+ hasNextPage === false;
95
+
96
+ if (currentPage !== page) {
97
+ throw new InsightsBundlePaginationError(
98
+ page,
99
+ `currentPage must equal ${page}, received ${currentPage}`,
100
+ );
101
+ }
102
+ if (!Number.isFinite(totalPages) || !Number.isInteger(totalPages)) {
103
+ throw new InsightsBundlePaginationError(
104
+ page,
105
+ `totalPages must be a finite integer, received ${totalPages}`,
106
+ );
107
+ }
108
+ if (totalPages < 0) {
109
+ throw new InsightsBundlePaginationError(
110
+ page,
111
+ `totalPages must be nonnegative, received ${totalPages}`,
112
+ );
113
+ }
114
+ if (!isEmptyFirstPage && totalPages < currentPage) {
115
+ throw new InsightsBundlePaginationError(
116
+ page,
117
+ `totalPages ${totalPages} is lower than currentPage ${currentPage}`,
118
+ );
119
+ }
120
+ if (!isEmptyFirstPage && hasNextPage !== currentPage < totalPages) {
121
+ throw new InsightsBundlePaginationError(
122
+ page,
123
+ "hasNextPage contradicts currentPage and totalPages",
124
+ );
125
+ }
126
+ bundles.push(...result.data);
127
+ if (!hasNextPage) {
128
+ return bundles;
129
+ }
130
+ page += 1;
131
+ }
132
+ };
133
+
134
+ const collectReleases = async (
135
+ getReleases: InsightsOverviewDependencies["getReleases"],
136
+ pageSize: number,
137
+ ): Promise<readonly ReleaseRow[]> => {
138
+ const releases: ReleaseRow[] = [];
139
+ const cursors = new Set<string>();
140
+ let beforeReleaseId: string | undefined;
141
+ while (true) {
142
+ const page = await getReleases({ beforeReleaseId, limit: pageSize });
143
+ releases.push(...page);
144
+ if (page.length < pageSize) return releases;
145
+ const nextCursor = page.at(-1)?.id;
146
+ if (!nextCursor || cursors.has(nextCursor)) {
147
+ throw new InsightsBundlePaginationError(
148
+ releases.length,
149
+ "release cursor did not advance",
150
+ );
151
+ }
152
+ cursors.add(nextCursor);
153
+ beforeReleaseId = nextCursor;
154
+ }
155
+ };
156
+
157
+ export const collectInsightsOverview = async ({
158
+ runtime,
159
+ getBundles,
160
+ getReleases,
161
+ getChannels,
162
+ pageSize = DEFAULT_INSIGHTS_PAGE_SIZE,
163
+ }: InsightsOverviewDependencies): Promise<InsightsOverview> => {
164
+ const { capabilities } = await getInsightsCapabilities(runtime);
165
+ if (!capabilities.insights) {
166
+ throw new InsightsNotSupportedError();
167
+ }
168
+
169
+ const [bundles, releases, channels] = await Promise.all([
170
+ collectBundles(getBundles, pageSize),
171
+ collectReleases(getReleases, pageSize),
172
+ getChannels(),
173
+ ]);
174
+ const overview = await (
175
+ runtime as import("@hot-updater/server").InsightsProvider
176
+ ).getBundleEventOverview();
177
+ return createInsightsOverviewFromCounts(
178
+ bundles,
179
+ releases,
180
+ channels,
181
+ overview.trackedInstallations,
182
+ overview.bundles,
183
+ );
184
+ };
185
+
186
+ export const getInsightsCapabilitiesRpc = createServerFn({
187
+ method: "GET",
188
+ }).handler(async () => {
189
+ const { prepareConfig } = await import("./server/config.server");
190
+ const { hotUpdater } = await prepareConfig();
191
+ return getInsightsCapabilities(hotUpdater);
192
+ });
193
+
194
+ export const getInsightsOverviewRpc = createServerFn({
195
+ method: "GET",
196
+ }).handler(async () => {
197
+ const { prepareConfig } = await import("./server/config.server");
198
+ const { config, databaseClient, hotUpdater } = await prepareConfig();
199
+ return collectInsightsOverview({
200
+ runtime: hotUpdater,
201
+ getBundles: (options) => databaseClient.getBundles(options),
202
+ getReleases: (options) => config.database.models.releases.findMany(options),
203
+ getChannels: () =>
204
+ config.database.models.channels.list({}).then(({ channels }) => channels),
205
+ });
206
+ });
207
+
208
+ export const getActiveInstallationOverviewRpc = createServerFn({
209
+ method: "GET",
210
+ })
211
+ .validator(parseActiveInstallationInput)
212
+ .handler(async ({ data }) => {
213
+ const { prepareConfig } = await import("./server/config.server");
214
+ const { getActiveInstallationOverview } =
215
+ await import("./server/runtime.server");
216
+ const { hotUpdater } = await prepareConfig();
217
+ return getActiveInstallationOverview(hotUpdater, data);
218
+ });
@@ -0,0 +1,43 @@
1
+ import type {
2
+ ConsoleAccess,
3
+ ConsoleAuthProvider,
4
+ ConsolePrincipal,
5
+ } from "../../index";
6
+ import { getConsoleAuthAdapter } from "./console-runtime.server";
7
+
8
+ const accessError = (
9
+ access: Exclude<ConsoleAccess, { status: "authorized" }>,
10
+ ) =>
11
+ new Response(
12
+ access.status === "unauthenticated" ? "Unauthorized" : "Forbidden",
13
+ { status: access.status === "unauthenticated" ? 401 : 403 },
14
+ );
15
+
16
+ export const getConsoleAccess = async (
17
+ request: Request,
18
+ ): Promise<ConsoleAccess> => {
19
+ const adapter = await getConsoleAuthAdapter();
20
+ return adapter.getAccess(request);
21
+ };
22
+
23
+ export const getConsoleAuthProviders = async (
24
+ request: Request,
25
+ ): Promise<readonly ConsoleAuthProvider[]> => {
26
+ const adapter = await getConsoleAuthAdapter();
27
+ return adapter.getProviders(request);
28
+ };
29
+
30
+ export const handleConsoleAuth = async (request: Request) => {
31
+ const adapter = await getConsoleAuthAdapter();
32
+ return adapter.handle(request);
33
+ };
34
+
35
+ export const requireConsoleAccess = async (
36
+ request: Request,
37
+ ): Promise<ConsolePrincipal> => {
38
+ const access = await getConsoleAccess(request);
39
+ if (access.status !== "authorized") {
40
+ throw accessError(access);
41
+ }
42
+ return access.principal;
43
+ };
@@ -0,0 +1,23 @@
1
+ import type { ConsoleSigningConfig } from "../../index";
2
+
3
+ const DEFAULT_PROVIDER = "Configured provider";
4
+
5
+ export type BundleSigningInspection =
6
+ | { readonly status: "disabled" }
7
+ | {
8
+ readonly status: "enabled";
9
+ readonly provider: string;
10
+ readonly algorithm: "RSA-SHA256";
11
+ };
12
+
13
+ export const inspectBundleSigning = async (
14
+ signing: ConsoleSigningConfig | undefined,
15
+ ): Promise<BundleSigningInspection> => {
16
+ if (!signing?.enabled) return { status: "disabled" };
17
+
18
+ return {
19
+ algorithm: "RSA-SHA256",
20
+ provider: signing.provider ?? DEFAULT_PROVIDER,
21
+ status: "enabled",
22
+ };
23
+ };