@hot-updater/cloudflare 1.0.0-rc.2 → 1.0.0-rc.3

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.
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
4
  // ../../packages/server/dist/package.mjs
5
- var version = "1.0.0-rc.1";
5
+ var version = "1.0.0-rc.2";
6
6
 
7
7
  // ../../packages/server/dist/version.mjs
8
8
  var HOT_UPDATER_SERVER_VERSION = version;
@@ -303,7 +303,8 @@ __name(parseStorageUri, "parseStorageUri");
303
303
  var isBrotliManifestAssetPath = /* @__PURE__ */ __name((assetPath) => /(^|\/)index\.[^/]+\.bundle$/.test(assetPath.replace(/\\/g, "/")), "isBrotliManifestAssetPath");
304
304
  var getManifestAssetDownloadPath = /* @__PURE__ */ __name((assetPath) => isBrotliManifestAssetPath(assetPath) ? `${assetPath}.br` : assetPath, "getManifestAssetDownloadPath");
305
305
  var createStorageUriWithRelativePath = /* @__PURE__ */ __name(({ baseStorageUri, relativePath }) => {
306
- const parsed = parseStorageUri(baseStorageUri, new URL(baseStorageUri).protocol.replace(":", ""));
306
+ const protocol = new URL(baseStorageUri).protocol.replace(":", "");
307
+ const parsed = parseStorageUri(baseStorageUri, protocol);
307
308
  const childKey = createStorageKeyBuilder(void 0)(relativePath);
308
309
  if (!childKey) throw new Error("Relative storage key must not be empty.");
309
310
  return createStorageUri({
@@ -334,6 +335,7 @@ var DatabasePluginInputError = class extends Error {
334
335
  static {
335
336
  __name(this, "DatabasePluginInputError");
336
337
  }
338
+ code;
337
339
  name = "DatabasePluginInputError";
338
340
  constructor(code) {
339
341
  super(`Invalid database plugin input: ${code}`);
@@ -502,7 +504,7 @@ var RELEASE_CATALOG_SCHEMA_VERSION = 1;
502
504
  var RELEASE_CATALOG_FALLBACK_POLICY = "BUILTIN_IF_ACTIVE_INELIGIBLE";
503
505
  var MAX_TARGET_COHORTS_PER_RELEASE = 100;
504
506
  var MAX_DISTINCT_TARGET_COHORTS_PER_SCOPE = 512;
505
- var MAX_COMPILED_CATALOG_BYTES = 256 * 1024;
507
+ var MAX_COMPILED_CATALOG_BYTES = 262144;
506
508
  var BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
507
509
  var MAX_CATALOG_SEGMENT_LENGTH = 255;
508
510
  function assertCatalogSegment(value, name) {
@@ -620,7 +622,7 @@ var isUUIDv7 = /* @__PURE__ */ __name((value) => typeof value === "string" && UU
620
622
 
621
623
  // ../plugin-core/dist/uuidv7.mjs
622
624
  function createUUIDv7FromTimestampHex(timestampHex) {
623
- const randomBytes = new Uint8Array(10);
625
+ const randomBytes = /* @__PURE__ */ new Uint8Array(10);
624
626
  crypto.getRandomValues(randomBytes);
625
627
  const randomHex = Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
626
628
  const randA = randomHex.slice(0, 3);
@@ -718,6 +720,19 @@ var databaseFields = {
718
720
  "sdk_version",
719
721
  "received_at_ms"
720
722
  ],
723
+ bundle_installations: [
724
+ "id",
725
+ "install_id",
726
+ "user_id",
727
+ "username",
728
+ "to_bundle_id",
729
+ "type",
730
+ "platform",
731
+ "app_version",
732
+ "channel",
733
+ "cohort",
734
+ "received_at_ms"
735
+ ],
721
736
  api_keys: [
722
737
  "id",
723
738
  "hash",
@@ -740,6 +755,8 @@ var isChannelText2 = /* @__PURE__ */ __name((value) => {
740
755
  }
741
756
  return true;
742
757
  }, "isChannelText");
758
+ var isInsightsIdentityText = /* @__PURE__ */ __name((value) => typeof value === "string" && value.length > 0 && value.length <= 255, "isInsightsIdentityText");
759
+ var isNullableInsightsIdentityText = /* @__PURE__ */ __name((value) => value === null || isInsightsIdentityText(value), "isNullableInsightsIdentityText");
743
760
  var modelValidators = {
744
761
  bundles: {
745
762
  id: /* @__PURE__ */ __name((value) => typeof value === "string", "id"),
@@ -815,8 +832,8 @@ var modelValidators = {
815
832
  bundle_events: {
816
833
  id: /* @__PURE__ */ __name((value) => typeof value === "string", "id"),
817
834
  type: /* @__PURE__ */ __name((value) => value === "UPDATE_APPLIED" || value === "RECOVERED" || value === "RELEASE_ADOPTED" || value === "UNCHANGED", "type"),
818
- install_id: /* @__PURE__ */ __name((value) => typeof value === "string", "install_id"),
819
- user_id: /* @__PURE__ */ __name((value) => value === null || typeof value === "string", "user_id"),
835
+ install_id: isInsightsIdentityText,
836
+ user_id: isNullableInsightsIdentityText,
820
837
  username: /* @__PURE__ */ __name((value) => value === null || typeof value === "string", "username"),
821
838
  from_bundle_id: /* @__PURE__ */ __name((value) => value === null || typeof value === "string", "from_bundle_id"),
822
839
  from_release_id: /* @__PURE__ */ __name((value) => value === null || typeof value === "string", "from_release_id"),
@@ -831,6 +848,19 @@ var modelValidators = {
831
848
  sdk_version: /* @__PURE__ */ __name((value) => value === null || typeof value === "string", "sdk_version"),
832
849
  received_at_ms: /* @__PURE__ */ __name((value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0, "received_at_ms")
833
850
  },
851
+ bundle_installations: {
852
+ id: /* @__PURE__ */ __name((value) => typeof value === "string", "id"),
853
+ install_id: isInsightsIdentityText,
854
+ user_id: isNullableInsightsIdentityText,
855
+ username: /* @__PURE__ */ __name((value) => value === null || typeof value === "string", "username"),
856
+ to_bundle_id: /* @__PURE__ */ __name((value) => typeof value === "string", "to_bundle_id"),
857
+ type: /* @__PURE__ */ __name((value) => value === "UPDATE_APPLIED" || value === "RECOVERED" || value === "RELEASE_ADOPTED" || value === "UNCHANGED", "type"),
858
+ platform: /* @__PURE__ */ __name((value) => value === "ios" || value === "android", "platform"),
859
+ app_version: /* @__PURE__ */ __name((value) => typeof value === "string", "app_version"),
860
+ channel: /* @__PURE__ */ __name((value) => typeof value === "string", "channel"),
861
+ cohort: /* @__PURE__ */ __name((value) => typeof value === "string", "cohort"),
862
+ received_at_ms: /* @__PURE__ */ __name((value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0, "received_at_ms")
863
+ },
834
864
  api_keys: {
835
865
  id: /* @__PURE__ */ __name((value) => typeof value === "string", "id"),
836
866
  hash: /* @__PURE__ */ __name((value) => typeof value === "string", "hash"),
@@ -982,6 +1012,19 @@ var sortableFields = {
982
1012
  "sdk_version",
983
1013
  "received_at_ms"
984
1014
  ]),
1015
+ bundle_installations: /* @__PURE__ */ new Set([
1016
+ "id",
1017
+ "install_id",
1018
+ "user_id",
1019
+ "username",
1020
+ "to_bundle_id",
1021
+ "type",
1022
+ "platform",
1023
+ "app_version",
1024
+ "channel",
1025
+ "cohort",
1026
+ "received_at_ms"
1027
+ ]),
985
1028
  api_keys: /* @__PURE__ */ new Set([
986
1029
  "id",
987
1030
  "hash",
@@ -1054,15 +1097,176 @@ var validateResult = /* @__PURE__ */ __name((model, row, select) => {
1054
1097
  ].every((field) => Object.hasOwn(row, field)) && !hasValidBundleEventInvariants(row)) throw new DatabasePluginInputError("invalid-result");
1055
1098
  }, "validateResult");
1056
1099
 
1100
+ // ../plugin-core/dist/insightsContract.mjs
1101
+ var encoder = new TextEncoder();
1102
+ var compareInsightsText = /* @__PURE__ */ __name((left, right) => {
1103
+ const a = encoder.encode(left);
1104
+ const b = encoder.encode(right);
1105
+ for (let index = 0; index < Math.min(a.length, b.length); index += 1) if (a[index] !== b[index]) return a[index] - b[index];
1106
+ return a.length - b.length;
1107
+ }, "compareInsightsText");
1108
+ var isWellFormedText = /* @__PURE__ */ __name((value) => {
1109
+ for (const character of value) {
1110
+ const point = character.codePointAt(0);
1111
+ if (point >= 55296 && point <= 57343) return false;
1112
+ }
1113
+ return true;
1114
+ }, "isWellFormedText");
1115
+ var isIdentity = /* @__PURE__ */ __name((value) => typeof value === "string" && value.length > 0 && value.length <= 255 && isWellFormedText(value), "isIdentity");
1116
+ var isText = /* @__PURE__ */ __name((value) => typeof value === "string" && value.length > 0 && isWellFormedText(value), "isText");
1117
+ var isTimestamp = /* @__PURE__ */ __name((value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0, "isTimestamp");
1118
+ var isLimit = /* @__PURE__ */ __name((value) => isTimestamp(value) && value >= 1 && value <= 101, "isLimit");
1119
+ var hasOnlyKeys = /* @__PURE__ */ __name((value, keys) => Object.keys(value).every((key) => keys.includes(key)), "hasOnlyKeys");
1120
+ var hasScope = /* @__PURE__ */ __name((value) => (value.platform === "ios" || value.platform === "android") && isText(value.channel), "hasScope");
1121
+ var isBundleFilter = /* @__PURE__ */ __name((value, withKind = false) => {
1122
+ if (!isRecord(value) || !hasScope(value)) return false;
1123
+ const keys = [
1124
+ "platform",
1125
+ "channel",
1126
+ "type",
1127
+ ...withKind ? ["kind"] : []
1128
+ ];
1129
+ return value.type === "RECOVERED" ? isText(value.fromBundleId) && hasOnlyKeys(value, [...keys, "fromBundleId"]) : (value.type === "UPDATE_APPLIED" || value.type === "RELEASE_ADOPTED") && isText(value.toBundleId) && hasOnlyKeys(value, [...keys, "toBundleId"]);
1130
+ }, "isBundleFilter");
1131
+ var isEventFilter = /* @__PURE__ */ __name((value) => {
1132
+ if (!isRecord(value)) return false;
1133
+ if (value.kind === "all") return hasOnlyKeys(value, ["kind"]);
1134
+ if (value.kind === "installationMovement") return isIdentity(value.installId) && hasOnlyKeys(value, ["kind", "installId"]);
1135
+ return value.kind === "bundle" && isBundleFilter(value, true);
1136
+ }, "isEventFilter");
1137
+ var validateRow = /* @__PURE__ */ __name((model, row, result = false) => {
1138
+ try {
1139
+ validateCreateData(model, row);
1140
+ if (!isRecord(row) || typeof row.id !== "string" || !isUUIDv7(row.id) || Object.values(row).some((value) => typeof value === "string" && !isWellFormedText(value))) throw new DatabasePluginInputError("invalid-data");
1141
+ } catch (error) {
1142
+ if (result) throw new DatabasePluginInputError("invalid-result");
1143
+ throw error;
1144
+ }
1145
+ }, "validateRow");
1146
+ var toInsightsInstallationRow = /* @__PURE__ */ __name((event) => {
1147
+ validateRow("bundle_events", event);
1148
+ return {
1149
+ id: event.id,
1150
+ install_id: event.install_id,
1151
+ user_id: event.user_id,
1152
+ username: event.username,
1153
+ to_bundle_id: event.to_bundle_id,
1154
+ type: event.type,
1155
+ platform: event.platform,
1156
+ app_version: event.app_version,
1157
+ channel: event.channel,
1158
+ cohort: event.cohort,
1159
+ received_at_ms: event.received_at_ms
1160
+ };
1161
+ }, "toInsightsInstallationRow");
1162
+ var isInsightsMovementEvent = /* @__PURE__ */ __name((event) => event.type === "UPDATE_APPLIED" || event.type === "RECOVERED", "isInsightsMovementEvent");
1163
+ var matchesInsightsEventFilter = /* @__PURE__ */ __name((event, filter) => {
1164
+ if (filter.kind === "all") return true;
1165
+ if (filter.kind === "installationMovement") return event.install_id === filter.installId && isInsightsMovementEvent(event);
1166
+ 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);
1167
+ }, "matchesInsightsEventFilter");
1168
+ var invalidQuery = /* @__PURE__ */ __name(() => {
1169
+ throw new DatabasePluginInputError("invalid-query");
1170
+ }, "invalidQuery");
1171
+ var invalidResult = /* @__PURE__ */ __name(() => {
1172
+ throw new DatabasePluginInputError("invalid-result");
1173
+ }, "invalidResult");
1174
+ var validateCount = /* @__PURE__ */ __name((count) => isTimestamp(count) ? count : invalidResult(), "validateCount");
1175
+ var createValidatedInsightsModel = /* @__PURE__ */ __name((model) => ({
1176
+ async record(input) {
1177
+ if (!isRecord(input) || !hasOnlyKeys(input, ["event", "installation"])) throw new DatabasePluginInputError("invalid-data");
1178
+ const expected = toInsightsInstallationRow(input.event);
1179
+ validateRow("bundle_installations", input.installation);
1180
+ if (Object.entries(expected).some(([field, value]) => Reflect.get(input.installation, field) !== value)) throw new DatabasePluginInputError("invalid-data");
1181
+ await model.record(input);
1182
+ },
1183
+ async listEvents(input) {
1184
+ if (!isRecord(input) || !hasOnlyKeys(input, [
1185
+ "filter",
1186
+ "sinceMs",
1187
+ "beforeReceivedAtMs",
1188
+ "after",
1189
+ "limit"
1190
+ ]) || !isEventFilter(input.filter) || !isLimit(input.limit) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs !== void 0 && !isTimestamp(input.sinceMs)) invalidQuery();
1191
+ const sinceMs = input.sinceMs ?? 0;
1192
+ if (sinceMs > input.beforeReceivedAtMs || input.after !== void 0 && (!isRecord(input.after) || !hasOnlyKeys(input.after, ["receivedAtMs", "id"]) || !isTimestamp(input.after.receivedAtMs) || typeof input.after.id !== "string" || !isUUIDv7(input.after.id) || input.after.receivedAtMs < sinceMs || input.after.receivedAtMs >= input.beforeReceivedAtMs)) invalidQuery();
1193
+ const rows = await model.listEvents(input);
1194
+ if (!Array.isArray(rows) || rows.length > input.limit) invalidResult();
1195
+ let previous = input.after;
1196
+ for (const row of rows) {
1197
+ validateRow("bundle_events", row, true);
1198
+ 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();
1199
+ previous = {
1200
+ receivedAtMs: row.received_at_ms,
1201
+ id: row.id
1202
+ };
1203
+ }
1204
+ return rows;
1205
+ },
1206
+ async findInstallations(input) {
1207
+ if (!isRecord(input)) invalidQuery();
1208
+ if ("installId" in input) {
1209
+ if (!hasOnlyKeys(input, ["installId"]) || !isIdentity(input.installId)) invalidQuery();
1210
+ } else if (!hasOnlyKeys(input, [
1211
+ "userId",
1212
+ "afterInstallId",
1213
+ "limit"
1214
+ ]) || !isIdentity(input.userId) || !isLimit(input.limit) || input.afterInstallId !== void 0 && !isIdentity(input.afterInstallId)) invalidQuery();
1215
+ const rows = await model.findInstallations(input);
1216
+ if (!Array.isArray(rows) || rows.length > ("installId" in input ? 1 : input.limit)) invalidResult();
1217
+ let previous = "installId" in input ? void 0 : input.afterInstallId;
1218
+ for (const row of rows) {
1219
+ validateRow("bundle_installations", row, true);
1220
+ if ("installId" in input ? row.install_id !== input.installId : row.user_id !== input.userId || previous !== void 0 && compareInsightsText(row.install_id, previous) <= 0) invalidResult();
1221
+ previous = row.install_id;
1222
+ }
1223
+ return rows;
1224
+ },
1225
+ async countInstallations(input) {
1226
+ if (!isRecord(input) || !hasOnlyKeys(input, [
1227
+ "platform",
1228
+ "channel",
1229
+ "sinceMs",
1230
+ "bundleId"
1231
+ ]) || !hasScope(input) || !isTimestamp(input.sinceMs) || input.bundleId !== void 0 && !isText(input.bundleId)) invalidQuery();
1232
+ return validateCount(await model.countInstallations(input));
1233
+ },
1234
+ async countEvents(input) {
1235
+ if (!isRecord(input) || !hasOnlyKeys(input, [
1236
+ "filter",
1237
+ "sinceMs",
1238
+ "beforeReceivedAtMs"
1239
+ ]) || !isBundleFilter(input.filter) || !isTimestamp(input.sinceMs) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs > input.beforeReceivedAtMs) invalidQuery();
1240
+ return validateCount(await model.countEvents(input));
1241
+ }
1242
+ }), "createValidatedInsightsModel");
1243
+
1057
1244
  // ../plugin-core/dist/databasePluginCrudValidationMutations.mjs
1058
1245
  var validateMutationWhere = /* @__PURE__ */ __name((where) => {
1059
1246
  if (where.length === 0) throw new DatabasePluginInputError("empty-mutation-where");
1060
1247
  }, "validateMutationWhere");
1061
1248
  var validateUpdateWhere = /* @__PURE__ */ __name((model, where) => {
1249
+ if (model === "bundle_installations") {
1250
+ const [install, receivedAt, id] = where;
1251
+ const exactInstall = isRecord(install) && install.field === "install_id" && (install.operator === void 0 || install.operator === "eq") && typeof install.value === "string" && install.connector === void 0 && install.mode === void 0;
1252
+ const receivedBefore = isRecord(receivedAt) && receivedAt.field === "received_at_ms" && (receivedAt.operator === "lt" || receivedAt.operator === "eq") && typeof receivedAt.value === "number" && Number.isSafeInteger(receivedAt.value) && receivedAt.value >= 0 && receivedAt.connector === void 0 && receivedAt.mode === void 0;
1253
+ const idBefore = isRecord(id) && id.field === "id" && id.operator === "lt" && typeof id.value === "string" && id.connector === void 0 && id.mode === void 0;
1254
+ if (!exactInstall || !receivedBefore || !(where.length === 2 && receivedAt.operator === "lt" || where.length === 3 && receivedAt.operator === "eq" && idBefore)) throw new DatabasePluginInputError("invalid-update-selector");
1255
+ return;
1256
+ }
1062
1257
  const selector = where[0];
1063
1258
  const primaryField = model === "release_catalogs" ? "scope_key" : "id";
1064
1259
  if (where.length !== 1 || !isRecord(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");
1065
1260
  }, "validateUpdateWhere");
1261
+ var validateInsightsInstallationUpdateData = /* @__PURE__ */ __name((update) => {
1262
+ if (!isRecord(update)) throw new DatabasePluginInputError("invalid-data");
1263
+ const fields = databaseFields.bundle_installations.filter((field) => field !== "install_id");
1264
+ if (Reflect.ownKeys(update).length !== fields.length || fields.some((field) => !Object.hasOwn(update, field))) throw new DatabasePluginInputError("invalid-data");
1265
+ for (const field of fields) {
1266
+ const validator = modelValidators.bundle_installations[field];
1267
+ if (!validator?.(Reflect.get(update, field))) throw new DatabasePluginInputError("invalid-data");
1268
+ }
1269
+ }, "validateInsightsInstallationUpdateData");
1066
1270
  var validateBundleUpdateData = /* @__PURE__ */ __name((update) => {
1067
1271
  if (!isRecord(update)) throw new DatabasePluginInputError("invalid-data");
1068
1272
  for (const [field, value] of Object.entries(update)) {
@@ -1232,7 +1436,7 @@ var createDatabasePluginCrud = /* @__PURE__ */ __name((implementation) => {
1232
1436
  async function create(input) {
1233
1437
  validateModel(input.model);
1234
1438
  validateCreateData(input.model, input.data);
1235
- if (input.onConflict !== void 0 && !(input.onConflict === "ignore" && (input.model === "channels" || input.model === "api_keys"))) throw new DatabasePluginInputError("invalid-operation");
1439
+ if (input.onConflict !== void 0 && !(input.onConflict === "ignore" && (input.model === "channels" || input.model === "api_keys" || input.model === "bundle_installations"))) throw new DatabasePluginInputError("invalid-operation");
1236
1440
  validateSelect(input.model, input.select);
1237
1441
  const row = await implementation.create(input);
1238
1442
  validateResult(input.model, row, input.select);
@@ -1247,6 +1451,7 @@ var createDatabasePluginCrud = /* @__PURE__ */ __name((implementation) => {
1247
1451
  if (input.model === "bundles") validateBundleUpdateData(input.update);
1248
1452
  else if (input.model === "releases") validateReleaseUpdateData(input.update);
1249
1453
  else if (input.model === "release_catalogs") validateReleaseCatalogUpdateData(input.update);
1454
+ else if (input.model === "bundle_installations") validateInsightsInstallationUpdateData(input.update);
1250
1455
  else if (input.model === "api_keys") validateApiKeyUpdateData(input.update);
1251
1456
  else throw new DatabasePluginInputError("invalid-operation");
1252
1457
  validateSelect(input.model, input.select);
@@ -1322,10 +1527,90 @@ var createTransactionDatabasePlugin = /* @__PURE__ */ __name((implementation) =>
1322
1527
  // ../plugin-core/dist/createDatabasePlugin.mjs
1323
1528
  var PAGE_SIZE = 100;
1324
1529
  var compareChannelRows = /* @__PURE__ */ __name((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0, "compareChannelRows");
1530
+ var toInsightsBundleWhere = /* @__PURE__ */ __name((filter) => [
1531
+ {
1532
+ field: "platform",
1533
+ value: filter.platform
1534
+ },
1535
+ {
1536
+ field: "channel",
1537
+ value: filter.channel
1538
+ },
1539
+ {
1540
+ field: "type",
1541
+ value: filter.type
1542
+ },
1543
+ filter.type === "RECOVERED" ? {
1544
+ field: "from_bundle_id",
1545
+ value: filter.fromBundleId
1546
+ } : {
1547
+ field: "to_bundle_id",
1548
+ value: filter.toBundleId
1549
+ }
1550
+ ], "toInsightsBundleWhere");
1551
+ var toInsightsEventRanges = /* @__PURE__ */ __name((filter) => {
1552
+ if (filter.kind === "all") return [[]];
1553
+ if (filter.kind === "bundle") return [toInsightsBundleWhere(filter)];
1554
+ return ["UPDATE_APPLIED", "RECOVERED"].map((type) => [{
1555
+ field: "install_id",
1556
+ value: filter.installId
1557
+ }, {
1558
+ field: "type",
1559
+ value: type
1560
+ }]);
1561
+ }, "toInsightsEventRanges");
1562
+ var listInsightsEventRange = /* @__PURE__ */ __name(async (crud, input, filterWhere) => {
1563
+ const where = [...filterWhere, {
1564
+ field: "received_at_ms",
1565
+ operator: "gte",
1566
+ value: input.sinceMs ?? 0
1567
+ }];
1568
+ const sameTimestamp = input.after === void 0 ? [] : await crud.findMany({
1569
+ model: "bundle_events",
1570
+ where: [
1571
+ ...filterWhere,
1572
+ {
1573
+ field: "received_at_ms",
1574
+ value: input.after.receivedAtMs
1575
+ },
1576
+ {
1577
+ field: "id",
1578
+ operator: "lt",
1579
+ value: input.after.id
1580
+ }
1581
+ ],
1582
+ orderBy: [{
1583
+ field: "id",
1584
+ direction: "desc"
1585
+ }],
1586
+ limit: input.limit,
1587
+ offset: 0
1588
+ });
1589
+ if (sameTimestamp.length === input.limit) return sameTimestamp;
1590
+ const older = await crud.findMany({
1591
+ model: "bundle_events",
1592
+ where: [...where, {
1593
+ field: "received_at_ms",
1594
+ operator: "lt",
1595
+ value: input.after?.receivedAtMs ?? input.beforeReceivedAtMs
1596
+ }],
1597
+ orderBy: [{
1598
+ field: "received_at_ms",
1599
+ direction: "desc"
1600
+ }, {
1601
+ field: "id",
1602
+ direction: "desc"
1603
+ }],
1604
+ limit: input.limit - sameTimestamp.length,
1605
+ offset: 0
1606
+ });
1607
+ return [...sameTimestamp, ...older];
1608
+ }, "listInsightsEventRange");
1325
1609
  var DatabaseAtomicCommitUnsupportedError = class extends Error {
1326
1610
  static {
1327
1611
  __name(this, "DatabaseAtomicCommitUnsupportedError");
1328
1612
  }
1613
+ pluginName;
1329
1614
  name = "DatabaseAtomicCommitUnsupportedError";
1330
1615
  constructor(pluginName) {
1331
1616
  super(`Database plugin "${pluginName}" cannot atomically commit changes across models.`);
@@ -1345,6 +1630,7 @@ var DatabaseCommitConflictError = class extends Error {
1345
1630
  static {
1346
1631
  __name(this, "DatabaseCommitConflictError");
1347
1632
  }
1633
+ result;
1348
1634
  name = "DatabaseCommitConflictError";
1349
1635
  constructor(result) {
1350
1636
  super("Database commit precondition failed.");
@@ -1576,12 +1862,6 @@ var applyChange = /* @__PURE__ */ __name(async (database, change, changeIndex) =
1576
1862
  }
1577
1863
  return;
1578
1864
  }
1579
- case "insights":
1580
- await database.create({
1581
- model: "bundle_events",
1582
- data: change.row
1583
- });
1584
- return;
1585
1865
  case "apiKeys":
1586
1866
  switch (change.operation) {
1587
1867
  case "insert":
@@ -1650,11 +1930,11 @@ var applyChanges = /* @__PURE__ */ __name(async (database, input) => {
1650
1930
  for (const [changeIndex, change] of input.changes.entries()) await applyChange(database, change, changeIndex);
1651
1931
  return { committed: true };
1652
1932
  }, "applyChanges");
1653
- var hasOnlyKeys = /* @__PURE__ */ __name((value, keys) => {
1933
+ var hasOnlyKeys2 = /* @__PURE__ */ __name((value, keys) => {
1654
1934
  return Reflect.ownKeys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
1655
1935
  }, "hasOnlyKeys");
1656
1936
  var validateWhere2 = /* @__PURE__ */ __name((where, field, validateValue = (value) => typeof value === "string") => {
1657
- if (!isRecord(where) || !hasOnlyKeys(where, [field]) || !validateValue(Reflect.get(where, field))) throw new DatabasePluginInputError("invalid-data");
1937
+ if (!isRecord(where) || !hasOnlyKeys2(where, [field]) || !validateValue(Reflect.get(where, field))) throw new DatabasePluginInputError("invalid-data");
1658
1938
  }, "validateWhere");
1659
1939
  var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1660
1940
  if (!isRecord(change)) throw new DatabasePluginInputError("invalid-data");
@@ -1662,7 +1942,7 @@ var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1662
1942
  case "bundles":
1663
1943
  switch (change.operation) {
1664
1944
  case "insert":
1665
- if (!hasOnlyKeys(change, [
1945
+ if (!hasOnlyKeys2(change, [
1666
1946
  "model",
1667
1947
  "operation",
1668
1948
  "row"
@@ -1670,7 +1950,7 @@ var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1670
1950
  validateCreateData("bundles", change.row);
1671
1951
  return;
1672
1952
  case "update":
1673
- if (!hasOnlyKeys(change, [
1953
+ if (!hasOnlyKeys2(change, [
1674
1954
  "model",
1675
1955
  "operation",
1676
1956
  "where",
@@ -1680,7 +1960,7 @@ var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1680
1960
  validateBundleUpdateData(change.update);
1681
1961
  return;
1682
1962
  case "delete":
1683
- if (!hasOnlyKeys(change, [
1963
+ if (!hasOnlyKeys2(change, [
1684
1964
  "model",
1685
1965
  "operation",
1686
1966
  "where"
@@ -1693,7 +1973,7 @@ var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1693
1973
  case "bundlePatches":
1694
1974
  switch (change.operation) {
1695
1975
  case "insert":
1696
- if (!hasOnlyKeys(change, [
1976
+ if (!hasOnlyKeys2(change, [
1697
1977
  "model",
1698
1978
  "operation",
1699
1979
  "row"
@@ -1701,7 +1981,7 @@ var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1701
1981
  validateCreateData("bundle_patches", change.row);
1702
1982
  return;
1703
1983
  case "delete":
1704
- if (!hasOnlyKeys(change, [
1984
+ if (!hasOnlyKeys2(change, [
1705
1985
  "model",
1706
1986
  "operation",
1707
1987
  "where"
@@ -1714,7 +1994,7 @@ var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1714
1994
  case "releases":
1715
1995
  switch (change.operation) {
1716
1996
  case "insert":
1717
- if (!hasOnlyKeys(change, [
1997
+ if (!hasOnlyKeys2(change, [
1718
1998
  "model",
1719
1999
  "operation",
1720
2000
  "row"
@@ -1722,7 +2002,7 @@ var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1722
2002
  validateCreateData("releases", change.row);
1723
2003
  return;
1724
2004
  case "update":
1725
- if (!hasOnlyKeys(change, [
2005
+ if (!hasOnlyKeys2(change, [
1726
2006
  "model",
1727
2007
  "operation",
1728
2008
  "where",
@@ -1732,7 +2012,7 @@ var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1732
2012
  validateReleaseUpdateData(change.update);
1733
2013
  return;
1734
2014
  case "delete":
1735
- if (!hasOnlyKeys(change, [
2015
+ if (!hasOnlyKeys2(change, [
1736
2016
  "model",
1737
2017
  "operation",
1738
2018
  "where"
@@ -1743,7 +2023,7 @@ var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1743
2023
  throw new DatabasePluginInputError("invalid-operation");
1744
2024
  }
1745
2025
  case "releaseCatalogs":
1746
- if (change.operation !== "put" || !hasOnlyKeys(change, [
2026
+ if (change.operation !== "put" || !hasOnlyKeys2(change, [
1747
2027
  "model",
1748
2028
  "operation",
1749
2029
  "row"
@@ -1753,7 +2033,7 @@ var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1753
2033
  case "channels":
1754
2034
  switch (change.operation) {
1755
2035
  case "insert":
1756
- if (!hasOnlyKeys(change, [
2036
+ if (!hasOnlyKeys2(change, [
1757
2037
  "model",
1758
2038
  "operation",
1759
2039
  "row",
@@ -1762,7 +2042,7 @@ var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1762
2042
  validateCreateData("channels", change.row);
1763
2043
  return;
1764
2044
  case "delete":
1765
- if (!hasOnlyKeys(change, [
2045
+ if (!hasOnlyKeys2(change, [
1766
2046
  "model",
1767
2047
  "operation",
1768
2048
  "where"
@@ -1772,18 +2052,10 @@ var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1772
2052
  default:
1773
2053
  throw new DatabasePluginInputError("invalid-operation");
1774
2054
  }
1775
- case "insights":
1776
- if (change.operation !== "insert" || !hasOnlyKeys(change, [
1777
- "model",
1778
- "operation",
1779
- "row"
1780
- ])) throw new DatabasePluginInputError("invalid-operation");
1781
- validateCreateData("bundle_events", change.row);
1782
- return;
1783
2055
  case "apiKeys":
1784
2056
  switch (change.operation) {
1785
2057
  case "insert":
1786
- if (!hasOnlyKeys(change, [
2058
+ if (!hasOnlyKeys2(change, [
1787
2059
  "model",
1788
2060
  "operation",
1789
2061
  "row",
@@ -1792,7 +2064,7 @@ var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1792
2064
  validateCreateData("api_keys", change.row);
1793
2065
  return;
1794
2066
  case "update":
1795
- if (!hasOnlyKeys(change, [
2067
+ if (!hasOnlyKeys2(change, [
1796
2068
  "model",
1797
2069
  "operation",
1798
2070
  "where",
@@ -1800,7 +2072,7 @@ var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1800
2072
  ])) throw new DatabasePluginInputError("invalid-data");
1801
2073
  validateWhere2(change.where, "id");
1802
2074
  if (!isRecord(change.update)) throw new DatabasePluginInputError("invalid-data");
1803
- if (!hasOnlyKeys(change.update, ["revokedAtMs"])) throw new DatabasePluginInputError("invalid-data");
2075
+ if (!hasOnlyKeys2(change.update, ["revokedAtMs"])) throw new DatabasePluginInputError("invalid-data");
1804
2076
  validateApiKeyUpdateData({ revoked_at_ms: change.update.revokedAtMs });
1805
2077
  return;
1806
2078
  default:
@@ -1813,7 +2085,7 @@ var validateDatabaseChange = /* @__PURE__ */ __name((change) => {
1813
2085
  var validateDatabaseCommitExpectation = /* @__PURE__ */ __name((expectation) => {
1814
2086
  if (!isRecord(expectation)) throw new DatabasePluginInputError("invalid-data");
1815
2087
  if (expectation.model === "releases") {
1816
- if (!hasOnlyKeys(expectation, [
2088
+ if (!hasOnlyKeys2(expectation, [
1817
2089
  "model",
1818
2090
  "id",
1819
2091
  "revision"
@@ -1821,7 +2093,7 @@ var validateDatabaseCommitExpectation = /* @__PURE__ */ __name((expectation) =>
1821
2093
  return;
1822
2094
  }
1823
2095
  if (expectation.model === "releaseCatalogs") {
1824
- if (!hasOnlyKeys(expectation, [
2096
+ if (!hasOnlyKeys2(expectation, [
1825
2097
  "model",
1826
2098
  "scopeKey",
1827
2099
  "generation"
@@ -1831,7 +2103,7 @@ var validateDatabaseCommitExpectation = /* @__PURE__ */ __name((expectation) =>
1831
2103
  throw new DatabasePluginInputError("invalid-model");
1832
2104
  }, "validateDatabaseCommitExpectation");
1833
2105
  function validateDatabaseCommit(input) {
1834
- if (!isRecord(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");
2106
+ if (!isRecord(input) || !Array.isArray(input.changes) || (Object.hasOwn(input, "expectations") ? !hasOnlyKeys2(input, ["changes", "expectations"]) || !Array.isArray(input.expectations) : !hasOnlyKeys2(input, ["changes"]))) throw new DatabasePluginInputError("invalid-data");
1835
2107
  input.changes.forEach(validateDatabaseChange);
1836
2108
  if (Array.isArray(input.expectations)) input.expectations.forEach(validateDatabaseCommitExpectation);
1837
2109
  }
@@ -2052,36 +2324,82 @@ var createDatabasePluginAdapter = /* @__PURE__ */ __name((name, implementation)
2052
2324
  }
2053
2325
  },
2054
2326
  insights: {
2055
- async append(row) {
2056
- await crud.create({
2057
- model: "bundle_events",
2058
- data: row
2059
- });
2327
+ record: /* @__PURE__ */ __name((input) => implementation.recordInsights(input), "record"),
2328
+ async listEvents(input) {
2329
+ const ranges = await Promise.all(toInsightsEventRanges(input.filter).map((where) => listInsightsEventRange(crud, input, where)));
2330
+ if (ranges.length === 1) return ranges[0];
2331
+ return ranges.flat().sort((left, right) => right.received_at_ms - left.received_at_ms || compareInsightsText(right.id, left.id)).slice(0, input.limit);
2060
2332
  },
2061
- async scan(input) {
2062
- const rows = [];
2063
- for (let offset = 0; rows.length < input.limit; offset += PAGE_SIZE) {
2064
- const page = await crud.findMany({
2065
- model: "bundle_events",
2333
+ async findInstallations(input) {
2334
+ if ("installId" in input) {
2335
+ const row = await crud.findOne({
2336
+ model: "bundle_installations",
2066
2337
  where: [{
2338
+ field: "install_id",
2339
+ value: input.installId
2340
+ }]
2341
+ });
2342
+ return row === null ? [] : [row];
2343
+ }
2344
+ return crud.findMany({
2345
+ model: "bundle_installations",
2346
+ where: [{
2347
+ field: "user_id",
2348
+ value: input.userId
2349
+ }, ...input.afterInstallId === void 0 ? [] : [{
2350
+ field: "install_id",
2351
+ operator: "gt",
2352
+ value: input.afterInstallId
2353
+ }]],
2354
+ orderBy: [{
2355
+ field: "install_id",
2356
+ direction: "asc"
2357
+ }],
2358
+ limit: input.limit,
2359
+ offset: 0
2360
+ });
2361
+ },
2362
+ countInstallations(input) {
2363
+ return crud.count({
2364
+ model: "bundle_installations",
2365
+ where: [
2366
+ {
2367
+ field: "platform",
2368
+ value: input.platform
2369
+ },
2370
+ {
2371
+ field: "channel",
2372
+ value: input.channel
2373
+ },
2374
+ {
2375
+ field: "received_at_ms",
2376
+ operator: "gte",
2377
+ value: input.sinceMs
2378
+ },
2379
+ ...input.bundleId === void 0 ? [] : [{
2380
+ field: "to_bundle_id",
2381
+ value: input.bundleId
2382
+ }]
2383
+ ]
2384
+ });
2385
+ },
2386
+ countEvents(input) {
2387
+ return crud.count({
2388
+ model: "bundle_events",
2389
+ where: [
2390
+ ...toInsightsBundleWhere(input.filter),
2391
+ {
2392
+ field: "received_at_ms",
2393
+ operator: "gte",
2394
+ value: input.sinceMs
2395
+ },
2396
+ {
2067
2397
  field: "received_at_ms",
2068
2398
  operator: "lt",
2069
2399
  value: input.beforeReceivedAtMs
2070
- }],
2071
- orderBy: [{
2072
- field: "received_at_ms",
2073
- direction: "asc"
2074
- }, {
2075
- field: "id",
2076
- direction: "asc"
2077
- }],
2078
- limit: PAGE_SIZE,
2079
- offset
2080
- });
2081
- 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));
2082
- if (page.length < PAGE_SIZE) break;
2083
- }
2084
- return rows.slice(0, input.limit);
2400
+ }
2401
+ ]
2402
+ });
2085
2403
  }
2086
2404
  },
2087
2405
  apiKeys: {
@@ -2125,7 +2443,13 @@ var createDatabasePluginAdapter = /* @__PURE__ */ __name((name, implementation)
2125
2443
  ...implementation.dispose ? { dispose: implementation.dispose } : {}
2126
2444
  };
2127
2445
  }, "createDatabasePluginAdapter");
2128
- var createDatabasePlugin = /* @__PURE__ */ __name((options) => ({ ...options }), "createDatabasePlugin");
2446
+ var createDatabasePlugin = /* @__PURE__ */ __name((options) => ({
2447
+ ...options,
2448
+ models: {
2449
+ ...options.models,
2450
+ insights: createValidatedInsightsModel(options.models.insights)
2451
+ }
2452
+ }), "createDatabasePlugin");
2129
2453
 
2130
2454
  // ../plugin-core/dist/createStoragePlugin.mjs
2131
2455
  var createStoragePlugin = /* @__PURE__ */ __name((options) => ({ ...options }), "createStoragePlugin");
@@ -2267,6 +2591,8 @@ var DatabasePatchUpdateUnsupportedError = class extends Error {
2267
2591
  static {
2268
2592
  __name(this, "DatabasePatchUpdateUnsupportedError");
2269
2593
  }
2594
+ bundleId;
2595
+ pluginName;
2270
2596
  name = "DatabasePatchUpdateUnsupportedError";
2271
2597
  constructor(bundleId, pluginName) {
2272
2598
  super(`Database plugin "${pluginName}" cannot atomically replace patches for bundle "${bundleId}".`);
@@ -2355,7 +2681,8 @@ var hydrateRows = /* @__PURE__ */ __name(async (database, ownerRows) => {
2355
2681
  const patchRows = await database.models.bundlePatches.findByBundleIds(ownerRows.map(({ id }) => id));
2356
2682
  const ownerIds = new Set(ownerRows.map(({ id }) => id));
2357
2683
  const referencedIds = [...new Set(patchRows.map(({ base_bundle_id }) => base_bundle_id).filter((id) => !ownerIds.has(id)))];
2358
- return rowsToBundles(ownerRows, patchRows, referencedIds.length === 0 ? [] : await loadBundleRows(database, { id: { in: referencedIds } }));
2684
+ const referencedRows = referencedIds.length === 0 ? [] : await loadBundleRows(database, { id: { in: referencedIds } });
2685
+ return rowsToBundles(ownerRows, patchRows, referencedRows);
2359
2686
  }, "hydrateRows");
2360
2687
  var cursorIdFilter = /* @__PURE__ */ __name((cursor, direction) => {
2361
2688
  if (cursor?.after) return { [direction === "desc" ? "lt" : "gt"]: cursor.after };
@@ -2410,6 +2737,7 @@ var DatabaseBundleNotFoundError = class extends Error {
2410
2737
  static {
2411
2738
  __name(this, "DatabaseBundleNotFoundError");
2412
2739
  }
2740
+ bundleId;
2413
2741
  name = "DatabaseBundleNotFoundError";
2414
2742
  constructor(bundleId) {
2415
2743
  super(`Bundle "${bundleId}" was not found.`);
@@ -2420,6 +2748,8 @@ var DatabasePatchInsertUnsupportedError = class extends Error {
2420
2748
  static {
2421
2749
  __name(this, "DatabasePatchInsertUnsupportedError");
2422
2750
  }
2751
+ bundleId;
2752
+ pluginName;
2423
2753
  name = "DatabasePatchInsertUnsupportedError";
2424
2754
  constructor(bundleId, pluginName) {
2425
2755
  super(`Database plugin "${pluginName}" cannot atomically insert patches for bundle "${bundleId}".`);
@@ -2438,12 +2768,13 @@ var insertChanges = /* @__PURE__ */ __name((bundle) => [{
2438
2768
  }))], "insertChanges");
2439
2769
  var updateChanges = /* @__PURE__ */ __name((bundleId, update) => {
2440
2770
  const rowUpdate = bundleUpdateToRow(update);
2771
+ const patchesPresent = Object.hasOwn(update, "patches");
2441
2772
  return [{
2442
2773
  model: "bundles",
2443
2774
  operation: "update",
2444
2775
  where: { id: bundleId },
2445
2776
  update: rowUpdate
2446
- }, ...Object.hasOwn(update, "patches") ? [{
2777
+ }, ...patchesPresent ? [{
2447
2778
  model: "bundlePatches",
2448
2779
  operation: "delete",
2449
2780
  where: { bundleId }
@@ -2516,7 +2847,7 @@ var createDatabaseClient = /* @__PURE__ */ __name((plugin) => {
2516
2847
  };
2517
2848
  }, "createDatabaseClient");
2518
2849
 
2519
- // ../plugin-core/dist/node_modules/.pnpm/verkit@0.3.2/node_modules/verkit/dist/index.mjs
2850
+ // ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/comparison-DenM3wCn.mjs
2520
2851
  var LETTER_DASH_NUMBER = "[a-zA-Z0-9-]";
2521
2852
  var NUMERIC_IDENTIFIER = String.raw`0|[1-9]\d*`;
2522
2853
  var NUMERIC_IDENTIFIER_LOOSE = String.raw`\d+`;
@@ -2555,23 +2886,9 @@ function safeRegex(source, flags) {
2555
2886
  return new RegExp(makeSafeRegexSource(source), flags);
2556
2887
  }
2557
2888
  __name(safeRegex, "safeRegex");
2558
- var NUMERIC$1 = /^\d+$/;
2559
- function compareIdentifiers(left, right) {
2560
- if (typeof left === "number" && typeof right === "number") return left === right ? 0 : left < right ? -1 : 1;
2561
- const leftNumeric = NUMERIC$1.test(String(left));
2562
- const rightNumeric = NUMERIC$1.test(String(right));
2563
- const normalizedLeft = leftNumeric ? Number(left) : left;
2564
- const normalizedRight = rightNumeric ? Number(right) : right;
2565
- return normalizedLeft === normalizedRight ? 0 : leftNumeric && !rightNumeric ? -1 : rightNumeric && !leftNumeric ? 1 : normalizedLeft < normalizedRight ? -1 : 1;
2566
- }
2567
- __name(compareIdentifiers, "compareIdentifiers");
2568
2889
  var FULL = safeRegex(`^${FULL_PLAIN}$`);
2569
2890
  var LOOSE = safeRegex(`^${LOOSE_PLAIN}$`);
2570
- safeRegex(`^${PRERELEASE}$`);
2571
- safeRegex(`^${PRERELEASE_LOOSE}$`);
2572
- var COERCE_EXACT = safeRegex(COERCE);
2573
- var COERCE_FULL_EXACT = safeRegex(COERCE_FULL);
2574
- var NUMERIC = /^\d+$/;
2891
+ var NUMERIC$1 = /^\d+$/;
2575
2892
  function formatComparableVersion(version2) {
2576
2893
  const base = `${version2.major}.${version2.minor}.${version2.patch}`;
2577
2894
  return version2.prerelease?.length ? `${base}-${version2.prerelease.join(".")}` : base;
@@ -2594,7 +2911,7 @@ function parse(version2, options = {}) {
2594
2911
  if (minor > Number.MAX_SAFE_INTEGER || minor < 0) throw new TypeError(`Invalid minor version: ${match2[2]}`);
2595
2912
  if (patch > Number.MAX_SAFE_INTEGER || patch < 0) throw new TypeError(`Invalid patch version: ${match2[3]}`);
2596
2913
  const prerelease = match2[4] ? match2[4].split(".").map((identifier) => {
2597
- if (NUMERIC.test(identifier)) {
2914
+ if (NUMERIC$1.test(identifier)) {
2598
2915
  const numeric = Number(identifier);
2599
2916
  if (numeric >= 0 && numeric < Number.MAX_SAFE_INTEGER) return numeric;
2600
2917
  }
@@ -2617,6 +2934,16 @@ function tryParse(version2, options = {}) {
2617
2934
  }
2618
2935
  }
2619
2936
  __name(tryParse, "tryParse");
2937
+ var NUMERIC = /^\d+$/;
2938
+ function compareIdentifiers(left, right) {
2939
+ if (typeof left === "number" && typeof right === "number") return left === right ? 0 : left < right ? -1 : 1;
2940
+ const leftNumeric = NUMERIC.test(String(left));
2941
+ const rightNumeric = NUMERIC.test(String(right));
2942
+ const normalizedLeft = leftNumeric ? Number(left) : left;
2943
+ const normalizedRight = rightNumeric ? Number(right) : right;
2944
+ return normalizedLeft === normalizedRight ? 0 : leftNumeric && !rightNumeric ? -1 : rightNumeric && !leftNumeric ? 1 : normalizedLeft < normalizedRight ? -1 : 1;
2945
+ }
2946
+ __name(compareIdentifiers, "compareIdentifiers");
2620
2947
  function compareMainParsed(left, right) {
2621
2948
  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;
2622
2949
  }
@@ -2641,29 +2968,18 @@ function compareParsed(left, right) {
2641
2968
  return compareMainParsed(left, right) || comparePrereleaseParsed(left, right);
2642
2969
  }
2643
2970
  __name(compareParsed, "compareParsed");
2644
- function coerceParsedVersion(value, options = {}) {
2645
- if (typeof value === "object") return value;
2646
- const input = typeof value === "number" ? String(value) : value;
2647
- if (typeof input !== "string") return null;
2648
- let match2 = null;
2649
- if (options.rtl) {
2650
- const expression = safeRegex(options.includePrerelease ? COERCE_FULL : COERCE, "g");
2651
- let next;
2652
- while ((next = expression.exec(input)) && (!match2 || match2.index + match2[0].length !== input.length)) {
2653
- if (!match2 || next.index + next[0].length !== match2.index + match2[0].length) match2 = next;
2654
- expression.lastIndex = next.index + next[1].length + next[2].length;
2655
- }
2656
- } else match2 = (options.includePrerelease ? COERCE_FULL_EXACT : COERCE_EXACT).exec(input);
2657
- if (!match2) return null;
2658
- const major = match2[2];
2659
- return tryParse(`${major}.${match2[3] || "0"}.${match2[4] || "0"}${options.includePrerelease && match2[5] ? `-${match2[5]}` : ""}${options.includePrerelease && match2[6] ? `+${match2[6]}` : ""}`, options);
2971
+ function compare(left, right, options = {}) {
2972
+ return compareParsed(parse(left, options), parse(right, options));
2660
2973
  }
2661
- __name(coerceParsedVersion, "coerceParsedVersion");
2974
+ __name(compare, "compare");
2975
+
2976
+ // ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/set-CC5YeoYX.mjs
2662
2977
  var STRICT_COMPARATOR = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*(${FULL_PLAIN})$|^$`);
2663
- var LOOSE_COMPARATOR$1 = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*(${LOOSE_PLAIN})$|^$`);
2978
+ var LOOSE_COMPARATOR = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*(${LOOSE_PLAIN})$|^$`);
2664
2979
  function parseComparator(comparator, options = {}) {
2980
+ if (typeof comparator !== "string") return comparator;
2665
2981
  const normalized = comparator.trim().replaceAll(/\s+/g, " ");
2666
- const match2 = normalized.match(options.loose ? LOOSE_COMPARATOR$1 : STRICT_COMPARATOR);
2982
+ const match2 = normalized.match(options.loose ? LOOSE_COMPARATOR : STRICT_COMPARATOR);
2667
2983
  if (!match2) throw new TypeError(`Invalid comparator: ${normalized}`);
2668
2984
  const operator = match2[1] === "=" ? "" : match2[1] || "";
2669
2985
  const version2 = match2[2] ? parse(match2[2], options) : null;
@@ -2675,10 +2991,8 @@ function parseComparator(comparator, options = {}) {
2675
2991
  };
2676
2992
  }
2677
2993
  __name(parseComparator, "parseComparator");
2678
- function compare(left, right, options = {}) {
2679
- return compareParsed(parse(left, options), parse(right, options));
2680
- }
2681
- __name(compare, "compare");
2994
+
2995
+ // ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/range-DvX-Y6iv.mjs
2682
2996
  var BUILD_STRIP = new RegExp(BUILD, "g");
2683
2997
  var BUILD_SAFE = safeRegex(BUILD);
2684
2998
  var STRICT_HYPHEN = safeRegex(String.raw`^\s*(${XRANGE_PLAIN})\s+-\s+(${XRANGE_PLAIN})\s*$`);
@@ -2695,7 +3009,7 @@ var LOOSE_XRANGE = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*${XRANGE_PLAIN_L
2695
3009
  var STAR = safeRegex(String.raw`(<|>)?=?\s*\*`);
2696
3010
  var GTE_ZERO = /^\s*>=\s*0\.0\.0\s*$/;
2697
3011
  var GTE_ZERO_PRERELEASE = /^\s*>=\s*0\.0\.0-0\s*$/;
2698
- var LOOSE_COMPARATOR = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*(${LOOSE_PLAIN})$|^$`);
3012
+ var LOOSE_COMPARATOR2 = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*(${LOOSE_PLAIN})$|^$`);
2699
3013
  function isWildcard(value) {
2700
3014
  return !value || String(value).toLowerCase() === "x" || String(value) === "*";
2701
3015
  }
@@ -2802,7 +3116,7 @@ function expandComparator(comparator, options) {
2802
3116
  __name(expandComparator, "expandComparator");
2803
3117
  function parseSimpleRange(input, options) {
2804
3118
  let parts = replaceHyphenRange(input.replace(BUILD_STRIP, ""), options).replace(COMPARATOR_TRIM, "$1$2$3").replace(TILDE_TRIM, "$1~").replace(CARET_TRIM, "$1^").split(" ").map((part) => expandComparator(part, options)).join(" ").split(/\s+/).map((part) => part.trim().replace(options.includePrerelease ? GTE_ZERO_PRERELEASE : GTE_ZERO, ""));
2805
- if (options.loose) parts = parts.filter((part) => LOOSE_COMPARATOR.test(part));
3119
+ if (options.loose) parts = parts.filter((part) => LOOSE_COMPARATOR2.test(part));
2806
3120
  const unique = /* @__PURE__ */ new Map();
2807
3121
  for (const comparator of parts.map((part) => parseComparator(part, options))) {
2808
3122
  if (comparator.value === "<0.0.0-0") return [comparator];
@@ -2815,9 +3129,9 @@ __name(parseSimpleRange, "parseSimpleRange");
2815
3129
  function parseRange(range, options = {}) {
2816
3130
  if (typeof range !== "string") return range;
2817
3131
  const parsedOptions = { ...options };
2818
- const raw2 = range.trim().replaceAll(/\s+/g, " ");
2819
- let sets = raw2.split("||").map((part) => parseSimpleRange(part.trim(), parsedOptions)).filter((set) => set.length);
2820
- if (!sets.length) throw new TypeError(`Range contains no valid comparator sets: ${raw2}`);
3132
+ const normalizedRange = range.trim().replaceAll(/\s+/g, " ");
3133
+ let sets = normalizedRange.split("||").map((part) => parseSimpleRange(part.trim(), parsedOptions)).filter((set) => set.length);
3134
+ if (!sets.length) throw new TypeError(`Range contains no valid comparator sets: ${normalizedRange}`);
2821
3135
  if (sets.length > 1) {
2822
3136
  const first = sets[0];
2823
3137
  sets = sets.filter((set) => set[0]?.value !== "<0.0.0-0");
@@ -2828,21 +3142,47 @@ function parseRange(range, options = {}) {
2828
3142
  }
2829
3143
  }
2830
3144
  return {
2831
- normalized: sets.map((set) => set.map((comparator) => comparator.value).join(" ")).join("||"),
2832
3145
  options: parsedOptions,
2833
- raw: raw2,
2834
3146
  sets
2835
3147
  };
2836
3148
  }
2837
3149
  __name(parseRange, "parseRange");
3150
+
3151
+ // ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/version-CQ98ZBpL.mjs
3152
+ var COERCE_EXACT = safeRegex(COERCE);
3153
+ var COERCE_FULL_EXACT = safeRegex(COERCE_FULL);
3154
+ safeRegex(`^${PRERELEASE}$`);
3155
+ safeRegex(`^${PRERELEASE_LOOSE}$`);
3156
+ function normalizeFull(version2, options = {}) {
3157
+ const parsed = tryParse(version2, options);
3158
+ return parsed ? formatFullVersion(parsed) : null;
3159
+ }
3160
+ __name(normalizeFull, "normalizeFull");
2838
3161
  function normalize(version2, options = {}) {
2839
3162
  const parsed = tryParse(version2, options);
2840
3163
  return parsed ? formatComparableVersion(parsed) : null;
2841
3164
  }
2842
3165
  __name(normalize, "normalize");
2843
3166
  function coerce(value, options = {}) {
2844
- const parsed = coerceParsedVersion(value, options);
2845
- return parsed ? formatFullVersion(parsed) : null;
3167
+ if (typeof value === "object") return value;
3168
+ const input = typeof value === "number" ? String(value) : value;
3169
+ let match2 = null;
3170
+ if (options.rtl) {
3171
+ const source = options.includePrerelease ? COERCE_FULL : COERCE;
3172
+ const expression = safeRegex(source, "g");
3173
+ let next;
3174
+ while ((next = expression.exec(input)) && (!match2 || match2.index + match2[0].length !== input.length)) {
3175
+ if (!match2 || next.index + next[0].length !== match2.index + match2[0].length) match2 = next;
3176
+ expression.lastIndex = next.index + next[1].length + next[2].length;
3177
+ }
3178
+ } else match2 = (options.includePrerelease ? COERCE_FULL_EXACT : COERCE_EXACT).exec(input);
3179
+ if (!match2) return null;
3180
+ const major = match2[2];
3181
+ const minor = match2[3] || "0";
3182
+ const patch = match2[4] || "0";
3183
+ const prerelease = options.includePrerelease && match2[5] ? `-${match2[5]}` : "";
3184
+ const build = options.includePrerelease && match2[6] ? `+${match2[6]}` : "";
3185
+ return tryParse(`${major}.${minor}.${patch}${prerelease}${build}`, options);
2846
3186
  }
2847
3187
  __name(coerce, "coerce");
2848
3188
 
@@ -3139,15 +3479,16 @@ function compileAppVersion(releases) {
3139
3479
  const retainedIds = new Set(segmentReleases.flatMap(({ retainedIds: retainedIds2, rollbackIds }) => [...retainedIds2, ...rollbackIds]));
3140
3480
  const retainedReleases = releases.filter((release) => retainedIds.has(release.id));
3141
3481
  const descriptorIndex = new Map(retainedReleases.map((release, index) => [release.id, index]));
3482
+ const segments = mergeSegments(segmentReleases.map(({ segment, retainedIds: retainedIds2, rollbackIds }) => ({
3483
+ ...segment,
3484
+ releaseIndexes: releases.filter((release) => retainedIds2.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0),
3485
+ rollbackReleaseIndexes: releases.filter((release) => rollbackIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0)
3486
+ })).filter((segment) => segment.releaseIndexes.length > 0 || segment.rollbackReleaseIndexes.length > 0));
3142
3487
  return {
3143
3488
  envelope: {
3144
3489
  fallbackPolicy: RELEASE_CATALOG_FALLBACK_POLICY,
3145
3490
  schemaVersion: RELEASE_CATALOG_SCHEMA_VERSION,
3146
- segments: mergeSegments(segmentReleases.map(({ segment, retainedIds: retainedIds2, rollbackIds }) => ({
3147
- ...segment,
3148
- releaseIndexes: releases.filter((release) => retainedIds2.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0),
3149
- rollbackReleaseIndexes: releases.filter((release) => rollbackIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0)
3150
- })).filter((segment) => segment.releaseIndexes.length > 0 || segment.rollbackReleaseIndexes.length > 0)),
3491
+ segments,
3151
3492
  strategy: "APP_VERSION"
3152
3493
  },
3153
3494
  retainedReleases
@@ -3221,7 +3562,8 @@ function versionInSegment(version2, segment) {
3221
3562
  }
3222
3563
  __name(versionInSegment, "versionInSegment");
3223
3564
  function canonicalizeAppVersion(appVersion) {
3224
- return coerce(appVersion);
3565
+ const version2 = coerce(appVersion);
3566
+ return version2 ? normalizeFull(version2) : null;
3225
3567
  }
3226
3568
  __name(canonicalizeAppVersion, "canonicalizeAppVersion");
3227
3569
  function projectCompiledCatalog(catalog, appVersion) {
@@ -3255,6 +3597,7 @@ var ReleaseCatalogMutationError = class extends Error {
3255
3597
  static {
3256
3598
  __name(this, "ReleaseCatalogMutationError");
3257
3599
  }
3600
+ code;
3258
3601
  name = "ReleaseCatalogMutationError";
3259
3602
  constructor(code, message) {
3260
3603
  super(message);
@@ -3521,6 +3864,7 @@ var ReleaseManagementError = class extends Error {
3521
3864
  static {
3522
3865
  __name(this, "ReleaseManagementError");
3523
3866
  }
3867
+ code;
3524
3868
  name = "ReleaseManagementError";
3525
3869
  constructor(code, message) {
3526
3870
  super(message);
@@ -3643,7 +3987,8 @@ var createStorageDownloadUrl = /* @__PURE__ */ __name((signingKey) => {
3643
3987
  name: "HMAC"
3644
3988
  }, false, ["sign"]);
3645
3989
  return async ({ storageUri }) => {
3646
- return { url: createStorageDownloadPath(storageUri, encodeBytes(new Uint8Array(await crypto.subtle.sign("HMAC", await key, new TextEncoder().encode(storageUri))))) };
3990
+ const signature = encodeBytes(new Uint8Array(await crypto.subtle.sign("HMAC", await key, new TextEncoder().encode(storageUri))));
3991
+ return { url: createStorageDownloadPath(storageUri, signature) };
3647
3992
  };
3648
3993
  }, "createStorageDownloadUrl");
3649
3994
 
@@ -3875,16 +4220,6 @@ var createReleaseManagementRouteHandlers = /* @__PURE__ */ __name(() => ({
3875
4220
  }), "createReleaseManagementRouteHandlers");
3876
4221
 
3877
4222
  // ../../packages/server/dist/insights/errors.mjs
3878
- var InsightsScanLimitExceededError = class extends Error {
3879
- static {
3880
- __name(this, "InsightsScanLimitExceededError");
3881
- }
3882
- constructor(limit) {
3883
- super(`Insights event scan exceeded ${limit} rows.`);
3884
- this.limit = limit;
3885
- this.name = "InsightsScanLimitExceededError";
3886
- }
3887
- };
3888
4223
  var InsightsBadRequestError = class extends Error {
3889
4224
  static {
3890
4225
  __name(this, "InsightsBadRequestError");
@@ -3895,6 +4230,7 @@ var InsightsPayloadTooLargeError = class extends Error {
3895
4230
  static {
3896
4231
  __name(this, "InsightsPayloadTooLargeError");
3897
4232
  }
4233
+ maximumBytes;
3898
4234
  name = "InsightsPayloadTooLargeError";
3899
4235
  constructor(maximumBytes) {
3900
4236
  super(`Event payload exceeds ${maximumBytes} bytes`);
@@ -3904,7 +4240,8 @@ var InsightsPayloadTooLargeError = class extends Error {
3904
4240
 
3905
4241
  // ../../packages/server/dist/insights/eventInput.mjs
3906
4242
  var MAX_EVENT_STRING_LENGTH = 1024;
3907
- var EVENT_BODY_MAX_BYTES = 16 * 1024;
4243
+ var MAX_IDENTITY_LENGTH = 255;
4244
+ var EVENT_BODY_MAX_BYTES = 16384;
3908
4245
  var eventKeys = /* @__PURE__ */ new Set([
3909
4246
  "type",
3910
4247
  "installId",
@@ -3928,7 +4265,7 @@ function isRecord3(value) {
3928
4265
  __name(isRecord3, "isRecord");
3929
4266
  function requireStringField(payload, key) {
3930
4267
  const value = payload[key];
3931
- if (typeof value !== "string" || value.length === 0 || value.length > MAX_EVENT_STRING_LENGTH) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
4268
+ 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}`);
3932
4269
  return value;
3933
4270
  }
3934
4271
  __name(requireStringField, "requireStringField");
@@ -3937,6 +4274,12 @@ function requireNullableStringField(payload, key) {
3937
4274
  return requireStringField(payload, key);
3938
4275
  }
3939
4276
  __name(requireNullableStringField, "requireNullableStringField");
4277
+ function requireIdentityField(payload, key) {
4278
+ const value = requireStringField(payload, key);
4279
+ if (value.length > MAX_IDENTITY_LENGTH) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
4280
+ return value;
4281
+ }
4282
+ __name(requireIdentityField, "requireIdentityField");
3940
4283
  async function readBoundedText(request) {
3941
4284
  const contentLength = request.headers.get("content-length");
3942
4285
  const declaredByteLength = Number(contentLength);
@@ -3974,9 +4317,9 @@ function requireEvent(payload) {
3974
4317
  const platform = requireStringField(payload, "platform");
3975
4318
  if (platform !== "ios" && platform !== "android") throw new InsightsBadRequestError("Invalid event field: platform");
3976
4319
  const base = {
3977
- installId: requireStringField(payload, "installId"),
4320
+ installId: requireIdentityField(payload, "installId"),
3978
4321
  toBundleId: requireStringField(payload, "toBundleId"),
3979
- ...payload.userId === void 0 ? {} : { userId: requireStringField(payload, "userId") },
4322
+ ...payload.userId === void 0 ? {} : { userId: requireIdentityField(payload, "userId") },
3980
4323
  ...payload.username === void 0 ? {} : { username: requireStringField(payload, "username") },
3981
4324
  platform,
3982
4325
  appVersion: requireStringField(payload, "appVersion"),
@@ -4018,60 +4361,133 @@ async function parseBundleEventRequest(request) {
4018
4361
  return requireEvent(await parseJson(request));
4019
4362
  }
4020
4363
  __name(parseBundleEventRequest, "parseBundleEventRequest");
4364
+ function createBundleEventRow(input) {
4365
+ input = requireEvent(input);
4366
+ const base = {
4367
+ app_version: input.appVersion,
4368
+ channel: input.channel,
4369
+ cohort: input.cohort,
4370
+ fingerprint_hash: input.fingerprintHash,
4371
+ from_release_id: input.fromReleaseId,
4372
+ id: createUUIDv7(),
4373
+ install_id: input.installId,
4374
+ platform: input.platform,
4375
+ received_at_ms: Date.now(),
4376
+ sdk_version: input.sdkVersion ?? null,
4377
+ to_bundle_id: input.toBundleId,
4378
+ to_release_id: input.toReleaseId,
4379
+ user_id: input.userId ?? null,
4380
+ username: input.username ?? null
4381
+ };
4382
+ switch (input.type) {
4383
+ case "UPDATE_APPLIED":
4384
+ case "RECOVERED":
4385
+ case "RELEASE_ADOPTED":
4386
+ return {
4387
+ ...base,
4388
+ from_bundle_id: input.fromBundleId,
4389
+ type: input.type,
4390
+ update_strategy: input.updateStrategy
4391
+ };
4392
+ case "UNCHANGED":
4393
+ return {
4394
+ ...base,
4395
+ from_bundle_id: null,
4396
+ type: input.type,
4397
+ update_strategy: null
4398
+ };
4399
+ }
4400
+ }
4401
+ __name(createBundleEventRow, "createBundleEventRow");
4021
4402
 
4022
4403
  // ../../packages/server/dist/insights/queryInput.mjs
4023
- var EVENT_LIST_BOUNDS = {
4024
- defaultValue: 50,
4025
- maximum: 100,
4026
- minimum: 1
4027
- };
4028
- var EVENT_LIST_OFFSET_BOUNDS = {
4029
- defaultValue: 0,
4030
- minimum: 0
4031
- };
4032
- var MAX_USER_ID_LENGTH = 1024;
4033
- function parseInteger(url, key, bounds) {
4034
- const value = url.searchParams.get(key);
4035
- if (value === null) return bounds.defaultValue;
4036
- const parsed = Number(value);
4037
- if (!Number.isSafeInteger(parsed) || parsed < bounds.minimum || bounds.maximum !== void 0 && parsed > bounds.maximum) throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
4038
- return parsed;
4039
- }
4040
- __name(parseInteger, "parseInteger");
4041
- var parsePagination = /* @__PURE__ */ __name((request) => {
4404
+ var MAX_PAGE_LIMIT = 100;
4405
+ var MAX_IDENTITY_LENGTH2 = 255;
4406
+ var MAX_CURSOR_LENGTH = 8192;
4407
+ var readSingle = /* @__PURE__ */ __name((url, key) => {
4408
+ const values = url.searchParams.getAll(key);
4409
+ if (values.length > 1) throw new InsightsBadRequestError(`Duplicate '${key}' query parameter.`);
4410
+ return values[0];
4411
+ }, "readSingle");
4412
+ var readPageLimit = /* @__PURE__ */ __name((url) => {
4413
+ const value = readSingle(url, "limit");
4414
+ if (value === void 0) return void 0;
4415
+ const limit = Number(value);
4416
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) throw new InsightsBadRequestError("Invalid 'limit' query parameter.");
4417
+ return limit;
4418
+ }, "readPageLimit");
4419
+ var readCursor = /* @__PURE__ */ __name((url) => {
4420
+ const cursor = readSingle(url, "cursor");
4421
+ if (cursor !== void 0 && (cursor.length === 0 || cursor.length > MAX_CURSOR_LENGTH)) throw new InsightsBadRequestError("Invalid 'cursor' query parameter.");
4422
+ return cursor;
4423
+ }, "readCursor");
4424
+ var readId = /* @__PURE__ */ __name((url, key, maximumLength = MAX_IDENTITY_LENGTH2) => {
4425
+ const value = readSingle(url, key);
4426
+ if (value === void 0 || value.length === 0 || value.length > maximumLength) throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
4427
+ return value;
4428
+ }, "readId");
4429
+ var readTimestamp = /* @__PURE__ */ __name((url, key) => {
4430
+ const raw2 = readSingle(url, key);
4431
+ if (raw2 === void 0) return void 0;
4432
+ const value = Number(raw2);
4433
+ if (!raw2.length || !Number.isSafeInteger(value) || value < 0) throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
4434
+ return value;
4435
+ }, "readTimestamp");
4436
+ var readScope = /* @__PURE__ */ __name((url) => {
4437
+ const platform = readSingle(url, "platform");
4438
+ if (platform !== "ios" && platform !== "android") throw new InsightsBadRequestError("Invalid 'platform' query parameter.");
4439
+ return {
4440
+ platform,
4441
+ channel: readId(url, "channel", 1024)
4442
+ };
4443
+ }, "readScope");
4444
+ var parseEventPageInput = /* @__PURE__ */ __name((request) => {
4042
4445
  const url = new URL(request.url);
4446
+ const bundleFields = [
4447
+ "bundleId",
4448
+ "outcome",
4449
+ "platform",
4450
+ "channel"
4451
+ ];
4452
+ let bundle;
4453
+ if (bundleFields.some((key) => url.searchParams.has(key))) {
4454
+ const outcome = readSingle(url, "outcome");
4455
+ if (outcome !== "applied" && outcome !== "recovered" && outcome !== "adopted") throw new InsightsBadRequestError("Invalid 'outcome' query parameter.");
4456
+ bundle = {
4457
+ ...readScope(url),
4458
+ bundleId: readId(url, "bundleId", 1024),
4459
+ outcome
4460
+ };
4461
+ }
4043
4462
  return {
4044
- limit: parseInteger(url, "limit", EVENT_LIST_BOUNDS),
4045
- offset: parseInteger(url, "offset", EVENT_LIST_OFFSET_BOUNDS)
4463
+ beforeReceivedAtMs: readTimestamp(url, "beforeReceivedAtMs"),
4464
+ sinceMs: readTimestamp(url, "sinceMs"),
4465
+ cursor: readCursor(url),
4466
+ limit: readPageLimit(url),
4467
+ ...bundle === void 0 ? {} : { bundle }
4046
4468
  };
4047
- }, "parsePagination");
4048
- var parseInsightsQuery = /* @__PURE__ */ __name((request) => {
4049
- const window = new URL(request.url).searchParams.get("window") ?? "24h";
4050
- if (window !== "24h" && window !== "7d" && window !== "30d" && window !== "all") throw new InsightsBadRequestError("Invalid 'window' query parameter.");
4469
+ }, "parseEventPageInput");
4470
+ var parseUserInstallationPageInput = /* @__PURE__ */ __name((request) => {
4471
+ const url = new URL(request.url);
4472
+ const cursor = readCursor(url);
4473
+ const limit = readPageLimit(url);
4051
4474
  return {
4052
- ...parsePagination(request),
4053
- window
4475
+ userId: readId(url, "userId"),
4476
+ ...cursor === void 0 ? {} : { cursor },
4477
+ ...limit === void 0 ? {} : { limit }
4054
4478
  };
4055
- }, "parseInsightsQuery");
4056
- var parseActiveInstallationInput = /* @__PURE__ */ __name((request) => {
4479
+ }, "parseUserInstallationPageInput");
4480
+ var parseReportingOverviewInput = /* @__PURE__ */ __name((request) => {
4057
4481
  const url = new URL(request.url);
4058
- const windows = url.searchParams.getAll("window");
4059
- if (windows.length > 1) throw new InsightsBadRequestError("Duplicate 'window' query parameter.");
4060
- const window = windows[0] ?? "30d";
4482
+ const window = readSingle(url, "window") ?? "30d";
4061
4483
  if (window !== "24h" && window !== "7d" && window !== "30d") throw new InsightsBadRequestError("Invalid 'window' query parameter.");
4062
- const userIds = url.searchParams.getAll("userId");
4063
- if (userIds.length > 1) throw new InsightsBadRequestError("Duplicate 'userId' query parameter.");
4064
- const userId = userIds[0];
4065
- if (userId !== void 0 && (userId.length === 0 || userId.length > MAX_USER_ID_LENGTH)) throw new InsightsBadRequestError("Invalid 'userId' query parameter.");
4066
- return userId === void 0 ? { window } : {
4484
+ const bundleId = readSingle(url, "bundleId");
4485
+ return {
4486
+ ...readScope(url),
4067
4487
  window,
4068
- userId
4488
+ ...bundleId === void 0 ? {} : { bundleId: readId(url, "bundleId", 1024) }
4069
4489
  };
4070
- }, "parseActiveInstallationInput");
4071
- var parseSearchInput = /* @__PURE__ */ __name((request) => ({
4072
- ...parsePagination(request),
4073
- query: new URL(request.url).searchParams.get("query")?.trim() ?? ""
4074
- }), "parseSearchInput");
4490
+ }, "parseReportingOverviewInput");
4075
4491
 
4076
4492
  // ../../packages/server/dist/insights/routes.mjs
4077
4493
  var json = /* @__PURE__ */ __name((body, status) => Response.json(body, {
@@ -4081,7 +4497,11 @@ var json = /* @__PURE__ */ __name((body, status) => Response.json(body, {
4081
4497
  var requireParam = /* @__PURE__ */ __name((params, key) => {
4082
4498
  const value = params[key];
4083
4499
  if (value === void 0 || value.length === 0) throw new InsightsBadRequestError(`Missing route parameter: ${key}`);
4084
- return value;
4500
+ try {
4501
+ return decodeURIComponent(value);
4502
+ } catch {
4503
+ throw new InsightsBadRequestError(`Invalid route parameter: ${key}`);
4504
+ }
4085
4505
  }, "requireParam");
4086
4506
  var run = /* @__PURE__ */ __name(async (operation) => {
4087
4507
  try {
@@ -4089,10 +4509,6 @@ var run = /* @__PURE__ */ __name(async (operation) => {
4089
4509
  } catch (error) {
4090
4510
  if (error instanceof InsightsBadRequestError) return json({ error: error.message }, 400);
4091
4511
  if (error instanceof InsightsPayloadTooLargeError) return json({ error: error.message }, 413);
4092
- if (error instanceof InsightsScanLimitExceededError) return json({ error: {
4093
- code: "INSIGHTS_SCAN_LIMIT_EXCEEDED",
4094
- limit: error.limit
4095
- } }, 503);
4096
4512
  throw error;
4097
4513
  }
4098
4514
  }, "run");
@@ -4102,32 +4518,27 @@ var createInsightsRouteHandlers = /* @__PURE__ */ __name((provider) => ({
4102
4518
  await provider.appendBundleEvent(await parseBundleEventRequest(request));
4103
4519
  return new Response(null, { status: 204 });
4104
4520
  }), "appendBundleEvent"),
4105
- getBundleEventSummary: /* @__PURE__ */ __name((params) => query(() => provider.getBundleEventSummary(requireParam(params, "id"))), "getBundleEventSummary"),
4106
- getBundleEventInsights: /* @__PURE__ */ __name((params, request) => query(() => {
4107
- const input = parseInsightsQuery(request);
4108
- return provider.getBundleEventInsights(requireParam(params, "id"), input.window, input.limit, input.offset);
4109
- }), "getBundleEventInsights"),
4110
- getBundleEventOverview: /* @__PURE__ */ __name(() => query(() => provider.getBundleEventOverview()), "getBundleEventOverview"),
4111
- getActiveInstallationOverview: /* @__PURE__ */ __name((_params, request) => query(() => provider.getActiveInstallationOverview(parseActiveInstallationInput(request))), "getActiveInstallationOverview"),
4112
- searchInstallations: /* @__PURE__ */ __name((_params, request) => query(() => {
4113
- const input = parseSearchInput(request);
4114
- return provider.searchInstallations(input.query, input.limit, input.offset);
4115
- }), "searchInstallations"),
4116
- getInstallationHistory: /* @__PURE__ */ __name((params, request) => query(() => {
4117
- const input = parsePagination(request);
4118
- return provider.getInstallationHistory(requireParam(params, "installId"), input.limit, input.offset);
4119
- }), "getInstallationHistory")
4521
+ getReportingOverview: /* @__PURE__ */ __name((_params, request) => query(() => provider.getReportingOverview(parseReportingOverviewInput(request))), "getReportingOverview"),
4522
+ getInstallation: /* @__PURE__ */ __name((params) => run(async () => {
4523
+ const installation = await provider.getInstallation({ installId: requireParam(params, "installId") });
4524
+ return installation === null ? json({ error: "Installation not found" }, 404) : json(installation, 200);
4525
+ }), "getInstallation"),
4526
+ listEvents: /* @__PURE__ */ __name((_params, request) => query(() => provider.listEvents(parseEventPageInput(request))), "listEvents"),
4527
+ listInstallationEvents: /* @__PURE__ */ __name((params, request) => query(() => provider.listInstallationEvents({
4528
+ ...parseEventPageInput(request),
4529
+ installId: requireParam(params, "installId")
4530
+ })), "listInstallationEvents"),
4531
+ pageInstallationsByCurrentUserId: /* @__PURE__ */ __name((_params, request) => query(() => provider.pageInstallationsByCurrentUserId(parseUserInstallationPageInput(request))), "pageInstallationsByCurrentUserId")
4120
4532
  }), "createInsightsRouteHandlers");
4121
4533
  var registerInsightsClientRoutes = /* @__PURE__ */ __name((add) => {
4122
4534
  add("POST", "/events", "appendBundleEvent");
4123
4535
  }, "registerInsightsClientRoutes");
4124
4536
  var registerInsightsAdminRoutes = /* @__PURE__ */ __name((add) => {
4125
- add("GET", "/bundles/:id/events/summary", "getBundleEventSummary");
4126
- add("GET", "/bundles/:id/events/insights", "getBundleEventInsights");
4127
- add("GET", "/installations/overview", "getBundleEventOverview");
4128
- add("GET", "/installations/active", "getActiveInstallationOverview");
4129
- add("GET", "/installations", "searchInstallations");
4130
- add("GET", "/installations/:installId/events", "getInstallationHistory");
4537
+ add("GET", "/events", "listEvents");
4538
+ add("GET", "/overview", "getReportingOverview");
4539
+ add("GET", "/installations", "pageInstallationsByCurrentUserId");
4540
+ add("GET", "/installations/:installId/events", "listInstallationEvents");
4541
+ add("GET", "/installations/:installId", "getInstallation");
4131
4542
  }, "registerInsightsAdminRoutes");
4132
4543
 
4133
4544
  // ../../packages/server/dist/internalRouter.mjs
@@ -4293,474 +4704,318 @@ function createHotUpdaterHandlers(api, insights, apiKeyAuth, downloadStorageObje
4293
4704
  }
4294
4705
  __name(createHotUpdaterHandlers, "createHotUpdaterHandlers");
4295
4706
 
4296
- // ../../packages/server/dist/insights/bounded/activeOverview.mjs
4297
- var HOUR_MS = 3600 * 1e3;
4298
- var DAY_MS = 24 * HOUR_MS;
4299
- var ACTIVE_BUNDLE_EVENT_TYPES = [
4300
- "UPDATE_APPLIED",
4301
- "RECOVERED",
4302
- "RELEASE_ADOPTED",
4303
- "UNCHANGED"
4304
- ];
4305
- var activeWindowDefinitions = {
4306
- "24h": {
4307
- bucketCount: 24,
4308
- bucketSizeMs: HOUR_MS
4309
- },
4310
- "7d": {
4311
- bucketCount: 7,
4312
- bucketSizeMs: DAY_MS
4313
- },
4314
- "30d": {
4315
- bucketCount: 30,
4316
- bucketSizeMs: DAY_MS
4317
- }
4707
+ // ../../packages/server/dist/insights/provider.mjs
4708
+ var DEFAULT_PAGE_LIMIT = 50;
4709
+ var MAX_PAGE_LIMIT2 = 100;
4710
+ var MAX_EVENT_ID_LENGTH = 1024;
4711
+ var MAX_IDENTITY_LENGTH3 = 255;
4712
+ var MAX_CURSOR_LENGTH2 = 8192;
4713
+ var WINDOW_MS = {
4714
+ "24h": 864e5,
4715
+ "7d": 6048e5,
4716
+ "30d": 2592e6
4318
4717
  };
4319
- function getActiveWindowDefinition(window) {
4320
- return activeWindowDefinitions[window];
4321
- }
4322
- __name(getActiveWindowDefinition, "getActiveWindowDefinition");
4323
- function compareCodePoints(left, right) {
4324
- if (left < right) return -1;
4325
- if (left > right) return 1;
4326
- return 0;
4327
- }
4328
- __name(compareCodePoints, "compareCodePoints");
4329
- function isNewer(candidate, current) {
4330
- return candidate.received_at_ms > current.received_at_ms || candidate.received_at_ms === current.received_at_ms && compareCodePoints(candidate.id, current.id) > 0;
4331
- }
4332
- __name(isNewer, "isNewer");
4333
- function collectActiveInstallationOverview(request) {
4334
- const definition = getActiveWindowDefinition(request.window);
4335
- const windowStartMs = request.asOfMs - definition.bucketCount * definition.bucketSizeMs;
4336
- const rows = request.rows.filter((row) => row.received_at_ms >= windowStartMs && row.received_at_ms < request.asOfMs);
4337
- const latestByInstall = /* @__PURE__ */ new Map();
4338
- for (const row of rows) {
4339
- const current = latestByInstall.get(row.install_id);
4340
- if (current === void 0 || isNewer(row, current)) latestByInstall.set(row.install_id, row);
4341
- }
4342
- const selectedRows = [...latestByInstall.values()].filter((row) => request.userId === void 0 || row.user_id === request.userId);
4343
- const selectedInstallIds = new Set(selectedRows.map((row) => row.install_id));
4344
- const bundleCounts = /* @__PURE__ */ new Map();
4345
- for (const row of selectedRows) bundleCounts.set(row.to_bundle_id, (bundleCounts.get(row.to_bundle_id) ?? 0) + 1);
4346
- const latestByBucket = Array.from({ length: definition.bucketCount }, () => /* @__PURE__ */ new Map());
4347
- for (const row of rows) {
4348
- if (!selectedInstallIds.has(row.install_id)) continue;
4349
- const bucket = latestByBucket[Math.floor((row.received_at_ms - windowStartMs) / definition.bucketSizeMs)];
4350
- if (bucket === void 0) continue;
4351
- const current = bucket.get(row.install_id);
4352
- if (current === void 0 || isNewer(row, current)) bucket.set(row.install_id, row);
4353
- }
4354
- const bundleCountsByBucket = latestByBucket.map((bucket) => {
4355
- const counts = /* @__PURE__ */ new Map();
4356
- for (const row of bucket.values()) counts.set(row.to_bundle_id, (counts.get(row.to_bundle_id) ?? 0) + 1);
4357
- return counts;
4358
- });
4359
- const bundleObservationTotals = /* @__PURE__ */ new Map();
4360
- for (const counts of bundleCountsByBucket) for (const [bundleId, count] of counts) bundleObservationTotals.set(bundleId, (bundleObservationTotals.get(bundleId) ?? 0) + count);
4361
- return {
4362
- asOfMs: request.asOfMs,
4363
- window: request.window,
4364
- activeInstallations: selectedInstallIds.size,
4365
- series: latestByBucket.map((bucket, index) => ({
4366
- bucketStartMs: windowStartMs + index * definition.bucketSizeMs,
4367
- value: bucket.size
4368
- })),
4369
- bundleSeries: [...bundleObservationTotals].sort(([leftId, leftTotal], [rightId, rightTotal]) => rightTotal - leftTotal || compareCodePoints(leftId, rightId)).map(([bundleId]) => ({
4370
- bundleId,
4371
- series: bundleCountsByBucket.map((counts, index) => ({
4372
- bucketStartMs: windowStartMs + index * definition.bucketSizeMs,
4373
- value: counts.get(bundleId) ?? 0
4374
- }))
4375
- })),
4376
- bundles: [...bundleCounts].map(([bundleId, installations]) => ({
4377
- bundleId,
4378
- installations
4379
- })).sort((left, right) => right.installations - left.installations || compareCodePoints(left.bundleId, right.bundleId))
4380
- };
4381
- }
4382
- __name(collectActiveInstallationOverview, "collectActiveInstallationOverview");
4383
-
4384
- // ../../packages/server/dist/insights/bounded/scan.mjs
4385
- var INSIGHTS_SCAN_MAX_ROWS = 5e4;
4386
- var INSIGHTS_MATERIALIZATION_LIMIT = INSIGHTS_SCAN_MAX_ROWS + 1;
4387
- var INSIGHTS_SCAN_PAGE_SIZE = 1e3;
4388
- var INSIGHTS_LOWER_BOUND_ID = "00000000-0000-0000-0000-000000000000";
4389
- var compareCodePoints2 = /* @__PURE__ */ __name((left, right) => {
4390
- if (left < right) return -1;
4391
- if (left > right) return 1;
4392
- return 0;
4393
- }, "compareCodePoints");
4394
- var compareEventNewest = /* @__PURE__ */ __name((left, right) => right.received_at_ms - left.received_at_ms || compareCodePoints2(right.id, left.id), "compareEventNewest");
4395
- var compareEventOldest = /* @__PURE__ */ __name((left, right) => left.received_at_ms - right.received_at_ms || compareCodePoints2(left.id, right.id), "compareEventOldest");
4396
- var materializeEventRows = /* @__PURE__ */ __name(async (scope) => {
4397
- const rows = [];
4398
- const seenIds = /* @__PURE__ */ new Set();
4399
- let after = scope.lowerBoundMs === void 0 ? void 0 : {
4400
- receivedAtMs: scope.lowerBoundMs,
4401
- id: INSIGHTS_LOWER_BOUND_ID
4402
- };
4403
- while (rows.length < INSIGHTS_MATERIALIZATION_LIMIT) {
4404
- const limit = Math.min(INSIGHTS_SCAN_PAGE_SIZE, INSIGHTS_MATERIALIZATION_LIMIT - rows.length);
4405
- const page = await scope.persistence.scan({
4406
- beforeReceivedAtMs: scope.cutoffMs,
4407
- ...after === void 0 ? {} : { after },
4408
- limit
4409
- });
4410
- if (page.length === 0) break;
4411
- if (page.length > limit) throw new InsightsPersistenceOrderError();
4412
- let previous = after === void 0 ? void 0 : {
4413
- received_at_ms: after.receivedAtMs,
4414
- id: after.id
4415
- };
4416
- for (const row of page) {
4417
- if (row.received_at_ms >= scope.cutoffMs || previous !== void 0 && compareEventOldest(previous, row) >= 0 || seenIds.has(row.id)) throw new InsightsPersistenceOrderError();
4418
- seenIds.add(row.id);
4419
- rows.push(row);
4420
- previous = row;
4421
- }
4422
- const last = page.at(-1);
4423
- if (last !== void 0) after = {
4424
- receivedAtMs: last.received_at_ms,
4425
- id: last.id
4426
- };
4427
- if (rows.length > 5e4) throw new InsightsScanLimitExceededError(INSIGHTS_SCAN_MAX_ROWS);
4428
- }
4429
- return rows;
4430
- }, "materializeEventRows");
4431
- var InsightsPersistenceOrderError = class extends Error {
4432
- static {
4433
- __name(this, "InsightsPersistenceOrderError");
4434
- }
4435
- name = "InsightsPersistenceOrderError";
4436
- constructor() {
4437
- super("Insights persistence did not advance its scan cursor.");
4718
+ var isRecord4 = /* @__PURE__ */ __name((value) => typeof value === "object" && value !== null && !Array.isArray(value), "isRecord");
4719
+ var requireString = /* @__PURE__ */ __name((value, label, maximumLength) => {
4720
+ 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}.`);
4721
+ return value;
4722
+ }, "requireString");
4723
+ var requireTimestamp = /* @__PURE__ */ __name((value, label) => {
4724
+ if (!Number.isSafeInteger(value) || Number(value) < 0) throw new InsightsBadRequestError(`Invalid ${label}.`);
4725
+ return Number(value);
4726
+ }, "requireTimestamp");
4727
+ var readLimit = /* @__PURE__ */ __name((value) => {
4728
+ const limit = value ?? DEFAULT_PAGE_LIMIT;
4729
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT2) throw new InsightsBadRequestError("Invalid page limit.");
4730
+ return limit;
4731
+ }, "readLimit");
4732
+ var encodeBase64Url2 = /* @__PURE__ */ __name((value) => {
4733
+ const bytes = new TextEncoder().encode(value);
4734
+ let binary = "";
4735
+ for (const byte of bytes) binary += String.fromCharCode(byte);
4736
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
4737
+ }, "encodeBase64Url");
4738
+ var decodeBase64Url2 = /* @__PURE__ */ __name((value) => {
4739
+ if (value.length === 0 || value.length > MAX_CURSOR_LENGTH2) throw new InsightsBadRequestError("Invalid Insights cursor.");
4740
+ try {
4741
+ const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
4742
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
4743
+ const binary = atob(padded);
4744
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
4745
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
4746
+ } catch {
4747
+ throw new InsightsBadRequestError("Invalid Insights cursor.");
4438
4748
  }
4439
- };
4440
- var materializeActiveRows = /* @__PURE__ */ __name(async (scope, window) => {
4441
- const definition = getActiveWindowDefinition(window);
4442
- const durationMs = definition.bucketCount * definition.bucketSizeMs;
4443
- return (await materializeEventRows({
4444
- ...scope,
4445
- lowerBoundMs: scope.cutoffMs - durationMs
4446
- })).filter((row) => row.received_at_ms >= scope.cutoffMs - durationMs && ACTIVE_BUNDLE_EVENT_TYPES.includes(row.type));
4447
- }, "materializeActiveRows");
4448
- var startOfUtcHour = /* @__PURE__ */ __name((value) => {
4449
- const date = new Date(value);
4450
- return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours());
4451
- }, "startOfUtcHour");
4452
- var startOfUtcDay = /* @__PURE__ */ __name((value) => {
4453
- const date = new Date(value);
4454
- return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
4455
- }, "startOfUtcDay");
4456
- var getWindowRange = /* @__PURE__ */ __name((window, now) => {
4457
- if (window === "24h") return {
4458
- sizeMs: 3600 * 1e3,
4459
- rangeStart: startOfUtcHour(now) - 1380 * 60 * 1e3
4460
- };
4461
- const days = window === "7d" ? 7 : 30;
4462
- return {
4463
- sizeMs: 1440 * 60 * 1e3,
4464
- rangeStart: startOfUtcDay(now) - (days - 1) * 24 * 60 * 60 * 1e3
4465
- };
4466
- }, "getWindowRange");
4467
- var materializeRowsForWindow = /* @__PURE__ */ __name(async (scope, window) => {
4468
- if (window === "all") return materializeEventRows(scope);
4469
- const range = getWindowRange(window, scope.cutoffMs);
4470
- return (await materializeEventRows({
4471
- ...scope,
4472
- lowerBoundMs: range.rangeStart
4473
- })).filter(({ received_at_ms }) => received_at_ms >= range.rangeStart);
4474
- }, "materializeRowsForWindow");
4475
- var bucketStart = /* @__PURE__ */ __name((receivedAtMs, sizeMs) => sizeMs === 3600 * 1e3 ? startOfUtcHour(receivedAtMs) : startOfUtcDay(receivedAtMs), "bucketStart");
4476
- var createSeries = /* @__PURE__ */ __name((request) => {
4477
- const range = request.window === "all" ? void 0 : getWindowRange(request.window, request.cutoffMs);
4478
- const sizeMs = range?.sizeMs ?? 1440 * 60 * 1e3;
4479
- const installIdsByBucket = /* @__PURE__ */ new Map();
4480
- let oldestMs = request.cutoffMs;
4481
- for (const row of request.rows) {
4482
- oldestMs = Math.min(oldestMs, row.received_at_ms);
4483
- const start = bucketStart(row.received_at_ms, sizeMs);
4484
- const installIds = installIdsByBucket.get(start) ?? /* @__PURE__ */ new Set();
4485
- installIds.add(row.install_id);
4486
- installIdsByBucket.set(start, installIds);
4487
- }
4488
- const first = range?.rangeStart ?? startOfUtcDay(oldestMs);
4489
- const last = bucketStart(request.cutoffMs, sizeMs);
4490
- return Array.from({ length: Math.floor((last - first) / sizeMs) + 1 }, (_, index) => {
4491
- const start = first + index * sizeMs;
4492
- return {
4493
- bucketStartMs: start,
4494
- value: installIdsByBucket.get(start)?.size ?? 0
4495
- };
4496
- });
4497
- }, "createSeries");
4498
- var collectEventActivity = /* @__PURE__ */ __name((request) => {
4499
- const range = request.window === "all" ? void 0 : getWindowRange(request.window, request.cutoffMs);
4500
- const rows = range === void 0 ? request.rows : request.rows.filter(({ received_at_ms }) => received_at_ms >= range.rangeStart && received_at_ms < request.cutoffMs);
4501
- const installs = /* @__PURE__ */ new Set();
4502
- const installsByCohort = /* @__PURE__ */ new Map();
4503
- for (const row of rows) {
4504
- installs.add(row.install_id);
4505
- const cohort = installsByCohort.get(row.cohort) ?? /* @__PURE__ */ new Set();
4506
- cohort.add(row.install_id);
4507
- installsByCohort.set(row.cohort, cohort);
4749
+ }, "decodeBase64Url");
4750
+ var encodeCursor = /* @__PURE__ */ __name((value) => encodeBase64Url2(JSON.stringify(value)), "encodeCursor");
4751
+ var decodeCursor = /* @__PURE__ */ __name((value) => {
4752
+ try {
4753
+ return JSON.parse(decodeBase64Url2(value));
4754
+ } catch (error) {
4755
+ if (error instanceof InsightsBadRequestError) throw error;
4756
+ throw new InsightsBadRequestError("Invalid Insights cursor.");
4508
4757
  }
4758
+ }, "decodeCursor");
4759
+ var readScope2 = /* @__PURE__ */ __name((input) => {
4760
+ if (input.platform !== "ios" && input.platform !== "android") throw new InsightsBadRequestError("Invalid Insights platform.");
4509
4761
  return {
4510
- summary: installs.size,
4511
- cohorts: [...installsByCohort].sort(([left], [right]) => compareCodePoints2(left, right)).map(([cohort, installIds]) => ({
4512
- cohort,
4513
- value: installIds.size
4514
- })),
4515
- series: createSeries({
4516
- ...request,
4517
- rows
4518
- })
4519
- };
4520
- }, "collectEventActivity");
4521
-
4522
- // ../../packages/server/dist/insights/bounded/installationSearch.mjs
4523
- function toSearchRow(row) {
4524
- return {
4525
- installId: row.install_id,
4526
- username: row.username,
4527
- userId: row.user_id,
4528
- lastKnownBundleId: row.to_bundle_id,
4529
- latestStatus: row.type,
4530
- platform: row.platform,
4531
- appVersion: row.app_version,
4532
- channel: row.channel,
4533
- cohort: row.cohort,
4534
- receivedAtMs: row.received_at_ms
4535
- };
4536
- }
4537
- __name(toSearchRow, "toSearchRow");
4538
- function matchesIdentity(row, query2) {
4539
- return row.install_id.toLowerCase().includes(query2) || row.user_id?.toLowerCase().includes(query2) === true || row.username?.toLowerCase().includes(query2) === true;
4540
- }
4541
- __name(matchesIdentity, "matchesIdentity");
4542
- function searchEventInstallations(request) {
4543
- const query2 = request.query.toLowerCase();
4544
- const matchingInstallIds = /* @__PURE__ */ new Set();
4545
- for (const row of request.rows) if (query2.length === 0 || matchesIdentity(row, query2)) matchingInstallIds.add(row.install_id);
4546
- const latestByInstall = /* @__PURE__ */ new Map();
4547
- for (const row of request.rows) {
4548
- if (!matchingInstallIds.has(row.install_id)) continue;
4549
- const current = latestByInstall.get(row.install_id);
4550
- if (current === void 0 || compareEventNewest(row, current) < 0) latestByInstall.set(row.install_id, row);
4551
- }
4552
- const matchingRows = [...latestByInstall.values()].sort((left, right) => compareCodePoints2(left.install_id, right.install_id));
4553
- const pageSize = Math.min(Math.max(request.limit, 0), 100);
4554
- return {
4555
- data: matchingRows.slice(request.offset, request.offset + pageSize).map(toSearchRow),
4556
- pagination: {
4557
- total: matchingRows.length,
4558
- limit: request.limit,
4559
- offset: request.offset
4560
- }
4561
- };
4562
- }
4563
- __name(searchEventInstallations, "searchEventInstallations");
4564
-
4565
- // ../../packages/server/dist/insights/bounded/persistence.mjs
4566
- function createBundleEventRow(input) {
4567
- const base = {
4568
- id: createUUIDv7(),
4569
- install_id: input.installId,
4570
- user_id: input.userId ?? null,
4571
- username: input.username ?? null,
4572
- from_release_id: input.fromReleaseId,
4573
- to_release_id: input.toReleaseId,
4574
- to_bundle_id: input.toBundleId,
4575
4762
  platform: input.platform,
4576
- app_version: input.appVersion,
4577
- channel: input.channel,
4578
- cohort: input.cohort,
4579
- fingerprint_hash: input.fingerprintHash,
4580
- sdk_version: input.sdkVersion ?? null,
4581
- received_at_ms: Date.now()
4763
+ channel: requireString(input.channel, "channel", MAX_EVENT_ID_LENGTH)
4582
4764
  };
4583
- switch (input.type) {
4584
- case "UPDATE_APPLIED":
4585
- case "RECOVERED":
4586
- case "RELEASE_ADOPTED":
4765
+ }, "readScope");
4766
+ var bundleFilter = /* @__PURE__ */ __name((input) => {
4767
+ const scope = readScope2(input);
4768
+ const bundleId = requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
4769
+ switch (input.outcome) {
4770
+ case "applied":
4587
4771
  return {
4588
- ...base,
4589
- type: input.type,
4590
- from_bundle_id: input.fromBundleId,
4591
- update_strategy: input.updateStrategy
4772
+ ...scope,
4773
+ type: "UPDATE_APPLIED",
4774
+ toBundleId: bundleId
4592
4775
  };
4593
- case "UNCHANGED":
4776
+ case "recovered":
4594
4777
  return {
4595
- ...base,
4596
- type: input.type,
4597
- from_bundle_id: null,
4598
- update_strategy: null
4778
+ ...scope,
4779
+ type: "RECOVERED",
4780
+ fromBundleId: bundleId
4599
4781
  };
4782
+ case "adopted":
4783
+ return {
4784
+ ...scope,
4785
+ type: "RELEASE_ADOPTED",
4786
+ toBundleId: bundleId
4787
+ };
4788
+ default:
4789
+ throw new InsightsBadRequestError("Invalid Insights outcome.");
4600
4790
  }
4601
- }
4602
- __name(createBundleEventRow, "createBundleEventRow");
4603
-
4604
- // ../../packages/server/dist/insights/bounded/provider.mjs
4605
- var isTransitionEventRow = /* @__PURE__ */ __name((row) => row.type === "UPDATE_APPLIED" || row.type === "RECOVERED", "isTransitionEventRow");
4606
- var toHistoryRow = /* @__PURE__ */ __name((row) => ({
4607
- id: row.id,
4608
- type: row.type,
4791
+ }, "bundleFilter");
4792
+ var sameFilter = /* @__PURE__ */ __name((left, right) => {
4793
+ if (!isRecord4(left) || left.kind !== right.kind) return false;
4794
+ switch (right.kind) {
4795
+ case "all":
4796
+ return true;
4797
+ case "installationMovement":
4798
+ return left.installId === right.installId;
4799
+ case "bundle":
4800
+ return left.platform === right.platform && left.channel === right.channel && left.type === right.type && (right.type === "RECOVERED" ? left.fromBundleId === right.fromBundleId : left.toBundleId === right.toBundleId);
4801
+ }
4802
+ }, "sameFilter");
4803
+ var readEventCursor = /* @__PURE__ */ __name((value, filter) => {
4804
+ const cursor = decodeCursor(value);
4805
+ if (!isRecord4(cursor) || cursor.version !== 2 || cursor.kind !== "events" || !isRecord4(cursor.filter) || !isRecord4(cursor.after)) throw new InsightsBadRequestError("Invalid Insights cursor.");
4806
+ if (!sameFilter(cursor.filter, filter)) throw new InsightsBadRequestError("Insights cursor does not match the requested events.");
4807
+ if (typeof cursor.after.id !== "string" || !isUUIDv7(cursor.after.id)) throw new InsightsBadRequestError("Invalid Insights event cursor ID.");
4808
+ return {
4809
+ after: {
4810
+ id: cursor.after.id,
4811
+ receivedAtMs: requireTimestamp(cursor.after.receivedAtMs, "event cursor")
4812
+ },
4813
+ beforeReceivedAtMs: requireTimestamp(cursor.beforeReceivedAtMs, "event cutoff"),
4814
+ kind: "events",
4815
+ sinceMs: requireTimestamp(cursor.sinceMs, "event start"),
4816
+ filter,
4817
+ version: 2
4818
+ };
4819
+ }, "readEventCursor");
4820
+ var readUserInstallationCursor = /* @__PURE__ */ __name((value, userId) => {
4821
+ const cursor = decodeCursor(value);
4822
+ if (!isRecord4(cursor) || cursor.version !== 1 || cursor.kind !== "user-installations" || cursor.userId !== userId) throw new InsightsBadRequestError("Insights cursor does not match the requested user ID.");
4823
+ return {
4824
+ afterInstallId: requireString(cursor.afterInstallId, "installation cursor", MAX_IDENTITY_LENGTH3),
4825
+ kind: "user-installations",
4826
+ userId,
4827
+ version: 1
4828
+ };
4829
+ }, "readUserInstallationCursor");
4830
+ var compareEventNewest = /* @__PURE__ */ __name((left, right) => right.received_at_ms - left.received_at_ms || compareInsightsText(right.id, left.id), "compareEventNewest");
4831
+ var isAfterEventCursor = /* @__PURE__ */ __name((row, after) => row.received_at_ms < after.receivedAtMs || row.received_at_ms === after.receivedAtMs && compareInsightsText(row.id, after.id) < 0, "isAfterEventCursor");
4832
+ var assertEventRows = /* @__PURE__ */ __name((rows, input) => {
4833
+ if (rows.length > input.limit) throw new Error("Insights database returned too many event rows.");
4834
+ for (let index = 0; index < rows.length; index += 1) {
4835
+ const row = rows[index];
4836
+ const previous = rows[index - 1];
4837
+ 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.");
4838
+ }
4839
+ }, "assertEventRows");
4840
+ var toEventHistoryRow = /* @__PURE__ */ __name((row) => ({
4841
+ appVersion: row.app_version,
4842
+ channel: row.channel,
4843
+ cohort: row.cohort,
4609
4844
  fromBundleId: row.from_bundle_id,
4845
+ id: row.id,
4846
+ installId: row.install_id,
4847
+ platform: row.platform,
4848
+ receivedAtMs: row.received_at_ms,
4610
4849
  toBundleId: row.to_bundle_id,
4611
- username: row.username,
4850
+ type: row.type,
4612
4851
  userId: row.user_id,
4613
- platform: row.platform,
4852
+ username: row.username
4853
+ }), "toEventHistoryRow");
4854
+ var toInstallationRow = /* @__PURE__ */ __name((row) => ({
4614
4855
  appVersion: row.app_version,
4615
4856
  channel: row.channel,
4616
4857
  cohort: row.cohort,
4617
- receivedAtMs: row.received_at_ms
4618
- }), "toHistoryRow");
4619
- var isInstalledForBundle = /* @__PURE__ */ __name((row, bundleId) => row.type === "UPDATE_APPLIED" && row.to_bundle_id === bundleId, "isInstalledForBundle");
4620
- var isRecoveredFromBundle = /* @__PURE__ */ __name((row, bundleId) => row.type === "RECOVERED" && row.from_bundle_id === bundleId, "isRecoveredFromBundle");
4621
- var countDistinctInstallations = /* @__PURE__ */ __name((rows) => new Set(rows.map(({ install_id }) => install_id)).size, "countDistinctInstallations");
4622
- var getInsightsResult = /* @__PURE__ */ __name(async (persistence, bundleId, window, limit, offset) => {
4623
- const scope = {
4624
- persistence,
4625
- cutoffMs: Date.now()
4858
+ installId: row.install_id,
4859
+ lastKnownBundleId: row.to_bundle_id,
4860
+ latestStatus: row.type,
4861
+ platform: row.platform,
4862
+ receivedAtMs: row.received_at_ms,
4863
+ userId: row.user_id,
4864
+ username: row.username
4865
+ }), "toInstallationRow");
4866
+ var pageEventRows = /* @__PURE__ */ __name(async (model, input, filter, map) => {
4867
+ const limit = readLimit(input.limit);
4868
+ const cursor = input.cursor === void 0 ? void 0 : readEventCursor(input.cursor, filter);
4869
+ const beforeReceivedAtMs = cursor?.beforeReceivedAtMs ?? (input.beforeReceivedAtMs === void 0 ? Date.now() : requireTimestamp(input.beforeReceivedAtMs, "event cutoff"));
4870
+ if (cursor !== void 0 && input.beforeReceivedAtMs !== void 0 && input.beforeReceivedAtMs !== beforeReceivedAtMs) throw new InsightsBadRequestError("Insights cursor does not match the requested event cutoff.");
4871
+ const sinceMs = cursor?.sinceMs ?? (input.sinceMs === void 0 ? 0 : requireTimestamp(input.sinceMs, "event start"));
4872
+ 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.");
4873
+ const databaseInput = {
4874
+ filter,
4875
+ sinceMs,
4876
+ beforeReceivedAtMs,
4877
+ ...cursor === void 0 ? {} : { after: cursor.after },
4878
+ limit: limit + 1
4626
4879
  };
4627
- const rows = (await materializeRowsForWindow(scope, window)).filter(isTransitionEventRow);
4628
- const installedRows = rows.filter((row) => isInstalledForBundle(row, bundleId));
4629
- const recoveredRows = rows.filter((row) => isRecoveredFromBundle(row, bundleId));
4630
- const installed = collectEventActivity({
4631
- rows: installedRows,
4632
- window,
4633
- cutoffMs: scope.cutoffMs
4634
- });
4635
- const recovered = collectEventActivity({
4636
- rows: recoveredRows,
4637
- window,
4638
- cutoffMs: scope.cutoffMs
4639
- });
4640
- const recentRows = [...installedRows, ...recoveredRows].sort(compareEventNewest);
4880
+ const rows = await model.listEvents(databaseInput);
4881
+ assertEventRows(rows, databaseInput);
4882
+ const pageRows = rows.slice(0, limit);
4883
+ const last = pageRows.at(-1);
4641
4884
  return {
4642
- summary: {
4643
- installed: installed.summary,
4644
- recovered: recovered.summary
4645
- },
4646
- series: {
4647
- installed: installed.series,
4648
- recovered: recovered.series
4649
- },
4650
- cohorts: {
4651
- installed: installed.cohorts,
4652
- recovered: recovered.cohorts
4653
- },
4654
- recentEvents: {
4655
- data: recentRows.slice(offset, offset + limit).map(toHistoryRow),
4656
- pagination: {
4657
- total: recentRows.length,
4658
- limit,
4659
- offset
4660
- }
4661
- }
4885
+ beforeReceivedAtMs,
4886
+ data: pageRows.map(map),
4887
+ nextCursor: rows.length > limit && last ? encodeCursor({
4888
+ after: {
4889
+ id: last.id,
4890
+ receivedAtMs: last.received_at_ms
4891
+ },
4892
+ beforeReceivedAtMs,
4893
+ kind: "events",
4894
+ filter,
4895
+ sinceMs,
4896
+ version: 2
4897
+ }) : null
4662
4898
  };
4663
- }, "getInsightsResult");
4664
- var getInsightsSummaries = /* @__PURE__ */ __name(async (persistence, bundleIds, window) => {
4665
- const normalizedBundleIds = [...new Set(bundleIds)];
4666
- if (normalizedBundleIds.length === 0) return [];
4667
- const requestedBundleIds = new Set(normalizedBundleIds);
4668
- const installedByBundleId = /* @__PURE__ */ new Map();
4669
- const recoveredByBundleId = /* @__PURE__ */ new Map();
4670
- const rows = (await materializeRowsForWindow({
4671
- persistence,
4672
- cutoffMs: Date.now()
4673
- }, window)).filter(isTransitionEventRow);
4674
- for (const row of rows) {
4675
- const bundleId = row.type === "UPDATE_APPLIED" ? row.to_bundle_id : row.from_bundle_id;
4676
- if (!requestedBundleIds.has(bundleId)) continue;
4677
- const counts = row.type === "UPDATE_APPLIED" ? installedByBundleId : recoveredByBundleId;
4678
- const installIds = counts.get(bundleId) ?? /* @__PURE__ */ new Set();
4679
- installIds.add(row.install_id);
4680
- counts.set(bundleId, installIds);
4681
- }
4682
- return normalizedBundleIds.map((bundleId) => ({
4683
- bundleId,
4684
- installed: installedByBundleId.get(bundleId)?.size ?? 0,
4685
- recovered: recoveredByBundleId.get(bundleId)?.size ?? 0
4686
- }));
4687
- }, "getInsightsSummaries");
4688
- var createInsightsProvider = /* @__PURE__ */ __name((persistence) => Object.freeze({
4689
- mode: "bounded",
4690
- maxMatchingRows: INSIGHTS_SCAN_MAX_ROWS,
4899
+ }, "pageEventRows");
4900
+ var assertInstallationRows = /* @__PURE__ */ __name((rows, input) => {
4901
+ if (rows.length > input.limit) throw new Error("Insights database returned too many installation rows.");
4902
+ for (let index = 0; index < rows.length; index += 1) {
4903
+ const row = rows[index];
4904
+ const previous = rows[index - 1];
4905
+ 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.");
4906
+ }
4907
+ }, "assertInstallationRows");
4908
+ var createInsightsProvider = /* @__PURE__ */ __name((model) => Object.freeze({
4691
4909
  async appendBundleEvent(input) {
4692
- await persistence.append(createBundleEventRow(input));
4910
+ const event = createBundleEventRow(input);
4911
+ await model.record({
4912
+ event,
4913
+ installation: toInsightsInstallationRow(event)
4914
+ });
4693
4915
  },
4694
- async getBundleEventSummary(bundleId) {
4695
- const rows = (await materializeEventRows({
4696
- persistence,
4697
- cutoffMs: Date.now()
4698
- })).filter(isTransitionEventRow);
4699
- return {
4700
- installed: countDistinctInstallations(rows.filter((row) => isInstalledForBundle(row, bundleId))),
4701
- recovered: countDistinctInstallations(rows.filter((row) => isRecoveredFromBundle(row, bundleId)))
4916
+ listEvents(input) {
4917
+ const filter = input.bundle === void 0 ? { kind: "all" } : {
4918
+ kind: "bundle",
4919
+ ...bundleFilter(input.bundle)
4702
4920
  };
4921
+ return pageEventRows(model, input, filter, toEventHistoryRow);
4703
4922
  },
4704
- getBundleEventSummaries(bundleIds, window) {
4705
- return getInsightsSummaries(persistence, bundleIds, window);
4923
+ async listInstallationEvents(input) {
4924
+ if ("bundle" in input && input.bundle !== void 0) throw new InsightsBadRequestError("Installation movement queries cannot include a bundle filter.");
4925
+ const installId = requireString(input.installId, "install ID", MAX_IDENTITY_LENGTH3);
4926
+ return pageEventRows(model, input, {
4927
+ kind: "installationMovement",
4928
+ installId
4929
+ }, (row) => toEventHistoryRow(row));
4706
4930
  },
4707
- getBundleEventInsights(bundleId, window, limit, offset) {
4708
- return getInsightsResult(persistence, bundleId, window, limit, offset);
4931
+ async getInstallation({ installId }) {
4932
+ const normalizedInstallId = requireString(installId, "install ID", MAX_IDENTITY_LENGTH3);
4933
+ const rows = await model.findInstallations({ installId: normalizedInstallId });
4934
+ const row = rows[0] ?? null;
4935
+ if (rows.length > 1 || row !== null && row.install_id !== normalizedInstallId) throw new Error("Insights database returned an invalid installation.");
4936
+ return row === null ? null : toInstallationRow(row);
4709
4937
  },
4710
- async getBundleEventOverview() {
4711
- const rows = await materializeEventRows({
4712
- persistence,
4713
- cutoffMs: Date.now()
4714
- });
4715
- const latestByInstall = /* @__PURE__ */ new Map();
4716
- for (const row of rows) {
4717
- const current = latestByInstall.get(row.install_id);
4718
- if (current === void 0 || compareEventNewest(row, current) < 0) latestByInstall.set(row.install_id, row);
4719
- }
4720
- const counts = /* @__PURE__ */ new Map();
4721
- for (const row of latestByInstall.values()) counts.set(row.to_bundle_id, (counts.get(row.to_bundle_id) ?? 0) + 1);
4938
+ async pageInstallationsByCurrentUserId(input) {
4939
+ const userId = requireString(input.userId, "user ID", MAX_IDENTITY_LENGTH3);
4940
+ const limit = readLimit(input.limit);
4941
+ const cursor = input.cursor === void 0 ? void 0 : readUserInstallationCursor(input.cursor, userId);
4942
+ const databaseInput = {
4943
+ userId,
4944
+ ...cursor === void 0 ? {} : { afterInstallId: cursor.afterInstallId },
4945
+ limit: limit + 1
4946
+ };
4947
+ const rows = await model.findInstallations(databaseInput);
4948
+ assertInstallationRows(rows, databaseInput);
4949
+ const pageRows = rows.slice(0, limit);
4950
+ const last = pageRows.at(-1);
4722
4951
  return {
4723
- trackedInstallations: latestByInstall.size,
4724
- bundles: [...counts].map(([bundleId, installations]) => ({
4725
- bundleId,
4726
- installations
4727
- })).sort((left, right) => right.installations - left.installations || compareCodePoints2(left.bundleId, right.bundleId))
4952
+ data: pageRows.map(toInstallationRow),
4953
+ nextCursor: rows.length > limit && last ? encodeCursor({
4954
+ afterInstallId: last.install_id,
4955
+ kind: "user-installations",
4956
+ userId,
4957
+ version: 1
4958
+ }) : null
4728
4959
  };
4729
4960
  },
4730
- async getActiveInstallationOverview(input) {
4731
- const asOfMs = Date.now();
4732
- return collectActiveInstallationOverview({
4733
- rows: await materializeActiveRows({
4734
- persistence,
4735
- cutoffMs: asOfMs
4736
- }, input.window),
4737
- asOfMs,
4738
- window: input.window,
4739
- ...input.userId === void 0 ? {} : { userId: input.userId }
4740
- });
4741
- },
4742
- async searchInstallations(query2, limit, offset) {
4743
- return searchEventInstallations({
4744
- rows: await materializeEventRows({
4745
- persistence,
4746
- cutoffMs: Date.now()
4961
+ async getReportingOverview(input) {
4962
+ const scope = readScope2(input);
4963
+ const { window } = input;
4964
+ if (!Object.hasOwn(WINDOW_MS, window)) throw new InsightsBadRequestError("Invalid reporting installation window.");
4965
+ const beforeReceivedAtMs = Date.now();
4966
+ const sinceMs = Math.max(0, beforeReceivedAtMs - WINDOW_MS[window]);
4967
+ const measure = /* @__PURE__ */ __name(async (count) => {
4968
+ const value = await count;
4969
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error("Insights database returned an invalid count.");
4970
+ return {
4971
+ count: value,
4972
+ measuredAtMs: Date.now()
4973
+ };
4974
+ }, "measure");
4975
+ const bundleId = input.bundleId === void 0 ? void 0 : requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
4976
+ const reporting = measure(model.countInstallations({
4977
+ ...scope,
4978
+ sinceMs
4979
+ }));
4980
+ if (bundleId === void 0) return {
4981
+ ...scope,
4982
+ window,
4983
+ sinceMs,
4984
+ beforeReceivedAtMs,
4985
+ reportingInstallations: await reporting
4986
+ };
4987
+ const countOutcome = /* @__PURE__ */ __name((outcome) => measure(model.countEvents({
4988
+ filter: bundleFilter({
4989
+ ...scope,
4990
+ bundleId,
4991
+ outcome
4747
4992
  }),
4748
- query: query2,
4749
- limit,
4750
- offset
4751
- });
4752
- },
4753
- async getInstallationHistory(installId, limit, offset) {
4754
- const ordered = (await materializeEventRows({
4755
- persistence,
4756
- cutoffMs: Date.now()
4757
- })).filter((row) => row.install_id === installId && isTransitionEventRow(row)).toSorted(compareEventNewest);
4993
+ sinceMs,
4994
+ beforeReceivedAtMs
4995
+ })), "countOutcome");
4996
+ const [reportingInstallations, bundleInstallations, appliedReports, recoveredReports, adoptedReports] = await Promise.all([
4997
+ reporting,
4998
+ measure(model.countInstallations({
4999
+ ...scope,
5000
+ sinceMs,
5001
+ bundleId
5002
+ })),
5003
+ countOutcome("applied"),
5004
+ countOutcome("recovered"),
5005
+ countOutcome("adopted")
5006
+ ]);
4758
5007
  return {
4759
- data: ordered.slice(offset, offset + limit).map(toHistoryRow),
4760
- pagination: {
4761
- total: ordered.length,
4762
- limit,
4763
- offset
5008
+ ...scope,
5009
+ window,
5010
+ sinceMs,
5011
+ beforeReceivedAtMs,
5012
+ reportingInstallations,
5013
+ bundle: {
5014
+ bundleId,
5015
+ reportingInstallations: bundleInstallations,
5016
+ appliedReports,
5017
+ recoveredReports,
5018
+ adoptedReports
4764
5019
  }
4765
5020
  };
4766
5021
  }
@@ -4801,7 +5056,7 @@ var hashApiKey = /* @__PURE__ */ __name(async (apiKey) => {
4801
5056
  return bytesToBase64Url(new Uint8Array(digest));
4802
5057
  }, "hashApiKey");
4803
5058
  var apiKeyId = /* @__PURE__ */ __name(() => {
4804
- const bytes = new Uint8Array(16);
5059
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
4805
5060
  crypto.getRandomValues(bytes);
4806
5061
  return `api-${bytesToBase64Url(bytes)}`;
4807
5062
  }, "apiKeyId");
@@ -4838,7 +5093,7 @@ var registerApiKey = /* @__PURE__ */ __name(async (input) => {
4838
5093
  });
4839
5094
  }, "registerApiKey");
4840
5095
  var createApiKey = /* @__PURE__ */ __name((input) => {
4841
- const bytes = new Uint8Array(32);
5096
+ const bytes = /* @__PURE__ */ new Uint8Array(32);
4842
5097
  crypto.getRandomValues(bytes);
4843
5098
  return registerApiKey({
4844
5099
  apiKey: bytesToBase64Url(bytes),
@@ -5267,6 +5522,8 @@ var HotUpdaterSchemaMigrationRequiredError = class extends Error {
5267
5522
  static {
5268
5523
  __name(this, "HotUpdaterSchemaMigrationRequiredError");
5269
5524
  }
5525
+ adapterName;
5526
+ currentVersion;
5270
5527
  constructor(adapterName, currentVersion) {
5271
5528
  super(currentVersion === void 0 ? `Hot Updater database schema is not initialized for ${adapterName}. Run \`hot-updater db migrate\` before using this adapter.` : unsupportedSchemaUpgradeMessage(currentVersion));
5272
5529
  this.adapterName = adapterName;
@@ -5297,7 +5554,7 @@ var sqlProviders = [
5297
5554
  var noSqlProviders = ["mongodb"];
5298
5555
  [...sqlProviders, ...noSqlProviders];
5299
5556
  function isDatabasePlugin(plugin) {
5300
- 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");
5557
+ 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 && "record" in plugin.models.insights && typeof plugin.models.insights.record === "function" && "listEvents" in plugin.models.insights && typeof plugin.models.insights.listEvents === "function" && "findInstallations" in plugin.models.insights && typeof plugin.models.insights.findInstallations === "function" && "countEvents" in plugin.models.insights && typeof plugin.models.insights.countEvents === "function" && "countInstallations" in plugin.models.insights && typeof plugin.models.insights.countInstallations === "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");
5301
5558
  }
5302
5559
  __name(isDatabasePlugin, "isDatabasePlugin");
5303
5560
 
@@ -5401,27 +5658,41 @@ var hotUpdaterCoreMetadata = /* @__PURE__ */ Symbol.for("@hot-updater/server/cor
5401
5658
  function createHotUpdaterCore(options) {
5402
5659
  for (const key of ["authorityId", "catalogId"]) if (Object.hasOwn(options, key)) throw new TypeError(`Remove ${key} from createHotUpdater options. Catalog identity is managed internally.`);
5403
5660
  const database = options.database;
5404
- const { downloadStorageObject, readStorageText, resolveFileUrl } = createStorageAccess((options.storage ?? []).map((storage) => {
5661
+ const storagePlugins = (options.storage ?? []).map((storage) => {
5405
5662
  assertStorageOperations(storage, ["get", "getDownloadUrl"]);
5406
5663
  return storage;
5407
- }));
5664
+ });
5665
+ const { downloadStorageObject, readStorageText, resolveFileUrl } = createStorageAccess(storagePlugins);
5408
5666
  const adapterCapabilities = database;
5409
5667
  if (!isDatabasePlugin(database)) throw new Error("@hot-updater/server only supports database plugins.");
5410
5668
  const plugin = database;
5411
- const assertSchemaReady = createSchemaReadinessChecker(adapterCapabilities.adapterName ?? plugin.name, adapterCapabilities.createMigrator);
5669
+ const adapterName = adapterCapabilities.adapterName ?? plugin.name;
5670
+ const assertSchemaReady = createSchemaReadinessChecker(adapterName, adapterCapabilities.createMigrator);
5412
5671
  const core = createDatabasePluginCore(plugin, resolveFileUrl, {
5413
5672
  beforeOperation: assertSchemaReady,
5414
5673
  readStorageText
5415
5674
  });
5416
5675
  const clientAccess = normalizeClientAccess(options.clientAccess);
5417
5676
  const insights = createInsightsProvider({
5418
- async append(row) {
5677
+ async record(input) {
5678
+ await assertSchemaReady();
5679
+ return plugin.models.insights.record(input);
5680
+ },
5681
+ async listEvents(input) {
5419
5682
  await assertSchemaReady();
5420
- return plugin.models.insights.append(row);
5683
+ return plugin.models.insights.listEvents(input);
5421
5684
  },
5422
- async scan(input) {
5685
+ async findInstallations(input) {
5423
5686
  await assertSchemaReady();
5424
- return plugin.models.insights.scan(input);
5687
+ return plugin.models.insights.findInstallations(input);
5688
+ },
5689
+ async countInstallations(input) {
5690
+ await assertSchemaReady();
5691
+ return plugin.models.insights.countInstallations(input);
5692
+ },
5693
+ async countEvents(input) {
5694
+ await assertSchemaReady();
5695
+ return plugin.models.insights.countEvents(input);
5425
5696
  }
5426
5697
  });
5427
5698
  const apiKeys = createApiKeyManagement({
@@ -7626,7 +7897,7 @@ var InvalidD1RowError = class extends Error {
7626
7897
  this.model = model;
7627
7898
  }
7628
7899
  };
7629
- var isRecord4 = /* @__PURE__ */ __name((value) => typeof value === "object" && value !== null && !Array.isArray(value), "isRecord");
7900
+ var isRecord5 = /* @__PURE__ */ __name((value) => typeof value === "object" && value !== null && !Array.isArray(value), "isRecord");
7630
7901
  var stringValue = /* @__PURE__ */ __name((row, field, model) => {
7631
7902
  const value = row[field];
7632
7903
  if (typeof value !== "string") throw new InvalidD1RowError(model);
@@ -7744,6 +8015,28 @@ var eventRow = /* @__PURE__ */ __name((row) => {
7744
8015
  received_at_ms: numberValue(row, "received_at_ms", "bundle_events")
7745
8016
  };
7746
8017
  }, "eventRow");
8018
+ var installationRow = /* @__PURE__ */ __name((row) => {
8019
+ const type = stringValue(row, "type", "bundle_installations");
8020
+ const platform = stringValue(row, "platform", "bundle_installations");
8021
+ if (!["UPDATE_APPLIED", "RECOVERED", "RELEASE_ADOPTED", "UNCHANGED"].includes(
8022
+ type
8023
+ ) || platform !== "ios" && platform !== "android") {
8024
+ throw new InvalidD1RowError("bundle_installations");
8025
+ }
8026
+ return {
8027
+ id: stringValue(row, "id", "bundle_installations"),
8028
+ install_id: stringValue(row, "install_id", "bundle_installations"),
8029
+ user_id: nullableString(row, "user_id", "bundle_installations"),
8030
+ username: nullableString(row, "username", "bundle_installations"),
8031
+ to_bundle_id: stringValue(row, "to_bundle_id", "bundle_installations"),
8032
+ type,
8033
+ platform,
8034
+ app_version: stringValue(row, "app_version", "bundle_installations"),
8035
+ channel: stringValue(row, "channel", "bundle_installations"),
8036
+ cohort: stringValue(row, "cohort", "bundle_installations"),
8037
+ received_at_ms: numberValue(row, "received_at_ms", "bundle_installations")
8038
+ };
8039
+ }, "installationRow");
7747
8040
  var apiKeyRow = /* @__PURE__ */ __name((row) => {
7748
8041
  const role = stringValue(row, "role", "api_keys");
7749
8042
  if (role !== "client") throw new InvalidD1RowError("api_keys");
@@ -7822,7 +8115,7 @@ var releaseCatalogRow = /* @__PURE__ */ __name((row) => {
7822
8115
  };
7823
8116
  }, "releaseCatalogRow");
7824
8117
  function parseD1Row(model, value) {
7825
- if (!isRecord4(value)) throw new InvalidD1RowError(model);
8118
+ if (!isRecord5(value)) throw new InvalidD1RowError(model);
7826
8119
  switch (model) {
7827
8120
  case "bundles":
7828
8121
  return bundleRow(value);
@@ -7832,6 +8125,8 @@ function parseD1Row(model, value) {
7832
8125
  return channelRow(value);
7833
8126
  case "bundle_events":
7834
8127
  return eventRow(value);
8128
+ case "bundle_installations":
8129
+ return installationRow(value);
7835
8130
  case "api_keys":
7836
8131
  return apiKeyRow(value);
7837
8132
  case "releases":
@@ -7968,6 +8263,7 @@ var d1TableNames = {
7968
8263
  bundle_patches: "bundle_patches",
7969
8264
  channels: "channels",
7970
8265
  bundle_events: "bundle_events",
8266
+ bundle_installations: "bundle_installations",
7971
8267
  api_keys: "api_keys",
7972
8268
  release_catalogs: "release_catalogs",
7973
8269
  releases: "releases"
@@ -8021,6 +8317,8 @@ var findManyD1Rows = /* @__PURE__ */ __name(async (executor, input) => {
8021
8317
  return rows.map((row) => parseD1Row("channels", row));
8022
8318
  case "bundle_events":
8023
8319
  return rows.map((row) => parseD1Row("bundle_events", row));
8320
+ case "bundle_installations":
8321
+ return rows.map((row) => parseD1Row("bundle_installations", row));
8024
8322
  case "api_keys":
8025
8323
  return rows.map((row) => parseD1Row("api_keys", row));
8026
8324
  case "releases":
@@ -8031,6 +8329,15 @@ var findManyD1Rows = /* @__PURE__ */ __name(async (executor, input) => {
8031
8329
  }, "findManyD1Rows");
8032
8330
 
8033
8331
  // src/d1Implementation.ts
8332
+ var D1ExecutionError = class extends Error {
8333
+ static {
8334
+ __name(this, "D1ExecutionError");
8335
+ }
8336
+ name = "D1ExecutionError";
8337
+ constructor() {
8338
+ super("D1 did not successfully execute every requested statement");
8339
+ }
8340
+ };
8034
8341
  var EXPECTATION_CONFLICT_MARKER = "HOT_UPDATER_COMMIT_EXPECTATION_CONFLICT";
8035
8342
  var expectationGuard = /* @__PURE__ */ __name((expectations) => {
8036
8343
  const clauses = [];
@@ -8157,6 +8464,7 @@ var insertQuery = /* @__PURE__ */ __name((input, guard, conflictMode = "returnEx
8157
8464
  values = channelValues(input.data);
8158
8465
  break;
8159
8466
  case "bundle_events":
8467
+ case "bundle_installations":
8160
8468
  case "api_keys":
8161
8469
  case "release_catalogs":
8162
8470
  case "releases":
@@ -8164,7 +8472,7 @@ var insertQuery = /* @__PURE__ */ __name((input, guard, conflictMode = "returnEx
8164
8472
  values = Object.values(input.data);
8165
8473
  break;
8166
8474
  }
8167
- const conflict = input.onConflict !== "ignore" ? "" : input.model === "channels" ? conflictMode === "ignore" ? " ON CONFLICT(name) DO NOTHING" : " ON CONFLICT(name) DO UPDATE SET name = excluded.name" : input.model === "api_keys" ? conflictMode === "ignore" ? " ON CONFLICT(hash) DO NOTHING" : " ON CONFLICT(hash) DO UPDATE SET hash = excluded.hash" : "";
8475
+ const conflict = input.onConflict !== "ignore" ? "" : input.model === "channels" ? conflictMode === "ignore" ? " ON CONFLICT(name) DO NOTHING" : " ON CONFLICT(name) DO UPDATE SET name = excluded.name" : input.model === "api_keys" ? conflictMode === "ignore" ? " ON CONFLICT(hash) DO NOTHING" : " ON CONFLICT(hash) DO UPDATE SET hash = excluded.hash" : " ON CONFLICT(install_id) DO UPDATE SET install_id = excluded.install_id";
8168
8476
  return {
8169
8477
  sql: `INSERT INTO ${d1TableNames[input.model]} (${columns.join(", ")}) ${guard === void 0 ? `VALUES (${d1Placeholders(values.length)})` : `SELECT ${d1Placeholders(values.length)} WHERE ${guard.sql}`}${conflict} RETURNING *`,
8170
8478
  params: [...encodeD1Values(values), ...guard?.params ?? []]
@@ -8331,8 +8639,6 @@ var changeQuery = /* @__PURE__ */ __name((change, guard) => {
8331
8639
  ...guard.params
8332
8640
  ]
8333
8641
  };
8334
- case "insights":
8335
- return insertQuery({ model: "bundle_events", data: change.row }, guard);
8336
8642
  case "apiKeys":
8337
8643
  return change.operation === "insert" ? insertQuery(
8338
8644
  {
@@ -8503,6 +8809,28 @@ var deleteChannel = /* @__PURE__ */ __name(async (executor, input) => {
8503
8809
  return deletedRows.length > 0 ? { deleted: true } : { deleted: false, reason: "not_empty" };
8504
8810
  }, "deleteChannel");
8505
8811
  var createD1Implementation = /* @__PURE__ */ __name((executor) => ({
8812
+ async recordInsights({ event, installation }) {
8813
+ const columns = Object.keys(installation);
8814
+ const eventInsert = insertQuery({ model: "bundle_events", data: event });
8815
+ await executor.batch([
8816
+ {
8817
+ sql: `INSERT INTO bundle_installations (${columns.join(", ")})
8818
+ SELECT ${d1Placeholders(columns.length)}
8819
+ WHERE NOT EXISTS (SELECT 1 FROM bundle_events WHERE id = json_extract(?, '$'))
8820
+ ON CONFLICT(install_id) DO UPDATE SET ${columns.filter((column) => column !== "install_id").map((column) => `${column} = excluded.${column}`).join(", ")}
8821
+ WHERE (excluded.received_at_ms, excluded.id) >
8822
+ (bundle_installations.received_at_ms, bundle_installations.id)`,
8823
+ params: encodeD1Values([...Object.values(installation), event.id])
8824
+ },
8825
+ {
8826
+ ...eventInsert,
8827
+ sql: eventInsert.sql.replace(
8828
+ " RETURNING *",
8829
+ " ON CONFLICT(id) DO NOTHING"
8830
+ )
8831
+ }
8832
+ ]);
8833
+ },
8506
8834
  async create(input) {
8507
8835
  const query2 = insertQuery(input);
8508
8836
  const rows = await executor.query(query2.sql, query2.params);
@@ -8515,6 +8843,8 @@ var createD1Implementation = /* @__PURE__ */ __name((executor) => ({
8515
8843
  return parseD1Row("channels", rows[0]);
8516
8844
  case "bundle_events":
8517
8845
  return parseD1Row("bundle_events", rows[0]);
8846
+ case "bundle_installations":
8847
+ return parseD1Row("bundle_installations", rows[0]);
8518
8848
  case "api_keys":
8519
8849
  return parseD1Row("api_keys", rows[0]);
8520
8850
  case "releases":
@@ -8536,6 +8866,8 @@ var createD1Implementation = /* @__PURE__ */ __name((executor) => ({
8536
8866
  return parseD1Row("releases", rows[0]);
8537
8867
  case "release_catalogs":
8538
8868
  return parseD1Row("release_catalogs", rows[0]);
8869
+ case "bundle_installations":
8870
+ return parseD1Row("bundle_installations", rows[0]);
8539
8871
  }
8540
8872
  },
8541
8873
  async delete(input) {
@@ -8563,6 +8895,8 @@ var createD1Implementation = /* @__PURE__ */ __name((executor) => ({
8563
8895
  return parseD1Row("releases", rows[0]);
8564
8896
  case "release_catalogs":
8565
8897
  return parseD1Row("release_catalogs", rows[0]);
8898
+ case "bundle_installations":
8899
+ return parseD1Row("bundle_installations", rows[0]);
8566
8900
  }
8567
8901
  },
8568
8902
  findMany: /* @__PURE__ */ __name((input) => findManyD1Rows(executor, input), "findMany"),
@@ -8600,6 +8934,7 @@ var d1Database = /* @__PURE__ */ __name((database) => {
8600
8934
  const implementation = createD1Implementation({
8601
8935
  async query(sql, params) {
8602
8936
  const result = await database.prepare(sql).bind(...params).all();
8937
+ if (result.success === false) throw new D1ExecutionError();
8603
8938
  return result.results ?? [];
8604
8939
  },
8605
8940
  async batch(statements) {
@@ -8608,6 +8943,9 @@ var d1Database = /* @__PURE__ */ __name((database) => {
8608
8943
  ({ sql, params }) => database.prepare(sql).bind(...params)
8609
8944
  )
8610
8945
  );
8946
+ if (results.length !== statements.length || results.some(({ success }) => success === false)) {
8947
+ throw new D1ExecutionError();
8948
+ }
8611
8949
  return results.map(({ results: results2 }) => results2 ?? []);
8612
8950
  }
8613
8951
  });