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

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 (203) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/HashValueDisplay-CB0Eskit.js +1 -0
  3. package/.output/public/assets/InsightsPageHeader-Car5LNiZ.js +1 -0
  4. package/.output/public/assets/api-keys-BzKocqRy.js +1 -0
  5. package/.output/public/assets/api-keys-api-BHcODB7q.js +1 -0
  6. package/.output/public/assets/card-Dhr_eFYj.js +1 -0
  7. package/.output/public/assets/{chart-no-axes-combined-FaYMGmQS.js → chart-no-axes-combined-Dy4WJv2U.js} +1 -1
  8. package/.output/public/assets/composite-Bp98A0Ld.js +1 -0
  9. package/.output/public/assets/createServerFn-CA8fs7QD.js +3 -0
  10. package/.output/public/assets/dialog-CUTrcPlB.js +1 -0
  11. package/.output/public/assets/{dist-DspNKfMj.js → dist-C_NydL0e.js} +1 -1
  12. package/.output/public/assets/index-DdAz-CXv.js +10 -0
  13. package/.output/public/assets/input-group-cSOG4wh_.js +1 -0
  14. package/.output/public/assets/insights-C0PBSZ-n.js +1 -0
  15. package/.output/public/assets/installations-CE33wgbY.js +2 -0
  16. package/.output/public/assets/installations-CStZja8x.js +1 -0
  17. package/.output/public/assets/lazyRouteComponent-Ccp3bwBY.js +1 -0
  18. package/.output/public/assets/link-CI8idAUi.js +1 -0
  19. package/.output/public/assets/loader-circle-BoxLrsOu.js +1 -0
  20. package/.output/public/assets/refresh-cw-Bqc72UZG.js +1 -0
  21. package/.output/public/assets/{routes-DcQnVajO.js → routes-DGVXdzpw.js} +2 -2
  22. package/.output/public/assets/routes-DTlwOr8I.js +1 -0
  23. package/.output/public/assets/{shield-check-DE_hVuHu.js → shield-check-uK3MHWJZ.js} +1 -1
  24. package/.output/public/assets/signing-CMf4aqDO.js +1 -0
  25. package/.output/public/assets/styles-BCvbtKsm.css +2 -0
  26. package/.output/public/assets/table-2vcV89QB.js +1 -0
  27. package/.output/public/assets/useCompositeListItem-CHLITE_4.js +1 -0
  28. package/.output/public/assets/x-DCd0j8c6.js +1 -0
  29. package/.output/server/{__23tanstack-start-server-fn-resolver-DBP9TQ7K.mjs → __23tanstack-start-server-fn-resolver-B7ioJhtE.mjs} +44 -52
  30. package/.output/server/_libs/@base-ui/react+[...].mjs +1906 -4040
  31. package/.output/server/_libs/@tanstack/react-router+[...].mjs +52 -4
  32. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +2 -2
  33. package/.output/server/_libs/lucide-react.mjs +36 -3
  34. package/.output/server/_libs/next-themes.mjs +2 -2
  35. package/.output/server/_libs/sonner.mjs +2 -2
  36. package/.output/server/_libs/tanstack__react-query.mjs +2 -2
  37. package/.output/server/_libs/verkit.mjs +20 -15
  38. package/.output/server/_runtime.mjs +1 -10
  39. package/.output/server/_ssr/HashValueDisplay-DwpGLfrl.mjs +55 -0
  40. package/.output/server/_ssr/InsightsPageHeader-Df4Brj8u.mjs +484 -0
  41. package/.output/server/_ssr/{api-keys-BnM_o2nS.mjs → api-keys-CZeTPQys.mjs} +11 -13
  42. package/.output/server/_ssr/{api-keys-api-CIywsSF5.mjs → api-keys-api-BUPQVSpk.mjs} +2 -2
  43. package/.output/server/_ssr/{api-keys-rpc-BSp1wkIV.mjs → api-keys-rpc-Cs7tS5_j.mjs} +5 -5
  44. package/.output/server/_ssr/{api-rpc-cG5wd_tO.mjs → api-rpc-tAupfGQm.mjs} +73 -142
  45. package/.output/server/_ssr/{auth-rpc-BrJ6obHJ.mjs → auth-rpc-DUaeOWKK.mjs} +4 -4
  46. package/.output/server/_ssr/{auth.server-DwKoZoQM.mjs → auth.server-DW71WzsD.mjs} +2 -5
  47. package/.output/server/_ssr/{bundle-signing-rpc-COoQhW8E.mjs → bundle-signing-rpc-BhnBw0-d.mjs} +2 -2
  48. package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +12 -0
  49. package/.output/server/_ssr/{alert-Dim3MGY5.mjs → card-BRiPmi0C.mjs} +61 -39
  50. package/.output/server/_ssr/{config.server-DEiLhKmy.mjs → config.server-afDuAwaG.mjs} +15 -8
  51. package/.output/server/_ssr/{createSsrRpc-9P0S0lJ3.mjs → createSsrRpc-DjWNJ0uc.mjs} +6 -6
  52. package/.output/server/_ssr/databaseJsonValue-d6I3nAS4.mjs +45 -0
  53. package/.output/server/_ssr/{databaseRows-CwbNwYSl.mjs → databaseRows-5bCRTxm-.mjs} +4 -44
  54. package/.output/server/_ssr/{deleteBundle-Cu_XOMt0.mjs → deleteBundle-BTGwBgzx.mjs} +2 -2
  55. package/.output/server/_ssr/{dialog-CQuRMNZ4.mjs → dialog-DS-nw-pA.mjs} +2 -2
  56. package/.output/server/_ssr/{dist-BVSLTpoP.mjs → dist-CyzPaI5v.mjs} +2 -2
  57. package/.output/server/_ssr/{getBundleChildren-BcRMFoDF.mjs → getBundleChildren-C0vh4ZvE.mjs} +2 -2
  58. package/.output/server/_ssr/input-group-BpsnmvZw.mjs +70 -0
  59. package/.output/server/_ssr/insights-BdS364Xu.mjs +460 -0
  60. package/.output/server/_ssr/insights-rpc-38DujK0G.mjs +57 -0
  61. package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +303 -0
  62. package/.output/server/_ssr/installations-CtA59MQG.mjs +29 -0
  63. package/.output/server/_ssr/installations-DiVKEF5T.mjs +627 -0
  64. package/.output/server/_ssr/{router-C-KUBfOi.mjs → router-B9wG_9f8.mjs} +37 -46
  65. package/.output/server/_ssr/{routes-Z7TwC6nC.mjs → routes-CKrrS2q2.mjs} +3 -3
  66. package/.output/server/_ssr/{routes-BM4ht9jb.mjs → routes-D7Yh_jfj.mjs} +402 -390
  67. package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +438 -0
  68. package/.output/server/_ssr/{server-BjzmvXnO.mjs → server-D_C2rMzf.mjs} +5 -5
  69. package/.output/server/_ssr/{signing-k8GxyBDF.mjs → signing-Dz1WkZOf.mjs} +16 -85
  70. package/.output/server/_ssr/ssr.mjs +1 -1
  71. package/.output/server/_ssr/{field-DRIXKjZG.mjs → table-CnZmon_Z.mjs} +51 -5
  72. package/.output/server/_ssr/{uuidv7-BbJ-9D_I.mjs → uuidv7-D0QFkPL_.mjs} +3 -3
  73. package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +97 -0
  74. package/.output/server/index.mjs +168 -203
  75. package/lib/index.d.mts +4 -4
  76. package/lib/vite-internal.mjs +1 -1
  77. package/lib/vite.d.mts +0 -1
  78. package/package.json +8 -8
  79. package/src/components/AppSidebar.tsx +12 -16
  80. package/src/components/ConsoleAccessPage.tsx +1 -1
  81. package/src/components/HashValueDisplay.tsx +11 -2
  82. package/src/components/features/api-keys/ApiKeysPage.tsx +1 -1
  83. package/src/components/features/bundles/BundleChildrenPanel.tsx +7 -6
  84. package/src/components/features/bundles/BundleMetadata.tsx +1 -20
  85. package/src/components/features/channels/ChannelManagementDialog.tsx +1 -1
  86. package/src/components/features/insights/EventDetails.tsx +151 -0
  87. package/src/components/features/insights/EventHistoryCard.tsx +245 -0
  88. package/src/components/features/insights/EventHistoryList.tsx +63 -0
  89. package/src/components/features/insights/InsightsBundleReports.tsx +51 -0
  90. package/src/components/features/insights/InsightsControls.tsx +121 -0
  91. package/src/components/features/{analytics/AnalyticsErrorAlert.tsx → insights/InsightsErrorAlert.tsx} +6 -6
  92. package/src/components/features/insights/InsightsOverview.tsx +203 -0
  93. package/src/components/features/insights/InsightsPageHeader.tsx +48 -0
  94. package/src/components/features/insights/InsightsPagination.tsx +52 -0
  95. package/src/components/features/insights/InstallationHistoryCard.tsx +250 -0
  96. package/src/components/features/insights/InstallationMatchesCard.tsx +178 -0
  97. package/src/components/features/insights/InstallationPageHeader.tsx +90 -0
  98. package/src/components/features/releases/ReleaseEditorSheet.tsx +24 -24
  99. package/src/components/features/signing/BundleSigningPage.tsx +18 -126
  100. package/src/components/ui/badge.tsx +2 -0
  101. package/src/components/ui/combobox.tsx +1 -0
  102. package/src/components/ui/skeleton.tsx +4 -1
  103. package/src/components/ui/tooltip.tsx +1 -1
  104. package/src/index.ts +7 -3
  105. package/src/lib/api-rpc.ts +19 -90
  106. package/src/lib/api.ts +0 -99
  107. package/src/lib/insights-api.ts +100 -0
  108. package/src/lib/insights-rpc.ts +88 -0
  109. package/src/lib/insights-view.ts +15 -0
  110. package/src/lib/public-bundle-error.ts +19 -0
  111. package/src/lib/server/bundle-signing.server.ts +5 -82
  112. package/src/lib/server/console-runtime.server.ts +0 -9
  113. package/src/lib/server/releaseReachability.ts +1 -4
  114. package/src/lib/server/runtime.server.ts +6 -142
  115. package/src/routeTree.gen.ts +21 -21
  116. package/src/router.tsx +3 -0
  117. package/src/routes/-installations-search.ts +25 -12
  118. package/src/routes/__root.tsx +6 -16
  119. package/src/routes/api-keys.tsx +1 -1
  120. package/src/routes/index.tsx +21 -81
  121. package/src/routes/insights.tsx +84 -0
  122. package/src/routes/installations.tsx +294 -142
  123. package/src/styles.css +6 -0
  124. package/.output/public/assets/AnalyticsCapabilityContext-BCke57-7.js +0 -1
  125. package/.output/public/assets/BundleActivityChart-CnsBe_1J.js +0 -42
  126. package/.output/public/assets/BundleIdDisplay-Dky75zLU.js +0 -1
  127. package/.output/public/assets/alert-C-1KvDW8.js +0 -1
  128. package/.output/public/assets/analytics-Bqz0c-BT.js +0 -1
  129. package/.output/public/assets/analytics-api-CJdrTLLO.js +0 -1
  130. package/.output/public/assets/api-B_Rh-uzC.js +0 -1
  131. package/.output/public/assets/api-keys-BurCFhzH.js +0 -1
  132. package/.output/public/assets/api-keys-api-BEDJdQg3.js +0 -1
  133. package/.output/public/assets/badge-Bf0fZ-Gx.js +0 -1
  134. package/.output/public/assets/createServerFn-gM2-LFBy.js +0 -3
  135. package/.output/public/assets/dialog-BS60BRr2.js +0 -1
  136. package/.output/public/assets/download-O-yNBc9H.js +0 -1
  137. package/.output/public/assets/field-Buq148-A.js +0 -1
  138. package/.output/public/assets/index-kFhd2S78.js +0 -10
  139. package/.output/public/assets/installations-BrQ-od41.js +0 -2
  140. package/.output/public/assets/installations-CGlZ3ROz.js +0 -1
  141. package/.output/public/assets/lazyRouteComponent-rPzgA7S-.js +0 -1
  142. package/.output/public/assets/loader-circle-DCYF-wJl.js +0 -1
  143. package/.output/public/assets/refresh-cw-n8XYbPPj.js +0 -1
  144. package/.output/public/assets/routes-CrdokJ80.js +0 -1
  145. package/.output/public/assets/search-DLhbWw3W.js +0 -1
  146. package/.output/public/assets/signing-HRexfjsH.js +0 -1
  147. package/.output/public/assets/styles-eB4EtvFS.css +0 -2
  148. package/.output/public/assets/table-D8CLieHX.js +0 -1
  149. package/.output/public/assets/textarea-YJLTVAoq.js +0 -1
  150. package/.output/public/assets/useMutation-JsGVjQMY.js +0 -1
  151. package/.output/public/assets/useOpenInteractionType-4cRD5xO0.js +0 -1
  152. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +0 -1554
  153. package/.output/server/_libs/d3-array.mjs +0 -231
  154. package/.output/server/_libs/d3-color.mjs +0 -330
  155. package/.output/server/_libs/d3-format.mjs +0 -252
  156. package/.output/server/_libs/d3-interpolate.mjs +0 -207
  157. package/.output/server/_libs/d3-path.mjs +0 -77
  158. package/.output/server/_libs/d3-scale+[...].mjs +0 -1907
  159. package/.output/server/_libs/d3-shape.mjs +0 -818
  160. package/.output/server/_libs/decimal.js-light.mjs +0 -884
  161. package/.output/server/_libs/es-toolkit.mjs +0 -1134
  162. package/.output/server/_libs/eventemitter3.mjs +0 -236
  163. package/.output/server/_libs/immer.mjs +0 -532
  164. package/.output/server/_libs/react-redux+[...].mjs +0 -205
  165. package/.output/server/_libs/recharts+[...].mjs +0 -14117
  166. package/.output/server/_ssr/AnalyticsCapabilityContext-ui4DX8GO.mjs +0 -15
  167. package/.output/server/_ssr/BundleActivityChart-BiB9ZORH.mjs +0 -288
  168. package/.output/server/_ssr/BundleIdDisplay-Czk6iR6p.mjs +0 -13
  169. package/.output/server/_ssr/analytics-DtMTn8fn.mjs +0 -858
  170. package/.output/server/_ssr/analytics-api-Cg1M5v_H.mjs +0 -71
  171. package/.output/server/_ssr/analytics-input-fRRRn0uA.mjs +0 -96
  172. package/.output/server/_ssr/analytics-rpc-DY5uXvHT.mjs +0 -155
  173. package/.output/server/_ssr/api-BTSv1tZb.mjs +0 -282
  174. package/.output/server/_ssr/badge-DCxbEeof.mjs +0 -26
  175. package/.output/server/_ssr/bundle-signing.server-D5u8KkMp.mjs +0 -51
  176. package/.output/server/_ssr/installations-B-azgAvX.mjs +0 -19
  177. package/.output/server/_ssr/installations-Ctvzp2JH.mjs +0 -535
  178. package/.output/server/_ssr/runtime.server-awTLiiXW.mjs +0 -527
  179. package/.output/server/_ssr/table-v1yCvYo7.mjs +0 -53
  180. package/.output/server/_ssr/textarea-DYHnskhb.mjs +0 -14
  181. package/.output/server/_tanstack-start-manifest_v-CH4StzJu.mjs +0 -108
  182. package/src/components/features/analytics/ActivityChart.tsx +0 -150
  183. package/src/components/features/analytics/AnalyticsCapabilityContext.tsx +0 -24
  184. package/src/components/features/analytics/AnalyticsControls.tsx +0 -115
  185. package/src/components/features/analytics/AnalyticsOverview.tsx +0 -208
  186. package/src/components/features/analytics/BundleDistribution.tsx +0 -133
  187. package/src/components/features/analytics/BundleSelector.tsx +0 -114
  188. package/src/components/features/analytics/InstallationHistoryCard.tsx +0 -248
  189. package/src/components/features/analytics/InstallationMatchesCard.tsx +0 -132
  190. package/src/components/features/analytics/InstallationPageHeader.tsx +0 -138
  191. package/src/components/features/analytics/InstallationPagination.tsx +0 -48
  192. package/src/components/features/analytics/InstallationSearch.tsx +0 -230
  193. package/src/components/features/analytics/UpdateOutcomes.tsx +0 -147
  194. package/src/components/features/bundles/BundleActivityChart.tsx +0 -193
  195. package/src/components/features/bundles/BundleAnalyticsSummary.tsx +0 -123
  196. package/src/lib/analytics-api.ts +0 -145
  197. package/src/lib/analytics-error.ts +0 -19
  198. package/src/lib/analytics-input.ts +0 -187
  199. package/src/lib/analytics-overview.ts +0 -170
  200. package/src/lib/analytics-rpc.ts +0 -218
  201. package/src/lib/server/releaseActivity.ts +0 -27
  202. package/src/routes/analytics.tsx +0 -139
  203. /package/.output/public/assets/{preload-helper-TKOBYGYD.js → preload-helper-CQ36nxok.js} +0 -0
@@ -0,0 +1,88 @@
1
+ import type {
2
+ ReportingOverview,
3
+ InsightsScope,
4
+ InsightsEventPageInput,
5
+ InsightsInstallationEventPageInput,
6
+ ActiveInstallationWindow,
7
+ InsightsProvider,
8
+ } from "@hot-updater/server";
9
+ import { createServerFn } from "@tanstack/react-start";
10
+
11
+ import type {
12
+ InsightsInstallationViewRow,
13
+ InsightsViewPage,
14
+ } from "./insights-view";
15
+
16
+ export type InsightsWindow = ActiveInstallationWindow;
17
+ export type ReportingInstallations = ReportingOverview;
18
+ export type InsightsOverviewInput = InsightsScope & {
19
+ readonly window: InsightsWindow;
20
+ readonly bundleId?: string;
21
+ };
22
+
23
+ type EventsPageInput = InsightsEventPageInput;
24
+
25
+ type InstallationEventsPageInput = InsightsInstallationEventPageInput;
26
+
27
+ type InstallationsPageInput = {
28
+ readonly cursor?: string;
29
+ readonly identity: string;
30
+ readonly limit: number;
31
+ };
32
+
33
+ const runtime = async (): Promise<InsightsProvider> => {
34
+ const { prepareConfig } = await import("./server/config.server");
35
+ const { hotUpdater } = await prepareConfig();
36
+ return hotUpdater;
37
+ };
38
+
39
+ const readOverview = (input: InsightsOverviewInput) => input;
40
+ const readEventsPage = (input: EventsPageInput) => input;
41
+ const readInstallationEventsPage = (input: InstallationEventsPageInput) =>
42
+ input;
43
+ const readInstallationPage = (input: InstallationsPageInput) => input;
44
+ const readInstallId = (input: { readonly installId: string }) => input;
45
+
46
+ export const getReportingInstallationsRpc = createServerFn({ method: "GET" })
47
+ .validator(readOverview)
48
+ .handler(async ({ data }) => (await runtime()).getReportingOverview(data));
49
+
50
+ export const listInsightsEventsRpc = createServerFn({ method: "GET" })
51
+ .validator(readEventsPage)
52
+ .handler(async ({ data }) => (await runtime()).listEvents(data));
53
+
54
+ export const listInsightsInstallationEventsRpc = createServerFn({
55
+ method: "GET",
56
+ })
57
+ .validator(readInstallationEventsPage)
58
+ .handler(async ({ data }) => (await runtime()).listInstallationEvents(data));
59
+
60
+ export const getInsightsInstallationRpc = createServerFn({ method: "GET" })
61
+ .validator(readInstallId)
62
+ .handler(async ({ data }) => (await runtime()).getInstallation(data));
63
+
64
+ export const findInsightsInstallationsRpc = createServerFn({ method: "GET" })
65
+ .validator(readInstallationPage)
66
+ .handler(async ({ data }) => {
67
+ const insights = await runtime();
68
+ const install = data.cursor
69
+ ? null
70
+ : await insights.getInstallation({ installId: data.identity });
71
+ const matches = await insights.pageInstallationsByCurrentUserId({
72
+ cursor: data.cursor,
73
+ limit: Math.max(1, data.limit - (install === null ? 0 : 1)),
74
+ userId: data.identity,
75
+ });
76
+ return {
77
+ data:
78
+ install === null
79
+ ? matches.data
80
+ : [
81
+ install,
82
+ ...matches.data.filter(
83
+ ({ installId }) => installId !== install.installId,
84
+ ),
85
+ ],
86
+ nextCursor: matches.nextCursor,
87
+ } satisfies InsightsViewPage<InsightsInstallationViewRow>;
88
+ });
@@ -0,0 +1,15 @@
1
+ import type { EventHistoryRow, InstallationRow } from "@hot-updater/server";
2
+
3
+ export type InsightsEventRow = EventHistoryRow;
4
+ export type InsightsInstallationViewRow = InstallationRow;
5
+
6
+ export type InsightsViewPage<TRow> = {
7
+ readonly data: readonly TRow[];
8
+ readonly nextCursor: string | null;
9
+ };
10
+
11
+ export const outcomeLabels = {
12
+ applied: "Applied reports",
13
+ recovered: "Recovered-from reports",
14
+ adopted: "Adopted reports",
15
+ } as const;
@@ -0,0 +1,19 @@
1
+ import { ReleaseManagementError } from "@hot-updater/plugin-core";
2
+
3
+ export const withPublicBundleMutationErrors = async <T>(
4
+ action: () => Promise<T>,
5
+ ): Promise<T> => {
6
+ try {
7
+ return await action();
8
+ } catch (error) {
9
+ if (error instanceof ReleaseManagementError) {
10
+ throw new Error(
11
+ error.message
12
+ .replace(/\bRelease\b/g, "Bundle")
13
+ .replace("Bundle Bundle", "deployed Bundle"),
14
+ { cause: error },
15
+ );
16
+ }
17
+ throw error;
18
+ }
19
+ };
@@ -1,100 +1,23 @@
1
- import { createHash, createPublicKey } from "node:crypto";
2
- import { readFile, stat } from "node:fs/promises";
3
- import path from "node:path";
4
-
5
1
  import type { ConsoleSigningConfig } from "../../index";
6
2
 
7
- const MAX_PUBLIC_KEY_BYTES = 16 * 1024;
8
3
  const DEFAULT_PROVIDER = "Configured provider";
9
- const PUBLIC_KEY_HEADER = "-----BEGIN PUBLIC KEY-----";
10
- const PUBLIC_KEY_FOOTER = "-----END PUBLIC KEY-----";
11
4
 
12
5
  export type BundleSigningInspection =
13
6
  | { readonly status: "disabled" }
14
- | {
15
- readonly status: "misconfigured";
16
- readonly provider: string;
17
- readonly message: string;
18
- }
19
7
  | {
20
8
  readonly status: "enabled";
21
9
  readonly provider: string;
22
10
  readonly algorithm: "RSA-SHA256";
23
- readonly fingerprint: string;
24
- readonly publicKey: string;
25
11
  };
26
12
 
27
- const misconfigured = (
28
- provider: string,
29
- message: string,
30
- ): BundleSigningInspection => ({ message, provider, status: "misconfigured" });
31
-
32
- const isSpkiPublicKeyPem = (value: string): boolean => {
33
- const trimmed = value.trim();
34
- return (
35
- trimmed.startsWith(PUBLIC_KEY_HEADER) &&
36
- trimmed.endsWith(PUBLIC_KEY_FOOTER) &&
37
- !trimmed.includes("PRIVATE KEY")
38
- );
39
- };
40
-
41
13
  export const inspectBundleSigning = async (
42
14
  signing: ConsoleSigningConfig | undefined,
43
15
  ): Promise<BundleSigningInspection> => {
44
16
  if (!signing?.enabled) return { status: "disabled" };
45
17
 
46
- const provider = signing.provider ?? DEFAULT_PROVIDER;
47
- if (!signing.publicKeyPath) {
48
- return misconfigured(provider, "A public key file is not configured.");
49
- }
50
-
51
- try {
52
- const publicKeyPath = path.isAbsolute(signing.publicKeyPath)
53
- ? signing.publicKeyPath
54
- : path.resolve(process.cwd(), signing.publicKeyPath);
55
- const publicKeyStat = await stat(publicKeyPath);
56
- if (!publicKeyStat.isFile() || publicKeyStat.size > MAX_PUBLIC_KEY_BYTES) {
57
- return misconfigured(
58
- provider,
59
- "The configured public key could not be loaded.",
60
- );
61
- }
62
-
63
- const publicKeyPem = await readFile(publicKeyPath, "utf8");
64
- if (!isSpkiPublicKeyPem(publicKeyPem)) {
65
- return misconfigured(
66
- provider,
67
- "The configured public key is not a valid SPKI public key.",
68
- );
69
- }
70
-
71
- const publicKey = createPublicKey(publicKeyPem);
72
- if (publicKey.asymmetricKeyType !== "rsa") {
73
- return misconfigured(
74
- provider,
75
- "The configured public key is not an RSA public key.",
76
- );
77
- }
78
- if ((publicKey.asymmetricKeyDetails?.modulusLength ?? 0) < 2048) {
79
- return misconfigured(
80
- provider,
81
- "The configured RSA public key must be at least 2048 bits.",
82
- );
83
- }
84
-
85
- const canonicalDer = publicKey.export({ format: "der", type: "spki" });
86
- const canonicalPem = publicKey.export({ format: "pem", type: "spki" });
87
- return {
88
- algorithm: "RSA-SHA256",
89
- fingerprint: createHash("sha256").update(canonicalDer).digest("hex"),
90
- provider,
91
- publicKey: canonicalPem.toString().trim(),
92
- status: "enabled",
93
- };
94
- } catch {
95
- return misconfigured(
96
- provider,
97
- "The configured public key could not be loaded.",
98
- );
99
- }
18
+ return {
19
+ algorithm: "RSA-SHA256",
20
+ provider: signing.provider ?? DEFAULT_PROVIDER,
21
+ status: "enabled",
22
+ };
100
23
  };
@@ -1,5 +1,3 @@
1
- import path from "node:path";
2
-
3
1
  import type {
4
2
  ConsoleAuthAdapter,
5
3
  ConsoleSigningConfig,
@@ -34,16 +32,9 @@ export const sanitizeConsoleSigningConfig = (
34
32
  )
35
33
  return undefined;
36
34
 
37
- // Console never reads private material, even for legacy local configs.
38
- const publicKeyPath =
39
- readString(signing, "publicKeyPath") ??
40
- (privateKeyPath
41
- ? path.join(path.dirname(privateKeyPath), "public-key.pem")
42
- : undefined);
43
35
  return {
44
36
  enabled: true,
45
37
  provider: getProviderName(signing),
46
- ...(publicKeyPath === undefined ? {} : { publicKeyPath }),
47
38
  };
48
39
  };
49
40
 
@@ -7,15 +7,12 @@ import type {
7
7
  ReleaseCatalogModel,
8
8
  ReleaseRow,
9
9
  } from "@hot-updater/plugin-core";
10
- import type { BundleEventSummary } from "@hot-updater/server";
11
10
 
12
11
  export type ReleaseReachabilityRow = ReleaseRow & {
13
12
  readonly currentlyUnreachable: boolean;
14
13
  };
15
14
 
16
- export type ReleaseListRow = ReleaseReachabilityRow & {
17
- readonly activity30d: BundleEventSummary | null;
18
- };
15
+ export type ReleaseListRow = ReleaseReachabilityRow;
19
16
 
20
17
  const NON_TARGETED_COHORT = "release-catalog-non-targeted-cohort";
21
18
 
@@ -1,47 +1,17 @@
1
1
  import type {
2
- AnalyticsModel,
3
2
  ApiKeyModel,
4
3
  BundleRepository,
4
+ DatabasePlugin,
5
5
  } from "@hot-updater/plugin-core";
6
6
  import {
7
- type ActiveInstallationInput,
8
- type AnalyticsProvider,
9
- createAnalyticsProvider,
10
- type InstallationHistoryRow,
11
- type InstallationSearchRow,
12
- type OffsetPaginationResult,
7
+ createInsightsProvider,
8
+ type InsightsProvider,
13
9
  } from "@hot-updater/server";
14
10
 
15
- import {
16
- parseActiveInstallationInput,
17
- parseBundleEventAnalyticsInput,
18
- parseBundleEventSummaryInput,
19
- parseInstallationHistoryInput,
20
- parseSearchInstallationsInput,
21
- } from "../analytics-input";
22
-
23
- export type InstallationSearchResult =
24
- OffsetPaginationResult<InstallationSearchRow>;
25
- export type InstallationHistoryResult =
26
- OffsetPaginationResult<InstallationHistoryRow>;
27
-
28
11
  export function createRuntimeHotUpdater(config: {
29
- readonly database: BundleRepository;
30
- }): AnalyticsProvider | null {
31
- const models: unknown = Reflect.get(config.database, "models");
32
- const analytics: unknown =
33
- typeof models === "object" && models !== null
34
- ? Reflect.get(models, "analytics")
35
- : undefined;
36
- if (
37
- typeof analytics !== "object" ||
38
- analytics === null ||
39
- typeof Reflect.get(analytics, "append") !== "function" ||
40
- typeof Reflect.get(analytics, "scan") !== "function"
41
- ) {
42
- return null;
43
- }
44
- return createAnalyticsProvider(analytics as AnalyticsModel);
12
+ readonly database: DatabasePlugin;
13
+ }): InsightsProvider {
14
+ return createInsightsProvider(config.database.models.insights);
45
15
  }
46
16
 
47
17
  export function createApiKeyStore(config: {
@@ -64,109 +34,3 @@ export function createApiKeyStore(config: {
64
34
  }
65
35
  return apiKeys as ApiKeyModel;
66
36
  }
67
-
68
- const providerMethods = [
69
- "appendBundleEvent",
70
- "getBundleEventSummary",
71
- "getBundleEventSummaries",
72
- "getBundleEventAnalytics",
73
- "getBundleEventOverview",
74
- "getActiveInstallationOverview",
75
- "searchInstallations",
76
- "getInstallationHistory",
77
- ] as const;
78
-
79
- const parseAnalyticsProvider = (value: unknown): AnalyticsProvider | null => {
80
- if (
81
- typeof value !== "object" ||
82
- value === null ||
83
- Reflect.get(value, "mode") !== "bounded" ||
84
- typeof Reflect.get(value, "maxMatchingRows") !== "number" ||
85
- !providerMethods.every(
86
- (method) => typeof Reflect.get(value, method) === "function",
87
- )
88
- ) {
89
- return null;
90
- }
91
- return value as AnalyticsProvider;
92
- };
93
-
94
- export const getAnalyticsCapability = async (provider: unknown) => {
95
- const parsed = parseAnalyticsProvider(provider);
96
- return parsed === null
97
- ? ({
98
- analytics: false,
99
- analyticsQueries: false,
100
- eventIngestion: false,
101
- } as const)
102
- : ({
103
- analytics: true,
104
- analyticsQueries: true,
105
- eventIngestion: true,
106
- maxMatchingRows: parsed.maxMatchingRows,
107
- mode: "bounded",
108
- } as const);
109
- };
110
-
111
- const requireAnalyticsSupport = async (
112
- provider: unknown,
113
- ): Promise<AnalyticsProvider> => {
114
- const parsedProvider = parseAnalyticsProvider(provider);
115
- if (parsedProvider === null) {
116
- throw new Error(
117
- "Analytics are not supported by the configured database plugin.",
118
- );
119
- }
120
- return parsedProvider;
121
- };
122
-
123
- export async function getBundleEventSummary(provider: unknown, input: unknown) {
124
- const { bundleId } = parseBundleEventSummaryInput(input);
125
- return (await requireAnalyticsSupport(provider)).getBundleEventSummary(
126
- bundleId,
127
- );
128
- }
129
-
130
- export async function getActiveInstallationOverview(
131
- provider: unknown,
132
- input: unknown,
133
- ) {
134
- const parsed: ActiveInstallationInput = parseActiveInstallationInput(input);
135
- return (
136
- await requireAnalyticsSupport(provider)
137
- ).getActiveInstallationOverview(parsed);
138
- }
139
-
140
- export async function getBundleEventAnalytics(
141
- provider: unknown,
142
- input: unknown,
143
- ) {
144
- const parsed = parseBundleEventAnalyticsInput(input);
145
- return (await requireAnalyticsSupport(provider)).getBundleEventAnalytics(
146
- parsed.bundleId,
147
- parsed.window,
148
- parsed.limit ?? 50,
149
- parsed.offset ?? 0,
150
- );
151
- }
152
-
153
- export async function searchInstallations(provider: unknown, input: unknown) {
154
- const parsed = parseSearchInstallationsInput(input);
155
- return (await requireAnalyticsSupport(provider)).searchInstallations(
156
- parsed.query,
157
- parsed.limit ?? 50,
158
- parsed.offset ?? 0,
159
- );
160
- }
161
-
162
- export async function getInstallationHistory(
163
- provider: unknown,
164
- input: unknown,
165
- ) {
166
- const parsed = parseInstallationHistoryInput(input);
167
- return (await requireAnalyticsSupport(provider)).getInstallationHistory(
168
- parsed.installId,
169
- parsed.limit ?? 50,
170
- parsed.offset ?? 0,
171
- );
172
- }
@@ -11,8 +11,8 @@
11
11
  import { Route as rootRouteImport } from './routes/__root'
12
12
  import { Route as SigningRouteImport } from './routes/signing'
13
13
  import { Route as InstallationsRouteImport } from './routes/installations'
14
+ import { Route as InsightsRouteImport } from './routes/insights'
14
15
  import { Route as ApiKeysRouteImport } from './routes/api-keys'
15
- import { Route as AnalyticsRouteImport } from './routes/analytics'
16
16
  import { Route as IndexRouteImport } from './routes/index'
17
17
  import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
18
18
  import { Route as ApiBundlesBundleIdDownloadRouteImport } from './routes/api/bundles/$bundleId/download'
@@ -27,16 +27,16 @@ const InstallationsRoute = InstallationsRouteImport.update({
27
27
  path: '/installations',
28
28
  getParentRoute: () => rootRouteImport,
29
29
  } as any)
30
+ const InsightsRoute = InsightsRouteImport.update({
31
+ id: '/insights',
32
+ path: '/insights',
33
+ getParentRoute: () => rootRouteImport,
34
+ } as any)
30
35
  const ApiKeysRoute = ApiKeysRouteImport.update({
31
36
  id: '/api-keys',
32
37
  path: '/api-keys',
33
38
  getParentRoute: () => rootRouteImport,
34
39
  } as any)
35
- const AnalyticsRoute = AnalyticsRouteImport.update({
36
- id: '/analytics',
37
- path: '/analytics',
38
- getParentRoute: () => rootRouteImport,
39
- } as any)
40
40
  const IndexRoute = IndexRouteImport.update({
41
41
  id: '/',
42
42
  path: '/',
@@ -56,8 +56,8 @@ const ApiBundlesBundleIdDownloadRoute =
56
56
 
57
57
  export interface FileRoutesByFullPath {
58
58
  '/': typeof IndexRoute
59
- '/analytics': typeof AnalyticsRoute
60
59
  '/api-keys': typeof ApiKeysRoute
60
+ '/insights': typeof InsightsRoute
61
61
  '/installations': typeof InstallationsRoute
62
62
  '/signing': typeof SigningRoute
63
63
  '/api/auth/$': typeof ApiAuthSplatRoute
@@ -65,8 +65,8 @@ export interface FileRoutesByFullPath {
65
65
  }
66
66
  export interface FileRoutesByTo {
67
67
  '/': typeof IndexRoute
68
- '/analytics': typeof AnalyticsRoute
69
68
  '/api-keys': typeof ApiKeysRoute
69
+ '/insights': typeof InsightsRoute
70
70
  '/installations': typeof InstallationsRoute
71
71
  '/signing': typeof SigningRoute
72
72
  '/api/auth/$': typeof ApiAuthSplatRoute
@@ -75,8 +75,8 @@ export interface FileRoutesByTo {
75
75
  export interface FileRoutesById {
76
76
  __root__: typeof rootRouteImport
77
77
  '/': typeof IndexRoute
78
- '/analytics': typeof AnalyticsRoute
79
78
  '/api-keys': typeof ApiKeysRoute
79
+ '/insights': typeof InsightsRoute
80
80
  '/installations': typeof InstallationsRoute
81
81
  '/signing': typeof SigningRoute
82
82
  '/api/auth/$': typeof ApiAuthSplatRoute
@@ -86,8 +86,8 @@ export interface FileRouteTypes {
86
86
  fileRoutesByFullPath: FileRoutesByFullPath
87
87
  fullPaths:
88
88
  | '/'
89
- | '/analytics'
90
89
  | '/api-keys'
90
+ | '/insights'
91
91
  | '/installations'
92
92
  | '/signing'
93
93
  | '/api/auth/$'
@@ -95,8 +95,8 @@ export interface FileRouteTypes {
95
95
  fileRoutesByTo: FileRoutesByTo
96
96
  to:
97
97
  | '/'
98
- | '/analytics'
99
98
  | '/api-keys'
99
+ | '/insights'
100
100
  | '/installations'
101
101
  | '/signing'
102
102
  | '/api/auth/$'
@@ -104,8 +104,8 @@ export interface FileRouteTypes {
104
104
  id:
105
105
  | '__root__'
106
106
  | '/'
107
- | '/analytics'
108
107
  | '/api-keys'
108
+ | '/insights'
109
109
  | '/installations'
110
110
  | '/signing'
111
111
  | '/api/auth/$'
@@ -114,8 +114,8 @@ export interface FileRouteTypes {
114
114
  }
115
115
  export interface RootRouteChildren {
116
116
  IndexRoute: typeof IndexRoute
117
- AnalyticsRoute: typeof AnalyticsRoute
118
117
  ApiKeysRoute: typeof ApiKeysRoute
118
+ InsightsRoute: typeof InsightsRoute
119
119
  InstallationsRoute: typeof InstallationsRoute
120
120
  SigningRoute: typeof SigningRoute
121
121
  ApiAuthSplatRoute: typeof ApiAuthSplatRoute
@@ -138,6 +138,13 @@ declare module '@tanstack/react-router' {
138
138
  preLoaderRoute: typeof InstallationsRouteImport
139
139
  parentRoute: typeof rootRouteImport
140
140
  }
141
+ '/insights': {
142
+ id: '/insights'
143
+ path: '/insights'
144
+ fullPath: '/insights'
145
+ preLoaderRoute: typeof InsightsRouteImport
146
+ parentRoute: typeof rootRouteImport
147
+ }
141
148
  '/api-keys': {
142
149
  id: '/api-keys'
143
150
  path: '/api-keys'
@@ -145,13 +152,6 @@ declare module '@tanstack/react-router' {
145
152
  preLoaderRoute: typeof ApiKeysRouteImport
146
153
  parentRoute: typeof rootRouteImport
147
154
  }
148
- '/analytics': {
149
- id: '/analytics'
150
- path: '/analytics'
151
- fullPath: '/analytics'
152
- preLoaderRoute: typeof AnalyticsRouteImport
153
- parentRoute: typeof rootRouteImport
154
- }
155
155
  '/': {
156
156
  id: '/'
157
157
  path: '/'
@@ -178,8 +178,8 @@ declare module '@tanstack/react-router' {
178
178
 
179
179
  const rootRouteChildren: RootRouteChildren = {
180
180
  IndexRoute: IndexRoute,
181
- AnalyticsRoute: AnalyticsRoute,
182
181
  ApiKeysRoute: ApiKeysRoute,
182
+ InsightsRoute: InsightsRoute,
183
183
  InstallationsRoute: InstallationsRoute,
184
184
  SigningRoute: SigningRoute,
185
185
  ApiAuthSplatRoute: ApiAuthSplatRoute,
package/src/router.tsx CHANGED
@@ -4,6 +4,8 @@ import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query
4
4
  // Import the route tree selected by the local or hosted Vite integration.
5
5
  import { routeTree } from "virtual:hot-updater-console/route-tree";
6
6
 
7
+ import { getInsightsScrollRestorationKey } from "./routes/-installations-search";
8
+
7
9
  // Create a new router instance
8
10
  export const getRouter = () => {
9
11
  const queryClient = new QueryClient();
@@ -12,6 +14,7 @@ export const getRouter = () => {
12
14
  context: { queryClient },
13
15
 
14
16
  scrollRestoration: true,
17
+ getScrollRestorationKey: getInsightsScrollRestorationKey,
15
18
  defaultPreloadStaleTime: 0,
16
19
  });
17
20
 
@@ -1,19 +1,32 @@
1
+ import type { ParsedLocation } from "@tanstack/react-router";
2
+
3
+ const readCursor = (value: unknown): string | undefined =>
4
+ typeof value === "string" && value.length > 0 ? value : undefined;
5
+
6
+ const readTimestamp = (value: unknown): number | undefined =>
7
+ typeof value === "number" && Number.isSafeInteger(value) && value >= 0
8
+ ? value
9
+ : undefined;
10
+
11
+ export function getInsightsScrollRestorationKey(location: ParsedLocation) {
12
+ if (location.pathname === "/installations") {
13
+ const search = validateInstallationsSearch(location.search);
14
+ if (search.query === undefined && search.installId === undefined) {
15
+ return `/installations?eventsBefore=${search.eventsBefore ?? "new"}&eventsCursor=${search.eventsCursor ?? "first"}`;
16
+ }
17
+ }
18
+ return location.state.__TSR_key!;
19
+ }
20
+
1
21
  export function validateInstallationsSearch(search: Record<string, unknown>) {
2
22
  return {
3
23
  query: typeof search.query === "string" ? search.query : undefined,
4
24
  installId:
5
25
  typeof search.installId === "string" ? search.installId : undefined,
6
- searchOffset:
7
- typeof search.searchOffset === "number" &&
8
- Number.isSafeInteger(search.searchOffset) &&
9
- search.searchOffset >= 0
10
- ? search.searchOffset
11
- : 0,
12
- historyOffset:
13
- typeof search.historyOffset === "number" &&
14
- Number.isSafeInteger(search.historyOffset) &&
15
- search.historyOffset >= 0
16
- ? search.historyOffset
17
- : 0,
26
+ eventsCursor: readCursor(search.eventsCursor),
27
+ eventsBefore: readTimestamp(search.eventsBefore),
28
+ searchCursor: readCursor(search.searchCursor),
29
+ historyCursor: readCursor(search.historyCursor),
30
+ historyBefore: readTimestamp(search.historyBefore),
18
31
  };
19
32
  }
@@ -9,16 +9,11 @@ import { useEffect, useState } from "react";
9
9
 
10
10
  import { AppSidebar } from "@/components/AppSidebar";
11
11
  import { ConsoleAccessPage } from "@/components/ConsoleAccessPage";
12
- import { AnalyticsCapabilityProvider } from "@/components/features/analytics/AnalyticsCapabilityContext";
13
12
  import { NotFoundPage } from "@/components/NotFoundPage";
14
13
  import { ThemeProvider } from "@/components/ThemeProvider";
15
14
  import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
16
15
  import { Toaster } from "@/components/ui/sonner";
17
16
  import { TooltipProvider } from "@/components/ui/tooltip";
18
- import {
19
- getAnalyticsCapabilityState,
20
- useAnalyticsCapabilitiesQuery,
21
- } from "@/lib/analytics-api";
22
17
  import {
23
18
  getConsoleAccessRpc,
24
19
  getConsoleAuthProvidersRpc,
@@ -149,9 +144,6 @@ function RootLayout() {
149
144
  }
150
145
 
151
146
  function AuthorizedConsole() {
152
- const capabilityQuery = useAnalyticsCapabilitiesQuery();
153
- const capability = getAnalyticsCapabilityState(capabilityQuery);
154
-
155
147
  useEffect(() => {
156
148
  if (
157
149
  import.meta.env.DEV &&
@@ -162,13 +154,11 @@ function AuthorizedConsole() {
162
154
  }
163
155
  }, []);
164
156
  return (
165
- <AnalyticsCapabilityProvider value={capability}>
166
- <SidebarProvider>
167
- <AppSidebar />
168
- <SidebarInset className="min-h-0 min-w-0 overflow-hidden">
169
- <Outlet />
170
- </SidebarInset>
171
- </SidebarProvider>
172
- </AnalyticsCapabilityProvider>
157
+ <SidebarProvider>
158
+ <AppSidebar />
159
+ <SidebarInset className="min-h-0 min-w-0 overflow-hidden">
160
+ <Outlet />
161
+ </SidebarInset>
162
+ </SidebarProvider>
173
163
  );
174
164
  }
@@ -23,7 +23,7 @@ function ApiKeysRoute() {
23
23
  <h1 className="text-sm font-medium">API keys</h1>
24
24
  </div>
25
25
  <p className="basis-full pl-9 text-xs text-muted-foreground sm:basis-auto sm:pl-0">
26
- API keys can download OTA updates and submit analytics only.
26
+ API keys can download OTA updates and submit insights only.
27
27
  </p>
28
28
  </header>
29
29