@hot-updater/cloudflare 1.1.3 → 1.2.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -214,7 +214,7 @@ var bundleMetadataToRow = /* @__PURE__ */ __name((metadata) => {
214
214
  return metadata;
215
215
  }, "bundleMetadataToRow");
216
216
 
217
- // ../../packages/protocol/dist/index.mjs
217
+ // ../../packages/protocol/dist/uuid-kZvLA0Ly.mjs
218
218
  var LETTER_DASH_NUMBER = "[a-zA-Z0-9-]";
219
219
  var NUMERIC_IDENTIFIER = String.raw`0|[1-9]\d*`;
220
220
  var NUMERIC_IDENTIFIER_LOOSE = String.raw`\d+`;
@@ -330,6 +330,142 @@ function canonicalizeAppVersion(appVersion) {
330
330
  return version2 ? normalizeFull(version2) : null;
331
331
  }
332
332
  __name(canonicalizeAppVersion, "canonicalizeAppVersion");
333
+ var BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
334
+ var MAX_CATALOG_SEGMENT_LENGTH = 255;
335
+ function assertCatalogSegment(value, name) {
336
+ if (value.length === 0 || value.length > MAX_CATALOG_SEGMENT_LENGTH || !/^[A-Za-z0-9._~-]+$/.test(value)) throw new Error(`${name} must be 1-${MAX_CATALOG_SEGMENT_LENGTH} URL-safe ASCII characters`);
337
+ }
338
+ __name(assertCatalogSegment, "assertCatalogSegment");
339
+ function encodeUtf8(value) {
340
+ const bytes = [];
341
+ for (const codePointText of value) {
342
+ const codePoint = codePointText.codePointAt(0);
343
+ if (codePoint <= 127) bytes.push(codePoint);
344
+ else if (codePoint <= 2047) bytes.push(192 | codePoint >> 6, 128 | codePoint & 63);
345
+ else if (codePoint <= 65535) bytes.push(224 | codePoint >> 12, 128 | codePoint >> 6 & 63, 128 | codePoint & 63);
346
+ else bytes.push(240 | codePoint >> 18, 128 | codePoint >> 12 & 63, 128 | codePoint >> 6 & 63, 128 | codePoint & 63);
347
+ }
348
+ return bytes;
349
+ }
350
+ __name(encodeUtf8, "encodeUtf8");
351
+ function decodeUtf8(bytes) {
352
+ let result = "";
353
+ for (let index = 0; index < bytes.length; ) {
354
+ const first = bytes[index++];
355
+ let codePoint;
356
+ let continuationCount;
357
+ if (first <= 127) {
358
+ codePoint = first;
359
+ continuationCount = 0;
360
+ } else if ((first & 224) === 192) {
361
+ codePoint = first & 31;
362
+ continuationCount = 1;
363
+ } else if ((first & 240) === 224) {
364
+ codePoint = first & 15;
365
+ continuationCount = 2;
366
+ } else if ((first & 248) === 240) {
367
+ codePoint = first & 7;
368
+ continuationCount = 3;
369
+ } else throw new Error("Invalid UTF-8 channel key");
370
+ for (let offset = 0; offset < continuationCount; offset += 1) {
371
+ const continuation = bytes[index++];
372
+ if (continuation === void 0 || (continuation & 192) !== 128) throw new Error("Invalid UTF-8 channel key");
373
+ codePoint = codePoint << 6 | continuation & 63;
374
+ }
375
+ if (codePoint > 1114111 || codePoint >= 55296 && codePoint <= 57343 || continuationCount === 1 && codePoint < 128 || continuationCount === 2 && codePoint < 2048 || continuationCount === 3 && codePoint < 65536) throw new Error("Invalid UTF-8 channel key");
376
+ result += String.fromCodePoint(codePoint);
377
+ }
378
+ return result;
379
+ }
380
+ __name(decodeUtf8, "decodeUtf8");
381
+ function base64UrlEncode(bytes) {
382
+ let result = "";
383
+ for (let index = 0; index < bytes.length; index += 3) {
384
+ const first = bytes[index];
385
+ const second = bytes[index + 1];
386
+ const third = bytes[index + 2];
387
+ const value = first << 16 | (second ?? 0) << 8 | (third ?? 0);
388
+ result += BASE64URL_ALPHABET[value >> 18 & 63];
389
+ result += BASE64URL_ALPHABET[value >> 12 & 63];
390
+ if (second !== void 0) result += BASE64URL_ALPHABET[value >> 6 & 63];
391
+ if (third !== void 0) result += BASE64URL_ALPHABET[value & 63];
392
+ }
393
+ return result;
394
+ }
395
+ __name(base64UrlEncode, "base64UrlEncode");
396
+ function base64UrlDecode(value) {
397
+ if (!/^[A-Za-z0-9_-]*$/.test(value) || value.length % 4 === 1) throw new Error("Invalid channel key");
398
+ const bytes = [];
399
+ for (let index = 0; index < value.length; index += 4) {
400
+ const chunk = value.slice(index, index + 4);
401
+ const encoded = [...chunk].map((character) => BASE64URL_ALPHABET.indexOf(character));
402
+ if (encoded.some((part) => part < 0)) throw new Error("Invalid channel key");
403
+ const bits = encoded[0] << 18 | encoded[1] << 12 | (encoded[2] ?? 0) << 6 | (encoded[3] ?? 0);
404
+ bytes.push(bits >> 16 & 255);
405
+ if (chunk.length >= 3) bytes.push(bits >> 8 & 255);
406
+ if (chunk.length === 4) bytes.push(bits & 255);
407
+ }
408
+ return bytes;
409
+ }
410
+ __name(base64UrlDecode, "base64UrlDecode");
411
+ function normalizeChannelName(channel) {
412
+ return channel.trim().normalize("NFC");
413
+ }
414
+ __name(normalizeChannelName, "normalizeChannelName");
415
+ function assertCanonicalChannelName(channel) {
416
+ const normalized = normalizeChannelName(channel);
417
+ if (normalized.length === 0 || normalized !== channel) throw new Error("Channel name must be non-empty, trimmed, and NFC-normalized");
418
+ let codePointCount = 0;
419
+ for (const _codePoint of normalized) {
420
+ codePointCount += 1;
421
+ if (codePointCount > 255) throw new Error("Channel name must not exceed 255 characters");
422
+ }
423
+ }
424
+ __name(assertCanonicalChannelName, "assertCanonicalChannelName");
425
+ function encodeChannelKey(channel) {
426
+ assertCanonicalChannelName(channel);
427
+ return base64UrlEncode(encodeUtf8(channel));
428
+ }
429
+ __name(encodeChannelKey, "encodeChannelKey");
430
+ function decodeChannelKey(channelKey) {
431
+ const channel = decodeUtf8(base64UrlDecode(channelKey));
432
+ assertCanonicalChannelName(channel);
433
+ if (encodeChannelKey(channel) !== channelKey) throw new Error("Channel key is not canonically encoded");
434
+ return channel;
435
+ }
436
+ __name(decodeChannelKey, "decodeChannelKey");
437
+ function createReleaseCatalogScopeKey(input) {
438
+ decodeChannelKey(input.channelKey);
439
+ if (input.strategy === "APP_VERSION") return `v1:app-version:${input.platform}:${input.channelKey}`;
440
+ assertCatalogSegment(input.fingerprintHash, "Fingerprint hash");
441
+ return `v1:fingerprint:${input.platform}:${input.channelKey}:${input.fingerprintHash}`;
442
+ }
443
+ __name(createReleaseCatalogScopeKey, "createReleaseCatalogScopeKey");
444
+ function parseReleaseCatalogScopeKey(scopeKey) {
445
+ const segments = scopeKey.split(":");
446
+ const [version2, strategy, platform, channelKey] = segments;
447
+ if (version2 !== "v1" || platform !== "ios" && platform !== "android" || channelKey === void 0) throw new Error("Invalid release catalog scope key");
448
+ const input = strategy === "app-version" && segments.length === 4 ? {
449
+ channelKey,
450
+ platform,
451
+ strategy: "APP_VERSION"
452
+ } : strategy === "fingerprint" && segments.length === 5 && segments[4] !== void 0 ? {
453
+ channelKey,
454
+ fingerprintHash: segments[4],
455
+ platform,
456
+ strategy: "FINGERPRINT"
457
+ } : (() => {
458
+ throw new Error("Invalid release catalog scope key");
459
+ })();
460
+ if (createReleaseCatalogScopeKey(input) !== scopeKey) throw new Error("Release catalog scope key is not canonical");
461
+ return input;
462
+ }
463
+ __name(parseReleaseCatalogScopeKey, "parseReleaseCatalogScopeKey");
464
+ var NIL_UUID = "00000000-0000-0000-0000-000000000000";
465
+ var UUID_V7_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
466
+ var isUUIDv7 = /* @__PURE__ */ __name((value) => typeof value === "string" && UUID_V7_PATTERN.test(value), "isUUIDv7");
467
+
468
+ // ../../packages/protocol/dist/index.mjs
333
469
  var getManifestStorageUri = /* @__PURE__ */ __name((bundle) => bundle.manifestStorageUri, "getManifestStorageUri");
334
470
  var getManifestFileHash = /* @__PURE__ */ __name((bundle) => bundle.manifestFileHash, "getManifestFileHash");
335
471
  var getAssetBaseStorageUri = /* @__PURE__ */ __name((bundle) => bundle.assetBaseStorageUri, "getAssetBaseStorageUri");
@@ -469,140 +605,6 @@ var RELEASE_CATALOG_FALLBACK_POLICY = "BUILTIN_IF_ACTIVE_INELIGIBLE";
469
605
  var MAX_TARGET_COHORTS_PER_RELEASE = 100;
470
606
  var MAX_DISTINCT_TARGET_COHORTS_PER_SCOPE = 512;
471
607
  var MAX_COMPILED_CATALOG_BYTES = 262144;
472
- var BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
473
- var MAX_CATALOG_SEGMENT_LENGTH = 255;
474
- function assertCatalogSegment(value, name) {
475
- if (value.length === 0 || value.length > MAX_CATALOG_SEGMENT_LENGTH || !/^[A-Za-z0-9._~-]+$/.test(value)) throw new Error(`${name} must be 1-${MAX_CATALOG_SEGMENT_LENGTH} URL-safe ASCII characters`);
476
- }
477
- __name(assertCatalogSegment, "assertCatalogSegment");
478
- function encodeUtf8(value) {
479
- const bytes = [];
480
- for (const codePointText of value) {
481
- const codePoint = codePointText.codePointAt(0);
482
- if (codePoint <= 127) bytes.push(codePoint);
483
- else if (codePoint <= 2047) bytes.push(192 | codePoint >> 6, 128 | codePoint & 63);
484
- else if (codePoint <= 65535) bytes.push(224 | codePoint >> 12, 128 | codePoint >> 6 & 63, 128 | codePoint & 63);
485
- else bytes.push(240 | codePoint >> 18, 128 | codePoint >> 12 & 63, 128 | codePoint >> 6 & 63, 128 | codePoint & 63);
486
- }
487
- return bytes;
488
- }
489
- __name(encodeUtf8, "encodeUtf8");
490
- function decodeUtf8(bytes) {
491
- let result = "";
492
- for (let index = 0; index < bytes.length; ) {
493
- const first = bytes[index++];
494
- let codePoint;
495
- let continuationCount;
496
- if (first <= 127) {
497
- codePoint = first;
498
- continuationCount = 0;
499
- } else if ((first & 224) === 192) {
500
- codePoint = first & 31;
501
- continuationCount = 1;
502
- } else if ((first & 240) === 224) {
503
- codePoint = first & 15;
504
- continuationCount = 2;
505
- } else if ((first & 248) === 240) {
506
- codePoint = first & 7;
507
- continuationCount = 3;
508
- } else throw new Error("Invalid UTF-8 channel key");
509
- for (let offset = 0; offset < continuationCount; offset += 1) {
510
- const continuation = bytes[index++];
511
- if (continuation === void 0 || (continuation & 192) !== 128) throw new Error("Invalid UTF-8 channel key");
512
- codePoint = codePoint << 6 | continuation & 63;
513
- }
514
- if (codePoint > 1114111 || codePoint >= 55296 && codePoint <= 57343 || continuationCount === 1 && codePoint < 128 || continuationCount === 2 && codePoint < 2048 || continuationCount === 3 && codePoint < 65536) throw new Error("Invalid UTF-8 channel key");
515
- result += String.fromCodePoint(codePoint);
516
- }
517
- return result;
518
- }
519
- __name(decodeUtf8, "decodeUtf8");
520
- function base64UrlEncode(bytes) {
521
- let result = "";
522
- for (let index = 0; index < bytes.length; index += 3) {
523
- const first = bytes[index];
524
- const second = bytes[index + 1];
525
- const third = bytes[index + 2];
526
- const value = first << 16 | (second ?? 0) << 8 | (third ?? 0);
527
- result += BASE64URL_ALPHABET[value >> 18 & 63];
528
- result += BASE64URL_ALPHABET[value >> 12 & 63];
529
- if (second !== void 0) result += BASE64URL_ALPHABET[value >> 6 & 63];
530
- if (third !== void 0) result += BASE64URL_ALPHABET[value & 63];
531
- }
532
- return result;
533
- }
534
- __name(base64UrlEncode, "base64UrlEncode");
535
- function base64UrlDecode(value) {
536
- if (!/^[A-Za-z0-9_-]*$/.test(value) || value.length % 4 === 1) throw new Error("Invalid channel key");
537
- const bytes = [];
538
- for (let index = 0; index < value.length; index += 4) {
539
- const chunk = value.slice(index, index + 4);
540
- const encoded = [...chunk].map((character) => BASE64URL_ALPHABET.indexOf(character));
541
- if (encoded.some((part) => part < 0)) throw new Error("Invalid channel key");
542
- const bits = encoded[0] << 18 | encoded[1] << 12 | (encoded[2] ?? 0) << 6 | (encoded[3] ?? 0);
543
- bytes.push(bits >> 16 & 255);
544
- if (chunk.length >= 3) bytes.push(bits >> 8 & 255);
545
- if (chunk.length === 4) bytes.push(bits & 255);
546
- }
547
- return bytes;
548
- }
549
- __name(base64UrlDecode, "base64UrlDecode");
550
- function normalizeChannelName(channel) {
551
- return channel.trim().normalize("NFC");
552
- }
553
- __name(normalizeChannelName, "normalizeChannelName");
554
- function assertCanonicalChannelName(channel) {
555
- const normalized = normalizeChannelName(channel);
556
- if (normalized.length === 0 || normalized !== channel) throw new Error("Channel name must be non-empty, trimmed, and NFC-normalized");
557
- let codePointCount = 0;
558
- for (const _codePoint of normalized) {
559
- codePointCount += 1;
560
- if (codePointCount > 255) throw new Error("Channel name must not exceed 255 characters");
561
- }
562
- }
563
- __name(assertCanonicalChannelName, "assertCanonicalChannelName");
564
- function encodeChannelKey(channel) {
565
- assertCanonicalChannelName(channel);
566
- return base64UrlEncode(encodeUtf8(channel));
567
- }
568
- __name(encodeChannelKey, "encodeChannelKey");
569
- function decodeChannelKey(channelKey) {
570
- const channel = decodeUtf8(base64UrlDecode(channelKey));
571
- assertCanonicalChannelName(channel);
572
- if (encodeChannelKey(channel) !== channelKey) throw new Error("Channel key is not canonically encoded");
573
- return channel;
574
- }
575
- __name(decodeChannelKey, "decodeChannelKey");
576
- function createReleaseCatalogScopeKey(input) {
577
- decodeChannelKey(input.channelKey);
578
- if (input.strategy === "APP_VERSION") return `v1:app-version:${input.platform}:${input.channelKey}`;
579
- assertCatalogSegment(input.fingerprintHash, "Fingerprint hash");
580
- return `v1:fingerprint:${input.platform}:${input.channelKey}:${input.fingerprintHash}`;
581
- }
582
- __name(createReleaseCatalogScopeKey, "createReleaseCatalogScopeKey");
583
- function parseReleaseCatalogScopeKey(scopeKey) {
584
- const segments = scopeKey.split(":");
585
- const [version2, strategy, platform, channelKey] = segments;
586
- if (version2 !== "v1" || platform !== "ios" && platform !== "android" || channelKey === void 0) throw new Error("Invalid release catalog scope key");
587
- const input = strategy === "app-version" && segments.length === 4 ? {
588
- channelKey,
589
- platform,
590
- strategy: "APP_VERSION"
591
- } : strategy === "fingerprint" && segments.length === 5 && segments[4] !== void 0 ? {
592
- channelKey,
593
- fingerprintHash: segments[4],
594
- platform,
595
- strategy: "FINGERPRINT"
596
- } : (() => {
597
- throw new Error("Invalid release catalog scope key");
598
- })();
599
- if (createReleaseCatalogScopeKey(input) !== scopeKey) throw new Error("Release catalog scope key is not canonical");
600
- return input;
601
- }
602
- __name(parseReleaseCatalogScopeKey, "parseReleaseCatalogScopeKey");
603
- var NIL_UUID = "00000000-0000-0000-0000-000000000000";
604
- var UUID_V7_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
605
- var isUUIDv7 = /* @__PURE__ */ __name((value) => typeof value === "string" && UUID_V7_PATTERN.test(value), "isUUIDv7");
606
608
  var ARTIFACT_PROTOCOL_VERSION = 1;
607
609
  var ARTIFACT_FILES_PROTOCOL_VERSION = 2;
608
610
  var ARTIFACT_FILES_REQUEST_LIMIT = 100;
@@ -1202,8 +1204,8 @@ function releaseDescriptor(release) {
1202
1204
  };
1203
1205
  }
1204
1206
  __name(releaseDescriptor, "releaseDescriptor");
1205
- function validateReleases(releases, strategy) {
1206
- const enabled = releases.filter((release) => release.enabled).sort((left, right) => right.id.localeCompare(left.id));
1207
+ function validateReleases(releases2, strategy) {
1208
+ const enabled = releases2.filter((release) => release.enabled).sort((left, right) => right.id.localeCompare(left.id));
1207
1209
  const seenIds = /* @__PURE__ */ new Set();
1208
1210
  for (const release of enabled) {
1209
1211
  if (!isUUIDv7(release.id)) throw new ReleaseCatalogCompilationError("INVALID_RELEASE", `Release ${release.id} must use a canonical lowercase UUIDv7 ID`);
@@ -1218,11 +1220,11 @@ function validateReleases(releases, strategy) {
1218
1220
  return enabled;
1219
1221
  }
1220
1222
  __name(validateReleases, "validateReleases");
1221
- function collectFrontierReleaseIds(releases) {
1222
- if (releases.every((release) => normalizeRolloutCohortCount(release.rolloutCohortCount) >= NUMERIC_COHORT_SIZE)) {
1223
+ function collectFrontierReleaseIds(releases2) {
1224
+ if (releases2.every((release) => normalizeRolloutCohortCount(release.rolloutCohortCount) >= NUMERIC_COHORT_SIZE)) {
1223
1225
  const retained3 = /* @__PURE__ */ new Set();
1224
1226
  const bundleIds = /* @__PURE__ */ new Set();
1225
- for (const release of releases) {
1227
+ for (const release of releases2) {
1226
1228
  if (release.bundleId !== null && !bundleIds.has(release.bundleId)) {
1227
1229
  bundleIds.add(release.bundleId);
1228
1230
  retained3.add(release.id);
@@ -1231,7 +1233,7 @@ function collectFrontierReleaseIds(releases) {
1231
1233
  }
1232
1234
  return retained3;
1233
1235
  }
1234
- const explicitTargetCohorts = new Set(releases.flatMap((release) => release.targetCohorts));
1236
+ const explicitTargetCohorts = new Set(releases2.flatMap((release) => release.targetCohorts));
1235
1237
  const cohortStates = [
1236
1238
  void 0,
1237
1239
  ...Array.from({ length: 1e3 }, (_, index) => String(index + 1)),
@@ -1259,7 +1261,7 @@ function collectFrontierReleaseIds(releases) {
1259
1261
  }
1260
1262
  const retained2 = /* @__PURE__ */ new Set();
1261
1263
  let incompleteStateCount = cohortStates.length;
1262
- for (const release of releases) {
1264
+ for (const release of releases2) {
1263
1265
  const rolloutCohortCount = normalizeRolloutCohortCount(release.rolloutCohortCount);
1264
1266
  const eligibleStateIndexes = /* @__PURE__ */ new Set();
1265
1267
  if (rolloutCohortCount >= NUMERIC_COHORT_SIZE) for (const index of fullRolloutStateIndexes) eligibleStateIndexes.add(index);
@@ -1360,11 +1362,11 @@ function mergeSegments(segments) {
1360
1362
  return merged;
1361
1363
  }
1362
1364
  __name(mergeSegments, "mergeSegments");
1363
- function compileAppVersion(releases) {
1365
+ function compileAppVersion(releases2) {
1364
1366
  const ranges = /* @__PURE__ */ new Map();
1365
- for (const release of releases) ranges.set(release.id, rangeToIntervals(release.targetAppVersion));
1367
+ for (const release of releases2) ranges.set(release.id, rangeToIntervals(release.targetAppVersion));
1366
1368
  const segmentReleases = atomicVersionSegments([...ranges.values()].flat()).map((segment) => {
1367
- const applicable = releases.filter((release) => ranges.get(release.id).some((range) => intervalContainsSegment(range, segment)));
1369
+ const applicable = releases2.filter((release) => ranges.get(release.id).some((range) => intervalContainsSegment(range, segment)));
1368
1370
  return {
1369
1371
  rollbackIds: new Set(applicable.map(({ id }) => id)),
1370
1372
  segment,
@@ -1372,12 +1374,12 @@ function compileAppVersion(releases) {
1372
1374
  };
1373
1375
  });
1374
1376
  const retainedIds = new Set(segmentReleases.flatMap(({ retainedIds: retainedIds2, rollbackIds }) => [...retainedIds2, ...rollbackIds]));
1375
- const retainedReleases = releases.filter((release) => retainedIds.has(release.id));
1377
+ const retainedReleases = releases2.filter((release) => retainedIds.has(release.id));
1376
1378
  const descriptorIndex = new Map(retainedReleases.map((release, index) => [release.id, index]));
1377
1379
  const segments = mergeSegments(segmentReleases.map(({ segment, retainedIds: retainedIds2, rollbackIds }) => ({
1378
1380
  ...segment,
1379
- releaseIndexes: releases.filter((release) => retainedIds2.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0),
1380
- rollbackReleaseIndexes: releases.filter((release) => rollbackIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0)
1381
+ releaseIndexes: releases2.filter((release) => retainedIds2.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0),
1382
+ rollbackReleaseIndexes: releases2.filter((release) => rollbackIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0)
1381
1383
  }))).filter((segment) => segment.releaseIndexes.length > 0 || segment.rollbackReleaseIndexes.length > 0);
1382
1384
  return {
1383
1385
  envelope: {
@@ -1390,9 +1392,9 @@ function compileAppVersion(releases) {
1390
1392
  };
1391
1393
  }
1392
1394
  __name(compileAppVersion, "compileAppVersion");
1393
- function compileFingerprint(releases) {
1394
- const retainedIds = collectFrontierReleaseIds(releases);
1395
- const retainedReleases = releases;
1395
+ function compileFingerprint(releases2) {
1396
+ const retainedIds = collectFrontierReleaseIds(releases2);
1397
+ const retainedReleases = releases2;
1396
1398
  return {
1397
1399
  envelope: {
1398
1400
  fallbackPolicy: RELEASE_CATALOG_FALLBACK_POLICY,
@@ -1418,10 +1420,10 @@ function canonicalStringify(value) {
1418
1420
  }
1419
1421
  __name(canonicalStringify, "canonicalStringify");
1420
1422
  async function compileReleaseCatalog(input) {
1421
- const releases = validateReleases(input.releases, input.strategy);
1422
- const targetCohorts = new Set(releases.flatMap((release) => release.targetCohorts));
1423
+ const releases2 = validateReleases(input.releases, input.strategy);
1424
+ const targetCohorts = new Set(releases2.flatMap((release) => release.targetCohorts));
1423
1425
  if (targetCohorts.size > MAX_DISTINCT_TARGET_COHORTS_PER_SCOPE) throw new ReleaseCatalogCompilationError("DISTINCT_TARGET_COHORT_LIMIT", `Scope exceeds ${MAX_DISTINCT_TARGET_COHORTS_PER_SCOPE} distinct target cohorts`);
1424
- const { envelope, retainedReleases } = input.strategy === "APP_VERSION" ? compileAppVersion(releases) : compileFingerprint(releases);
1426
+ const { envelope, retainedReleases } = input.strategy === "APP_VERSION" ? compileAppVersion(releases2) : compileFingerprint(releases2);
1425
1427
  const payload = {
1426
1428
  ...envelope,
1427
1429
  releaseDescriptors: retainedReleases.map(releaseDescriptor)
@@ -1437,7 +1439,7 @@ async function compileReleaseCatalog(input) {
1437
1439
  byteSize,
1438
1440
  descriptorCount: payload.releaseDescriptors.length,
1439
1441
  distinctTargetCohortCount: targetCohorts.size,
1440
- releaseCount: releases.length,
1442
+ releaseCount: releases2.length,
1441
1443
  segmentCount: payload.strategy === "APP_VERSION" ? payload.segments.length : 1
1442
1444
  },
1443
1445
  payload
@@ -1984,10 +1986,11 @@ var SETTINGS_TABLE = {
1984
1986
  var ENGINE_SCHEMA_KEY = "schema.engine";
1985
1987
  var CORE_SCHEMA_KEY = "schema.core";
1986
1988
  var stored = /* @__PURE__ */ __name(({ key, expected, found }) => `"${key}" is ${found === null ? "missing" : `"${found}"`}; expected "${expected}"`, "stored");
1987
- var settingMessage = /* @__PURE__ */ __name((adapterName, { key, expected, found }) => `Hot Updater schema setting "${key}" for ${adapterName} is ${found === null ? "missing" : `"${found}"`}; expected "${expected}". Run \`hot-updater db migrate\`.`, "settingMessage");
1989
+ var MIGRATE_SCHEMA_FIX = "Run `npx hot-updater doctor` in the project whose hot-updater.config.ts names this database: it names the command that migrates it. In a server project, `hot-updater db migrate` migrates the database its server file names.";
1990
+ var settingMessage = /* @__PURE__ */ __name((adapterName, { key, expected, found }) => `Hot Updater schema setting "${key}" for ${adapterName} is ${found === null ? "missing" : `"${found}"`}; expected "${expected}". ${MIGRATE_SCHEMA_FIX}`, "settingMessage");
1988
1991
  var pluginOf = /* @__PURE__ */ __name(({ key }) => key.startsWith("schema.") && key !== "schema.engine" && key !== CORE_SCHEMA_KEY ? key.slice(7) : void 0, "pluginOf");
1989
1992
  var listed = /* @__PURE__ */ __name((items) => items.length < 2 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`, "listed");
1990
- var PLUGIN_TABLES_FIX = "Run `hot-updater db migrate`. A managed server runs only its provider's plugins, and rerunning `hot-updater init --provider <provider>` applies the provider's migrations. Run `hot-updater db` commands in the server's project: they load the server file named on the command line, or src/hotUpdater.* or src/db.*, and don't read hot-updater.config.ts.";
1993
+ var settingsMessage = /* @__PURE__ */ __name((adapterName, settings) => `Hot Updater schema settings for ${adapterName} are not migrated: ${settings.map(stored).join(", and ")}. ${MIGRATE_SCHEMA_FIX}`, "settingsMessage");
1991
1994
  var pluginsMessage = /* @__PURE__ */ __name((adapterName, plugins, settings, fix) => `The tables of ${plugins.length === 1 ? "plugin" : "plugins"} ${listed(plugins.map((id) => `"${id}"`))} are not migrated on ${adapterName}: schema ${settings.length === 1 ? "setting" : "settings"} ${settings.map(stored).join(", and ")}. ${fix}`, "pluginsMessage");
1992
1995
  var HotUpdaterSchemaMigrationRequiredError = class extends Error {
1993
1996
  static {
@@ -2006,7 +2009,7 @@ var HotUpdaterSchemaMigrationRequiredError = class extends Error {
2006
2009
  constructor(adapterName, currentVersion, setting, options) {
2007
2010
  const settings = options?.settings ?? (setting === void 0 ? [] : [setting]);
2008
2011
  const plugins = settings.flatMap((mismatch) => pluginOf(mismatch) ?? []);
2009
- super(setting === void 0 ? currentVersion === void 0 ? `Hot Updater database schema is not initialized for ${adapterName}. Run \`hot-updater db migrate\` before using this adapter.` : `Hot Updater v1 cannot migrate schema ${currentVersion} in place. Create a new empty database and run \`hot-updater db migrate\`.` : plugins.length > 0 && plugins.length === settings.length ? pluginsMessage(adapterName, plugins, settings, options?.fix ?? PLUGIN_TABLES_FIX) : settingMessage(adapterName, setting), options?.cause === void 0 ? void 0 : { cause: options.cause });
2012
+ super(setting === void 0 ? currentVersion === void 0 ? `Hot Updater database schema is not initialized for ${adapterName}. ${MIGRATE_SCHEMA_FIX}` : `Hot Updater v1 cannot migrate schema ${currentVersion} in place. Create a new empty database and run \`hot-updater db migrate\`.` : plugins.length > 0 && plugins.length === settings.length ? pluginsMessage(adapterName, plugins, settings, options?.fix ?? MIGRATE_SCHEMA_FIX) : settings.length > 1 && !(setting.key === "schema.engine" && setting.found === null) ? settingsMessage(adapterName, settings) : settingMessage(adapterName, setting), options?.cause === void 0 ? void 0 : { cause: options.cause });
2010
2013
  this.adapterName = adapterName;
2011
2014
  this.currentVersion = currentVersion;
2012
2015
  this.setting = setting;
@@ -2059,11 +2062,23 @@ var checkSchemaFence = /* @__PURE__ */ __name(async (adapter, adapterName, expec
2059
2062
  });
2060
2063
  }, "checkSchemaFence");
2061
2064
  var FENCED = /* @__PURE__ */ Symbol.for("@hot-updater/server/schema-fence");
2065
+ var FENCED_SETTINGS = /* @__PURE__ */ Symbol.for("@hot-updater/server/schema-fence-settings");
2062
2066
  var fencedName = /* @__PURE__ */ __name((adapter) => adapter[FENCED], "fencedName");
2063
2067
  var withSchemaFence = /* @__PURE__ */ __name((adapter, adapterName, expected, fix) => {
2068
+ const inner = adapter[FENCED_SETTINGS];
2069
+ const settings = inner === void 0 ? {
2070
+ adapter,
2071
+ expected
2072
+ } : {
2073
+ adapter: inner.adapter,
2074
+ expected: {
2075
+ ...inner.expected,
2076
+ ...expected
2077
+ }
2078
+ };
2064
2079
  let ready;
2065
2080
  const fence = /* @__PURE__ */ __name(() => {
2066
- ready ??= checkSchemaFence(adapter, adapterName, expected, fix).catch((error) => {
2081
+ ready ??= checkSchemaFence(settings.adapter, adapterName, settings.expected, fix).catch((error) => {
2067
2082
  ready = void 0;
2068
2083
  throw error;
2069
2084
  });
@@ -2072,6 +2087,7 @@ var withSchemaFence = /* @__PURE__ */ __name((adapter, adapterName, expected, fi
2072
2087
  return {
2073
2088
  ...adapter,
2074
2089
  [FENCED]: adapterName,
2090
+ [FENCED_SETTINGS]: settings,
2075
2091
  get: /* @__PURE__ */ __name((table, keys) => fence().then(() => adapter.get(table, keys)), "get"),
2076
2092
  query: /* @__PURE__ */ __name((table, request) => fence().then(() => adapter.query(table, request)), "query"),
2077
2093
  write: /* @__PURE__ */ __name((ops) => fence().then(() => adapter.write(ops)), "write"),
@@ -2215,11 +2231,11 @@ var createTableStatements = /* @__PURE__ */ __name((dialect, tables, tablePrefix
2215
2231
  const render = /* @__PURE__ */ __name((shape) => {
2216
2232
  const definitions = shape.columns.map((column) => `${quote(column.name)} ${column.type}${column.notNull ? " NOT NULL" : ""}${column.default === void 0 ? "" : ` DEFAULT ${column.default}`}`);
2217
2233
  definitions.push(`PRIMARY KEY (${list(shape.key)})`);
2218
- const after = [];
2219
- for (const index of shape.indexes) if (index.kind === "table") after.push(...render(index.table));
2234
+ const after2 = [];
2235
+ for (const index of shape.indexes) if (index.kind === "table") after2.push(...render(index.table));
2220
2236
  else if (dialect === "mysql") definitions.push(`${index.unique ? "UNIQUE " : ""}INDEX ${quote(index.name)} (${list(index.columns)})`);
2221
- else after.push(`CREATE ${index.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${quote(index.name)} ON ${quote(shape.name)} (${list(index.columns)})`);
2222
- return [`CREATE TABLE IF NOT EXISTS ${quote(shape.name)} (${definitions.join(", ")})`, ...after];
2237
+ else after2.push(`CREATE ${index.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${quote(index.name)} ON ${quote(shape.name)} (${list(index.columns)})`);
2238
+ return [`CREATE TABLE IF NOT EXISTS ${quote(shape.name)} (${definitions.join(", ")})`, ...after2];
2223
2239
  }, "render");
2224
2240
  return sqlTableShapes(dialect, tables, tablePrefix).flatMap(render);
2225
2241
  }, "createTableStatements");
@@ -2261,255 +2277,358 @@ var CHANNEL_ID = {
2261
2277
  maxLength: 1408,
2262
2278
  ascii: true
2263
2279
  };
2264
- var HOT_UPDATER_SCHEMA_VERSION = "1.0.0";
2265
- var coreSchema = {
2266
- bundles: defineTable({
2267
- id: {
2268
- type: "string",
2269
- maxLength: 36
2270
- },
2271
- platform: {
2272
- type: "string",
2273
- maxLength: 16
2274
- },
2275
- git_commit_hash: {
2276
- type: "string",
2277
- maxLength: 255,
2278
- required: false
2280
+ var HOT_UPDATER_SCHEMA_VERSION = "1.2.0";
2281
+ var bundles = defineTable({
2282
+ id: {
2283
+ type: "string",
2284
+ maxLength: 36
2285
+ },
2286
+ platform: {
2287
+ type: "string",
2288
+ maxLength: 16
2289
+ },
2290
+ git_commit_hash: {
2291
+ type: "string",
2292
+ maxLength: 255,
2293
+ required: false
2294
+ },
2295
+ metadata: { type: "json" },
2296
+ manifest_storage_uri: { type: "string" },
2297
+ manifest_file_hash: { type: "string" },
2298
+ asset_base_storage_uri: { type: "string" }
2299
+ }, {
2300
+ key: ["id"],
2301
+ indexes: {
2302
+ byPlatform: {
2303
+ eq: ["platform"],
2304
+ sort: ["id"]
2279
2305
  },
2280
- metadata: { type: "json" },
2281
- manifest_storage_uri: { type: "string" },
2282
- manifest_file_hash: { type: "string" },
2283
- asset_base_storage_uri: { type: "string" }
2284
- }, {
2285
- key: ["id"],
2286
- indexes: {
2287
- byPlatform: {
2288
- eq: ["platform"],
2289
- sort: ["id"]
2290
- },
2291
- all: {
2292
- eq: [],
2293
- sort: ["id"]
2294
- }
2306
+ all: {
2307
+ eq: [],
2308
+ sort: ["id"]
2295
2309
  }
2296
- }),
2297
- bundle_patches: defineTable({
2298
- id: {
2299
- type: "string",
2300
- maxLength: 255
2301
- },
2302
- bundle_id: {
2303
- type: "string",
2304
- maxLength: 36,
2305
- references: {
2306
- model: "bundles",
2307
- onDelete: "cascade"
2308
- }
2309
- },
2310
- base_bundle_id: {
2311
- type: "string",
2312
- maxLength: 36,
2313
- references: {
2314
- model: "bundles",
2315
- onDelete: "cascade"
2316
- }
2317
- },
2318
- base_file_hash: { type: "string" },
2319
- patch_file_hash: { type: "string" },
2320
- patch_storage_uri: { type: "string" },
2321
- byte_size: { type: "integer" },
2322
- order_index: { type: "integer" }
2323
- }, {
2324
- key: ["id"],
2325
- indexes: {
2326
- pair: {
2327
- eq: ["bundle_id", "base_bundle_id"],
2328
- sort: [],
2329
- unique: true
2330
- },
2331
- byBundle: {
2332
- eq: ["bundle_id"],
2333
- sort: ["order_index"],
2334
- root: { model: "bundles" }
2335
- },
2336
- byBase: {
2337
- eq: ["base_bundle_id"],
2338
- sort: ["bundle_id"]
2339
- }
2310
+ }
2311
+ });
2312
+ var bundlePatches = defineTable({
2313
+ id: {
2314
+ type: "string",
2315
+ maxLength: 255
2316
+ },
2317
+ bundle_id: {
2318
+ type: "string",
2319
+ maxLength: 36,
2320
+ references: {
2321
+ model: "bundles",
2322
+ onDelete: "cascade"
2340
2323
  }
2341
- }),
2342
- releases: defineTable({
2343
- id: {
2344
- type: "string",
2345
- maxLength: 36
2346
- },
2347
- revision: { type: "integer" },
2348
- scope_key: {
2349
- type: "string",
2350
- maxLength: 2048,
2351
- ascii: true
2352
- },
2353
- channel_id: {
2354
- ...CHANNEL_ID,
2355
- references: {
2356
- model: "channels",
2357
- onDelete: "restrict"
2358
- }
2359
- },
2360
- platform: {
2361
- type: "string",
2362
- maxLength: 16
2363
- },
2364
- kind: {
2365
- type: "string",
2366
- maxLength: 16
2367
- },
2368
- bundle_id: {
2369
- type: "string",
2370
- maxLength: 36,
2371
- required: false,
2372
- references: {
2373
- model: "bundles",
2374
- onDelete: "restrict"
2375
- }
2376
- },
2377
- strategy: {
2378
- type: "string",
2379
- maxLength: 16
2380
- },
2381
- target_app_version: {
2382
- type: "string",
2383
- required: false
2384
- },
2385
- fingerprint_hash: {
2386
- type: "string",
2387
- maxLength: 255,
2388
- required: false
2389
- },
2390
- enabled: { type: "boolean" },
2391
- should_force_update: { type: "boolean" },
2392
- message: {
2393
- type: "string",
2394
- required: false
2395
- },
2396
- rollout_cohort_count: { type: "integer" },
2397
- target_cohorts: { type: "json" },
2398
- operation: {
2399
- type: "string",
2400
- maxLength: 16
2324
+ },
2325
+ base_bundle_id: {
2326
+ type: "string",
2327
+ maxLength: 36,
2328
+ references: {
2329
+ model: "bundles",
2330
+ onDelete: "cascade"
2331
+ }
2332
+ },
2333
+ base_file_hash: { type: "string" },
2334
+ patch_file_hash: { type: "string" },
2335
+ patch_storage_uri: { type: "string" },
2336
+ byte_size: { type: "integer" },
2337
+ order_index: { type: "integer" }
2338
+ }, {
2339
+ key: ["id"],
2340
+ indexes: {
2341
+ pair: {
2342
+ eq: ["bundle_id", "base_bundle_id"],
2343
+ sort: [],
2344
+ unique: true
2401
2345
  },
2402
- source_release_id: {
2403
- type: "string",
2404
- maxLength: 36,
2405
- required: false,
2406
- references: {
2407
- model: "releases",
2408
- onDelete: "none"
2409
- }
2346
+ byBundle: {
2347
+ eq: ["bundle_id"],
2348
+ sort: ["order_index"],
2349
+ root: { model: "bundles" }
2410
2350
  },
2411
- created_at_ms: { type: "integer" },
2412
- updated_at_ms: { type: "integer" }
2413
- }, {
2414
- key: ["id"],
2415
- indexes: {
2416
- byScope: {
2417
- eq: ["scope_key"],
2418
- sort: ["id"],
2419
- root: { model: "release_catalogs" }
2420
- },
2421
- byScopeEnabled: {
2422
- eq: ["scope_key", "enabled"],
2423
- sort: ["id"],
2424
- root: { model: "release_catalogs" }
2425
- },
2426
- byChannelPlatform: {
2427
- eq: ["channel_id", "platform"],
2428
- sort: ["id"]
2429
- },
2430
- byChannelPlatformEnabled: {
2431
- eq: [
2432
- "channel_id",
2433
- "platform",
2434
- "enabled"
2435
- ],
2436
- sort: ["id"]
2437
- },
2438
- byBundle: {
2439
- eq: ["bundle_id"],
2440
- sort: ["id"]
2441
- },
2442
- all: {
2443
- eq: [],
2444
- sort: ["id"]
2445
- }
2351
+ byBase: {
2352
+ eq: ["base_bundle_id"],
2353
+ sort: ["bundle_id"]
2354
+ }
2355
+ }
2356
+ });
2357
+ var releases = defineTable({
2358
+ id: {
2359
+ type: "string",
2360
+ maxLength: 36
2361
+ },
2362
+ revision: { type: "integer" },
2363
+ scope_key: {
2364
+ type: "string",
2365
+ maxLength: 2048,
2366
+ ascii: true
2367
+ },
2368
+ channel_id: {
2369
+ ...CHANNEL_ID,
2370
+ references: {
2371
+ model: "channels",
2372
+ onDelete: "restrict"
2373
+ }
2374
+ },
2375
+ platform: {
2376
+ type: "string",
2377
+ maxLength: 16
2378
+ },
2379
+ kind: {
2380
+ type: "string",
2381
+ maxLength: 16
2382
+ },
2383
+ bundle_id: {
2384
+ type: "string",
2385
+ maxLength: 36,
2386
+ required: false,
2387
+ references: {
2388
+ model: "bundles",
2389
+ onDelete: "restrict"
2446
2390
  }
2447
- }),
2448
- release_catalogs: defineTable({
2449
- scope_key: {
2450
- type: "string",
2451
- maxLength: 2048,
2452
- ascii: true
2391
+ },
2392
+ strategy: {
2393
+ type: "string",
2394
+ maxLength: 16
2395
+ },
2396
+ target_app_version: {
2397
+ type: "string",
2398
+ required: false
2399
+ },
2400
+ fingerprint_hash: {
2401
+ type: "string",
2402
+ maxLength: 255,
2403
+ required: false
2404
+ },
2405
+ enabled: { type: "boolean" },
2406
+ should_force_update: { type: "boolean" },
2407
+ message: {
2408
+ type: "string",
2409
+ required: false
2410
+ },
2411
+ rollout_cohort_count: { type: "integer" },
2412
+ target_cohorts: { type: "json" },
2413
+ operation: {
2414
+ type: "string",
2415
+ maxLength: 16
2416
+ },
2417
+ source_release_id: {
2418
+ type: "string",
2419
+ maxLength: 36,
2420
+ required: false,
2421
+ references: {
2422
+ model: "releases",
2423
+ onDelete: "none"
2424
+ }
2425
+ },
2426
+ created_at_ms: { type: "integer" },
2427
+ updated_at_ms: { type: "integer" }
2428
+ }, {
2429
+ key: ["id"],
2430
+ indexes: {
2431
+ byScope: {
2432
+ eq: ["scope_key"],
2433
+ sort: ["id"],
2434
+ root: { model: "release_catalogs" }
2453
2435
  },
2454
- catalog_id: {
2455
- type: "string",
2456
- maxLength: 255
2436
+ byScopeEnabled: {
2437
+ eq: ["scope_key", "enabled"],
2438
+ sort: ["id"],
2439
+ root: { model: "release_catalogs" }
2457
2440
  },
2458
- strategy: {
2459
- type: "string",
2460
- maxLength: 16
2441
+ byChannelPlatform: {
2442
+ eq: ["channel_id", "platform"],
2443
+ sort: ["id"]
2461
2444
  },
2462
- channel_id: {
2463
- ...CHANNEL_ID,
2464
- references: {
2465
- model: "channels",
2466
- onDelete: "none"
2467
- }
2445
+ byChannelPlatformEnabled: {
2446
+ eq: [
2447
+ "channel_id",
2448
+ "platform",
2449
+ "enabled"
2450
+ ],
2451
+ sort: ["id"]
2468
2452
  },
2469
- channel_key: {
2470
- type: "string",
2471
- maxLength: 1400,
2472
- ascii: true
2453
+ byBundle: {
2454
+ eq: ["bundle_id"],
2455
+ sort: ["id"]
2473
2456
  },
2474
- platform: {
2475
- type: "string",
2476
- maxLength: 16
2457
+ all: {
2458
+ eq: [],
2459
+ sort: ["id"]
2460
+ }
2461
+ }
2462
+ });
2463
+ var releaseCatalogs = defineTable({
2464
+ scope_key: {
2465
+ type: "string",
2466
+ maxLength: 2048,
2467
+ ascii: true
2468
+ },
2469
+ catalog_id: {
2470
+ type: "string",
2471
+ maxLength: 255
2472
+ },
2473
+ strategy: {
2474
+ type: "string",
2475
+ maxLength: 16
2476
+ },
2477
+ channel_id: {
2478
+ ...CHANNEL_ID,
2479
+ references: {
2480
+ model: "channels",
2481
+ onDelete: "none"
2482
+ }
2483
+ },
2484
+ channel_key: {
2485
+ type: "string",
2486
+ maxLength: 1400,
2487
+ ascii: true
2488
+ },
2489
+ platform: {
2490
+ type: "string",
2491
+ maxLength: 16
2492
+ },
2493
+ fingerprint_hash: {
2494
+ type: "string",
2495
+ maxLength: 255,
2496
+ required: false
2497
+ },
2498
+ generation: { type: "integer" },
2499
+ payload: { type: "string" },
2500
+ catalog_hash: {
2501
+ type: "string",
2502
+ maxLength: 71
2503
+ },
2504
+ byte_size: { type: "integer" },
2505
+ is_tombstone: { type: "boolean" },
2506
+ updated_at_ms: { type: "integer" }
2507
+ }, {
2508
+ key: ["scope_key"],
2509
+ indexes: { all: {
2510
+ eq: [],
2511
+ sort: ["scope_key"]
2512
+ } }
2513
+ });
2514
+ var channels = defineTable({
2515
+ id: CHANNEL_ID,
2516
+ name: {
2517
+ type: "string",
2518
+ maxLength: 255,
2519
+ unique: true
2520
+ }
2521
+ }, {
2522
+ key: ["id"],
2523
+ indexes: { all: {
2524
+ eq: [],
2525
+ sort: ["name"]
2526
+ } }
2527
+ });
2528
+ var nativeBuilds = defineTable({
2529
+ id: {
2530
+ type: "string",
2531
+ maxLength: 36
2532
+ },
2533
+ scope_key: {
2534
+ type: "string",
2535
+ maxLength: 2048,
2536
+ ascii: true
2537
+ },
2538
+ platform: {
2539
+ type: "string",
2540
+ maxLength: 16
2541
+ },
2542
+ channel_id: {
2543
+ ...CHANNEL_ID,
2544
+ references: {
2545
+ model: "channels",
2546
+ onDelete: "none"
2547
+ }
2548
+ },
2549
+ fingerprint_hash: {
2550
+ type: "string",
2551
+ maxLength: 255,
2552
+ required: false
2553
+ },
2554
+ app_version: {
2555
+ type: "string",
2556
+ required: false
2557
+ },
2558
+ min_bundle_id: {
2559
+ type: "string",
2560
+ maxLength: 36,
2561
+ unique: true,
2562
+ references: {
2563
+ model: "bundles",
2564
+ onDelete: "none"
2565
+ }
2566
+ },
2567
+ artifact_kind: {
2568
+ type: "string",
2569
+ maxLength: 16
2570
+ },
2571
+ artifact_storage_uri: { type: "string" },
2572
+ byte_size: { type: "integer" },
2573
+ git_commit_hash: {
2574
+ type: "string",
2575
+ maxLength: 255,
2576
+ required: false
2577
+ },
2578
+ created_at_ms: { type: "integer" },
2579
+ build_key: {
2580
+ type: "string",
2581
+ maxLength: 69,
2582
+ ascii: true,
2583
+ required: false
2584
+ },
2585
+ build_number: {
2586
+ type: "string",
2587
+ maxLength: 64,
2588
+ required: false
2589
+ },
2590
+ submitted_to: {
2591
+ type: "string",
2592
+ maxLength: 16,
2593
+ required: false
2594
+ },
2595
+ submitted_at_ms: {
2596
+ type: "integer",
2597
+ required: false
2598
+ },
2599
+ submitted_version: {
2600
+ type: "string",
2601
+ maxLength: 255,
2602
+ required: false
2603
+ }
2604
+ }, {
2605
+ key: ["id"],
2606
+ indexes: {
2607
+ byScope: {
2608
+ eq: ["scope_key"],
2609
+ sort: ["id"]
2477
2610
  },
2478
- fingerprint_hash: {
2479
- type: "string",
2480
- maxLength: 255,
2481
- required: false
2611
+ byBuildKey: {
2612
+ eq: ["scope_key", "build_key"],
2613
+ sort: ["id"]
2482
2614
  },
2483
- generation: { type: "integer" },
2484
- payload: { type: "string" },
2485
- catalog_hash: {
2486
- type: "string",
2487
- maxLength: 71
2615
+ byChannel: {
2616
+ eq: ["channel_id"],
2617
+ sort: ["id"],
2618
+ root: { model: "channels" }
2488
2619
  },
2489
- byte_size: { type: "integer" },
2490
- is_tombstone: { type: "boolean" },
2491
- updated_at_ms: { type: "integer" }
2492
- }, {
2493
- key: ["scope_key"],
2494
- indexes: { all: {
2620
+ all: {
2495
2621
  eq: [],
2496
- sort: ["scope_key"]
2497
- } }
2498
- }),
2499
- channels: defineTable({
2500
- id: CHANNEL_ID,
2501
- name: {
2502
- type: "string",
2503
- maxLength: 255,
2504
- unique: true
2622
+ sort: ["id"]
2505
2623
  }
2506
- }, {
2507
- key: ["id"],
2508
- indexes: { all: {
2509
- eq: [],
2510
- sort: ["name"]
2511
- } }
2512
- }),
2624
+ }
2625
+ });
2626
+ var coreSchema = {
2627
+ bundles,
2628
+ bundle_patches: bundlePatches,
2629
+ releases,
2630
+ release_catalogs: releaseCatalogs,
2631
+ channels,
2513
2632
  bundle_totals: defineAggregate({ platform_key: {
2514
2633
  type: "string",
2515
2634
  maxLength: 16
@@ -2520,7 +2639,8 @@ var coreSchema = {
2520
2639
  eq: ["platform_key"],
2521
2640
  sort: []
2522
2641
  } }
2523
- })
2642
+ }),
2643
+ native_builds: nativeBuilds
2524
2644
  };
2525
2645
  var coreModule = {
2526
2646
  id: "core",
@@ -2564,7 +2684,7 @@ var fnv1a = /* @__PURE__ */ __name((text) => {
2564
2684
  return hash2;
2565
2685
  }, "fnv1a");
2566
2686
  var cursorScope = /* @__PURE__ */ __name((parts) => fnv1a(JSON.stringify(parts)).toString(36), "cursorScope");
2567
- var encodeCursor = /* @__PURE__ */ __name((scope, after) => toBase64Url(JSON.stringify([scope, after])), "encodeCursor");
2687
+ var encodeCursor = /* @__PURE__ */ __name((scope, after2) => toBase64Url(JSON.stringify([scope, after2])), "encodeCursor");
2568
2688
  var decodeCursor = /* @__PURE__ */ __name((scope, cursor) => {
2569
2689
  let decoded;
2570
2690
  try {
@@ -2572,10 +2692,10 @@ var decodeCursor = /* @__PURE__ */ __name((scope, cursor) => {
2572
2692
  } catch {
2573
2693
  decoded = void 0;
2574
2694
  }
2575
- const [from, after] = Array.isArray(decoded) ? decoded : [];
2576
- if (typeof from !== "string" || !Array.isArray(after) || !after.every(isKeyValue)) throw new DatabaseCursorError("The cursor is malformed.");
2695
+ const [from, after2] = Array.isArray(decoded) ? decoded : [];
2696
+ if (typeof from !== "string" || !Array.isArray(after2) || !after2.every(isKeyValue)) throw new DatabaseCursorError("The cursor is malformed.");
2577
2697
  if (from !== scope) throw new DatabaseCursorError("The cursor belongs to a read with another model, index, filter, order, or range.");
2578
- return after;
2698
+ return after2;
2579
2699
  }, "decodeCursor");
2580
2700
 
2581
2701
  // ../plugin-core/dist/engine/database/engineAggregates.mjs
@@ -3499,14 +3619,14 @@ var createEngineMigrator = /* @__PURE__ */ __name((options) => {
3499
3619
  return {
3500
3620
  getVersion,
3501
3621
  getNameVariants: /* @__PURE__ */ __name(async () => void 0, "getNameVariants"),
3502
- next: /* @__PURE__ */ __name(async () => await getVersion() === "1.0.0" ? void 0 : { version: HOT_UPDATER_SCHEMA_VERSION }, "next"),
3622
+ next: /* @__PURE__ */ __name(async () => await getVersion() === "1.2.0" ? void 0 : { version: HOT_UPDATER_SCHEMA_VERSION }, "next"),
3503
3623
  previous: /* @__PURE__ */ __name(async () => void 0, "previous"),
3504
3624
  up: makeResult,
3505
3625
  down: /* @__PURE__ */ __name(async () => {
3506
3626
  throw new Error("No previous schema to migrate to.");
3507
3627
  }, "down"),
3508
3628
  migrateTo: /* @__PURE__ */ __name(async (version2, migrate) => {
3509
- if (version2 !== "1.0.0") throw new Error(`Invalid version ${version2}`);
3629
+ if (version2 !== "1.2.0") throw new Error(`Invalid version ${version2}`);
3510
3630
  return makeResult(migrate);
3511
3631
  }, "migrateTo"),
3512
3632
  migrateToLatest: makeResult
@@ -3637,9 +3757,9 @@ var createTransactions = /* @__PURE__ */ __name(({ adapter, schema, reads, retry
3637
3757
  else if (pending.type !== "delete") for (const [column, delta] of Object.entries(by)) pending.row[column] = Number(pending.row[column] ?? pending.read?.[column] ?? 0) + delta;
3638
3758
  else if (Object.values(by).some((delta) => delta > 0)) throw new DatabaseConstraintError("not_found", model.table.name);
3639
3759
  }, "bump");
3640
- const touchParents = /* @__PURE__ */ __name((model, before, after) => {
3760
+ const touchParents = /* @__PURE__ */ __name((model, before, after2) => {
3641
3761
  for (const { field, counter, target } of model.references) {
3642
- const [from, to] = [before?.[field], after?.[field]];
3762
+ const [from, to] = [before?.[field], after2?.[field]];
3643
3763
  if (counter === void 0 || from === to) continue;
3644
3764
  if (isKeyed(from)) bump(modelOf(target), [from], { [counter]: -1 });
3645
3765
  if (isKeyed(to)) bump(modelOf(target), [to], { [counter]: 1 });
@@ -3647,7 +3767,7 @@ var createTransactions = /* @__PURE__ */ __name(({ adapter, schema, reads, retry
3647
3767
  for (const [index, root] of model.roots) {
3648
3768
  const parent = modelOf(root);
3649
3769
  const { eq } = model.table.indexes.find(({ name }) => name === index);
3650
- for (const row of [before, after]) {
3770
+ for (const row of [before, after2]) {
3651
3771
  const key = eq.slice(0, parent.table.key.length).map((field) => row?.[field]);
3652
3772
  if (row && key.every(isKeyed)) bump(parent, key, {});
3653
3773
  }
@@ -4207,9 +4327,9 @@ var createDatabaseEngine = /* @__PURE__ */ __name(({ batching, now = Date.now, m
4207
4327
  // ../plugin-core/dist/engine/createEngine.mjs
4208
4328
  var pluginTablesFix = /* @__PURE__ */ __name((database) => {
4209
4329
  const { createMigrator, generateSchema } = database;
4210
- const managed = "A managed server runs only its provider's plugins, and rerunning `hot-updater init --provider <provider>` applies the provider's migrations.";
4330
+ const doctor = "Run `npx hot-updater doctor` in the project whose hot-updater.config.ts names this database: it names the command that migrates it.";
4211
4331
  const serverProject = "Run `hot-updater db` commands in the server's project: they load the server file named on the command line, or src/hotUpdater.* or src/db.*, and don't read hot-updater.config.ts.";
4212
- return createMigrator !== void 0 ? `Run \`hot-updater db migrate\`. ${managed} ${serverProject}` : generateSchema !== void 0 ? `Run \`hot-updater db generate\` and apply the file it writes. ${managed} ${serverProject}` : managed;
4332
+ return createMigrator !== void 0 ? `${doctor} In a server project, run \`hot-updater db migrate\`. ${serverProject}` : generateSchema !== void 0 ? `${doctor} On a database \`hot-updater init\` created, that is init again with its saved inputs; for your own server, run \`hot-updater db generate\` and apply the file it writes. ${serverProject}` : `${doctor} On a database \`hot-updater init\` created, that is init again with its saved inputs.`;
4213
4333
  }, "pluginTablesFix");
4214
4334
  var createEngine = /* @__PURE__ */ __name((database, { plugins = [], now = Date.now } = {}) => {
4215
4335
  const modules = /* @__PURE__ */ new Map();
@@ -4512,7 +4632,7 @@ var createSqlAdapter = /* @__PURE__ */ __name(({ executor, tablePrefix = "", max
4512
4632
  }, "createSqlAdapter");
4513
4633
 
4514
4634
  // ../../packages/server/dist/package.mjs
4515
- var version = "1.1.3";
4635
+ var version = "1.2.0-rc.0";
4516
4636
 
4517
4637
  // ../../packages/server/dist/version.mjs
4518
4638
  var HOT_UPDATER_SERVER_VERSION = version;
@@ -5364,6 +5484,7 @@ var drain = /* @__PURE__ */ __name(async (read) => {
5364
5484
  } while (cursor !== void 0);
5365
5485
  return rows;
5366
5486
  }, "drain");
5487
+ var after = /* @__PURE__ */ __name((input) => input.after === void 0 ? {} : { range: (input.order ?? "asc") === "asc" ? { gt: input.after } : { lt: input.after } }, "after");
5367
5488
  var createCoreReads = /* @__PURE__ */ __name((db, storage) => {
5368
5489
  const detail = /* @__PURE__ */ __name(async (row) => {
5369
5490
  const count = row._refs_bundle_patches_bundle_id ?? 0;
@@ -5386,7 +5507,6 @@ var createCoreReads = /* @__PURE__ */ __name((db, storage) => {
5386
5507
  childCount: row._refs_bundle_patches_base_bundle_id ?? 0
5387
5508
  };
5388
5509
  }, "detail");
5389
- const after = /* @__PURE__ */ __name((input) => input.after === void 0 ? {} : { range: (input.order ?? "asc") === "asc" ? { gt: input.after } : { lt: input.after } }, "after");
5390
5510
  return {
5391
5511
  /** The update check: one point read of its scope's catalog. */
5392
5512
  async getReleaseCatalog(input) {
@@ -5576,9 +5696,9 @@ var insertBundle = /* @__PURE__ */ __name((tx, row) => {
5576
5696
  tx.create("bundles", row);
5577
5697
  countBundle(tx, row.platform, 1);
5578
5698
  }, "insertBundle");
5579
- var updateBundle = /* @__PURE__ */ __name((tx, current, set) => {
5699
+ var updateBundle = /* @__PURE__ */ __name((tx, current, set, { counted = true } = {}) => {
5580
5700
  tx.update("bundles", current, set);
5581
- if (set.platform !== void 0 && set.platform !== current.platform) {
5701
+ if (counted && set.platform !== void 0 && set.platform !== current.platform) {
5582
5702
  countBundle(tx, current.platform, -1);
5583
5703
  countBundle(tx, set.platform, 1);
5584
5704
  }
@@ -5643,10 +5763,226 @@ var deleteChannel = /* @__PURE__ */ __name((db, id) => db.transaction(async (tx)
5643
5763
  deleted: false,
5644
5764
  reason: "not_empty"
5645
5765
  };
5766
+ const { rows: builds } = await tx.findMany("native_builds", {
5767
+ index: "byChannel",
5768
+ where: { channel_id: id },
5769
+ limit: 1
5770
+ });
5771
+ if (builds.length > 0) return {
5772
+ deleted: false,
5773
+ reason: "not_empty"
5774
+ };
5646
5775
  await tx.delete("channels", row);
5647
5776
  return { deleted: true };
5648
5777
  }), "deleteChannel");
5649
5778
 
5779
+ // ../../packages/server/dist/core/nativeBuilds.mjs
5780
+ var ARTIFACT_KINDS = {
5781
+ android: ["apk", "aab"],
5782
+ ios: ["ipa", "app"]
5783
+ };
5784
+ var SCOPE_KEY_MAX_LENGTH = 2048;
5785
+ var GIT_COMMIT_HASH_MAX_LENGTH = 255;
5786
+ var BUILD_KEY = /^nbk1:[0-9a-f]{64}$/;
5787
+ var NativeBuildInputError = class extends TypeError {
5788
+ static {
5789
+ __name(this, "NativeBuildInputError");
5790
+ }
5791
+ name = "NativeBuildInputError";
5792
+ };
5793
+ var pick2 = /* @__PURE__ */ __name((row) => ({
5794
+ id: row.id,
5795
+ scope_key: row.scope_key,
5796
+ platform: row.platform,
5797
+ channel_id: row.channel_id,
5798
+ fingerprint_hash: row.fingerprint_hash ?? null,
5799
+ app_version: row.app_version ?? null,
5800
+ min_bundle_id: row.min_bundle_id,
5801
+ artifact_kind: row.artifact_kind,
5802
+ artifact_storage_uri: row.artifact_storage_uri,
5803
+ byte_size: row.byte_size,
5804
+ git_commit_hash: row.git_commit_hash ?? null,
5805
+ created_at_ms: row.created_at_ms,
5806
+ build_key: row.build_key ?? null,
5807
+ build_number: row.build_number ?? null,
5808
+ submitted_to: row.submitted_to ?? null,
5809
+ submitted_at_ms: row.submitted_at_ms ?? null,
5810
+ submitted_version: row.submitted_version ?? null
5811
+ }), "pick");
5812
+ var SUBMISSION_DESTINATIONS = {
5813
+ android: "play-internal",
5814
+ ios: "testflight"
5815
+ };
5816
+ var BUILD_NUMBER_MAX_LENGTH = 64;
5817
+ var SUBMITTED_VERSION_MAX_LENGTH = 255;
5818
+ var nativeBuildScopeKey = /* @__PURE__ */ __name((scope) => {
5819
+ let scopeKey;
5820
+ try {
5821
+ const channelKey = encodeChannelKey(scope.channel);
5822
+ scopeKey = scope.fingerprintHash === null ? `${createReleaseCatalogScopeKey({
5823
+ channelKey,
5824
+ platform: scope.platform,
5825
+ strategy: "APP_VERSION"
5826
+ })}:${encodeURIComponent(scope.appVersion)}` : createReleaseCatalogScopeKey({
5827
+ channelKey,
5828
+ fingerprintHash: scope.fingerprintHash,
5829
+ platform: scope.platform,
5830
+ strategy: "FINGERPRINT"
5831
+ });
5832
+ } catch (error) {
5833
+ throw new NativeBuildInputError(`Invalid native build scope: ${error instanceof Error ? error.message : String(error)}.`);
5834
+ }
5835
+ if (scopeKey.length > SCOPE_KEY_MAX_LENGTH) throw new NativeBuildInputError(`Invalid native build scope: its key is longer than ${SCOPE_KEY_MAX_LENGTH} characters; shorten the channel or app version.`);
5836
+ return scopeKey;
5837
+ }, "nativeBuildScopeKey");
5838
+ var isBuiltInBundle = /* @__PURE__ */ __name(async (db, bundleId) => await db.findOne("native_builds", { min_bundle_id: bundleId }) !== null, "isBuiltInBundle");
5839
+ var problemOf = /* @__PURE__ */ __name(({ id, minBundleId, platform, channel, fingerprintHash, appVersion, gitCommitHash, buildKey, buildNumber, artifact }) => {
5840
+ if (!isUUIDv7(id)) return "id must be a UUIDv7";
5841
+ if (!isUUIDv7(minBundleId)) return "minBundleId must be the UUIDv7 a release build compiles in";
5842
+ if (platform !== "ios" && platform !== "android") return "platform must be ios or android";
5843
+ if (channel.trim().length === 0) return "channel is required";
5844
+ if (gitCommitHash !== null && gitCommitHash.length > GIT_COMMIT_HASH_MAX_LENGTH) return `gitCommitHash must be at most ${GIT_COMMIT_HASH_MAX_LENGTH} characters`;
5845
+ if (fingerprintHash === null === (appVersion === null)) return "exactly one of fingerprintHash and appVersion is required";
5846
+ if (buildKey !== null && !BUILD_KEY.test(buildKey)) return "buildKey must be nbk1: and a SHA-256, or null";
5847
+ if (buildNumber !== void 0 && buildNumber !== null && (buildNumber.trim().length === 0 || buildNumber.length > BUILD_NUMBER_MAX_LENGTH)) return `buildNumber must be the artifact's build number, up to ${BUILD_NUMBER_MAX_LENGTH} characters, or null`;
5848
+ const kinds = ARTIFACT_KINDS[platform];
5849
+ if (!kinds.includes(artifact.kind)) return `artifact.kind of an ${platform} build must be ${kinds.join(" or ")}`;
5850
+ if (!Number.isSafeInteger(artifact.byteSize) || artifact.byteSize < 0) return "artifact.byteSize must be a byte count";
5851
+ return null;
5852
+ }, "problemOf");
5853
+ var createNativeBuildApi = /* @__PURE__ */ __name((db, { now = Date.now } = {}) => ({
5854
+ async registerNativeBuild(registration) {
5855
+ const problem = problemOf(registration);
5856
+ if (problem !== null) throw new NativeBuildInputError(`Invalid native build: ${problem}.`);
5857
+ const scopeKey = nativeBuildScopeKey(registration);
5858
+ const channelId = `channel:${encodeChannelKey(registration.channel)}`;
5859
+ const { embeddedBundle } = registration;
5860
+ const row = {
5861
+ id: registration.id,
5862
+ scope_key: scopeKey,
5863
+ platform: registration.platform,
5864
+ channel_id: channelId,
5865
+ fingerprint_hash: registration.fingerprintHash,
5866
+ app_version: registration.appVersion,
5867
+ min_bundle_id: registration.minBundleId,
5868
+ artifact_kind: registration.artifact.kind,
5869
+ artifact_storage_uri: registration.artifact.storageUri,
5870
+ byte_size: registration.artifact.byteSize,
5871
+ git_commit_hash: registration.gitCommitHash,
5872
+ created_at_ms: now(),
5873
+ build_key: registration.buildKey,
5874
+ build_number: registration.buildNumber ?? null,
5875
+ submitted_to: null,
5876
+ submitted_at_ms: null,
5877
+ submitted_version: null
5878
+ };
5879
+ for (let attempt = 1; ; attempt += 1) {
5880
+ await insertChannel(db, {
5881
+ id: channelId,
5882
+ name: registration.channel
5883
+ });
5884
+ try {
5885
+ await db.transaction(async (tx) => {
5886
+ tx.create("bundles", {
5887
+ id: registration.minBundleId,
5888
+ platform: registration.platform,
5889
+ git_commit_hash: registration.gitCommitHash,
5890
+ metadata: embeddedBundle.metadata ?? {},
5891
+ manifest_storage_uri: embeddedBundle.manifestStorageUri,
5892
+ manifest_file_hash: embeddedBundle.manifestFileHash,
5893
+ asset_base_storage_uri: embeddedBundle.assetBaseStorageUri
5894
+ });
5895
+ tx.create("native_builds", row);
5896
+ });
5897
+ break;
5898
+ } catch (error) {
5899
+ if (!(error instanceof DatabaseConstraintError)) throw error;
5900
+ if (error.reason === "exists" || error.reason === "unique") throw new DatabaseConstraintError(error.reason, "native_builds");
5901
+ if (attempt < 3 && error.reason === "not_found" && error.model === "channels") continue;
5902
+ throw error;
5903
+ }
5904
+ }
5905
+ return row;
5906
+ },
5907
+ async getNativeBuild(id) {
5908
+ const row = await db.findOne("native_builds", { id });
5909
+ return row === null ? null : pick2(row);
5910
+ },
5911
+ async findNativeBuild(scope, options) {
5912
+ const scopeKey = nativeBuildScopeKey(scope);
5913
+ const buildKey = options?.buildKey;
5914
+ if (buildKey !== void 0 && !BUILD_KEY.test(buildKey)) throw new NativeBuildInputError("Invalid native build key: it must be nbk1: and a SHA-256.");
5915
+ const { rows } = buildKey === void 0 ? await db.findMany("native_builds", {
5916
+ index: "byScope",
5917
+ where: { scope_key: scopeKey },
5918
+ order: "desc",
5919
+ limit: 1
5920
+ }) : await db.findMany("native_builds", {
5921
+ index: "byBuildKey",
5922
+ where: {
5923
+ scope_key: scopeKey,
5924
+ build_key: buildKey
5925
+ },
5926
+ order: "desc",
5927
+ limit: 1
5928
+ });
5929
+ return rows[0] === void 0 ? null : pick2(rows[0]);
5930
+ },
5931
+ async recordNativeBuildSubmission(id, { destination, version: version2 }) {
5932
+ if (version2.trim().length === 0 || version2.length > SUBMITTED_VERSION_MAX_LENGTH) throw new NativeBuildInputError(`Invalid native build submission: version must be the store's label of the build, up to ${SUBMITTED_VERSION_MAX_LENGTH} characters.`);
5933
+ return db.transaction(async (tx) => {
5934
+ const row = await tx.findOne("native_builds", { id });
5935
+ if (row === null) return null;
5936
+ const build = pick2(row);
5937
+ if (SUBMISSION_DESTINATIONS[build.platform] !== destination) throw new NativeBuildInputError(`Invalid native build submission: an ${build.platform} build is submitted to ${SUBMISSION_DESTINATIONS[build.platform]}, not ${destination}.`);
5938
+ const set = {
5939
+ submitted_to: destination,
5940
+ submitted_at_ms: now(),
5941
+ submitted_version: version2
5942
+ };
5943
+ tx.update("native_builds", row, set);
5944
+ return {
5945
+ ...build,
5946
+ ...set
5947
+ };
5948
+ });
5949
+ },
5950
+ async deleteNativeBuild(id) {
5951
+ return db.transaction(async (tx) => {
5952
+ const row = await tx.findOne("native_builds", { id });
5953
+ if (row === null) return {
5954
+ deleted: false,
5955
+ reason: "not_found"
5956
+ };
5957
+ await tx.delete("native_builds", row);
5958
+ const bundle = await tx.findOne("bundles", { id: row.min_bundle_id });
5959
+ if (bundle !== null) await tx.delete("bundles", bundle);
5960
+ return {
5961
+ deleted: true,
5962
+ nativeBuild: pick2(row)
5963
+ };
5964
+ });
5965
+ },
5966
+ async listNativeBuilds(input) {
5967
+ const page2 = {
5968
+ order: input.order ?? "asc",
5969
+ limit: input.limit
5970
+ };
5971
+ const { rows } = input.channelId === void 0 ? await db.findMany("native_builds", {
5972
+ index: "all",
5973
+ where: {},
5974
+ ...page2,
5975
+ ...after(input)
5976
+ }) : await db.findMany("native_builds", {
5977
+ index: "byChannel",
5978
+ where: { channel_id: input.channelId },
5979
+ ...page2,
5980
+ ...after(input)
5981
+ });
5982
+ return rows.map(pick2);
5983
+ }
5984
+ }), "createNativeBuildApi");
5985
+
5650
5986
  // ../../packages/server/dist/core/releases.mjs
5651
5987
  var PAGE3 = 500;
5652
5988
  var fail2 = /* @__PURE__ */ __name((code, message) => {
@@ -5929,13 +6265,14 @@ var createCoreOperations = /* @__PURE__ */ __name((db, { now = Date.now } = {})
5929
6265
  }
5930
6266
  }, "withChannels");
5931
6267
  const deployOnce = /* @__PURE__ */ __name(async (deployments) => {
5932
- const channels = /* @__PURE__ */ new Map();
5933
- for (const { release } of deployments) if (!channels.has(release.channel)) channels.set(release.channel, await ensureChannel(release.channel));
6268
+ const channels2 = /* @__PURE__ */ new Map();
6269
+ for (const { release } of deployments) if (!channels2.has(release.channel)) channels2.set(release.channel, await ensureChannel(release.channel));
5934
6270
  const platforms = /* @__PURE__ */ new Map();
5935
6271
  for (const deployment of deployments) {
5936
6272
  if (!("bundleId" in deployment) || platforms.has(deployment.bundleId)) continue;
5937
6273
  const row = await db.findOne("bundles", { id: deployment.bundleId });
5938
6274
  if (row === null) throw new DatabaseBundleNotFoundError(deployment.bundleId);
6275
+ if (await isBuiltInBundle(db, deployment.bundleId)) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", `Bundle "${deployment.bundleId}" is a native build's built-in bundle, which no release publishes.`);
5939
6276
  platforms.set(deployment.bundleId, toBundleRow(row).platform);
5940
6277
  }
5941
6278
  const updatedAtMs = now();
@@ -5944,7 +6281,7 @@ var createCoreOperations = /* @__PURE__ */ __name((db, { now = Date.now } = {})
5944
6281
  const stored2 = "bundleId" in deployment;
5945
6282
  const bundleId = stored2 ? deployment.bundleId : deployment.bundle.id;
5946
6283
  const platform = stored2 ? platforms.get(deployment.bundleId) : deployment.bundle.platform;
5947
- const scope = scopeOf(channels.get(policy.channel), platform, policy.fingerprintHash);
6284
+ const scope = scopeOf(channels2.get(policy.channel), platform, policy.fingerprintHash);
5948
6285
  return {
5949
6286
  scope,
5950
6287
  ...stored2 ? {} : { bundle: {
@@ -6131,7 +6468,8 @@ var createCoreOperations = /* @__PURE__ */ __name((db, { now = Date.now } = {})
6131
6468
  id
6132
6469
  };
6133
6470
  const { id: _id, ...set } = bundleToRow(next);
6134
- updateBundle(tx, current, set);
6471
+ const counted = set.platform === current.platform || !await isBuiltInBundle(tx, id);
6472
+ updateBundle(tx, current, set, { counted });
6135
6473
  if (patches !== void 0) await replaceBundlePatches(tx, current, bundleToPatchRows({
6136
6474
  ...next,
6137
6475
  patches
@@ -6142,7 +6480,9 @@ var createCoreOperations = /* @__PURE__ */ __name((db, { now = Date.now } = {})
6142
6480
  await db.transaction(async (tx) => {
6143
6481
  for (const id of new Set(ids)) {
6144
6482
  const current = await tx.findOne("bundles", { id });
6145
- if (current !== null) await deleteBundle(tx, current);
6483
+ if (current === null) continue;
6484
+ if (await isBuiltInBundle(tx, id)) throw new DatabaseRowReferencedError();
6485
+ await deleteBundle(tx, current);
6146
6486
  }
6147
6487
  });
6148
6488
  } catch (error) {
@@ -6188,6 +6528,7 @@ var createCoreApi = /* @__PURE__ */ __name((db, storage, options = {}) => {
6188
6528
  return {
6189
6529
  ...reads,
6190
6530
  ...createCoreOperations(purgingCachedRoutes(db, options.onCachedRoutesChange), options),
6531
+ ...createNativeBuildApi(db, options),
6191
6532
  /** One read, which checks the schema settings first. */
6192
6533
  ready: /* @__PURE__ */ __name(async () => {
6193
6534
  await reads.listReleaseCatalogs({ limit: 1 });
@@ -6383,6 +6724,10 @@ var answer = /* @__PURE__ */ __name(async (operation) => {
6383
6724
  code: "REFERENCED",
6384
6725
  error: error.message
6385
6726
  }, 409);
6727
+ if (error instanceof DatabaseConstraintError && error.model === "native_builds") return json({
6728
+ code: "NATIVE_BUILD_EXISTS",
6729
+ error: "A native build with this ID or built-in bundle ID is already registered."
6730
+ }, 409);
6386
6731
  throw error;
6387
6732
  }
6388
6733
  }, "answer");
@@ -6478,6 +6823,40 @@ var bundleUpdateOf = /* @__PURE__ */ __name((value, bundleId) => {
6478
6823
  if (id !== void 0 && id !== bundleId) throw new HandlerBadRequestError("Bundle id mismatch");
6479
6824
  return update;
6480
6825
  }, "bundleUpdateOf");
6826
+ var NATIVE_BUILD_ARTIFACT_KINDS = /* @__PURE__ */ new Set([
6827
+ "apk",
6828
+ "aab",
6829
+ "ipa",
6830
+ "app"
6831
+ ]);
6832
+ var nativeBuildScopeOf = /* @__PURE__ */ __name((url) => {
6833
+ const params = url.searchParams;
6834
+ const platform = platformOf(url);
6835
+ const channel = params.get("channel");
6836
+ const fingerprintHash = params.get("fingerprintHash");
6837
+ const appVersion = params.get("appVersion");
6838
+ if (platform === void 0 || !isText(channel) || fingerprintHash === null === (appVersion === null) || fingerprintHash === "" || appVersion === "") throw new HandlerBadRequestError("A native build lookup needs platform, channel, and fingerprintHash or appVersion.");
6839
+ return fingerprintHash !== null ? {
6840
+ platform,
6841
+ channel,
6842
+ fingerprintHash,
6843
+ appVersion: null
6844
+ } : {
6845
+ platform,
6846
+ channel,
6847
+ fingerprintHash: null,
6848
+ appVersion
6849
+ };
6850
+ }, "nativeBuildScopeOf");
6851
+ var refusingInvalid = /* @__PURE__ */ __name(async (operation) => {
6852
+ try {
6853
+ return await operation();
6854
+ } catch (error) {
6855
+ if (error instanceof NativeBuildInputError) throw new HandlerBadRequestError(error.message);
6856
+ throw error;
6857
+ }
6858
+ }, "refusingInvalid");
6859
+ var isNativeBuildRegistration = /* @__PURE__ */ __name((value) => isRecord3(value) && isText(value.id) && isText(value.minBundleId) && typeof value.platform === "string" && isPlatform(value.platform) && isText(value.channel) && isTextOrNull(value.fingerprintHash) && isTextOrNull(value.appVersion) && value.fingerprintHash === null !== (value.appVersion === null) && isTextOrNull(value.gitCommitHash) && isTextOrNull(value.buildKey) && (value.buildNumber === void 0 || isTextOrNull(value.buildNumber)) && isRecord3(value.artifact) && typeof value.artifact.kind === "string" && NATIVE_BUILD_ARTIFACT_KINDS.has(value.artifact.kind) && isText(value.artifact.storageUri) && Number.isSafeInteger(value.artifact.byteSize) && value.artifact.byteSize >= 0 && isRecord3(value.embeddedBundle) && isText(value.embeddedBundle.manifestStorageUri) && isText(value.embeddedBundle.manifestFileHash) && isText(value.embeddedBundle.assetBaseStorageUri) && (value.embeddedBundle.metadata === void 0 || isRecord3(value.embeddedBundle.metadata)), "isNativeBuildRegistration");
6481
6860
  var scopeKeyOf = /* @__PURE__ */ __name((params) => decodeMaybe(requireRouteParam(params, "scopeKey")) ?? "", "scopeKeyOf");
6482
6861
  var createAdminRouteHandlers = /* @__PURE__ */ __name(() => ({
6483
6862
  listBundles: onCore(async (core, request) => {
@@ -6596,6 +6975,37 @@ var createAdminRouteHandlers = /* @__PURE__ */ __name(() => ({
6596
6975
  }) });
6597
6976
  }),
6598
6977
  preflightReleaseCatalogRebuild: onCore(async (core, _request, params) => json({ data: await core.preflightReleaseCatalogRebuild(scopeKeyOf(params)) })),
6978
+ registerNativeBuild: onCore(async (core, request) => {
6979
+ const input = await body(request);
6980
+ if (!isNativeBuildRegistration(input)) throw new HandlerBadRequestError("Invalid native build");
6981
+ return json({ data: await refusingInvalid(() => core.registerNativeBuild(input)) }, 201);
6982
+ }),
6983
+ findNativeBuild: onCore(async (core, request) => {
6984
+ const url = new URL(request.url);
6985
+ const scope = nativeBuildScopeOf(url);
6986
+ const buildKey = url.searchParams.get("buildKey");
6987
+ return json({ data: await refusingInvalid(() => buildKey === null ? core.findNativeBuild(scope) : core.findNativeBuild(scope, { buildKey })) });
6988
+ }),
6989
+ getNativeBuild: onCore(async (core, _request, params) => json({ data: await core.getNativeBuild(requireRouteParam(params, "id")) })),
6990
+ listNativeBuilds: onCore(async (core, request) => {
6991
+ const url = new URL(request.url);
6992
+ const input = keyset(url);
6993
+ const channelId = url.searchParams.get("channelId");
6994
+ return page(await core.listNativeBuilds({
6995
+ ...input,
6996
+ ...channelId === null ? {} : { channelId }
6997
+ }), input, ({ id }) => id);
6998
+ }),
6999
+ recordNativeBuildSubmission: onCore(async (core, request, params) => {
7000
+ const input = await body(request);
7001
+ if (!isRecord3(input) || input.destination !== "testflight" && input.destination !== "play-internal" || !isText(input.version)) throw new HandlerBadRequestError("A native build submission needs destination (testflight or play-internal) and version.");
7002
+ const submission = {
7003
+ destination: input.destination,
7004
+ version: input.version
7005
+ };
7006
+ return json({ data: await refusingInvalid(() => core.recordNativeBuildSubmission(requireRouteParam(params, "id"), submission)) });
7007
+ }),
7008
+ deleteNativeBuild: onCore(async (core, _request, params) => json({ data: await core.deleteNativeBuild(requireRouteParam(params, "id")) })),
6599
7009
  deleteBundles: onCore(async (core, request) => {
6600
7010
  const { ids } = await body(request);
6601
7011
  if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && id.length > 0)) throw new HandlerBadRequestError("Bundle deletion needs ids");
@@ -6713,6 +7123,36 @@ var ADMIN_ROUTES = [
6713
7123
  method: "GET",
6714
7124
  path: "/base-candidates/:candidateKey",
6715
7125
  handler: "findBaseCandidates"
7126
+ },
7127
+ {
7128
+ method: "POST",
7129
+ path: "/native-builds",
7130
+ handler: "registerNativeBuild"
7131
+ },
7132
+ {
7133
+ method: "GET",
7134
+ path: "/native-builds/latest",
7135
+ handler: "findNativeBuild"
7136
+ },
7137
+ {
7138
+ method: "GET",
7139
+ path: "/native-builds/:id",
7140
+ handler: "getNativeBuild"
7141
+ },
7142
+ {
7143
+ method: "GET",
7144
+ path: "/native-builds",
7145
+ handler: "listNativeBuilds"
7146
+ },
7147
+ {
7148
+ method: "POST",
7149
+ path: "/native-builds/:id/submission",
7150
+ handler: "recordNativeBuildSubmission"
7151
+ },
7152
+ {
7153
+ method: "DELETE",
7154
+ path: "/native-builds/:id",
7155
+ handler: "deleteNativeBuild"
6716
7156
  }
6717
7157
  ];
6718
7158
 
@@ -8621,7 +9061,7 @@ var readUserInstallationCursor = /* @__PURE__ */ __name((value, userId) => {
8621
9061
  };
8622
9062
  }, "readUserInstallationCursor");
8623
9063
  var compareEventNewest = /* @__PURE__ */ __name((left, right) => right.received_at_ms - left.received_at_ms || compareInsightsText(right.id, left.id), "compareEventNewest");
8624
- 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");
9064
+ var isAfterEventCursor = /* @__PURE__ */ __name((row, after2) => row.received_at_ms < after2.receivedAtMs || row.received_at_ms === after2.receivedAtMs && compareInsightsText(row.id, after2.id) < 0, "isAfterEventCursor");
8625
9065
  var assertEventRows = /* @__PURE__ */ __name((rows, input) => {
8626
9066
  if (rows.length > input.limit) throw new Error("Insights database returned too many event rows.");
8627
9067
  for (let index = 0; index < rows.length; index += 1) {
@@ -8698,7 +9138,7 @@ var toInstallationRow = /* @__PURE__ */ __name((row) => ({
8698
9138
  userId: row.user_id
8699
9139
  }), "toInstallationRow");
8700
9140
  var defaultEventStart = /* @__PURE__ */ __name((filter, beforeReceivedAtMs) => filter.kind === "installationMovement" ? 0 : Math.max(0, beforeReceivedAtMs - EVENT_LIST_RANGE_MS), "defaultEventStart");
8701
- var isInsideBounds = /* @__PURE__ */ __name(({ after }, bounds) => after.receivedAtMs >= bounds.sinceMs && after.receivedAtMs < bounds.beforeReceivedAtMs, "isInsideBounds");
9141
+ var isInsideBounds = /* @__PURE__ */ __name(({ after: after2 }, bounds) => after2.receivedAtMs >= bounds.sinceMs && after2.receivedAtMs < bounds.beforeReceivedAtMs, "isInsideBounds");
8702
9142
  var readEventBounds = /* @__PURE__ */ __name((input, cursor, filter) => {
8703
9143
  const beforeReceivedAtMs = cursor?.beforeReceivedAtMs ?? (input.beforeReceivedAtMs === void 0 ? Date.now() : requireTimestamp(input.beforeReceivedAtMs, "event cutoff"));
8704
9144
  if (cursor !== void 0 && input.beforeReceivedAtMs !== void 0 && input.beforeReceivedAtMs !== beforeReceivedAtMs) throw new InsightsBadRequestError("Insights cursor does not match the requested event cutoff.");
@@ -9714,7 +10154,7 @@ var getAppUsage = /* @__PURE__ */ __name(async (db, input, now, retention) => {
9714
10154
  });
9715
10155
  const versions = /* @__PURE__ */ new Map();
9716
10156
  const platforms = /* @__PURE__ */ new Map();
9717
- const bundles = /* @__PURE__ */ new Map();
10157
+ const bundles2 = /* @__PURE__ */ new Map();
9718
10158
  const countBundle2 = /* @__PURE__ */ __name((row, releaseId, builtinBundleId, count) => {
9719
10159
  const key = JSON.stringify([
9720
10160
  row.app_version,
@@ -9722,12 +10162,12 @@ var getAppUsage = /* @__PURE__ */ __name(async (db, input, now, retention) => {
9722
10162
  releaseId,
9723
10163
  builtinBundleId
9724
10164
  ]);
9725
- bundles.set(key, {
10165
+ bundles2.set(key, {
9726
10166
  appVersion: row.app_version,
9727
10167
  platform: row.platform,
9728
10168
  releaseId,
9729
10169
  builtinBundleId,
9730
- installations: (bundles.get(key)?.installations ?? 0) + count
10170
+ installations: (bundles2.get(key)?.installations ?? 0) + count
9731
10171
  });
9732
10172
  }, "countBundle");
9733
10173
  for (const row of distribution) {
@@ -9749,7 +10189,7 @@ var getAppUsage = /* @__PURE__ */ __name(async (db, input, now, retention) => {
9749
10189
  appVersions: sortedVersions.map(({ name }) => name),
9750
10190
  versions: sortedVersions,
9751
10191
  platforms: byInstallations(platforms),
9752
- bundleDistribution: [...bundles.values()].filter(({ installations }) => installations > 0),
10192
+ bundleDistribution: [...bundles2.values()].filter(({ installations }) => installations > 0),
9753
10193
  measuredAtMs: at
9754
10194
  };
9755
10195
  }, "getAppUsage");
@@ -10427,10 +10867,10 @@ var validateRules = /* @__PURE__ */ __name((value, path, issue) => {
10427
10867
  return;
10428
10868
  }
10429
10869
  case "channel": {
10430
- const channels = textList(raw2.channels, `${rulePath}.channels`, issue, (text) => visibleText(text, MAX_RULE_TEXT_LENGTH), `must be a channel name of 1-${MAX_RULE_TEXT_LENGTH} characters.`);
10870
+ const channels2 = textList(raw2.channels, `${rulePath}.channels`, issue, (text) => visibleText(text, MAX_RULE_TEXT_LENGTH), `must be a channel name of 1-${MAX_RULE_TEXT_LENGTH} characters.`);
10431
10871
  rules.push({
10432
10872
  type,
10433
- channels
10873
+ channels: channels2
10434
10874
  });
10435
10875
  return;
10436
10876
  }