@hot-updater/azure 1.1.4 → 1.2.0-rc.1

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.
@@ -4039,7 +4039,7 @@ const bundleMetadataToRow = (metadata) => {
4039
4039
  return metadata;
4040
4040
  };
4041
4041
  //#endregion
4042
- //#region ../../packages/protocol/dist/index.mjs
4042
+ //#region ../../packages/protocol/dist/uuid-kZvLA0Ly.mjs
4043
4043
  const LETTER_DASH_NUMBER$2 = "[a-zA-Z0-9-]";
4044
4044
  const NUMERIC_IDENTIFIER$2 = String.raw`0|[1-9]\d*`;
4045
4045
  const NUMERIC_IDENTIFIER_LOOSE$2 = String.raw`\d+`;
@@ -4146,128 +4146,6 @@ function canonicalizeAppVersion(appVersion) {
4146
4146
  const version = coerce$1(appVersion);
4147
4147
  return version ? normalizeFull(version) : null;
4148
4148
  }
4149
- const getManifestStorageUri = (bundle) => bundle.manifestStorageUri;
4150
- const getManifestFileHash = (bundle) => bundle.manifestFileHash;
4151
- const getAssetBaseStorageUri = (bundle) => bundle.assetBaseStorageUri;
4152
- const isBundlePatchArtifact = (value) => {
4153
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
4154
- const candidate = value;
4155
- return typeof candidate.baseBundleId === "string" && typeof candidate.baseFileHash === "string" && typeof candidate.patchFileHash === "string" && typeof candidate.patchStorageUri === "string";
4156
- };
4157
- const readBundlePatchArray = (patches) => {
4158
- if (!Array.isArray(patches)) return [];
4159
- return patches.filter(isBundlePatchArtifact);
4160
- };
4161
- const getBundlePatches = (bundle) => {
4162
- const patches = readBundlePatchArray(bundle.patches);
4163
- const seenBaseBundleIds = /* @__PURE__ */ new Set();
4164
- return patches.filter((patch) => {
4165
- if (seenBaseBundleIds.has(patch.baseBundleId)) return false;
4166
- seenBaseBundleIds.add(patch.baseBundleId);
4167
- return true;
4168
- });
4169
- };
4170
- const getBundlePatch = (bundle, baseBundleId) => {
4171
- return getBundlePatches(bundle).find((patch) => patch.baseBundleId === baseBundleId) ?? null;
4172
- };
4173
- const NUMERIC_COHORT_SIZE = 1e3;
4174
- const DEFAULT_ROLLOUT_COHORT_COUNT = NUMERIC_COHORT_SIZE;
4175
- const CUSTOM_COHORT_PATTERN = /^[a-z0-9-]+$/;
4176
- function parseNumericCohortValue(cohort) {
4177
- if (!/^\d+$/.test(cohort)) return null;
4178
- const parsed = Number.parseInt(cohort, 10);
4179
- if (Number.isNaN(parsed) || parsed < 1 || parsed > 1e3) return null;
4180
- return parsed;
4181
- }
4182
- function positiveMod(value, modulus) {
4183
- return (value % modulus + modulus) % modulus;
4184
- }
4185
- function hashString(value) {
4186
- let hash = 0;
4187
- for (let i = 0; i < value.length; i++) {
4188
- const char = value.charCodeAt(i);
4189
- hash = (hash << 5) - hash + char;
4190
- hash |= 0;
4191
- }
4192
- return hash;
4193
- }
4194
- function gcd(a, b) {
4195
- let x = Math.abs(a);
4196
- let y = Math.abs(b);
4197
- while (y !== 0) {
4198
- const next = x % y;
4199
- x = y;
4200
- y = next;
4201
- }
4202
- return x;
4203
- }
4204
- function modularInverse(value, modulus) {
4205
- let t = 0;
4206
- let newT = 1;
4207
- let r = modulus;
4208
- let newR = positiveMod(value, modulus);
4209
- while (newR !== 0) {
4210
- const quotient = Math.floor(r / newR);
4211
- [t, newT] = [newT, t - quotient * newT];
4212
- [r, newR] = [newR, r - quotient * newR];
4213
- }
4214
- if (r > 1) throw new Error(`No modular inverse for ${value} mod ${modulus}`);
4215
- return positiveMod(t, modulus);
4216
- }
4217
- function getRolloutShuffleParameters(bundleId) {
4218
- let multiplier = positiveMod(hashString(`${bundleId}:multiplier`), 997);
4219
- if (multiplier === 0) multiplier = 1;
4220
- while (gcd(multiplier, NUMERIC_COHORT_SIZE) !== 1) {
4221
- multiplier = positiveMod(multiplier + 1, NUMERIC_COHORT_SIZE);
4222
- if (multiplier === 0) multiplier = 1;
4223
- }
4224
- const offset = positiveMod(hashString(`${bundleId}:offset`), NUMERIC_COHORT_SIZE);
4225
- return {
4226
- multiplier,
4227
- offset,
4228
- inverseMultiplier: modularInverse(multiplier, NUMERIC_COHORT_SIZE)
4229
- };
4230
- }
4231
- function normalizeRolloutCohortCount(rolloutCohortCount) {
4232
- if (rolloutCohortCount === null || rolloutCohortCount === void 0) return DEFAULT_ROLLOUT_COHORT_COUNT;
4233
- if (rolloutCohortCount <= 0) return 0;
4234
- if (rolloutCohortCount >= 1e3) return NUMERIC_COHORT_SIZE;
4235
- return Math.floor(rolloutCohortCount);
4236
- }
4237
- function normalizeCohortValue(cohort) {
4238
- const normalized = cohort.trim().toLowerCase();
4239
- const numericCohort = parseNumericCohortValue(normalized);
4240
- if (numericCohort !== null) return String(numericCohort);
4241
- return normalized;
4242
- }
4243
- function getNumericCohortValue(cohort) {
4244
- return parseNumericCohortValue(normalizeCohortValue(cohort));
4245
- }
4246
- function isNumericCohort(cohort) {
4247
- return getNumericCohortValue(cohort) !== null;
4248
- }
4249
- function isCustomCohort(cohort) {
4250
- const normalized = normalizeCohortValue(cohort);
4251
- return normalized.length > 0 && normalized.length <= 64 && !/^\d+$/.test(normalized) && CUSTOM_COHORT_PATTERN.test(normalized);
4252
- }
4253
- function isValidCohort(cohort) {
4254
- const normalized = normalizeCohortValue(cohort);
4255
- return isNumericCohort(normalized) || isCustomCohort(normalized);
4256
- }
4257
- function getNumericCohortRolloutPosition(bundleId, cohortValue) {
4258
- if (cohortValue < 1 || cohortValue > 1e3) throw new Error(`Invalid numeric cohort: ${cohortValue}`);
4259
- const { offset, inverseMultiplier } = getRolloutShuffleParameters(bundleId);
4260
- return positiveMod(inverseMultiplier * (cohortValue - 1 - offset), NUMERIC_COHORT_SIZE);
4261
- }
4262
- function getRolledOutNumericCohorts(bundleId, rolloutCohortCount) {
4263
- const normalizedRolloutCount = normalizeRolloutCohortCount(rolloutCohortCount);
4264
- if (normalizedRolloutCount <= 0) return [];
4265
- if (normalizedRolloutCount >= 1e3) return Array.from({ length: NUMERIC_COHORT_SIZE }, (_, index) => index + 1);
4266
- const { multiplier, offset } = getRolloutShuffleParameters(bundleId);
4267
- return Array.from({ length: normalizedRolloutCount }, (_, position) => positiveMod(multiplier * position + offset, NUMERIC_COHORT_SIZE)).map((zeroBasedCohort) => zeroBasedCohort + 1).sort((left, right) => left - right);
4268
- }
4269
- const RELEASE_CATALOG_FALLBACK_POLICY = "BUILTIN_IF_ACTIVE_INELIGIBLE";
4270
- const MAX_COMPILED_CATALOG_BYTES = 262144;
4271
4149
  const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
4272
4150
  const MAX_CATALOG_SEGMENT_LENGTH = 255;
4273
4151
  function assertCatalogSegment(value, name) {
@@ -4392,6 +4270,130 @@ const NIL_UUID = "00000000-0000-0000-0000-000000000000";
4392
4270
  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}$/;
4393
4271
  const isUUIDv7 = (value) => typeof value === "string" && UUID_V7_PATTERN.test(value);
4394
4272
  //#endregion
4273
+ //#region ../../packages/protocol/dist/index.mjs
4274
+ const getManifestStorageUri = (bundle) => bundle.manifestStorageUri;
4275
+ const getManifestFileHash = (bundle) => bundle.manifestFileHash;
4276
+ const getAssetBaseStorageUri = (bundle) => bundle.assetBaseStorageUri;
4277
+ const isBundlePatchArtifact = (value) => {
4278
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
4279
+ const candidate = value;
4280
+ return typeof candidate.baseBundleId === "string" && typeof candidate.baseFileHash === "string" && typeof candidate.patchFileHash === "string" && typeof candidate.patchStorageUri === "string";
4281
+ };
4282
+ const readBundlePatchArray = (patches) => {
4283
+ if (!Array.isArray(patches)) return [];
4284
+ return patches.filter(isBundlePatchArtifact);
4285
+ };
4286
+ const getBundlePatches = (bundle) => {
4287
+ const patches = readBundlePatchArray(bundle.patches);
4288
+ const seenBaseBundleIds = /* @__PURE__ */ new Set();
4289
+ return patches.filter((patch) => {
4290
+ if (seenBaseBundleIds.has(patch.baseBundleId)) return false;
4291
+ seenBaseBundleIds.add(patch.baseBundleId);
4292
+ return true;
4293
+ });
4294
+ };
4295
+ const getBundlePatch = (bundle, baseBundleId) => {
4296
+ return getBundlePatches(bundle).find((patch) => patch.baseBundleId === baseBundleId) ?? null;
4297
+ };
4298
+ const NUMERIC_COHORT_SIZE = 1e3;
4299
+ const DEFAULT_ROLLOUT_COHORT_COUNT = NUMERIC_COHORT_SIZE;
4300
+ const CUSTOM_COHORT_PATTERN = /^[a-z0-9-]+$/;
4301
+ function parseNumericCohortValue(cohort) {
4302
+ if (!/^\d+$/.test(cohort)) return null;
4303
+ const parsed = Number.parseInt(cohort, 10);
4304
+ if (Number.isNaN(parsed) || parsed < 1 || parsed > 1e3) return null;
4305
+ return parsed;
4306
+ }
4307
+ function positiveMod(value, modulus) {
4308
+ return (value % modulus + modulus) % modulus;
4309
+ }
4310
+ function hashString(value) {
4311
+ let hash = 0;
4312
+ for (let i = 0; i < value.length; i++) {
4313
+ const char = value.charCodeAt(i);
4314
+ hash = (hash << 5) - hash + char;
4315
+ hash |= 0;
4316
+ }
4317
+ return hash;
4318
+ }
4319
+ function gcd(a, b) {
4320
+ let x = Math.abs(a);
4321
+ let y = Math.abs(b);
4322
+ while (y !== 0) {
4323
+ const next = x % y;
4324
+ x = y;
4325
+ y = next;
4326
+ }
4327
+ return x;
4328
+ }
4329
+ function modularInverse(value, modulus) {
4330
+ let t = 0;
4331
+ let newT = 1;
4332
+ let r = modulus;
4333
+ let newR = positiveMod(value, modulus);
4334
+ while (newR !== 0) {
4335
+ const quotient = Math.floor(r / newR);
4336
+ [t, newT] = [newT, t - quotient * newT];
4337
+ [r, newR] = [newR, r - quotient * newR];
4338
+ }
4339
+ if (r > 1) throw new Error(`No modular inverse for ${value} mod ${modulus}`);
4340
+ return positiveMod(t, modulus);
4341
+ }
4342
+ function getRolloutShuffleParameters(bundleId) {
4343
+ let multiplier = positiveMod(hashString(`${bundleId}:multiplier`), 997);
4344
+ if (multiplier === 0) multiplier = 1;
4345
+ while (gcd(multiplier, NUMERIC_COHORT_SIZE) !== 1) {
4346
+ multiplier = positiveMod(multiplier + 1, NUMERIC_COHORT_SIZE);
4347
+ if (multiplier === 0) multiplier = 1;
4348
+ }
4349
+ const offset = positiveMod(hashString(`${bundleId}:offset`), NUMERIC_COHORT_SIZE);
4350
+ return {
4351
+ multiplier,
4352
+ offset,
4353
+ inverseMultiplier: modularInverse(multiplier, NUMERIC_COHORT_SIZE)
4354
+ };
4355
+ }
4356
+ function normalizeRolloutCohortCount(rolloutCohortCount) {
4357
+ if (rolloutCohortCount === null || rolloutCohortCount === void 0) return DEFAULT_ROLLOUT_COHORT_COUNT;
4358
+ if (rolloutCohortCount <= 0) return 0;
4359
+ if (rolloutCohortCount >= 1e3) return NUMERIC_COHORT_SIZE;
4360
+ return Math.floor(rolloutCohortCount);
4361
+ }
4362
+ function normalizeCohortValue(cohort) {
4363
+ const normalized = cohort.trim().toLowerCase();
4364
+ const numericCohort = parseNumericCohortValue(normalized);
4365
+ if (numericCohort !== null) return String(numericCohort);
4366
+ return normalized;
4367
+ }
4368
+ function getNumericCohortValue(cohort) {
4369
+ return parseNumericCohortValue(normalizeCohortValue(cohort));
4370
+ }
4371
+ function isNumericCohort(cohort) {
4372
+ return getNumericCohortValue(cohort) !== null;
4373
+ }
4374
+ function isCustomCohort(cohort) {
4375
+ const normalized = normalizeCohortValue(cohort);
4376
+ return normalized.length > 0 && normalized.length <= 64 && !/^\d+$/.test(normalized) && CUSTOM_COHORT_PATTERN.test(normalized);
4377
+ }
4378
+ function isValidCohort(cohort) {
4379
+ const normalized = normalizeCohortValue(cohort);
4380
+ return isNumericCohort(normalized) || isCustomCohort(normalized);
4381
+ }
4382
+ function getNumericCohortRolloutPosition(bundleId, cohortValue) {
4383
+ if (cohortValue < 1 || cohortValue > 1e3) throw new Error(`Invalid numeric cohort: ${cohortValue}`);
4384
+ const { offset, inverseMultiplier } = getRolloutShuffleParameters(bundleId);
4385
+ return positiveMod(inverseMultiplier * (cohortValue - 1 - offset), NUMERIC_COHORT_SIZE);
4386
+ }
4387
+ function getRolledOutNumericCohorts(bundleId, rolloutCohortCount) {
4388
+ const normalizedRolloutCount = normalizeRolloutCohortCount(rolloutCohortCount);
4389
+ if (normalizedRolloutCount <= 0) return [];
4390
+ if (normalizedRolloutCount >= 1e3) return Array.from({ length: NUMERIC_COHORT_SIZE }, (_, index) => index + 1);
4391
+ const { multiplier, offset } = getRolloutShuffleParameters(bundleId);
4392
+ return Array.from({ length: normalizedRolloutCount }, (_, position) => positiveMod(multiplier * position + offset, NUMERIC_COHORT_SIZE)).map((zeroBasedCohort) => zeroBasedCohort + 1).sort((left, right) => left - right);
4393
+ }
4394
+ const RELEASE_CATALOG_FALLBACK_POLICY = "BUILTIN_IF_ACTIVE_INELIGIBLE";
4395
+ const MAX_COMPILED_CATALOG_BYTES = 262144;
4396
+ //#endregion
4395
4397
  //#region ../plugin-core/dist/databaseRows.mjs
4396
4398
  const releaseRowToRelease = (row) => ({
4397
4399
  id: row.id,
@@ -5631,10 +5633,11 @@ const SETTINGS_TABLE = {
5631
5633
  const ENGINE_SCHEMA_KEY = "schema.engine";
5632
5634
  const CORE_SCHEMA_KEY = "schema.core";
5633
5635
  const stored = ({ key, expected, found }) => `"${key}" is ${found === null ? "missing" : `"${found}"`}; expected "${expected}"`;
5634
- 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\`.`;
5636
+ 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.";
5637
+ const settingMessage = (adapterName, { key, expected, found }) => `Hot Updater schema setting "${key}" for ${adapterName} is ${found === null ? "missing" : `"${found}"`}; expected "${expected}". ${MIGRATE_SCHEMA_FIX}`;
5635
5638
  const pluginOf = ({ key }) => key.startsWith("schema.") && key !== "schema.engine" && key !== CORE_SCHEMA_KEY ? key.slice(7) : void 0;
5636
5639
  const listed = (items) => items.length < 2 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
5637
- 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.";
5640
+ const settingsMessage = (adapterName, settings) => `Hot Updater schema settings for ${adapterName} are not migrated: ${settings.map(stored).join(", and ")}. ${MIGRATE_SCHEMA_FIX}`;
5638
5641
  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}`;
5639
5642
  var HotUpdaterSchemaMigrationRequiredError = class extends Error {
5640
5643
  adapterName;
@@ -5645,7 +5648,7 @@ var HotUpdaterSchemaMigrationRequiredError = class extends Error {
5645
5648
  constructor(adapterName, currentVersion, setting, options) {
5646
5649
  const settings = options?.settings ?? (setting === void 0 ? [] : [setting]);
5647
5650
  const plugins = settings.flatMap((mismatch) => pluginOf(mismatch) ?? []);
5648
- 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 });
5651
+ 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 });
5649
5652
  this.adapterName = adapterName;
5650
5653
  this.currentVersion = currentVersion;
5651
5654
  this.setting = setting;
@@ -5698,11 +5701,23 @@ const checkSchemaFence = async (adapter, adapterName, expected, fix) => {
5698
5701
  });
5699
5702
  };
5700
5703
  const FENCED = Symbol.for("@hot-updater/server/schema-fence");
5704
+ const FENCED_SETTINGS = Symbol.for("@hot-updater/server/schema-fence-settings");
5701
5705
  const fencedName = (adapter) => adapter[FENCED];
5702
5706
  const withSchemaFence = (adapter, adapterName, expected, fix) => {
5707
+ const inner = adapter[FENCED_SETTINGS];
5708
+ const settings = inner === void 0 ? {
5709
+ adapter,
5710
+ expected
5711
+ } : {
5712
+ adapter: inner.adapter,
5713
+ expected: {
5714
+ ...inner.expected,
5715
+ ...expected
5716
+ }
5717
+ };
5703
5718
  let ready;
5704
5719
  const fence = () => {
5705
- ready ??= checkSchemaFence(adapter, adapterName, expected, fix).catch((error) => {
5720
+ ready ??= checkSchemaFence(settings.adapter, adapterName, settings.expected, fix).catch((error) => {
5706
5721
  ready = void 0;
5707
5722
  throw error;
5708
5723
  });
@@ -5711,6 +5726,7 @@ const withSchemaFence = (adapter, adapterName, expected, fix) => {
5711
5726
  return {
5712
5727
  ...adapter,
5713
5728
  [FENCED]: adapterName,
5729
+ [FENCED_SETTINGS]: settings,
5714
5730
  get: (table, keys) => fence().then(() => adapter.get(table, keys)),
5715
5731
  query: (table, request) => fence().then(() => adapter.query(table, request)),
5716
5732
  write: (ops) => fence().then(() => adapter.write(ops)),
@@ -5765,257 +5781,360 @@ const CHANNEL_ID = {
5765
5781
  maxLength: 1408,
5766
5782
  ascii: true
5767
5783
  };
5768
- const HOT_UPDATER_SCHEMA_VERSION = "1.0.0";
5784
+ const HOT_UPDATER_SCHEMA_VERSION = "1.2.0";
5785
+ const bundles = defineTable({
5786
+ id: {
5787
+ type: "string",
5788
+ maxLength: 36
5789
+ },
5790
+ platform: {
5791
+ type: "string",
5792
+ maxLength: 16
5793
+ },
5794
+ git_commit_hash: {
5795
+ type: "string",
5796
+ maxLength: 255,
5797
+ required: false
5798
+ },
5799
+ metadata: { type: "json" },
5800
+ manifest_storage_uri: { type: "string" },
5801
+ manifest_file_hash: { type: "string" },
5802
+ asset_base_storage_uri: { type: "string" }
5803
+ }, {
5804
+ key: ["id"],
5805
+ indexes: {
5806
+ byPlatform: {
5807
+ eq: ["platform"],
5808
+ sort: ["id"]
5809
+ },
5810
+ all: {
5811
+ eq: [],
5812
+ sort: ["id"]
5813
+ }
5814
+ }
5815
+ });
5816
+ const bundlePatches = defineTable({
5817
+ id: {
5818
+ type: "string",
5819
+ maxLength: 255
5820
+ },
5821
+ bundle_id: {
5822
+ type: "string",
5823
+ maxLength: 36,
5824
+ references: {
5825
+ model: "bundles",
5826
+ onDelete: "cascade"
5827
+ }
5828
+ },
5829
+ base_bundle_id: {
5830
+ type: "string",
5831
+ maxLength: 36,
5832
+ references: {
5833
+ model: "bundles",
5834
+ onDelete: "cascade"
5835
+ }
5836
+ },
5837
+ base_file_hash: { type: "string" },
5838
+ patch_file_hash: { type: "string" },
5839
+ patch_storage_uri: { type: "string" },
5840
+ byte_size: { type: "integer" },
5841
+ order_index: { type: "integer" }
5842
+ }, {
5843
+ key: ["id"],
5844
+ indexes: {
5845
+ pair: {
5846
+ eq: ["bundle_id", "base_bundle_id"],
5847
+ sort: [],
5848
+ unique: true
5849
+ },
5850
+ byBundle: {
5851
+ eq: ["bundle_id"],
5852
+ sort: ["order_index"],
5853
+ root: { model: "bundles" }
5854
+ },
5855
+ byBase: {
5856
+ eq: ["base_bundle_id"],
5857
+ sort: ["bundle_id"]
5858
+ }
5859
+ }
5860
+ });
5861
+ const releases = defineTable({
5862
+ id: {
5863
+ type: "string",
5864
+ maxLength: 36
5865
+ },
5866
+ revision: { type: "integer" },
5867
+ scope_key: {
5868
+ type: "string",
5869
+ maxLength: 2048,
5870
+ ascii: true
5871
+ },
5872
+ channel_id: {
5873
+ ...CHANNEL_ID,
5874
+ references: {
5875
+ model: "channels",
5876
+ onDelete: "restrict"
5877
+ }
5878
+ },
5879
+ platform: {
5880
+ type: "string",
5881
+ maxLength: 16
5882
+ },
5883
+ kind: {
5884
+ type: "string",
5885
+ maxLength: 16
5886
+ },
5887
+ bundle_id: {
5888
+ type: "string",
5889
+ maxLength: 36,
5890
+ required: false,
5891
+ references: {
5892
+ model: "bundles",
5893
+ onDelete: "restrict"
5894
+ }
5895
+ },
5896
+ strategy: {
5897
+ type: "string",
5898
+ maxLength: 16
5899
+ },
5900
+ target_app_version: {
5901
+ type: "string",
5902
+ required: false
5903
+ },
5904
+ fingerprint_hash: {
5905
+ type: "string",
5906
+ maxLength: 255,
5907
+ required: false
5908
+ },
5909
+ enabled: { type: "boolean" },
5910
+ should_force_update: { type: "boolean" },
5911
+ message: {
5912
+ type: "string",
5913
+ required: false
5914
+ },
5915
+ rollout_cohort_count: { type: "integer" },
5916
+ target_cohorts: { type: "json" },
5917
+ operation: {
5918
+ type: "string",
5919
+ maxLength: 16
5920
+ },
5921
+ source_release_id: {
5922
+ type: "string",
5923
+ maxLength: 36,
5924
+ required: false,
5925
+ references: {
5926
+ model: "releases",
5927
+ onDelete: "none"
5928
+ }
5929
+ },
5930
+ created_at_ms: { type: "integer" },
5931
+ updated_at_ms: { type: "integer" }
5932
+ }, {
5933
+ key: ["id"],
5934
+ indexes: {
5935
+ byScope: {
5936
+ eq: ["scope_key"],
5937
+ sort: ["id"],
5938
+ root: { model: "release_catalogs" }
5939
+ },
5940
+ byScopeEnabled: {
5941
+ eq: ["scope_key", "enabled"],
5942
+ sort: ["id"],
5943
+ root: { model: "release_catalogs" }
5944
+ },
5945
+ byChannelPlatform: {
5946
+ eq: ["channel_id", "platform"],
5947
+ sort: ["id"]
5948
+ },
5949
+ byChannelPlatformEnabled: {
5950
+ eq: [
5951
+ "channel_id",
5952
+ "platform",
5953
+ "enabled"
5954
+ ],
5955
+ sort: ["id"]
5956
+ },
5957
+ byBundle: {
5958
+ eq: ["bundle_id"],
5959
+ sort: ["id"]
5960
+ },
5961
+ all: {
5962
+ eq: [],
5963
+ sort: ["id"]
5964
+ }
5965
+ }
5966
+ });
5967
+ const releaseCatalogs = defineTable({
5968
+ scope_key: {
5969
+ type: "string",
5970
+ maxLength: 2048,
5971
+ ascii: true
5972
+ },
5973
+ catalog_id: {
5974
+ type: "string",
5975
+ maxLength: 255
5976
+ },
5977
+ strategy: {
5978
+ type: "string",
5979
+ maxLength: 16
5980
+ },
5981
+ channel_id: {
5982
+ ...CHANNEL_ID,
5983
+ references: {
5984
+ model: "channels",
5985
+ onDelete: "none"
5986
+ }
5987
+ },
5988
+ channel_key: {
5989
+ type: "string",
5990
+ maxLength: 1400,
5991
+ ascii: true
5992
+ },
5993
+ platform: {
5994
+ type: "string",
5995
+ maxLength: 16
5996
+ },
5997
+ fingerprint_hash: {
5998
+ type: "string",
5999
+ maxLength: 255,
6000
+ required: false
6001
+ },
6002
+ generation: { type: "integer" },
6003
+ payload: { type: "string" },
6004
+ catalog_hash: {
6005
+ type: "string",
6006
+ maxLength: 71
6007
+ },
6008
+ byte_size: { type: "integer" },
6009
+ is_tombstone: { type: "boolean" },
6010
+ updated_at_ms: { type: "integer" }
6011
+ }, {
6012
+ key: ["scope_key"],
6013
+ indexes: { all: {
6014
+ eq: [],
6015
+ sort: ["scope_key"]
6016
+ } }
6017
+ });
6018
+ const channels = defineTable({
6019
+ id: CHANNEL_ID,
6020
+ name: {
6021
+ type: "string",
6022
+ maxLength: 255,
6023
+ unique: true
6024
+ }
6025
+ }, {
6026
+ key: ["id"],
6027
+ indexes: { all: {
6028
+ eq: [],
6029
+ sort: ["name"]
6030
+ } }
6031
+ });
6032
+ const nativeBuilds = defineTable({
6033
+ id: {
6034
+ type: "string",
6035
+ maxLength: 36
6036
+ },
6037
+ scope_key: {
6038
+ type: "string",
6039
+ maxLength: 2048,
6040
+ ascii: true
6041
+ },
6042
+ platform: {
6043
+ type: "string",
6044
+ maxLength: 16
6045
+ },
6046
+ channel_id: {
6047
+ ...CHANNEL_ID,
6048
+ references: {
6049
+ model: "channels",
6050
+ onDelete: "none"
6051
+ }
6052
+ },
6053
+ fingerprint_hash: {
6054
+ type: "string",
6055
+ maxLength: 255,
6056
+ required: false
6057
+ },
6058
+ app_version: {
6059
+ type: "string",
6060
+ required: false
6061
+ },
6062
+ min_bundle_id: {
6063
+ type: "string",
6064
+ maxLength: 36,
6065
+ unique: true,
6066
+ references: {
6067
+ model: "bundles",
6068
+ onDelete: "none"
6069
+ }
6070
+ },
6071
+ artifact_kind: {
6072
+ type: "string",
6073
+ maxLength: 16
6074
+ },
6075
+ artifact_storage_uri: { type: "string" },
6076
+ byte_size: { type: "integer" },
6077
+ git_commit_hash: {
6078
+ type: "string",
6079
+ maxLength: 255,
6080
+ required: false
6081
+ },
6082
+ created_at_ms: { type: "integer" },
6083
+ build_key: {
6084
+ type: "string",
6085
+ maxLength: 69,
6086
+ ascii: true,
6087
+ required: false
6088
+ },
6089
+ build_number: {
6090
+ type: "string",
6091
+ maxLength: 64,
6092
+ required: false
6093
+ },
6094
+ submitted_to: {
6095
+ type: "string",
6096
+ maxLength: 16,
6097
+ required: false
6098
+ },
6099
+ submitted_at_ms: {
6100
+ type: "integer",
6101
+ required: false
6102
+ },
6103
+ submitted_version: {
6104
+ type: "string",
6105
+ maxLength: 255,
6106
+ required: false
6107
+ }
6108
+ }, {
6109
+ key: ["id"],
6110
+ indexes: {
6111
+ byScope: {
6112
+ eq: ["scope_key"],
6113
+ sort: ["id"]
6114
+ },
6115
+ byBuildKey: {
6116
+ eq: ["scope_key", "build_key"],
6117
+ sort: ["id"]
6118
+ },
6119
+ byChannel: {
6120
+ eq: ["channel_id"],
6121
+ sort: ["id"],
6122
+ root: { model: "channels" }
6123
+ },
6124
+ all: {
6125
+ eq: [],
6126
+ sort: ["id"]
6127
+ }
6128
+ }
6129
+ });
5769
6130
  const coreModule = {
5770
6131
  id: "core",
5771
6132
  schema: {
5772
- bundles: defineTable({
5773
- id: {
5774
- type: "string",
5775
- maxLength: 36
5776
- },
5777
- platform: {
5778
- type: "string",
5779
- maxLength: 16
5780
- },
5781
- git_commit_hash: {
5782
- type: "string",
5783
- maxLength: 255,
5784
- required: false
5785
- },
5786
- metadata: { type: "json" },
5787
- manifest_storage_uri: { type: "string" },
5788
- manifest_file_hash: { type: "string" },
5789
- asset_base_storage_uri: { type: "string" }
5790
- }, {
5791
- key: ["id"],
5792
- indexes: {
5793
- byPlatform: {
5794
- eq: ["platform"],
5795
- sort: ["id"]
5796
- },
5797
- all: {
5798
- eq: [],
5799
- sort: ["id"]
5800
- }
5801
- }
5802
- }),
5803
- bundle_patches: defineTable({
5804
- id: {
5805
- type: "string",
5806
- maxLength: 255
5807
- },
5808
- bundle_id: {
5809
- type: "string",
5810
- maxLength: 36,
5811
- references: {
5812
- model: "bundles",
5813
- onDelete: "cascade"
5814
- }
5815
- },
5816
- base_bundle_id: {
5817
- type: "string",
5818
- maxLength: 36,
5819
- references: {
5820
- model: "bundles",
5821
- onDelete: "cascade"
5822
- }
5823
- },
5824
- base_file_hash: { type: "string" },
5825
- patch_file_hash: { type: "string" },
5826
- patch_storage_uri: { type: "string" },
5827
- byte_size: { type: "integer" },
5828
- order_index: { type: "integer" }
5829
- }, {
5830
- key: ["id"],
5831
- indexes: {
5832
- pair: {
5833
- eq: ["bundle_id", "base_bundle_id"],
5834
- sort: [],
5835
- unique: true
5836
- },
5837
- byBundle: {
5838
- eq: ["bundle_id"],
5839
- sort: ["order_index"],
5840
- root: { model: "bundles" }
5841
- },
5842
- byBase: {
5843
- eq: ["base_bundle_id"],
5844
- sort: ["bundle_id"]
5845
- }
5846
- }
5847
- }),
5848
- releases: defineTable({
5849
- id: {
5850
- type: "string",
5851
- maxLength: 36
5852
- },
5853
- revision: { type: "integer" },
5854
- scope_key: {
5855
- type: "string",
5856
- maxLength: 2048,
5857
- ascii: true
5858
- },
5859
- channel_id: {
5860
- ...CHANNEL_ID,
5861
- references: {
5862
- model: "channels",
5863
- onDelete: "restrict"
5864
- }
5865
- },
5866
- platform: {
5867
- type: "string",
5868
- maxLength: 16
5869
- },
5870
- kind: {
5871
- type: "string",
5872
- maxLength: 16
5873
- },
5874
- bundle_id: {
5875
- type: "string",
5876
- maxLength: 36,
5877
- required: false,
5878
- references: {
5879
- model: "bundles",
5880
- onDelete: "restrict"
5881
- }
5882
- },
5883
- strategy: {
5884
- type: "string",
5885
- maxLength: 16
5886
- },
5887
- target_app_version: {
5888
- type: "string",
5889
- required: false
5890
- },
5891
- fingerprint_hash: {
5892
- type: "string",
5893
- maxLength: 255,
5894
- required: false
5895
- },
5896
- enabled: { type: "boolean" },
5897
- should_force_update: { type: "boolean" },
5898
- message: {
5899
- type: "string",
5900
- required: false
5901
- },
5902
- rollout_cohort_count: { type: "integer" },
5903
- target_cohorts: { type: "json" },
5904
- operation: {
5905
- type: "string",
5906
- maxLength: 16
5907
- },
5908
- source_release_id: {
5909
- type: "string",
5910
- maxLength: 36,
5911
- required: false,
5912
- references: {
5913
- model: "releases",
5914
- onDelete: "none"
5915
- }
5916
- },
5917
- created_at_ms: { type: "integer" },
5918
- updated_at_ms: { type: "integer" }
5919
- }, {
5920
- key: ["id"],
5921
- indexes: {
5922
- byScope: {
5923
- eq: ["scope_key"],
5924
- sort: ["id"],
5925
- root: { model: "release_catalogs" }
5926
- },
5927
- byScopeEnabled: {
5928
- eq: ["scope_key", "enabled"],
5929
- sort: ["id"],
5930
- root: { model: "release_catalogs" }
5931
- },
5932
- byChannelPlatform: {
5933
- eq: ["channel_id", "platform"],
5934
- sort: ["id"]
5935
- },
5936
- byChannelPlatformEnabled: {
5937
- eq: [
5938
- "channel_id",
5939
- "platform",
5940
- "enabled"
5941
- ],
5942
- sort: ["id"]
5943
- },
5944
- byBundle: {
5945
- eq: ["bundle_id"],
5946
- sort: ["id"]
5947
- },
5948
- all: {
5949
- eq: [],
5950
- sort: ["id"]
5951
- }
5952
- }
5953
- }),
5954
- release_catalogs: defineTable({
5955
- scope_key: {
5956
- type: "string",
5957
- maxLength: 2048,
5958
- ascii: true
5959
- },
5960
- catalog_id: {
5961
- type: "string",
5962
- maxLength: 255
5963
- },
5964
- strategy: {
5965
- type: "string",
5966
- maxLength: 16
5967
- },
5968
- channel_id: {
5969
- ...CHANNEL_ID,
5970
- references: {
5971
- model: "channels",
5972
- onDelete: "none"
5973
- }
5974
- },
5975
- channel_key: {
5976
- type: "string",
5977
- maxLength: 1400,
5978
- ascii: true
5979
- },
5980
- platform: {
5981
- type: "string",
5982
- maxLength: 16
5983
- },
5984
- fingerprint_hash: {
5985
- type: "string",
5986
- maxLength: 255,
5987
- required: false
5988
- },
5989
- generation: { type: "integer" },
5990
- payload: { type: "string" },
5991
- catalog_hash: {
5992
- type: "string",
5993
- maxLength: 71
5994
- },
5995
- byte_size: { type: "integer" },
5996
- is_tombstone: { type: "boolean" },
5997
- updated_at_ms: { type: "integer" }
5998
- }, {
5999
- key: ["scope_key"],
6000
- indexes: { all: {
6001
- eq: [],
6002
- sort: ["scope_key"]
6003
- } }
6004
- }),
6005
- channels: defineTable({
6006
- id: CHANNEL_ID,
6007
- name: {
6008
- type: "string",
6009
- maxLength: 255,
6010
- unique: true
6011
- }
6012
- }, {
6013
- key: ["id"],
6014
- indexes: { all: {
6015
- eq: [],
6016
- sort: ["name"]
6017
- } }
6018
- }),
6133
+ bundles,
6134
+ bundle_patches: bundlePatches,
6135
+ releases,
6136
+ release_catalogs: releaseCatalogs,
6137
+ channels,
6019
6138
  bundle_totals: defineAggregate({ platform_key: {
6020
6139
  type: "string",
6021
6140
  maxLength: 16
@@ -6026,7 +6145,8 @@ const coreModule = {
6026
6145
  eq: ["platform_key"],
6027
6146
  sort: []
6028
6147
  } }
6029
- })
6148
+ }),
6149
+ native_builds: nativeBuilds
6030
6150
  }
6031
6151
  };
6032
6152
  //#endregion
@@ -6995,14 +7115,14 @@ const createEngineMigrator = (options) => {
6995
7115
  return {
6996
7116
  getVersion,
6997
7117
  getNameVariants: async () => void 0,
6998
- next: async () => await getVersion() === "1.0.0" ? void 0 : { version: HOT_UPDATER_SCHEMA_VERSION },
7118
+ next: async () => await getVersion() === "1.2.0" ? void 0 : { version: HOT_UPDATER_SCHEMA_VERSION },
6999
7119
  previous: async () => void 0,
7000
7120
  up: makeResult,
7001
7121
  down: async () => {
7002
7122
  throw new Error("No previous schema to migrate to.");
7003
7123
  },
7004
7124
  migrateTo: async (version, migrate) => {
7005
- if (version !== "1.0.0") throw new Error(`Invalid version ${version}`);
7125
+ if (version !== "1.2.0") throw new Error(`Invalid version ${version}`);
7006
7126
  return makeResult(migrate);
7007
7127
  },
7008
7128
  migrateToLatest: makeResult
@@ -7697,9 +7817,9 @@ const createDatabaseEngine = ({ batching, now = Date.now, meter, ...options }) =
7697
7817
  //#region ../plugin-core/dist/engine/createEngine.mjs
7698
7818
  const pluginTablesFix = (database) => {
7699
7819
  const { createMigrator, generateSchema } = database;
7700
- const managed = "A managed server runs only its provider's plugins, and rerunning `hot-updater init --provider <provider>` applies the provider's migrations.";
7820
+ 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.";
7701
7821
  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.";
7702
- 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;
7822
+ 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.`;
7703
7823
  };
7704
7824
  const createEngine = (database, { plugins = [], now = Date.now } = {}) => {
7705
7825
  const modules = /* @__PURE__ */ new Map();
@@ -7998,7 +8118,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
7998
8118
  };
7999
8119
  //#endregion
8000
8120
  //#region ../../packages/server/dist/version.mjs
8001
- const HOT_UPDATER_SERVER_VERSION = "1.1.4";
8121
+ const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.1";
8002
8122
  //#endregion
8003
8123
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
8004
8124
  const createVersionRouteHandlers = (plugins = []) => {
@@ -8731,8 +8851,8 @@ const baseBundleIdsOf = (row, range, bundleId, limit) => {
8731
8851
  //#endregion
8732
8852
  //#region ../../packages/server/dist/core/reads.mjs
8733
8853
  const PAGE$3 = 500;
8734
- const pick = (fields) => (row) => Object.fromEntries(fields.map((field) => [field, row[field]]));
8735
- const toBundleRow = pick([
8854
+ const pick$1 = (fields) => (row) => Object.fromEntries(fields.map((field) => [field, row[field]]));
8855
+ const toBundleRow = pick$1([
8736
8856
  "id",
8737
8857
  "platform",
8738
8858
  "git_commit_hash",
@@ -8741,7 +8861,7 @@ const toBundleRow = pick([
8741
8861
  "manifest_file_hash",
8742
8862
  "asset_base_storage_uri"
8743
8863
  ]);
8744
- const toPatchRow = pick([
8864
+ const toPatchRow = pick$1([
8745
8865
  "id",
8746
8866
  "bundle_id",
8747
8867
  "base_bundle_id",
@@ -8751,7 +8871,7 @@ const toPatchRow = pick([
8751
8871
  "byte_size",
8752
8872
  "order_index"
8753
8873
  ]);
8754
- const toReleaseRow = pick([
8874
+ const toReleaseRow = pick$1([
8755
8875
  "id",
8756
8876
  "revision",
8757
8877
  "scope_key",
@@ -8772,7 +8892,7 @@ const toReleaseRow = pick([
8772
8892
  "created_at_ms",
8773
8893
  "updated_at_ms"
8774
8894
  ]);
8775
- const toCatalogRow = pick([
8895
+ const toCatalogRow = pick$1([
8776
8896
  "scope_key",
8777
8897
  "catalog_id",
8778
8898
  "strategy",
@@ -8787,7 +8907,7 @@ const toCatalogRow = pick([
8787
8907
  "is_tombstone",
8788
8908
  "updated_at_ms"
8789
8909
  ]);
8790
- const toChannelRow = pick(["id", "name"]);
8910
+ const toChannelRow = pick$1(["id", "name"]);
8791
8911
  const drain$1 = async (read) => {
8792
8912
  const rows = [];
8793
8913
  let cursor;
@@ -8798,6 +8918,7 @@ const drain$1 = async (read) => {
8798
8918
  } while (cursor !== void 0);
8799
8919
  return rows;
8800
8920
  };
8921
+ const after = (input) => input.after === void 0 ? {} : { range: (input.order ?? "asc") === "asc" ? { gt: input.after } : { lt: input.after } };
8801
8922
  const createCoreReads = (db, storage) => {
8802
8923
  const detail = async (row) => {
8803
8924
  const count = row._refs_bundle_patches_bundle_id ?? 0;
@@ -8820,7 +8941,6 @@ const createCoreReads = (db, storage) => {
8820
8941
  childCount: row._refs_bundle_patches_base_bundle_id ?? 0
8821
8942
  };
8822
8943
  };
8823
- const after = (input) => input.after === void 0 ? {} : { range: (input.order ?? "asc") === "asc" ? { gt: input.after } : { lt: input.after } };
8824
8944
  return {
8825
8945
  async getReleaseCatalog(input) {
8826
8946
  const scopeKey = releaseCatalogScopeKeyOf(input);
@@ -8998,9 +9118,9 @@ const insertBundle = (tx, row) => {
8998
9118
  tx.create("bundles", row);
8999
9119
  countBundle(tx, row.platform, 1);
9000
9120
  };
9001
- const updateBundle = (tx, current, set) => {
9121
+ const updateBundle = (tx, current, set, { counted = true } = {}) => {
9002
9122
  tx.update("bundles", current, set);
9003
- if (set.platform !== void 0 && set.platform !== current.platform) {
9123
+ if (counted && set.platform !== void 0 && set.platform !== current.platform) {
9004
9124
  countBundle(tx, current.platform, -1);
9005
9125
  countBundle(tx, set.platform, 1);
9006
9126
  }
@@ -9065,10 +9185,223 @@ const deleteChannel = (db, id) => db.transaction(async (tx) => {
9065
9185
  deleted: false,
9066
9186
  reason: "not_empty"
9067
9187
  };
9188
+ const { rows: builds } = await tx.findMany("native_builds", {
9189
+ index: "byChannel",
9190
+ where: { channel_id: id },
9191
+ limit: 1
9192
+ });
9193
+ if (builds.length > 0) return {
9194
+ deleted: false,
9195
+ reason: "not_empty"
9196
+ };
9068
9197
  await tx.delete("channels", row);
9069
9198
  return { deleted: true };
9070
9199
  });
9071
9200
  //#endregion
9201
+ //#region ../../packages/server/dist/core/nativeBuilds.mjs
9202
+ const ARTIFACT_KINDS = {
9203
+ android: ["apk", "aab"],
9204
+ ios: ["ipa", "app"]
9205
+ };
9206
+ const SCOPE_KEY_MAX_LENGTH = 2048;
9207
+ const GIT_COMMIT_HASH_MAX_LENGTH = 255;
9208
+ const BUILD_KEY = /^nbk1:[0-9a-f]{64}$/;
9209
+ var NativeBuildInputError = class extends TypeError {
9210
+ name = "NativeBuildInputError";
9211
+ };
9212
+ const pick = (row) => ({
9213
+ id: row.id,
9214
+ scope_key: row.scope_key,
9215
+ platform: row.platform,
9216
+ channel_id: row.channel_id,
9217
+ fingerprint_hash: row.fingerprint_hash ?? null,
9218
+ app_version: row.app_version ?? null,
9219
+ min_bundle_id: row.min_bundle_id,
9220
+ artifact_kind: row.artifact_kind,
9221
+ artifact_storage_uri: row.artifact_storage_uri,
9222
+ byte_size: row.byte_size,
9223
+ git_commit_hash: row.git_commit_hash ?? null,
9224
+ created_at_ms: row.created_at_ms,
9225
+ build_key: row.build_key ?? null,
9226
+ build_number: row.build_number ?? null,
9227
+ submitted_to: row.submitted_to ?? null,
9228
+ submitted_at_ms: row.submitted_at_ms ?? null,
9229
+ submitted_version: row.submitted_version ?? null
9230
+ });
9231
+ const SUBMISSION_DESTINATIONS = {
9232
+ android: "play-internal",
9233
+ ios: "testflight"
9234
+ };
9235
+ const BUILD_NUMBER_MAX_LENGTH = 64;
9236
+ const SUBMITTED_VERSION_MAX_LENGTH = 255;
9237
+ const nativeBuildScopeKey = (scope) => {
9238
+ let scopeKey;
9239
+ try {
9240
+ const channelKey = encodeChannelKey(scope.channel);
9241
+ scopeKey = scope.fingerprintHash === null ? `${createReleaseCatalogScopeKey({
9242
+ channelKey,
9243
+ platform: scope.platform,
9244
+ strategy: "APP_VERSION"
9245
+ })}:${encodeURIComponent(scope.appVersion)}` : createReleaseCatalogScopeKey({
9246
+ channelKey,
9247
+ fingerprintHash: scope.fingerprintHash,
9248
+ platform: scope.platform,
9249
+ strategy: "FINGERPRINT"
9250
+ });
9251
+ } catch (error) {
9252
+ throw new NativeBuildInputError(`Invalid native build scope: ${error instanceof Error ? error.message : String(error)}.`);
9253
+ }
9254
+ 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.`);
9255
+ return scopeKey;
9256
+ };
9257
+ const isBuiltInBundle = async (db, bundleId) => await db.findOne("native_builds", { min_bundle_id: bundleId }) !== null;
9258
+ const problemOf = ({ id, minBundleId, platform, channel, fingerprintHash, appVersion, gitCommitHash, buildKey, buildNumber, artifact }) => {
9259
+ if (!isUUIDv7(id)) return "id must be a UUIDv7";
9260
+ if (!isUUIDv7(minBundleId)) return "minBundleId must be the UUIDv7 a release build compiles in";
9261
+ if (platform !== "ios" && platform !== "android") return "platform must be ios or android";
9262
+ if (channel.trim().length === 0) return "channel is required";
9263
+ if (gitCommitHash !== null && gitCommitHash.length > GIT_COMMIT_HASH_MAX_LENGTH) return `gitCommitHash must be at most ${GIT_COMMIT_HASH_MAX_LENGTH} characters`;
9264
+ if (fingerprintHash === null === (appVersion === null)) return "exactly one of fingerprintHash and appVersion is required";
9265
+ if (buildKey !== null && !BUILD_KEY.test(buildKey)) return "buildKey must be nbk1: and a SHA-256, or null";
9266
+ 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`;
9267
+ const kinds = ARTIFACT_KINDS[platform];
9268
+ if (!kinds.includes(artifact.kind)) return `artifact.kind of an ${platform} build must be ${kinds.join(" or ")}`;
9269
+ if (!Number.isSafeInteger(artifact.byteSize) || artifact.byteSize < 0) return "artifact.byteSize must be a byte count";
9270
+ return null;
9271
+ };
9272
+ const createNativeBuildApi = (db, { now = Date.now } = {}) => ({
9273
+ async registerNativeBuild(registration) {
9274
+ const problem = problemOf(registration);
9275
+ if (problem !== null) throw new NativeBuildInputError(`Invalid native build: ${problem}.`);
9276
+ const scopeKey = nativeBuildScopeKey(registration);
9277
+ const channelId = `channel:${encodeChannelKey(registration.channel)}`;
9278
+ const { embeddedBundle } = registration;
9279
+ const row = {
9280
+ id: registration.id,
9281
+ scope_key: scopeKey,
9282
+ platform: registration.platform,
9283
+ channel_id: channelId,
9284
+ fingerprint_hash: registration.fingerprintHash,
9285
+ app_version: registration.appVersion,
9286
+ min_bundle_id: registration.minBundleId,
9287
+ artifact_kind: registration.artifact.kind,
9288
+ artifact_storage_uri: registration.artifact.storageUri,
9289
+ byte_size: registration.artifact.byteSize,
9290
+ git_commit_hash: registration.gitCommitHash,
9291
+ created_at_ms: now(),
9292
+ build_key: registration.buildKey,
9293
+ build_number: registration.buildNumber ?? null,
9294
+ submitted_to: null,
9295
+ submitted_at_ms: null,
9296
+ submitted_version: null
9297
+ };
9298
+ for (let attempt = 1;; attempt += 1) {
9299
+ await insertChannel(db, {
9300
+ id: channelId,
9301
+ name: registration.channel
9302
+ });
9303
+ try {
9304
+ await db.transaction(async (tx) => {
9305
+ tx.create("bundles", {
9306
+ id: registration.minBundleId,
9307
+ platform: registration.platform,
9308
+ git_commit_hash: registration.gitCommitHash,
9309
+ metadata: embeddedBundle.metadata ?? {},
9310
+ manifest_storage_uri: embeddedBundle.manifestStorageUri,
9311
+ manifest_file_hash: embeddedBundle.manifestFileHash,
9312
+ asset_base_storage_uri: embeddedBundle.assetBaseStorageUri
9313
+ });
9314
+ tx.create("native_builds", row);
9315
+ });
9316
+ break;
9317
+ } catch (error) {
9318
+ if (!(error instanceof DatabaseConstraintError)) throw error;
9319
+ if (error.reason === "exists" || error.reason === "unique") throw new DatabaseConstraintError(error.reason, "native_builds");
9320
+ if (attempt < 3 && error.reason === "not_found" && error.model === "channels") continue;
9321
+ throw error;
9322
+ }
9323
+ }
9324
+ return row;
9325
+ },
9326
+ async getNativeBuild(id) {
9327
+ const row = await db.findOne("native_builds", { id });
9328
+ return row === null ? null : pick(row);
9329
+ },
9330
+ async findNativeBuild(scope, options) {
9331
+ const scopeKey = nativeBuildScopeKey(scope);
9332
+ const buildKey = options?.buildKey;
9333
+ if (buildKey !== void 0 && !BUILD_KEY.test(buildKey)) throw new NativeBuildInputError("Invalid native build key: it must be nbk1: and a SHA-256.");
9334
+ const { rows } = buildKey === void 0 ? await db.findMany("native_builds", {
9335
+ index: "byScope",
9336
+ where: { scope_key: scopeKey },
9337
+ order: "desc",
9338
+ limit: 1
9339
+ }) : await db.findMany("native_builds", {
9340
+ index: "byBuildKey",
9341
+ where: {
9342
+ scope_key: scopeKey,
9343
+ build_key: buildKey
9344
+ },
9345
+ order: "desc",
9346
+ limit: 1
9347
+ });
9348
+ return rows[0] === void 0 ? null : pick(rows[0]);
9349
+ },
9350
+ async recordNativeBuildSubmission(id, { destination, version }) {
9351
+ 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.`);
9352
+ return db.transaction(async (tx) => {
9353
+ const row = await tx.findOne("native_builds", { id });
9354
+ if (row === null) return null;
9355
+ const build = pick(row);
9356
+ 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}.`);
9357
+ const set = {
9358
+ submitted_to: destination,
9359
+ submitted_at_ms: now(),
9360
+ submitted_version: version
9361
+ };
9362
+ tx.update("native_builds", row, set);
9363
+ return {
9364
+ ...build,
9365
+ ...set
9366
+ };
9367
+ });
9368
+ },
9369
+ async deleteNativeBuild(id) {
9370
+ return db.transaction(async (tx) => {
9371
+ const row = await tx.findOne("native_builds", { id });
9372
+ if (row === null) return {
9373
+ deleted: false,
9374
+ reason: "not_found"
9375
+ };
9376
+ await tx.delete("native_builds", row);
9377
+ const bundle = await tx.findOne("bundles", { id: row.min_bundle_id });
9378
+ if (bundle !== null) await tx.delete("bundles", bundle);
9379
+ return {
9380
+ deleted: true,
9381
+ nativeBuild: pick(row)
9382
+ };
9383
+ });
9384
+ },
9385
+ async listNativeBuilds(input) {
9386
+ const page = {
9387
+ order: input.order ?? "asc",
9388
+ limit: input.limit
9389
+ };
9390
+ const { rows } = input.channelId === void 0 ? await db.findMany("native_builds", {
9391
+ index: "all",
9392
+ where: {},
9393
+ ...page,
9394
+ ...after(input)
9395
+ }) : await db.findMany("native_builds", {
9396
+ index: "byChannel",
9397
+ where: { channel_id: input.channelId },
9398
+ ...page,
9399
+ ...after(input)
9400
+ });
9401
+ return rows.map(pick);
9402
+ }
9403
+ });
9404
+ //#endregion
9072
9405
  //#region ../../packages/server/dist/core/releases.mjs
9073
9406
  const PAGE$1 = 500;
9074
9407
  const fail$1 = (code, message) => {
@@ -9355,6 +9688,7 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
9355
9688
  if (!("bundleId" in deployment) || platforms.has(deployment.bundleId)) continue;
9356
9689
  const row = await db.findOne("bundles", { id: deployment.bundleId });
9357
9690
  if (row === null) throw new DatabaseBundleNotFoundError(deployment.bundleId);
9691
+ 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.`);
9358
9692
  platforms.set(deployment.bundleId, toBundleRow(row).platform);
9359
9693
  }
9360
9694
  const updatedAtMs = now();
@@ -9550,7 +9884,8 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
9550
9884
  id
9551
9885
  };
9552
9886
  const { id: _id, ...set } = bundleToRow(next);
9553
- updateBundle(tx, current, set);
9887
+ const counted = set.platform === current.platform || !await isBuiltInBundle(tx, id);
9888
+ updateBundle(tx, current, set, { counted });
9554
9889
  if (patches !== void 0) await replaceBundlePatches(tx, current, bundleToPatchRows({
9555
9890
  ...next,
9556
9891
  patches
@@ -9561,7 +9896,9 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
9561
9896
  await db.transaction(async (tx) => {
9562
9897
  for (const id of new Set(ids)) {
9563
9898
  const current = await tx.findOne("bundles", { id });
9564
- if (current !== null) await deleteBundle(tx, current);
9899
+ if (current === null) continue;
9900
+ if (await isBuiltInBundle(tx, id)) throw new DatabaseRowReferencedError();
9901
+ await deleteBundle(tx, current);
9565
9902
  }
9566
9903
  });
9567
9904
  } catch (error) {
@@ -9607,6 +9944,7 @@ const createCoreApi = (db, storage, options = {}) => {
9607
9944
  return {
9608
9945
  ...reads,
9609
9946
  ...createCoreOperations(purgingCachedRoutes(db, options.onCachedRoutesChange), options),
9947
+ ...createNativeBuildApi(db, options),
9610
9948
  ready: async () => {
9611
9949
  await reads.listReleaseCatalogs({ limit: 1 });
9612
9950
  }
@@ -9798,6 +10136,10 @@ const answer = async (operation) => {
9798
10136
  code: "REFERENCED",
9799
10137
  error: error.message
9800
10138
  }, 409);
10139
+ if (error instanceof DatabaseConstraintError && error.model === "native_builds") return json$2({
10140
+ code: "NATIVE_BUILD_EXISTS",
10141
+ error: "A native build with this ID or built-in bundle ID is already registered."
10142
+ }, 409);
9801
10143
  throw error;
9802
10144
  }
9803
10145
  };
@@ -9893,6 +10235,40 @@ const bundleUpdateOf = (value, bundleId) => {
9893
10235
  if (id !== void 0 && id !== bundleId) throw new HandlerBadRequestError("Bundle id mismatch");
9894
10236
  return update;
9895
10237
  };
10238
+ const NATIVE_BUILD_ARTIFACT_KINDS = /* @__PURE__ */ new Set([
10239
+ "apk",
10240
+ "aab",
10241
+ "ipa",
10242
+ "app"
10243
+ ]);
10244
+ const nativeBuildScopeOf = (url) => {
10245
+ const params = url.searchParams;
10246
+ const platform = platformOf(url);
10247
+ const channel = params.get("channel");
10248
+ const fingerprintHash = params.get("fingerprintHash");
10249
+ const appVersion = params.get("appVersion");
10250
+ 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.");
10251
+ return fingerprintHash !== null ? {
10252
+ platform,
10253
+ channel,
10254
+ fingerprintHash,
10255
+ appVersion: null
10256
+ } : {
10257
+ platform,
10258
+ channel,
10259
+ fingerprintHash: null,
10260
+ appVersion
10261
+ };
10262
+ };
10263
+ const refusingInvalid = async (operation) => {
10264
+ try {
10265
+ return await operation();
10266
+ } catch (error) {
10267
+ if (error instanceof NativeBuildInputError) throw new HandlerBadRequestError(error.message);
10268
+ throw error;
10269
+ }
10270
+ };
10271
+ 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));
9896
10272
  const scopeKeyOf = (params) => decodeMaybe(requireRouteParam(params, "scopeKey")) ?? "";
9897
10273
  const createAdminRouteHandlers = () => ({
9898
10274
  listBundles: onCore(async (core, request) => {
@@ -10007,6 +10383,37 @@ const createAdminRouteHandlers = () => ({
10007
10383
  }) });
10008
10384
  }),
10009
10385
  preflightReleaseCatalogRebuild: onCore(async (core, _request, params) => json$2({ data: await core.preflightReleaseCatalogRebuild(scopeKeyOf(params)) })),
10386
+ registerNativeBuild: onCore(async (core, request) => {
10387
+ const input = await body$1(request);
10388
+ if (!isNativeBuildRegistration(input)) throw new HandlerBadRequestError("Invalid native build");
10389
+ return json$2({ data: await refusingInvalid(() => core.registerNativeBuild(input)) }, 201);
10390
+ }),
10391
+ findNativeBuild: onCore(async (core, request) => {
10392
+ const url = new URL(request.url);
10393
+ const scope = nativeBuildScopeOf(url);
10394
+ const buildKey = url.searchParams.get("buildKey");
10395
+ return json$2({ data: await refusingInvalid(() => buildKey === null ? core.findNativeBuild(scope) : core.findNativeBuild(scope, { buildKey })) });
10396
+ }),
10397
+ getNativeBuild: onCore(async (core, _request, params) => json$2({ data: await core.getNativeBuild(requireRouteParam(params, "id")) })),
10398
+ listNativeBuilds: onCore(async (core, request) => {
10399
+ const url = new URL(request.url);
10400
+ const input = keyset(url);
10401
+ const channelId = url.searchParams.get("channelId");
10402
+ return page(await core.listNativeBuilds({
10403
+ ...input,
10404
+ ...channelId === null ? {} : { channelId }
10405
+ }), input, ({ id }) => id);
10406
+ }),
10407
+ recordNativeBuildSubmission: onCore(async (core, request, params) => {
10408
+ const input = await body$1(request);
10409
+ 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.");
10410
+ const submission = {
10411
+ destination: input.destination,
10412
+ version: input.version
10413
+ };
10414
+ return json$2({ data: await refusingInvalid(() => core.recordNativeBuildSubmission(requireRouteParam(params, "id"), submission)) });
10415
+ }),
10416
+ deleteNativeBuild: onCore(async (core, _request, params) => json$2({ data: await core.deleteNativeBuild(requireRouteParam(params, "id")) })),
10010
10417
  deleteBundles: onCore(async (core, request) => {
10011
10418
  const { ids } = await body$1(request);
10012
10419
  if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && id.length > 0)) throw new HandlerBadRequestError("Bundle deletion needs ids");
@@ -10124,6 +10531,36 @@ const ADMIN_ROUTES = [
10124
10531
  method: "GET",
10125
10532
  path: "/base-candidates/:candidateKey",
10126
10533
  handler: "findBaseCandidates"
10534
+ },
10535
+ {
10536
+ method: "POST",
10537
+ path: "/native-builds",
10538
+ handler: "registerNativeBuild"
10539
+ },
10540
+ {
10541
+ method: "GET",
10542
+ path: "/native-builds/latest",
10543
+ handler: "findNativeBuild"
10544
+ },
10545
+ {
10546
+ method: "GET",
10547
+ path: "/native-builds/:id",
10548
+ handler: "getNativeBuild"
10549
+ },
10550
+ {
10551
+ method: "GET",
10552
+ path: "/native-builds",
10553
+ handler: "listNativeBuilds"
10554
+ },
10555
+ {
10556
+ method: "POST",
10557
+ path: "/native-builds/:id/submission",
10558
+ handler: "recordNativeBuildSubmission"
10559
+ },
10560
+ {
10561
+ method: "DELETE",
10562
+ path: "/native-builds/:id",
10563
+ handler: "deleteNativeBuild"
10127
10564
  }
10128
10565
  ];
10129
10566
  //#endregion
@@ -56981,8 +57418,9 @@ const isNotFound = (error) => error instanceof RestError && error.statusCode ===
56981
57418
  const serviceOf = (config) => "connectionString" in config ? BlobServiceClient.fromConnectionString(config.connectionString) : new BlobServiceClient(config.accountUrl, config.credential);
56982
57419
  function azureBlobStorage(config) {
56983
57420
  const { containerName, basePath, getDownloadUrl } = config;
56984
- const service = serviceOf(config);
56985
- const container = service.getContainerClient(containerName);
57421
+ let client;
57422
+ const service = () => client ??= serviceOf(config);
57423
+ const container = () => service().getContainerClient(containerName);
56986
57424
  const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
56987
57425
  const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
56988
57426
  const getListPrefix = (prefix = "") => {
@@ -57013,7 +57451,7 @@ function azureBlobStorage(config) {
57013
57451
  const delegationKey = () => {
57014
57452
  const now = Date.now();
57015
57453
  if (delegation === void 0 || delegation.renewAt <= now) {
57016
- const key = service.getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS));
57454
+ const key = service().getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS));
57017
57455
  key.catch(() => {
57018
57456
  delegation = void 0;
57019
57457
  });
@@ -57025,7 +57463,7 @@ function azureBlobStorage(config) {
57025
57463
  return delegation.key;
57026
57464
  };
57027
57465
  const signedUrl = async (key) => {
57028
- const blob = container.getBlobClient(key);
57466
+ const blob = container().getBlobClient(key);
57029
57467
  const expiresOn = new Date(Date.now() + SAS_EXPIRES_IN_MS);
57030
57468
  const permissions = BlobSASPermissions.parse("r");
57031
57469
  if (blob.credential instanceof StorageSharedKeyCredential) return blob.generateSasUrl({
@@ -57046,7 +57484,7 @@ function azureBlobStorage(config) {
57046
57484
  protocol: PROTOCOL,
57047
57485
  async listObjects(prefix) {
57048
57486
  const objects = [];
57049
- for await (const blob of container.listBlobsFlat({ prefix: getListPrefix(prefix) })) {
57487
+ for await (const blob of container().listBlobsFlat({ prefix: getListPrefix(prefix) })) {
57050
57488
  const storageUri = getListedStorageUri(blob.name);
57051
57489
  if (storageUri === null) continue;
57052
57490
  objects.push({
@@ -57063,17 +57501,36 @@ function azureBlobStorage(config) {
57063
57501
  for (let offset = 0; offset < keys.length; offset += 256) {
57064
57502
  const batch = keys.slice(offset, offset + 256);
57065
57503
  if (batch.length === 0) continue;
57066
- const failed = (await container.getBlobBatchClient().deleteBlobs(batch.map((key) => container.getBlobClient(key)))).subResponses.filter(({ status }) => status >= 300 && status !== 404);
57504
+ const failed = (await container().getBlobBatchClient().deleteBlobs(batch.map((key) => container().getBlobClient(key)))).subResponses.filter(({ status }) => status >= 300 && status !== 404);
57067
57505
  if (failed.length > 0) throw new Error(`Failed to delete ${failed.length} Azure blob(s).`);
57068
57506
  }
57069
57507
  },
57070
- async put({ key, body, contentType }) {
57508
+ async put({ key, body, contentLength, contentType }) {
57071
57509
  const storageKey = getStorageKey(key);
57072
- const bytes = new Uint8Array(await new Response(body).arrayBuffer());
57073
- await container.getBlockBlobClient(storageKey).uploadData(bytes, { blobHTTPHeaders: {
57510
+ const options = { blobHTTPHeaders: {
57074
57511
  blobContentType: contentType,
57075
57512
  blobCacheControl: "max-age=31536000"
57076
- } });
57513
+ } };
57514
+ const blob = container().getBlockBlobClient(storageKey);
57515
+ if (contentLength === void 0) await blob.uploadData(new Uint8Array(await new Response(body).arrayBuffer()), options);
57516
+ else {
57517
+ const { Readable } = await import("node:stream");
57518
+ const source = Readable.fromWeb(body);
57519
+ const aborted = new AbortController();
57520
+ let failure;
57521
+ source.once("error", (error) => {
57522
+ failure = error;
57523
+ aborted.abort(error);
57524
+ });
57525
+ try {
57526
+ await blob.upload(source, contentLength, {
57527
+ ...options,
57528
+ abortSignal: aborted.signal
57529
+ });
57530
+ } catch (error) {
57531
+ throw failure ?? error;
57532
+ }
57533
+ }
57077
57534
  return { storageUri: createStorageUri({
57078
57535
  bucket: containerName,
57079
57536
  key: storageKey,
@@ -57083,7 +57540,7 @@ function azureBlobStorage(config) {
57083
57540
  async get({ storageUri }) {
57084
57541
  const { key } = parseAndValidate(storageUri);
57085
57542
  try {
57086
- const download = await container.getBlobClient(key).download();
57543
+ const download = await container().getBlobClient(key).download();
57087
57544
  const body = download.readableStreamBody;
57088
57545
  if (body === void 0) return { response: null };
57089
57546
  const headers = new Headers();
@@ -57103,11 +57560,11 @@ function azureBlobStorage(config) {
57103
57560
  },
57104
57561
  async exists({ storageUri }) {
57105
57562
  const { key } = parseAndValidate(storageUri);
57106
- return { exists: await container.getBlobClient(key).exists() };
57563
+ return { exists: await container().getBlobClient(key).exists() };
57107
57564
  },
57108
57565
  async delete({ storageUri }) {
57109
57566
  const { key } = parseAndValidate(storageUri);
57110
- await container.getBlobClient(key).deleteIfExists();
57567
+ await container().getBlobClient(key).deleteIfExists();
57111
57568
  return { deleted: true };
57112
57569
  }
57113
57570
  });
@@ -70399,11 +70856,19 @@ const assertCosmosContainerReusable = async (container, expected) => {
70399
70856
  const isWideAdd = (op) => op.type === "add" && Object.keys(op.by).length + (op.expiresAt === void 0 ? 0 : 2) > PATCH_OPERATIONS;
70400
70857
  const emulatorAborted = (error) => statusOf(error) === 500 && /PostgresError\(SqlState\(E(25P02|40001|40P01)\)/.test(String(error instanceof Error ? error.message : error));
70401
70858
  const createCosmosStore = ({ client, databaseId, containerId, engine, nativePageSize }) => {
70402
- const container = client.database(databaseId).container(containerId);
70859
+ let opened;
70860
+ const open = () => opened ??= (() => {
70861
+ const account = typeof client === "function" ? client() : client;
70862
+ return {
70863
+ client: account,
70864
+ container: account.database(databaseId).container(containerId)
70865
+ };
70866
+ })();
70867
+ const container = () => open().container;
70403
70868
  const readStates = async (ids, rowIds) => {
70404
70869
  const states = /* @__PURE__ */ new Map();
70405
70870
  for (const [group, projection] of [[ids.filter((id) => !rowIds.has(id)), ""], [ids.filter((id) => rowIds.has(id)), ", c.row"]]) for (let at = 0; at < group.length; at += GET_BATCH) {
70406
- const { resources } = await container.items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
70871
+ const { resources } = await container().items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
70407
70872
  for (const { id, etag, v, expireAt, row } of resources) states.set(id, {
70408
70873
  etag,
70409
70874
  v,
@@ -70535,7 +71000,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
70535
71000
  const found = /* @__PURE__ */ new Map();
70536
71001
  const ids = [...new Set(keys.map(cosmosItemId))];
70537
71002
  for (let at = 0; at < ids.length; at += GET_BATCH) {
70538
- const { resources } = await container.items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
71003
+ const { resources } = await container().items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
70539
71004
  for (const resource of resources) found.set(resource.id, toRow(resource.row));
70540
71005
  }
70541
71006
  return keys.map((key) => found.get(cosmosItemId(key)) ?? null);
@@ -70571,7 +71036,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
70571
71036
  value: after
70572
71037
  });
70573
71038
  }
70574
- const { resources } = await container.items.query({
71039
+ const { resources } = await container().items.query({
70575
71040
  query: `SELECT TOP @size c.sk, c.row FROM c WHERE ${filters.join(" AND ")} ORDER BY c.pk ${order === "asc" ? "ASC" : "DESC"}, c.sk ${order === "asc" ? "ASC" : "DESC"}`,
70576
71041
  parameters
70577
71042
  }, {
@@ -70621,7 +71086,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
70621
71086
  if (planned.length > BATCH_OPERATIONS) throw new Error(`A Cosmos DB batch takes ${BATCH_OPERATIONS} operations; this write needs ${planned.length}.`);
70622
71087
  let results;
70623
71088
  try {
70624
- const response = await container.items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
71089
+ const response = await container().items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
70625
71090
  if (response.result === void 0) throw Object.assign(/* @__PURE__ */ new Error(`Cosmos DB refused the batch with status ${response.code}`), { code: response.code });
70626
71091
  results = response.result;
70627
71092
  } catch (error) {
@@ -70654,7 +71119,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
70654
71119
  async deleteConsumed(keys) {
70655
71120
  let remaining = [...new Set(keys.map(cosmosItemId))];
70656
71121
  for (let attempt = 0; remaining.length > 0; attempt += 1) {
70657
- const failed = (await container.items.executeBulkOperations(remaining.map((id) => ({
71122
+ const failed = (await container().items.executeBulkOperations(remaining.map((id) => ({
70658
71123
  operationType: "Delete",
70659
71124
  id,
70660
71125
  partitionKey: COSMOS_PARTITION
@@ -70673,8 +71138,8 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
70673
71138
  }
70674
71139
  },
70675
71140
  migrations: { async apply() {
70676
- if (engine !== void 0) await assertCosmosContainerReusable(container, engine);
70677
- const { database } = await client.databases.createIfNotExists({ id: databaseId });
71141
+ if (engine !== void 0) await assertCosmosContainerReusable(container(), engine);
71142
+ const { database } = await open().client.databases.createIfNotExists({ id: databaseId });
70678
71143
  const { resource } = await database.containers.createIfNotExists({
70679
71144
  id: containerId,
70680
71145
  partitionKey: { paths: [COSMOS_PARTITION_KEY_PATH] },
@@ -70710,29 +71175,40 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
70710
71175
  const adapterOf = (config) => {
70711
71176
  const { databaseId = AZURE_COSMOS_V1_DATABASE, containerId = AZURE_COSMOS_V1_CONTAINER, aggregateBatching: _, ...connection } = config;
70712
71177
  if (connection.consistencyLevel !== void 0 && connection.consistencyLevel !== "Strong") throw new Error(`cosmosDatabase reads with strong consistency; consistencyLevel "${String(connection.consistencyLevel)}" is weaker.`);
70713
- const client = new CosmosClient(connection);
71178
+ let client;
70714
71179
  const store = createCosmosStore({
70715
- client,
71180
+ client: () => client ??= new CosmosClient(connection),
70716
71181
  databaseId,
70717
71182
  containerId,
70718
71183
  engine: currentCosmosEngine()
70719
71184
  });
70720
71185
  return {
70721
- client,
71186
+ dispose: () => client?.dispose(),
70722
71187
  adapter: createKvAdapter({ store })
70723
71188
  };
70724
71189
  };
71190
+ const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
71191
+ const accountHostOf = ({ endpoint, connectionString }) => {
71192
+ const url = endpoint ?? connectionString?.match(/AccountEndpoint=([^;]+)/iu)?.[1];
71193
+ try {
71194
+ return url === void 0 ? void 0 : new URL(url).hostname;
71195
+ } catch {
71196
+ return;
71197
+ }
71198
+ };
70725
71199
  const cosmosDatabase = (config) => {
70726
- const { client, adapter } = adapterOf(config);
71200
+ const { dispose, adapter } = adapterOf(config);
71201
+ const host = accountHostOf(config);
70727
71202
  return {
70728
71203
  ...createEngineDatabase({
70729
71204
  name: "cosmosDatabase",
70730
71205
  adapter,
70731
71206
  aggregateBatching: config.aggregateBatching ?? {}
70732
71207
  }),
71208
+ ...host === void 0 ? {} : { [DATABASE_TARGET]: `cosmos/${host}` },
70733
71209
  async dispose() {
70734
71210
  await adapter.dispose?.();
70735
- client.dispose();
71211
+ dispose();
70736
71212
  }
70737
71213
  };
70738
71214
  };