@hot-updater/console 1.0.0-rc.2 → 1.0.0-rc.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/ConsoleFeatureUnavailable-B9VYGA3N.js +1 -0
- package/.output/public/assets/HashValueDisplay-D880zntR.js +1 -0
- package/.output/public/assets/InsightsInfo-By_sPjTE.js +1 -0
- package/.output/public/assets/InsightsPageHeader-9_kx_LL7.js +1 -0
- package/.output/public/assets/NotFoundPage-Dys8C0cW.js +1 -0
- package/.output/public/assets/api-CtarlV0l.js +1 -0
- package/.output/public/assets/api-keys-DFF_QqtN.js +1 -0
- package/.output/public/assets/api-keys-evfsQ4sN.js +1 -0
- package/.output/public/assets/arrow-left-BP9D-ta_.js +1 -0
- package/.output/public/assets/arrow-up-right-CDP9Tevw.js +1 -0
- package/.output/public/assets/card-DzCPSRsh.js +1 -0
- package/.output/public/assets/console-features-api-yv7rm3DX.js +1 -0
- package/.output/public/assets/dialog-BDvCy_QO.js +1 -0
- package/.output/public/assets/dist-Clb6cQ9l.js +1 -0
- package/.output/public/assets/empty-C5x9mn_s.js +1 -0
- package/.output/public/assets/field-Bou7shoO.js +1 -0
- package/.output/public/assets/index-0eg3l8aC.js +10 -0
- package/.output/public/assets/input-group-DVtWFUUF.js +1 -0
- package/.output/public/assets/insights-2MbdiSNv.js +1 -0
- package/.output/public/assets/insights-C6tmCZHk.js +2 -0
- package/.output/public/assets/insights-CadS_aIA.js +42 -0
- package/.output/public/assets/insights-recovery-rpc-wvdyM4LD.js +1 -0
- package/.output/public/assets/insights-search-KIF7mRtB.js +1 -0
- package/.output/public/assets/insights-usage-rpc-qUkNtrWr.js +1 -0
- package/.output/public/assets/insights_.distribution-BhBs6PNA.js +1 -0
- package/.output/public/assets/insights_.distribution-CItZw3-8.js +1 -0
- package/.output/public/assets/insights_.distribution-CJUH_wAS.js +2 -0
- package/.output/public/assets/installations-CH2H7JQt.js +2 -0
- package/.output/public/assets/installations-DMeMuwsW.js +1 -0
- package/.output/public/assets/installations-DnvxI9Dt.js +1 -0
- package/.output/public/assets/loader-circle-Ca7C9YY1.js +1 -0
- package/.output/public/assets/mutation-x9L28_CD.js +1 -0
- package/.output/public/assets/preload-helper-BN-UkjOE.js +1 -0
- package/.output/public/assets/refresh-cw-CVGqvy8Z.js +1 -0
- package/.output/public/assets/routes-BhK8DuZA.js +1 -0
- package/.output/public/assets/routes-veKtDW39.js +2 -0
- package/.output/public/assets/sidebar-B45aMqRm.js +3 -0
- package/.output/public/assets/styles-Dn0pK1EX.css +2 -0
- package/.output/public/assets/table-CwLnN6KX.js +1 -0
- package/.output/public/assets/useCompositeListItem-CF7vsfcA.js +1 -0
- package/.output/public/assets/useMutation-CN1ygYEr.js +1 -0
- package/.output/public/assets/useOpenInteractionType-AnAMWYIY.js +1 -0
- package/.output/public/assets/valueToPercent-C-t8FT0y.js +1 -0
- package/.output/server/{__23tanstack-start-server-fn-resolver-B7ioJhtE.mjs → __23tanstack-start-server-fn-resolver-CUQLz7hs.mjs} +52 -40
- package/.output/server/_libs/@base-ui/react+[...].mjs +7044 -3622
- package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +49 -3
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +4 -4
- 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 +1159 -0
- package/.output/server/_libs/eventemitter3.mjs +236 -0
- package/.output/server/_libs/immer.mjs +532 -0
- package/.output/server/_libs/lucide-react.mjs +46 -32
- package/.output/server/_libs/next-themes.mjs +2 -2
- package/.output/server/_libs/react-redux+[...].mjs +205 -0
- package/.output/server/_libs/recharts+[...].mjs +14350 -0
- package/.output/server/_libs/sonner.mjs +2 -2
- package/.output/server/_libs/tanstack__react-query.mjs +8 -3
- package/.output/server/_libs/verkit.mjs +99 -1
- package/.output/server/_runtime.mjs +10 -1
- package/.output/server/_ssr/ConsoleFeatureUnavailable-DoxET2AT.mjs +58 -0
- package/.output/server/_ssr/{HashValueDisplay-DwpGLfrl.mjs → HashValueDisplay-BplboHEb.mjs} +3 -3
- package/.output/server/_ssr/InsightsInfo-o2YPd5Qc.mjs +37 -0
- package/.output/server/_ssr/InsightsPageHeader-NExCJL1I.mjs +154 -0
- package/.output/server/_ssr/NotFoundPage-Cd-A8ock.mjs +37 -0
- package/.output/server/_ssr/api-B1Ro0uys.mjs +202 -0
- package/.output/server/_ssr/api-C0G9MTMI.mjs +4205 -0
- package/.output/server/_ssr/{api-keys-CZeTPQys.mjs → api-keys-CT3HG7KT.mjs} +49 -9
- package/.output/server/_ssr/api-keys-Dvvy1sem.mjs +5 -0
- package/.output/server/_ssr/api-keys-rpc-b1qTTFfG.mjs +52 -0
- package/.output/server/_ssr/api-rpc-Cbns2MGx.mjs +338 -0
- package/.output/server/_ssr/{auth-rpc-DUaeOWKK.mjs → auth-rpc-DChS7m9y.mjs} +6 -6
- package/.output/server/_ssr/auth.server-Ccqz2iVv.mjs +37 -0
- package/.output/server/_ssr/bundleActivity-D17Pb_h5.mjs +19 -0
- package/.output/server/_ssr/card-DMbcvlPz.mjs +49 -0
- package/.output/server/_ssr/config-DMEJdhoW.mjs +13 -0
- package/.output/server/_ssr/config.server-DyypN6cQ.mjs +60 -0
- package/.output/server/_ssr/console-access-KpPv5lin.mjs +17 -0
- package/.output/server/_ssr/console-features-CsbyHYfK.mjs +55 -0
- package/.output/server/_ssr/console-features-api-B-KVuo4p.mjs +43 -0
- package/.output/server/_ssr/console-features-rpc-Djyo5740.mjs +19 -0
- package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-B_I3Ll4M.mjs} +28 -2
- package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-DuNm5mmo.mjs} +2 -2
- package/.output/server/_ssr/databaseRows-CXxf_VIB.mjs +132 -0
- package/.output/server/_ssr/{deleteBundle-BTGwBgzx.mjs → deleteBundle-Dcc21Lbd.mjs} +9 -19
- package/.output/server/_ssr/{dialog-DS-nw-pA.mjs → dialog-cOmLTUV2.mjs} +2 -2
- package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-B-r4u3Lg.mjs} +28 -5
- package/.output/server/_ssr/{downloadBundle-CNnYxYO2.mjs → downloadBundle-CexOFoZ4.mjs} +6 -7
- package/.output/server/_ssr/empty-BHoY_5jt.mjs +49 -0
- package/.output/server/_ssr/{table-CnZmon_Z.mjs → field-mjhcNEXD.mjs} +63 -51
- package/.output/server/_ssr/getBundleChildren-DbDXe9wB.mjs +22 -0
- package/.output/server/_ssr/{input-group-BpsnmvZw.mjs → input-group-96H30HWC.mjs} +2 -2
- package/.output/server/_ssr/insights-D25VDVaz.mjs +5 -0
- package/.output/server/_ssr/insights-D359GJFL.mjs +14 -0
- package/.output/server/_ssr/insights-DgFv1NsO.mjs +1238 -0
- package/.output/server/_ssr/insights-api-ChAKrbhG.mjs +105 -0
- package/.output/server/_ssr/insights-failures-WQ-ttjOx.mjs +44 -0
- package/.output/server/_ssr/insights-failures-rpc-BFsqlltN.mjs +21 -0
- package/.output/server/_ssr/insights-recovery-DkL6bxMW.mjs +28 -0
- package/.output/server/_ssr/insights-recovery-rpc-2P5m8F62.mjs +46 -0
- package/.output/server/_ssr/insights-recovery-rpc-Tj8vNC19.mjs +20 -0
- package/.output/server/_ssr/{insights-rpc-38DujK0G.mjs → insights-rpc-DrkVmV9o.mjs} +22 -14
- package/.output/server/_ssr/insights-search-BsalnNw-.mjs +25 -0
- package/.output/server/_ssr/insights-usage-1nVTQrLy.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-Ct_4xHhZ.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-DaYIRtFK.mjs +41 -0
- package/.output/server/_ssr/insightsRecovery-CpGucWN1.mjs +45 -0
- package/.output/server/_ssr/insightsUsage-JYpKZAF7.mjs +56 -0
- package/.output/server/_ssr/insights_.distribution-Bcu6uYs_.mjs +5 -0
- package/.output/server/_ssr/insights_.distribution-D3Csuyjw.mjs +14 -0
- package/.output/server/_ssr/insights_.distribution-jQZEF07J.mjs +335 -0
- package/.output/server/_ssr/installations-B8Vh54lH.mjs +69 -0
- package/.output/server/_ssr/installations-BI5kelHv.mjs +5 -0
- package/.output/server/_ssr/{installations-DiVKEF5T.mjs → installations-Cxikllmx.mjs} +517 -54
- package/.output/server/_ssr/listReleases-DqU9t1l3.mjs +79 -0
- package/.output/server/_ssr/releaseManagement-B3e7I8J9.mjs +11 -0
- package/.output/server/_ssr/{router-B9wG_9f8.mjs → router-CRkm-wHD.mjs} +117 -124
- package/.output/server/_ssr/routes-B3IJo_jL.mjs +292 -0
- package/.output/server/_ssr/{routes-D7Yh_jfj.mjs → routes-CNAi0Mmv.mjs} +176 -455
- package/.output/server/_ssr/runtime.server-NucbYjc7.mjs +1372 -0
- package/.output/server/_ssr/select-BkPAJFL7.mjs +107 -0
- package/.output/server/_ssr/{server-D_C2rMzf.mjs → server-DKJYsaCP.mjs} +6 -32
- package/.output/server/_ssr/{createSsrRpc-DjWNJ0uc.mjs → sidebar-Cw1VuC7d.mjs} +117 -63
- package/.output/server/_ssr/ssr.mjs +1 -1
- package/.output/server/_ssr/table-q0P9wz7x.mjs +53 -0
- package/.output/server/_ssr/updateFailures-DMU9f0pK.mjs +24 -0
- package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-BzSO-fr-.mjs} +2 -2
- package/.output/server/_tanstack-start-manifest_v-Bb0gKDbb.mjs +124 -0
- package/.output/server/index.mjs +273 -168
- package/README.md +71 -65
- package/lib/index.d.mts +13 -11
- package/lib/vite-internal.mjs +6 -3
- package/package.json +8 -8
- package/src/components/AppSidebar.tsx +91 -71
- package/src/components/ConsoleAccessPage.tsx +5 -1
- package/src/components/ConsoleFeatureUnavailable.tsx +82 -0
- package/src/components/NotFoundPage.tsx +7 -7
- package/src/components/features/FeatureSlots.tsx +99 -0
- package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
- package/src/components/features/bundles/BundleMetadata.tsx +7 -4
- package/src/components/features/channels/ChannelManagementDialog.tsx +2 -5
- package/src/components/features/insights/AppUsage.tsx +370 -0
- package/src/components/features/insights/EstimatedCount.tsx +18 -0
- package/src/components/features/insights/EventDetails.tsx +117 -37
- package/src/components/features/insights/EventHistoryCard.tsx +112 -26
- package/src/components/features/insights/EventHistoryList.tsx +2 -2
- package/src/components/features/insights/InsightsControls.tsx +265 -102
- package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
- package/src/components/features/insights/InsightsInfo.tsx +48 -0
- package/src/components/features/insights/InsightsOverview.tsx +176 -173
- package/src/components/features/insights/InsightsPageHeader.tsx +35 -20
- package/src/components/features/insights/InsightsPeriodSelector.tsx +89 -0
- package/src/components/features/insights/InstallationHistoryCard.tsx +52 -10
- package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
- package/src/components/features/insights/ReleaseActivity.tsx +188 -0
- package/src/components/features/insights/ReportingDevicesSummary.tsx +65 -0
- package/src/components/features/insights/UpdateFailures.tsx +319 -0
- package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
- package/src/components/features/releases/ReleaseEditorSheet.tsx +24 -1
- package/src/components/ui/card.tsx +1 -1
- package/src/components/ui/empty.tsx +101 -0
- package/src/components/ui/scroll-area.tsx +52 -0
- package/src/components/ui/sidebar.tsx +22 -29
- package/src/components/ui/tabs.tsx +81 -0
- package/src/index.ts +12 -17
- package/src/lib/api-keys-api.ts +0 -28
- package/src/lib/api-keys-rpc.ts +27 -38
- package/src/lib/api-rpc.ts +197 -221
- package/src/lib/api.ts +11 -13
- package/src/lib/bundle-activity.ts +31 -0
- package/src/lib/console-access.ts +20 -0
- package/src/lib/console-features-api.ts +68 -0
- package/src/lib/console-features-rpc.ts +13 -0
- package/src/lib/console-features.ts +91 -0
- package/src/lib/insights-api.ts +37 -1
- package/src/lib/insights-failures-rpc.ts +21 -0
- package/src/lib/insights-failures.ts +129 -0
- package/src/lib/insights-recovery-rpc.ts +51 -0
- package/src/lib/insights-recovery.ts +68 -0
- package/src/lib/insights-retention.ts +21 -0
- package/src/lib/insights-rpc.ts +30 -12
- package/src/lib/insights-search.ts +58 -0
- package/src/lib/insights-usage-rpc.ts +29 -0
- package/src/lib/insights-usage.ts +74 -0
- package/src/lib/insights-view.ts +38 -2
- package/src/lib/scroll-restoration.ts +21 -0
- package/src/lib/server/adminInsights.ts +124 -0
- package/src/lib/server/auth.server.ts +13 -5
- package/src/lib/server/bundleActivity.ts +37 -0
- package/src/lib/server/config.server.ts +16 -32
- package/src/lib/server/console-feature-apis.ts +59 -0
- package/src/lib/server/console-runtime.server.ts +6 -40
- package/src/lib/server/deleteBundle.ts +16 -25
- package/src/lib/server/downloadBundle.ts +7 -10
- package/src/lib/server/getBundleChildren.ts +31 -122
- package/src/lib/server/insightsRecovery.ts +55 -0
- package/src/lib/server/insightsUsage.ts +63 -0
- package/src/lib/server/listReleases.ts +109 -44
- package/src/lib/server/releaseReachability.ts +4 -3
- package/src/lib/server/runtime.server.ts +132 -31
- package/src/lib/server/updateFailures.ts +31 -0
- package/src/routeTree.gen.ts +21 -21
- package/src/router.tsx +3 -2
- package/src/routes/-installations-search.ts +32 -8
- package/src/routes/-releases-search.ts +76 -29
- package/src/routes/__root.tsx +10 -4
- package/src/routes/api/bundles/$bundleId/download.ts +2 -5
- package/src/routes/api-keys.tsx +5 -2
- package/src/routes/index.tsx +167 -104
- package/src/routes/insights.tsx +100 -59
- package/src/routes/insights_.distribution.tsx +398 -0
- package/src/routes/installations.tsx +98 -41
- package/src/vite-internal.ts +6 -3
- package/.output/public/assets/HashValueDisplay-CB0Eskit.js +0 -1
- package/.output/public/assets/InsightsPageHeader-Car5LNiZ.js +0 -1
- package/.output/public/assets/api-keys-BzKocqRy.js +0 -1
- package/.output/public/assets/api-keys-api-BHcODB7q.js +0 -1
- package/.output/public/assets/card-Dhr_eFYj.js +0 -1
- package/.output/public/assets/chart-no-axes-combined-Dy4WJv2U.js +0 -1
- package/.output/public/assets/composite-Bp98A0Ld.js +0 -1
- package/.output/public/assets/createServerFn-CA8fs7QD.js +0 -3
- package/.output/public/assets/dialog-CUTrcPlB.js +0 -1
- package/.output/public/assets/dist-C_NydL0e.js +0 -1
- package/.output/public/assets/index-DdAz-CXv.js +0 -10
- package/.output/public/assets/input-group-cSOG4wh_.js +0 -1
- package/.output/public/assets/insights-C0PBSZ-n.js +0 -1
- package/.output/public/assets/installations-CE33wgbY.js +0 -2
- package/.output/public/assets/installations-CStZja8x.js +0 -1
- package/.output/public/assets/lazyRouteComponent-Ccp3bwBY.js +0 -1
- package/.output/public/assets/link-CI8idAUi.js +0 -1
- package/.output/public/assets/loader-circle-BoxLrsOu.js +0 -1
- package/.output/public/assets/preload-helper-CQ36nxok.js +0 -1
- package/.output/public/assets/refresh-cw-Bqc72UZG.js +0 -1
- package/.output/public/assets/routes-DGVXdzpw.js +0 -2
- package/.output/public/assets/routes-DTlwOr8I.js +0 -1
- package/.output/public/assets/shield-check-uK3MHWJZ.js +0 -1
- package/.output/public/assets/signing-CMf4aqDO.js +0 -1
- package/.output/public/assets/styles-BCvbtKsm.css +0 -2
- package/.output/public/assets/table-2vcV89QB.js +0 -1
- package/.output/public/assets/useCompositeListItem-CHLITE_4.js +0 -1
- package/.output/public/assets/x-DCd0j8c6.js +0 -1
- package/.output/server/_ssr/InsightsPageHeader-Df4Brj8u.mjs +0 -484
- package/.output/server/_ssr/api-keys-api-BUPQVSpk.mjs +0 -60
- package/.output/server/_ssr/api-keys-rpc-Cs7tS5_j.mjs +0 -136
- package/.output/server/_ssr/api-rpc-tAupfGQm.mjs +0 -1399
- package/.output/server/_ssr/auth.server-DW71WzsD.mjs +0 -47
- package/.output/server/_ssr/bundle-signing-rpc-BhnBw0-d.mjs +0 -16
- package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +0 -12
- package/.output/server/_ssr/card-BRiPmi0C.mjs +0 -108
- package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
- package/.output/server/_ssr/config.server-afDuAwaG.mjs +0 -368
- package/.output/server/_ssr/databaseJsonValue-d6I3nAS4.mjs +0 -45
- package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
- package/.output/server/_ssr/getBundleChildren-C0vh4ZvE.mjs +0 -50
- package/.output/server/_ssr/insights-BdS364Xu.mjs +0 -460
- package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +0 -303
- package/.output/server/_ssr/installations-CtA59MQG.mjs +0 -29
- package/.output/server/_ssr/routes-CKrrS2q2.mjs +0 -37
- package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +0 -438
- package/.output/server/_ssr/signing-Dz1WkZOf.mjs +0 -133
- package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +0 -97
- package/src/components/features/insights/InsightsBundleReports.tsx +0 -51
- package/src/components/features/signing/BundleSigningPage.tsx +0 -139
- package/src/lib/bundle-signing-api.ts +0 -14
- package/src/lib/bundle-signing-rpc.ts +0 -16
- package/src/lib/server/bundle-signing.server.ts +0 -23
- package/src/routes/signing.tsx +0 -37
|
@@ -0,0 +1,1372 @@
|
|
|
1
|
+
import { i as resolveConsoleFeatures, n as consoleFeatures, t as ConsoleFeatureUnavailableError } from "./console-features-CsbyHYfK.mjs";
|
|
2
|
+
import { C as isUUIDv7 } from "./dist-B-r4u3Lg.mjs";
|
|
3
|
+
import { n as DatabasePluginInputError, o as isDatabaseJsonObject } from "./databaseRows-CXxf_VIB.mjs";
|
|
4
|
+
import { t as createUUIDv7 } from "./uuidv7-BzSO-fr-.mjs";
|
|
5
|
+
import { a as createCoreReads, c as engineAdapterOf, d as validateSchema, i as createCoreApi, l as pruneDuringWrites, n as aggregateBatchingModule, r as coreModule, s as createDatabaseEngine, t as OFF_ENGINE_DATABASE, u as resolveSchema } from "./api-C0G9MTMI.mjs";
|
|
6
|
+
//#region node_modules/.nitro/vite/services/ssr/assets/runtime.server-NucbYjc7.js
|
|
7
|
+
/** Whether a configured database is a self-hosted server's. */
|
|
8
|
+
var isRemoteDatabase = (database) => "core" in database && "fetchAdmin" in database;
|
|
9
|
+
/** The settings rows migrations write, last, and the fence reads. */
|
|
10
|
+
var SETTINGS_TABLE = {
|
|
11
|
+
name: "private_hot_updater_settings",
|
|
12
|
+
columns: [
|
|
13
|
+
{
|
|
14
|
+
name: "key",
|
|
15
|
+
type: "string",
|
|
16
|
+
nullable: false,
|
|
17
|
+
maxLength: 255
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "value",
|
|
21
|
+
type: "string",
|
|
22
|
+
nullable: false,
|
|
23
|
+
maxLength: 255
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "_v",
|
|
27
|
+
type: "integer",
|
|
28
|
+
nullable: false,
|
|
29
|
+
default: 0
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
key: ["key"],
|
|
33
|
+
indexes: []
|
|
34
|
+
};
|
|
35
|
+
/** The engine's storage-layout generation; it changes only with a migration. */
|
|
36
|
+
var ENGINE_SCHEMA_KEY = "schema.engine";
|
|
37
|
+
var settingMessage = (adapterName, { key, expected, found }) => `Hot Updater schema setting "${key}" for ${adapterName} is ${found === null ? "missing" : `"${found}"`}; expected "${expected}". ${key === "schema.engine" ? "Create a new empty database and run `hot-updater db migrate`; databases from before the storage engine are not converted." : "Run `hot-updater db migrate`."}`;
|
|
38
|
+
/** The database needs `hot-updater db migrate`, or a new database; the handler answers 503. */
|
|
39
|
+
var HotUpdaterSchemaMigrationRequiredError = class extends Error {
|
|
40
|
+
adapterName;
|
|
41
|
+
currentVersion;
|
|
42
|
+
setting;
|
|
43
|
+
constructor(adapterName, currentVersion, setting, options) {
|
|
44
|
+
super(setting !== void 0 ? settingMessage(adapterName, setting) : currentVersion === void 0 ? `Hot Updater database schema is not initialized for ${adapterName}. Run \`hot-updater db migrate\` before using this adapter.` : `Hot Updater v1 cannot migrate schema ${currentVersion} in place. Create a new empty database and run \`hot-updater db migrate\`.`, options);
|
|
45
|
+
this.adapterName = adapterName;
|
|
46
|
+
this.currentVersion = currentVersion;
|
|
47
|
+
this.setting = setting;
|
|
48
|
+
this.name = "HotUpdaterSchemaMigrationRequiredError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/** Driver codes for a table or column the database lacks. */
|
|
52
|
+
var MISSING_SCHEMA_CODES = /* @__PURE__ */ new Set([
|
|
53
|
+
"42P01",
|
|
54
|
+
"42703",
|
|
55
|
+
"ER_NO_SUCH_TABLE",
|
|
56
|
+
"ER_BAD_FIELD_ERROR"
|
|
57
|
+
]);
|
|
58
|
+
var MISSING_SCHEMA_MESSAGE = /no such (?:table|column)|(?:relation|column) .+ does not exist|table .+ doesn't exist|unknown column/iu;
|
|
59
|
+
/**
|
|
60
|
+
* Whether a read failed because a table or column is missing, so the
|
|
61
|
+
* database needs migrations. ORMs may wrap the driver's error in `cause`.
|
|
62
|
+
*/
|
|
63
|
+
var isMissingSchemaError = (error) => {
|
|
64
|
+
for (let depth = 0; error instanceof Object && depth < 8; depth += 1) {
|
|
65
|
+
const { code, message, cause } = error;
|
|
66
|
+
if (MISSING_SCHEMA_CODES.has(String(code)) || typeof message === "string" && MISSING_SCHEMA_MESSAGE.test(message)) return true;
|
|
67
|
+
error = cause;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
};
|
|
71
|
+
var readSettings = (adapter, keys) => adapter.get(SETTINGS_TABLE, keys.map((key) => [key]));
|
|
72
|
+
/** Throws unless every expected setting is stored with its value: one batch read. */
|
|
73
|
+
var checkSchemaFence = async (adapter, adapterName, expected) => {
|
|
74
|
+
const keys = Object.keys(expected);
|
|
75
|
+
let rows;
|
|
76
|
+
try {
|
|
77
|
+
rows = await readSettings(adapter, keys);
|
|
78
|
+
} catch (cause) {
|
|
79
|
+
if (!isMissingSchemaError(cause)) throw cause;
|
|
80
|
+
throw new HotUpdaterSchemaMigrationRequiredError(adapterName, void 0, {
|
|
81
|
+
key: ENGINE_SCHEMA_KEY,
|
|
82
|
+
expected: "1",
|
|
83
|
+
found: null
|
|
84
|
+
}, { cause });
|
|
85
|
+
}
|
|
86
|
+
keys.forEach((key, position) => {
|
|
87
|
+
const found = rows[position]?.value;
|
|
88
|
+
if (found !== expected[key]) throw new HotUpdaterSchemaMigrationRequiredError(adapterName, void 0, {
|
|
89
|
+
key,
|
|
90
|
+
expected: expected[key],
|
|
91
|
+
found: typeof found === "string" ? found : null
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
/** The database name a fenced adapter carries, so a server can fence its plugins' rows too. */
|
|
96
|
+
var FENCED = Symbol.for("@hot-updater/server/schema-fence");
|
|
97
|
+
/** The database name an adapter behind the schema fence reports under. */
|
|
98
|
+
var fencedName = (adapter) => adapter[FENCED];
|
|
99
|
+
/**
|
|
100
|
+
* The adapter behind the schema fence: a process's first read or write
|
|
101
|
+
* checks the settings first. Success is kept; a failure is checked again on
|
|
102
|
+
* the next call.
|
|
103
|
+
*/
|
|
104
|
+
var withSchemaFence = (adapter, adapterName, expected) => {
|
|
105
|
+
let ready;
|
|
106
|
+
const fence = () => {
|
|
107
|
+
ready ??= checkSchemaFence(adapter, adapterName, expected).catch((error) => {
|
|
108
|
+
ready = void 0;
|
|
109
|
+
throw error;
|
|
110
|
+
});
|
|
111
|
+
return ready;
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
...adapter,
|
|
115
|
+
[FENCED]: adapterName,
|
|
116
|
+
get: (table, keys) => fence().then(() => adapter.get(table, keys)),
|
|
117
|
+
query: (table, request) => fence().then(() => adapter.query(table, request)),
|
|
118
|
+
write: (ops) => fence().then(() => adapter.write(ops)),
|
|
119
|
+
...adapter.prune && { prune: (table, before, limit) => fence().then(() => adapter.prune(table, before, limit)) }
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
/** A misconfigured `createHotUpdater` call, reported at startup. */
|
|
123
|
+
var HotUpdaterConfigError = class extends Error {
|
|
124
|
+
name = "HotUpdaterConfigError";
|
|
125
|
+
};
|
|
126
|
+
var EXPORT_NAME = /^[A-Za-z_$][\w$]*$/u;
|
|
127
|
+
/**
|
|
128
|
+
* Names app code cannot import as they are: reserved words, and the names
|
|
129
|
+
* the printed app code declares itself.
|
|
130
|
+
*/
|
|
131
|
+
var UNAVAILABLE_NAMES = /* @__PURE__ */ new Set([
|
|
132
|
+
"App",
|
|
133
|
+
"HotUpdater",
|
|
134
|
+
"arguments",
|
|
135
|
+
"await",
|
|
136
|
+
"break",
|
|
137
|
+
"case",
|
|
138
|
+
"catch",
|
|
139
|
+
"class",
|
|
140
|
+
"const",
|
|
141
|
+
"continue",
|
|
142
|
+
"debugger",
|
|
143
|
+
"default",
|
|
144
|
+
"delete",
|
|
145
|
+
"do",
|
|
146
|
+
"else",
|
|
147
|
+
"enum",
|
|
148
|
+
"eval",
|
|
149
|
+
"export",
|
|
150
|
+
"extends",
|
|
151
|
+
"false",
|
|
152
|
+
"finally",
|
|
153
|
+
"for",
|
|
154
|
+
"function",
|
|
155
|
+
"if",
|
|
156
|
+
"implements",
|
|
157
|
+
"import",
|
|
158
|
+
"in",
|
|
159
|
+
"instanceof",
|
|
160
|
+
"interface",
|
|
161
|
+
"let",
|
|
162
|
+
"new",
|
|
163
|
+
"null",
|
|
164
|
+
"package",
|
|
165
|
+
"private",
|
|
166
|
+
"protected",
|
|
167
|
+
"public",
|
|
168
|
+
"return",
|
|
169
|
+
"static",
|
|
170
|
+
"super",
|
|
171
|
+
"switch",
|
|
172
|
+
"this",
|
|
173
|
+
"throw",
|
|
174
|
+
"true",
|
|
175
|
+
"try",
|
|
176
|
+
"typeof",
|
|
177
|
+
"var",
|
|
178
|
+
"void",
|
|
179
|
+
"while",
|
|
180
|
+
"with",
|
|
181
|
+
"yield"
|
|
182
|
+
]);
|
|
183
|
+
var isRecord$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
184
|
+
/**
|
|
185
|
+
* The client plugins an app adds for the plugins, each once, in plugin
|
|
186
|
+
* order: what init prints in `HotUpdater.init`'s `plugins`. A malformed one,
|
|
187
|
+
* or one name from two modules, throws `HotUpdaterConfigError`.
|
|
188
|
+
*/
|
|
189
|
+
var clientPluginsOf = (plugins) => {
|
|
190
|
+
if (!Array.isArray(plugins)) throw new HotUpdaterConfigError("plugins must be an array of plugins.");
|
|
191
|
+
const found = [];
|
|
192
|
+
for (const plugin of plugins) {
|
|
193
|
+
if (!isRecord$5(plugin) || !isRecord$5(plugin.cli)) continue;
|
|
194
|
+
const value = plugin.cli.clientPlugin;
|
|
195
|
+
if (value === void 0) continue;
|
|
196
|
+
const id = String(plugin.id);
|
|
197
|
+
if (!isRecord$5(value) || typeof value.module !== "string" || value.module === "" || typeof value.name !== "string" || !EXPORT_NAME.test(value.name) || UNAVAILABLE_NAMES.has(value.name)) throw new HotUpdaterConfigError(`Plugin "${id}" cli.clientPlugin needs a module and an export name the app can import: an identifier other than a reserved word, App, or HotUpdater.`);
|
|
198
|
+
const { module, name } = value;
|
|
199
|
+
const named = found.find((entry) => entry.name === name);
|
|
200
|
+
if (named === void 0) found.push({
|
|
201
|
+
module,
|
|
202
|
+
name
|
|
203
|
+
});
|
|
204
|
+
else if (named.module !== module) throw new HotUpdaterConfigError(`Plugin "${id}" names the client plugin "${name}" from ${module}, but another plugin names it from ${named.module}.`);
|
|
205
|
+
}
|
|
206
|
+
return found;
|
|
207
|
+
};
|
|
208
|
+
resolveSchema([coreModule]);
|
|
209
|
+
/** Each plugin's `schema.<id>` settings row. */
|
|
210
|
+
var pluginSettings = (plugins) => Object.fromEntries(plugins.map(({ id, schemaVersion }) => [`schema.${id}`, schemaVersion]));
|
|
211
|
+
/** A plugin's schema as the engine resolves it: prefixed by its id unless it keeps its names. */
|
|
212
|
+
var pluginModule = ({ id, schema, namespace }) => ({
|
|
213
|
+
id,
|
|
214
|
+
schema,
|
|
215
|
+
...namespace === false ? {} : { namespace: id }
|
|
216
|
+
});
|
|
217
|
+
var PLUGIN_KEYS = /* @__PURE__ */ new Set([
|
|
218
|
+
"id",
|
|
219
|
+
"provides",
|
|
220
|
+
"schemaVersion",
|
|
221
|
+
"schema",
|
|
222
|
+
"namespace",
|
|
223
|
+
"init",
|
|
224
|
+
"cli"
|
|
225
|
+
]);
|
|
226
|
+
var CLI_KEYS = /* @__PURE__ */ new Set([
|
|
227
|
+
"commands",
|
|
228
|
+
"clientCredential",
|
|
229
|
+
"clientPlugin"
|
|
230
|
+
]);
|
|
231
|
+
var INSTANCE_KEYS = /* @__PURE__ */ new Set([
|
|
232
|
+
"api",
|
|
233
|
+
"endpoints",
|
|
234
|
+
"clientAuth"
|
|
235
|
+
]);
|
|
236
|
+
var METHODS = /* @__PURE__ */ new Set([
|
|
237
|
+
"GET",
|
|
238
|
+
"POST",
|
|
239
|
+
"PUT",
|
|
240
|
+
"PATCH",
|
|
241
|
+
"DELETE"
|
|
242
|
+
]);
|
|
243
|
+
/** An id that prefixes table names is a SQL name; one that does not may use camelCase. */
|
|
244
|
+
var PREFIX_ID = /^[a-z][a-z0-9_]*$/u;
|
|
245
|
+
var UNPREFIXED_ID = /^[a-z][A-Za-z0-9_]*$/u;
|
|
246
|
+
var PATH = /^(\/(:?[A-Za-z0-9_.~-]+))+$/u;
|
|
247
|
+
var fail = (message) => {
|
|
248
|
+
throw new HotUpdaterConfigError(message);
|
|
249
|
+
};
|
|
250
|
+
var isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
251
|
+
var checkPlugin = (value, at) => {
|
|
252
|
+
if (!isRecord$4(value)) return fail(`${at} is not a plugin.`);
|
|
253
|
+
if ("kind" in value) fail(`${at} has a kind; core is built in and is never passed as a plugin.`);
|
|
254
|
+
const unknown = Object.keys(value).find((key) => !PLUGIN_KEYS.has(key));
|
|
255
|
+
if (unknown !== void 0) fail(`${at} has an unknown key "${unknown}".`);
|
|
256
|
+
const plugin = value;
|
|
257
|
+
if (plugin.namespace !== void 0 && plugin.namespace !== false) fail(`${at} may only set namespace: false.`);
|
|
258
|
+
const pattern = plugin.namespace === false ? UNPREFIXED_ID : PREFIX_ID;
|
|
259
|
+
if (typeof plugin.id !== "string" || !pattern.test(plugin.id)) fail(`${at} needs an id matching ${pattern.source}.`);
|
|
260
|
+
if (plugin.id === coreModule.id) fail(`${at} uses the id "core", which is core's own.`);
|
|
261
|
+
if (typeof plugin.schemaVersion !== "string" || !isRecord$4(plugin.schema) || typeof plugin.init !== "function") fail(`Plugin "${plugin.id}" needs a schemaVersion, a schema, and init.`);
|
|
262
|
+
const provides = plugin.provides;
|
|
263
|
+
if (provides !== void 0 && (!isRecord$4(provides) || Object.entries(provides).some(([key, flag]) => key !== "clientAuth" || flag !== true))) fail(`Plugin "${plugin.id}" may only declare provides: { clientAuth: true }.`);
|
|
264
|
+
const cli = value.cli;
|
|
265
|
+
if (cli !== void 0) {
|
|
266
|
+
if (!isRecord$4(cli) || Object.keys(cli).some((key) => !CLI_KEYS.has(key)) || cli.commands !== void 0 && !Array.isArray(cli.commands)) fail(`Plugin "${plugin.id}" cli may only hold commands (an array), clientCredential, and clientPlugin.`);
|
|
267
|
+
if (cli.clientCredential !== void 0 && plugin.provides?.clientAuth !== true) fail(`Plugin "${plugin.id}" adds cli.clientCredential without declaring provides: { clientAuth: true }.`);
|
|
268
|
+
}
|
|
269
|
+
return plugin;
|
|
270
|
+
};
|
|
271
|
+
var checkEndpoint = (id, value) => {
|
|
272
|
+
const endpoint = isRecord$4(value) ? value : {};
|
|
273
|
+
if (!METHODS.has(endpoint.method) || typeof endpoint.path !== "string" || !PATH.test(endpoint.path) || endpoint.access !== "client" && endpoint.access !== "admin" || typeof endpoint.handler !== "function") fail(`Plugin "${id}" has an endpoint that needs a method, a path like /name/:param, access "client" or "admin", and a handler.`);
|
|
274
|
+
return {
|
|
275
|
+
...endpoint,
|
|
276
|
+
plugin: id
|
|
277
|
+
};
|
|
278
|
+
};
|
|
279
|
+
var checkInstance = (plugin, instance) => {
|
|
280
|
+
const { id } = plugin;
|
|
281
|
+
if (!isRecord$4(instance)) return fail(`Plugin "${id}" init returned no instance.`);
|
|
282
|
+
if (typeof instance.then === "function") fail(`Plugin "${id}" init returned a promise; init must be synchronous.`);
|
|
283
|
+
const unknown = Object.keys(instance).find((key) => !INSTANCE_KEYS.has(key));
|
|
284
|
+
if (unknown !== void 0 || !("api" in instance)) fail(`Plugin "${id}" init must return { api, endpoints?, clientAuth? }${unknown === void 0 ? "" : `, not "${unknown}"`}.`);
|
|
285
|
+
const declared = plugin.provides?.clientAuth === true;
|
|
286
|
+
const clientAuth = instance.clientAuth;
|
|
287
|
+
if (declared !== (clientAuth !== void 0)) fail(declared ? `Plugin "${id}" declares provides: { clientAuth: true } but its instance has no clientAuth.` : `Plugin "${id}" returns clientAuth without declaring provides: { clientAuth: true }.`);
|
|
288
|
+
if (clientAuth !== void 0 && (typeof clientAuth.authenticate !== "function" || !Array.isArray(clientAuth.varyHeaders) || !clientAuth.varyHeaders.every((header) => typeof header === "string"))) fail(`Plugin "${id}" clientAuth needs varyHeaders and authenticate.`);
|
|
289
|
+
const endpoints = instance.endpoints ?? [];
|
|
290
|
+
if (!Array.isArray(endpoints)) fail(`Plugin "${id}" endpoints must be an array.`);
|
|
291
|
+
return {
|
|
292
|
+
api: instance.api,
|
|
293
|
+
clientAuth,
|
|
294
|
+
endpoints: endpoints.map((endpoint) => checkEndpoint(id, endpoint))
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
/**
|
|
298
|
+
* Checks every plugin, runs each `init` once against one engine over the
|
|
299
|
+
* database's storage adapter, and collects APIs, endpoints, and clientAuth.
|
|
300
|
+
* Core's reads run on the same engine, so plugins read core through `ctx.core`.
|
|
301
|
+
*/
|
|
302
|
+
var assemblePlugins = (value, adapter, { now = Date.now, storage = { resolveFileUrl: async () => null }, verify = false, onCachedRoutesChange, batching } = {}) => {
|
|
303
|
+
if (!Array.isArray(value)) return fail("plugins must be an array of plugins.");
|
|
304
|
+
const plugins = value.map((plugin, position) => checkPlugin(plugin, `plugins[${position}]`));
|
|
305
|
+
const ids = /* @__PURE__ */ new Set();
|
|
306
|
+
for (const { id } of plugins) {
|
|
307
|
+
if (ids.has(id)) fail(`Plugin "${id}" is registered twice.`);
|
|
308
|
+
ids.add(id);
|
|
309
|
+
}
|
|
310
|
+
clientPluginsOf(plugins);
|
|
311
|
+
const modules = plugins.map(pluginModule);
|
|
312
|
+
if (modules.length > 0) validateSchema([coreModule, ...modules]);
|
|
313
|
+
const name = fencedName(adapter);
|
|
314
|
+
const fenced = name === void 0 || plugins.length === 0 ? adapter : withSchemaFence(adapter, name, pluginSettings(plugins));
|
|
315
|
+
const schema = resolveSchema([
|
|
316
|
+
coreModule,
|
|
317
|
+
...modules,
|
|
318
|
+
...batching === void 0 ? [] : [aggregateBatchingModule]
|
|
319
|
+
]);
|
|
320
|
+
const engine = createDatabaseEngine({
|
|
321
|
+
adapter: verify ? fenced : pruneDuringWrites(fenced, schema.tables, {
|
|
322
|
+
leaseTable: SETTINGS_TABLE,
|
|
323
|
+
now
|
|
324
|
+
}),
|
|
325
|
+
schema,
|
|
326
|
+
verify,
|
|
327
|
+
...batching === void 0 ? {} : {
|
|
328
|
+
batching,
|
|
329
|
+
now
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
const coreDatabase = engine.database(coreModule);
|
|
333
|
+
const core = createCoreApi(coreDatabase, storage, {
|
|
334
|
+
now,
|
|
335
|
+
...onCachedRoutesChange === void 0 ? {} : { onCachedRoutesChange }
|
|
336
|
+
});
|
|
337
|
+
const reads = createCoreReads(coreDatabase, storage);
|
|
338
|
+
const api = {};
|
|
339
|
+
const endpoints = [];
|
|
340
|
+
let clientAuth;
|
|
341
|
+
plugins.forEach((plugin, position) => {
|
|
342
|
+
const instance = checkInstance(plugin, plugin.init({
|
|
343
|
+
db: engine.database(modules[position]),
|
|
344
|
+
core: reads,
|
|
345
|
+
now
|
|
346
|
+
}));
|
|
347
|
+
if (instance.clientAuth !== void 0) {
|
|
348
|
+
if (clientAuth !== void 0) fail(`Plugins "${clientAuth.plugin}" and "${plugin.id}" both provide clientAuth; keep one.`);
|
|
349
|
+
const provided = instance.clientAuth;
|
|
350
|
+
clientAuth = {
|
|
351
|
+
plugin: plugin.id,
|
|
352
|
+
varyHeaders: provided.varyHeaders,
|
|
353
|
+
authenticate: (headers) => provided.authenticate(headers)
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
endpoints.push(...instance.endpoints);
|
|
357
|
+
api[plugin.id] = instance.api;
|
|
358
|
+
});
|
|
359
|
+
return {
|
|
360
|
+
core,
|
|
361
|
+
api,
|
|
362
|
+
endpoints,
|
|
363
|
+
...clientAuth === void 0 ? {} : { clientAuth },
|
|
364
|
+
...verify ? { measureReads: engine.measureReads } : {},
|
|
365
|
+
flush: engine.flush
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
function createDatabasePluginApis(database, plugins, options = {}) {
|
|
369
|
+
const adapter = engineAdapterOf(database);
|
|
370
|
+
if (adapter === void 0) throw new Error(OFF_ENGINE_DATABASE);
|
|
371
|
+
const { aggregateBatching } = database;
|
|
372
|
+
return assemblePlugins(plugins, adapter, {
|
|
373
|
+
...options,
|
|
374
|
+
...aggregateBatching === void 0 ? {} : { batching: aggregateBatching }
|
|
375
|
+
}).api;
|
|
376
|
+
}
|
|
377
|
+
var isOptional = (value, key, valid) => !Object.hasOwn(value, key) || valid(value[key]);
|
|
378
|
+
var isString = (value) => typeof value === "string";
|
|
379
|
+
/** An `UPDATE_FAILED` report's stored failure: a stage and a reason, and what else the client knew. */
|
|
380
|
+
var isDatabaseBundleEventFailure = (value) => isDatabaseJsonObject(value) && isString(value.stage) && isString(value.reason) && isOptional(value, "resource", isString) && isOptional(value, "http_status", (status) => Number.isSafeInteger(status)) && isOptional(value, "transport", isString) && isOptional(value, "origin_code", isString) && isOptional(value, "previous_process_exit", isString);
|
|
381
|
+
var isDatabaseBundleEventMetadata = (value) => isDatabaseJsonObject(value) && typeof value.cohort === "string" && (value.update_strategy === null || value.update_strategy === "fingerprint" || value.update_strategy === "appVersion") && (value.fingerprint_hash === null || typeof value.fingerprint_hash === "string") && (value.sdk_version === null || typeof value.sdk_version === "string") && isOptional(value, "failure", isDatabaseBundleEventFailure) && isOptional(value, "delivery", isString) && isOptional(value, "patch_fallback", (flag) => typeof flag === "boolean") && isOptional(value, "previous_process_exit", isString);
|
|
382
|
+
var isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
383
|
+
var isIdentity$1 = (value) => typeof value === "string" && value.length > 0 && value.length <= 255;
|
|
384
|
+
var isTextOrNull = (value) => value === null || typeof value === "string";
|
|
385
|
+
/** Each bundle event field and what it holds. */
|
|
386
|
+
var BUNDLE_EVENT_FIELDS = {
|
|
387
|
+
id: (value) => typeof value === "string",
|
|
388
|
+
type: (value) => value === "UPDATE_DOWNLOADED" || value === "UPDATE_APPLIED" || value === "RECOVERED" || value === "UPDATE_FAILED" || value === "UNCHANGED",
|
|
389
|
+
install_id: isIdentity$1,
|
|
390
|
+
user_id: (value) => value === null || isIdentity$1(value),
|
|
391
|
+
from_bundle_id: isTextOrNull,
|
|
392
|
+
from_release_id: isTextOrNull,
|
|
393
|
+
to_release_id: isTextOrNull,
|
|
394
|
+
to_bundle_id: (value) => typeof value === "string",
|
|
395
|
+
platform: (value) => value === "ios" || value === "android",
|
|
396
|
+
app_version: (value) => typeof value === "string",
|
|
397
|
+
channel: (value) => typeof value === "string",
|
|
398
|
+
metadata: isDatabaseBundleEventMetadata,
|
|
399
|
+
received_at_ms: (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0
|
|
400
|
+
};
|
|
401
|
+
var updateStrategyOf = (row) => isRecord$3(row.metadata) ? row.metadata.update_strategy : void 0;
|
|
402
|
+
/**
|
|
403
|
+
* A movement, or a failed update, names the bundle it left and how the
|
|
404
|
+
* device updates, and a failed update says what failed; an unchanged report
|
|
405
|
+
* names neither.
|
|
406
|
+
*/
|
|
407
|
+
var hasEventInvariants = (row) => (row.type === "UPDATE_DOWNLOADED" || row.type === "UPDATE_APPLIED" || row.type === "RECOVERED" || row.type === "UPDATE_FAILED") && typeof row.from_bundle_id === "string" && (updateStrategyOf(row) === "fingerprint" || updateStrategyOf(row) === "appVersion") && (row.type !== "UPDATE_FAILED" || isRecord$3(row.metadata) && isRecord$3(row.metadata.failure)) || row.type === "UNCHANGED" && row.from_bundle_id === null && updateStrategyOf(row) === null;
|
|
408
|
+
/**
|
|
409
|
+
* Throws unless `row` has exactly the bundle event fields, each of its type,
|
|
410
|
+
* and the invariants of its event type: `invalid-field` for an unknown field,
|
|
411
|
+
* `invalid-data` for anything else.
|
|
412
|
+
*/
|
|
413
|
+
var validateBundleEventFields = (row) => {
|
|
414
|
+
if (!isRecord$3(row)) throw new DatabasePluginInputError("invalid-data");
|
|
415
|
+
for (const field of Object.keys(row)) if (!Object.hasOwn(BUNDLE_EVENT_FIELDS, field)) throw new DatabasePluginInputError("invalid-field");
|
|
416
|
+
for (const [field, valid] of Object.entries(BUNDLE_EVENT_FIELDS)) if (!Object.hasOwn(row, field) || !valid(row[field])) throw new DatabasePluginInputError("invalid-data");
|
|
417
|
+
if (!hasEventInvariants(row)) throw new DatabasePluginInputError("invalid-data");
|
|
418
|
+
};
|
|
419
|
+
var encoder = new TextEncoder();
|
|
420
|
+
/** Exact UTF-8 byte ordering, without case folding or Unicode normalization. */
|
|
421
|
+
var compareInsightsText = (left, right) => {
|
|
422
|
+
const a = encoder.encode(left);
|
|
423
|
+
const b = encoder.encode(right);
|
|
424
|
+
for (let index = 0; index < Math.min(a.length, b.length); index += 1) if (a[index] !== b[index]) return a[index] - b[index];
|
|
425
|
+
return a.length - b.length;
|
|
426
|
+
};
|
|
427
|
+
var isWellFormedText = (value) => {
|
|
428
|
+
for (const character of value) {
|
|
429
|
+
const point = character.codePointAt(0);
|
|
430
|
+
if (point >= 55296 && point <= 57343) return false;
|
|
431
|
+
}
|
|
432
|
+
return true;
|
|
433
|
+
};
|
|
434
|
+
var isIdentity = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 && isWellFormedText(value);
|
|
435
|
+
var isText = (value) => typeof value === "string" && value.length > 0 && isWellFormedText(value);
|
|
436
|
+
var isTimestamp = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
437
|
+
var isLimit = (value) => isTimestamp(value) && value >= 1 && value <= 101;
|
|
438
|
+
var hasOnlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
|
|
439
|
+
var hasScope = (value) => (value.platform === "ios" || value.platform === "android") && isText(value.channel);
|
|
440
|
+
var isBundleFilter = (value, withKind = false) => {
|
|
441
|
+
if (!isRecord$3(value) || !hasScope(value)) return false;
|
|
442
|
+
const keys = [
|
|
443
|
+
"platform",
|
|
444
|
+
"channel",
|
|
445
|
+
"type",
|
|
446
|
+
...withKind ? ["kind"] : []
|
|
447
|
+
];
|
|
448
|
+
return value.type === "RECOVERED" ? isText(value.fromBundleId) && hasOnlyKeys(value, [...keys, "fromBundleId"]) : (value.type === "UPDATE_DOWNLOADED" || value.type === "UPDATE_APPLIED" || value.type === "UPDATE_FAILED") && isText(value.toBundleId) && hasOnlyKeys(value, [...keys, "toBundleId"]);
|
|
449
|
+
};
|
|
450
|
+
var isEventFilter = (value) => {
|
|
451
|
+
if (!isRecord$3(value)) return false;
|
|
452
|
+
if (value.kind === "all") return hasOnlyKeys(value, ["kind"]);
|
|
453
|
+
if (value.kind === "installationMovement") return isIdentity(value.installId) && hasOnlyKeys(value, ["kind", "installId"]);
|
|
454
|
+
return value.kind === "bundle" && isBundleFilter(value, true);
|
|
455
|
+
};
|
|
456
|
+
var validateRow = (model, row, result = false) => {
|
|
457
|
+
try {
|
|
458
|
+
validateBundleEventFields(row);
|
|
459
|
+
if (!isRecord$3(row) || typeof row.id !== "string" || !isUUIDv7(row.id) || [...Object.values(row), ...isRecord$3(row.metadata) ? Object.values(row.metadata) : []].some((value) => typeof value === "string" && !isWellFormedText(value))) throw new DatabasePluginInputError("invalid-data");
|
|
460
|
+
} catch (error) {
|
|
461
|
+
if (result) throw new DatabasePluginInputError("invalid-result");
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
/** Downloads, applies, recoveries, and failed updates belong in installation history. */
|
|
466
|
+
var isInsightsMovementEvent = (event) => event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_APPLIED" || event.type === "RECOVERED" || event.type === "UPDATE_FAILED";
|
|
467
|
+
var matchesInsightsEventFilter = (event, filter) => {
|
|
468
|
+
if (filter.kind === "all") return true;
|
|
469
|
+
if (filter.kind === "installationMovement") return event.install_id === filter.installId && isInsightsMovementEvent(event);
|
|
470
|
+
return event.type === filter.type && event.platform === filter.platform && event.channel === filter.channel && (filter.type === "RECOVERED" ? event.from_bundle_id === filter.fromBundleId : event.to_bundle_id === filter.toBundleId && event.metadata.failure?.stage !== "check");
|
|
471
|
+
};
|
|
472
|
+
var invalidQuery = () => {
|
|
473
|
+
throw new DatabasePluginInputError("invalid-query");
|
|
474
|
+
};
|
|
475
|
+
var invalidResult = () => {
|
|
476
|
+
throw new DatabasePluginInputError("invalid-result");
|
|
477
|
+
};
|
|
478
|
+
var validateCount = (count) => isTimestamp(count) ? count : invalidResult();
|
|
479
|
+
var isTimeRange = (value) => isRecord$3(value) && hasOnlyKeys(value, ["start", "end"]) && isTimestamp(value.start) && isTimestamp(value.end) && value.start < value.end;
|
|
480
|
+
var isReleaseReference = (value) => isRecord$3(value) && hasOnlyKeys(value, [
|
|
481
|
+
"releaseId",
|
|
482
|
+
"platform",
|
|
483
|
+
"channel"
|
|
484
|
+
]) && isIdentity(value.releaseId) && hasScope(value);
|
|
485
|
+
var validateAggregateResult = (value) => {
|
|
486
|
+
const result = isRecord$3(value) ? value : invalidResult();
|
|
487
|
+
if (!isTimestamp(result.measuredAtMs)) invalidResult();
|
|
488
|
+
const coverage = result.coverage;
|
|
489
|
+
if (!isRecord$3(coverage) || coverage.kind !== "complete" && coverage.kind !== "partial" || coverage.sinceMs !== null && !isTimestamp(coverage.sinceMs)) invalidResult();
|
|
490
|
+
};
|
|
491
|
+
/** Validate custom and bundled providers at the same public boundary. */
|
|
492
|
+
var createValidatedInsightsModel = (model) => ({
|
|
493
|
+
async recordEvent(input) {
|
|
494
|
+
if (!isRecord$3(input) || !hasOnlyKeys(input, ["event"])) throw new DatabasePluginInputError("invalid-data");
|
|
495
|
+
validateRow("bundle_events", input.event);
|
|
496
|
+
await model.recordEvent(input);
|
|
497
|
+
},
|
|
498
|
+
async listEvents(input) {
|
|
499
|
+
if (!isRecord$3(input) || !hasOnlyKeys(input, [
|
|
500
|
+
"filter",
|
|
501
|
+
"sinceMs",
|
|
502
|
+
"beforeReceivedAtMs",
|
|
503
|
+
"after",
|
|
504
|
+
"limit"
|
|
505
|
+
]) || !isEventFilter(input.filter) || !isLimit(input.limit) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs !== void 0 && !isTimestamp(input.sinceMs)) invalidQuery();
|
|
506
|
+
const sinceMs = input.sinceMs ?? 0;
|
|
507
|
+
if (sinceMs > input.beforeReceivedAtMs || input.after !== void 0 && (!isRecord$3(input.after) || !hasOnlyKeys(input.after, ["receivedAtMs", "id"]) || !isTimestamp(input.after.receivedAtMs) || typeof input.after.id !== "string" || !isUUIDv7(input.after.id) || input.after.receivedAtMs < sinceMs || input.after.receivedAtMs >= input.beforeReceivedAtMs)) invalidQuery();
|
|
508
|
+
const rows = await model.listEvents(input);
|
|
509
|
+
if (!Array.isArray(rows) || rows.length > input.limit) invalidResult();
|
|
510
|
+
let previous = input.after;
|
|
511
|
+
for (const row of rows) {
|
|
512
|
+
validateRow("bundle_events", row, true);
|
|
513
|
+
if (row.received_at_ms < sinceMs || row.received_at_ms >= input.beforeReceivedAtMs || !matchesInsightsEventFilter(row, input.filter) || previous !== void 0 && (row.received_at_ms > previous.receivedAtMs || row.received_at_ms === previous.receivedAtMs && row.id >= previous.id)) invalidResult();
|
|
514
|
+
previous = {
|
|
515
|
+
receivedAtMs: row.received_at_ms,
|
|
516
|
+
id: row.id
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
return rows;
|
|
520
|
+
},
|
|
521
|
+
async findLatestEvents(input) {
|
|
522
|
+
if (!isRecord$3(input)) invalidQuery();
|
|
523
|
+
if ("installId" in input) {
|
|
524
|
+
if (!hasOnlyKeys(input, ["installId"]) || !isIdentity(input.installId)) invalidQuery();
|
|
525
|
+
} else if (!hasOnlyKeys(input, [
|
|
526
|
+
"userId",
|
|
527
|
+
"afterInstallId",
|
|
528
|
+
"limit"
|
|
529
|
+
]) || !isIdentity(input.userId) || !isLimit(input.limit) || input.afterInstallId !== void 0 && !isIdentity(input.afterInstallId)) invalidQuery();
|
|
530
|
+
const rows = await model.findLatestEvents(input);
|
|
531
|
+
if (!Array.isArray(rows) || rows.length > ("installId" in input ? 1 : input.limit)) invalidResult();
|
|
532
|
+
let previous = "installId" in input ? void 0 : input.afterInstallId;
|
|
533
|
+
for (const row of rows) {
|
|
534
|
+
validateRow("bundle_events", row, true);
|
|
535
|
+
if ("installId" in input ? row.install_id !== input.installId : row.user_id !== input.userId || previous !== void 0 && compareInsightsText(row.install_id, previous) <= 0) invalidResult();
|
|
536
|
+
previous = row.install_id;
|
|
537
|
+
}
|
|
538
|
+
return rows;
|
|
539
|
+
},
|
|
540
|
+
async countLatestEvents(input) {
|
|
541
|
+
if (!isRecord$3(input) || !hasOnlyKeys(input, [
|
|
542
|
+
"platform",
|
|
543
|
+
"channel",
|
|
544
|
+
"sinceMs",
|
|
545
|
+
"bundle"
|
|
546
|
+
]) || !hasScope(input) || !isTimestamp(input.sinceMs) || input.bundle !== void 0 && (!Array.isArray(input.bundle) || input.bundle.length < 1 || input.bundle.length > 2 || input.bundle.some((bundle) => !isRecord$3(bundle) || !hasOnlyKeys(bundle, [
|
|
547
|
+
"field",
|
|
548
|
+
"value",
|
|
549
|
+
"types"
|
|
550
|
+
]) || bundle.field !== "from_bundle_id" && bundle.field !== "to_bundle_id" || !isText(bundle.value) || !Array.isArray(bundle.types) || bundle.types.length === 0 || bundle.types.some((type) => ![
|
|
551
|
+
"UNCHANGED",
|
|
552
|
+
"UPDATE_DOWNLOADED",
|
|
553
|
+
"UPDATE_APPLIED",
|
|
554
|
+
"RECOVERED"
|
|
555
|
+
].includes(type))))) invalidQuery();
|
|
556
|
+
return validateCount(await model.countLatestEvents(input));
|
|
557
|
+
},
|
|
558
|
+
async countEvents(input) {
|
|
559
|
+
if (!isRecord$3(input) || !hasOnlyKeys(input, [
|
|
560
|
+
"filter",
|
|
561
|
+
"sinceMs",
|
|
562
|
+
"beforeReceivedAtMs"
|
|
563
|
+
]) || !isBundleFilter(input.filter) || !isTimestamp(input.sinceMs) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs > input.beforeReceivedAtMs) invalidQuery();
|
|
564
|
+
return validateCount(await model.countEvents(input));
|
|
565
|
+
},
|
|
566
|
+
async getReleaseActivity(input) {
|
|
567
|
+
if (!isRecord$3(input) || !hasOnlyKeys(input, [
|
|
568
|
+
"releases",
|
|
569
|
+
"scope",
|
|
570
|
+
"timeRange"
|
|
571
|
+
])) invalidQuery();
|
|
572
|
+
const releases = input.releases;
|
|
573
|
+
const scope = input.scope;
|
|
574
|
+
const timeRange = input.timeRange;
|
|
575
|
+
const releaseQuery = Array.isArray(releases) && releases.length > 0 && releases.length <= 100 && releases.every(isReleaseReference) && scope === void 0 && (timeRange === void 0 || isTimeRange(timeRange));
|
|
576
|
+
const scopeQuery = releases === void 0 && isRecord$3(scope) && hasOnlyKeys(scope, ["platform", "channel"]) && hasScope(scope) && isTimeRange(timeRange);
|
|
577
|
+
if (!releaseQuery && !scopeQuery) invalidQuery();
|
|
578
|
+
const result = await model.getReleaseActivity(input);
|
|
579
|
+
validateAggregateResult(result);
|
|
580
|
+
if (!Array.isArray(result.data)) invalidResult();
|
|
581
|
+
return result;
|
|
582
|
+
},
|
|
583
|
+
async getAppUsage(input) {
|
|
584
|
+
if (!isRecord$3(input) || !hasOnlyKeys(input, [
|
|
585
|
+
"channel",
|
|
586
|
+
"platform",
|
|
587
|
+
"appVersion",
|
|
588
|
+
"timeRange",
|
|
589
|
+
"intervalMs"
|
|
590
|
+
]) || !isText(input.channel) || ![
|
|
591
|
+
"all",
|
|
592
|
+
"ios",
|
|
593
|
+
"android"
|
|
594
|
+
].includes(input.platform) || input.appVersion !== void 0 && !isText(input.appVersion) || !isTimeRange(input.timeRange) || !isTimestamp(input.intervalMs) || input.intervalMs < 36e5) invalidQuery();
|
|
595
|
+
const result = await model.getAppUsage(input);
|
|
596
|
+
validateAggregateResult(result);
|
|
597
|
+
if (!isTimestamp(result.activeInstallations) || !Array.isArray(result.points) || !Array.isArray(result.appVersions) || !Array.isArray(result.versions) || !Array.isArray(result.platforms) || !Array.isArray(result.bundleDistribution)) invalidResult();
|
|
598
|
+
return result;
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
/**
|
|
602
|
+
* The plugin's API as `InsightsModel`, validated at its boundary: what the
|
|
603
|
+
* Insights routes, the console, and the e2e harness read through.
|
|
604
|
+
*/
|
|
605
|
+
var createInsightsModel = (api) => createValidatedInsightsModel({
|
|
606
|
+
recordEvent: ({ event }) => api.recordEvent(event),
|
|
607
|
+
listEvents: api.listEvents,
|
|
608
|
+
findLatestEvents: api.findLatestEvents,
|
|
609
|
+
countLatestEvents: api.countLatestEvents,
|
|
610
|
+
countEvents: api.countEvents,
|
|
611
|
+
getReleaseActivity: api.getReleaseActivity,
|
|
612
|
+
getAppUsage: api.getAppUsage
|
|
613
|
+
});
|
|
614
|
+
var InsightsBadRequestError = class extends Error {
|
|
615
|
+
name = "InsightsBadRequestError";
|
|
616
|
+
};
|
|
617
|
+
var MAX_EVENT_STRING_LENGTH = 1024;
|
|
618
|
+
var MAX_IDENTITY_LENGTH$1 = 255;
|
|
619
|
+
function isRecord$2(value) {
|
|
620
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
621
|
+
}
|
|
622
|
+
function requireStringField(payload, key) {
|
|
623
|
+
const value = payload[key];
|
|
624
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_EVENT_STRING_LENGTH || new TextDecoder("utf-8", { ignoreBOM: true }).decode(new TextEncoder().encode(value)) !== value) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
|
|
625
|
+
return value;
|
|
626
|
+
}
|
|
627
|
+
function requireNullableStringField(payload, key) {
|
|
628
|
+
if (payload[key] === null) return null;
|
|
629
|
+
return requireStringField(payload, key);
|
|
630
|
+
}
|
|
631
|
+
function requireIdentityField(payload, key) {
|
|
632
|
+
const value = requireStringField(payload, key);
|
|
633
|
+
if (value.length > MAX_IDENTITY_LENGTH$1) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
|
|
634
|
+
return value;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* The report's idempotency key, when the client sends one: it creates the
|
|
638
|
+
* UUIDv7 once and repeats it on every retry of that report.
|
|
639
|
+
*/
|
|
640
|
+
function readEventId(payload) {
|
|
641
|
+
const value = payload.eventId;
|
|
642
|
+
if (value === void 0) return void 0;
|
|
643
|
+
if (!isUUIDv7(value)) throw new InsightsBadRequestError("Invalid event field: eventId");
|
|
644
|
+
return value;
|
|
645
|
+
}
|
|
646
|
+
/** One value of an open set: any other value reads as `unknown`. */
|
|
647
|
+
var oneOf = (values, value) => values.includes(value) ? value : "unknown";
|
|
648
|
+
var STAGES = [
|
|
649
|
+
"check",
|
|
650
|
+
"download",
|
|
651
|
+
"install"
|
|
652
|
+
];
|
|
653
|
+
var REASONS = [
|
|
654
|
+
"network",
|
|
655
|
+
"http",
|
|
656
|
+
"invalid_response",
|
|
657
|
+
"hash_mismatch",
|
|
658
|
+
"signature",
|
|
659
|
+
"patch",
|
|
660
|
+
"extract",
|
|
661
|
+
"storage"
|
|
662
|
+
];
|
|
663
|
+
var RESOURCES = [
|
|
664
|
+
"catalog",
|
|
665
|
+
"artifact",
|
|
666
|
+
"manifest",
|
|
667
|
+
"file",
|
|
668
|
+
"patch",
|
|
669
|
+
"archive"
|
|
670
|
+
];
|
|
671
|
+
var TRANSPORTS = [
|
|
672
|
+
"timeout",
|
|
673
|
+
"dns",
|
|
674
|
+
"tls",
|
|
675
|
+
"connection",
|
|
676
|
+
"offline",
|
|
677
|
+
"cancelled"
|
|
678
|
+
];
|
|
679
|
+
var DELIVERIES = [
|
|
680
|
+
"patch",
|
|
681
|
+
"manifest",
|
|
682
|
+
"archive"
|
|
683
|
+
];
|
|
684
|
+
/** A storage error code or an Android exit reason: letters, digits, and `._-`. */
|
|
685
|
+
var CODE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
686
|
+
var readCode = (value) => typeof value === "string" && CODE.test(value) ? value : void 0;
|
|
687
|
+
var readMetadata = (payload) => isRecord$2(payload.metadata) ? payload.metadata : {};
|
|
688
|
+
/**
|
|
689
|
+
* What an UPDATE_FAILED report says failed. It never refuses the report: an
|
|
690
|
+
* unknown value of a set reads as `unknown`, a missing or malformed failure
|
|
691
|
+
* as an unknown stage and reason, and a malformed optional field is left out,
|
|
692
|
+
* as is a null one.
|
|
693
|
+
*/
|
|
694
|
+
function readFailure(value) {
|
|
695
|
+
const failure = isRecord$2(value) ? value : {};
|
|
696
|
+
const httpStatus = failure.httpStatus;
|
|
697
|
+
const originCode = readCode(failure.originCode);
|
|
698
|
+
const previousProcessExit = readCode(failure.previousProcessExit);
|
|
699
|
+
return {
|
|
700
|
+
stage: oneOf(STAGES, failure.stage),
|
|
701
|
+
reason: oneOf(REASONS, failure.reason),
|
|
702
|
+
...failure.resource == null ? {} : { resource: oneOf(RESOURCES, failure.resource) },
|
|
703
|
+
...Number.isSafeInteger(httpStatus) && httpStatus >= 100 && httpStatus <= 599 ? { httpStatus } : {},
|
|
704
|
+
...failure.transport == null ? {} : { transport: oneOf(TRANSPORTS, failure.transport) },
|
|
705
|
+
...originCode === void 0 ? {} : { originCode },
|
|
706
|
+
...previousProcessExit === void 0 ? {} : { previousProcessExit }
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function requireEvent(payload) {
|
|
710
|
+
if (!isRecord$2(payload)) throw new InsightsBadRequestError("Invalid event payload");
|
|
711
|
+
const platform = requireStringField(payload, "platform");
|
|
712
|
+
if (platform !== "ios" && platform !== "android") throw new InsightsBadRequestError("Invalid event field: platform");
|
|
713
|
+
const eventId = readEventId(payload);
|
|
714
|
+
const base = {
|
|
715
|
+
...eventId === void 0 ? {} : { eventId },
|
|
716
|
+
installId: requireIdentityField(payload, "installId"),
|
|
717
|
+
toBundleId: requireStringField(payload, "toBundleId"),
|
|
718
|
+
...payload.userId === void 0 ? {} : { userId: requireIdentityField(payload, "userId") },
|
|
719
|
+
platform,
|
|
720
|
+
appVersion: requireStringField(payload, "appVersion"),
|
|
721
|
+
channel: requireStringField(payload, "channel"),
|
|
722
|
+
cohort: requireStringField(payload, "cohort"),
|
|
723
|
+
fingerprintHash: requireNullableStringField(payload, "fingerprintHash"),
|
|
724
|
+
sdkVersion: payload.sdkVersion === void 0 ? null : requireNullableStringField(payload, "sdkVersion"),
|
|
725
|
+
fromReleaseId: requireNullableStringField(payload, "fromReleaseId"),
|
|
726
|
+
toReleaseId: requireNullableStringField(payload, "toReleaseId")
|
|
727
|
+
};
|
|
728
|
+
const type = requireStringField(payload, "type");
|
|
729
|
+
const movement = () => {
|
|
730
|
+
const updateStrategy = requireStringField(payload, "updateStrategy");
|
|
731
|
+
if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") throw new InsightsBadRequestError("Invalid event field: updateStrategy");
|
|
732
|
+
return {
|
|
733
|
+
...base,
|
|
734
|
+
fromBundleId: requireStringField(payload, "fromBundleId"),
|
|
735
|
+
updateStrategy
|
|
736
|
+
};
|
|
737
|
+
};
|
|
738
|
+
const metadata = readMetadata(payload);
|
|
739
|
+
switch (type) {
|
|
740
|
+
case "UPDATE_DOWNLOADED": {
|
|
741
|
+
const delivery = metadata.delivery;
|
|
742
|
+
const read = {
|
|
743
|
+
...delivery == null ? {} : { delivery: oneOf(DELIVERIES, delivery) },
|
|
744
|
+
...metadata.patchFallback === true ? { patchFallback: true } : {}
|
|
745
|
+
};
|
|
746
|
+
return {
|
|
747
|
+
...movement(),
|
|
748
|
+
type,
|
|
749
|
+
...Object.keys(read).length === 0 ? {} : { metadata: read }
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
case "UPDATE_APPLIED": return {
|
|
753
|
+
...movement(),
|
|
754
|
+
type
|
|
755
|
+
};
|
|
756
|
+
case "RECOVERED": {
|
|
757
|
+
const previousProcessExit = readCode(metadata.previousProcessExit);
|
|
758
|
+
return {
|
|
759
|
+
...movement(),
|
|
760
|
+
type,
|
|
761
|
+
...previousProcessExit === void 0 ? {} : { metadata: { previousProcessExit } }
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
case "UPDATE_FAILED": return {
|
|
765
|
+
...movement(),
|
|
766
|
+
type,
|
|
767
|
+
metadata: { failure: readFailure(metadata.failure) }
|
|
768
|
+
};
|
|
769
|
+
case "UNCHANGED":
|
|
770
|
+
if (payload.fromBundleId !== null || payload.updateStrategy !== null) throw new InsightsBadRequestError("Invalid unchanged event shape");
|
|
771
|
+
return {
|
|
772
|
+
...base,
|
|
773
|
+
type,
|
|
774
|
+
fromBundleId: null,
|
|
775
|
+
updateStrategy: null
|
|
776
|
+
};
|
|
777
|
+
default: throw new InsightsBadRequestError("Invalid event field: type");
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function createBundleEventRow(input) {
|
|
781
|
+
input = requireEvent(input);
|
|
782
|
+
const base = {
|
|
783
|
+
app_version: input.appVersion,
|
|
784
|
+
channel: input.channel,
|
|
785
|
+
from_release_id: input.fromReleaseId,
|
|
786
|
+
id: input.eventId ?? createUUIDv7(),
|
|
787
|
+
install_id: input.installId,
|
|
788
|
+
platform: input.platform,
|
|
789
|
+
received_at_ms: Date.now(),
|
|
790
|
+
to_bundle_id: input.toBundleId,
|
|
791
|
+
to_release_id: input.toReleaseId,
|
|
792
|
+
user_id: input.userId ?? null
|
|
793
|
+
};
|
|
794
|
+
const metadata = {
|
|
795
|
+
cohort: input.cohort,
|
|
796
|
+
fingerprint_hash: input.fingerprintHash,
|
|
797
|
+
sdk_version: input.sdkVersion ?? null,
|
|
798
|
+
update_strategy: input.updateStrategy
|
|
799
|
+
};
|
|
800
|
+
switch (input.type) {
|
|
801
|
+
case "UPDATE_DOWNLOADED": {
|
|
802
|
+
const { delivery, patchFallback } = input.metadata ?? {};
|
|
803
|
+
return {
|
|
804
|
+
...base,
|
|
805
|
+
from_bundle_id: input.fromBundleId,
|
|
806
|
+
type: input.type,
|
|
807
|
+
metadata: {
|
|
808
|
+
...metadata,
|
|
809
|
+
...delivery === void 0 ? {} : { delivery },
|
|
810
|
+
...patchFallback ? { patch_fallback: true } : {}
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
case "UPDATE_APPLIED": return {
|
|
815
|
+
...base,
|
|
816
|
+
from_bundle_id: input.fromBundleId,
|
|
817
|
+
type: input.type,
|
|
818
|
+
metadata
|
|
819
|
+
};
|
|
820
|
+
case "RECOVERED": {
|
|
821
|
+
const exit = input.metadata?.previousProcessExit;
|
|
822
|
+
return {
|
|
823
|
+
...base,
|
|
824
|
+
from_bundle_id: input.fromBundleId,
|
|
825
|
+
type: input.type,
|
|
826
|
+
metadata: {
|
|
827
|
+
...metadata,
|
|
828
|
+
...exit === void 0 ? {} : { previous_process_exit: exit }
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
case "UPDATE_FAILED": {
|
|
833
|
+
const { failure } = input.metadata;
|
|
834
|
+
return {
|
|
835
|
+
...base,
|
|
836
|
+
from_bundle_id: input.fromBundleId,
|
|
837
|
+
type: input.type,
|
|
838
|
+
metadata: {
|
|
839
|
+
...metadata,
|
|
840
|
+
failure: {
|
|
841
|
+
stage: failure.stage,
|
|
842
|
+
reason: failure.reason,
|
|
843
|
+
...failure.resource === void 0 ? {} : { resource: failure.resource },
|
|
844
|
+
...failure.httpStatus === void 0 ? {} : { http_status: failure.httpStatus },
|
|
845
|
+
...failure.transport === void 0 ? {} : { transport: failure.transport },
|
|
846
|
+
...failure.originCode === void 0 ? {} : { origin_code: failure.originCode },
|
|
847
|
+
...failure.previousProcessExit === void 0 ? {} : { previous_process_exit: failure.previousProcessExit }
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
case "UNCHANGED": return {
|
|
853
|
+
...base,
|
|
854
|
+
from_bundle_id: null,
|
|
855
|
+
type: input.type,
|
|
856
|
+
metadata
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
var DEFAULT_PAGE_LIMIT = 50;
|
|
861
|
+
var MAX_PAGE_LIMIT = 100;
|
|
862
|
+
var MAX_EVENT_ID_LENGTH = 1024;
|
|
863
|
+
var MAX_IDENTITY_LENGTH = 255;
|
|
864
|
+
var MAX_CURSOR_LENGTH = 8192;
|
|
865
|
+
var HOUR_MS = 36e5;
|
|
866
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
867
|
+
var WINDOW_MS = {
|
|
868
|
+
"24h": 864e5,
|
|
869
|
+
"7d": 6048e5,
|
|
870
|
+
"30d": 2592e6
|
|
871
|
+
};
|
|
872
|
+
/**
|
|
873
|
+
* The longest receipt range, `[sinceMs, beforeReceivedAtMs)`, of a global or
|
|
874
|
+
* bundle event list, and the range one covers without `sinceMs`: raw events
|
|
875
|
+
* are listed inside a time range, as analytics products list them. It is a
|
|
876
|
+
* duration, so a range that ends mid-day touches 91 UTC days. Installation
|
|
877
|
+
* history is one index range and reads back to `sinceMs`.
|
|
878
|
+
*/
|
|
879
|
+
var EVENT_LIST_RANGE_MS = 2160 * HOUR_MS;
|
|
880
|
+
var isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
881
|
+
var requireString = (value, label, maximumLength) => {
|
|
882
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maximumLength || new TextDecoder("utf-8", { ignoreBOM: true }).decode(new TextEncoder().encode(value)) !== value) throw new InsightsBadRequestError(`Invalid ${label}.`);
|
|
883
|
+
return value;
|
|
884
|
+
};
|
|
885
|
+
var requireTimestamp = (value, label) => {
|
|
886
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0) throw new InsightsBadRequestError(`Invalid ${label}.`);
|
|
887
|
+
return Number(value);
|
|
888
|
+
};
|
|
889
|
+
var readLimit = (value) => {
|
|
890
|
+
const limit = value ?? DEFAULT_PAGE_LIMIT;
|
|
891
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) throw new InsightsBadRequestError("Invalid page limit.");
|
|
892
|
+
return limit;
|
|
893
|
+
};
|
|
894
|
+
var encodeBase64Url = (value) => {
|
|
895
|
+
const bytes = new TextEncoder().encode(value);
|
|
896
|
+
let binary = "";
|
|
897
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
898
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
899
|
+
};
|
|
900
|
+
var decodeBase64Url = (value) => {
|
|
901
|
+
if (value.length === 0 || value.length > MAX_CURSOR_LENGTH) throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
902
|
+
try {
|
|
903
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
904
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
905
|
+
const binary = atob(padded);
|
|
906
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
907
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
908
|
+
} catch {
|
|
909
|
+
throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
var encodeCursor = (value) => encodeBase64Url(JSON.stringify(value));
|
|
913
|
+
var decodeCursor = (value) => {
|
|
914
|
+
try {
|
|
915
|
+
return JSON.parse(decodeBase64Url(value));
|
|
916
|
+
} catch (error) {
|
|
917
|
+
if (error instanceof InsightsBadRequestError) throw error;
|
|
918
|
+
throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
var readScope = (input) => {
|
|
922
|
+
if (input.platform !== "ios" && input.platform !== "android") throw new InsightsBadRequestError("Invalid Insights platform.");
|
|
923
|
+
return {
|
|
924
|
+
platform: input.platform,
|
|
925
|
+
channel: requireString(input.channel, "channel", MAX_EVENT_ID_LENGTH)
|
|
926
|
+
};
|
|
927
|
+
};
|
|
928
|
+
var bundleFilter = (input) => {
|
|
929
|
+
const scope = readScope(input);
|
|
930
|
+
const bundleId = requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
|
|
931
|
+
switch (input.outcome) {
|
|
932
|
+
case "downloaded": return {
|
|
933
|
+
...scope,
|
|
934
|
+
type: "UPDATE_DOWNLOADED",
|
|
935
|
+
toBundleId: bundleId
|
|
936
|
+
};
|
|
937
|
+
case "applied": return {
|
|
938
|
+
...scope,
|
|
939
|
+
type: "UPDATE_APPLIED",
|
|
940
|
+
toBundleId: bundleId
|
|
941
|
+
};
|
|
942
|
+
case "recovered": return {
|
|
943
|
+
...scope,
|
|
944
|
+
type: "RECOVERED",
|
|
945
|
+
fromBundleId: bundleId
|
|
946
|
+
};
|
|
947
|
+
case "failed": return {
|
|
948
|
+
...scope,
|
|
949
|
+
type: "UPDATE_FAILED",
|
|
950
|
+
toBundleId: bundleId
|
|
951
|
+
};
|
|
952
|
+
default: throw new InsightsBadRequestError("Invalid Insights outcome.");
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
var sameFilter = (left, right) => {
|
|
956
|
+
if (!isRecord$1(left) || left.kind !== right.kind) return false;
|
|
957
|
+
switch (right.kind) {
|
|
958
|
+
case "all": return true;
|
|
959
|
+
case "installationMovement": return left.installId === right.installId;
|
|
960
|
+
case "bundle": return left.platform === right.platform && left.channel === right.channel && left.type === right.type && (right.type === "RECOVERED" ? left.fromBundleId === right.fromBundleId : left.toBundleId === right.toBundleId);
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
var readEventCursor = (value, filter) => {
|
|
964
|
+
const cursor = decodeCursor(value);
|
|
965
|
+
if (!isRecord$1(cursor) || cursor.version !== 2 || cursor.kind !== "events" || !isRecord$1(cursor.filter) || !isRecord$1(cursor.after)) throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
966
|
+
if (!sameFilter(cursor.filter, filter)) throw new InsightsBadRequestError("Insights cursor does not match the requested events.");
|
|
967
|
+
if (typeof cursor.after.id !== "string" || !isUUIDv7(cursor.after.id)) throw new InsightsBadRequestError("Invalid Insights event cursor ID.");
|
|
968
|
+
return {
|
|
969
|
+
after: {
|
|
970
|
+
id: cursor.after.id,
|
|
971
|
+
receivedAtMs: requireTimestamp(cursor.after.receivedAtMs, "event cursor")
|
|
972
|
+
},
|
|
973
|
+
beforeReceivedAtMs: requireTimestamp(cursor.beforeReceivedAtMs, "event cutoff"),
|
|
974
|
+
kind: "events",
|
|
975
|
+
sinceMs: requireTimestamp(cursor.sinceMs, "event start"),
|
|
976
|
+
filter,
|
|
977
|
+
version: 2
|
|
978
|
+
};
|
|
979
|
+
};
|
|
980
|
+
var readUserInstallationCursor = (value, userId) => {
|
|
981
|
+
const cursor = decodeCursor(value);
|
|
982
|
+
if (!isRecord$1(cursor) || cursor.version !== 1 || cursor.kind !== "user-installations" || cursor.userId !== userId) throw new InsightsBadRequestError("Insights cursor does not match the requested user ID.");
|
|
983
|
+
return {
|
|
984
|
+
afterInstallId: requireString(cursor.afterInstallId, "installation cursor", MAX_IDENTITY_LENGTH),
|
|
985
|
+
kind: "user-installations",
|
|
986
|
+
userId,
|
|
987
|
+
version: 1
|
|
988
|
+
};
|
|
989
|
+
};
|
|
990
|
+
var compareEventNewest = (left, right) => right.received_at_ms - left.received_at_ms || compareInsightsText(right.id, left.id);
|
|
991
|
+
var isAfterEventCursor = (row, after) => row.received_at_ms < after.receivedAtMs || row.received_at_ms === after.receivedAtMs && compareInsightsText(row.id, after.id) < 0;
|
|
992
|
+
var assertEventRows = (rows, input) => {
|
|
993
|
+
if (rows.length > input.limit) throw new Error("Insights database returned too many event rows.");
|
|
994
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
995
|
+
const row = rows[index];
|
|
996
|
+
const previous = rows[index - 1];
|
|
997
|
+
if (!row || typeof row.id !== "string" || !isUUIDv7(row.id) || !Number.isSafeInteger(row.received_at_ms) || row.received_at_ms >= input.beforeReceivedAtMs || row.received_at_ms < input.sinceMs || previous !== void 0 && compareEventNewest(previous, row) >= 0 || input.after !== void 0 && !isAfterEventCursor(row, input.after) || input.filter.kind === "installationMovement" && (row.install_id !== input.filter.installId || !isInsightsMovementEvent(row)) || input.filter.kind === "bundle" && (row.platform !== input.filter.platform || row.channel !== input.filter.channel || row.type !== input.filter.type || (input.filter.type === "RECOVERED" ? row.from_bundle_id !== input.filter.fromBundleId : row.to_bundle_id !== input.filter.toBundleId))) throw new Error("Insights database returned invalid event rows.");
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
/** An `UPDATE_FAILED` row's failure, as the event lists show it. */
|
|
1001
|
+
var toFailure = (failure) => ({
|
|
1002
|
+
stage: failure.stage,
|
|
1003
|
+
reason: failure.reason,
|
|
1004
|
+
...failure.resource === void 0 ? {} : { resource: failure.resource },
|
|
1005
|
+
...failure.http_status === void 0 ? {} : { httpStatus: failure.http_status },
|
|
1006
|
+
...failure.transport === void 0 ? {} : { transport: failure.transport },
|
|
1007
|
+
...failure.origin_code === void 0 ? {} : { originCode: failure.origin_code },
|
|
1008
|
+
...failure.previous_process_exit === void 0 ? {} : { previousProcessExit: failure.previous_process_exit }
|
|
1009
|
+
});
|
|
1010
|
+
/** What a row's metadata adds for its type: a failure, a delivery, an exit reason. */
|
|
1011
|
+
var eventDetails = ({ type, metadata }) => ({
|
|
1012
|
+
...type === "UPDATE_FAILED" && metadata.failure !== void 0 ? { failure: toFailure(metadata.failure) } : {},
|
|
1013
|
+
...type === "UPDATE_DOWNLOADED" && metadata.delivery !== void 0 ? { delivery: metadata.delivery } : {},
|
|
1014
|
+
...type === "UPDATE_DOWNLOADED" && metadata.patch_fallback === true ? { patchFallback: true } : {},
|
|
1015
|
+
...type === "RECOVERED" && metadata.previous_process_exit !== void 0 ? { previousProcessExit: metadata.previous_process_exit } : {}
|
|
1016
|
+
});
|
|
1017
|
+
var toEventHistoryRow = (row) => ({
|
|
1018
|
+
appVersion: row.app_version,
|
|
1019
|
+
channel: row.channel,
|
|
1020
|
+
cohort: row.metadata.cohort,
|
|
1021
|
+
fromBundleId: row.from_bundle_id,
|
|
1022
|
+
id: row.id,
|
|
1023
|
+
installId: row.install_id,
|
|
1024
|
+
platform: row.platform,
|
|
1025
|
+
receivedAtMs: row.received_at_ms,
|
|
1026
|
+
toBundleId: row.to_bundle_id,
|
|
1027
|
+
type: row.type,
|
|
1028
|
+
userId: row.user_id,
|
|
1029
|
+
...eventDetails(row)
|
|
1030
|
+
});
|
|
1031
|
+
var toInstallationRow = (row) => ({
|
|
1032
|
+
appVersion: row.app_version,
|
|
1033
|
+
channel: row.channel,
|
|
1034
|
+
cohort: row.metadata.cohort,
|
|
1035
|
+
installId: row.install_id,
|
|
1036
|
+
lastKnownBundleId: row.type === "UPDATE_DOWNLOADED" ? row.from_bundle_id : row.to_bundle_id,
|
|
1037
|
+
pendingBundleId: row.type === "UPDATE_DOWNLOADED" ? row.to_bundle_id : null,
|
|
1038
|
+
pendingReleaseId: row.type === "UPDATE_DOWNLOADED" ? row.to_release_id : null,
|
|
1039
|
+
latestStatus: row.type,
|
|
1040
|
+
platform: row.platform,
|
|
1041
|
+
receivedAtMs: row.received_at_ms,
|
|
1042
|
+
userId: row.user_id
|
|
1043
|
+
});
|
|
1044
|
+
/**
|
|
1045
|
+
* Where a list without `sinceMs` starts: 90 days before its cutoff for the
|
|
1046
|
+
* global and bundle lists, at the first event for installation history.
|
|
1047
|
+
*/
|
|
1048
|
+
var defaultEventStart = (filter, beforeReceivedAtMs) => filter.kind === "installationMovement" ? 0 : Math.max(0, beforeReceivedAtMs - EVENT_LIST_RANGE_MS);
|
|
1049
|
+
/** Whether a cursor's row lies inside the list's range. */
|
|
1050
|
+
var isInsideBounds = ({ after }, bounds) => after.receivedAtMs >= bounds.sinceMs && after.receivedAtMs < bounds.beforeReceivedAtMs;
|
|
1051
|
+
/** A page's range: its cursor's, which the request may repeat but not change. */
|
|
1052
|
+
var readEventBounds = (input, cursor, filter) => {
|
|
1053
|
+
const beforeReceivedAtMs = cursor?.beforeReceivedAtMs ?? (input.beforeReceivedAtMs === void 0 ? Date.now() : requireTimestamp(input.beforeReceivedAtMs, "event cutoff"));
|
|
1054
|
+
if (cursor !== void 0 && input.beforeReceivedAtMs !== void 0 && input.beforeReceivedAtMs !== beforeReceivedAtMs) throw new InsightsBadRequestError("Insights cursor does not match the requested event cutoff.");
|
|
1055
|
+
const sinceMs = cursor?.sinceMs ?? (input.sinceMs === void 0 ? defaultEventStart(filter, beforeReceivedAtMs) : requireTimestamp(input.sinceMs, "event start"));
|
|
1056
|
+
const bounds = {
|
|
1057
|
+
beforeReceivedAtMs,
|
|
1058
|
+
sinceMs
|
|
1059
|
+
};
|
|
1060
|
+
if (sinceMs > beforeReceivedAtMs || input.sinceMs !== void 0 && input.sinceMs !== sinceMs || cursor !== void 0 && !isInsideBounds(cursor, bounds)) throw new InsightsBadRequestError("Insights cursor or range does not match the requested event start.");
|
|
1061
|
+
if (filter.kind !== "installationMovement" && beforeReceivedAtMs - sinceMs > 7776e6) throw new InsightsBadRequestError("Insights event lists cover at most 90 days: send a sinceMs no more than 90 days before beforeReceivedAtMs.");
|
|
1062
|
+
return bounds;
|
|
1063
|
+
};
|
|
1064
|
+
var pageEventRows = async (model, input, filter, map) => {
|
|
1065
|
+
const limit = readLimit(input.limit);
|
|
1066
|
+
const cursor = input.cursor === void 0 ? void 0 : readEventCursor(input.cursor, filter);
|
|
1067
|
+
const bounds = readEventBounds(input, cursor, filter);
|
|
1068
|
+
const query = {
|
|
1069
|
+
filter,
|
|
1070
|
+
...bounds,
|
|
1071
|
+
...cursor === void 0 ? {} : { after: cursor.after },
|
|
1072
|
+
limit
|
|
1073
|
+
};
|
|
1074
|
+
const rows = await model.listEvents(query);
|
|
1075
|
+
assertEventRows(rows, query);
|
|
1076
|
+
const last = rows.at(-1);
|
|
1077
|
+
return {
|
|
1078
|
+
beforeReceivedAtMs: bounds.beforeReceivedAtMs,
|
|
1079
|
+
data: rows.map(map),
|
|
1080
|
+
nextCursor: rows.length === limit && last ? encodeCursor({
|
|
1081
|
+
after: {
|
|
1082
|
+
id: last.id,
|
|
1083
|
+
receivedAtMs: last.received_at_ms
|
|
1084
|
+
},
|
|
1085
|
+
...bounds,
|
|
1086
|
+
kind: "events",
|
|
1087
|
+
filter,
|
|
1088
|
+
version: 2
|
|
1089
|
+
}) : null
|
|
1090
|
+
};
|
|
1091
|
+
};
|
|
1092
|
+
var assertInstallationRows = (rows, input) => {
|
|
1093
|
+
if (rows.length > input.limit) throw new Error("Insights database returned too many installation rows.");
|
|
1094
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
1095
|
+
const row = rows[index];
|
|
1096
|
+
const previous = rows[index - 1];
|
|
1097
|
+
if (!row || row.user_id !== input.userId || input.afterInstallId !== void 0 && compareInsightsText(row.install_id, input.afterInstallId) <= 0 || previous !== void 0 && compareInsightsText(previous.install_id, row.install_id) >= 0) throw new Error("Insights database returned invalid installation rows.");
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
var createInsightsProvider = (model) => Object.freeze({
|
|
1101
|
+
async appendBundleEvent(input) {
|
|
1102
|
+
const event = createBundleEventRow(input);
|
|
1103
|
+
await model.recordEvent({ event });
|
|
1104
|
+
},
|
|
1105
|
+
listEvents(input) {
|
|
1106
|
+
return pageEventRows(model, input, input.bundle === void 0 ? { kind: "all" } : {
|
|
1107
|
+
kind: "bundle",
|
|
1108
|
+
...bundleFilter(input.bundle)
|
|
1109
|
+
}, toEventHistoryRow);
|
|
1110
|
+
},
|
|
1111
|
+
async listInstallationEvents(input) {
|
|
1112
|
+
if ("bundle" in input && input.bundle !== void 0) throw new InsightsBadRequestError("Installation movement queries cannot include a bundle filter.");
|
|
1113
|
+
return pageEventRows(model, input, {
|
|
1114
|
+
kind: "installationMovement",
|
|
1115
|
+
installId: requireString(input.installId, "install ID", MAX_IDENTITY_LENGTH)
|
|
1116
|
+
}, (row) => toEventHistoryRow(row));
|
|
1117
|
+
},
|
|
1118
|
+
async getInstallation({ installId }) {
|
|
1119
|
+
const normalizedInstallId = requireString(installId, "install ID", MAX_IDENTITY_LENGTH);
|
|
1120
|
+
const rows = await model.findLatestEvents({ installId: normalizedInstallId });
|
|
1121
|
+
const row = rows[0] ?? null;
|
|
1122
|
+
if (rows.length > 1 || row !== null && row.install_id !== normalizedInstallId) throw new Error("Insights database returned an invalid installation.");
|
|
1123
|
+
return row === null ? null : toInstallationRow(row);
|
|
1124
|
+
},
|
|
1125
|
+
async pageInstallationsByCurrentUserId(input) {
|
|
1126
|
+
const userId = requireString(input.userId, "user ID", MAX_IDENTITY_LENGTH);
|
|
1127
|
+
const limit = readLimit(input.limit);
|
|
1128
|
+
const cursor = input.cursor === void 0 ? void 0 : readUserInstallationCursor(input.cursor, userId);
|
|
1129
|
+
const databaseInput = {
|
|
1130
|
+
userId,
|
|
1131
|
+
...cursor === void 0 ? {} : { afterInstallId: cursor.afterInstallId },
|
|
1132
|
+
limit
|
|
1133
|
+
};
|
|
1134
|
+
const rows = await model.findLatestEvents(databaseInput);
|
|
1135
|
+
assertInstallationRows(rows, databaseInput);
|
|
1136
|
+
const last = rows.at(-1);
|
|
1137
|
+
return {
|
|
1138
|
+
data: rows.map(toInstallationRow),
|
|
1139
|
+
nextCursor: rows.length === limit && last ? encodeCursor({
|
|
1140
|
+
afterInstallId: last.install_id,
|
|
1141
|
+
kind: "user-installations",
|
|
1142
|
+
userId,
|
|
1143
|
+
version: 1
|
|
1144
|
+
}) : null
|
|
1145
|
+
};
|
|
1146
|
+
},
|
|
1147
|
+
async getReportingOverview(input) {
|
|
1148
|
+
const scope = readScope(input);
|
|
1149
|
+
const { window } = input;
|
|
1150
|
+
if (!Object.hasOwn(WINDOW_MS, window)) throw new InsightsBadRequestError("Invalid reporting installation window.");
|
|
1151
|
+
const now = Date.now();
|
|
1152
|
+
const beforeReceivedAtMs = now - now % HOUR_MS + (now % HOUR_MS === 0 ? 0 : HOUR_MS);
|
|
1153
|
+
const reach = Math.max(0, beforeReceivedAtMs - WINDOW_MS[window]);
|
|
1154
|
+
const sinceMs = reach - reach % DAY_MS;
|
|
1155
|
+
const measure = async (count) => {
|
|
1156
|
+
const value = await count;
|
|
1157
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error("Insights database returned an invalid count.");
|
|
1158
|
+
return {
|
|
1159
|
+
count: value,
|
|
1160
|
+
measuredAtMs: Date.now()
|
|
1161
|
+
};
|
|
1162
|
+
};
|
|
1163
|
+
const bundleId = input.bundleId === void 0 ? void 0 : requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
|
|
1164
|
+
const reporting = measure(model.countLatestEvents({
|
|
1165
|
+
...scope,
|
|
1166
|
+
sinceMs
|
|
1167
|
+
}));
|
|
1168
|
+
if (bundleId === void 0) return {
|
|
1169
|
+
...scope,
|
|
1170
|
+
window,
|
|
1171
|
+
sinceMs,
|
|
1172
|
+
beforeReceivedAtMs,
|
|
1173
|
+
reportingInstallations: await reporting
|
|
1174
|
+
};
|
|
1175
|
+
const countOutcome = (outcome) => measure(model.countEvents({
|
|
1176
|
+
filter: bundleFilter({
|
|
1177
|
+
...scope,
|
|
1178
|
+
bundleId,
|
|
1179
|
+
outcome
|
|
1180
|
+
}),
|
|
1181
|
+
sinceMs,
|
|
1182
|
+
beforeReceivedAtMs
|
|
1183
|
+
}));
|
|
1184
|
+
const [reportingInstallations, bundleInstallations, downloadedReports, appliedReports, recoveredReports, failedReports] = await Promise.all([
|
|
1185
|
+
reporting,
|
|
1186
|
+
measure(model.countLatestEvents({
|
|
1187
|
+
...scope,
|
|
1188
|
+
sinceMs,
|
|
1189
|
+
bundle: [{
|
|
1190
|
+
field: "from_bundle_id",
|
|
1191
|
+
value: bundleId,
|
|
1192
|
+
types: ["UPDATE_DOWNLOADED"]
|
|
1193
|
+
}, {
|
|
1194
|
+
field: "to_bundle_id",
|
|
1195
|
+
value: bundleId,
|
|
1196
|
+
types: [
|
|
1197
|
+
"UNCHANGED",
|
|
1198
|
+
"UPDATE_APPLIED",
|
|
1199
|
+
"RECOVERED"
|
|
1200
|
+
]
|
|
1201
|
+
}]
|
|
1202
|
+
})),
|
|
1203
|
+
countOutcome("downloaded"),
|
|
1204
|
+
countOutcome("applied"),
|
|
1205
|
+
countOutcome("recovered"),
|
|
1206
|
+
countOutcome("failed")
|
|
1207
|
+
]);
|
|
1208
|
+
return {
|
|
1209
|
+
...scope,
|
|
1210
|
+
window,
|
|
1211
|
+
sinceMs,
|
|
1212
|
+
beforeReceivedAtMs,
|
|
1213
|
+
reportingInstallations,
|
|
1214
|
+
bundle: {
|
|
1215
|
+
bundleId,
|
|
1216
|
+
reportingInstallations: bundleInstallations,
|
|
1217
|
+
downloadedReports,
|
|
1218
|
+
appliedReports,
|
|
1219
|
+
recoveredReports,
|
|
1220
|
+
failedReports
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
/** The `error` a failed admin route answered with, or a message naming its status. */
|
|
1226
|
+
var errorOf = async (response, path) => {
|
|
1227
|
+
const body = await response.json().catch(() => null);
|
|
1228
|
+
return new Error(typeof body === "object" && body !== null && typeof Reflect.get(body, "error") === "string" ? String(Reflect.get(body, "error")) : `The server answered ${path.split("?")[0]} with ${response.status}.`);
|
|
1229
|
+
};
|
|
1230
|
+
var withQuery = (path, input) => {
|
|
1231
|
+
const query = new URLSearchParams();
|
|
1232
|
+
for (const [key, value] of Object.entries(input)) if (value !== void 0) query.set(key, String(value));
|
|
1233
|
+
const value = query.toString();
|
|
1234
|
+
return value.length === 0 ? path : `${path}?${value}`;
|
|
1235
|
+
};
|
|
1236
|
+
var eventQuery = ({ beforeReceivedAtMs, sinceMs, cursor, limit }) => ({
|
|
1237
|
+
beforeReceivedAtMs,
|
|
1238
|
+
sinceMs,
|
|
1239
|
+
cursor,
|
|
1240
|
+
limit
|
|
1241
|
+
});
|
|
1242
|
+
/**
|
|
1243
|
+
* A self-hosted server's Insights, read through the admin routes its
|
|
1244
|
+
* `insights()` plugin serves.
|
|
1245
|
+
*/
|
|
1246
|
+
var createAdminInsightsReads = (fetchAdmin) => {
|
|
1247
|
+
const read = async (path) => {
|
|
1248
|
+
const response = await fetchAdmin(path);
|
|
1249
|
+
if (response.status === 404) return null;
|
|
1250
|
+
if (!response.ok) throw await errorOf(response, path);
|
|
1251
|
+
return await response.json();
|
|
1252
|
+
};
|
|
1253
|
+
const required = async (path) => {
|
|
1254
|
+
const value = await read(path);
|
|
1255
|
+
if (value === null) throw new ConsoleFeatureUnavailableError("insights", { remote: true });
|
|
1256
|
+
return value;
|
|
1257
|
+
};
|
|
1258
|
+
return {
|
|
1259
|
+
getReportingOverview: ({ platform, channel, window, bundleId }) => required(withQuery("/overview", {
|
|
1260
|
+
platform,
|
|
1261
|
+
channel,
|
|
1262
|
+
window,
|
|
1263
|
+
bundleId
|
|
1264
|
+
})),
|
|
1265
|
+
listEvents: ({ bundle, ...input }) => required(withQuery("/events", {
|
|
1266
|
+
...eventQuery(input),
|
|
1267
|
+
...bundle === void 0 ? {} : {
|
|
1268
|
+
platform: bundle.platform,
|
|
1269
|
+
channel: bundle.channel,
|
|
1270
|
+
bundleId: bundle.bundleId,
|
|
1271
|
+
outcome: bundle.outcome
|
|
1272
|
+
}
|
|
1273
|
+
})),
|
|
1274
|
+
listInstallationEvents: ({ installId, ...input }) => required(withQuery(`/installations/${encodeURIComponent(installId)}/events`, eventQuery(input))),
|
|
1275
|
+
getInstallation: ({ installId }) => read(`/installations/${encodeURIComponent(installId)}`),
|
|
1276
|
+
pageInstallationsByCurrentUserId: ({ userId, cursor, limit }) => required(withQuery("/installations", {
|
|
1277
|
+
userId,
|
|
1278
|
+
cursor,
|
|
1279
|
+
limit
|
|
1280
|
+
})),
|
|
1281
|
+
getUpdateFailures: ({ platform, channel, releaseId, timeRange }) => required(withQuery("/failures", {
|
|
1282
|
+
platform,
|
|
1283
|
+
channel,
|
|
1284
|
+
releaseId,
|
|
1285
|
+
start: timeRange?.start,
|
|
1286
|
+
end: timeRange?.end
|
|
1287
|
+
})),
|
|
1288
|
+
getRetention: () => required("/retention")
|
|
1289
|
+
};
|
|
1290
|
+
};
|
|
1291
|
+
var featureApi = (api) => api;
|
|
1292
|
+
/** What serves each console feature, keyed like `consoleFeatures`. */
|
|
1293
|
+
var consoleFeatureApis = {
|
|
1294
|
+
insights: featureApi({
|
|
1295
|
+
local: (api) => ({
|
|
1296
|
+
...createInsightsProvider(createInsightsModel(api)),
|
|
1297
|
+
getRetention: async () => api.retention,
|
|
1298
|
+
getUpdateFailures: api.getUpdateFailures
|
|
1299
|
+
}),
|
|
1300
|
+
remote: createAdminInsightsReads
|
|
1301
|
+
}),
|
|
1302
|
+
insightsAnalytics: featureApi({ local: (api) => createInsightsModel(api) }),
|
|
1303
|
+
apiKeys: featureApi({ local: (api) => api })
|
|
1304
|
+
};
|
|
1305
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
1306
|
+
/** The ids of the plugins a self-hosted server lists on its admin `/version`. */
|
|
1307
|
+
var readServerPlugins = async (fetchAdmin) => {
|
|
1308
|
+
const response = await fetchAdmin("/version");
|
|
1309
|
+
if (!response.ok) throw new Error(`The server answered /version with ${response.status}.`);
|
|
1310
|
+
const body = await response.json();
|
|
1311
|
+
const plugins = isRecord(body) ? body.plugins : void 0;
|
|
1312
|
+
return Array.isArray(plugins) ? plugins.filter((id) => typeof id === "string") : [];
|
|
1313
|
+
};
|
|
1314
|
+
/** Runs `load` once and keeps its result; a failure is not kept, so the next call retries. */
|
|
1315
|
+
var once = (load) => {
|
|
1316
|
+
let pending;
|
|
1317
|
+
return () => pending ??= load().catch((error) => {
|
|
1318
|
+
pending = void 0;
|
|
1319
|
+
throw error;
|
|
1320
|
+
});
|
|
1321
|
+
};
|
|
1322
|
+
var featureIds = Object.keys(consoleFeatureApis);
|
|
1323
|
+
/**
|
|
1324
|
+
* What serves each feature, built by its factory: over the plugins' APIs the
|
|
1325
|
+
* console assembled, or over a self-hosted server's admin API. A feature
|
|
1326
|
+
* without its plugin, or without a factory for the admin API, has none.
|
|
1327
|
+
*/
|
|
1328
|
+
var featureApis = (build) => Object.fromEntries(featureIds.flatMap((feature) => {
|
|
1329
|
+
const api = build(feature);
|
|
1330
|
+
return api === void 0 ? [] : [[feature, api]];
|
|
1331
|
+
}));
|
|
1332
|
+
/**
|
|
1333
|
+
* The console's features and what serves them, for a console config:
|
|
1334
|
+
* - a self-hosted server (`standaloneRepository`) runs its plugins and lists
|
|
1335
|
+
* them on its admin `/version`; the console serves what that server's admin
|
|
1336
|
+
* API serves, and nothing that needs the database;
|
|
1337
|
+
* - otherwise the console assembles `plugins` over the database, as the
|
|
1338
|
+
* server does, and serves the features of the plugins it assembled.
|
|
1339
|
+
*/
|
|
1340
|
+
var createConsoleRuntime = (config) => {
|
|
1341
|
+
const { database } = config;
|
|
1342
|
+
if (isRemoteDatabase(database)) {
|
|
1343
|
+
const { fetchAdmin } = database;
|
|
1344
|
+
return {
|
|
1345
|
+
remote: true,
|
|
1346
|
+
features: once(async () => resolveConsoleFeatures(await readServerPlugins(fetchAdmin), { remote: true })),
|
|
1347
|
+
apis: featureApis((feature) => consoleFeatureApis[feature].remote?.(fetchAdmin))
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
const api = createDatabasePluginApis(database, config.plugins ?? []);
|
|
1351
|
+
const features = resolveConsoleFeatures(Object.keys(api), { remote: false });
|
|
1352
|
+
return {
|
|
1353
|
+
remote: false,
|
|
1354
|
+
features: async () => features,
|
|
1355
|
+
apis: featureApis((feature) => {
|
|
1356
|
+
const pluginApi = api[consoleFeatures[feature].plugin];
|
|
1357
|
+
return pluginApi === void 0 ? void 0 : consoleFeatureApis[feature].local(pluginApi);
|
|
1358
|
+
})
|
|
1359
|
+
};
|
|
1360
|
+
};
|
|
1361
|
+
/**
|
|
1362
|
+
* What serves `feature`. Every feature's server function asks for it first,
|
|
1363
|
+
* so a feature the console does not serve is refused with one error the
|
|
1364
|
+
* client recognizes, before anything is read.
|
|
1365
|
+
*/
|
|
1366
|
+
var requireFeature = async (runtime, feature) => {
|
|
1367
|
+
const api = runtime.apis[feature];
|
|
1368
|
+
if (api === void 0 || !(await runtime.features())[feature]) throw new ConsoleFeatureUnavailableError(feature, { remote: runtime.remote });
|
|
1369
|
+
return api;
|
|
1370
|
+
};
|
|
1371
|
+
//#endregion
|
|
1372
|
+
export { createConsoleRuntime, requireFeature };
|