@hot-updater/console 0.36.6 → 1.0.0-rc.0
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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/AnalyticsCapabilityContext-BCke57-7.js +1 -0
- package/.output/public/assets/BundleActivityChart-CnsBe_1J.js +42 -0
- package/.output/public/assets/BundleIdDisplay-Dky75zLU.js +1 -0
- package/.output/public/assets/alert-C-1KvDW8.js +1 -0
- package/.output/public/assets/analytics-Bqz0c-BT.js +1 -0
- package/.output/public/assets/analytics-api-CJdrTLLO.js +1 -0
- package/.output/public/assets/api-B_Rh-uzC.js +1 -0
- package/.output/public/assets/api-keys-BurCFhzH.js +1 -0
- package/.output/public/assets/api-keys-api-BEDJdQg3.js +1 -0
- package/.output/public/assets/badge-Bf0fZ-Gx.js +1 -0
- package/.output/public/assets/chart-no-axes-combined-FaYMGmQS.js +1 -0
- package/.output/public/assets/createServerFn-gM2-LFBy.js +3 -0
- package/.output/public/assets/dialog-BS60BRr2.js +1 -0
- package/.output/public/assets/dist-DspNKfMj.js +1 -0
- package/.output/public/assets/download-O-yNBc9H.js +1 -0
- package/.output/public/assets/field-Buq148-A.js +1 -0
- package/.output/public/assets/index-kFhd2S78.js +10 -0
- package/.output/public/assets/installations-BrQ-od41.js +2 -0
- package/.output/public/assets/installations-CGlZ3ROz.js +1 -0
- package/.output/public/assets/lazyRouteComponent-rPzgA7S-.js +1 -0
- package/.output/public/assets/loader-circle-DCYF-wJl.js +1 -0
- package/.output/public/assets/preload-helper-TKOBYGYD.js +1 -0
- package/.output/public/assets/refresh-cw-n8XYbPPj.js +1 -0
- package/.output/public/assets/routes-CrdokJ80.js +1 -0
- package/.output/public/assets/routes-DcQnVajO.js +2 -0
- package/.output/public/assets/search-DLhbWw3W.js +1 -0
- package/.output/public/assets/shield-check-DE_hVuHu.js +1 -0
- package/.output/public/assets/signing-HRexfjsH.js +1 -0
- package/.output/public/assets/styles-eB4EtvFS.css +2 -0
- package/.output/public/assets/table-D8CLieHX.js +1 -0
- package/.output/public/assets/textarea-YJLTVAoq.js +1 -0
- package/.output/public/assets/useMutation-JsGVjQMY.js +1 -0
- package/.output/public/assets/useOpenInteractionType-4cRD5xO0.js +1 -0
- package/.output/server/__23tanstack-start-server-fn-resolver-DBP9TQ7K.mjs +146 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +16124 -0
- package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +41 -10
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +132 -0
- package/.output/server/_libs/d3-array.mjs +231 -0
- package/.output/server/_libs/d3-color.mjs +330 -0
- package/.output/server/_libs/d3-format.mjs +252 -0
- package/.output/server/_libs/d3-interpolate.mjs +207 -0
- package/.output/server/_libs/d3-path.mjs +77 -0
- package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
- package/.output/server/_libs/d3-shape.mjs +818 -0
- package/.output/server/_libs/decimal.js-light.mjs +884 -0
- package/.output/server/_libs/es-toolkit.mjs +1134 -0
- package/.output/server/_libs/eventemitter3.mjs +236 -0
- package/.output/server/_libs/immer.mjs +532 -0
- package/.output/server/_libs/lucide-react.mjs +141 -13
- package/.output/server/_libs/next-themes.mjs +2 -2
- package/.output/server/_libs/react-redux+[...].mjs +205 -0
- package/.output/server/_libs/recharts+[...].mjs +14117 -0
- package/.output/server/_libs/sonner.mjs +2 -2
- package/.output/server/_libs/tanstack__query-core.mjs +114 -1
- package/.output/server/_libs/tanstack__react-query.mjs +3 -4
- package/.output/server/_runtime.mjs +10 -1
- package/.output/server/_ssr/AnalyticsCapabilityContext-ui4DX8GO.mjs +15 -0
- package/.output/server/_ssr/BundleActivityChart-BiB9ZORH.mjs +288 -0
- package/.output/server/_ssr/BundleIdDisplay-Czk6iR6p.mjs +13 -0
- package/.output/server/_ssr/alert-Dim3MGY5.mjs +86 -0
- package/.output/server/_ssr/analytics-DtMTn8fn.mjs +858 -0
- package/.output/server/_ssr/analytics-api-Cg1M5v_H.mjs +71 -0
- package/.output/server/_ssr/analytics-input-fRRRn0uA.mjs +96 -0
- package/.output/server/_ssr/analytics-rpc-DY5uXvHT.mjs +155 -0
- package/.output/server/_ssr/api-BTSv1tZb.mjs +282 -0
- package/.output/server/_ssr/api-keys-BnM_o2nS.mjs +300 -0
- package/.output/server/_ssr/api-keys-api-CIywsSF5.mjs +60 -0
- package/.output/server/_ssr/api-keys-rpc-BSp1wkIV.mjs +136 -0
- package/.output/server/_ssr/api-rpc-cG5wd_tO.mjs +1468 -0
- package/.output/server/_ssr/auth-BF8mbAgF.mjs +21 -0
- package/.output/server/_ssr/auth-rpc-BrJ6obHJ.mjs +24 -0
- package/.output/server/_ssr/auth.server-DwKoZoQM.mjs +50 -0
- package/.output/server/_ssr/badge-DCxbEeof.mjs +26 -0
- package/.output/server/_ssr/bundle-signing-rpc-COoQhW8E.mjs +16 -0
- package/.output/server/_ssr/bundle-signing.server-D5u8KkMp.mjs +51 -0
- package/.output/server/_ssr/config-Dv8qLDZ0.mjs +13 -0
- package/.output/server/_ssr/config.server-DEiLhKmy.mjs +361 -0
- package/.output/server/_ssr/{createServerFn-DzU0k62V.mjs → createServerFn-B2hcXoPM.mjs} +1 -1
- package/.output/server/_ssr/createServerRpc-DB29C_5p.mjs +12 -0
- package/.output/server/_ssr/{sidebar-Cv_H0_Jv.mjs → createSsrRpc-9P0S0lJ3.mjs} +68 -50
- package/.output/server/_ssr/databaseRows-CwbNwYSl.mjs +166 -0
- package/.output/server/_ssr/deleteBundle-Cu_XOMt0.mjs +66 -0
- package/.output/server/_ssr/dialog-CQuRMNZ4.mjs +170 -0
- package/.output/server/_ssr/dist-BVSLTpoP.mjs +250 -0
- package/.output/server/_ssr/downloadBundle-CNnYxYO2.mjs +24 -0
- package/.output/server/_ssr/{empty-plugin-adapters-B4HCzgmM.mjs → empty-plugin-adapters-B5QvGpEl.mjs} +1 -1
- package/.output/server/_ssr/field-DRIXKjZG.mjs +74 -0
- package/.output/server/_ssr/{getBundleChildren-CVtSiH5t.mjs → getBundleChildren-BcRMFoDF.mjs} +10 -18
- package/.output/server/_ssr/installations-B-azgAvX.mjs +19 -0
- package/.output/server/_ssr/installations-Ctvzp2JH.mjs +535 -0
- package/.output/server/_ssr/router-C-KUBfOi.mjs +513 -0
- package/.output/server/_ssr/routes-BM4ht9jb.mjs +2286 -0
- package/.output/server/_ssr/routes-Z7TwC6nC.mjs +37 -0
- package/.output/server/_ssr/runtime.server-awTLiiXW.mjs +527 -0
- package/.output/server/_ssr/server-BjzmvXnO.mjs +7468 -0
- package/.output/server/_ssr/signing-k8GxyBDF.mjs +202 -0
- package/.output/server/_ssr/ssr.mjs +4 -5740
- package/.output/server/_ssr/start-DpViQGnt.mjs +4 -0
- package/.output/server/_ssr/table-v1yCvYo7.mjs +53 -0
- package/.output/server/_ssr/textarea-DYHnskhb.mjs +14 -0
- package/.output/server/_ssr/uuidv7-BbJ-9D_I.mjs +31 -0
- package/.output/server/_tanstack-start-manifest_v-CH4StzJu.mjs +108 -0
- package/.output/server/index.mjs +249 -46
- package/README.md +29 -8
- package/lib/index.d.mts +36 -0
- package/lib/index.mjs +4 -0
- package/lib/vite-internal.mjs +127 -0
- package/lib/vite.d.mts +10 -0
- package/lib/vite.mjs +5 -0
- package/package.json +51 -29
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/logo.svg +1 -0
- package/public/manifest.json +30 -0
- package/public/robots.txt +3 -0
- package/src/components/AppSidebar.tsx +159 -0
- package/src/components/BundleIdDisplay.tsx +18 -0
- package/src/components/ChannelBadge.tsx +31 -0
- package/src/components/ConsoleAccessPage.tsx +122 -0
- package/src/components/EnabledStatusIcon.tsx +54 -0
- package/src/components/HashValueDisplay.tsx +85 -0
- package/src/components/HotUpdaterLogo.tsx +30 -0
- package/src/components/NotFoundPage.tsx +33 -0
- package/src/components/PlatformIcon.tsx +39 -0
- package/src/components/RolloutPercentageBadge.tsx +28 -0
- package/src/components/ThemeProvider.tsx +116 -0
- package/src/components/TimestampDisplay.tsx +41 -0
- package/src/components/features/analytics/ActivityChart.tsx +150 -0
- package/src/components/features/analytics/AnalyticsCapabilityContext.tsx +24 -0
- package/src/components/features/analytics/AnalyticsControls.tsx +115 -0
- package/src/components/features/analytics/AnalyticsErrorAlert.tsx +21 -0
- package/src/components/features/analytics/AnalyticsOverview.tsx +208 -0
- package/src/components/features/analytics/BundleDistribution.tsx +133 -0
- package/src/components/features/analytics/BundleSelector.tsx +114 -0
- package/src/components/features/analytics/InstallationHistoryCard.tsx +248 -0
- package/src/components/features/analytics/InstallationMatchesCard.tsx +132 -0
- package/src/components/features/analytics/InstallationPageHeader.tsx +138 -0
- package/src/components/features/analytics/InstallationPagination.tsx +48 -0
- package/src/components/features/analytics/InstallationSearch.tsx +230 -0
- package/src/components/features/analytics/UpdateOutcomes.tsx +147 -0
- package/src/components/features/api-keys/ApiKeysPage.tsx +377 -0
- package/src/components/features/bundles/BundleActivityChart.tsx +193 -0
- package/src/components/features/bundles/BundleAnalyticsSummary.tsx +123 -0
- package/src/components/features/bundles/BundleChildrenPanel.tsx +201 -0
- package/src/components/features/bundles/BundleMetadata.tsx +136 -0
- package/src/components/features/bundles/RolloutCohortsDialog.tsx +201 -0
- package/src/components/features/channels/ChannelManagementDialog.tsx +228 -0
- package/src/components/features/releases/ReleaseEditorSheet.tsx +879 -0
- package/src/components/features/releases/ReleaseStateBadge.tsx +20 -0
- package/src/components/features/signing/BundleSigningPage.tsx +247 -0
- package/src/components/ui/alert-dialog.tsx +185 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/card.tsx +89 -0
- package/src/components/ui/chart.tsx +371 -0
- package/src/components/ui/combobox.tsx +303 -0
- package/src/components/ui/dialog.tsx +156 -0
- package/src/components/ui/dropdown-menu.tsx +270 -0
- package/src/components/ui/field.tsx +235 -0
- package/src/components/ui/input-group.tsx +147 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/label.tsx +25 -0
- package/src/components/ui/progress.tsx +84 -0
- package/src/components/ui/select.tsx +205 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +132 -0
- package/src/components/ui/sidebar.tsx +779 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +56 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/switch.tsx +25 -0
- package/src/components/ui/table.tsx +114 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +89 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +66 -0
- package/src/hooks/use-mobile.ts +30 -0
- package/src/index.ts +48 -0
- package/src/lib/analytics-api.ts +145 -0
- package/src/lib/analytics-error.ts +19 -0
- package/src/lib/analytics-input.ts +187 -0
- package/src/lib/analytics-overview.ts +170 -0
- package/src/lib/analytics-rpc.ts +218 -0
- package/src/lib/api-keys-api.ts +69 -0
- package/src/lib/api-keys-rpc.ts +87 -0
- package/src/lib/api-rpc.ts +478 -0
- package/src/lib/api.ts +444 -0
- package/src/lib/auth-rpc.ts +16 -0
- package/src/lib/bundle-signing-api.ts +14 -0
- package/src/lib/bundle-signing-rpc.ts +16 -0
- package/src/lib/constants.ts +1 -0
- package/src/lib/git.ts +51 -0
- package/src/lib/server/auth.server.ts +43 -0
- package/src/lib/server/bundle-signing.server.ts +100 -0
- package/src/lib/server/config.server.ts +109 -0
- package/src/lib/server/console-runtime.server.ts +70 -0
- package/src/lib/server/deleteBundle.ts +145 -0
- package/src/lib/server/downloadBundle.ts +46 -0
- package/src/lib/server/getBundleChildren.ts +130 -0
- package/src/lib/server/listReleases.ts +61 -0
- package/src/lib/server/releaseActivity.ts +27 -0
- package/src/lib/server/releaseReachability.ts +95 -0
- package/src/lib/server/runtime.server.ts +172 -0
- package/src/lib/utils.ts +9 -0
- package/src/logo.svg +1 -0
- package/src/routeTree.gen.ts +199 -0
- package/src/router.tsx +21 -0
- package/src/routes/-installations-search.ts +19 -0
- package/src/routes/-releases-search.ts +74 -0
- package/src/routes/__root.tsx +174 -0
- package/src/routes/analytics.tsx +139 -0
- package/src/routes/api/auth/$.ts +19 -0
- package/src/routes/api/bundles/$bundleId/download.ts +17 -0
- package/src/routes/api-keys.tsx +37 -0
- package/src/routes/index.tsx +982 -0
- package/src/routes/installations.tsx +194 -0
- package/src/routes/signing.tsx +37 -0
- package/src/styles.css +152 -0
- package/src/virtual-modules.d.ts +21 -0
- package/src/vite-env.d.ts +1 -0
- package/src/vite-internal.ts +195 -0
- package/src/vite.ts +12 -0
- package/.output/public/assets/dist-DJX53faP.js +0 -3
- package/.output/public/assets/index-Iylis5kY.js +0 -10
- package/.output/public/assets/routes-HT21hs0C.js +0 -54
- package/.output/public/assets/styles-C0Re4quB.css +0 -2
- package/.output/server/__23tanstack-start-server-fn-resolver-CgwgZ0-i.mjs +0 -66
- package/.output/server/_libs/@floating-ui/core+[...].mjs +0 -698
- package/.output/server/_libs/@floating-ui/dom+[...].mjs +0 -644
- package/.output/server/_libs/@floating-ui/react-dom+[...].mjs +0 -839
- package/.output/server/_libs/@radix-ui/react-alert-dialog+[...].mjs +0 -2138
- package/.output/server/_libs/@radix-ui/react-popper+[...].mjs +0 -308
- package/.output/server/_libs/@radix-ui/react-select+[...].mjs +0 -1038
- package/.output/server/_libs/@tanstack/devtools-event-client+[...].mjs +0 -196
- package/.output/server/_libs/@tanstack/form-core+[...].mjs +0 -3228
- package/.output/server/_libs/@tanstack/react-form+[...].mjs +0 -430
- package/.output/server/_libs/@tanstack/react-table+[...].mjs +0 -2382
- package/.output/server/_libs/radix-ui__number.mjs +0 -6
- package/.output/server/_libs/radix-ui__primitive.mjs +0 -9
- package/.output/server/_libs/radix-ui__react-arrow.mjs +0 -23
- package/.output/server/_libs/radix-ui__react-collection.mjs +0 -78
- package/.output/server/_libs/radix-ui__react-direction.mjs +0 -13
- package/.output/server/_libs/radix-ui__react-label.mjs +0 -22
- package/.output/server/_libs/radix-ui__react-separator.mjs +0 -30
- package/.output/server/_libs/radix-ui__react-slider.mjs +0 -510
- package/.output/server/_libs/radix-ui__react-switch.mjs +0 -157
- package/.output/server/_libs/radix-ui__react-tooltip.mjs +0 -493
- package/.output/server/_ssr/api-rpc-BIbDUKSz.mjs +0 -292
- package/.output/server/_ssr/config.server-BSS366KT.mjs +0 -41
- package/.output/server/_ssr/deleteBundle-BgbZTtpW.mjs +0 -141
- package/.output/server/_ssr/dist-KboydRVk.mjs +0 -120
- package/.output/server/_ssr/router-C3hqEy3k.mjs +0 -307
- package/.output/server/_ssr/routes-B0lQvvlC.mjs +0 -3048
- package/.output/server/_ssr/start-02sLteYG.mjs +0 -4
- package/.output/server/_ssr/storageProfile-FVkjPoGb.mjs +0 -9
- package/.output/server/_tanstack-start-manifest_v-Bpx52WOU.mjs +0 -20
- package/.output/server/node_modules/tslib/modules/index.js +0 -70
- package/.output/server/node_modules/tslib/modules/package.json +0 -3
- package/.output/server/node_modules/tslib/package.json +0 -47
- package/.output/server/node_modules/tslib/tslib.js +0 -484
- package/.output/server/package.json +0 -9
package/src/lib/api.ts
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChannelDeleteInput,
|
|
3
|
+
ChannelInsertInput,
|
|
4
|
+
ReleasePolicyPatch,
|
|
5
|
+
} from "@hot-updater/plugin-core";
|
|
6
|
+
import type { BundleEventAnalyticsWindow } from "@hot-updater/server";
|
|
7
|
+
import {
|
|
8
|
+
type QueryClient,
|
|
9
|
+
type QueryKey,
|
|
10
|
+
useMutation,
|
|
11
|
+
useQuery,
|
|
12
|
+
useQueryClient,
|
|
13
|
+
} from "@tanstack/react-query";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
createChannel as createChannelApi,
|
|
17
|
+
deleteChannel as deleteChannelApi,
|
|
18
|
+
deleteBundle as deleteBundleApi,
|
|
19
|
+
deleteBundles as deleteBundlesApi,
|
|
20
|
+
deleteRelease as deleteReleaseApi,
|
|
21
|
+
getBundle,
|
|
22
|
+
getBundleChildCounts,
|
|
23
|
+
getBundleChildren,
|
|
24
|
+
getBundleEventAnalytics as getBundleEventAnalyticsApi,
|
|
25
|
+
getBundleEventSummary as getBundleEventSummaryApi,
|
|
26
|
+
getBundles,
|
|
27
|
+
getChannels,
|
|
28
|
+
getConfig,
|
|
29
|
+
getConfigLoaded,
|
|
30
|
+
getInstallationHistory as getInstallationHistoryApi,
|
|
31
|
+
getRelease,
|
|
32
|
+
getReleaseCatalogDiagnostics,
|
|
33
|
+
getReleases,
|
|
34
|
+
preflightRelease as preflightReleaseApi,
|
|
35
|
+
promoteRelease as promoteReleaseApi,
|
|
36
|
+
searchInstallations as searchInstallationsApi,
|
|
37
|
+
updateRelease as updateReleaseApi,
|
|
38
|
+
} from "./api-rpc";
|
|
39
|
+
|
|
40
|
+
type BundleFilters = {
|
|
41
|
+
platform?: "ios" | "android";
|
|
42
|
+
page?: number;
|
|
43
|
+
limit?: string;
|
|
44
|
+
after?: string;
|
|
45
|
+
before?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type BundlesQueryData = Awaited<ReturnType<typeof getBundles>>;
|
|
49
|
+
|
|
50
|
+
const ANALYTICS_STALE_TIME_MS = 30_000;
|
|
51
|
+
|
|
52
|
+
export type BundleEventSummary = Awaited<
|
|
53
|
+
ReturnType<typeof getBundleEventSummaryApi>
|
|
54
|
+
>;
|
|
55
|
+
export type BundleEventAnalytics = Awaited<
|
|
56
|
+
ReturnType<typeof getBundleEventAnalyticsApi>
|
|
57
|
+
>;
|
|
58
|
+
export type InstallationSearchResult = Awaited<
|
|
59
|
+
ReturnType<typeof searchInstallationsApi>
|
|
60
|
+
>;
|
|
61
|
+
export type InstallationHistoryResult = Awaited<
|
|
62
|
+
ReturnType<typeof getInstallationHistoryApi>
|
|
63
|
+
>;
|
|
64
|
+
export type InstallationSearchRow = InstallationSearchResult["data"][number];
|
|
65
|
+
export type InstallationHistoryRow = InstallationHistoryResult["data"][number];
|
|
66
|
+
|
|
67
|
+
const bundleListQueryKey = ["bundles"] as const;
|
|
68
|
+
const releaseListQueryKey = ["releases"] as const;
|
|
69
|
+
|
|
70
|
+
export const queryKeys = {
|
|
71
|
+
config: ["config"] as const,
|
|
72
|
+
channels: ["channels"] as const,
|
|
73
|
+
configLoaded: ["config-loaded"] as const,
|
|
74
|
+
bundles: {
|
|
75
|
+
all: bundleListQueryKey,
|
|
76
|
+
list: (filters?: BundleFilters) =>
|
|
77
|
+
[...bundleListQueryKey, filters ?? {}] as const,
|
|
78
|
+
},
|
|
79
|
+
releases: {
|
|
80
|
+
all: releaseListQueryKey,
|
|
81
|
+
list: (filters?: ReleaseFilters) =>
|
|
82
|
+
[...releaseListQueryKey, filters ?? {}] as const,
|
|
83
|
+
},
|
|
84
|
+
release: (releaseId: string) => ["release", releaseId] as const,
|
|
85
|
+
releaseCatalog: (scopeKey: string) => ["release-catalog", scopeKey] as const,
|
|
86
|
+
bundleChildren: {
|
|
87
|
+
all: ["bundle-children"] as const,
|
|
88
|
+
list: (baseBundleId: string) => ["bundle-children", baseBundleId] as const,
|
|
89
|
+
counts: (bundleIds: string[]) =>
|
|
90
|
+
["bundle-children", "counts", ...bundleIds] as const,
|
|
91
|
+
},
|
|
92
|
+
bundle: (bundleId: string) => ["bundle", bundleId] as const,
|
|
93
|
+
bundleEventSummary: (bundleId: string) =>
|
|
94
|
+
["bundle-event-summary", bundleId] as const,
|
|
95
|
+
bundleEventAnalytics: (input: {
|
|
96
|
+
bundleId: string;
|
|
97
|
+
window: BundleEventAnalyticsWindow;
|
|
98
|
+
limit?: number;
|
|
99
|
+
offset?: number;
|
|
100
|
+
}) => ["bundle-event-analytics", input] as const,
|
|
101
|
+
|
|
102
|
+
installations: {
|
|
103
|
+
search: (input: { query: string; limit?: number; offset?: number }) =>
|
|
104
|
+
["installations", "search", input] as const,
|
|
105
|
+
history: (input: { installId: string; limit?: number; offset?: number }) =>
|
|
106
|
+
["installations", "history", input] as const,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type ReleaseFilters = {
|
|
111
|
+
afterReleaseId?: string;
|
|
112
|
+
beforeReleaseId?: string;
|
|
113
|
+
bundleId?: string;
|
|
114
|
+
channelId?: string;
|
|
115
|
+
enabled?: boolean;
|
|
116
|
+
platform?: "ios" | "android";
|
|
117
|
+
limit?: number;
|
|
118
|
+
page?: number;
|
|
119
|
+
targetAppVersion?: string;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function removeBundleFromQueryData(
|
|
123
|
+
data: BundlesQueryData | undefined,
|
|
124
|
+
bundleId: string,
|
|
125
|
+
) {
|
|
126
|
+
if (!data) {
|
|
127
|
+
return data;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
...data,
|
|
132
|
+
data: data.data.filter((bundle) => bundle.id !== bundleId),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function removeBundlesFromQueryData(
|
|
137
|
+
data: BundlesQueryData | undefined,
|
|
138
|
+
bundleIds: readonly string[],
|
|
139
|
+
) {
|
|
140
|
+
if (!data) {
|
|
141
|
+
return data;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const bundleIdSet = new Set(bundleIds);
|
|
145
|
+
return {
|
|
146
|
+
...data,
|
|
147
|
+
data: data.data.filter((bundle) => !bundleIdSet.has(bundle.id)),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const invalidateInBackground = (
|
|
152
|
+
queryClient: QueryClient,
|
|
153
|
+
queryKey: QueryKey,
|
|
154
|
+
) => {
|
|
155
|
+
void queryClient.invalidateQueries({ queryKey }).catch(() => undefined);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Query Hooks
|
|
159
|
+
export function useConfigQuery() {
|
|
160
|
+
return useQuery({
|
|
161
|
+
queryKey: queryKeys.config,
|
|
162
|
+
queryFn: () => getConfig(),
|
|
163
|
+
staleTime: Infinity,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function useChannelsQuery() {
|
|
168
|
+
return useQuery({
|
|
169
|
+
queryKey: queryKeys.channels,
|
|
170
|
+
queryFn: () => getChannels(),
|
|
171
|
+
staleTime: Infinity,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function useConfigLoadedQuery() {
|
|
176
|
+
return useQuery({
|
|
177
|
+
queryKey: queryKeys.configLoaded,
|
|
178
|
+
queryFn: () => getConfigLoaded(),
|
|
179
|
+
staleTime: Infinity,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function useBundlesQuery(filters?: BundleFilters) {
|
|
184
|
+
return useQuery({
|
|
185
|
+
queryKey: queryKeys.bundles.list(filters),
|
|
186
|
+
queryFn: () => getBundles({ data: filters }),
|
|
187
|
+
staleTime: Infinity,
|
|
188
|
+
placeholderData: (previousData) => previousData,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function useReleasesQuery(filters?: ReleaseFilters) {
|
|
193
|
+
return useQuery({
|
|
194
|
+
queryKey: queryKeys.releases.list(filters),
|
|
195
|
+
queryFn: () => getReleases({ data: filters }),
|
|
196
|
+
placeholderData: (previousData) => previousData,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function useReleaseQuery(releaseId: string) {
|
|
201
|
+
return useQuery({
|
|
202
|
+
enabled: releaseId.length > 0,
|
|
203
|
+
queryFn: () => getRelease({ data: { releaseId } }),
|
|
204
|
+
queryKey: queryKeys.release(releaseId),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function useReleaseCatalogDiagnosticsQuery(scopeKey: string) {
|
|
209
|
+
return useQuery({
|
|
210
|
+
enabled: scopeKey.length > 0,
|
|
211
|
+
queryFn: () => getReleaseCatalogDiagnostics({ data: { scopeKey } }),
|
|
212
|
+
queryKey: queryKeys.releaseCatalog(scopeKey),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function useBundleQuery(bundleId: string) {
|
|
217
|
+
return useQuery({
|
|
218
|
+
queryKey: queryKeys.bundle(bundleId),
|
|
219
|
+
queryFn: () => getBundle({ data: { bundleId } }),
|
|
220
|
+
staleTime: Infinity,
|
|
221
|
+
enabled: !!bundleId,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function useBundleEventSummaryQuery(bundleId: string, enabled: boolean) {
|
|
226
|
+
return useQuery({
|
|
227
|
+
queryKey: queryKeys.bundleEventSummary(bundleId),
|
|
228
|
+
queryFn: () => getBundleEventSummaryApi({ data: { bundleId } }),
|
|
229
|
+
staleTime: ANALYTICS_STALE_TIME_MS,
|
|
230
|
+
refetchOnWindowFocus: true,
|
|
231
|
+
enabled: enabled && bundleId.length > 0,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function useBundleEventAnalyticsQuery(
|
|
236
|
+
input: {
|
|
237
|
+
bundleId: string;
|
|
238
|
+
window: BundleEventAnalyticsWindow;
|
|
239
|
+
limit?: number;
|
|
240
|
+
offset?: number;
|
|
241
|
+
},
|
|
242
|
+
enabled: boolean,
|
|
243
|
+
) {
|
|
244
|
+
return useQuery({
|
|
245
|
+
queryKey: queryKeys.bundleEventAnalytics(input),
|
|
246
|
+
queryFn: () => getBundleEventAnalyticsApi({ data: input }),
|
|
247
|
+
staleTime: ANALYTICS_STALE_TIME_MS,
|
|
248
|
+
refetchOnWindowFocus: true,
|
|
249
|
+
enabled: enabled && input.bundleId.length > 0,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function useInstallationSearchQuery(
|
|
254
|
+
input: {
|
|
255
|
+
query: string;
|
|
256
|
+
limit?: number;
|
|
257
|
+
offset?: number;
|
|
258
|
+
},
|
|
259
|
+
enabled: boolean,
|
|
260
|
+
) {
|
|
261
|
+
return useQuery({
|
|
262
|
+
queryKey: queryKeys.installations.search(input),
|
|
263
|
+
queryFn: () => searchInstallationsApi({ data: input }),
|
|
264
|
+
staleTime: ANALYTICS_STALE_TIME_MS,
|
|
265
|
+
refetchOnWindowFocus: true,
|
|
266
|
+
enabled: enabled && input.query.trim().length > 0,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function useInstallationHistoryQuery(
|
|
271
|
+
input: {
|
|
272
|
+
installId: string;
|
|
273
|
+
limit?: number;
|
|
274
|
+
offset?: number;
|
|
275
|
+
},
|
|
276
|
+
enabled: boolean,
|
|
277
|
+
) {
|
|
278
|
+
return useQuery({
|
|
279
|
+
queryKey: queryKeys.installations.history(input),
|
|
280
|
+
queryFn: () => getInstallationHistoryApi({ data: input }),
|
|
281
|
+
staleTime: ANALYTICS_STALE_TIME_MS,
|
|
282
|
+
refetchOnWindowFocus: true,
|
|
283
|
+
enabled: enabled && input.installId.length > 0,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function useBundleChildrenQuery(baseBundleId: string) {
|
|
288
|
+
return useQuery({
|
|
289
|
+
queryKey: queryKeys.bundleChildren.list(baseBundleId),
|
|
290
|
+
queryFn: () => getBundleChildren({ data: { baseBundleId } }),
|
|
291
|
+
staleTime: Infinity,
|
|
292
|
+
enabled: !!baseBundleId,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function useBundleChildCountsQuery(bundleIds: string[]) {
|
|
297
|
+
const normalizedBundleIds = [...bundleIds].sort((left, right) =>
|
|
298
|
+
left.localeCompare(right),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
return useQuery({
|
|
302
|
+
queryKey: queryKeys.bundleChildren.counts(normalizedBundleIds),
|
|
303
|
+
queryFn: () =>
|
|
304
|
+
getBundleChildCounts({ data: { bundleIds: normalizedBundleIds } }),
|
|
305
|
+
staleTime: Infinity,
|
|
306
|
+
enabled: normalizedBundleIds.length > 0,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Mutation Hooks
|
|
311
|
+
export function useCreateChannelMutation() {
|
|
312
|
+
const queryClient = useQueryClient();
|
|
313
|
+
|
|
314
|
+
return useMutation({
|
|
315
|
+
mutationFn: (input: ChannelInsertInput) =>
|
|
316
|
+
createChannelApi({ data: input }).then((response) => response.data),
|
|
317
|
+
onSuccess: async () => {
|
|
318
|
+
await queryClient.invalidateQueries({ queryKey: queryKeys.channels });
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function useDeleteChannelMutation() {
|
|
324
|
+
const queryClient = useQueryClient();
|
|
325
|
+
|
|
326
|
+
return useMutation({
|
|
327
|
+
mutationFn: (input: ChannelDeleteInput) =>
|
|
328
|
+
deleteChannelApi({ data: input }).then((response) => response.data),
|
|
329
|
+
onSettled: async () => {
|
|
330
|
+
await queryClient.invalidateQueries({ queryKey: queryKeys.channels });
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function useDeleteBundleMutation() {
|
|
336
|
+
const queryClient = useQueryClient();
|
|
337
|
+
|
|
338
|
+
return useMutation({
|
|
339
|
+
mutationFn: (params: { bundleId: string }) =>
|
|
340
|
+
deleteBundleApi({ data: params }),
|
|
341
|
+
onSuccess: (_, vars) => {
|
|
342
|
+
queryClient.removeQueries({ queryKey: queryKeys.bundle(vars.bundleId) });
|
|
343
|
+
queryClient.setQueriesData(
|
|
344
|
+
{ queryKey: queryKeys.bundles.all },
|
|
345
|
+
(data: BundlesQueryData | undefined) =>
|
|
346
|
+
removeBundleFromQueryData(data, vars.bundleId),
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
invalidateInBackground(queryClient, queryKeys.bundles.all);
|
|
350
|
+
invalidateInBackground(queryClient, queryKeys.bundleChildren.all);
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function useDeleteBundlesMutation() {
|
|
356
|
+
const queryClient = useQueryClient();
|
|
357
|
+
|
|
358
|
+
return useMutation({
|
|
359
|
+
mutationFn: (params: { bundleIds: string[] }) =>
|
|
360
|
+
deleteBundlesApi({ data: params }),
|
|
361
|
+
onSuccess: (_, vars) => {
|
|
362
|
+
for (const bundleId of vars.bundleIds) {
|
|
363
|
+
queryClient.removeQueries({ queryKey: queryKeys.bundle(bundleId) });
|
|
364
|
+
}
|
|
365
|
+
queryClient.setQueriesData(
|
|
366
|
+
{ queryKey: queryKeys.bundles.all },
|
|
367
|
+
(data: BundlesQueryData | undefined) =>
|
|
368
|
+
removeBundlesFromQueryData(data, vars.bundleIds),
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
invalidateInBackground(queryClient, queryKeys.bundles.all);
|
|
372
|
+
invalidateInBackground(queryClient, queryKeys.bundleChildren.all);
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function useUpdateReleaseMutation() {
|
|
378
|
+
const queryClient = useQueryClient();
|
|
379
|
+
return useMutation({
|
|
380
|
+
mutationFn: (input: {
|
|
381
|
+
expectedRevision: number;
|
|
382
|
+
patch: ReleasePolicyPatch;
|
|
383
|
+
releaseId: string;
|
|
384
|
+
}) => updateReleaseApi({ data: input }),
|
|
385
|
+
onSuccess: async (_, input) => {
|
|
386
|
+
await Promise.all([
|
|
387
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.releases.all }),
|
|
388
|
+
queryClient.invalidateQueries({
|
|
389
|
+
queryKey: queryKeys.release(input.releaseId),
|
|
390
|
+
}),
|
|
391
|
+
queryClient.invalidateQueries({ queryKey: ["release-catalog"] }),
|
|
392
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.channels }),
|
|
393
|
+
]);
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function usePreflightReleaseMutation() {
|
|
399
|
+
return useMutation({
|
|
400
|
+
mutationFn: (input: {
|
|
401
|
+
expectedRevision: number;
|
|
402
|
+
patch: ReleasePolicyPatch;
|
|
403
|
+
releaseId: string;
|
|
404
|
+
}) => preflightReleaseApi({ data: input }),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function useDeleteReleaseMutation() {
|
|
409
|
+
const queryClient = useQueryClient();
|
|
410
|
+
return useMutation({
|
|
411
|
+
mutationFn: (input: { expectedRevision: number; releaseId: string }) =>
|
|
412
|
+
deleteReleaseApi({ data: input }),
|
|
413
|
+
onSuccess: async (_, input) => {
|
|
414
|
+
queryClient.removeQueries({
|
|
415
|
+
queryKey: queryKeys.release(input.releaseId),
|
|
416
|
+
});
|
|
417
|
+
await Promise.all([
|
|
418
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.releases.all }),
|
|
419
|
+
queryClient.invalidateQueries({ queryKey: ["release-catalog"] }),
|
|
420
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.channels }),
|
|
421
|
+
]);
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function usePromoteReleaseMutation() {
|
|
427
|
+
const queryClient = useQueryClient();
|
|
428
|
+
return useMutation({
|
|
429
|
+
mutationFn: (input: {
|
|
430
|
+
action: "copy" | "move";
|
|
431
|
+
expectedRevision: number;
|
|
432
|
+
releaseId: string;
|
|
433
|
+
targetChannel: string;
|
|
434
|
+
}) => promoteReleaseApi({ data: input }),
|
|
435
|
+
onSuccess: async () => {
|
|
436
|
+
await Promise.all([
|
|
437
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.releases.all }),
|
|
438
|
+
queryClient.invalidateQueries({ queryKey: ["release"] }),
|
|
439
|
+
queryClient.invalidateQueries({ queryKey: ["release-catalog"] }),
|
|
440
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.channels }),
|
|
441
|
+
]);
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
+
import { getRequest } from "@tanstack/react-start/server";
|
|
3
|
+
|
|
4
|
+
export const getConsoleAccessRpc = createServerFn({ method: "GET" }).handler(
|
|
5
|
+
async () => {
|
|
6
|
+
const { getConsoleAccess } = await import("./server/auth.server");
|
|
7
|
+
return getConsoleAccess(getRequest());
|
|
8
|
+
},
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export const getConsoleAuthProvidersRpc = createServerFn({
|
|
12
|
+
method: "GET",
|
|
13
|
+
}).handler(async () => {
|
|
14
|
+
const { getConsoleAuthProviders } = await import("./server/auth.server");
|
|
15
|
+
return getConsoleAuthProviders(getRequest());
|
|
16
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
import { getBundleSigningInspectionRpc } from "./bundle-signing-rpc";
|
|
4
|
+
|
|
5
|
+
export const bundleSigningQueryKey = ["bundle-signing"] as const;
|
|
6
|
+
|
|
7
|
+
export const getBundleSigningInspectionQueryOptions = () => ({
|
|
8
|
+
queryFn: () => getBundleSigningInspectionRpc(),
|
|
9
|
+
queryKey: bundleSigningQueryKey,
|
|
10
|
+
staleTime: 30_000,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const useBundleSigningInspectionQuery = () =>
|
|
14
|
+
useQuery(getBundleSigningInspectionQueryOptions());
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
+
|
|
3
|
+
export type { BundleSigningInspection } from "./server/bundle-signing.server";
|
|
4
|
+
|
|
5
|
+
export const getBundleSigningInspection = async () => {
|
|
6
|
+
const [{ prepareConfig }, { inspectBundleSigning }] = await Promise.all([
|
|
7
|
+
import("./server/config.server"),
|
|
8
|
+
import("./server/bundle-signing.server"),
|
|
9
|
+
]);
|
|
10
|
+
const { config } = await prepareConfig();
|
|
11
|
+
return inspectBundleSigning(config.signing);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const getBundleSigningInspectionRpc = createServerFn({
|
|
15
|
+
method: "GET",
|
|
16
|
+
}).handler(getBundleSigningInspection);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_PAGE_LIMIT = 20;
|
package/src/lib/git.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const normalizeGitUrl = (gitUrl: string) => {
|
|
2
|
+
const trimmedGitUrl = gitUrl.trim();
|
|
3
|
+
|
|
4
|
+
if (trimmedGitUrl.startsWith("git@")) {
|
|
5
|
+
const sshMatch = trimmedGitUrl.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
|
|
6
|
+
|
|
7
|
+
if (sshMatch) {
|
|
8
|
+
return `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (trimmedGitUrl.startsWith("ssh://")) {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(trimmedGitUrl);
|
|
15
|
+
return `https://${url.hostname}${url.pathname}`
|
|
16
|
+
.replace(/\/+$/, "")
|
|
17
|
+
.replace(/\.git$/, "");
|
|
18
|
+
} catch {
|
|
19
|
+
return trimmedGitUrl.replace(/\/+$/, "").replace(/\.git$/, "");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return trimmedGitUrl.replace(/\/+$/, "").replace(/\.git$/, "");
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const getCommitUrl = (
|
|
27
|
+
gitUrl: string | undefined,
|
|
28
|
+
commitHash: string,
|
|
29
|
+
) => {
|
|
30
|
+
if (!gitUrl?.trim()) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const normalizedGitUrl = normalizeGitUrl(gitUrl);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const { hostname } = new URL(normalizedGitUrl);
|
|
38
|
+
|
|
39
|
+
if (hostname.includes("gitlab")) {
|
|
40
|
+
return `${normalizedGitUrl}/-/commit/${commitHash}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (hostname.includes("bitbucket")) {
|
|
44
|
+
return `${normalizedGitUrl}/commits/${commitHash}`;
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// Fall back to the default commit route for custom git hosts.
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return `${normalizedGitUrl}/commit/${commitHash}`;
|
|
51
|
+
};
|
|
@@ -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,100 @@
|
|
|
1
|
+
import { createHash, createPublicKey } from "node:crypto";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import type { ConsoleSigningConfig } from "../../index";
|
|
6
|
+
|
|
7
|
+
const MAX_PUBLIC_KEY_BYTES = 16 * 1024;
|
|
8
|
+
const DEFAULT_PROVIDER = "Configured provider";
|
|
9
|
+
const PUBLIC_KEY_HEADER = "-----BEGIN PUBLIC KEY-----";
|
|
10
|
+
const PUBLIC_KEY_FOOTER = "-----END PUBLIC KEY-----";
|
|
11
|
+
|
|
12
|
+
export type BundleSigningInspection =
|
|
13
|
+
| { readonly status: "disabled" }
|
|
14
|
+
| {
|
|
15
|
+
readonly status: "misconfigured";
|
|
16
|
+
readonly provider: string;
|
|
17
|
+
readonly message: string;
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
readonly status: "enabled";
|
|
21
|
+
readonly provider: string;
|
|
22
|
+
readonly algorithm: "RSA-SHA256";
|
|
23
|
+
readonly fingerprint: string;
|
|
24
|
+
readonly publicKey: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
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
|
+
export const inspectBundleSigning = async (
|
|
42
|
+
signing: ConsoleSigningConfig | undefined,
|
|
43
|
+
): Promise<BundleSigningInspection> => {
|
|
44
|
+
if (!signing?.enabled) return { status: "disabled" };
|
|
45
|
+
|
|
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
|
+
}
|
|
100
|
+
};
|