@hot-updater/firebase 1.0.0-rc.1 → 1.0.0-rc.14

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.
@@ -6,7 +6,7 @@ let firebase_admin_firestore = require("firebase-admin/firestore");
6
6
  let firebase_admin_storage = require("firebase-admin/storage");
7
7
  //#endregion
8
8
  //#region ../../packages/server/dist/version.mjs
9
- const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.1";
9
+ const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.14";
10
10
  //#endregion
11
11
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
12
12
  const createVersionRouteHandlers = () => ({ version: async () => Response.json({
@@ -339,6 +339,7 @@ const resolveManifestAssetStorageUri = ({ assetBaseStorageUri, assetPath, downlo
339
339
  //#endregion
340
340
  //#region ../plugin-core/dist/databasePluginCrudValidationErrors.mjs
341
341
  var DatabasePluginInputError = class extends Error {
342
+ code;
342
343
  name = "DatabasePluginInputError";
343
344
  constructor(code) {
344
345
  super(`Invalid database plugin input: ${code}`);
@@ -381,6 +382,7 @@ const isDatabaseJsonValue = (value) => {
381
382
  };
382
383
  const isDatabaseJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && isDatabaseJsonValue(value);
383
384
  const isDatabaseMetadataObject = (value) => isDatabaseJsonObject(value) && (!Object.hasOwn(value, "app_version") || typeof value["app_version"] === "string");
385
+ const isDatabaseBundleEventMetadata = (value) => isDatabaseJsonObject(value) && (value.username === null || typeof value.username === "string") && 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");
384
386
  //#endregion
385
387
  //#region ../../packages/core/dist/index.mjs
386
388
  const stripBundleArtifactMetadata = (metadata) => metadata;
@@ -493,7 +495,7 @@ function getRolledOutNumericCohorts(bundleId, rolloutCohortCount) {
493
495
  return Array.from({ length: normalizedRolloutCount }, (_, position) => positiveMod(multiplier * position + offset, NUMERIC_COHORT_SIZE)).map((zeroBasedCohort) => zeroBasedCohort + 1).sort((left, right) => left - right);
494
496
  }
495
497
  const RELEASE_CATALOG_FALLBACK_POLICY = "BUILTIN_IF_ACTIVE_INELIGIBLE";
496
- const MAX_COMPILED_CATALOG_BYTES = 256 * 1024;
498
+ const MAX_COMPILED_CATALOG_BYTES = 262144;
497
499
  const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
498
500
  const MAX_CATALOG_SEGMENT_LENGTH = 255;
499
501
  function assertCatalogSegment(value, name) {
@@ -600,7 +602,7 @@ const isUUIDv7 = (value) => typeof value === "string" && UUID_V7_PATTERN.test(va
600
602
  //#endregion
601
603
  //#region ../plugin-core/dist/uuidv7.mjs
602
604
  function createUUIDv7FromTimestampHex(timestampHex) {
603
- const randomBytes = new Uint8Array(10);
605
+ const randomBytes = /* @__PURE__ */ new Uint8Array(10);
604
606
  crypto.getRandomValues(randomBytes);
605
607
  const randomHex = Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
606
608
  const randA = randomHex.slice(0, 3);
@@ -683,7 +685,6 @@ const databaseFields = {
683
685
  "type",
684
686
  "install_id",
685
687
  "user_id",
686
- "username",
687
688
  "from_bundle_id",
688
689
  "from_release_id",
689
690
  "to_release_id",
@@ -691,10 +692,7 @@ const databaseFields = {
691
692
  "platform",
692
693
  "app_version",
693
694
  "channel",
694
- "cohort",
695
- "update_strategy",
696
- "fingerprint_hash",
697
- "sdk_version",
695
+ "metadata",
698
696
  "received_at_ms"
699
697
  ],
700
698
  api_keys: [
@@ -709,7 +707,7 @@ const databaseFields = {
709
707
  };
710
708
  //#endregion
711
709
  //#region ../plugin-core/dist/databasePluginCrudValidationFields.mjs
712
- const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
710
+ const isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
713
711
  const isChannelText = (value) => {
714
712
  if (typeof value !== "string" || value.length === 0) return false;
715
713
  let codePointCount = 0;
@@ -719,6 +717,8 @@ const isChannelText = (value) => {
719
717
  }
720
718
  return true;
721
719
  };
720
+ const isInsightsIdentityText = (value) => typeof value === "string" && value.length > 0 && value.length <= 255;
721
+ const isNullableInsightsIdentityText = (value) => value === null || isInsightsIdentityText(value);
722
722
  const modelValidators = {
723
723
  bundles: {
724
724
  id: (value) => typeof value === "string",
@@ -793,10 +793,9 @@ const modelValidators = {
793
793
  },
794
794
  bundle_events: {
795
795
  id: (value) => typeof value === "string",
796
- type: (value) => value === "UPDATE_APPLIED" || value === "RECOVERED" || value === "RELEASE_ADOPTED" || value === "UNCHANGED",
797
- install_id: (value) => typeof value === "string",
798
- user_id: (value) => value === null || typeof value === "string",
799
- username: (value) => value === null || typeof value === "string",
796
+ type: (value) => value === "UPDATE_DOWNLOADED" || value === "UPDATE_APPLIED" || value === "RECOVERED" || value === "UNCHANGED",
797
+ install_id: isInsightsIdentityText,
798
+ user_id: isNullableInsightsIdentityText,
800
799
  from_bundle_id: (value) => value === null || typeof value === "string",
801
800
  from_release_id: (value) => value === null || typeof value === "string",
802
801
  to_release_id: (value) => value === null || typeof value === "string",
@@ -804,10 +803,7 @@ const modelValidators = {
804
803
  platform: (value) => value === "ios" || value === "android",
805
804
  app_version: (value) => typeof value === "string",
806
805
  channel: (value) => typeof value === "string",
807
- cohort: (value) => typeof value === "string",
808
- update_strategy: (value) => value === null || value === "fingerprint" || value === "appVersion",
809
- fingerprint_hash: (value) => value === null || typeof value === "string",
810
- sdk_version: (value) => value === null || typeof value === "string",
806
+ metadata: isDatabaseBundleEventMetadata,
811
807
  received_at_ms: (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0
812
808
  },
813
809
  api_keys: {
@@ -820,7 +816,7 @@ const modelValidators = {
820
816
  revoked_at_ms: (value) => value === null || typeof value === "number" && Number.isSafeInteger(value) && value >= 0
821
817
  }
822
818
  };
823
- const stringFields = new Set([
819
+ const stringFields = /* @__PURE__ */ new Set([
824
820
  "id",
825
821
  "platform",
826
822
  "file_hash",
@@ -861,7 +857,7 @@ const stringFields = new Set([
861
857
  "prefix",
862
858
  "role"
863
859
  ]);
864
- const numberFields = new Set([
860
+ const numberFields = /* @__PURE__ */ new Set([
865
861
  "archive_byte_size",
866
862
  "byte_size",
867
863
  "rollout_cohort_count",
@@ -874,13 +870,13 @@ const numberFields = new Set([
874
870
  "created_at_ms",
875
871
  "revoked_at_ms"
876
872
  ]);
877
- const booleanFields = new Set([
873
+ const booleanFields = /* @__PURE__ */ new Set([
878
874
  "should_force_update",
879
875
  "enabled",
880
876
  "is_tombstone"
881
877
  ]);
882
878
  const sortableFields = {
883
- bundles: new Set([
879
+ bundles: /* @__PURE__ */ new Set([
884
880
  "id",
885
881
  "platform",
886
882
  "file_hash",
@@ -897,7 +893,7 @@ const sortableFields = {
897
893
  "manifest_file_hash",
898
894
  "asset_base_storage_uri"
899
895
  ]),
900
- bundle_patches: new Set([
896
+ bundle_patches: /* @__PURE__ */ new Set([
901
897
  "id",
902
898
  "bundle_id",
903
899
  "base_bundle_id",
@@ -907,7 +903,7 @@ const sortableFields = {
907
903
  "byte_size",
908
904
  "order_index"
909
905
  ]),
910
- releases: new Set([
906
+ releases: /* @__PURE__ */ new Set([
911
907
  "id",
912
908
  "revision",
913
909
  "scope_key",
@@ -927,7 +923,7 @@ const sortableFields = {
927
923
  "created_at_ms",
928
924
  "updated_at_ms"
929
925
  ]),
930
- release_catalogs: new Set([
926
+ release_catalogs: /* @__PURE__ */ new Set([
931
927
  "scope_key",
932
928
  "catalog_id",
933
929
  "strategy",
@@ -941,8 +937,8 @@ const sortableFields = {
941
937
  "is_tombstone",
942
938
  "updated_at_ms"
943
939
  ]),
944
- channels: new Set(["id", "name"]),
945
- bundle_events: new Set([
940
+ channels: /* @__PURE__ */ new Set(["id", "name"]),
941
+ bundle_events: /* @__PURE__ */ new Set([
946
942
  "id",
947
943
  "type",
948
944
  "install_id",
@@ -952,6 +948,8 @@ const sortableFields = {
952
948
  "from_release_id",
953
949
  "to_release_id",
954
950
  "to_bundle_id",
951
+ "pending_bundle_id",
952
+ "pending_release_id",
955
953
  "platform",
956
954
  "app_version",
957
955
  "channel",
@@ -961,7 +959,7 @@ const sortableFields = {
961
959
  "sdk_version",
962
960
  "received_at_ms"
963
961
  ]),
964
- api_keys: new Set([
962
+ api_keys: /* @__PURE__ */ new Set([
965
963
  "id",
966
964
  "hash",
967
965
  "name",
@@ -993,9 +991,9 @@ const hasValidReleaseInvariants = (data) => {
993
991
  const fingerprintHash = data.fingerprint_hash;
994
992
  return (kind === "BUNDLE" && typeof bundleId === "string" || kind === "EMBEDDED" && bundleId === null) && (strategy === "APP_VERSION" && typeof targetAppVersion === "string" && fingerprintHash === null || strategy === "FINGERPRINT" && targetAppVersion === null && typeof fingerprintHash === "string");
995
993
  };
996
- const hasValidBundleEventInvariants = (data) => (data.type === "UPDATE_APPLIED" || data.type === "RECOVERED" || data.type === "RELEASE_ADOPTED") && typeof data.from_bundle_id === "string" && (data.update_strategy === "fingerprint" || data.update_strategy === "appVersion") || data.type === "UNCHANGED" && data.from_bundle_id === null && data.update_strategy === null;
994
+ const hasValidBundleEventInvariants = (data) => (data.type === "UPDATE_DOWNLOADED" || data.type === "UPDATE_APPLIED" || data.type === "RECOVERED") && typeof data.from_bundle_id === "string" && ((isRecord$3(data.metadata) ? data.metadata.update_strategy : void 0) === "fingerprint" || (isRecord$3(data.metadata) ? data.metadata.update_strategy : void 0) === "appVersion") || data.type === "UNCHANGED" && data.from_bundle_id === null && (isRecord$3(data.metadata) ? data.metadata.update_strategy : void 0) === null;
997
995
  const validateCreateData = (model, data) => {
998
- if (!isRecord$2(data)) throw new DatabasePluginInputError("invalid-data");
996
+ if (!isRecord$3(data)) throw new DatabasePluginInputError("invalid-data");
999
997
  validateFields(model, Object.keys(data));
1000
998
  for (const field of databaseFields[model]) {
1001
999
  const validator = modelValidators[model][field];
@@ -1012,7 +1010,7 @@ const selectRow = (row, input) => {
1012
1010
  return Object.fromEntries(select.map((field) => [field, Reflect.get(row, field)]));
1013
1011
  };
1014
1012
  const validateResult = (model, row, select) => {
1015
- if (!isRecord$2(row)) throw new DatabasePluginInputError("invalid-result");
1013
+ if (!isRecord$3(row)) throw new DatabasePluginInputError("invalid-result");
1016
1014
  const fields = select ?? databaseFields[model];
1017
1015
  for (const field of fields) {
1018
1016
  const validator = modelValidators[model][field];
@@ -1029,10 +1027,148 @@ const validateResult = (model, row, select) => {
1029
1027
  if (model === "bundle_events" && [
1030
1028
  "type",
1031
1029
  "from_bundle_id",
1032
- "update_strategy"
1030
+ "metadata"
1033
1031
  ].every((field) => Object.hasOwn(row, field)) && !hasValidBundleEventInvariants(row)) throw new DatabasePluginInputError("invalid-result");
1034
1032
  };
1035
1033
  //#endregion
1034
+ //#region ../plugin-core/dist/insightsContract.mjs
1035
+ const encoder = new TextEncoder();
1036
+ /** Exact UTF-8 byte ordering, without case folding or Unicode normalization. */
1037
+ const compareInsightsText = (left, right) => {
1038
+ const a = encoder.encode(left);
1039
+ const b = encoder.encode(right);
1040
+ for (let index = 0; index < Math.min(a.length, b.length); index += 1) if (a[index] !== b[index]) return a[index] - b[index];
1041
+ return a.length - b.length;
1042
+ };
1043
+ const isWellFormedText = (value) => {
1044
+ for (const character of value) {
1045
+ const point = character.codePointAt(0);
1046
+ if (point >= 55296 && point <= 57343) return false;
1047
+ }
1048
+ return true;
1049
+ };
1050
+ const isIdentity = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 && isWellFormedText(value);
1051
+ const isText = (value) => typeof value === "string" && value.length > 0 && isWellFormedText(value);
1052
+ const isTimestamp = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1053
+ const isLimit = (value) => isTimestamp(value) && value >= 1 && value <= 101;
1054
+ const hasOnlyKeys$1 = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
1055
+ const hasScope = (value) => (value.platform === "ios" || value.platform === "android") && isText(value.channel);
1056
+ const isBundleFilter = (value, withKind = false) => {
1057
+ if (!isRecord$3(value) || !hasScope(value)) return false;
1058
+ const keys = [
1059
+ "platform",
1060
+ "channel",
1061
+ "type",
1062
+ ...withKind ? ["kind"] : []
1063
+ ];
1064
+ return value.type === "RECOVERED" ? isText(value.fromBundleId) && hasOnlyKeys$1(value, [...keys, "fromBundleId"]) : (value.type === "UPDATE_DOWNLOADED" || value.type === "UPDATE_APPLIED" || value.type === "UNCHANGED") && isText(value.toBundleId) && hasOnlyKeys$1(value, [...keys, "toBundleId"]);
1065
+ };
1066
+ const isEventFilter = (value) => {
1067
+ if (!isRecord$3(value)) return false;
1068
+ if (value.kind === "all") return hasOnlyKeys$1(value, ["kind"]);
1069
+ if (value.kind === "installationMovement") return isIdentity(value.installId) && hasOnlyKeys$1(value, ["kind", "installId"]);
1070
+ return value.kind === "bundle" && isBundleFilter(value, true);
1071
+ };
1072
+ const validateRow = (model, row, result = false) => {
1073
+ try {
1074
+ validateCreateData(model, row);
1075
+ 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");
1076
+ } catch (error) {
1077
+ if (result) throw new DatabasePluginInputError("invalid-result");
1078
+ throw error;
1079
+ }
1080
+ };
1081
+ /** Downloads and applied transitions belong in installation history. */
1082
+ const isInsightsMovementEvent = (event) => event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_APPLIED" || event.type === "RECOVERED";
1083
+ const matchesInsightsEventFilter = (event, filter) => {
1084
+ if (filter.kind === "all") return true;
1085
+ if (filter.kind === "installationMovement") return event.install_id === filter.installId && isInsightsMovementEvent(event);
1086
+ 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);
1087
+ };
1088
+ const invalidQuery = () => {
1089
+ throw new DatabasePluginInputError("invalid-query");
1090
+ };
1091
+ const invalidResult = () => {
1092
+ throw new DatabasePluginInputError("invalid-result");
1093
+ };
1094
+ const validateCount = (count) => isTimestamp(count) ? count : invalidResult();
1095
+ /** Validate custom and bundled providers at the same public boundary. */
1096
+ const createValidatedInsightsModel = (model) => ({
1097
+ async recordEvent(input) {
1098
+ if (!isRecord$3(input) || !hasOnlyKeys$1(input, ["event"])) throw new DatabasePluginInputError("invalid-data");
1099
+ validateRow("bundle_events", input.event);
1100
+ await model.recordEvent(input);
1101
+ },
1102
+ async listEvents(input) {
1103
+ if (!isRecord$3(input) || !hasOnlyKeys$1(input, [
1104
+ "filter",
1105
+ "sinceMs",
1106
+ "beforeReceivedAtMs",
1107
+ "after",
1108
+ "limit"
1109
+ ]) || !isEventFilter(input.filter) || !isLimit(input.limit) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs !== void 0 && !isTimestamp(input.sinceMs)) invalidQuery();
1110
+ const sinceMs = input.sinceMs ?? 0;
1111
+ if (sinceMs > input.beforeReceivedAtMs || input.after !== void 0 && (!isRecord$3(input.after) || !hasOnlyKeys$1(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();
1112
+ const rows = await model.listEvents(input);
1113
+ if (!Array.isArray(rows) || rows.length > input.limit) invalidResult();
1114
+ let previous = input.after;
1115
+ for (const row of rows) {
1116
+ validateRow("bundle_events", row, true);
1117
+ 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();
1118
+ previous = {
1119
+ receivedAtMs: row.received_at_ms,
1120
+ id: row.id
1121
+ };
1122
+ }
1123
+ return rows;
1124
+ },
1125
+ async findLatestEvents(input) {
1126
+ if (!isRecord$3(input)) invalidQuery();
1127
+ if ("installId" in input) {
1128
+ if (!hasOnlyKeys$1(input, ["installId"]) || !isIdentity(input.installId)) invalidQuery();
1129
+ } else if (!hasOnlyKeys$1(input, [
1130
+ "userId",
1131
+ "afterInstallId",
1132
+ "limit"
1133
+ ]) || !isIdentity(input.userId) || !isLimit(input.limit) || input.afterInstallId !== void 0 && !isIdentity(input.afterInstallId)) invalidQuery();
1134
+ const rows = await model.findLatestEvents(input);
1135
+ if (!Array.isArray(rows) || rows.length > ("installId" in input ? 1 : input.limit)) invalidResult();
1136
+ let previous = "installId" in input ? void 0 : input.afterInstallId;
1137
+ for (const row of rows) {
1138
+ validateRow("bundle_events", row, true);
1139
+ if ("installId" in input ? row.install_id !== input.installId : row.user_id !== input.userId || previous !== void 0 && compareInsightsText(row.install_id, previous) <= 0) invalidResult();
1140
+ previous = row.install_id;
1141
+ }
1142
+ return rows;
1143
+ },
1144
+ async countLatestEvents(input) {
1145
+ if (!isRecord$3(input) || !hasOnlyKeys$1(input, [
1146
+ "platform",
1147
+ "channel",
1148
+ "sinceMs",
1149
+ "bundle"
1150
+ ]) || !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$1(bundle, [
1151
+ "field",
1152
+ "value",
1153
+ "types"
1154
+ ]) || 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) => ![
1155
+ "UNCHANGED",
1156
+ "UPDATE_DOWNLOADED",
1157
+ "UPDATE_APPLIED",
1158
+ "RECOVERED"
1159
+ ].includes(type))))) invalidQuery();
1160
+ return validateCount(await model.countLatestEvents(input));
1161
+ },
1162
+ async countEvents(input) {
1163
+ if (!isRecord$3(input) || !hasOnlyKeys$1(input, [
1164
+ "filter",
1165
+ "sinceMs",
1166
+ "beforeReceivedAtMs"
1167
+ ]) || !isBundleFilter(input.filter) || !isTimestamp(input.sinceMs) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs > input.beforeReceivedAtMs) invalidQuery();
1168
+ return validateCount(await model.countEvents(input));
1169
+ }
1170
+ });
1171
+ //#endregion
1036
1172
  //#region ../plugin-core/dist/databasePluginCrudValidationMutations.mjs
1037
1173
  const validateMutationWhere = (where) => {
1038
1174
  if (where.length === 0) throw new DatabasePluginInputError("empty-mutation-where");
@@ -1040,10 +1176,10 @@ const validateMutationWhere = (where) => {
1040
1176
  const validateUpdateWhere = (model, where) => {
1041
1177
  const selector = where[0];
1042
1178
  const primaryField = model === "release_catalogs" ? "scope_key" : "id";
1043
- if (where.length !== 1 || !isRecord$2(selector) || selector.field !== primaryField || selector.operator !== void 0 && selector.operator !== "eq" || typeof selector.value !== "string" || selector.connector !== void 0 || selector.mode !== void 0) throw new DatabasePluginInputError("invalid-update-selector");
1179
+ if (where.length !== 1 || !isRecord$3(selector) || selector.field !== primaryField || selector.operator !== void 0 && selector.operator !== "eq" || typeof selector.value !== "string" || selector.connector !== void 0 || selector.mode !== void 0) throw new DatabasePluginInputError("invalid-update-selector");
1044
1180
  };
1045
1181
  const validateBundleUpdateData = (update) => {
1046
- if (!isRecord$2(update)) throw new DatabasePluginInputError("invalid-data");
1182
+ if (!isRecord$3(update)) throw new DatabasePluginInputError("invalid-data");
1047
1183
  for (const [field, value] of Object.entries(update)) {
1048
1184
  if (field === "id") throw new DatabasePluginInputError("invalid-data");
1049
1185
  validateField("bundles", field);
@@ -1052,9 +1188,9 @@ const validateBundleUpdateData = (update) => {
1052
1188
  }
1053
1189
  };
1054
1190
  const validateApiKeyUpdateData = (update) => {
1055
- if (!isRecord$2(update) || Reflect.ownKeys(update).length !== 1 || !Object.hasOwn(update, "revoked_at_ms") || !modelValidators.api_keys.revoked_at_ms(Reflect.get(update, "revoked_at_ms"))) throw new DatabasePluginInputError("invalid-data");
1191
+ if (!isRecord$3(update) || Reflect.ownKeys(update).length !== 1 || !Object.hasOwn(update, "revoked_at_ms") || !modelValidators.api_keys.revoked_at_ms(Reflect.get(update, "revoked_at_ms"))) throw new DatabasePluginInputError("invalid-data");
1056
1192
  };
1057
- const RELEASE_MUTABLE_FIELDS = new Set([
1193
+ const RELEASE_MUTABLE_FIELDS = /* @__PURE__ */ new Set([
1058
1194
  "revision",
1059
1195
  "scope_key",
1060
1196
  "target_app_version",
@@ -1067,7 +1203,7 @@ const RELEASE_MUTABLE_FIELDS = new Set([
1067
1203
  "updated_at_ms"
1068
1204
  ]);
1069
1205
  const validateReleaseUpdateData = (update) => {
1070
- if (!isRecord$2(update) || Reflect.ownKeys(update).length === 0) throw new DatabasePluginInputError("invalid-data");
1206
+ if (!isRecord$3(update) || Reflect.ownKeys(update).length === 0) throw new DatabasePluginInputError("invalid-data");
1071
1207
  for (const [field, value] of Object.entries(update)) {
1072
1208
  if (!RELEASE_MUTABLE_FIELDS.has(field)) throw new DatabasePluginInputError("invalid-data");
1073
1209
  const validator = modelValidators.releases[field];
@@ -1075,7 +1211,7 @@ const validateReleaseUpdateData = (update) => {
1075
1211
  }
1076
1212
  };
1077
1213
  const validateReleaseCatalogUpdateData = (update) => {
1078
- if (!isRecord$2(update)) throw new DatabasePluginInputError("invalid-data");
1214
+ if (!isRecord$3(update)) throw new DatabasePluginInputError("invalid-data");
1079
1215
  const expectedFields = Object.keys(modelValidators.release_catalogs).filter((field) => field !== "scope_key");
1080
1216
  if (Reflect.ownKeys(update).length !== expectedFields.length || expectedFields.some((field) => !Object.hasOwn(update, field))) throw new DatabasePluginInputError("invalid-data");
1081
1217
  for (const [field, value] of Object.entries(update)) {
@@ -1155,7 +1291,7 @@ const validateWhere$1 = (model, where) => {
1155
1291
  if (where === void 0) return;
1156
1292
  if (!Array.isArray(where)) throw new DatabasePluginInputError("invalid-query");
1157
1293
  for (const item of where) {
1158
- if (!isRecord$2(item)) throw new DatabasePluginInputError("invalid-query");
1294
+ if (!isRecord$3(item)) throw new DatabasePluginInputError("invalid-query");
1159
1295
  if (item.connector !== void 0 && item.connector !== "AND" && item.connector !== "OR") throw new DatabasePluginInputError("invalid-query");
1160
1296
  validateWhereValue(model, item);
1161
1297
  }
@@ -1172,7 +1308,7 @@ const validateOrderBy = (model, orderBy) => {
1172
1308
  if (!Array.isArray(orderBy) || orderBy.length === 0) throw new DatabasePluginInputError("invalid-query");
1173
1309
  const fields = /* @__PURE__ */ new Set();
1174
1310
  return orderBy.map((clause) => {
1175
- if (!isRecord$2(clause) || typeof clause.field !== "string") throw new DatabasePluginInputError("invalid-query");
1311
+ if (!isRecord$3(clause) || typeof clause.field !== "string") throw new DatabasePluginInputError("invalid-query");
1176
1312
  validateField(model, clause.field);
1177
1313
  if (!sortableFields[model].has(clause.field)) throw new DatabasePluginInputError("invalid-query");
1178
1314
  if (clause.direction !== "asc" && clause.direction !== "desc") throw new DatabasePluginInputError("invalid-query");
@@ -1184,7 +1320,7 @@ const validateOrderBy = (model, orderBy) => {
1184
1320
  };
1185
1321
  const validateDistinctOn = (model, distinctOn, orderBy) => {
1186
1322
  if (distinctOn === void 0) return;
1187
- if (!isRecord$2(distinctOn)) throw new DatabasePluginInputError("invalid-distinct");
1323
+ if (!isRecord$3(distinctOn)) throw new DatabasePluginInputError("invalid-distinct");
1188
1324
  const fields = validateDistinctFields(model, distinctOn.fields);
1189
1325
  if (fields === void 0 || orderBy === void 0) throw new DatabasePluginInputError("invalid-distinct");
1190
1326
  for (const [index, field] of fields.entries()) if (orderBy[index]?.field !== field) throw new DatabasePluginInputError("invalid-distinct");
@@ -1198,7 +1334,7 @@ const validateBundlePagination = (options) => {
1198
1334
  if (!Number.isSafeInteger(options.limit) || options.limit <= 0 || options.page !== void 0 && (!Number.isSafeInteger(options.page) || options.page <= 0) || options.page !== void 0 && options.cursor !== void 0) throw new DatabasePluginInputError("invalid-pagination");
1199
1335
  const cursor = options.cursor;
1200
1336
  if (cursor === void 0) return;
1201
- if (!isRecord$2(cursor)) throw new DatabasePluginInputError("invalid-pagination");
1337
+ if (!isRecord$3(cursor)) throw new DatabasePluginInputError("invalid-pagination");
1202
1338
  const hasAfter = Object.hasOwn(cursor, "after");
1203
1339
  if (hasAfter === Object.hasOwn(cursor, "before")) throw new DatabasePluginInputError("invalid-pagination");
1204
1340
  const value = hasAfter ? cursor.after : cursor.before;
@@ -1293,7 +1429,91 @@ const createTransactionDatabasePlugin = (implementation) => {
1293
1429
  //#region ../plugin-core/dist/createDatabasePlugin.mjs
1294
1430
  const PAGE_SIZE$1 = 100;
1295
1431
  const compareChannelRows = (left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
1432
+ const toInsightsBundleWhere = (filter) => [
1433
+ {
1434
+ field: "platform",
1435
+ value: filter.platform
1436
+ },
1437
+ {
1438
+ field: "channel",
1439
+ value: filter.channel
1440
+ },
1441
+ {
1442
+ field: "type",
1443
+ value: filter.type
1444
+ },
1445
+ filter.type === "RECOVERED" ? {
1446
+ field: "from_bundle_id",
1447
+ value: filter.fromBundleId
1448
+ } : {
1449
+ field: "to_bundle_id",
1450
+ value: filter.toBundleId
1451
+ }
1452
+ ];
1453
+ const toInsightsEventRanges = (filter) => {
1454
+ if (filter.kind === "all") return [[]];
1455
+ if (filter.kind === "bundle") return [toInsightsBundleWhere(filter)];
1456
+ return [
1457
+ "UPDATE_DOWNLOADED",
1458
+ "UPDATE_APPLIED",
1459
+ "RECOVERED"
1460
+ ].map((type) => [{
1461
+ field: "install_id",
1462
+ value: filter.installId
1463
+ }, {
1464
+ field: "type",
1465
+ value: type
1466
+ }]);
1467
+ };
1468
+ const listInsightsEventRange = async (crud, input, filterWhere) => {
1469
+ const where = [...filterWhere, {
1470
+ field: "received_at_ms",
1471
+ operator: "gte",
1472
+ value: input.sinceMs ?? 0
1473
+ }];
1474
+ const sameTimestamp = input.after === void 0 ? [] : await crud.findMany({
1475
+ model: "bundle_events",
1476
+ where: [
1477
+ ...filterWhere,
1478
+ {
1479
+ field: "received_at_ms",
1480
+ value: input.after.receivedAtMs
1481
+ },
1482
+ {
1483
+ field: "id",
1484
+ operator: "lt",
1485
+ value: input.after.id
1486
+ }
1487
+ ],
1488
+ orderBy: [{
1489
+ field: "id",
1490
+ direction: "desc"
1491
+ }],
1492
+ limit: input.limit,
1493
+ offset: 0
1494
+ });
1495
+ if (sameTimestamp.length === input.limit) return sameTimestamp;
1496
+ const older = await crud.findMany({
1497
+ model: "bundle_events",
1498
+ where: [...where, {
1499
+ field: "received_at_ms",
1500
+ operator: "lt",
1501
+ value: input.after?.receivedAtMs ?? input.beforeReceivedAtMs
1502
+ }],
1503
+ orderBy: [{
1504
+ field: "received_at_ms",
1505
+ direction: "desc"
1506
+ }, {
1507
+ field: "id",
1508
+ direction: "desc"
1509
+ }],
1510
+ limit: input.limit - sameTimestamp.length,
1511
+ offset: 0
1512
+ });
1513
+ return [...sameTimestamp, ...older];
1514
+ };
1296
1515
  var DatabaseAtomicCommitUnsupportedError = class extends Error {
1516
+ pluginName;
1297
1517
  name = "DatabaseAtomicCommitUnsupportedError";
1298
1518
  constructor(pluginName) {
1299
1519
  super(`Database plugin "${pluginName}" cannot atomically commit changes across models.`);
@@ -1314,6 +1534,7 @@ var DatabaseRowReferencedError = class extends Error {
1314
1534
  }
1315
1535
  };
1316
1536
  var DatabaseCommitConflictError = class extends Error {
1537
+ result;
1317
1538
  name = "DatabaseCommitConflictError";
1318
1539
  constructor(result) {
1319
1540
  super("Database commit precondition failed.");
@@ -1541,12 +1762,6 @@ const applyChange = async (database, change, changeIndex) => {
1541
1762
  }
1542
1763
  return;
1543
1764
  }
1544
- case "insights":
1545
- await database.create({
1546
- model: "bundle_events",
1547
- data: change.row
1548
- });
1549
- return;
1550
1765
  case "apiKeys": switch (change.operation) {
1551
1766
  case "insert":
1552
1767
  await database.create({
@@ -1618,10 +1833,10 @@ const hasOnlyKeys = (value, keys) => {
1618
1833
  return Reflect.ownKeys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
1619
1834
  };
1620
1835
  const validateWhere = (where, field, validateValue = (value) => typeof value === "string") => {
1621
- if (!isRecord$2(where) || !hasOnlyKeys(where, [field]) || !validateValue(Reflect.get(where, field))) throw new DatabasePluginInputError("invalid-data");
1836
+ if (!isRecord$3(where) || !hasOnlyKeys(where, [field]) || !validateValue(Reflect.get(where, field))) throw new DatabasePluginInputError("invalid-data");
1622
1837
  };
1623
1838
  const validateDatabaseChange = (change) => {
1624
- if (!isRecord$2(change)) throw new DatabasePluginInputError("invalid-data");
1839
+ if (!isRecord$3(change)) throw new DatabasePluginInputError("invalid-data");
1625
1840
  switch (change.model) {
1626
1841
  case "bundles": switch (change.operation) {
1627
1842
  case "insert":
@@ -1728,14 +1943,6 @@ const validateDatabaseChange = (change) => {
1728
1943
  return;
1729
1944
  default: throw new DatabasePluginInputError("invalid-operation");
1730
1945
  }
1731
- case "insights":
1732
- if (change.operation !== "insert" || !hasOnlyKeys(change, [
1733
- "model",
1734
- "operation",
1735
- "row"
1736
- ])) throw new DatabasePluginInputError("invalid-operation");
1737
- validateCreateData("bundle_events", change.row);
1738
- return;
1739
1946
  case "apiKeys": switch (change.operation) {
1740
1947
  case "insert":
1741
1948
  if (!hasOnlyKeys(change, [
@@ -1754,7 +1961,7 @@ const validateDatabaseChange = (change) => {
1754
1961
  "update"
1755
1962
  ])) throw new DatabasePluginInputError("invalid-data");
1756
1963
  validateWhere(change.where, "id");
1757
- if (!isRecord$2(change.update)) throw new DatabasePluginInputError("invalid-data");
1964
+ if (!isRecord$3(change.update)) throw new DatabasePluginInputError("invalid-data");
1758
1965
  if (!hasOnlyKeys(change.update, ["revokedAtMs"])) throw new DatabasePluginInputError("invalid-data");
1759
1966
  validateApiKeyUpdateData({ revoked_at_ms: change.update.revokedAtMs });
1760
1967
  return;
@@ -1764,7 +1971,7 @@ const validateDatabaseChange = (change) => {
1764
1971
  }
1765
1972
  };
1766
1973
  const validateDatabaseCommitExpectation = (expectation) => {
1767
- if (!isRecord$2(expectation)) throw new DatabasePluginInputError("invalid-data");
1974
+ if (!isRecord$3(expectation)) throw new DatabasePluginInputError("invalid-data");
1768
1975
  if (expectation.model === "releases") {
1769
1976
  if (!hasOnlyKeys(expectation, [
1770
1977
  "model",
@@ -1784,7 +1991,7 @@ const validateDatabaseCommitExpectation = (expectation) => {
1784
1991
  throw new DatabasePluginInputError("invalid-model");
1785
1992
  };
1786
1993
  function validateDatabaseCommit(input) {
1787
- if (!isRecord$2(input) || !Array.isArray(input.changes) || (Object.hasOwn(input, "expectations") ? !hasOnlyKeys(input, ["changes", "expectations"]) || !Array.isArray(input.expectations) : !hasOnlyKeys(input, ["changes"]))) throw new DatabasePluginInputError("invalid-data");
1994
+ if (!isRecord$3(input) || !Array.isArray(input.changes) || (Object.hasOwn(input, "expectations") ? !hasOnlyKeys(input, ["changes", "expectations"]) || !Array.isArray(input.expectations) : !hasOnlyKeys(input, ["changes"]))) throw new DatabasePluginInputError("invalid-data");
1788
1995
  input.changes.forEach(validateDatabaseChange);
1789
1996
  if (Array.isArray(input.expectations)) input.expectations.forEach(validateDatabaseCommitExpectation);
1790
1997
  }
@@ -2004,36 +2211,31 @@ const createDatabasePluginAdapter = (name, implementation) => {
2004
2211
  }
2005
2212
  },
2006
2213
  insights: {
2007
- async append(row) {
2008
- await crud.create({
2009
- model: "bundle_events",
2010
- data: row
2011
- });
2214
+ recordEvent: (input) => implementation.recordInsights(input),
2215
+ async listEvents(input) {
2216
+ const ranges = await Promise.all(toInsightsEventRanges(input.filter).map((where) => listInsightsEventRange(crud, input, where)));
2217
+ if (ranges.length === 1) return ranges[0];
2218
+ return ranges.flat().sort((left, right) => right.received_at_ms - left.received_at_ms || compareInsightsText(right.id, left.id)).slice(0, input.limit);
2012
2219
  },
2013
- async scan(input) {
2014
- const rows = [];
2015
- for (let offset = 0; rows.length < input.limit; offset += PAGE_SIZE$1) {
2016
- const page = await crud.findMany({
2017
- model: "bundle_events",
2018
- where: [{
2220
+ findLatestEvents: (input) => implementation.findLatestInsightsEvents(input),
2221
+ countLatestEvents: (input) => implementation.countLatestInsightsEvents(input),
2222
+ countEvents(input) {
2223
+ return crud.count({
2224
+ model: "bundle_events",
2225
+ where: [
2226
+ ...toInsightsBundleWhere(input.filter),
2227
+ {
2228
+ field: "received_at_ms",
2229
+ operator: "gte",
2230
+ value: input.sinceMs
2231
+ },
2232
+ {
2019
2233
  field: "received_at_ms",
2020
2234
  operator: "lt",
2021
2235
  value: input.beforeReceivedAtMs
2022
- }],
2023
- orderBy: [{
2024
- field: "received_at_ms",
2025
- direction: "asc"
2026
- }, {
2027
- field: "id",
2028
- direction: "asc"
2029
- }],
2030
- limit: PAGE_SIZE$1,
2031
- offset
2032
- });
2033
- rows.push(...page.filter((row) => input.after === void 0 || row.received_at_ms > input.after.receivedAtMs || row.received_at_ms === input.after.receivedAtMs && row.id > input.after.id));
2034
- if (page.length < PAGE_SIZE$1) break;
2035
- }
2036
- return rows.slice(0, input.limit);
2236
+ }
2237
+ ]
2238
+ });
2037
2239
  }
2038
2240
  },
2039
2241
  apiKeys: {
@@ -2077,7 +2279,13 @@ const createDatabasePluginAdapter = (name, implementation) => {
2077
2279
  ...implementation.dispose ? { dispose: implementation.dispose } : {}
2078
2280
  };
2079
2281
  };
2080
- const createDatabasePlugin = (options) => ({ ...options });
2282
+ const createDatabasePlugin = (options) => ({
2283
+ ...options,
2284
+ models: {
2285
+ ...options.models,
2286
+ insights: createValidatedInsightsModel(options.models.insights)
2287
+ }
2288
+ });
2081
2289
  //#endregion
2082
2290
  //#region ../plugin-core/dist/createStoragePlugin.mjs
2083
2291
  const createStoragePlugin = (options) => ({ ...options });
@@ -2212,6 +2420,8 @@ const rowsToBundles = (bundleRows, patchRows, referencedBundleRows) => {
2212
2420
  //#endregion
2213
2421
  //#region ../plugin-core/dist/databaseClientUpdates.mjs
2214
2422
  var DatabasePatchUpdateUnsupportedError = class extends Error {
2423
+ bundleId;
2424
+ pluginName;
2215
2425
  name = "DatabasePatchUpdateUnsupportedError";
2216
2426
  constructor(bundleId, pluginName) {
2217
2427
  super(`Database plugin "${pluginName}" cannot atomically replace patches for bundle "${bundleId}".`);
@@ -2300,7 +2510,8 @@ const hydrateRows = async (database, ownerRows) => {
2300
2510
  const patchRows = await database.models.bundlePatches.findByBundleIds(ownerRows.map(({ id }) => id));
2301
2511
  const ownerIds = new Set(ownerRows.map(({ id }) => id));
2302
2512
  const referencedIds = [...new Set(patchRows.map(({ base_bundle_id }) => base_bundle_id).filter((id) => !ownerIds.has(id)))];
2303
- return rowsToBundles(ownerRows, patchRows, referencedIds.length === 0 ? [] : await loadBundleRows(database, { id: { in: referencedIds } }));
2513
+ const referencedRows = referencedIds.length === 0 ? [] : await loadBundleRows(database, { id: { in: referencedIds } });
2514
+ return rowsToBundles(ownerRows, patchRows, referencedRows);
2304
2515
  };
2305
2516
  const cursorIdFilter = (cursor, direction) => {
2306
2517
  if (cursor?.after) return { [direction === "desc" ? "lt" : "gt"]: cursor.after };
@@ -2352,6 +2563,7 @@ const responsePage = async (database, options) => {
2352
2563
  //#endregion
2353
2564
  //#region ../plugin-core/dist/databaseClient.mjs
2354
2565
  var DatabaseBundleNotFoundError = class extends Error {
2566
+ bundleId;
2355
2567
  name = "DatabaseBundleNotFoundError";
2356
2568
  constructor(bundleId) {
2357
2569
  super(`Bundle "${bundleId}" was not found.`);
@@ -2359,6 +2571,8 @@ var DatabaseBundleNotFoundError = class extends Error {
2359
2571
  }
2360
2572
  };
2361
2573
  var DatabasePatchInsertUnsupportedError = class extends Error {
2574
+ bundleId;
2575
+ pluginName;
2362
2576
  name = "DatabasePatchInsertUnsupportedError";
2363
2577
  constructor(bundleId, pluginName) {
2364
2578
  super(`Database plugin "${pluginName}" cannot atomically insert patches for bundle "${bundleId}".`);
@@ -2377,12 +2591,13 @@ const insertChanges = (bundle) => [{
2377
2591
  }))];
2378
2592
  const updateChanges = (bundleId, update) => {
2379
2593
  const rowUpdate = bundleUpdateToRow(update);
2594
+ const patchesPresent = Object.hasOwn(update, "patches");
2380
2595
  return [{
2381
2596
  model: "bundles",
2382
2597
  operation: "update",
2383
2598
  where: { id: bundleId },
2384
2599
  update: rowUpdate
2385
- }, ...Object.hasOwn(update, "patches") ? [{
2600
+ }, ...patchesPresent ? [{
2386
2601
  model: "bundlePatches",
2387
2602
  operation: "delete",
2388
2603
  where: { bundleId }
@@ -2455,7 +2670,7 @@ const createDatabaseClient = (plugin) => {
2455
2670
  };
2456
2671
  };
2457
2672
  //#endregion
2458
- //#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.3.2/node_modules/verkit/dist/index.mjs
2673
+ //#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/comparison-DenM3wCn.mjs
2459
2674
  const LETTER_DASH_NUMBER = "[a-zA-Z0-9-]";
2460
2675
  const NUMERIC_IDENTIFIER = String.raw`0|[1-9]\d*`;
2461
2676
  const NUMERIC_IDENTIFIER_LOOSE = String.raw`\d+`;
@@ -2492,22 +2707,9 @@ function makeSafeRegexSource(source) {
2492
2707
  function safeRegex(source, flags) {
2493
2708
  return new RegExp(makeSafeRegexSource(source), flags);
2494
2709
  }
2495
- const NUMERIC$1 = /^\d+$/;
2496
- function compareIdentifiers(left, right) {
2497
- if (typeof left === "number" && typeof right === "number") return left === right ? 0 : left < right ? -1 : 1;
2498
- const leftNumeric = NUMERIC$1.test(String(left));
2499
- const rightNumeric = NUMERIC$1.test(String(right));
2500
- const normalizedLeft = leftNumeric ? Number(left) : left;
2501
- const normalizedRight = rightNumeric ? Number(right) : right;
2502
- return normalizedLeft === normalizedRight ? 0 : leftNumeric && !rightNumeric ? -1 : rightNumeric && !leftNumeric ? 1 : normalizedLeft < normalizedRight ? -1 : 1;
2503
- }
2504
2710
  const FULL = safeRegex(`^${FULL_PLAIN}$`);
2505
2711
  const LOOSE = safeRegex(`^${LOOSE_PLAIN}$`);
2506
- safeRegex(`^${PRERELEASE}$`);
2507
- safeRegex(`^${PRERELEASE_LOOSE}$`);
2508
- const COERCE_EXACT = safeRegex(COERCE);
2509
- const COERCE_FULL_EXACT = safeRegex(COERCE_FULL);
2510
- const NUMERIC = /^\d+$/;
2712
+ const NUMERIC$1 = /^\d+$/;
2511
2713
  function formatComparableVersion(version) {
2512
2714
  const base = `${version.major}.${version.minor}.${version.patch}`;
2513
2715
  return version.prerelease?.length ? `${base}-${version.prerelease.join(".")}` : base;
@@ -2528,7 +2730,7 @@ function parse(version, options = {}) {
2528
2730
  if (minor > Number.MAX_SAFE_INTEGER || minor < 0) throw new TypeError(`Invalid minor version: ${match[2]}`);
2529
2731
  if (patch > Number.MAX_SAFE_INTEGER || patch < 0) throw new TypeError(`Invalid patch version: ${match[3]}`);
2530
2732
  const prerelease = match[4] ? match[4].split(".").map((identifier) => {
2531
- if (NUMERIC.test(identifier)) {
2733
+ if (NUMERIC$1.test(identifier)) {
2532
2734
  const numeric = Number(identifier);
2533
2735
  if (numeric >= 0 && numeric < Number.MAX_SAFE_INTEGER) return numeric;
2534
2736
  }
@@ -2549,6 +2751,15 @@ function tryParse(version, options = {}) {
2549
2751
  return null;
2550
2752
  }
2551
2753
  }
2754
+ const NUMERIC = /^\d+$/;
2755
+ function compareIdentifiers(left, right) {
2756
+ if (typeof left === "number" && typeof right === "number") return left === right ? 0 : left < right ? -1 : 1;
2757
+ const leftNumeric = NUMERIC.test(String(left));
2758
+ const rightNumeric = NUMERIC.test(String(right));
2759
+ const normalizedLeft = leftNumeric ? Number(left) : left;
2760
+ const normalizedRight = rightNumeric ? Number(right) : right;
2761
+ return normalizedLeft === normalizedRight ? 0 : leftNumeric && !rightNumeric ? -1 : rightNumeric && !leftNumeric ? 1 : normalizedLeft < normalizedRight ? -1 : 1;
2762
+ }
2552
2763
  function compareMainParsed(left, right) {
2553
2764
  return left.major === right.major ? left.minor === right.minor ? left.patch === right.patch ? 0 : left.patch < right.patch ? -1 : 1 : left.minor < right.minor ? -1 : 1 : left.major < right.major ? -1 : 1;
2554
2765
  }
@@ -2570,26 +2781,15 @@ function comparePrereleaseParsed(left, right) {
2570
2781
  function compareParsed(left, right) {
2571
2782
  return compareMainParsed(left, right) || comparePrereleaseParsed(left, right);
2572
2783
  }
2573
- function coerceParsedVersion(value, options = {}) {
2574
- if (typeof value === "object") return value;
2575
- const input = typeof value === "number" ? String(value) : value;
2576
- if (typeof input !== "string") return null;
2577
- let match = null;
2578
- if (options.rtl) {
2579
- const expression = safeRegex(options.includePrerelease ? COERCE_FULL : COERCE, "g");
2580
- let next;
2581
- while ((next = expression.exec(input)) && (!match || match.index + match[0].length !== input.length)) {
2582
- if (!match || next.index + next[0].length !== match.index + match[0].length) match = next;
2583
- expression.lastIndex = next.index + next[1].length + next[2].length;
2584
- }
2585
- } else match = (options.includePrerelease ? COERCE_FULL_EXACT : COERCE_EXACT).exec(input);
2586
- if (!match) return null;
2587
- const major = match[2];
2588
- return tryParse(`${major}.${match[3] || "0"}.${match[4] || "0"}${options.includePrerelease && match[5] ? `-${match[5]}` : ""}${options.includePrerelease && match[6] ? `+${match[6]}` : ""}`, options);
2784
+ function compare$1(left, right, options = {}) {
2785
+ return compareParsed(parse(left, options), parse(right, options));
2589
2786
  }
2787
+ //#endregion
2788
+ //#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/set-CC5YeoYX.mjs
2590
2789
  const STRICT_COMPARATOR = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*(${FULL_PLAIN})$|^$`);
2591
2790
  const LOOSE_COMPARATOR$1 = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*(${LOOSE_PLAIN})$|^$`);
2592
2791
  function parseComparator(comparator, options = {}) {
2792
+ if (typeof comparator !== "string") return comparator;
2593
2793
  const normalized = comparator.trim().replaceAll(/\s+/g, " ");
2594
2794
  const match = normalized.match(options.loose ? LOOSE_COMPARATOR$1 : STRICT_COMPARATOR);
2595
2795
  if (!match) throw new TypeError(`Invalid comparator: ${normalized}`);
@@ -2602,9 +2802,8 @@ function parseComparator(comparator, options = {}) {
2602
2802
  version
2603
2803
  };
2604
2804
  }
2605
- function compare$1(left, right, options = {}) {
2606
- return compareParsed(parse(left, options), parse(right, options));
2607
- }
2805
+ //#endregion
2806
+ //#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/range-DvX-Y6iv.mjs
2608
2807
  const BUILD_STRIP = new RegExp(BUILD, "g");
2609
2808
  const BUILD_SAFE = safeRegex(BUILD);
2610
2809
  const STRICT_HYPHEN = safeRegex(String.raw`^\s*(${XRANGE_PLAIN})\s+-\s+(${XRANGE_PLAIN})\s*$`);
@@ -2730,9 +2929,9 @@ function parseSimpleRange(input, options) {
2730
2929
  function parseRange(range, options = {}) {
2731
2930
  if (typeof range !== "string") return range;
2732
2931
  const parsedOptions = { ...options };
2733
- const raw = range.trim().replaceAll(/\s+/g, " ");
2734
- let sets = raw.split("||").map((part) => parseSimpleRange(part.trim(), parsedOptions)).filter((set) => set.length);
2735
- if (!sets.length) throw new TypeError(`Range contains no valid comparator sets: ${raw}`);
2932
+ const normalizedRange = range.trim().replaceAll(/\s+/g, " ");
2933
+ let sets = normalizedRange.split("||").map((part) => parseSimpleRange(part.trim(), parsedOptions)).filter((set) => set.length);
2934
+ if (!sets.length) throw new TypeError(`Range contains no valid comparator sets: ${normalizedRange}`);
2736
2935
  if (sets.length > 1) {
2737
2936
  const first = sets[0];
2738
2937
  sets = sets.filter((set) => set[0]?.value !== "<0.0.0-0");
@@ -2743,19 +2942,39 @@ function parseRange(range, options = {}) {
2743
2942
  }
2744
2943
  }
2745
2944
  return {
2746
- normalized: sets.map((set) => set.map((comparator) => comparator.value).join(" ")).join("||"),
2747
2945
  options: parsedOptions,
2748
- raw,
2749
2946
  sets
2750
2947
  };
2751
2948
  }
2949
+ //#endregion
2950
+ //#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/version-CQ98ZBpL.mjs
2951
+ const COERCE_EXACT = safeRegex(COERCE);
2952
+ const COERCE_FULL_EXACT = safeRegex(COERCE_FULL);
2953
+ safeRegex(`^${PRERELEASE}$`);
2954
+ safeRegex(`^${PRERELEASE_LOOSE}$`);
2955
+ function normalizeFull(version, options = {}) {
2956
+ const parsed = tryParse(version, options);
2957
+ return parsed ? formatFullVersion(parsed) : null;
2958
+ }
2752
2959
  function normalize(version, options = {}) {
2753
2960
  const parsed = tryParse(version, options);
2754
2961
  return parsed ? formatComparableVersion(parsed) : null;
2755
2962
  }
2756
2963
  function coerce(value, options = {}) {
2757
- const parsed = coerceParsedVersion(value, options);
2758
- return parsed ? formatFullVersion(parsed) : null;
2964
+ if (typeof value === "object") return value;
2965
+ const input = typeof value === "number" ? String(value) : value;
2966
+ let match = null;
2967
+ if (options.rtl) {
2968
+ const expression = safeRegex(options.includePrerelease ? COERCE_FULL : COERCE, "g");
2969
+ let next;
2970
+ while ((next = expression.exec(input)) && (!match || match.index + match[0].length !== input.length)) {
2971
+ if (!match || next.index + next[0].length !== match.index + match[0].length) match = next;
2972
+ expression.lastIndex = next.index + next[1].length + next[2].length;
2973
+ }
2974
+ } else match = (options.includePrerelease ? COERCE_FULL_EXACT : COERCE_EXACT).exec(input);
2975
+ if (!match) return null;
2976
+ const major = match[2];
2977
+ return tryParse(`${major}.${match[3] || "0"}.${match[4] || "0"}${options.includePrerelease && match[5] ? `-${match[5]}` : ""}${options.includePrerelease && match[6] ? `+${match[6]}` : ""}`, options);
2759
2978
  }
2760
2979
  //#endregion
2761
2980
  //#region ../plugin-core/dist/releaseCatalogCompiler.mjs
@@ -3035,15 +3254,16 @@ function compileAppVersion(releases) {
3035
3254
  const retainedIds = new Set(segmentReleases.flatMap(({ retainedIds, rollbackIds }) => [...retainedIds, ...rollbackIds]));
3036
3255
  const retainedReleases = releases.filter((release) => retainedIds.has(release.id));
3037
3256
  const descriptorIndex = new Map(retainedReleases.map((release, index) => [release.id, index]));
3257
+ const segments = mergeSegments(segmentReleases.map(({ segment, retainedIds, rollbackIds }) => ({
3258
+ ...segment,
3259
+ releaseIndexes: releases.filter((release) => retainedIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0),
3260
+ rollbackReleaseIndexes: releases.filter((release) => rollbackIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0)
3261
+ })).filter((segment) => segment.releaseIndexes.length > 0 || segment.rollbackReleaseIndexes.length > 0));
3038
3262
  return {
3039
3263
  envelope: {
3040
3264
  fallbackPolicy: RELEASE_CATALOG_FALLBACK_POLICY,
3041
3265
  schemaVersion: 1,
3042
- segments: mergeSegments(segmentReleases.map(({ segment, retainedIds, rollbackIds }) => ({
3043
- ...segment,
3044
- releaseIndexes: releases.filter((release) => retainedIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0),
3045
- rollbackReleaseIndexes: releases.filter((release) => rollbackIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0)
3046
- })).filter((segment) => segment.releaseIndexes.length > 0 || segment.rollbackReleaseIndexes.length > 0)),
3266
+ segments,
3047
3267
  strategy: "APP_VERSION"
3048
3268
  },
3049
3269
  retainedReleases
@@ -3111,7 +3331,8 @@ function versionInSegment(version, segment) {
3111
3331
  return true;
3112
3332
  }
3113
3333
  function canonicalizeAppVersion(appVersion) {
3114
- return coerce(appVersion);
3334
+ const version = coerce(appVersion);
3335
+ return version ? normalizeFull(version) : null;
3115
3336
  }
3116
3337
  function projectCompiledCatalog(catalog, appVersion) {
3117
3338
  let indexes;
@@ -3139,6 +3360,7 @@ function projectCompiledRollbackCatalog(catalog, appVersion) {
3139
3360
  const RELEASE_PAGE_SIZE = 1e3;
3140
3361
  const DEFAULT_MAX_ATTEMPTS = 3;
3141
3362
  var ReleaseCatalogMutationError = class extends Error {
3363
+ code;
3142
3364
  name = "ReleaseCatalogMutationError";
3143
3365
  constructor(code, message) {
3144
3366
  super(message);
@@ -3398,6 +3620,7 @@ async function rebuildReleaseCatalog(input) {
3398
3620
  //#endregion
3399
3621
  //#region ../plugin-core/dist/releaseManagement.mjs
3400
3622
  var ReleaseManagementError = class extends Error {
3623
+ code;
3401
3624
  name = "ReleaseManagementError";
3402
3625
  constructor(code, message) {
3403
3626
  super(message);
@@ -3477,7 +3700,7 @@ async function deleteRelease(input) {
3477
3700
  }
3478
3701
  //#endregion
3479
3702
  //#region ../plugin-core/dist/storageDownloadPath.mjs
3480
- const decodeBase64Url = (value) => {
3703
+ const decodeBase64Url$1 = (value) => {
3481
3704
  try {
3482
3705
  const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
3483
3706
  const binary = atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, "="));
@@ -3489,7 +3712,7 @@ const decodeBase64Url = (value) => {
3489
3712
  const parseStorageDownloadPath = (path) => {
3490
3713
  const match = /^\/storage\/([^/]+)\/([^/]+)$/.exec(path);
3491
3714
  if (!match) return null;
3492
- const storageUri = decodeBase64Url(match[1]);
3715
+ const storageUri = decodeBase64Url$1(match[1]);
3493
3716
  if (storageUri === null) return null;
3494
3717
  try {
3495
3718
  return {
@@ -3610,7 +3833,7 @@ const createReleaseCatalogRouteHandlers = (clientAccessHeaderName = "x-api-key")
3610
3833
  //#endregion
3611
3834
  //#region ../../packages/server/dist/handlerReleaseManagementRoutes.mjs
3612
3835
  const unavailable = () => Response.json({ error: "Not found" }, { status: 404 });
3613
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3836
+ const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3614
3837
  const parseRevision = (value) => {
3615
3838
  if (value === void 0 || value === null || value === "") return void 0;
3616
3839
  const revision = typeof value === "number" ? value : Number(value);
@@ -3619,7 +3842,7 @@ const parseRevision = (value) => {
3619
3842
  };
3620
3843
  const parsePolicyInput = async (request) => {
3621
3844
  const body = await request.json();
3622
- if (!isRecord$1(body) || !isRecord$1(body.patch)) throw new HandlerBadRequestError("Invalid Release policy mutation");
3845
+ if (!isRecord$2(body) || !isRecord$2(body.patch)) throw new HandlerBadRequestError("Invalid Release policy mutation");
3623
3846
  return {
3624
3847
  expectedRevision: parseRevision(body.expectedRevision),
3625
3848
  patch: body.patch
@@ -3729,17 +3952,11 @@ const createReleaseManagementRouteHandlers = () => ({
3729
3952
  });
3730
3953
  //#endregion
3731
3954
  //#region ../../packages/server/dist/insights/errors.mjs
3732
- var InsightsScanLimitExceededError = class extends Error {
3733
- constructor(limit) {
3734
- super(`Insights event scan exceeded ${limit} rows.`);
3735
- this.limit = limit;
3736
- this.name = "InsightsScanLimitExceededError";
3737
- }
3738
- };
3739
3955
  var InsightsBadRequestError = class extends Error {
3740
3956
  name = "InsightsBadRequestError";
3741
3957
  };
3742
3958
  var InsightsPayloadTooLargeError = class extends Error {
3959
+ maximumBytes;
3743
3960
  name = "InsightsPayloadTooLargeError";
3744
3961
  constructor(maximumBytes) {
3745
3962
  super(`Event payload exceeds ${maximumBytes} bytes`);
@@ -3749,8 +3966,9 @@ var InsightsPayloadTooLargeError = class extends Error {
3749
3966
  //#endregion
3750
3967
  //#region ../../packages/server/dist/insights/eventInput.mjs
3751
3968
  const MAX_EVENT_STRING_LENGTH = 1024;
3752
- const EVENT_BODY_MAX_BYTES = 16 * 1024;
3753
- const eventKeys = new Set([
3969
+ const MAX_IDENTITY_LENGTH$2 = 255;
3970
+ const EVENT_BODY_MAX_BYTES = 16384;
3971
+ const eventKeys = /* @__PURE__ */ new Set([
3754
3972
  "type",
3755
3973
  "installId",
3756
3974
  "toBundleId",
@@ -3767,18 +3985,23 @@ const eventKeys = new Set([
3767
3985
  "updateStrategy",
3768
3986
  "sdkVersion"
3769
3987
  ]);
3770
- function isRecord(value) {
3988
+ function isRecord$1(value) {
3771
3989
  return typeof value === "object" && value !== null && !Array.isArray(value);
3772
3990
  }
3773
3991
  function requireStringField(payload, key) {
3774
3992
  const value = payload[key];
3775
- if (typeof value !== "string" || value.length === 0 || value.length > MAX_EVENT_STRING_LENGTH) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
3993
+ 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}`);
3776
3994
  return value;
3777
3995
  }
3778
3996
  function requireNullableStringField(payload, key) {
3779
3997
  if (payload[key] === null) return null;
3780
3998
  return requireStringField(payload, key);
3781
3999
  }
4000
+ function requireIdentityField(payload, key) {
4001
+ const value = requireStringField(payload, key);
4002
+ if (value.length > MAX_IDENTITY_LENGTH$2) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
4003
+ return value;
4004
+ }
3782
4005
  async function readBoundedText(request) {
3783
4006
  const contentLength = request.headers.get("content-length");
3784
4007
  const declaredByteLength = Number(contentLength);
@@ -3810,13 +4033,13 @@ async function parseJson(request) {
3810
4033
  }
3811
4034
  }
3812
4035
  function requireEvent(payload) {
3813
- if (!isRecord(payload) || Object.keys(payload).some((key) => !eventKeys.has(key))) throw new InsightsBadRequestError("Invalid event payload");
4036
+ if (!isRecord$1(payload) || Object.keys(payload).some((key) => !eventKeys.has(key))) throw new InsightsBadRequestError("Invalid event payload");
3814
4037
  const platform = requireStringField(payload, "platform");
3815
4038
  if (platform !== "ios" && platform !== "android") throw new InsightsBadRequestError("Invalid event field: platform");
3816
4039
  const base = {
3817
- installId: requireStringField(payload, "installId"),
4040
+ installId: requireIdentityField(payload, "installId"),
3818
4041
  toBundleId: requireStringField(payload, "toBundleId"),
3819
- ...payload.userId === void 0 ? {} : { userId: requireStringField(payload, "userId") },
4042
+ ...payload.userId === void 0 ? {} : { userId: requireIdentityField(payload, "userId") },
3820
4043
  ...payload.username === void 0 ? {} : { username: requireStringField(payload, "username") },
3821
4044
  platform,
3822
4045
  appVersion: requireStringField(payload, "appVersion"),
@@ -3829,9 +4052,9 @@ function requireEvent(payload) {
3829
4052
  };
3830
4053
  const type = requireStringField(payload, "type");
3831
4054
  switch (type) {
4055
+ case "UPDATE_DOWNLOADED":
3832
4056
  case "UPDATE_APPLIED":
3833
- case "RECOVERED":
3834
- case "RELEASE_ADOPTED": {
4057
+ case "RECOVERED": {
3835
4058
  const updateStrategy = requireStringField(payload, "updateStrategy");
3836
4059
  if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") throw new InsightsBadRequestError("Invalid event field: updateStrategy");
3837
4060
  return {
@@ -3855,59 +4078,131 @@ function requireEvent(payload) {
3855
4078
  async function parseBundleEventRequest(request) {
3856
4079
  return requireEvent(await parseJson(request));
3857
4080
  }
4081
+ function createBundleEventRow(input) {
4082
+ input = requireEvent(input);
4083
+ const base = {
4084
+ app_version: input.appVersion,
4085
+ channel: input.channel,
4086
+ from_release_id: input.fromReleaseId,
4087
+ id: createUUIDv7(),
4088
+ install_id: input.installId,
4089
+ platform: input.platform,
4090
+ received_at_ms: Date.now(),
4091
+ to_bundle_id: input.toBundleId,
4092
+ to_release_id: input.toReleaseId,
4093
+ user_id: input.userId ?? null,
4094
+ metadata: {
4095
+ cohort: input.cohort,
4096
+ fingerprint_hash: input.fingerprintHash,
4097
+ sdk_version: input.sdkVersion ?? null,
4098
+ username: input.username ?? null,
4099
+ update_strategy: input.updateStrategy
4100
+ }
4101
+ };
4102
+ switch (input.type) {
4103
+ case "UPDATE_DOWNLOADED":
4104
+ case "UPDATE_APPLIED":
4105
+ case "RECOVERED": return {
4106
+ ...base,
4107
+ from_bundle_id: input.fromBundleId,
4108
+ type: input.type
4109
+ };
4110
+ case "UNCHANGED": return {
4111
+ ...base,
4112
+ from_bundle_id: null,
4113
+ type: input.type
4114
+ };
4115
+ }
4116
+ }
3858
4117
  //#endregion
3859
4118
  //#region ../../packages/server/dist/insights/queryInput.mjs
3860
- const EVENT_LIST_BOUNDS = {
3861
- defaultValue: 50,
3862
- maximum: 100,
3863
- minimum: 1
4119
+ const MAX_PAGE_LIMIT$1 = 100;
4120
+ const MAX_IDENTITY_LENGTH$1 = 255;
4121
+ const MAX_CURSOR_LENGTH$1 = 8192;
4122
+ const readSingle = (url, key) => {
4123
+ const values = url.searchParams.getAll(key);
4124
+ if (values.length > 1) throw new InsightsBadRequestError(`Duplicate '${key}' query parameter.`);
4125
+ return values[0];
3864
4126
  };
3865
- const EVENT_LIST_OFFSET_BOUNDS = {
3866
- defaultValue: 0,
3867
- minimum: 0
4127
+ const readPageLimit = (url) => {
4128
+ const value = readSingle(url, "limit");
4129
+ if (value === void 0) return void 0;
4130
+ const limit = Number(value);
4131
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT$1) throw new InsightsBadRequestError("Invalid 'limit' query parameter.");
4132
+ return limit;
4133
+ };
4134
+ const readCursor = (url) => {
4135
+ const cursor = readSingle(url, "cursor");
4136
+ if (cursor !== void 0 && (cursor.length === 0 || cursor.length > MAX_CURSOR_LENGTH$1)) throw new InsightsBadRequestError("Invalid 'cursor' query parameter.");
4137
+ return cursor;
4138
+ };
4139
+ const readId = (url, key, maximumLength = MAX_IDENTITY_LENGTH$1) => {
4140
+ const value = readSingle(url, key);
4141
+ if (value === void 0 || value.length === 0 || value.length > maximumLength) throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
4142
+ return value;
3868
4143
  };
3869
- const MAX_USER_ID_LENGTH = 1024;
3870
- function parseInteger(url, key, bounds) {
3871
- const value = url.searchParams.get(key);
3872
- if (value === null) return bounds.defaultValue;
3873
- const parsed = Number(value);
3874
- if (!Number.isSafeInteger(parsed) || parsed < bounds.minimum || bounds.maximum !== void 0 && parsed > bounds.maximum) throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
3875
- return parsed;
3876
- }
3877
- const parsePagination = (request) => {
4144
+ const readTimestamp = (url, key) => {
4145
+ const raw = readSingle(url, key);
4146
+ if (raw === void 0) return void 0;
4147
+ const value = Number(raw);
4148
+ if (!raw.length || !Number.isSafeInteger(value) || value < 0) throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
4149
+ return value;
4150
+ };
4151
+ const readScope$1 = (url) => {
4152
+ const platform = readSingle(url, "platform");
4153
+ if (platform !== "ios" && platform !== "android") throw new InsightsBadRequestError("Invalid 'platform' query parameter.");
4154
+ return {
4155
+ platform,
4156
+ channel: readId(url, "channel", 1024)
4157
+ };
4158
+ };
4159
+ const parseEventPageInput = (request) => {
3878
4160
  const url = new URL(request.url);
4161
+ const bundleFields = [
4162
+ "bundleId",
4163
+ "outcome",
4164
+ "platform",
4165
+ "channel"
4166
+ ];
4167
+ let bundle;
4168
+ if (bundleFields.some((key) => url.searchParams.has(key))) {
4169
+ const outcome = readSingle(url, "outcome");
4170
+ if (outcome !== "downloaded" && outcome !== "applied" && outcome !== "recovered" && outcome !== "unchanged") throw new InsightsBadRequestError("Invalid 'outcome' query parameter.");
4171
+ bundle = {
4172
+ ...readScope$1(url),
4173
+ bundleId: readId(url, "bundleId", 1024),
4174
+ outcome
4175
+ };
4176
+ }
3879
4177
  return {
3880
- limit: parseInteger(url, "limit", EVENT_LIST_BOUNDS),
3881
- offset: parseInteger(url, "offset", EVENT_LIST_OFFSET_BOUNDS)
4178
+ beforeReceivedAtMs: readTimestamp(url, "beforeReceivedAtMs"),
4179
+ sinceMs: readTimestamp(url, "sinceMs"),
4180
+ cursor: readCursor(url),
4181
+ limit: readPageLimit(url),
4182
+ ...bundle === void 0 ? {} : { bundle }
3882
4183
  };
3883
4184
  };
3884
- const parseInsightsQuery = (request) => {
3885
- const window = new URL(request.url).searchParams.get("window") ?? "24h";
3886
- if (window !== "24h" && window !== "7d" && window !== "30d" && window !== "all") throw new InsightsBadRequestError("Invalid 'window' query parameter.");
4185
+ const parseUserInstallationPageInput = (request) => {
4186
+ const url = new URL(request.url);
4187
+ const cursor = readCursor(url);
4188
+ const limit = readPageLimit(url);
3887
4189
  return {
3888
- ...parsePagination(request),
3889
- window
4190
+ userId: readId(url, "userId"),
4191
+ ...cursor === void 0 ? {} : { cursor },
4192
+ ...limit === void 0 ? {} : { limit }
3890
4193
  };
3891
4194
  };
3892
- const parseActiveInstallationInput = (request) => {
4195
+ const parseReportingOverviewInput = (request) => {
3893
4196
  const url = new URL(request.url);
3894
- const windows = url.searchParams.getAll("window");
3895
- if (windows.length > 1) throw new InsightsBadRequestError("Duplicate 'window' query parameter.");
3896
- const window = windows[0] ?? "30d";
4197
+ const window = readSingle(url, "window") ?? "30d";
3897
4198
  if (window !== "24h" && window !== "7d" && window !== "30d") throw new InsightsBadRequestError("Invalid 'window' query parameter.");
3898
- const userIds = url.searchParams.getAll("userId");
3899
- if (userIds.length > 1) throw new InsightsBadRequestError("Duplicate 'userId' query parameter.");
3900
- const userId = userIds[0];
3901
- if (userId !== void 0 && (userId.length === 0 || userId.length > MAX_USER_ID_LENGTH)) throw new InsightsBadRequestError("Invalid 'userId' query parameter.");
3902
- return userId === void 0 ? { window } : {
4199
+ const bundleId = readSingle(url, "bundleId");
4200
+ return {
4201
+ ...readScope$1(url),
3903
4202
  window,
3904
- userId
4203
+ ...bundleId === void 0 ? {} : { bundleId: readId(url, "bundleId", 1024) }
3905
4204
  };
3906
4205
  };
3907
- const parseSearchInput = (request) => ({
3908
- ...parsePagination(request),
3909
- query: new URL(request.url).searchParams.get("query")?.trim() ?? ""
3910
- });
3911
4206
  //#endregion
3912
4207
  //#region ../../packages/server/dist/insights/routes.mjs
3913
4208
  const json = (body, status) => Response.json(body, {
@@ -3917,7 +4212,11 @@ const json = (body, status) => Response.json(body, {
3917
4212
  const requireParam = (params, key) => {
3918
4213
  const value = params[key];
3919
4214
  if (value === void 0 || value.length === 0) throw new InsightsBadRequestError(`Missing route parameter: ${key}`);
3920
- return value;
4215
+ try {
4216
+ return decodeURIComponent(value);
4217
+ } catch {
4218
+ throw new InsightsBadRequestError(`Invalid route parameter: ${key}`);
4219
+ }
3921
4220
  };
3922
4221
  const run = async (operation) => {
3923
4222
  try {
@@ -3925,10 +4224,6 @@ const run = async (operation) => {
3925
4224
  } catch (error) {
3926
4225
  if (error instanceof InsightsBadRequestError) return json({ error: error.message }, 400);
3927
4226
  if (error instanceof InsightsPayloadTooLargeError) return json({ error: error.message }, 413);
3928
- if (error instanceof InsightsScanLimitExceededError) return json({ error: {
3929
- code: "INSIGHTS_SCAN_LIMIT_EXCEEDED",
3930
- limit: error.limit
3931
- } }, 503);
3932
4227
  throw error;
3933
4228
  }
3934
4229
  };
@@ -3938,32 +4233,27 @@ const createInsightsRouteHandlers = (provider) => ({
3938
4233
  await provider.appendBundleEvent(await parseBundleEventRequest(request));
3939
4234
  return new Response(null, { status: 204 });
3940
4235
  }),
3941
- getBundleEventSummary: (params) => query(() => provider.getBundleEventSummary(requireParam(params, "id"))),
3942
- getBundleEventInsights: (params, request) => query(() => {
3943
- const input = parseInsightsQuery(request);
3944
- return provider.getBundleEventInsights(requireParam(params, "id"), input.window, input.limit, input.offset);
4236
+ getReportingOverview: (_params, request) => query(() => provider.getReportingOverview(parseReportingOverviewInput(request))),
4237
+ getInstallation: (params) => run(async () => {
4238
+ const installation = await provider.getInstallation({ installId: requireParam(params, "installId") });
4239
+ return installation === null ? json({ error: "Installation not found" }, 404) : json(installation, 200);
3945
4240
  }),
3946
- getBundleEventOverview: () => query(() => provider.getBundleEventOverview()),
3947
- getActiveInstallationOverview: (_params, request) => query(() => provider.getActiveInstallationOverview(parseActiveInstallationInput(request))),
3948
- searchInstallations: (_params, request) => query(() => {
3949
- const input = parseSearchInput(request);
3950
- return provider.searchInstallations(input.query, input.limit, input.offset);
3951
- }),
3952
- getInstallationHistory: (params, request) => query(() => {
3953
- const input = parsePagination(request);
3954
- return provider.getInstallationHistory(requireParam(params, "installId"), input.limit, input.offset);
3955
- })
4241
+ listEvents: (_params, request) => query(() => provider.listEvents(parseEventPageInput(request))),
4242
+ listInstallationEvents: (params, request) => query(() => provider.listInstallationEvents({
4243
+ ...parseEventPageInput(request),
4244
+ installId: requireParam(params, "installId")
4245
+ })),
4246
+ pageInstallationsByCurrentUserId: (_params, request) => query(() => provider.pageInstallationsByCurrentUserId(parseUserInstallationPageInput(request)))
3956
4247
  });
3957
4248
  const registerInsightsClientRoutes = (add) => {
3958
4249
  add("POST", "/events", "appendBundleEvent");
3959
4250
  };
3960
4251
  const registerInsightsAdminRoutes = (add) => {
3961
- add("GET", "/bundles/:id/events/summary", "getBundleEventSummary");
3962
- add("GET", "/bundles/:id/events/insights", "getBundleEventInsights");
3963
- add("GET", "/installations/overview", "getBundleEventOverview");
3964
- add("GET", "/installations/active", "getActiveInstallationOverview");
3965
- add("GET", "/installations", "searchInstallations");
3966
- add("GET", "/installations/:installId/events", "getInstallationHistory");
4252
+ add("GET", "/events", "listEvents");
4253
+ add("GET", "/overview", "getReportingOverview");
4254
+ add("GET", "/installations", "pageInstallationsByCurrentUserId");
4255
+ add("GET", "/installations/:installId/events", "listInstallationEvents");
4256
+ add("GET", "/installations/:installId", "getInstallation");
3967
4257
  };
3968
4258
  //#endregion
3969
4259
  //#region ../../packages/server/dist/internalRouter.mjs
@@ -4125,461 +4415,329 @@ function createHotUpdaterHandlers(api, insights, apiKeyAuth, downloadStorageObje
4125
4415
  });
4126
4416
  }
4127
4417
  //#endregion
4128
- //#region ../../packages/server/dist/insights/bounded/activeOverview.mjs
4129
- const HOUR_MS = 3600 * 1e3;
4130
- const DAY_MS = 24 * HOUR_MS;
4131
- const ACTIVE_BUNDLE_EVENT_TYPES = [
4132
- "UPDATE_APPLIED",
4133
- "RECOVERED",
4134
- "RELEASE_ADOPTED",
4135
- "UNCHANGED"
4136
- ];
4137
- const activeWindowDefinitions = {
4138
- "24h": {
4139
- bucketCount: 24,
4140
- bucketSizeMs: HOUR_MS
4141
- },
4142
- "7d": {
4143
- bucketCount: 7,
4144
- bucketSizeMs: DAY_MS
4145
- },
4146
- "30d": {
4147
- bucketCount: 30,
4148
- bucketSizeMs: DAY_MS
4149
- }
4418
+ //#region ../../packages/server/dist/insights/provider.mjs
4419
+ const DEFAULT_PAGE_LIMIT = 50;
4420
+ const MAX_PAGE_LIMIT = 100;
4421
+ const MAX_EVENT_ID_LENGTH = 1024;
4422
+ const MAX_IDENTITY_LENGTH = 255;
4423
+ const MAX_CURSOR_LENGTH = 8192;
4424
+ const WINDOW_MS = {
4425
+ "24h": 864e5,
4426
+ "7d": 6048e5,
4427
+ "30d": 2592e6
4428
+ };
4429
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4430
+ const requireString = (value, label, maximumLength) => {
4431
+ 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}.`);
4432
+ return value;
4150
4433
  };
4151
- function getActiveWindowDefinition(window) {
4152
- return activeWindowDefinitions[window];
4153
- }
4154
- function compareCodePoints$1(left, right) {
4155
- if (left < right) return -1;
4156
- if (left > right) return 1;
4157
- return 0;
4158
- }
4159
- function isNewer(candidate, current) {
4160
- return candidate.received_at_ms > current.received_at_ms || candidate.received_at_ms === current.received_at_ms && compareCodePoints$1(candidate.id, current.id) > 0;
4161
- }
4162
- function collectActiveInstallationOverview(request) {
4163
- const definition = getActiveWindowDefinition(request.window);
4164
- const windowStartMs = request.asOfMs - definition.bucketCount * definition.bucketSizeMs;
4165
- const rows = request.rows.filter((row) => row.received_at_ms >= windowStartMs && row.received_at_ms < request.asOfMs);
4166
- const latestByInstall = /* @__PURE__ */ new Map();
4167
- for (const row of rows) {
4168
- const current = latestByInstall.get(row.install_id);
4169
- if (current === void 0 || isNewer(row, current)) latestByInstall.set(row.install_id, row);
4434
+ const requireTimestamp = (value, label) => {
4435
+ if (!Number.isSafeInteger(value) || Number(value) < 0) throw new InsightsBadRequestError(`Invalid ${label}.`);
4436
+ return Number(value);
4437
+ };
4438
+ const readLimit = (value) => {
4439
+ const limit = value ?? DEFAULT_PAGE_LIMIT;
4440
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) throw new InsightsBadRequestError("Invalid page limit.");
4441
+ return limit;
4442
+ };
4443
+ const encodeBase64Url = (value) => {
4444
+ const bytes = new TextEncoder().encode(value);
4445
+ let binary = "";
4446
+ for (const byte of bytes) binary += String.fromCharCode(byte);
4447
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
4448
+ };
4449
+ const decodeBase64Url = (value) => {
4450
+ if (value.length === 0 || value.length > MAX_CURSOR_LENGTH) throw new InsightsBadRequestError("Invalid Insights cursor.");
4451
+ try {
4452
+ const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
4453
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
4454
+ const binary = atob(padded);
4455
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
4456
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
4457
+ } catch {
4458
+ throw new InsightsBadRequestError("Invalid Insights cursor.");
4170
4459
  }
4171
- const selectedRows = [...latestByInstall.values()].filter((row) => request.userId === void 0 || row.user_id === request.userId);
4172
- const selectedInstallIds = new Set(selectedRows.map((row) => row.install_id));
4173
- const bundleCounts = /* @__PURE__ */ new Map();
4174
- for (const row of selectedRows) bundleCounts.set(row.to_bundle_id, (bundleCounts.get(row.to_bundle_id) ?? 0) + 1);
4175
- const latestByBucket = Array.from({ length: definition.bucketCount }, () => /* @__PURE__ */ new Map());
4176
- for (const row of rows) {
4177
- if (!selectedInstallIds.has(row.install_id)) continue;
4178
- const bucket = latestByBucket[Math.floor((row.received_at_ms - windowStartMs) / definition.bucketSizeMs)];
4179
- if (bucket === void 0) continue;
4180
- const current = bucket.get(row.install_id);
4181
- if (current === void 0 || isNewer(row, current)) bucket.set(row.install_id, row);
4460
+ };
4461
+ const encodeCursor = (value) => encodeBase64Url(JSON.stringify(value));
4462
+ const decodeCursor = (value) => {
4463
+ try {
4464
+ return JSON.parse(decodeBase64Url(value));
4465
+ } catch (error) {
4466
+ if (error instanceof InsightsBadRequestError) throw error;
4467
+ throw new InsightsBadRequestError("Invalid Insights cursor.");
4182
4468
  }
4183
- const bundleCountsByBucket = latestByBucket.map((bucket) => {
4184
- const counts = /* @__PURE__ */ new Map();
4185
- for (const row of bucket.values()) counts.set(row.to_bundle_id, (counts.get(row.to_bundle_id) ?? 0) + 1);
4186
- return counts;
4187
- });
4188
- const bundleObservationTotals = /* @__PURE__ */ new Map();
4189
- for (const counts of bundleCountsByBucket) for (const [bundleId, count] of counts) bundleObservationTotals.set(bundleId, (bundleObservationTotals.get(bundleId) ?? 0) + count);
4469
+ };
4470
+ const readScope = (input) => {
4471
+ if (input.platform !== "ios" && input.platform !== "android") throw new InsightsBadRequestError("Invalid Insights platform.");
4190
4472
  return {
4191
- asOfMs: request.asOfMs,
4192
- window: request.window,
4193
- activeInstallations: selectedInstallIds.size,
4194
- series: latestByBucket.map((bucket, index) => ({
4195
- bucketStartMs: windowStartMs + index * definition.bucketSizeMs,
4196
- value: bucket.size
4197
- })),
4198
- bundleSeries: [...bundleObservationTotals].sort(([leftId, leftTotal], [rightId, rightTotal]) => rightTotal - leftTotal || compareCodePoints$1(leftId, rightId)).map(([bundleId]) => ({
4199
- bundleId,
4200
- series: bundleCountsByBucket.map((counts, index) => ({
4201
- bucketStartMs: windowStartMs + index * definition.bucketSizeMs,
4202
- value: counts.get(bundleId) ?? 0
4203
- }))
4204
- })),
4205
- bundles: [...bundleCounts].map(([bundleId, installations]) => ({
4206
- bundleId,
4207
- installations
4208
- })).sort((left, right) => right.installations - left.installations || compareCodePoints$1(left.bundleId, right.bundleId))
4209
- };
4210
- }
4211
- //#endregion
4212
- //#region ../../packages/server/dist/insights/bounded/scan.mjs
4213
- const INSIGHTS_SCAN_MAX_ROWS = 5e4;
4214
- const INSIGHTS_MATERIALIZATION_LIMIT = INSIGHTS_SCAN_MAX_ROWS + 1;
4215
- const INSIGHTS_SCAN_PAGE_SIZE = 1e3;
4216
- const INSIGHTS_LOWER_BOUND_ID = "00000000-0000-0000-0000-000000000000";
4217
- const compareCodePoints = (left, right) => {
4218
- if (left < right) return -1;
4219
- if (left > right) return 1;
4220
- return 0;
4221
- };
4222
- const compareEventNewest = (left, right) => right.received_at_ms - left.received_at_ms || compareCodePoints(right.id, left.id);
4223
- const compareEventOldest = (left, right) => left.received_at_ms - right.received_at_ms || compareCodePoints(left.id, right.id);
4224
- const materializeEventRows = async (scope) => {
4225
- const rows = [];
4226
- const seenIds = /* @__PURE__ */ new Set();
4227
- let after = scope.lowerBoundMs === void 0 ? void 0 : {
4228
- receivedAtMs: scope.lowerBoundMs,
4229
- id: INSIGHTS_LOWER_BOUND_ID
4473
+ platform: input.platform,
4474
+ channel: requireString(input.channel, "channel", MAX_EVENT_ID_LENGTH)
4230
4475
  };
4231
- while (rows.length < INSIGHTS_MATERIALIZATION_LIMIT) {
4232
- const limit = Math.min(INSIGHTS_SCAN_PAGE_SIZE, INSIGHTS_MATERIALIZATION_LIMIT - rows.length);
4233
- const page = await scope.persistence.scan({
4234
- beforeReceivedAtMs: scope.cutoffMs,
4235
- ...after === void 0 ? {} : { after },
4236
- limit
4237
- });
4238
- if (page.length === 0) break;
4239
- if (page.length > limit) throw new InsightsPersistenceOrderError();
4240
- let previous = after === void 0 ? void 0 : {
4241
- received_at_ms: after.receivedAtMs,
4242
- id: after.id
4476
+ };
4477
+ const bundleFilter = (input) => {
4478
+ const scope = readScope(input);
4479
+ const bundleId = requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
4480
+ switch (input.outcome) {
4481
+ case "downloaded": return {
4482
+ ...scope,
4483
+ type: "UPDATE_DOWNLOADED",
4484
+ toBundleId: bundleId
4243
4485
  };
4244
- for (const row of page) {
4245
- if (row.received_at_ms >= scope.cutoffMs || previous !== void 0 && compareEventOldest(previous, row) >= 0 || seenIds.has(row.id)) throw new InsightsPersistenceOrderError();
4246
- seenIds.add(row.id);
4247
- rows.push(row);
4248
- previous = row;
4249
- }
4250
- const last = page.at(-1);
4251
- if (last !== void 0) after = {
4252
- receivedAtMs: last.received_at_ms,
4253
- id: last.id
4486
+ case "applied": return {
4487
+ ...scope,
4488
+ type: "UPDATE_APPLIED",
4489
+ toBundleId: bundleId
4490
+ };
4491
+ case "recovered": return {
4492
+ ...scope,
4493
+ type: "RECOVERED",
4494
+ fromBundleId: bundleId
4254
4495
  };
4255
- if (rows.length > 5e4) throw new InsightsScanLimitExceededError(INSIGHTS_SCAN_MAX_ROWS);
4496
+ case "unchanged": return {
4497
+ ...scope,
4498
+ type: "UNCHANGED",
4499
+ toBundleId: bundleId
4500
+ };
4501
+ default: throw new InsightsBadRequestError("Invalid Insights outcome.");
4256
4502
  }
4257
- return rows;
4258
4503
  };
4259
- var InsightsPersistenceOrderError = class extends Error {
4260
- name = "InsightsPersistenceOrderError";
4261
- constructor() {
4262
- super("Insights persistence did not advance its scan cursor.");
4504
+ const sameFilter = (left, right) => {
4505
+ if (!isRecord(left) || left.kind !== right.kind) return false;
4506
+ switch (right.kind) {
4507
+ case "all": return true;
4508
+ case "installationMovement": return left.installId === right.installId;
4509
+ 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);
4263
4510
  }
4264
4511
  };
4265
- const materializeActiveRows = async (scope, window) => {
4266
- const definition = getActiveWindowDefinition(window);
4267
- const durationMs = definition.bucketCount * definition.bucketSizeMs;
4268
- return (await materializeEventRows({
4269
- ...scope,
4270
- lowerBoundMs: scope.cutoffMs - durationMs
4271
- })).filter((row) => row.received_at_ms >= scope.cutoffMs - durationMs && ACTIVE_BUNDLE_EVENT_TYPES.includes(row.type));
4272
- };
4273
- const startOfUtcHour = (value) => {
4274
- const date = new Date(value);
4275
- return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours());
4276
- };
4277
- const startOfUtcDay = (value) => {
4278
- const date = new Date(value);
4279
- return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
4280
- };
4281
- const getWindowRange = (window, now) => {
4282
- if (window === "24h") return {
4283
- sizeMs: 3600 * 1e3,
4284
- rangeStart: startOfUtcHour(now) - 1380 * 60 * 1e3
4285
- };
4286
- const days = window === "7d" ? 7 : 30;
4512
+ const readEventCursor = (value, filter) => {
4513
+ const cursor = decodeCursor(value);
4514
+ if (!isRecord(cursor) || cursor.version !== 2 || cursor.kind !== "events" || !isRecord(cursor.filter) || !isRecord(cursor.after)) throw new InsightsBadRequestError("Invalid Insights cursor.");
4515
+ if (!sameFilter(cursor.filter, filter)) throw new InsightsBadRequestError("Insights cursor does not match the requested events.");
4516
+ if (typeof cursor.after.id !== "string" || !isUUIDv7(cursor.after.id)) throw new InsightsBadRequestError("Invalid Insights event cursor ID.");
4287
4517
  return {
4288
- sizeMs: 1440 * 60 * 1e3,
4289
- rangeStart: startOfUtcDay(now) - (days - 1) * 24 * 60 * 60 * 1e3
4518
+ after: {
4519
+ id: cursor.after.id,
4520
+ receivedAtMs: requireTimestamp(cursor.after.receivedAtMs, "event cursor")
4521
+ },
4522
+ beforeReceivedAtMs: requireTimestamp(cursor.beforeReceivedAtMs, "event cutoff"),
4523
+ kind: "events",
4524
+ sinceMs: requireTimestamp(cursor.sinceMs, "event start"),
4525
+ filter,
4526
+ version: 2
4290
4527
  };
4291
4528
  };
4292
- const materializeRowsForWindow = async (scope, window) => {
4293
- if (window === "all") return materializeEventRows(scope);
4294
- const range = getWindowRange(window, scope.cutoffMs);
4295
- return (await materializeEventRows({
4296
- ...scope,
4297
- lowerBoundMs: range.rangeStart
4298
- })).filter(({ received_at_ms }) => received_at_ms >= range.rangeStart);
4299
- };
4300
- const bucketStart = (receivedAtMs, sizeMs) => sizeMs === 3600 * 1e3 ? startOfUtcHour(receivedAtMs) : startOfUtcDay(receivedAtMs);
4301
- const createSeries = (request) => {
4302
- const range = request.window === "all" ? void 0 : getWindowRange(request.window, request.cutoffMs);
4303
- const sizeMs = range?.sizeMs ?? 1440 * 60 * 1e3;
4304
- const installIdsByBucket = /* @__PURE__ */ new Map();
4305
- let oldestMs = request.cutoffMs;
4306
- for (const row of request.rows) {
4307
- oldestMs = Math.min(oldestMs, row.received_at_ms);
4308
- const start = bucketStart(row.received_at_ms, sizeMs);
4309
- const installIds = installIdsByBucket.get(start) ?? /* @__PURE__ */ new Set();
4310
- installIds.add(row.install_id);
4311
- installIdsByBucket.set(start, installIds);
4312
- }
4313
- const first = range?.rangeStart ?? startOfUtcDay(oldestMs);
4314
- const last = bucketStart(request.cutoffMs, sizeMs);
4315
- return Array.from({ length: Math.floor((last - first) / sizeMs) + 1 }, (_, index) => {
4316
- const start = first + index * sizeMs;
4317
- return {
4318
- bucketStartMs: start,
4319
- value: installIdsByBucket.get(start)?.size ?? 0
4320
- };
4321
- });
4322
- };
4323
- const collectEventActivity = (request) => {
4324
- const range = request.window === "all" ? void 0 : getWindowRange(request.window, request.cutoffMs);
4325
- const rows = range === void 0 ? request.rows : request.rows.filter(({ received_at_ms }) => received_at_ms >= range.rangeStart && received_at_ms < request.cutoffMs);
4326
- const installs = /* @__PURE__ */ new Set();
4327
- const installsByCohort = /* @__PURE__ */ new Map();
4328
- for (const row of rows) {
4329
- installs.add(row.install_id);
4330
- const cohort = installsByCohort.get(row.cohort) ?? /* @__PURE__ */ new Set();
4331
- cohort.add(row.install_id);
4332
- installsByCohort.set(row.cohort, cohort);
4333
- }
4529
+ const readUserInstallationCursor = (value, userId) => {
4530
+ const cursor = decodeCursor(value);
4531
+ if (!isRecord(cursor) || cursor.version !== 1 || cursor.kind !== "user-installations" || cursor.userId !== userId) throw new InsightsBadRequestError("Insights cursor does not match the requested user ID.");
4334
4532
  return {
4335
- summary: installs.size,
4336
- cohorts: [...installsByCohort].sort(([left], [right]) => compareCodePoints(left, right)).map(([cohort, installIds]) => ({
4337
- cohort,
4338
- value: installIds.size
4339
- })),
4340
- series: createSeries({
4341
- ...request,
4342
- rows
4343
- })
4533
+ afterInstallId: requireString(cursor.afterInstallId, "installation cursor", MAX_IDENTITY_LENGTH),
4534
+ kind: "user-installations",
4535
+ userId,
4536
+ version: 1
4344
4537
  };
4345
4538
  };
4346
- //#endregion
4347
- //#region ../../packages/server/dist/insights/bounded/installationSearch.mjs
4348
- function toSearchRow(row) {
4349
- return {
4350
- installId: row.install_id,
4351
- username: row.username,
4352
- userId: row.user_id,
4353
- lastKnownBundleId: row.to_bundle_id,
4354
- latestStatus: row.type,
4355
- platform: row.platform,
4356
- appVersion: row.app_version,
4357
- channel: row.channel,
4358
- cohort: row.cohort,
4359
- receivedAtMs: row.received_at_ms
4360
- };
4361
- }
4362
- function matchesIdentity(row, query) {
4363
- return row.install_id.toLowerCase().includes(query) || row.user_id?.toLowerCase().includes(query) === true || row.username?.toLowerCase().includes(query) === true;
4364
- }
4365
- function searchEventInstallations(request) {
4366
- const query = request.query.toLowerCase();
4367
- const matchingInstallIds = /* @__PURE__ */ new Set();
4368
- for (const row of request.rows) if (query.length === 0 || matchesIdentity(row, query)) matchingInstallIds.add(row.install_id);
4369
- const latestByInstall = /* @__PURE__ */ new Map();
4370
- for (const row of request.rows) {
4371
- if (!matchingInstallIds.has(row.install_id)) continue;
4372
- const current = latestByInstall.get(row.install_id);
4373
- if (current === void 0 || compareEventNewest(row, current) < 0) latestByInstall.set(row.install_id, row);
4539
+ const compareEventNewest = (left, right) => right.received_at_ms - left.received_at_ms || compareInsightsText(right.id, left.id);
4540
+ const isAfterEventCursor = (row, after) => row.received_at_ms < after.receivedAtMs || row.received_at_ms === after.receivedAtMs && compareInsightsText(row.id, after.id) < 0;
4541
+ const assertEventRows = (rows, input) => {
4542
+ if (rows.length > input.limit) throw new Error("Insights database returned too many event rows.");
4543
+ for (let index = 0; index < rows.length; index += 1) {
4544
+ const row = rows[index];
4545
+ const previous = rows[index - 1];
4546
+ 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.");
4374
4547
  }
4375
- const matchingRows = [...latestByInstall.values()].sort((left, right) => compareCodePoints(left.install_id, right.install_id));
4376
- const pageSize = Math.min(Math.max(request.limit, 0), 100);
4377
- return {
4378
- data: matchingRows.slice(request.offset, request.offset + pageSize).map(toSearchRow),
4379
- pagination: {
4380
- total: matchingRows.length,
4381
- limit: request.limit,
4382
- offset: request.offset
4383
- }
4384
- };
4385
- }
4386
- //#endregion
4387
- //#region ../../packages/server/dist/insights/bounded/persistence.mjs
4388
- function createBundleEventRow(input) {
4389
- const base = {
4390
- id: createUUIDv7(),
4391
- install_id: input.installId,
4392
- user_id: input.userId ?? null,
4393
- username: input.username ?? null,
4394
- from_release_id: input.fromReleaseId,
4395
- to_release_id: input.toReleaseId,
4396
- to_bundle_id: input.toBundleId,
4397
- platform: input.platform,
4398
- app_version: input.appVersion,
4399
- channel: input.channel,
4400
- cohort: input.cohort,
4401
- fingerprint_hash: input.fingerprintHash,
4402
- sdk_version: input.sdkVersion ?? null,
4403
- received_at_ms: Date.now()
4404
- };
4405
- switch (input.type) {
4406
- case "UPDATE_APPLIED":
4407
- case "RECOVERED":
4408
- case "RELEASE_ADOPTED": return {
4409
- ...base,
4410
- type: input.type,
4411
- from_bundle_id: input.fromBundleId,
4412
- update_strategy: input.updateStrategy
4413
- };
4414
- case "UNCHANGED": return {
4415
- ...base,
4416
- type: input.type,
4417
- from_bundle_id: null,
4418
- update_strategy: null
4419
- };
4420
- }
4421
- }
4422
- //#endregion
4423
- //#region ../../packages/server/dist/insights/bounded/provider.mjs
4424
- const isTransitionEventRow = (row) => row.type === "UPDATE_APPLIED" || row.type === "RECOVERED";
4425
- const toHistoryRow = (row) => ({
4426
- id: row.id,
4427
- type: row.type,
4548
+ };
4549
+ const toEventHistoryRow = (row) => ({
4550
+ appVersion: row.app_version,
4551
+ channel: row.channel,
4552
+ cohort: row.metadata.cohort,
4428
4553
  fromBundleId: row.from_bundle_id,
4554
+ id: row.id,
4555
+ installId: row.install_id,
4556
+ platform: row.platform,
4557
+ receivedAtMs: row.received_at_ms,
4429
4558
  toBundleId: row.to_bundle_id,
4430
- username: row.username,
4559
+ type: row.type,
4431
4560
  userId: row.user_id,
4432
- platform: row.platform,
4561
+ username: row.metadata.username
4562
+ });
4563
+ const toInstallationRow = (row) => ({
4433
4564
  appVersion: row.app_version,
4434
4565
  channel: row.channel,
4435
- cohort: row.cohort,
4436
- receivedAtMs: row.received_at_ms
4566
+ cohort: row.metadata.cohort,
4567
+ installId: row.install_id,
4568
+ lastKnownBundleId: row.type === "UPDATE_DOWNLOADED" ? row.from_bundle_id : row.to_bundle_id,
4569
+ pendingBundleId: row.type === "UPDATE_DOWNLOADED" ? row.to_bundle_id : null,
4570
+ pendingReleaseId: row.type === "UPDATE_DOWNLOADED" ? row.to_release_id : null,
4571
+ latestStatus: row.type,
4572
+ platform: row.platform,
4573
+ receivedAtMs: row.received_at_ms,
4574
+ userId: row.user_id,
4575
+ username: row.metadata.username
4437
4576
  });
4438
- const isInstalledForBundle = (row, bundleId) => row.type === "UPDATE_APPLIED" && row.to_bundle_id === bundleId;
4439
- const isRecoveredFromBundle = (row, bundleId) => row.type === "RECOVERED" && row.from_bundle_id === bundleId;
4440
- const countDistinctInstallations = (rows) => new Set(rows.map(({ install_id }) => install_id)).size;
4441
- const getInsightsResult = async (persistence, bundleId, window, limit, offset) => {
4442
- const scope = {
4443
- persistence,
4444
- cutoffMs: Date.now()
4577
+ const pageEventRows = async (model, input, filter, map) => {
4578
+ const limit = readLimit(input.limit);
4579
+ const cursor = input.cursor === void 0 ? void 0 : readEventCursor(input.cursor, filter);
4580
+ const beforeReceivedAtMs = cursor?.beforeReceivedAtMs ?? (input.beforeReceivedAtMs === void 0 ? Date.now() : requireTimestamp(input.beforeReceivedAtMs, "event cutoff"));
4581
+ if (cursor !== void 0 && input.beforeReceivedAtMs !== void 0 && input.beforeReceivedAtMs !== beforeReceivedAtMs) throw new InsightsBadRequestError("Insights cursor does not match the requested event cutoff.");
4582
+ const sinceMs = cursor?.sinceMs ?? (input.sinceMs === void 0 ? 0 : requireTimestamp(input.sinceMs, "event start"));
4583
+ if (sinceMs > beforeReceivedAtMs || input.sinceMs !== void 0 && input.sinceMs !== sinceMs || cursor !== void 0 && (cursor.after.receivedAtMs < sinceMs || cursor.after.receivedAtMs >= beforeReceivedAtMs)) throw new InsightsBadRequestError("Insights cursor or range does not match the requested event start.");
4584
+ const databaseInput = {
4585
+ filter,
4586
+ sinceMs,
4587
+ beforeReceivedAtMs,
4588
+ ...cursor === void 0 ? {} : { after: cursor.after },
4589
+ limit: limit + 1
4445
4590
  };
4446
- const rows = (await materializeRowsForWindow(scope, window)).filter(isTransitionEventRow);
4447
- const installedRows = rows.filter((row) => isInstalledForBundle(row, bundleId));
4448
- const recoveredRows = rows.filter((row) => isRecoveredFromBundle(row, bundleId));
4449
- const installed = collectEventActivity({
4450
- rows: installedRows,
4451
- window,
4452
- cutoffMs: scope.cutoffMs
4453
- });
4454
- const recovered = collectEventActivity({
4455
- rows: recoveredRows,
4456
- window,
4457
- cutoffMs: scope.cutoffMs
4458
- });
4459
- const recentRows = [...installedRows, ...recoveredRows].sort(compareEventNewest);
4591
+ const rows = await model.listEvents(databaseInput);
4592
+ assertEventRows(rows, databaseInput);
4593
+ const pageRows = rows.slice(0, limit);
4594
+ const last = pageRows.at(-1);
4460
4595
  return {
4461
- summary: {
4462
- installed: installed.summary,
4463
- recovered: recovered.summary
4464
- },
4465
- series: {
4466
- installed: installed.series,
4467
- recovered: recovered.series
4468
- },
4469
- cohorts: {
4470
- installed: installed.cohorts,
4471
- recovered: recovered.cohorts
4472
- },
4473
- recentEvents: {
4474
- data: recentRows.slice(offset, offset + limit).map(toHistoryRow),
4475
- pagination: {
4476
- total: recentRows.length,
4477
- limit,
4478
- offset
4479
- }
4480
- }
4596
+ beforeReceivedAtMs,
4597
+ data: pageRows.map(map),
4598
+ nextCursor: rows.length > limit && last ? encodeCursor({
4599
+ after: {
4600
+ id: last.id,
4601
+ receivedAtMs: last.received_at_ms
4602
+ },
4603
+ beforeReceivedAtMs,
4604
+ kind: "events",
4605
+ filter,
4606
+ sinceMs,
4607
+ version: 2
4608
+ }) : null
4481
4609
  };
4482
4610
  };
4483
- const getInsightsSummaries = async (persistence, bundleIds, window) => {
4484
- const normalizedBundleIds = [...new Set(bundleIds)];
4485
- if (normalizedBundleIds.length === 0) return [];
4486
- const requestedBundleIds = new Set(normalizedBundleIds);
4487
- const installedByBundleId = /* @__PURE__ */ new Map();
4488
- const recoveredByBundleId = /* @__PURE__ */ new Map();
4489
- const rows = (await materializeRowsForWindow({
4490
- persistence,
4491
- cutoffMs: Date.now()
4492
- }, window)).filter(isTransitionEventRow);
4493
- for (const row of rows) {
4494
- const bundleId = row.type === "UPDATE_APPLIED" ? row.to_bundle_id : row.from_bundle_id;
4495
- if (!requestedBundleIds.has(bundleId)) continue;
4496
- const counts = row.type === "UPDATE_APPLIED" ? installedByBundleId : recoveredByBundleId;
4497
- const installIds = counts.get(bundleId) ?? /* @__PURE__ */ new Set();
4498
- installIds.add(row.install_id);
4499
- counts.set(bundleId, installIds);
4611
+ const assertInstallationRows = (rows, input) => {
4612
+ if (rows.length > input.limit) throw new Error("Insights database returned too many installation rows.");
4613
+ for (let index = 0; index < rows.length; index += 1) {
4614
+ const row = rows[index];
4615
+ const previous = rows[index - 1];
4616
+ 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.");
4500
4617
  }
4501
- return normalizedBundleIds.map((bundleId) => ({
4502
- bundleId,
4503
- installed: installedByBundleId.get(bundleId)?.size ?? 0,
4504
- recovered: recoveredByBundleId.get(bundleId)?.size ?? 0
4505
- }));
4506
4618
  };
4507
- const createInsightsProvider = (persistence) => Object.freeze({
4508
- mode: "bounded",
4509
- maxMatchingRows: INSIGHTS_SCAN_MAX_ROWS,
4619
+ const createInsightsProvider = (model) => Object.freeze({
4510
4620
  async appendBundleEvent(input) {
4511
- await persistence.append(createBundleEventRow(input));
4621
+ const event = createBundleEventRow(input);
4622
+ await model.recordEvent({ event });
4512
4623
  },
4513
- async getBundleEventSummary(bundleId) {
4514
- const rows = (await materializeEventRows({
4515
- persistence,
4516
- cutoffMs: Date.now()
4517
- })).filter(isTransitionEventRow);
4518
- return {
4519
- installed: countDistinctInstallations(rows.filter((row) => isInstalledForBundle(row, bundleId))),
4520
- recovered: countDistinctInstallations(rows.filter((row) => isRecoveredFromBundle(row, bundleId)))
4624
+ listEvents(input) {
4625
+ const filter = input.bundle === void 0 ? { kind: "all" } : {
4626
+ kind: "bundle",
4627
+ ...bundleFilter(input.bundle)
4521
4628
  };
4629
+ return pageEventRows(model, input, filter, toEventHistoryRow);
4522
4630
  },
4523
- getBundleEventSummaries(bundleIds, window) {
4524
- return getInsightsSummaries(persistence, bundleIds, window);
4631
+ async listInstallationEvents(input) {
4632
+ if ("bundle" in input && input.bundle !== void 0) throw new InsightsBadRequestError("Installation movement queries cannot include a bundle filter.");
4633
+ const installId = requireString(input.installId, "install ID", MAX_IDENTITY_LENGTH);
4634
+ return pageEventRows(model, input, {
4635
+ kind: "installationMovement",
4636
+ installId
4637
+ }, (row) => toEventHistoryRow(row));
4525
4638
  },
4526
- getBundleEventInsights(bundleId, window, limit, offset) {
4527
- return getInsightsResult(persistence, bundleId, window, limit, offset);
4639
+ async getInstallation({ installId }) {
4640
+ const normalizedInstallId = requireString(installId, "install ID", MAX_IDENTITY_LENGTH);
4641
+ const rows = await model.findLatestEvents({ installId: normalizedInstallId });
4642
+ const row = rows[0] ?? null;
4643
+ if (rows.length > 1 || row !== null && row.install_id !== normalizedInstallId) throw new Error("Insights database returned an invalid installation.");
4644
+ return row === null ? null : toInstallationRow(row);
4528
4645
  },
4529
- async getBundleEventOverview() {
4530
- const rows = await materializeEventRows({
4531
- persistence,
4532
- cutoffMs: Date.now()
4533
- });
4534
- const latestByInstall = /* @__PURE__ */ new Map();
4535
- for (const row of rows) {
4536
- const current = latestByInstall.get(row.install_id);
4537
- if (current === void 0 || compareEventNewest(row, current) < 0) latestByInstall.set(row.install_id, row);
4538
- }
4539
- const counts = /* @__PURE__ */ new Map();
4540
- for (const row of latestByInstall.values()) counts.set(row.to_bundle_id, (counts.get(row.to_bundle_id) ?? 0) + 1);
4646
+ async pageInstallationsByCurrentUserId(input) {
4647
+ const userId = requireString(input.userId, "user ID", MAX_IDENTITY_LENGTH);
4648
+ const limit = readLimit(input.limit);
4649
+ const cursor = input.cursor === void 0 ? void 0 : readUserInstallationCursor(input.cursor, userId);
4650
+ const databaseInput = {
4651
+ userId,
4652
+ ...cursor === void 0 ? {} : { afterInstallId: cursor.afterInstallId },
4653
+ limit: limit + 1
4654
+ };
4655
+ const rows = await model.findLatestEvents(databaseInput);
4656
+ assertInstallationRows(rows, databaseInput);
4657
+ const pageRows = rows.slice(0, limit);
4658
+ const last = pageRows.at(-1);
4541
4659
  return {
4542
- trackedInstallations: latestByInstall.size,
4543
- bundles: [...counts].map(([bundleId, installations]) => ({
4544
- bundleId,
4545
- installations
4546
- })).sort((left, right) => right.installations - left.installations || compareCodePoints(left.bundleId, right.bundleId))
4660
+ data: pageRows.map(toInstallationRow),
4661
+ nextCursor: rows.length > limit && last ? encodeCursor({
4662
+ afterInstallId: last.install_id,
4663
+ kind: "user-installations",
4664
+ userId,
4665
+ version: 1
4666
+ }) : null
4547
4667
  };
4548
4668
  },
4549
- async getActiveInstallationOverview(input) {
4550
- const asOfMs = Date.now();
4551
- return collectActiveInstallationOverview({
4552
- rows: await materializeActiveRows({
4553
- persistence,
4554
- cutoffMs: asOfMs
4555
- }, input.window),
4556
- asOfMs,
4557
- window: input.window,
4558
- ...input.userId === void 0 ? {} : { userId: input.userId }
4559
- });
4560
- },
4561
- async searchInstallations(query, limit, offset) {
4562
- return searchEventInstallations({
4563
- rows: await materializeEventRows({
4564
- persistence,
4565
- cutoffMs: Date.now()
4669
+ async getReportingOverview(input) {
4670
+ const scope = readScope(input);
4671
+ const { window } = input;
4672
+ if (!Object.hasOwn(WINDOW_MS, window)) throw new InsightsBadRequestError("Invalid reporting installation window.");
4673
+ const beforeReceivedAtMs = Date.now();
4674
+ const sinceMs = Math.max(0, beforeReceivedAtMs - WINDOW_MS[window]);
4675
+ const measure = async (count) => {
4676
+ const value = await count;
4677
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error("Insights database returned an invalid count.");
4678
+ return {
4679
+ count: value,
4680
+ measuredAtMs: Date.now()
4681
+ };
4682
+ };
4683
+ const bundleId = input.bundleId === void 0 ? void 0 : requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
4684
+ const reporting = measure(model.countLatestEvents({
4685
+ ...scope,
4686
+ sinceMs
4687
+ }));
4688
+ if (bundleId === void 0) return {
4689
+ ...scope,
4690
+ window,
4691
+ sinceMs,
4692
+ beforeReceivedAtMs,
4693
+ reportingInstallations: await reporting
4694
+ };
4695
+ const countOutcome = (outcome) => measure(model.countEvents({
4696
+ filter: bundleFilter({
4697
+ ...scope,
4698
+ bundleId,
4699
+ outcome
4566
4700
  }),
4567
- query,
4568
- limit,
4569
- offset
4570
- });
4571
- },
4572
- async getInstallationHistory(installId, limit, offset) {
4573
- const ordered = (await materializeEventRows({
4574
- persistence,
4575
- cutoffMs: Date.now()
4576
- })).filter((row) => row.install_id === installId && isTransitionEventRow(row)).toSorted(compareEventNewest);
4701
+ sinceMs,
4702
+ beforeReceivedAtMs
4703
+ }));
4704
+ const [reportingInstallations, bundleInstallations, downloadedReports, appliedReports, recoveredReports, unchangedReports] = await Promise.all([
4705
+ reporting,
4706
+ measure(model.countLatestEvents({
4707
+ ...scope,
4708
+ sinceMs,
4709
+ bundle: [{
4710
+ field: "from_bundle_id",
4711
+ value: bundleId,
4712
+ types: ["UPDATE_DOWNLOADED"]
4713
+ }, {
4714
+ field: "to_bundle_id",
4715
+ value: bundleId,
4716
+ types: [
4717
+ "UNCHANGED",
4718
+ "UPDATE_APPLIED",
4719
+ "RECOVERED"
4720
+ ]
4721
+ }]
4722
+ })),
4723
+ countOutcome("downloaded"),
4724
+ countOutcome("applied"),
4725
+ countOutcome("recovered"),
4726
+ countOutcome("unchanged")
4727
+ ]);
4577
4728
  return {
4578
- data: ordered.slice(offset, offset + limit).map(toHistoryRow),
4579
- pagination: {
4580
- total: ordered.length,
4581
- limit,
4582
- offset
4729
+ ...scope,
4730
+ window,
4731
+ sinceMs,
4732
+ beforeReceivedAtMs,
4733
+ reportingInstallations,
4734
+ bundle: {
4735
+ bundleId,
4736
+ reportingInstallations: bundleInstallations,
4737
+ downloadedReports,
4738
+ appliedReports,
4739
+ recoveredReports,
4740
+ unchangedReports
4583
4741
  }
4584
4742
  };
4585
4743
  }
@@ -4620,7 +4778,7 @@ const hashApiKey = async (apiKey) => {
4620
4778
  return bytesToBase64Url(new Uint8Array(digest));
4621
4779
  };
4622
4780
  const apiKeyId = () => {
4623
- const bytes = new Uint8Array(16);
4781
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
4624
4782
  crypto.getRandomValues(bytes);
4625
4783
  return `api-${bytesToBase64Url(bytes)}`;
4626
4784
  };
@@ -4657,7 +4815,7 @@ const registerApiKey = async (input) => {
4657
4815
  });
4658
4816
  };
4659
4817
  const createApiKey = (input) => {
4660
- const bytes = new Uint8Array(32);
4818
+ const bytes = /* @__PURE__ */ new Uint8Array(32);
4661
4819
  crypto.getRandomValues(bytes);
4662
4820
  return registerApiKey({
4663
4821
  apiKey: bytesToBase64Url(bytes),
@@ -5077,6 +5235,8 @@ const unsupportedSchemaUpgradeMessage = (version) => `Hot Updater v1 cannot migr
5077
5235
  //#endregion
5078
5236
  //#region ../../packages/server/dist/db/schemaReadiness.mjs
5079
5237
  var HotUpdaterSchemaMigrationRequiredError = class extends Error {
5238
+ adapterName;
5239
+ currentVersion;
5080
5240
  constructor(adapterName, currentVersion) {
5081
5241
  super(currentVersion === void 0 ? `Hot Updater database schema is not initialized for ${adapterName}. Run \`hot-updater db migrate\` before using this adapter.` : unsupportedSchemaUpgradeMessage(currentVersion));
5082
5242
  this.adapterName = adapterName;
@@ -5106,7 +5266,7 @@ const sqlProviders = [
5106
5266
  const noSqlProviders = ["mongodb"];
5107
5267
  [...sqlProviders, ...noSqlProviders];
5108
5268
  function isDatabasePlugin(plugin) {
5109
- return typeof plugin === "object" && plugin !== null && "name" in plugin && typeof plugin.name === "string" && "models" in plugin && typeof plugin.models === "object" && plugin.models !== null && "bundles" in plugin.models && typeof plugin.models.bundles === "object" && plugin.models.bundles !== null && "findById" in plugin.models.bundles && typeof plugin.models.bundles.findById === "function" && "findMany" in plugin.models.bundles && typeof plugin.models.bundles.findMany === "function" && "count" in plugin.models.bundles && typeof plugin.models.bundles.count === "function" && "bundlePatches" in plugin.models && typeof plugin.models.bundlePatches === "object" && plugin.models.bundlePatches !== null && "findByBundleIds" in plugin.models.bundlePatches && typeof plugin.models.bundlePatches.findByBundleIds === "function" && "channels" in plugin.models && typeof plugin.models.channels === "object" && plugin.models.channels !== null && "insert" in plugin.models.channels && typeof plugin.models.channels.insert === "function" && "list" in plugin.models.channels && typeof plugin.models.channels.list === "function" && "delete" in plugin.models.channels && typeof plugin.models.channels.delete === "function" && "insights" in plugin.models && typeof plugin.models.insights === "object" && plugin.models.insights !== null && "append" in plugin.models.insights && typeof plugin.models.insights.append === "function" && "scan" in plugin.models.insights && typeof plugin.models.insights.scan === "function" && "apiKeys" in plugin.models && typeof plugin.models.apiKeys === "object" && plugin.models.apiKeys !== null && "create" in plugin.models.apiKeys && typeof plugin.models.apiKeys.create === "function" && "findByHash" in plugin.models.apiKeys && typeof plugin.models.apiKeys.findByHash === "function" && "list" in plugin.models.apiKeys && typeof plugin.models.apiKeys.list === "function" && "revoke" in plugin.models.apiKeys && typeof plugin.models.apiKeys.revoke === "function" && "commit" in plugin && typeof plugin.commit === "function" && (!("dispose" in plugin) || plugin.dispose === void 0 || typeof plugin.dispose === "function");
5269
+ return typeof plugin === "object" && plugin !== null && "name" in plugin && typeof plugin.name === "string" && "models" in plugin && typeof plugin.models === "object" && plugin.models !== null && "bundles" in plugin.models && typeof plugin.models.bundles === "object" && plugin.models.bundles !== null && "findById" in plugin.models.bundles && typeof plugin.models.bundles.findById === "function" && "findMany" in plugin.models.bundles && typeof plugin.models.bundles.findMany === "function" && "count" in plugin.models.bundles && typeof plugin.models.bundles.count === "function" && "bundlePatches" in plugin.models && typeof plugin.models.bundlePatches === "object" && plugin.models.bundlePatches !== null && "findByBundleIds" in plugin.models.bundlePatches && typeof plugin.models.bundlePatches.findByBundleIds === "function" && "channels" in plugin.models && typeof plugin.models.channels === "object" && plugin.models.channels !== null && "insert" in plugin.models.channels && typeof plugin.models.channels.insert === "function" && "list" in plugin.models.channels && typeof plugin.models.channels.list === "function" && "delete" in plugin.models.channels && typeof plugin.models.channels.delete === "function" && "insights" in plugin.models && typeof plugin.models.insights === "object" && plugin.models.insights !== null && "recordEvent" in plugin.models.insights && typeof plugin.models.insights.recordEvent === "function" && "listEvents" in plugin.models.insights && typeof plugin.models.insights.listEvents === "function" && "findLatestEvents" in plugin.models.insights && typeof plugin.models.insights.findLatestEvents === "function" && "countEvents" in plugin.models.insights && typeof plugin.models.insights.countEvents === "function" && "countLatestEvents" in plugin.models.insights && typeof plugin.models.insights.countLatestEvents === "function" && "apiKeys" in plugin.models && typeof plugin.models.apiKeys === "object" && plugin.models.apiKeys !== null && "create" in plugin.models.apiKeys && typeof plugin.models.apiKeys.create === "function" && "findByHash" in plugin.models.apiKeys && typeof plugin.models.apiKeys.findByHash === "function" && "list" in plugin.models.apiKeys && typeof plugin.models.apiKeys.list === "function" && "revoke" in plugin.models.apiKeys && typeof plugin.models.apiKeys.revoke === "function" && "commit" in plugin && typeof plugin.commit === "function" && (!("dispose" in plugin) || plugin.dispose === void 0 || typeof plugin.dispose === "function");
5110
5270
  }
5111
5271
  //#endregion
5112
5272
  //#region ../../packages/server/dist/storageAccess.mjs
@@ -5209,27 +5369,41 @@ const hotUpdaterCoreMetadata = Symbol.for("@hot-updater/server/core-metadata");
5209
5369
  function createHotUpdaterCore(options) {
5210
5370
  for (const key of ["authorityId", "catalogId"]) if (Object.hasOwn(options, key)) throw new TypeError(`Remove ${key} from createHotUpdater options. Catalog identity is managed internally.`);
5211
5371
  const database = options.database;
5212
- const { downloadStorageObject, readStorageText, resolveFileUrl } = createStorageAccess((options.storage ?? []).map((storage) => {
5372
+ const storagePlugins = (options.storage ?? []).map((storage) => {
5213
5373
  assertStorageOperations(storage, ["get", "getDownloadUrl"]);
5214
5374
  return storage;
5215
- }));
5375
+ });
5376
+ const { downloadStorageObject, readStorageText, resolveFileUrl } = createStorageAccess(storagePlugins);
5216
5377
  const adapterCapabilities = database;
5217
5378
  if (!isDatabasePlugin(database)) throw new Error("@hot-updater/server only supports database plugins.");
5218
5379
  const plugin = database;
5219
- const assertSchemaReady = createSchemaReadinessChecker(adapterCapabilities.adapterName ?? plugin.name, adapterCapabilities.createMigrator);
5380
+ const adapterName = adapterCapabilities.adapterName ?? plugin.name;
5381
+ const assertSchemaReady = createSchemaReadinessChecker(adapterName, adapterCapabilities.createMigrator);
5220
5382
  const core = createDatabasePluginCore(plugin, resolveFileUrl, {
5221
5383
  beforeOperation: assertSchemaReady,
5222
5384
  readStorageText
5223
5385
  });
5224
5386
  const clientAccess = normalizeClientAccess(options.clientAccess);
5225
5387
  const insights = createInsightsProvider({
5226
- async append(row) {
5388
+ async recordEvent(input) {
5389
+ await assertSchemaReady();
5390
+ return plugin.models.insights.recordEvent(input);
5391
+ },
5392
+ async listEvents(input) {
5227
5393
  await assertSchemaReady();
5228
- return plugin.models.insights.append(row);
5394
+ return plugin.models.insights.listEvents(input);
5229
5395
  },
5230
- async scan(input) {
5396
+ async findLatestEvents(input) {
5231
5397
  await assertSchemaReady();
5232
- return plugin.models.insights.scan(input);
5398
+ return plugin.models.insights.findLatestEvents(input);
5399
+ },
5400
+ async countLatestEvents(input) {
5401
+ await assertSchemaReady();
5402
+ return plugin.models.insights.countLatestEvents(input);
5403
+ },
5404
+ async countEvents(input) {
5405
+ await assertSchemaReady();
5406
+ return plugin.models.insights.countEvents(input);
5233
5407
  }
5234
5408
  });
5235
5409
  const apiKeys = createApiKeyManagement({
@@ -5268,8 +5442,60 @@ function createHotUpdater(options) {
5268
5442
  return createHotUpdaterCore(options).api;
5269
5443
  }
5270
5444
  //#endregion
5445
+ //#region ../plugin-core/dist/insightsLatestQueries.mjs
5446
+ /** Internal query translation for the bundled CRUD adapters. */
5447
+ const latestInsightsWhere = (input) => {
5448
+ if ("installId" in input) return [{
5449
+ field: "install_id",
5450
+ value: input.installId
5451
+ }];
5452
+ if ("userId" in input) return [{
5453
+ field: "user_id",
5454
+ value: input.userId
5455
+ }, ...input.afterInstallId === void 0 ? [] : [{
5456
+ field: "install_id",
5457
+ operator: "gt",
5458
+ value: input.afterInstallId
5459
+ }]];
5460
+ return [
5461
+ {
5462
+ field: "platform",
5463
+ value: input.platform
5464
+ },
5465
+ {
5466
+ field: "channel",
5467
+ value: input.channel
5468
+ },
5469
+ {
5470
+ field: "received_at_ms",
5471
+ operator: "gte",
5472
+ value: input.sinceMs
5473
+ },
5474
+ ...input.bundle === void 0 ? [] : [input.bundle.field === "from_bundle_id" ? {
5475
+ field: "from_bundle_id",
5476
+ value: input.bundle.value
5477
+ } : {
5478
+ field: "to_bundle_id",
5479
+ value: input.bundle.value
5480
+ }, {
5481
+ field: "type",
5482
+ operator: "in",
5483
+ value: input.bundle.types
5484
+ }]
5485
+ ];
5486
+ };
5487
+ /** OR groups for one latest-event count; an installation is counted once. */
5488
+ const latestInsightsCountGroups = (input) => input.bundle === void 0 ? [latestInsightsWhere({
5489
+ ...input,
5490
+ bundle: void 0
5491
+ })] : input.bundle.map((bundle) => latestInsightsWhere({
5492
+ ...input,
5493
+ bundle
5494
+ }));
5495
+ //#endregion
5271
5496
  //#region src/firebaseDatabaseParserShared.ts
5272
5497
  var FirebaseDatabaseDataError = class extends Error {
5498
+ source;
5273
5499
  name = "FirebaseDatabaseDataError";
5274
5500
  constructor(source) {
5275
5501
  super(`Invalid Firebase database data at "${source}".`);
@@ -5359,14 +5585,15 @@ const parseFirebaseBundleEventRow = (value, source) => {
5359
5585
  const input = record(value, source);
5360
5586
  const type = string(property(input, "type"), source);
5361
5587
  const fromBundleId = requiredNullableString(input, "from_bundle_id", source);
5362
- const updateStrategy = requiredNullableString(input, "update_strategy", source);
5363
- if (!((type === "UPDATE_APPLIED" || type === "RECOVERED" || type === "RELEASE_ADOPTED") && typeof fromBundleId === "string" && (updateStrategy === "fingerprint" || updateStrategy === "appVersion") || type === "UNCHANGED" && fromBundleId === null && updateStrategy === null)) throw new FirebaseDatabaseDataError(source);
5588
+ const metadata = property(input, "metadata");
5589
+ if (!isDatabaseBundleEventMetadata(metadata)) throw new FirebaseDatabaseDataError(source);
5590
+ const updateStrategy = metadata.update_strategy;
5591
+ if (!((type === "UPDATE_DOWNLOADED" || type === "UPDATE_APPLIED" || type === "RECOVERED") && typeof fromBundleId === "string" && (updateStrategy === "fingerprint" || updateStrategy === "appVersion") || type === "UNCHANGED" && fromBundleId === null && updateStrategy === null)) throw new FirebaseDatabaseDataError(source);
5364
5592
  return {
5365
5593
  id: string(property(input, "id"), source),
5366
5594
  type,
5367
5595
  install_id: string(property(input, "install_id"), source),
5368
5596
  user_id: nullableString(property(input, "user_id"), source),
5369
- username: nullableString(property(input, "username"), source),
5370
5597
  from_release_id: requiredNullableString(input, "from_release_id", source),
5371
5598
  from_bundle_id: fromBundleId,
5372
5599
  to_release_id: requiredNullableString(input, "to_release_id", source),
@@ -5374,10 +5601,7 @@ const parseFirebaseBundleEventRow = (value, source) => {
5374
5601
  platform: platform(property(input, "platform"), source),
5375
5602
  app_version: string(property(input, "app_version"), source),
5376
5603
  channel: string(property(input, "channel"), source),
5377
- cohort: string(property(input, "cohort"), source),
5378
- update_strategy: updateStrategy,
5379
- fingerprint_hash: nullableString(property(input, "fingerprint_hash"), source),
5380
- sdk_version: nullableString(property(input, "sdk_version"), source),
5604
+ metadata,
5381
5605
  received_at_ms: number(property(input, "received_at_ms"), source)
5382
5606
  };
5383
5607
  };
@@ -5461,13 +5685,15 @@ const matchesCondition = (row, condition) => {
5461
5685
  switch (condition.operator ?? "eq") {
5462
5686
  case "eq": {
5463
5687
  if (typeof expected !== "string") return actual === expected;
5464
- const comparison = normalizeStringComparison(actual, expected, "mode" in condition ? condition.mode : void 0);
5688
+ const mode = "mode" in condition ? condition.mode : void 0;
5689
+ const comparison = normalizeStringComparison(actual, expected, mode);
5465
5690
  return comparison !== null && comparison[0] === comparison[1];
5466
5691
  }
5467
5692
  case "ne": {
5468
5693
  if (actual === null || actual === void 0) return false;
5469
5694
  if (typeof expected !== "string") return actual !== expected;
5470
- const comparison = normalizeStringComparison(actual, expected, "mode" in condition ? condition.mode : void 0);
5695
+ const mode = "mode" in condition ? condition.mode : void 0;
5696
+ const comparison = normalizeStringComparison(actual, expected, mode);
5471
5697
  return comparison === null || comparison[0] !== comparison[1];
5472
5698
  }
5473
5699
  case "gt":
@@ -5492,17 +5718,20 @@ const matchesCondition = (row, condition) => {
5492
5718
  }
5493
5719
  case "contains": {
5494
5720
  if (typeof expected !== "string") return false;
5495
- const comparison = normalizeStringComparison(actual, expected, "mode" in condition ? condition.mode : void 0);
5721
+ const mode = "mode" in condition ? condition.mode : void 0;
5722
+ const comparison = normalizeStringComparison(actual, expected, mode);
5496
5723
  return comparison?.[0].includes(comparison[1]) ?? false;
5497
5724
  }
5498
5725
  case "starts_with": {
5499
5726
  if (typeof expected !== "string") return false;
5500
- const comparison = normalizeStringComparison(actual, expected, "mode" in condition ? condition.mode : void 0);
5727
+ const mode = "mode" in condition ? condition.mode : void 0;
5728
+ const comparison = normalizeStringComparison(actual, expected, mode);
5501
5729
  return comparison?.[0].startsWith(comparison[1]) ?? false;
5502
5730
  }
5503
5731
  case "ends_with": {
5504
5732
  if (typeof expected !== "string") return false;
5505
- const comparison = normalizeStringComparison(actual, expected, "mode" in condition ? condition.mode : void 0);
5733
+ const mode = "mode" in condition ? condition.mode : void 0;
5734
+ const comparison = normalizeStringComparison(actual, expected, mode);
5506
5735
  return comparison?.[0].endsWith(comparison[1]) ?? false;
5507
5736
  }
5508
5737
  }
@@ -5547,6 +5776,7 @@ const queryFirebaseDatabaseRows = (rows, input) => {
5547
5776
  //#endregion
5548
5777
  //#region src/firebaseDatabaseState.ts
5549
5778
  var FirebaseDatabaseConstraintError = class extends Error {
5779
+ constraint;
5550
5780
  name = "FirebaseDatabaseConstraintError";
5551
5781
  constructor(constraint) {
5552
5782
  super(`Firebase database constraint failed: ${constraint}`);
@@ -5676,6 +5906,7 @@ const createFirebaseDatabaseState = (snapshot) => ({
5676
5906
  case "bundles": return distinctCount([...snapshot.bundles.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
5677
5907
  case "bundle_patches": return distinctCount([...snapshot.bundlePatches.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
5678
5908
  case "releases": return distinctCount([...snapshot.releases.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
5909
+ case "bundle_events": return distinctCount([...snapshot.bundleEvents.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
5679
5910
  }
5680
5911
  },
5681
5912
  async findOne(input) {
@@ -5705,6 +5936,7 @@ const createFirebaseDatabaseState = (snapshot) => ({
5705
5936
  const FIREBASE_V1_COLLECTION_NAMES = {
5706
5937
  apiKeys: "hot_updater_v1_api_keys",
5707
5938
  bundleEvents: "hot_updater_v1_bundle_events",
5939
+ insightsLatest: "hot_updater_v1_insights_latest",
5708
5940
  bundlePatches: "hot_updater_v1_bundle_patches",
5709
5941
  bundles: "hot_updater_v1_bundles",
5710
5942
  channels: "hot_updater_v1_channels",
@@ -5715,6 +5947,7 @@ const FIREBASE_V1_COLLECTION_NAMES = {
5715
5947
  //#endregion
5716
5948
  //#region src/firebaseDatabasePersistence.ts
5717
5949
  var FirebaseDatabaseAdapterVersionError = class extends Error {
5950
+ version;
5718
5951
  name = "FirebaseDatabaseAdapterVersionError";
5719
5952
  constructor(version) {
5720
5953
  super(`Unsupported Firebase database adapter version: ${String(version)}`);
@@ -5725,6 +5958,7 @@ const createFirebaseDatabaseCollections = (db) => ({
5725
5958
  bundles: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundles),
5726
5959
  bundlePatches: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundlePatches),
5727
5960
  bundleEvents: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundleEvents),
5961
+ insightsLatest: db.collection(FIREBASE_V1_COLLECTION_NAMES.insightsLatest),
5728
5962
  channels: db.collection(FIREBASE_V1_COLLECTION_NAMES.channels),
5729
5963
  apiKeys: db.collection(FIREBASE_V1_COLLECTION_NAMES.apiKeys),
5730
5964
  releaseCatalogs: db.collection(FIREBASE_V1_COLLECTION_NAMES.releaseCatalogs),
@@ -5733,14 +5967,15 @@ const createFirebaseDatabaseCollections = (db) => ({
5733
5967
  });
5734
5968
  const firebaseChannelDocumentId = (name) => `name_${Buffer.from(name, "utf8").toString("base64url")}`;
5735
5969
  const firebaseChannelIdDocumentId = (id) => `channel_id_${Buffer.from(id, "utf8").toString("base64url")}`;
5970
+ const firebaseInstallationDocumentId = (id) => `install_${Buffer.from(id, "utf8").toString("base64url")}`;
5736
5971
  const requireFirebaseDocumentKey = (model, documentId, row) => {
5737
- if (documentId !== ("id" in row ? row.id : row.scope_key)) throw new FirebaseDatabaseConstraintError(`${model}.id.document-key`);
5972
+ if (documentId !== (model === "insights_latest" ? firebaseInstallationDocumentId(row.install_id) : "id" in row ? row.id : row.scope_key)) throw new FirebaseDatabaseConstraintError(`${model}.id.document-key`);
5738
5973
  return row;
5739
5974
  };
5740
5975
  const documentMap = (model, documents) => {
5741
5976
  const rows = /* @__PURE__ */ new Map();
5742
5977
  for (const { row } of documents) {
5743
- const key = "id" in row ? row.id : row.scope_key;
5978
+ const key = model === "insights_latest" ? row.install_id : "id" in row ? row.id : row.scope_key;
5744
5979
  if (rows.has(key)) throw new FirebaseDatabaseConstraintError(`${model}.id.unique`);
5745
5980
  rows.set(key, row);
5746
5981
  }
@@ -5755,10 +5990,6 @@ const patchMap = (snapshot) => documentMap("bundle_patches", snapshot.docs.map((
5755
5990
  document,
5756
5991
  row: parseFirebasePatchRow(document.data(), `bundle_patches/${document.id}`)
5757
5992
  })));
5758
- const eventMap = (snapshot) => documentMap("bundle_events", snapshot.docs.map((document) => ({
5759
- document,
5760
- row: parseFirebaseBundleEventRow(document.data(), `bundle_events/${document.id}`)
5761
- })));
5762
5993
  const channelMap = (snapshot) => {
5763
5994
  const rows = /* @__PURE__ */ new Map();
5764
5995
  const names = /* @__PURE__ */ new Set();
@@ -5789,18 +6020,17 @@ const toSnapshot = (documents) => {
5789
6020
  return {
5790
6021
  bundles: bundleMap(documents[0]),
5791
6022
  bundlePatches: patchMap(documents[1]),
5792
- bundleEvents: eventMap(documents[2]),
5793
- channels: channelMap(documents[3]),
5794
- apiKeys: apiKeyMap(documents[4]),
5795
- releases: releaseMap(documents[5]),
5796
- releaseCatalogs: releaseCatalogMap(documents[6])
6023
+ bundleEvents: /* @__PURE__ */ new Map(),
6024
+ channels: channelMap(documents[2]),
6025
+ apiKeys: apiKeyMap(documents[3]),
6026
+ releases: releaseMap(documents[4]),
6027
+ releaseCatalogs: releaseCatalogMap(documents[5])
5797
6028
  };
5798
6029
  };
5799
6030
  const loadFirebaseDatabaseSnapshot = async (collections) => {
5800
- const [bundles, patches, events, channels, apiKeys, releases, releaseCatalogs] = await Promise.all([
6031
+ const [bundles, patches, channels, apiKeys, releases, releaseCatalogs] = await Promise.all([
5801
6032
  collections.bundles.get(),
5802
6033
  collections.bundlePatches.get(),
5803
- collections.bundleEvents.get(),
5804
6034
  collections.channels.get(),
5805
6035
  collections.apiKeys.get(),
5806
6036
  collections.releases.get(),
@@ -5809,7 +6039,6 @@ const loadFirebaseDatabaseSnapshot = async (collections) => {
5809
6039
  return toSnapshot([
5810
6040
  bundles,
5811
6041
  patches,
5812
- events,
5813
6042
  channels,
5814
6043
  apiKeys,
5815
6044
  releases,
@@ -5817,10 +6046,9 @@ const loadFirebaseDatabaseSnapshot = async (collections) => {
5817
6046
  ]);
5818
6047
  };
5819
6048
  const loadFirebaseTransactionSnapshot = async (transaction, collections) => {
5820
- const [bundles, patches, events, channels, apiKeys, releases, releaseCatalogs] = await Promise.all([
6049
+ const [bundles, patches, channels, apiKeys, releases, releaseCatalogs] = await Promise.all([
5821
6050
  transaction.get(collections.bundles),
5822
6051
  transaction.get(collections.bundlePatches),
5823
- transaction.get(collections.bundleEvents),
5824
6052
  transaction.get(collections.channels),
5825
6053
  transaction.get(collections.apiKeys),
5826
6054
  transaction.get(collections.releases),
@@ -5829,7 +6057,6 @@ const loadFirebaseTransactionSnapshot = async (transaction, collections) => {
5829
6057
  return toSnapshot([
5830
6058
  bundles,
5831
6059
  patches,
5832
- events,
5833
6060
  channels,
5834
6061
  apiKeys,
5835
6062
  releases,
@@ -5855,13 +6082,6 @@ const persistFirebaseDatabaseSnapshot = ({ transaction, collections, before, aft
5855
6082
  after: after.bundlePatches,
5856
6083
  documentId: (row) => row.id
5857
6084
  });
5858
- persistCollection({
5859
- transaction,
5860
- collection: collections.bundleEvents,
5861
- before: before.bundleEvents,
5862
- after: after.bundleEvents,
5863
- documentId: (row) => row.id
5864
- });
5865
6085
  persistCollection({
5866
6086
  transaction,
5867
6087
  collection: collections.channels,
@@ -5893,7 +6113,7 @@ const persistFirebaseDatabaseSnapshot = ({ transaction, collections, before, aft
5893
6113
  documentId: (row) => row.scope_key
5894
6114
  });
5895
6115
  };
5896
- const migrateFirebaseDatabase = async (_db, collections) => {
6116
+ const migrateFirebaseDatabase = async (collections) => {
5897
6117
  const versionDocument = collections.settings.doc("database_adapter_version");
5898
6118
  const version = await versionDocument.get();
5899
6119
  const adapterVersion = version.data()?.version;
@@ -5904,7 +6124,9 @@ const migrateFirebaseDatabase = async (_db, collections) => {
5904
6124
  collections.bundlePatches.limit(1).get(),
5905
6125
  collections.channels.limit(1).get(),
5906
6126
  collections.releases.limit(1).get(),
5907
- collections.releaseCatalogs.limit(1).get()
6127
+ collections.releaseCatalogs.limit(1).get(),
6128
+ collections.insightsLatest.limit(1).get(),
6129
+ collections.bundleEvents.limit(1).get()
5908
6130
  ])).some((snapshot) => !snapshot.empty)) throw new FirebaseDatabaseAdapterVersionError("v0");
5909
6131
  try {
5910
6132
  await versionDocument.create({ version: 4 });
@@ -5919,13 +6141,36 @@ const exactId = (input) => {
5919
6141
  const [condition] = input.where;
5920
6142
  return condition.field === "id" && (condition.operator === void 0 || condition.operator === "eq") && typeof condition.value === "string" ? condition.value : void 0;
5921
6143
  };
6144
+ const firestoreOperator = (operator) => {
6145
+ switch (operator ?? "eq") {
6146
+ case "eq": return "==";
6147
+ case "ne": return "!=";
6148
+ case "gt": return ">";
6149
+ case "gte": return ">=";
6150
+ case "lt": return "<";
6151
+ case "lte": return "<=";
6152
+ case "in": return "in";
6153
+ case "not_in": return "not-in";
6154
+ default: return;
6155
+ }
6156
+ };
6157
+ const applyFirebaseWhere = (initial, where) => {
6158
+ let query = initial;
6159
+ for (const condition of where) {
6160
+ const operator = firestoreOperator(condition.operator);
6161
+ if (condition.connector === "OR" || operator === void 0) throw new FirebaseDatabaseConstraintError("query.unsupported");
6162
+ query = query.where(condition.field, operator, condition.value);
6163
+ }
6164
+ return query;
6165
+ };
5922
6166
  const firebaseDatabase = (config) => {
5923
- const adapter = createDatabasePluginAdapter("firebaseDatabase", (() => {
5924
- const db = (0, firebase_admin_firestore.getFirestore)((0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config));
6167
+ const implementation = (() => {
6168
+ const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
6169
+ const db = (0, firebase_admin_firestore.getFirestore)(app);
5925
6170
  const collections = createFirebaseDatabaseCollections(db);
5926
6171
  let migration;
5927
6172
  const ensureMigrated = () => {
5928
- migration ??= migrateFirebaseDatabase(db, collections).catch((error) => {
6173
+ migration ??= migrateFirebaseDatabase(collections).catch((error) => {
5929
6174
  migration = void 0;
5930
6175
  throw error;
5931
6176
  });
@@ -5948,13 +6193,52 @@ const firebaseDatabase = (config) => {
5948
6193
  };
5949
6194
  const read = async (operation) => {
5950
6195
  await ensureMigrated();
5951
- return operation(createFirebaseDatabaseState(await loadFirebaseDatabaseSnapshot(collections)));
6196
+ const snapshot = await loadFirebaseDatabaseSnapshot(collections);
6197
+ return operation(createFirebaseDatabaseState(snapshot));
5952
6198
  };
5953
6199
  return {
5954
- create: (input) => mutate((database) => database.create(input)),
6200
+ recordInsights: async ({ event }) => {
6201
+ await ensureMigrated();
6202
+ await db.runTransaction(async (transaction) => {
6203
+ const eventReference = collections.bundleEvents.doc(event.id);
6204
+ const installationReference = collections.insightsLatest.doc(firebaseInstallationDocumentId(event.install_id));
6205
+ const [storedEvent, storedInstallation] = await transaction.getAll(eventReference, installationReference);
6206
+ if (storedEvent.exists) return;
6207
+ const current = storedInstallation.exists ? requireFirebaseDocumentKey("insights_latest", storedInstallation.id, parseFirebaseBundleEventRow(storedInstallation.data(), `insights_latest/${storedInstallation.id}`)) : null;
6208
+ transaction.create(eventReference, event);
6209
+ if (current === null || event.received_at_ms > current.received_at_ms || event.received_at_ms === current.received_at_ms && compareInsightsText(event.id, current.id) > 0) transaction.set(installationReference, event);
6210
+ });
6211
+ },
6212
+ findLatestInsightsEvents: async (input) => {
6213
+ await ensureMigrated();
6214
+ if ("installId" in input) {
6215
+ const document = await collections.insightsLatest.doc(firebaseInstallationDocumentId(input.installId)).get();
6216
+ return document.exists ? [requireFirebaseDocumentKey("insights_latest", document.id, parseFirebaseBundleEventRow(document.data(), `insights_latest/${document.id}`))] : [];
6217
+ }
6218
+ return (await applyFirebaseWhere(collections.insightsLatest, latestInsightsWhere(input)).orderBy("install_id", "asc").limit(input.limit).get()).docs.map((document) => requireFirebaseDocumentKey("insights_latest", document.id, parseFirebaseBundleEventRow(document.data(), `insights_latest/${document.id}`)));
6219
+ },
6220
+ countLatestInsightsEvents: async (input) => {
6221
+ await ensureMigrated();
6222
+ const filter = firebase_admin_firestore.Filter.or(...latestInsightsCountGroups(input).map((where) => firebase_admin_firestore.Filter.and(...where.map((condition) => {
6223
+ const operator = firestoreOperator(condition.operator);
6224
+ if (operator === void 0) throw new FirebaseDatabaseConstraintError("query.unsupported");
6225
+ return firebase_admin_firestore.Filter.where(condition.field, operator, condition.value);
6226
+ }))));
6227
+ return (await collections.insightsLatest.where(filter).count().get()).data().count;
6228
+ },
6229
+ create: async (input) => {
6230
+ if (input.model !== "bundle_events") return mutate((database) => database.create(input));
6231
+ await ensureMigrated();
6232
+ await collections.bundleEvents.doc(input.data.id).create(input.data);
6233
+ return input.data;
6234
+ },
5955
6235
  update: (input) => mutate((database) => database.update(input)),
5956
6236
  delete: (input) => mutate((database) => database.delete(input)),
5957
- count: (input) => read((database) => database.count(input)),
6237
+ count: async (input) => {
6238
+ if (input.model !== "bundle_events") return read((database) => database.count(input));
6239
+ await ensureMigrated();
6240
+ return (await applyFirebaseWhere(collections.bundleEvents, input.where ?? []).orderBy("received_at_ms", "desc").orderBy("id", "desc").count().get()).data().count;
6241
+ },
5958
6242
  findOne: async (input) => {
5959
6243
  const id = exactId(input);
5960
6244
  if (id === void 0) return read((database) => database.findOne(input));
@@ -5976,9 +6260,20 @@ const firebaseDatabase = (config) => {
5976
6260
  }
5977
6261
  },
5978
6262
  findMany: async (input) => {
5979
- if (input.model !== "channels") return read((database) => database.findMany(input));
5980
- await ensureMigrated();
5981
- return queryFirebaseDatabaseRows(await loadFirebaseChannels(collections), input);
6263
+ if (input.model === "bundle_events") {
6264
+ await ensureMigrated();
6265
+ let query = applyFirebaseWhere(collections.bundleEvents, input.where ?? []);
6266
+ for (const order of input.orderBy ?? []) {
6267
+ if (order.nulls !== void 0) throw new FirebaseDatabaseConstraintError("query.unsupported");
6268
+ query = query.orderBy(order.field, order.direction);
6269
+ }
6270
+ return (await query.offset(input.offset).limit(input.limit).get()).docs.map((document) => requireFirebaseDocumentKey("bundle_events", document.id, parseFirebaseBundleEventRow(document.data(), `bundle_events/${document.id}`)));
6271
+ }
6272
+ if (input.model === "channels") {
6273
+ await ensureMigrated();
6274
+ return queryFirebaseDatabaseRows(await loadFirebaseChannels(collections), input);
6275
+ }
6276
+ return read((database) => database.findMany(input));
5982
6277
  },
5983
6278
  insertChannel: async (input) => {
5984
6279
  await ensureMigrated();
@@ -6030,7 +6325,8 @@ const firebaseDatabase = (config) => {
6030
6325
  },
6031
6326
  transaction: (callback) => mutate(callback)
6032
6327
  };
6033
- })());
6328
+ })();
6329
+ const adapter = createDatabasePluginAdapter("firebaseDatabase", implementation);
6034
6330
  return createDatabasePlugin({
6035
6331
  name: "firebaseDatabase",
6036
6332
  models: adapter.models,
@@ -6040,7 +6336,8 @@ const firebaseDatabase = (config) => {
6040
6336
  //#endregion
6041
6337
  //#region src/firebaseStorage.ts
6042
6338
  const firebaseStorage = (config) => {
6043
- const bucket = (0, firebase_admin_storage.getStorage)((0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config)).bucket(config.storageBucket);
6339
+ const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
6340
+ const bucket = (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
6044
6341
  const getStorageKey = createStorageKeyBuilder(config.basePath);
6045
6342
  const parseAndValidate = (storageUri) => {
6046
6343
  const parsed = parseStorageUri(storageUri, "gs");
@@ -6134,7 +6431,7 @@ const hot = { updater: { v1: (0, firebase_functions_v2_https.onRequest)({ region
6134
6431
  const request = new Request(fullUrl, {
6135
6432
  method: req.method,
6136
6433
  headers: req.headers,
6137
- body: req.method !== "GET" && req.method !== "HEAD" ? req.body : void 0
6434
+ body: req.method !== "GET" && req.method !== "HEAD" ? new Uint8Array(req.rawBody) : void 0
6138
6435
  });
6139
6436
  const honoResponse = await app.fetch(request);
6140
6437
  res.status(honoResponse.status);