@hot-updater/vercel 1.1.4 → 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.
@@ -241,7 +241,7 @@ const bundleMetadataToRow = (metadata) => {
241
241
  return metadata;
242
242
  };
243
243
  //#endregion
244
- //#region ../../packages/protocol/dist/index.mjs
244
+ //#region ../../packages/protocol/dist/uuid-kZvLA0Ly.mjs
245
245
  const LETTER_DASH_NUMBER$2 = "[a-zA-Z0-9-]";
246
246
  const NUMERIC_IDENTIFIER$2 = String.raw`0|[1-9]\d*`;
247
247
  const NUMERIC_IDENTIFIER_LOOSE$2 = String.raw`\d+`;
@@ -348,128 +348,6 @@ function canonicalizeAppVersion(appVersion) {
348
348
  const version = coerce$1(appVersion);
349
349
  return version ? normalizeFull(version) : null;
350
350
  }
351
- const getManifestStorageUri = (bundle) => bundle.manifestStorageUri;
352
- const getManifestFileHash = (bundle) => bundle.manifestFileHash;
353
- const getAssetBaseStorageUri = (bundle) => bundle.assetBaseStorageUri;
354
- const isBundlePatchArtifact = (value) => {
355
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
356
- const candidate = value;
357
- return typeof candidate.baseBundleId === "string" && typeof candidate.baseFileHash === "string" && typeof candidate.patchFileHash === "string" && typeof candidate.patchStorageUri === "string";
358
- };
359
- const readBundlePatchArray = (patches) => {
360
- if (!Array.isArray(patches)) return [];
361
- return patches.filter(isBundlePatchArtifact);
362
- };
363
- const getBundlePatches = (bundle) => {
364
- const patches = readBundlePatchArray(bundle.patches);
365
- const seenBaseBundleIds = /* @__PURE__ */ new Set();
366
- return patches.filter((patch) => {
367
- if (seenBaseBundleIds.has(patch.baseBundleId)) return false;
368
- seenBaseBundleIds.add(patch.baseBundleId);
369
- return true;
370
- });
371
- };
372
- const getBundlePatch = (bundle, baseBundleId) => {
373
- return getBundlePatches(bundle).find((patch) => patch.baseBundleId === baseBundleId) ?? null;
374
- };
375
- const NUMERIC_COHORT_SIZE = 1e3;
376
- const DEFAULT_ROLLOUT_COHORT_COUNT = NUMERIC_COHORT_SIZE;
377
- const CUSTOM_COHORT_PATTERN = /^[a-z0-9-]+$/;
378
- function parseNumericCohortValue(cohort) {
379
- if (!/^\d+$/.test(cohort)) return null;
380
- const parsed = Number.parseInt(cohort, 10);
381
- if (Number.isNaN(parsed) || parsed < 1 || parsed > 1e3) return null;
382
- return parsed;
383
- }
384
- function positiveMod(value, modulus) {
385
- return (value % modulus + modulus) % modulus;
386
- }
387
- function hashString(value) {
388
- let hash = 0;
389
- for (let i = 0; i < value.length; i++) {
390
- const char = value.charCodeAt(i);
391
- hash = (hash << 5) - hash + char;
392
- hash |= 0;
393
- }
394
- return hash;
395
- }
396
- function gcd(a, b) {
397
- let x = Math.abs(a);
398
- let y = Math.abs(b);
399
- while (y !== 0) {
400
- const next = x % y;
401
- x = y;
402
- y = next;
403
- }
404
- return x;
405
- }
406
- function modularInverse(value, modulus) {
407
- let t = 0;
408
- let newT = 1;
409
- let r = modulus;
410
- let newR = positiveMod(value, modulus);
411
- while (newR !== 0) {
412
- const quotient = Math.floor(r / newR);
413
- [t, newT] = [newT, t - quotient * newT];
414
- [r, newR] = [newR, r - quotient * newR];
415
- }
416
- if (r > 1) throw new Error(`No modular inverse for ${value} mod ${modulus}`);
417
- return positiveMod(t, modulus);
418
- }
419
- function getRolloutShuffleParameters(bundleId) {
420
- let multiplier = positiveMod(hashString(`${bundleId}:multiplier`), 997);
421
- if (multiplier === 0) multiplier = 1;
422
- while (gcd(multiplier, NUMERIC_COHORT_SIZE) !== 1) {
423
- multiplier = positiveMod(multiplier + 1, NUMERIC_COHORT_SIZE);
424
- if (multiplier === 0) multiplier = 1;
425
- }
426
- const offset = positiveMod(hashString(`${bundleId}:offset`), NUMERIC_COHORT_SIZE);
427
- return {
428
- multiplier,
429
- offset,
430
- inverseMultiplier: modularInverse(multiplier, NUMERIC_COHORT_SIZE)
431
- };
432
- }
433
- function normalizeRolloutCohortCount(rolloutCohortCount) {
434
- if (rolloutCohortCount === null || rolloutCohortCount === void 0) return DEFAULT_ROLLOUT_COHORT_COUNT;
435
- if (rolloutCohortCount <= 0) return 0;
436
- if (rolloutCohortCount >= 1e3) return NUMERIC_COHORT_SIZE;
437
- return Math.floor(rolloutCohortCount);
438
- }
439
- function normalizeCohortValue(cohort) {
440
- const normalized = cohort.trim().toLowerCase();
441
- const numericCohort = parseNumericCohortValue(normalized);
442
- if (numericCohort !== null) return String(numericCohort);
443
- return normalized;
444
- }
445
- function getNumericCohortValue(cohort) {
446
- return parseNumericCohortValue(normalizeCohortValue(cohort));
447
- }
448
- function isNumericCohort(cohort) {
449
- return getNumericCohortValue(cohort) !== null;
450
- }
451
- function isCustomCohort(cohort) {
452
- const normalized = normalizeCohortValue(cohort);
453
- return normalized.length > 0 && normalized.length <= 64 && !/^\d+$/.test(normalized) && CUSTOM_COHORT_PATTERN.test(normalized);
454
- }
455
- function isValidCohort(cohort) {
456
- const normalized = normalizeCohortValue(cohort);
457
- return isNumericCohort(normalized) || isCustomCohort(normalized);
458
- }
459
- function getNumericCohortRolloutPosition(bundleId, cohortValue) {
460
- if (cohortValue < 1 || cohortValue > 1e3) throw new Error(`Invalid numeric cohort: ${cohortValue}`);
461
- const { offset, inverseMultiplier } = getRolloutShuffleParameters(bundleId);
462
- return positiveMod(inverseMultiplier * (cohortValue - 1 - offset), NUMERIC_COHORT_SIZE);
463
- }
464
- function getRolledOutNumericCohorts(bundleId, rolloutCohortCount) {
465
- const normalizedRolloutCount = normalizeRolloutCohortCount(rolloutCohortCount);
466
- if (normalizedRolloutCount <= 0) return [];
467
- if (normalizedRolloutCount >= 1e3) return Array.from({ length: NUMERIC_COHORT_SIZE }, (_, index) => index + 1);
468
- const { multiplier, offset } = getRolloutShuffleParameters(bundleId);
469
- return Array.from({ length: normalizedRolloutCount }, (_, position) => positiveMod(multiplier * position + offset, NUMERIC_COHORT_SIZE)).map((zeroBasedCohort) => zeroBasedCohort + 1).sort((left, right) => left - right);
470
- }
471
- const RELEASE_CATALOG_FALLBACK_POLICY = "BUILTIN_IF_ACTIVE_INELIGIBLE";
472
- const MAX_COMPILED_CATALOG_BYTES = 262144;
473
351
  const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
474
352
  const MAX_CATALOG_SEGMENT_LENGTH = 255;
475
353
  function assertCatalogSegment(value, name) {
@@ -594,6 +472,130 @@ const NIL_UUID = "00000000-0000-0000-0000-000000000000";
594
472
  const 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}$/;
595
473
  const isUUIDv7 = (value) => typeof value === "string" && UUID_V7_PATTERN.test(value);
596
474
  //#endregion
475
+ //#region ../../packages/protocol/dist/index.mjs
476
+ const getManifestStorageUri = (bundle) => bundle.manifestStorageUri;
477
+ const getManifestFileHash = (bundle) => bundle.manifestFileHash;
478
+ const getAssetBaseStorageUri = (bundle) => bundle.assetBaseStorageUri;
479
+ const isBundlePatchArtifact = (value) => {
480
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
481
+ const candidate = value;
482
+ return typeof candidate.baseBundleId === "string" && typeof candidate.baseFileHash === "string" && typeof candidate.patchFileHash === "string" && typeof candidate.patchStorageUri === "string";
483
+ };
484
+ const readBundlePatchArray = (patches) => {
485
+ if (!Array.isArray(patches)) return [];
486
+ return patches.filter(isBundlePatchArtifact);
487
+ };
488
+ const getBundlePatches = (bundle) => {
489
+ const patches = readBundlePatchArray(bundle.patches);
490
+ const seenBaseBundleIds = /* @__PURE__ */ new Set();
491
+ return patches.filter((patch) => {
492
+ if (seenBaseBundleIds.has(patch.baseBundleId)) return false;
493
+ seenBaseBundleIds.add(patch.baseBundleId);
494
+ return true;
495
+ });
496
+ };
497
+ const getBundlePatch = (bundle, baseBundleId) => {
498
+ return getBundlePatches(bundle).find((patch) => patch.baseBundleId === baseBundleId) ?? null;
499
+ };
500
+ const NUMERIC_COHORT_SIZE = 1e3;
501
+ const DEFAULT_ROLLOUT_COHORT_COUNT = NUMERIC_COHORT_SIZE;
502
+ const CUSTOM_COHORT_PATTERN = /^[a-z0-9-]+$/;
503
+ function parseNumericCohortValue(cohort) {
504
+ if (!/^\d+$/.test(cohort)) return null;
505
+ const parsed = Number.parseInt(cohort, 10);
506
+ if (Number.isNaN(parsed) || parsed < 1 || parsed > 1e3) return null;
507
+ return parsed;
508
+ }
509
+ function positiveMod(value, modulus) {
510
+ return (value % modulus + modulus) % modulus;
511
+ }
512
+ function hashString(value) {
513
+ let hash = 0;
514
+ for (let i = 0; i < value.length; i++) {
515
+ const char = value.charCodeAt(i);
516
+ hash = (hash << 5) - hash + char;
517
+ hash |= 0;
518
+ }
519
+ return hash;
520
+ }
521
+ function gcd(a, b) {
522
+ let x = Math.abs(a);
523
+ let y = Math.abs(b);
524
+ while (y !== 0) {
525
+ const next = x % y;
526
+ x = y;
527
+ y = next;
528
+ }
529
+ return x;
530
+ }
531
+ function modularInverse(value, modulus) {
532
+ let t = 0;
533
+ let newT = 1;
534
+ let r = modulus;
535
+ let newR = positiveMod(value, modulus);
536
+ while (newR !== 0) {
537
+ const quotient = Math.floor(r / newR);
538
+ [t, newT] = [newT, t - quotient * newT];
539
+ [r, newR] = [newR, r - quotient * newR];
540
+ }
541
+ if (r > 1) throw new Error(`No modular inverse for ${value} mod ${modulus}`);
542
+ return positiveMod(t, modulus);
543
+ }
544
+ function getRolloutShuffleParameters(bundleId) {
545
+ let multiplier = positiveMod(hashString(`${bundleId}:multiplier`), 997);
546
+ if (multiplier === 0) multiplier = 1;
547
+ while (gcd(multiplier, NUMERIC_COHORT_SIZE) !== 1) {
548
+ multiplier = positiveMod(multiplier + 1, NUMERIC_COHORT_SIZE);
549
+ if (multiplier === 0) multiplier = 1;
550
+ }
551
+ const offset = positiveMod(hashString(`${bundleId}:offset`), NUMERIC_COHORT_SIZE);
552
+ return {
553
+ multiplier,
554
+ offset,
555
+ inverseMultiplier: modularInverse(multiplier, NUMERIC_COHORT_SIZE)
556
+ };
557
+ }
558
+ function normalizeRolloutCohortCount(rolloutCohortCount) {
559
+ if (rolloutCohortCount === null || rolloutCohortCount === void 0) return DEFAULT_ROLLOUT_COHORT_COUNT;
560
+ if (rolloutCohortCount <= 0) return 0;
561
+ if (rolloutCohortCount >= 1e3) return NUMERIC_COHORT_SIZE;
562
+ return Math.floor(rolloutCohortCount);
563
+ }
564
+ function normalizeCohortValue(cohort) {
565
+ const normalized = cohort.trim().toLowerCase();
566
+ const numericCohort = parseNumericCohortValue(normalized);
567
+ if (numericCohort !== null) return String(numericCohort);
568
+ return normalized;
569
+ }
570
+ function getNumericCohortValue(cohort) {
571
+ return parseNumericCohortValue(normalizeCohortValue(cohort));
572
+ }
573
+ function isNumericCohort(cohort) {
574
+ return getNumericCohortValue(cohort) !== null;
575
+ }
576
+ function isCustomCohort(cohort) {
577
+ const normalized = normalizeCohortValue(cohort);
578
+ return normalized.length > 0 && normalized.length <= 64 && !/^\d+$/.test(normalized) && CUSTOM_COHORT_PATTERN.test(normalized);
579
+ }
580
+ function isValidCohort(cohort) {
581
+ const normalized = normalizeCohortValue(cohort);
582
+ return isNumericCohort(normalized) || isCustomCohort(normalized);
583
+ }
584
+ function getNumericCohortRolloutPosition(bundleId, cohortValue) {
585
+ if (cohortValue < 1 || cohortValue > 1e3) throw new Error(`Invalid numeric cohort: ${cohortValue}`);
586
+ const { offset, inverseMultiplier } = getRolloutShuffleParameters(bundleId);
587
+ return positiveMod(inverseMultiplier * (cohortValue - 1 - offset), NUMERIC_COHORT_SIZE);
588
+ }
589
+ function getRolledOutNumericCohorts(bundleId, rolloutCohortCount) {
590
+ const normalizedRolloutCount = normalizeRolloutCohortCount(rolloutCohortCount);
591
+ if (normalizedRolloutCount <= 0) return [];
592
+ if (normalizedRolloutCount >= 1e3) return Array.from({ length: NUMERIC_COHORT_SIZE }, (_, index) => index + 1);
593
+ const { multiplier, offset } = getRolloutShuffleParameters(bundleId);
594
+ return Array.from({ length: normalizedRolloutCount }, (_, position) => positiveMod(multiplier * position + offset, NUMERIC_COHORT_SIZE)).map((zeroBasedCohort) => zeroBasedCohort + 1).sort((left, right) => left - right);
595
+ }
596
+ const RELEASE_CATALOG_FALLBACK_POLICY = "BUILTIN_IF_ACTIVE_INELIGIBLE";
597
+ const MAX_COMPILED_CATALOG_BYTES = 262144;
598
+ //#endregion
597
599
  //#region ../plugin-core/dist/databaseRows.mjs
598
600
  const releaseRowToRelease = (row) => ({
599
601
  id: row.id,
@@ -1862,10 +1864,11 @@ const SETTINGS_TABLE = {
1862
1864
  const ENGINE_SCHEMA_KEY = "schema.engine";
1863
1865
  const CORE_SCHEMA_KEY = "schema.core";
1864
1866
  const stored = ({ key, expected, found }) => `"${key}" is ${found === null ? "missing" : `"${found}"`}; expected "${expected}"`;
1865
- const settingMessage = (adapterName, { key, expected, found }) => `Hot Updater schema setting "${key}" for ${adapterName} is ${found === null ? "missing" : `"${found}"`}; expected "${expected}". Run \`hot-updater db migrate\`.`;
1867
+ const 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.";
1868
+ const settingMessage = (adapterName, { key, expected, found }) => `Hot Updater schema setting "${key}" for ${adapterName} is ${found === null ? "missing" : `"${found}"`}; expected "${expected}". ${MIGRATE_SCHEMA_FIX}`;
1866
1869
  const pluginOf = ({ key }) => key.startsWith("schema.") && key !== "schema.engine" && key !== CORE_SCHEMA_KEY ? key.slice(7) : void 0;
1867
1870
  const listed = (items) => items.length < 2 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
1868
- const 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.";
1871
+ const settingsMessage = (adapterName, settings) => `Hot Updater schema settings for ${adapterName} are not migrated: ${settings.map(stored).join(", and ")}. ${MIGRATE_SCHEMA_FIX}`;
1869
1872
  const pluginsMessage = (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}`;
1870
1873
  var HotUpdaterSchemaMigrationRequiredError = class extends Error {
1871
1874
  adapterName;
@@ -1876,7 +1879,7 @@ var HotUpdaterSchemaMigrationRequiredError = class extends Error {
1876
1879
  constructor(adapterName, currentVersion, setting, options) {
1877
1880
  const settings = options?.settings ?? (setting === void 0 ? [] : [setting]);
1878
1881
  const plugins = settings.flatMap((mismatch) => pluginOf(mismatch) ?? []);
1879
- 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 });
1882
+ 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 });
1880
1883
  this.adapterName = adapterName;
1881
1884
  this.currentVersion = currentVersion;
1882
1885
  this.setting = setting;
@@ -1929,11 +1932,23 @@ const checkSchemaFence = async (adapter, adapterName, expected, fix) => {
1929
1932
  });
1930
1933
  };
1931
1934
  const FENCED = Symbol.for("@hot-updater/server/schema-fence");
1935
+ const FENCED_SETTINGS = Symbol.for("@hot-updater/server/schema-fence-settings");
1932
1936
  const fencedName = (adapter) => adapter[FENCED];
1933
1937
  const withSchemaFence = (adapter, adapterName, expected, fix) => {
1938
+ const inner = adapter[FENCED_SETTINGS];
1939
+ const settings = inner === void 0 ? {
1940
+ adapter,
1941
+ expected
1942
+ } : {
1943
+ adapter: inner.adapter,
1944
+ expected: {
1945
+ ...inner.expected,
1946
+ ...expected
1947
+ }
1948
+ };
1934
1949
  let ready;
1935
1950
  const fence = () => {
1936
- ready ??= checkSchemaFence(adapter, adapterName, expected, fix).catch((error) => {
1951
+ ready ??= checkSchemaFence(settings.adapter, adapterName, settings.expected, fix).catch((error) => {
1937
1952
  ready = void 0;
1938
1953
  throw error;
1939
1954
  });
@@ -1942,6 +1957,7 @@ const withSchemaFence = (adapter, adapterName, expected, fix) => {
1942
1957
  return {
1943
1958
  ...adapter,
1944
1959
  [FENCED]: adapterName,
1960
+ [FENCED_SETTINGS]: settings,
1945
1961
  get: (table, keys) => fence().then(() => adapter.get(table, keys)),
1946
1962
  query: (table, request) => fence().then(() => adapter.query(table, request)),
1947
1963
  write: (ops) => fence().then(() => adapter.write(ops)),
@@ -2131,257 +2147,360 @@ const CHANNEL_ID = {
2131
2147
  maxLength: 1408,
2132
2148
  ascii: true
2133
2149
  };
2134
- const HOT_UPDATER_SCHEMA_VERSION = "1.0.0";
2150
+ const HOT_UPDATER_SCHEMA_VERSION = "1.2.0";
2151
+ const bundles = defineTable({
2152
+ id: {
2153
+ type: "string",
2154
+ maxLength: 36
2155
+ },
2156
+ platform: {
2157
+ type: "string",
2158
+ maxLength: 16
2159
+ },
2160
+ git_commit_hash: {
2161
+ type: "string",
2162
+ maxLength: 255,
2163
+ required: false
2164
+ },
2165
+ metadata: { type: "json" },
2166
+ manifest_storage_uri: { type: "string" },
2167
+ manifest_file_hash: { type: "string" },
2168
+ asset_base_storage_uri: { type: "string" }
2169
+ }, {
2170
+ key: ["id"],
2171
+ indexes: {
2172
+ byPlatform: {
2173
+ eq: ["platform"],
2174
+ sort: ["id"]
2175
+ },
2176
+ all: {
2177
+ eq: [],
2178
+ sort: ["id"]
2179
+ }
2180
+ }
2181
+ });
2182
+ const bundlePatches = defineTable({
2183
+ id: {
2184
+ type: "string",
2185
+ maxLength: 255
2186
+ },
2187
+ bundle_id: {
2188
+ type: "string",
2189
+ maxLength: 36,
2190
+ references: {
2191
+ model: "bundles",
2192
+ onDelete: "cascade"
2193
+ }
2194
+ },
2195
+ base_bundle_id: {
2196
+ type: "string",
2197
+ maxLength: 36,
2198
+ references: {
2199
+ model: "bundles",
2200
+ onDelete: "cascade"
2201
+ }
2202
+ },
2203
+ base_file_hash: { type: "string" },
2204
+ patch_file_hash: { type: "string" },
2205
+ patch_storage_uri: { type: "string" },
2206
+ byte_size: { type: "integer" },
2207
+ order_index: { type: "integer" }
2208
+ }, {
2209
+ key: ["id"],
2210
+ indexes: {
2211
+ pair: {
2212
+ eq: ["bundle_id", "base_bundle_id"],
2213
+ sort: [],
2214
+ unique: true
2215
+ },
2216
+ byBundle: {
2217
+ eq: ["bundle_id"],
2218
+ sort: ["order_index"],
2219
+ root: { model: "bundles" }
2220
+ },
2221
+ byBase: {
2222
+ eq: ["base_bundle_id"],
2223
+ sort: ["bundle_id"]
2224
+ }
2225
+ }
2226
+ });
2227
+ const releases = defineTable({
2228
+ id: {
2229
+ type: "string",
2230
+ maxLength: 36
2231
+ },
2232
+ revision: { type: "integer" },
2233
+ scope_key: {
2234
+ type: "string",
2235
+ maxLength: 2048,
2236
+ ascii: true
2237
+ },
2238
+ channel_id: {
2239
+ ...CHANNEL_ID,
2240
+ references: {
2241
+ model: "channels",
2242
+ onDelete: "restrict"
2243
+ }
2244
+ },
2245
+ platform: {
2246
+ type: "string",
2247
+ maxLength: 16
2248
+ },
2249
+ kind: {
2250
+ type: "string",
2251
+ maxLength: 16
2252
+ },
2253
+ bundle_id: {
2254
+ type: "string",
2255
+ maxLength: 36,
2256
+ required: false,
2257
+ references: {
2258
+ model: "bundles",
2259
+ onDelete: "restrict"
2260
+ }
2261
+ },
2262
+ strategy: {
2263
+ type: "string",
2264
+ maxLength: 16
2265
+ },
2266
+ target_app_version: {
2267
+ type: "string",
2268
+ required: false
2269
+ },
2270
+ fingerprint_hash: {
2271
+ type: "string",
2272
+ maxLength: 255,
2273
+ required: false
2274
+ },
2275
+ enabled: { type: "boolean" },
2276
+ should_force_update: { type: "boolean" },
2277
+ message: {
2278
+ type: "string",
2279
+ required: false
2280
+ },
2281
+ rollout_cohort_count: { type: "integer" },
2282
+ target_cohorts: { type: "json" },
2283
+ operation: {
2284
+ type: "string",
2285
+ maxLength: 16
2286
+ },
2287
+ source_release_id: {
2288
+ type: "string",
2289
+ maxLength: 36,
2290
+ required: false,
2291
+ references: {
2292
+ model: "releases",
2293
+ onDelete: "none"
2294
+ }
2295
+ },
2296
+ created_at_ms: { type: "integer" },
2297
+ updated_at_ms: { type: "integer" }
2298
+ }, {
2299
+ key: ["id"],
2300
+ indexes: {
2301
+ byScope: {
2302
+ eq: ["scope_key"],
2303
+ sort: ["id"],
2304
+ root: { model: "release_catalogs" }
2305
+ },
2306
+ byScopeEnabled: {
2307
+ eq: ["scope_key", "enabled"],
2308
+ sort: ["id"],
2309
+ root: { model: "release_catalogs" }
2310
+ },
2311
+ byChannelPlatform: {
2312
+ eq: ["channel_id", "platform"],
2313
+ sort: ["id"]
2314
+ },
2315
+ byChannelPlatformEnabled: {
2316
+ eq: [
2317
+ "channel_id",
2318
+ "platform",
2319
+ "enabled"
2320
+ ],
2321
+ sort: ["id"]
2322
+ },
2323
+ byBundle: {
2324
+ eq: ["bundle_id"],
2325
+ sort: ["id"]
2326
+ },
2327
+ all: {
2328
+ eq: [],
2329
+ sort: ["id"]
2330
+ }
2331
+ }
2332
+ });
2333
+ const releaseCatalogs = defineTable({
2334
+ scope_key: {
2335
+ type: "string",
2336
+ maxLength: 2048,
2337
+ ascii: true
2338
+ },
2339
+ catalog_id: {
2340
+ type: "string",
2341
+ maxLength: 255
2342
+ },
2343
+ strategy: {
2344
+ type: "string",
2345
+ maxLength: 16
2346
+ },
2347
+ channel_id: {
2348
+ ...CHANNEL_ID,
2349
+ references: {
2350
+ model: "channels",
2351
+ onDelete: "none"
2352
+ }
2353
+ },
2354
+ channel_key: {
2355
+ type: "string",
2356
+ maxLength: 1400,
2357
+ ascii: true
2358
+ },
2359
+ platform: {
2360
+ type: "string",
2361
+ maxLength: 16
2362
+ },
2363
+ fingerprint_hash: {
2364
+ type: "string",
2365
+ maxLength: 255,
2366
+ required: false
2367
+ },
2368
+ generation: { type: "integer" },
2369
+ payload: { type: "string" },
2370
+ catalog_hash: {
2371
+ type: "string",
2372
+ maxLength: 71
2373
+ },
2374
+ byte_size: { type: "integer" },
2375
+ is_tombstone: { type: "boolean" },
2376
+ updated_at_ms: { type: "integer" }
2377
+ }, {
2378
+ key: ["scope_key"],
2379
+ indexes: { all: {
2380
+ eq: [],
2381
+ sort: ["scope_key"]
2382
+ } }
2383
+ });
2384
+ const channels = defineTable({
2385
+ id: CHANNEL_ID,
2386
+ name: {
2387
+ type: "string",
2388
+ maxLength: 255,
2389
+ unique: true
2390
+ }
2391
+ }, {
2392
+ key: ["id"],
2393
+ indexes: { all: {
2394
+ eq: [],
2395
+ sort: ["name"]
2396
+ } }
2397
+ });
2398
+ const nativeBuilds = defineTable({
2399
+ id: {
2400
+ type: "string",
2401
+ maxLength: 36
2402
+ },
2403
+ scope_key: {
2404
+ type: "string",
2405
+ maxLength: 2048,
2406
+ ascii: true
2407
+ },
2408
+ platform: {
2409
+ type: "string",
2410
+ maxLength: 16
2411
+ },
2412
+ channel_id: {
2413
+ ...CHANNEL_ID,
2414
+ references: {
2415
+ model: "channels",
2416
+ onDelete: "none"
2417
+ }
2418
+ },
2419
+ fingerprint_hash: {
2420
+ type: "string",
2421
+ maxLength: 255,
2422
+ required: false
2423
+ },
2424
+ app_version: {
2425
+ type: "string",
2426
+ required: false
2427
+ },
2428
+ min_bundle_id: {
2429
+ type: "string",
2430
+ maxLength: 36,
2431
+ unique: true,
2432
+ references: {
2433
+ model: "bundles",
2434
+ onDelete: "none"
2435
+ }
2436
+ },
2437
+ artifact_kind: {
2438
+ type: "string",
2439
+ maxLength: 16
2440
+ },
2441
+ artifact_storage_uri: { type: "string" },
2442
+ byte_size: { type: "integer" },
2443
+ git_commit_hash: {
2444
+ type: "string",
2445
+ maxLength: 255,
2446
+ required: false
2447
+ },
2448
+ created_at_ms: { type: "integer" },
2449
+ build_key: {
2450
+ type: "string",
2451
+ maxLength: 69,
2452
+ ascii: true,
2453
+ required: false
2454
+ },
2455
+ build_number: {
2456
+ type: "string",
2457
+ maxLength: 64,
2458
+ required: false
2459
+ },
2460
+ submitted_to: {
2461
+ type: "string",
2462
+ maxLength: 16,
2463
+ required: false
2464
+ },
2465
+ submitted_at_ms: {
2466
+ type: "integer",
2467
+ required: false
2468
+ },
2469
+ submitted_version: {
2470
+ type: "string",
2471
+ maxLength: 255,
2472
+ required: false
2473
+ }
2474
+ }, {
2475
+ key: ["id"],
2476
+ indexes: {
2477
+ byScope: {
2478
+ eq: ["scope_key"],
2479
+ sort: ["id"]
2480
+ },
2481
+ byBuildKey: {
2482
+ eq: ["scope_key", "build_key"],
2483
+ sort: ["id"]
2484
+ },
2485
+ byChannel: {
2486
+ eq: ["channel_id"],
2487
+ sort: ["id"],
2488
+ root: { model: "channels" }
2489
+ },
2490
+ all: {
2491
+ eq: [],
2492
+ sort: ["id"]
2493
+ }
2494
+ }
2495
+ });
2135
2496
  const coreModule = {
2136
2497
  id: "core",
2137
2498
  schema: {
2138
- bundles: defineTable({
2139
- id: {
2140
- type: "string",
2141
- maxLength: 36
2142
- },
2143
- platform: {
2144
- type: "string",
2145
- maxLength: 16
2146
- },
2147
- git_commit_hash: {
2148
- type: "string",
2149
- maxLength: 255,
2150
- required: false
2151
- },
2152
- metadata: { type: "json" },
2153
- manifest_storage_uri: { type: "string" },
2154
- manifest_file_hash: { type: "string" },
2155
- asset_base_storage_uri: { type: "string" }
2156
- }, {
2157
- key: ["id"],
2158
- indexes: {
2159
- byPlatform: {
2160
- eq: ["platform"],
2161
- sort: ["id"]
2162
- },
2163
- all: {
2164
- eq: [],
2165
- sort: ["id"]
2166
- }
2167
- }
2168
- }),
2169
- bundle_patches: defineTable({
2170
- id: {
2171
- type: "string",
2172
- maxLength: 255
2173
- },
2174
- bundle_id: {
2175
- type: "string",
2176
- maxLength: 36,
2177
- references: {
2178
- model: "bundles",
2179
- onDelete: "cascade"
2180
- }
2181
- },
2182
- base_bundle_id: {
2183
- type: "string",
2184
- maxLength: 36,
2185
- references: {
2186
- model: "bundles",
2187
- onDelete: "cascade"
2188
- }
2189
- },
2190
- base_file_hash: { type: "string" },
2191
- patch_file_hash: { type: "string" },
2192
- patch_storage_uri: { type: "string" },
2193
- byte_size: { type: "integer" },
2194
- order_index: { type: "integer" }
2195
- }, {
2196
- key: ["id"],
2197
- indexes: {
2198
- pair: {
2199
- eq: ["bundle_id", "base_bundle_id"],
2200
- sort: [],
2201
- unique: true
2202
- },
2203
- byBundle: {
2204
- eq: ["bundle_id"],
2205
- sort: ["order_index"],
2206
- root: { model: "bundles" }
2207
- },
2208
- byBase: {
2209
- eq: ["base_bundle_id"],
2210
- sort: ["bundle_id"]
2211
- }
2212
- }
2213
- }),
2214
- releases: defineTable({
2215
- id: {
2216
- type: "string",
2217
- maxLength: 36
2218
- },
2219
- revision: { type: "integer" },
2220
- scope_key: {
2221
- type: "string",
2222
- maxLength: 2048,
2223
- ascii: true
2224
- },
2225
- channel_id: {
2226
- ...CHANNEL_ID,
2227
- references: {
2228
- model: "channels",
2229
- onDelete: "restrict"
2230
- }
2231
- },
2232
- platform: {
2233
- type: "string",
2234
- maxLength: 16
2235
- },
2236
- kind: {
2237
- type: "string",
2238
- maxLength: 16
2239
- },
2240
- bundle_id: {
2241
- type: "string",
2242
- maxLength: 36,
2243
- required: false,
2244
- references: {
2245
- model: "bundles",
2246
- onDelete: "restrict"
2247
- }
2248
- },
2249
- strategy: {
2250
- type: "string",
2251
- maxLength: 16
2252
- },
2253
- target_app_version: {
2254
- type: "string",
2255
- required: false
2256
- },
2257
- fingerprint_hash: {
2258
- type: "string",
2259
- maxLength: 255,
2260
- required: false
2261
- },
2262
- enabled: { type: "boolean" },
2263
- should_force_update: { type: "boolean" },
2264
- message: {
2265
- type: "string",
2266
- required: false
2267
- },
2268
- rollout_cohort_count: { type: "integer" },
2269
- target_cohorts: { type: "json" },
2270
- operation: {
2271
- type: "string",
2272
- maxLength: 16
2273
- },
2274
- source_release_id: {
2275
- type: "string",
2276
- maxLength: 36,
2277
- required: false,
2278
- references: {
2279
- model: "releases",
2280
- onDelete: "none"
2281
- }
2282
- },
2283
- created_at_ms: { type: "integer" },
2284
- updated_at_ms: { type: "integer" }
2285
- }, {
2286
- key: ["id"],
2287
- indexes: {
2288
- byScope: {
2289
- eq: ["scope_key"],
2290
- sort: ["id"],
2291
- root: { model: "release_catalogs" }
2292
- },
2293
- byScopeEnabled: {
2294
- eq: ["scope_key", "enabled"],
2295
- sort: ["id"],
2296
- root: { model: "release_catalogs" }
2297
- },
2298
- byChannelPlatform: {
2299
- eq: ["channel_id", "platform"],
2300
- sort: ["id"]
2301
- },
2302
- byChannelPlatformEnabled: {
2303
- eq: [
2304
- "channel_id",
2305
- "platform",
2306
- "enabled"
2307
- ],
2308
- sort: ["id"]
2309
- },
2310
- byBundle: {
2311
- eq: ["bundle_id"],
2312
- sort: ["id"]
2313
- },
2314
- all: {
2315
- eq: [],
2316
- sort: ["id"]
2317
- }
2318
- }
2319
- }),
2320
- release_catalogs: defineTable({
2321
- scope_key: {
2322
- type: "string",
2323
- maxLength: 2048,
2324
- ascii: true
2325
- },
2326
- catalog_id: {
2327
- type: "string",
2328
- maxLength: 255
2329
- },
2330
- strategy: {
2331
- type: "string",
2332
- maxLength: 16
2333
- },
2334
- channel_id: {
2335
- ...CHANNEL_ID,
2336
- references: {
2337
- model: "channels",
2338
- onDelete: "none"
2339
- }
2340
- },
2341
- channel_key: {
2342
- type: "string",
2343
- maxLength: 1400,
2344
- ascii: true
2345
- },
2346
- platform: {
2347
- type: "string",
2348
- maxLength: 16
2349
- },
2350
- fingerprint_hash: {
2351
- type: "string",
2352
- maxLength: 255,
2353
- required: false
2354
- },
2355
- generation: { type: "integer" },
2356
- payload: { type: "string" },
2357
- catalog_hash: {
2358
- type: "string",
2359
- maxLength: 71
2360
- },
2361
- byte_size: { type: "integer" },
2362
- is_tombstone: { type: "boolean" },
2363
- updated_at_ms: { type: "integer" }
2364
- }, {
2365
- key: ["scope_key"],
2366
- indexes: { all: {
2367
- eq: [],
2368
- sort: ["scope_key"]
2369
- } }
2370
- }),
2371
- channels: defineTable({
2372
- id: CHANNEL_ID,
2373
- name: {
2374
- type: "string",
2375
- maxLength: 255,
2376
- unique: true
2377
- }
2378
- }, {
2379
- key: ["id"],
2380
- indexes: { all: {
2381
- eq: [],
2382
- sort: ["name"]
2383
- } }
2384
- }),
2499
+ bundles,
2500
+ bundle_patches: bundlePatches,
2501
+ releases,
2502
+ release_catalogs: releaseCatalogs,
2503
+ channels,
2385
2504
  bundle_totals: defineAggregate({ platform_key: {
2386
2505
  type: "string",
2387
2506
  maxLength: 16
@@ -2392,7 +2511,8 @@ const coreModule = {
2392
2511
  eq: ["platform_key"],
2393
2512
  sort: []
2394
2513
  } }
2395
- })
2514
+ }),
2515
+ native_builds: nativeBuilds
2396
2516
  }
2397
2517
  };
2398
2518
  //#endregion
@@ -3361,14 +3481,14 @@ const createEngineMigrator = (options) => {
3361
3481
  return {
3362
3482
  getVersion,
3363
3483
  getNameVariants: async () => void 0,
3364
- next: async () => await getVersion() === "1.0.0" ? void 0 : { version: HOT_UPDATER_SCHEMA_VERSION },
3484
+ next: async () => await getVersion() === "1.2.0" ? void 0 : { version: HOT_UPDATER_SCHEMA_VERSION },
3365
3485
  previous: async () => void 0,
3366
3486
  up: makeResult,
3367
3487
  down: async () => {
3368
3488
  throw new Error("No previous schema to migrate to.");
3369
3489
  },
3370
3490
  migrateTo: async (version, migrate) => {
3371
- if (version !== "1.0.0") throw new Error(`Invalid version ${version}`);
3491
+ if (version !== "1.2.0") throw new Error(`Invalid version ${version}`);
3372
3492
  return makeResult(migrate);
3373
3493
  },
3374
3494
  migrateToLatest: makeResult
@@ -4056,9 +4176,9 @@ const createDatabaseEngine = ({ batching, now = Date.now, meter, ...options }) =
4056
4176
  //#region ../plugin-core/dist/engine/createEngine.mjs
4057
4177
  const pluginTablesFix = (database) => {
4058
4178
  const { createMigrator, generateSchema } = database;
4059
- const managed = "A managed server runs only its provider's plugins, and rerunning `hot-updater init --provider <provider>` applies the provider's migrations.";
4179
+ 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.";
4060
4180
  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.";
4061
- 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;
4181
+ 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.`;
4062
4182
  };
4063
4183
  const createEngine = (database, { plugins = [], now = Date.now } = {}) => {
4064
4184
  const modules = /* @__PURE__ */ new Map();
@@ -17545,20 +17665,32 @@ var require_lib$2 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
17545
17665
  //#region ../postgres/dist/index.mjs
17546
17666
  var import_lib$1 = /* @__PURE__ */ __toESM(require_lib$2(), 1);
17547
17667
  const { Pool } = import_lib$1.default;
17668
+ const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
17669
+ const targetOf = ({ connectionString, host, port, database }) => {
17670
+ if (connectionString !== void 0) try {
17671
+ const url = new URL(connectionString);
17672
+ return `postgres/${url.hostname}:${url.port || "5432"}/${decodeURIComponent(url.pathname.slice(1))}`;
17673
+ } catch {
17674
+ return;
17675
+ }
17676
+ return host === void 0 || database === void 0 ? void 0 : `postgres/${host}:${port ?? 5432}/${database}`;
17677
+ };
17548
17678
  const postgres = (config) => {
17549
17679
  const { dialect, ...poolConfig } = config;
17680
+ const target = targetOf(poolConfig);
17550
17681
  const db = new Kysely({ dialect: dialect ?? new PostgresDialect({ pool: new Pool(poolConfig) }) });
17551
17682
  return {
17552
17683
  ...createEngineDatabase({
17553
17684
  name: "postgres",
17554
17685
  adapter: createSqlAdapter({ executor: kyselyExecutor(db, "postgresql") })
17555
17686
  }),
17687
+ ...target === void 0 ? {} : { [DATABASE_TARGET]: target },
17556
17688
  dispose: () => db.destroy()
17557
17689
  };
17558
17690
  };
17559
17691
  //#endregion
17560
17692
  //#region ../../packages/server/dist/version.mjs
17561
- const HOT_UPDATER_SERVER_VERSION = "1.1.4";
17693
+ const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.0";
17562
17694
  //#endregion
17563
17695
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
17564
17696
  const createVersionRouteHandlers = (plugins = []) => {
@@ -18291,8 +18423,8 @@ const baseBundleIdsOf = (row, range, bundleId, limit) => {
18291
18423
  //#endregion
18292
18424
  //#region ../../packages/server/dist/core/reads.mjs
18293
18425
  const PAGE$3 = 500;
18294
- const pick = (fields) => (row) => Object.fromEntries(fields.map((field) => [field, row[field]]));
18295
- const toBundleRow = pick([
18426
+ const pick$1 = (fields) => (row) => Object.fromEntries(fields.map((field) => [field, row[field]]));
18427
+ const toBundleRow = pick$1([
18296
18428
  "id",
18297
18429
  "platform",
18298
18430
  "git_commit_hash",
@@ -18301,7 +18433,7 @@ const toBundleRow = pick([
18301
18433
  "manifest_file_hash",
18302
18434
  "asset_base_storage_uri"
18303
18435
  ]);
18304
- const toPatchRow = pick([
18436
+ const toPatchRow = pick$1([
18305
18437
  "id",
18306
18438
  "bundle_id",
18307
18439
  "base_bundle_id",
@@ -18311,7 +18443,7 @@ const toPatchRow = pick([
18311
18443
  "byte_size",
18312
18444
  "order_index"
18313
18445
  ]);
18314
- const toReleaseRow = pick([
18446
+ const toReleaseRow = pick$1([
18315
18447
  "id",
18316
18448
  "revision",
18317
18449
  "scope_key",
@@ -18332,7 +18464,7 @@ const toReleaseRow = pick([
18332
18464
  "created_at_ms",
18333
18465
  "updated_at_ms"
18334
18466
  ]);
18335
- const toCatalogRow = pick([
18467
+ const toCatalogRow = pick$1([
18336
18468
  "scope_key",
18337
18469
  "catalog_id",
18338
18470
  "strategy",
@@ -18347,7 +18479,7 @@ const toCatalogRow = pick([
18347
18479
  "is_tombstone",
18348
18480
  "updated_at_ms"
18349
18481
  ]);
18350
- const toChannelRow = pick(["id", "name"]);
18482
+ const toChannelRow = pick$1(["id", "name"]);
18351
18483
  const drain$1 = async (read) => {
18352
18484
  const rows = [];
18353
18485
  let cursor;
@@ -18358,6 +18490,7 @@ const drain$1 = async (read) => {
18358
18490
  } while (cursor !== void 0);
18359
18491
  return rows;
18360
18492
  };
18493
+ const after = (input) => input.after === void 0 ? {} : { range: (input.order ?? "asc") === "asc" ? { gt: input.after } : { lt: input.after } };
18361
18494
  const createCoreReads = (db, storage) => {
18362
18495
  const detail = async (row) => {
18363
18496
  const count = row._refs_bundle_patches_bundle_id ?? 0;
@@ -18380,7 +18513,6 @@ const createCoreReads = (db, storage) => {
18380
18513
  childCount: row._refs_bundle_patches_base_bundle_id ?? 0
18381
18514
  };
18382
18515
  };
18383
- const after = (input) => input.after === void 0 ? {} : { range: (input.order ?? "asc") === "asc" ? { gt: input.after } : { lt: input.after } };
18384
18516
  return {
18385
18517
  async getReleaseCatalog(input) {
18386
18518
  const scopeKey = releaseCatalogScopeKeyOf(input);
@@ -18558,9 +18690,9 @@ const insertBundle = (tx, row) => {
18558
18690
  tx.create("bundles", row);
18559
18691
  countBundle(tx, row.platform, 1);
18560
18692
  };
18561
- const updateBundle = (tx, current, set) => {
18693
+ const updateBundle = (tx, current, set, { counted = true } = {}) => {
18562
18694
  tx.update("bundles", current, set);
18563
- if (set.platform !== void 0 && set.platform !== current.platform) {
18695
+ if (counted && set.platform !== void 0 && set.platform !== current.platform) {
18564
18696
  countBundle(tx, current.platform, -1);
18565
18697
  countBundle(tx, set.platform, 1);
18566
18698
  }
@@ -18625,10 +18757,223 @@ const deleteChannel = (db, id) => db.transaction(async (tx) => {
18625
18757
  deleted: false,
18626
18758
  reason: "not_empty"
18627
18759
  };
18760
+ const { rows: builds } = await tx.findMany("native_builds", {
18761
+ index: "byChannel",
18762
+ where: { channel_id: id },
18763
+ limit: 1
18764
+ });
18765
+ if (builds.length > 0) return {
18766
+ deleted: false,
18767
+ reason: "not_empty"
18768
+ };
18628
18769
  await tx.delete("channels", row);
18629
18770
  return { deleted: true };
18630
18771
  });
18631
18772
  //#endregion
18773
+ //#region ../../packages/server/dist/core/nativeBuilds.mjs
18774
+ const ARTIFACT_KINDS = {
18775
+ android: ["apk", "aab"],
18776
+ ios: ["ipa", "app"]
18777
+ };
18778
+ const SCOPE_KEY_MAX_LENGTH = 2048;
18779
+ const GIT_COMMIT_HASH_MAX_LENGTH = 255;
18780
+ const BUILD_KEY = /^nbk1:[0-9a-f]{64}$/;
18781
+ var NativeBuildInputError = class extends TypeError {
18782
+ name = "NativeBuildInputError";
18783
+ };
18784
+ const pick = (row) => ({
18785
+ id: row.id,
18786
+ scope_key: row.scope_key,
18787
+ platform: row.platform,
18788
+ channel_id: row.channel_id,
18789
+ fingerprint_hash: row.fingerprint_hash ?? null,
18790
+ app_version: row.app_version ?? null,
18791
+ min_bundle_id: row.min_bundle_id,
18792
+ artifact_kind: row.artifact_kind,
18793
+ artifact_storage_uri: row.artifact_storage_uri,
18794
+ byte_size: row.byte_size,
18795
+ git_commit_hash: row.git_commit_hash ?? null,
18796
+ created_at_ms: row.created_at_ms,
18797
+ build_key: row.build_key ?? null,
18798
+ build_number: row.build_number ?? null,
18799
+ submitted_to: row.submitted_to ?? null,
18800
+ submitted_at_ms: row.submitted_at_ms ?? null,
18801
+ submitted_version: row.submitted_version ?? null
18802
+ });
18803
+ const SUBMISSION_DESTINATIONS = {
18804
+ android: "play-internal",
18805
+ ios: "testflight"
18806
+ };
18807
+ const BUILD_NUMBER_MAX_LENGTH = 64;
18808
+ const SUBMITTED_VERSION_MAX_LENGTH = 255;
18809
+ const nativeBuildScopeKey = (scope) => {
18810
+ let scopeKey;
18811
+ try {
18812
+ const channelKey = encodeChannelKey(scope.channel);
18813
+ scopeKey = scope.fingerprintHash === null ? `${createReleaseCatalogScopeKey({
18814
+ channelKey,
18815
+ platform: scope.platform,
18816
+ strategy: "APP_VERSION"
18817
+ })}:${encodeURIComponent(scope.appVersion)}` : createReleaseCatalogScopeKey({
18818
+ channelKey,
18819
+ fingerprintHash: scope.fingerprintHash,
18820
+ platform: scope.platform,
18821
+ strategy: "FINGERPRINT"
18822
+ });
18823
+ } catch (error) {
18824
+ throw new NativeBuildInputError(`Invalid native build scope: ${error instanceof Error ? error.message : String(error)}.`);
18825
+ }
18826
+ 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.`);
18827
+ return scopeKey;
18828
+ };
18829
+ const isBuiltInBundle = async (db, bundleId) => await db.findOne("native_builds", { min_bundle_id: bundleId }) !== null;
18830
+ const problemOf = ({ id, minBundleId, platform, channel, fingerprintHash, appVersion, gitCommitHash, buildKey, buildNumber, artifact }) => {
18831
+ if (!isUUIDv7(id)) return "id must be a UUIDv7";
18832
+ if (!isUUIDv7(minBundleId)) return "minBundleId must be the UUIDv7 a release build compiles in";
18833
+ if (platform !== "ios" && platform !== "android") return "platform must be ios or android";
18834
+ if (channel.trim().length === 0) return "channel is required";
18835
+ if (gitCommitHash !== null && gitCommitHash.length > GIT_COMMIT_HASH_MAX_LENGTH) return `gitCommitHash must be at most ${GIT_COMMIT_HASH_MAX_LENGTH} characters`;
18836
+ if (fingerprintHash === null === (appVersion === null)) return "exactly one of fingerprintHash and appVersion is required";
18837
+ if (buildKey !== null && !BUILD_KEY.test(buildKey)) return "buildKey must be nbk1: and a SHA-256, or null";
18838
+ 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`;
18839
+ const kinds = ARTIFACT_KINDS[platform];
18840
+ if (!kinds.includes(artifact.kind)) return `artifact.kind of an ${platform} build must be ${kinds.join(" or ")}`;
18841
+ if (!Number.isSafeInteger(artifact.byteSize) || artifact.byteSize < 0) return "artifact.byteSize must be a byte count";
18842
+ return null;
18843
+ };
18844
+ const createNativeBuildApi = (db, { now = Date.now } = {}) => ({
18845
+ async registerNativeBuild(registration) {
18846
+ const problem = problemOf(registration);
18847
+ if (problem !== null) throw new NativeBuildInputError(`Invalid native build: ${problem}.`);
18848
+ const scopeKey = nativeBuildScopeKey(registration);
18849
+ const channelId = `channel:${encodeChannelKey(registration.channel)}`;
18850
+ const { embeddedBundle } = registration;
18851
+ const row = {
18852
+ id: registration.id,
18853
+ scope_key: scopeKey,
18854
+ platform: registration.platform,
18855
+ channel_id: channelId,
18856
+ fingerprint_hash: registration.fingerprintHash,
18857
+ app_version: registration.appVersion,
18858
+ min_bundle_id: registration.minBundleId,
18859
+ artifact_kind: registration.artifact.kind,
18860
+ artifact_storage_uri: registration.artifact.storageUri,
18861
+ byte_size: registration.artifact.byteSize,
18862
+ git_commit_hash: registration.gitCommitHash,
18863
+ created_at_ms: now(),
18864
+ build_key: registration.buildKey,
18865
+ build_number: registration.buildNumber ?? null,
18866
+ submitted_to: null,
18867
+ submitted_at_ms: null,
18868
+ submitted_version: null
18869
+ };
18870
+ for (let attempt = 1;; attempt += 1) {
18871
+ await insertChannel(db, {
18872
+ id: channelId,
18873
+ name: registration.channel
18874
+ });
18875
+ try {
18876
+ await db.transaction(async (tx) => {
18877
+ tx.create("bundles", {
18878
+ id: registration.minBundleId,
18879
+ platform: registration.platform,
18880
+ git_commit_hash: registration.gitCommitHash,
18881
+ metadata: embeddedBundle.metadata ?? {},
18882
+ manifest_storage_uri: embeddedBundle.manifestStorageUri,
18883
+ manifest_file_hash: embeddedBundle.manifestFileHash,
18884
+ asset_base_storage_uri: embeddedBundle.assetBaseStorageUri
18885
+ });
18886
+ tx.create("native_builds", row);
18887
+ });
18888
+ break;
18889
+ } catch (error) {
18890
+ if (!(error instanceof DatabaseConstraintError)) throw error;
18891
+ if (error.reason === "exists" || error.reason === "unique") throw new DatabaseConstraintError(error.reason, "native_builds");
18892
+ if (attempt < 3 && error.reason === "not_found" && error.model === "channels") continue;
18893
+ throw error;
18894
+ }
18895
+ }
18896
+ return row;
18897
+ },
18898
+ async getNativeBuild(id) {
18899
+ const row = await db.findOne("native_builds", { id });
18900
+ return row === null ? null : pick(row);
18901
+ },
18902
+ async findNativeBuild(scope, options) {
18903
+ const scopeKey = nativeBuildScopeKey(scope);
18904
+ const buildKey = options?.buildKey;
18905
+ if (buildKey !== void 0 && !BUILD_KEY.test(buildKey)) throw new NativeBuildInputError("Invalid native build key: it must be nbk1: and a SHA-256.");
18906
+ const { rows } = buildKey === void 0 ? await db.findMany("native_builds", {
18907
+ index: "byScope",
18908
+ where: { scope_key: scopeKey },
18909
+ order: "desc",
18910
+ limit: 1
18911
+ }) : await db.findMany("native_builds", {
18912
+ index: "byBuildKey",
18913
+ where: {
18914
+ scope_key: scopeKey,
18915
+ build_key: buildKey
18916
+ },
18917
+ order: "desc",
18918
+ limit: 1
18919
+ });
18920
+ return rows[0] === void 0 ? null : pick(rows[0]);
18921
+ },
18922
+ async recordNativeBuildSubmission(id, { destination, version }) {
18923
+ if (version.trim().length === 0 || version.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.`);
18924
+ return db.transaction(async (tx) => {
18925
+ const row = await tx.findOne("native_builds", { id });
18926
+ if (row === null) return null;
18927
+ const build = pick(row);
18928
+ 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}.`);
18929
+ const set = {
18930
+ submitted_to: destination,
18931
+ submitted_at_ms: now(),
18932
+ submitted_version: version
18933
+ };
18934
+ tx.update("native_builds", row, set);
18935
+ return {
18936
+ ...build,
18937
+ ...set
18938
+ };
18939
+ });
18940
+ },
18941
+ async deleteNativeBuild(id) {
18942
+ return db.transaction(async (tx) => {
18943
+ const row = await tx.findOne("native_builds", { id });
18944
+ if (row === null) return {
18945
+ deleted: false,
18946
+ reason: "not_found"
18947
+ };
18948
+ await tx.delete("native_builds", row);
18949
+ const bundle = await tx.findOne("bundles", { id: row.min_bundle_id });
18950
+ if (bundle !== null) await tx.delete("bundles", bundle);
18951
+ return {
18952
+ deleted: true,
18953
+ nativeBuild: pick(row)
18954
+ };
18955
+ });
18956
+ },
18957
+ async listNativeBuilds(input) {
18958
+ const page = {
18959
+ order: input.order ?? "asc",
18960
+ limit: input.limit
18961
+ };
18962
+ const { rows } = input.channelId === void 0 ? await db.findMany("native_builds", {
18963
+ index: "all",
18964
+ where: {},
18965
+ ...page,
18966
+ ...after(input)
18967
+ }) : await db.findMany("native_builds", {
18968
+ index: "byChannel",
18969
+ where: { channel_id: input.channelId },
18970
+ ...page,
18971
+ ...after(input)
18972
+ });
18973
+ return rows.map(pick);
18974
+ }
18975
+ });
18976
+ //#endregion
18632
18977
  //#region ../../packages/server/dist/core/releases.mjs
18633
18978
  const PAGE$1 = 500;
18634
18979
  const fail$1 = (code, message) => {
@@ -18915,6 +19260,7 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
18915
19260
  if (!("bundleId" in deployment) || platforms.has(deployment.bundleId)) continue;
18916
19261
  const row = await db.findOne("bundles", { id: deployment.bundleId });
18917
19262
  if (row === null) throw new DatabaseBundleNotFoundError(deployment.bundleId);
19263
+ 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.`);
18918
19264
  platforms.set(deployment.bundleId, toBundleRow(row).platform);
18919
19265
  }
18920
19266
  const updatedAtMs = now();
@@ -19110,7 +19456,8 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
19110
19456
  id
19111
19457
  };
19112
19458
  const { id: _id, ...set } = bundleToRow(next);
19113
- updateBundle(tx, current, set);
19459
+ const counted = set.platform === current.platform || !await isBuiltInBundle(tx, id);
19460
+ updateBundle(tx, current, set, { counted });
19114
19461
  if (patches !== void 0) await replaceBundlePatches(tx, current, bundleToPatchRows({
19115
19462
  ...next,
19116
19463
  patches
@@ -19121,7 +19468,9 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
19121
19468
  await db.transaction(async (tx) => {
19122
19469
  for (const id of new Set(ids)) {
19123
19470
  const current = await tx.findOne("bundles", { id });
19124
- if (current !== null) await deleteBundle(tx, current);
19471
+ if (current === null) continue;
19472
+ if (await isBuiltInBundle(tx, id)) throw new DatabaseRowReferencedError();
19473
+ await deleteBundle(tx, current);
19125
19474
  }
19126
19475
  });
19127
19476
  } catch (error) {
@@ -19167,6 +19516,7 @@ const createCoreApi = (db, storage, options = {}) => {
19167
19516
  return {
19168
19517
  ...reads,
19169
19518
  ...createCoreOperations(purgingCachedRoutes(db, options.onCachedRoutesChange), options),
19519
+ ...createNativeBuildApi(db, options),
19170
19520
  ready: async () => {
19171
19521
  await reads.listReleaseCatalogs({ limit: 1 });
19172
19522
  }
@@ -19358,6 +19708,10 @@ const answer = async (operation) => {
19358
19708
  code: "REFERENCED",
19359
19709
  error: error.message
19360
19710
  }, 409);
19711
+ if (error instanceof DatabaseConstraintError && error.model === "native_builds") return json$2({
19712
+ code: "NATIVE_BUILD_EXISTS",
19713
+ error: "A native build with this ID or built-in bundle ID is already registered."
19714
+ }, 409);
19361
19715
  throw error;
19362
19716
  }
19363
19717
  };
@@ -19453,6 +19807,40 @@ const bundleUpdateOf = (value, bundleId) => {
19453
19807
  if (id !== void 0 && id !== bundleId) throw new HandlerBadRequestError("Bundle id mismatch");
19454
19808
  return update;
19455
19809
  };
19810
+ const NATIVE_BUILD_ARTIFACT_KINDS = /* @__PURE__ */ new Set([
19811
+ "apk",
19812
+ "aab",
19813
+ "ipa",
19814
+ "app"
19815
+ ]);
19816
+ const nativeBuildScopeOf = (url) => {
19817
+ const params = url.searchParams;
19818
+ const platform = platformOf(url);
19819
+ const channel = params.get("channel");
19820
+ const fingerprintHash = params.get("fingerprintHash");
19821
+ const appVersion = params.get("appVersion");
19822
+ if (platform === void 0 || !isText$2(channel) || fingerprintHash === null === (appVersion === null) || fingerprintHash === "" || appVersion === "") throw new HandlerBadRequestError("A native build lookup needs platform, channel, and fingerprintHash or appVersion.");
19823
+ return fingerprintHash !== null ? {
19824
+ platform,
19825
+ channel,
19826
+ fingerprintHash,
19827
+ appVersion: null
19828
+ } : {
19829
+ platform,
19830
+ channel,
19831
+ fingerprintHash: null,
19832
+ appVersion
19833
+ };
19834
+ };
19835
+ const refusingInvalid = async (operation) => {
19836
+ try {
19837
+ return await operation();
19838
+ } catch (error) {
19839
+ if (error instanceof NativeBuildInputError) throw new HandlerBadRequestError(error.message);
19840
+ throw error;
19841
+ }
19842
+ };
19843
+ const isNativeBuildRegistration = (value) => isRecord$5(value) && isText$2(value.id) && isText$2(value.minBundleId) && typeof value.platform === "string" && isPlatform(value.platform) && isText$2(value.channel) && isTextOrNull$1(value.fingerprintHash) && isTextOrNull$1(value.appVersion) && value.fingerprintHash === null !== (value.appVersion === null) && isTextOrNull$1(value.gitCommitHash) && isTextOrNull$1(value.buildKey) && (value.buildNumber === void 0 || isTextOrNull$1(value.buildNumber)) && isRecord$5(value.artifact) && typeof value.artifact.kind === "string" && NATIVE_BUILD_ARTIFACT_KINDS.has(value.artifact.kind) && isText$2(value.artifact.storageUri) && Number.isSafeInteger(value.artifact.byteSize) && value.artifact.byteSize >= 0 && isRecord$5(value.embeddedBundle) && isText$2(value.embeddedBundle.manifestStorageUri) && isText$2(value.embeddedBundle.manifestFileHash) && isText$2(value.embeddedBundle.assetBaseStorageUri) && (value.embeddedBundle.metadata === void 0 || isRecord$5(value.embeddedBundle.metadata));
19456
19844
  const scopeKeyOf = (params) => decodeMaybe(requireRouteParam(params, "scopeKey")) ?? "";
19457
19845
  const createAdminRouteHandlers = () => ({
19458
19846
  listBundles: onCore(async (core, request) => {
@@ -19567,6 +19955,37 @@ const createAdminRouteHandlers = () => ({
19567
19955
  }) });
19568
19956
  }),
19569
19957
  preflightReleaseCatalogRebuild: onCore(async (core, _request, params) => json$2({ data: await core.preflightReleaseCatalogRebuild(scopeKeyOf(params)) })),
19958
+ registerNativeBuild: onCore(async (core, request) => {
19959
+ const input = await body(request);
19960
+ if (!isNativeBuildRegistration(input)) throw new HandlerBadRequestError("Invalid native build");
19961
+ return json$2({ data: await refusingInvalid(() => core.registerNativeBuild(input)) }, 201);
19962
+ }),
19963
+ findNativeBuild: onCore(async (core, request) => {
19964
+ const url = new URL(request.url);
19965
+ const scope = nativeBuildScopeOf(url);
19966
+ const buildKey = url.searchParams.get("buildKey");
19967
+ return json$2({ data: await refusingInvalid(() => buildKey === null ? core.findNativeBuild(scope) : core.findNativeBuild(scope, { buildKey })) });
19968
+ }),
19969
+ getNativeBuild: onCore(async (core, _request, params) => json$2({ data: await core.getNativeBuild(requireRouteParam(params, "id")) })),
19970
+ listNativeBuilds: onCore(async (core, request) => {
19971
+ const url = new URL(request.url);
19972
+ const input = keyset(url);
19973
+ const channelId = url.searchParams.get("channelId");
19974
+ return page(await core.listNativeBuilds({
19975
+ ...input,
19976
+ ...channelId === null ? {} : { channelId }
19977
+ }), input, ({ id }) => id);
19978
+ }),
19979
+ recordNativeBuildSubmission: onCore(async (core, request, params) => {
19980
+ const input = await body(request);
19981
+ if (!isRecord$5(input) || input.destination !== "testflight" && input.destination !== "play-internal" || !isText$2(input.version)) throw new HandlerBadRequestError("A native build submission needs destination (testflight or play-internal) and version.");
19982
+ const submission = {
19983
+ destination: input.destination,
19984
+ version: input.version
19985
+ };
19986
+ return json$2({ data: await refusingInvalid(() => core.recordNativeBuildSubmission(requireRouteParam(params, "id"), submission)) });
19987
+ }),
19988
+ deleteNativeBuild: onCore(async (core, _request, params) => json$2({ data: await core.deleteNativeBuild(requireRouteParam(params, "id")) })),
19570
19989
  deleteBundles: onCore(async (core, request) => {
19571
19990
  const { ids } = await body(request);
19572
19991
  if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && id.length > 0)) throw new HandlerBadRequestError("Bundle deletion needs ids");
@@ -19684,6 +20103,36 @@ const ADMIN_ROUTES = [
19684
20103
  method: "GET",
19685
20104
  path: "/base-candidates/:candidateKey",
19686
20105
  handler: "findBaseCandidates"
20106
+ },
20107
+ {
20108
+ method: "POST",
20109
+ path: "/native-builds",
20110
+ handler: "registerNativeBuild"
20111
+ },
20112
+ {
20113
+ method: "GET",
20114
+ path: "/native-builds/latest",
20115
+ handler: "findNativeBuild"
20116
+ },
20117
+ {
20118
+ method: "GET",
20119
+ path: "/native-builds/:id",
20120
+ handler: "getNativeBuild"
20121
+ },
20122
+ {
20123
+ method: "GET",
20124
+ path: "/native-builds",
20125
+ handler: "listNativeBuilds"
20126
+ },
20127
+ {
20128
+ method: "POST",
20129
+ path: "/native-builds/:id/submission",
20130
+ handler: "recordNativeBuildSubmission"
20131
+ },
20132
+ {
20133
+ method: "DELETE",
20134
+ path: "/native-builds/:id",
20135
+ handler: "deleteNativeBuild"
19687
20136
  }
19688
20137
  ];
19689
20138
  //#endregion
@@ -53537,7 +53986,8 @@ const storeIdOf = (token) => {
53537
53986
  const access = "private";
53538
53987
  const vercelBlobStorage = (config) => {
53539
53988
  const { token } = config;
53540
- const storeId = storeIdOf(token);
53989
+ let store;
53990
+ const storeId = () => store ??= storeIdOf(token);
53541
53991
  const normalizedBasePath = config.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
53542
53992
  const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
53543
53993
  const expiresIn = config.signedUrlExpiresIn ?? 3600;
@@ -53557,7 +54007,7 @@ const vercelBlobStorage = (config) => {
53557
54007
  };
53558
54008
  const parseAndValidate = (storageUri) => {
53559
54009
  const parsed = parseStorageUri(storageUri, PROTOCOL);
53560
- if (parsed.bucket !== storeId) throw new Error(`Blob store mismatch: expected "${storeId}", but found "${parsed.bucket}".`);
54010
+ if (parsed.bucket !== storeId()) throw new Error(`Blob store mismatch: expected "${storeId()}", but found "${parsed.bucket}".`);
53561
54011
  return parsed;
53562
54012
  };
53563
54013
  const getListPrefix = (prefix = "") => {
@@ -53576,7 +54026,7 @@ const vercelBlobStorage = (config) => {
53576
54026
  const storageKey = getStorageKey(key);
53577
54027
  const storageUri = createStorageUri({
53578
54028
  protocol: PROTOCOL,
53579
- bucket: storeId,
54029
+ bucket: storeId(),
53580
54030
  key: storageKey
53581
54031
  });
53582
54032
  await put(toPathname(storageKey), body, {
@@ -53648,7 +54098,7 @@ const vercelBlobStorage = (config) => {
53648
54098
  try {
53649
54099
  storageUri = createStorageUri({
53650
54100
  protocol: PROTOCOL,
53651
- bucket: storeId,
54101
+ bucket: storeId(),
53652
54102
  key
53653
54103
  });
53654
54104
  } catch {