@hot-updater/firebase 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.
@@ -223,7 +223,7 @@ const bundleMetadataToRow = (metadata) => {
223
223
  return metadata;
224
224
  };
225
225
  //#endregion
226
- //#region ../../packages/protocol/dist/index.mjs
226
+ //#region ../../packages/protocol/dist/uuid-kZvLA0Ly.mjs
227
227
  const LETTER_DASH_NUMBER$2 = "[a-zA-Z0-9-]";
228
228
  const NUMERIC_IDENTIFIER$2 = String.raw`0|[1-9]\d*`;
229
229
  const NUMERIC_IDENTIFIER_LOOSE$2 = String.raw`\d+`;
@@ -336,128 +336,6 @@ function canonicalizeAppVersion(appVersion) {
336
336
  const version = coerce$1(appVersion);
337
337
  return version ? normalizeFull(version) : null;
338
338
  }
339
- const getManifestStorageUri = (bundle) => bundle.manifestStorageUri;
340
- const getManifestFileHash = (bundle) => bundle.manifestFileHash;
341
- const getAssetBaseStorageUri = (bundle) => bundle.assetBaseStorageUri;
342
- const isBundlePatchArtifact = (value) => {
343
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
344
- const candidate = value;
345
- return typeof candidate.baseBundleId === "string" && typeof candidate.baseFileHash === "string" && typeof candidate.patchFileHash === "string" && typeof candidate.patchStorageUri === "string";
346
- };
347
- const readBundlePatchArray = (patches) => {
348
- if (!Array.isArray(patches)) return [];
349
- return patches.filter(isBundlePatchArtifact);
350
- };
351
- const getBundlePatches = (bundle) => {
352
- const patches = readBundlePatchArray(bundle.patches);
353
- const seenBaseBundleIds = /* @__PURE__ */ new Set();
354
- return patches.filter((patch) => {
355
- if (seenBaseBundleIds.has(patch.baseBundleId)) return false;
356
- seenBaseBundleIds.add(patch.baseBundleId);
357
- return true;
358
- });
359
- };
360
- const getBundlePatch = (bundle, baseBundleId) => {
361
- return getBundlePatches(bundle).find((patch) => patch.baseBundleId === baseBundleId) ?? null;
362
- };
363
- const NUMERIC_COHORT_SIZE = 1e3;
364
- const DEFAULT_ROLLOUT_COHORT_COUNT = NUMERIC_COHORT_SIZE;
365
- const CUSTOM_COHORT_PATTERN = /^[a-z0-9-]+$/;
366
- function parseNumericCohortValue(cohort) {
367
- if (!/^\d+$/.test(cohort)) return null;
368
- const parsed = Number.parseInt(cohort, 10);
369
- if (Number.isNaN(parsed) || parsed < 1 || parsed > 1e3) return null;
370
- return parsed;
371
- }
372
- function positiveMod(value, modulus) {
373
- return (value % modulus + modulus) % modulus;
374
- }
375
- function hashString(value) {
376
- let hash = 0;
377
- for (let i = 0; i < value.length; i++) {
378
- const char = value.charCodeAt(i);
379
- hash = (hash << 5) - hash + char;
380
- hash |= 0;
381
- }
382
- return hash;
383
- }
384
- function gcd(a, b) {
385
- let x = Math.abs(a);
386
- let y = Math.abs(b);
387
- while (y !== 0) {
388
- const next = x % y;
389
- x = y;
390
- y = next;
391
- }
392
- return x;
393
- }
394
- function modularInverse(value, modulus) {
395
- let t = 0;
396
- let newT = 1;
397
- let r = modulus;
398
- let newR = positiveMod(value, modulus);
399
- while (newR !== 0) {
400
- const quotient = Math.floor(r / newR);
401
- [t, newT] = [newT, t - quotient * newT];
402
- [r, newR] = [newR, r - quotient * newR];
403
- }
404
- if (r > 1) throw new Error(`No modular inverse for ${value} mod ${modulus}`);
405
- return positiveMod(t, modulus);
406
- }
407
- function getRolloutShuffleParameters(bundleId) {
408
- let multiplier = positiveMod(hashString(`${bundleId}:multiplier`), 997);
409
- if (multiplier === 0) multiplier = 1;
410
- while (gcd(multiplier, NUMERIC_COHORT_SIZE) !== 1) {
411
- multiplier = positiveMod(multiplier + 1, NUMERIC_COHORT_SIZE);
412
- if (multiplier === 0) multiplier = 1;
413
- }
414
- const offset = positiveMod(hashString(`${bundleId}:offset`), NUMERIC_COHORT_SIZE);
415
- return {
416
- multiplier,
417
- offset,
418
- inverseMultiplier: modularInverse(multiplier, NUMERIC_COHORT_SIZE)
419
- };
420
- }
421
- function normalizeRolloutCohortCount(rolloutCohortCount) {
422
- if (rolloutCohortCount === null || rolloutCohortCount === void 0) return DEFAULT_ROLLOUT_COHORT_COUNT;
423
- if (rolloutCohortCount <= 0) return 0;
424
- if (rolloutCohortCount >= 1e3) return NUMERIC_COHORT_SIZE;
425
- return Math.floor(rolloutCohortCount);
426
- }
427
- function normalizeCohortValue(cohort) {
428
- const normalized = cohort.trim().toLowerCase();
429
- const numericCohort = parseNumericCohortValue(normalized);
430
- if (numericCohort !== null) return String(numericCohort);
431
- return normalized;
432
- }
433
- function getNumericCohortValue(cohort) {
434
- return parseNumericCohortValue(normalizeCohortValue(cohort));
435
- }
436
- function isNumericCohort(cohort) {
437
- return getNumericCohortValue(cohort) !== null;
438
- }
439
- function isCustomCohort(cohort) {
440
- const normalized = normalizeCohortValue(cohort);
441
- return normalized.length > 0 && normalized.length <= 64 && !/^\d+$/.test(normalized) && CUSTOM_COHORT_PATTERN.test(normalized);
442
- }
443
- function isValidCohort(cohort) {
444
- const normalized = normalizeCohortValue(cohort);
445
- return isNumericCohort(normalized) || isCustomCohort(normalized);
446
- }
447
- function getNumericCohortRolloutPosition(bundleId, cohortValue) {
448
- if (cohortValue < 1 || cohortValue > 1e3) throw new Error(`Invalid numeric cohort: ${cohortValue}`);
449
- const { offset, inverseMultiplier } = getRolloutShuffleParameters(bundleId);
450
- return positiveMod(inverseMultiplier * (cohortValue - 1 - offset), NUMERIC_COHORT_SIZE);
451
- }
452
- function getRolledOutNumericCohorts(bundleId, rolloutCohortCount) {
453
- const normalizedRolloutCount = normalizeRolloutCohortCount(rolloutCohortCount);
454
- if (normalizedRolloutCount <= 0) return [];
455
- if (normalizedRolloutCount >= 1e3) return Array.from({ length: NUMERIC_COHORT_SIZE }, (_, index) => index + 1);
456
- const { multiplier, offset } = getRolloutShuffleParameters(bundleId);
457
- return Array.from({ length: normalizedRolloutCount }, (_, position) => positiveMod(multiplier * position + offset, NUMERIC_COHORT_SIZE)).map((zeroBasedCohort) => zeroBasedCohort + 1).sort((left, right) => left - right);
458
- }
459
- const RELEASE_CATALOG_FALLBACK_POLICY = "BUILTIN_IF_ACTIVE_INELIGIBLE";
460
- const MAX_COMPILED_CATALOG_BYTES = 262144;
461
339
  const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
462
340
  const MAX_CATALOG_SEGMENT_LENGTH = 255;
463
341
  function assertCatalogSegment(value, name) {
@@ -582,6 +460,130 @@ const NIL_UUID = "00000000-0000-0000-0000-000000000000";
582
460
  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}$/;
583
461
  const isUUIDv7 = (value) => typeof value === "string" && UUID_V7_PATTERN.test(value);
584
462
  //#endregion
463
+ //#region ../../packages/protocol/dist/index.mjs
464
+ const getManifestStorageUri = (bundle) => bundle.manifestStorageUri;
465
+ const getManifestFileHash = (bundle) => bundle.manifestFileHash;
466
+ const getAssetBaseStorageUri = (bundle) => bundle.assetBaseStorageUri;
467
+ const isBundlePatchArtifact = (value) => {
468
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
469
+ const candidate = value;
470
+ return typeof candidate.baseBundleId === "string" && typeof candidate.baseFileHash === "string" && typeof candidate.patchFileHash === "string" && typeof candidate.patchStorageUri === "string";
471
+ };
472
+ const readBundlePatchArray = (patches) => {
473
+ if (!Array.isArray(patches)) return [];
474
+ return patches.filter(isBundlePatchArtifact);
475
+ };
476
+ const getBundlePatches = (bundle) => {
477
+ const patches = readBundlePatchArray(bundle.patches);
478
+ const seenBaseBundleIds = /* @__PURE__ */ new Set();
479
+ return patches.filter((patch) => {
480
+ if (seenBaseBundleIds.has(patch.baseBundleId)) return false;
481
+ seenBaseBundleIds.add(patch.baseBundleId);
482
+ return true;
483
+ });
484
+ };
485
+ const getBundlePatch = (bundle, baseBundleId) => {
486
+ return getBundlePatches(bundle).find((patch) => patch.baseBundleId === baseBundleId) ?? null;
487
+ };
488
+ const NUMERIC_COHORT_SIZE = 1e3;
489
+ const DEFAULT_ROLLOUT_COHORT_COUNT = NUMERIC_COHORT_SIZE;
490
+ const CUSTOM_COHORT_PATTERN = /^[a-z0-9-]+$/;
491
+ function parseNumericCohortValue(cohort) {
492
+ if (!/^\d+$/.test(cohort)) return null;
493
+ const parsed = Number.parseInt(cohort, 10);
494
+ if (Number.isNaN(parsed) || parsed < 1 || parsed > 1e3) return null;
495
+ return parsed;
496
+ }
497
+ function positiveMod(value, modulus) {
498
+ return (value % modulus + modulus) % modulus;
499
+ }
500
+ function hashString(value) {
501
+ let hash = 0;
502
+ for (let i = 0; i < value.length; i++) {
503
+ const char = value.charCodeAt(i);
504
+ hash = (hash << 5) - hash + char;
505
+ hash |= 0;
506
+ }
507
+ return hash;
508
+ }
509
+ function gcd(a, b) {
510
+ let x = Math.abs(a);
511
+ let y = Math.abs(b);
512
+ while (y !== 0) {
513
+ const next = x % y;
514
+ x = y;
515
+ y = next;
516
+ }
517
+ return x;
518
+ }
519
+ function modularInverse(value, modulus) {
520
+ let t = 0;
521
+ let newT = 1;
522
+ let r = modulus;
523
+ let newR = positiveMod(value, modulus);
524
+ while (newR !== 0) {
525
+ const quotient = Math.floor(r / newR);
526
+ [t, newT] = [newT, t - quotient * newT];
527
+ [r, newR] = [newR, r - quotient * newR];
528
+ }
529
+ if (r > 1) throw new Error(`No modular inverse for ${value} mod ${modulus}`);
530
+ return positiveMod(t, modulus);
531
+ }
532
+ function getRolloutShuffleParameters(bundleId) {
533
+ let multiplier = positiveMod(hashString(`${bundleId}:multiplier`), 997);
534
+ if (multiplier === 0) multiplier = 1;
535
+ while (gcd(multiplier, NUMERIC_COHORT_SIZE) !== 1) {
536
+ multiplier = positiveMod(multiplier + 1, NUMERIC_COHORT_SIZE);
537
+ if (multiplier === 0) multiplier = 1;
538
+ }
539
+ const offset = positiveMod(hashString(`${bundleId}:offset`), NUMERIC_COHORT_SIZE);
540
+ return {
541
+ multiplier,
542
+ offset,
543
+ inverseMultiplier: modularInverse(multiplier, NUMERIC_COHORT_SIZE)
544
+ };
545
+ }
546
+ function normalizeRolloutCohortCount(rolloutCohortCount) {
547
+ if (rolloutCohortCount === null || rolloutCohortCount === void 0) return DEFAULT_ROLLOUT_COHORT_COUNT;
548
+ if (rolloutCohortCount <= 0) return 0;
549
+ if (rolloutCohortCount >= 1e3) return NUMERIC_COHORT_SIZE;
550
+ return Math.floor(rolloutCohortCount);
551
+ }
552
+ function normalizeCohortValue(cohort) {
553
+ const normalized = cohort.trim().toLowerCase();
554
+ const numericCohort = parseNumericCohortValue(normalized);
555
+ if (numericCohort !== null) return String(numericCohort);
556
+ return normalized;
557
+ }
558
+ function getNumericCohortValue(cohort) {
559
+ return parseNumericCohortValue(normalizeCohortValue(cohort));
560
+ }
561
+ function isNumericCohort(cohort) {
562
+ return getNumericCohortValue(cohort) !== null;
563
+ }
564
+ function isCustomCohort(cohort) {
565
+ const normalized = normalizeCohortValue(cohort);
566
+ return normalized.length > 0 && normalized.length <= 64 && !/^\d+$/.test(normalized) && CUSTOM_COHORT_PATTERN.test(normalized);
567
+ }
568
+ function isValidCohort(cohort) {
569
+ const normalized = normalizeCohortValue(cohort);
570
+ return isNumericCohort(normalized) || isCustomCohort(normalized);
571
+ }
572
+ function getNumericCohortRolloutPosition(bundleId, cohortValue) {
573
+ if (cohortValue < 1 || cohortValue > 1e3) throw new Error(`Invalid numeric cohort: ${cohortValue}`);
574
+ const { offset, inverseMultiplier } = getRolloutShuffleParameters(bundleId);
575
+ return positiveMod(inverseMultiplier * (cohortValue - 1 - offset), NUMERIC_COHORT_SIZE);
576
+ }
577
+ function getRolledOutNumericCohorts(bundleId, rolloutCohortCount) {
578
+ const normalizedRolloutCount = normalizeRolloutCohortCount(rolloutCohortCount);
579
+ if (normalizedRolloutCount <= 0) return [];
580
+ if (normalizedRolloutCount >= 1e3) return Array.from({ length: NUMERIC_COHORT_SIZE }, (_, index) => index + 1);
581
+ const { multiplier, offset } = getRolloutShuffleParameters(bundleId);
582
+ return Array.from({ length: normalizedRolloutCount }, (_, position) => positiveMod(multiplier * position + offset, NUMERIC_COHORT_SIZE)).map((zeroBasedCohort) => zeroBasedCohort + 1).sort((left, right) => left - right);
583
+ }
584
+ const RELEASE_CATALOG_FALLBACK_POLICY = "BUILTIN_IF_ACTIVE_INELIGIBLE";
585
+ const MAX_COMPILED_CATALOG_BYTES = 262144;
586
+ //#endregion
585
587
  //#region ../plugin-core/dist/databaseRows.mjs
586
588
  const releaseRowToRelease = (row) => ({
587
589
  id: row.id,
@@ -1901,12 +1903,17 @@ const SETTINGS_TABLE = {
1901
1903
  const ENGINE_SCHEMA_KEY = "schema.engine";
1902
1904
  const CORE_SCHEMA_KEY = "schema.core";
1903
1905
  const stored = ({ key, expected, found }) => `"${key}" is ${found === null ? "missing" : `"${found}"`}; expected "${expected}"`;
1904
- 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\`.`;
1906
+ /**
1907
+ * How to migrate a database the fence refused: doctor names the command for
1908
+ * it, init again for a deployment init manages, or the database's own
1909
+ * tooling in the server's project.
1910
+ */
1911
+ 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.";
1912
+ const settingMessage = (adapterName, { key, expected, found }) => `Hot Updater schema setting "${key}" for ${adapterName} is ${found === null ? "missing" : `"${found}"`}; expected "${expected}". ${MIGRATE_SCHEMA_FIX}`;
1905
1913
  /** The plugin a `schema.<id>` setting belongs to; undefined for core's and the engine's. */
1906
1914
  const pluginOf = ({ key }) => key.startsWith("schema.") && key !== "schema.engine" && key !== CORE_SCHEMA_KEY ? key.slice(7) : void 0;
1907
1915
  const listed = (items) => items.length < 2 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
1908
- /** How to create plugins' tables when the fence was given no fix. */
1909
- 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.";
1916
+ const settingsMessage = (adapterName, settings) => `Hot Updater schema settings for ${adapterName} are not migrated: ${settings.map(stored).join(", and ")}. ${MIGRATE_SCHEMA_FIX}`;
1910
1917
  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}`;
1911
1918
  /** The database needs `hot-updater db migrate`, or a new database; the handler answers 503. */
1912
1919
  var HotUpdaterSchemaMigrationRequiredError = class extends Error {
@@ -1923,7 +1930,7 @@ var HotUpdaterSchemaMigrationRequiredError = class extends Error {
1923
1930
  constructor(adapterName, currentVersion, setting, options) {
1924
1931
  const settings = options?.settings ?? (setting === void 0 ? [] : [setting]);
1925
1932
  const plugins = settings.flatMap((mismatch) => pluginOf(mismatch) ?? []);
1926
- 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 });
1933
+ 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 });
1927
1934
  this.adapterName = adapterName;
1928
1935
  this.currentVersion = currentVersion;
1929
1936
  this.setting = setting;
@@ -1987,17 +1994,37 @@ const checkSchemaFence = async (adapter, adapterName, expected, fix) => {
1987
1994
  };
1988
1995
  /** The database name a fenced adapter carries, so a server can fence its plugins' rows too. */
1989
1996
  const FENCED = Symbol.for("@hot-updater/server/schema-fence");
1997
+ /**
1998
+ * The adapter a fence reads its settings through and the settings it
1999
+ * expects, so a fence over it checks them with its own in one read. Another
2000
+ * key than `FENCED`, whose value another copy of plugin-core reads as the
2001
+ * name.
2002
+ */
2003
+ const FENCED_SETTINGS = Symbol.for("@hot-updater/server/schema-fence-settings");
1990
2004
  /** The database name an adapter behind the schema fence reports under. */
1991
2005
  const fencedName = (adapter) => adapter[FENCED];
1992
2006
  /**
1993
2007
  * The adapter behind the schema fence: a process's first read or write
1994
2008
  * checks the settings first. Success is kept; a failure is checked again on
1995
- * the next call.
2009
+ * the next call. A fence over another one, as a server's plugins' over
2010
+ * core's, checks the inner fence's settings with its own in that one read,
2011
+ * so its refusal lists every setting missing or different.
1996
2012
  */
1997
2013
  const withSchemaFence = (adapter, adapterName, expected, fix) => {
2014
+ const inner = adapter[FENCED_SETTINGS];
2015
+ const settings = inner === void 0 ? {
2016
+ adapter,
2017
+ expected
2018
+ } : {
2019
+ adapter: inner.adapter,
2020
+ expected: {
2021
+ ...inner.expected,
2022
+ ...expected
2023
+ }
2024
+ };
1998
2025
  let ready;
1999
2026
  const fence = () => {
2000
- ready ??= checkSchemaFence(adapter, adapterName, expected, fix).catch((error) => {
2027
+ ready ??= checkSchemaFence(settings.adapter, adapterName, settings.expected, fix).catch((error) => {
2001
2028
  ready = void 0;
2002
2029
  throw error;
2003
2030
  });
@@ -2006,6 +2033,7 @@ const withSchemaFence = (adapter, adapterName, expected, fix) => {
2006
2033
  return {
2007
2034
  ...adapter,
2008
2035
  [FENCED]: adapterName,
2036
+ [FENCED_SETTINGS]: settings,
2009
2037
  get: (table, keys) => fence().then(() => adapter.get(table, keys)),
2010
2038
  query: (table, request) => fence().then(() => adapter.query(table, request)),
2011
2039
  write: (ops) => fence().then(() => adapter.write(ops)),
@@ -2064,259 +2092,388 @@ const CHANNEL_ID = {
2064
2092
  maxLength: 1408,
2065
2093
  ascii: true
2066
2094
  };
2067
- /** Core's schema version: the `schema.core` settings row. */
2068
- const HOT_UPDATER_SCHEMA_VERSION = "1.0.0";
2095
+ /**
2096
+ * Core's schema version: the `schema.core` settings row. Bump it when core
2097
+ * adds a table, then regenerate the managed migrations with
2098
+ * HOT_UPDATER_UPDATE_SQL=1 (the D1 and Supabase schema specs), so the next
2099
+ * release's numbered migration carries the change and `hot-updater init`
2100
+ * applies it to managed databases an earlier release created. The official
2101
+ * plugins' `schemaVersion`s follow the same rule. A bump only adds tables:
2102
+ * every database's migrator creates missing tables and then records the
2103
+ * version, and none of them changes an existing table, so changing one
2104
+ * needs a migration step of its own first. The schema fence answers 503 on
2105
+ * a database whose row differs, until it runs.
2106
+ */
2107
+ const HOT_UPDATER_SCHEMA_VERSION = "1.2.0";
2108
+ const bundles = defineTable({
2109
+ id: {
2110
+ type: "string",
2111
+ maxLength: 36
2112
+ },
2113
+ platform: {
2114
+ type: "string",
2115
+ maxLength: 16
2116
+ },
2117
+ git_commit_hash: {
2118
+ type: "string",
2119
+ maxLength: 255,
2120
+ required: false
2121
+ },
2122
+ metadata: { type: "json" },
2123
+ manifest_storage_uri: { type: "string" },
2124
+ manifest_file_hash: { type: "string" },
2125
+ asset_base_storage_uri: { type: "string" }
2126
+ }, {
2127
+ key: ["id"],
2128
+ indexes: {
2129
+ byPlatform: {
2130
+ eq: ["platform"],
2131
+ sort: ["id"]
2132
+ },
2133
+ all: {
2134
+ eq: [],
2135
+ sort: ["id"]
2136
+ }
2137
+ }
2138
+ });
2139
+ const bundlePatches = defineTable({
2140
+ id: {
2141
+ type: "string",
2142
+ maxLength: 255
2143
+ },
2144
+ bundle_id: {
2145
+ type: "string",
2146
+ maxLength: 36,
2147
+ references: {
2148
+ model: "bundles",
2149
+ onDelete: "cascade"
2150
+ }
2151
+ },
2152
+ base_bundle_id: {
2153
+ type: "string",
2154
+ maxLength: 36,
2155
+ references: {
2156
+ model: "bundles",
2157
+ onDelete: "cascade"
2158
+ }
2159
+ },
2160
+ base_file_hash: { type: "string" },
2161
+ patch_file_hash: { type: "string" },
2162
+ patch_storage_uri: { type: "string" },
2163
+ byte_size: { type: "integer" },
2164
+ order_index: { type: "integer" }
2165
+ }, {
2166
+ key: ["id"],
2167
+ indexes: {
2168
+ pair: {
2169
+ eq: ["bundle_id", "base_bundle_id"],
2170
+ sort: [],
2171
+ unique: true
2172
+ },
2173
+ byBundle: {
2174
+ eq: ["bundle_id"],
2175
+ sort: ["order_index"],
2176
+ root: { model: "bundles" }
2177
+ },
2178
+ byBase: {
2179
+ eq: ["base_bundle_id"],
2180
+ sort: ["bundle_id"]
2181
+ }
2182
+ }
2183
+ });
2184
+ const releases = defineTable({
2185
+ id: {
2186
+ type: "string",
2187
+ maxLength: 36
2188
+ },
2189
+ revision: { type: "integer" },
2190
+ scope_key: {
2191
+ type: "string",
2192
+ maxLength: 2048,
2193
+ ascii: true
2194
+ },
2195
+ channel_id: {
2196
+ ...CHANNEL_ID,
2197
+ references: {
2198
+ model: "channels",
2199
+ onDelete: "restrict"
2200
+ }
2201
+ },
2202
+ platform: {
2203
+ type: "string",
2204
+ maxLength: 16
2205
+ },
2206
+ kind: {
2207
+ type: "string",
2208
+ maxLength: 16
2209
+ },
2210
+ bundle_id: {
2211
+ type: "string",
2212
+ maxLength: 36,
2213
+ required: false,
2214
+ references: {
2215
+ model: "bundles",
2216
+ onDelete: "restrict"
2217
+ }
2218
+ },
2219
+ strategy: {
2220
+ type: "string",
2221
+ maxLength: 16
2222
+ },
2223
+ target_app_version: {
2224
+ type: "string",
2225
+ required: false
2226
+ },
2227
+ fingerprint_hash: {
2228
+ type: "string",
2229
+ maxLength: 255,
2230
+ required: false
2231
+ },
2232
+ enabled: { type: "boolean" },
2233
+ should_force_update: { type: "boolean" },
2234
+ message: {
2235
+ type: "string",
2236
+ required: false
2237
+ },
2238
+ rollout_cohort_count: { type: "integer" },
2239
+ target_cohorts: { type: "json" },
2240
+ operation: {
2241
+ type: "string",
2242
+ maxLength: 16
2243
+ },
2244
+ source_release_id: {
2245
+ type: "string",
2246
+ maxLength: 36,
2247
+ required: false,
2248
+ references: {
2249
+ model: "releases",
2250
+ onDelete: "none"
2251
+ }
2252
+ },
2253
+ created_at_ms: { type: "integer" },
2254
+ updated_at_ms: { type: "integer" }
2255
+ }, {
2256
+ key: ["id"],
2257
+ indexes: {
2258
+ byScope: {
2259
+ eq: ["scope_key"],
2260
+ sort: ["id"],
2261
+ root: { model: "release_catalogs" }
2262
+ },
2263
+ byScopeEnabled: {
2264
+ eq: ["scope_key", "enabled"],
2265
+ sort: ["id"],
2266
+ root: { model: "release_catalogs" }
2267
+ },
2268
+ byChannelPlatform: {
2269
+ eq: ["channel_id", "platform"],
2270
+ sort: ["id"]
2271
+ },
2272
+ byChannelPlatformEnabled: {
2273
+ eq: [
2274
+ "channel_id",
2275
+ "platform",
2276
+ "enabled"
2277
+ ],
2278
+ sort: ["id"]
2279
+ },
2280
+ byBundle: {
2281
+ eq: ["bundle_id"],
2282
+ sort: ["id"]
2283
+ },
2284
+ all: {
2285
+ eq: [],
2286
+ sort: ["id"]
2287
+ }
2288
+ }
2289
+ });
2290
+ /** One compiled catalog per scope; the update check reads exactly this row. */
2291
+ const releaseCatalogs = defineTable({
2292
+ scope_key: {
2293
+ type: "string",
2294
+ maxLength: 2048,
2295
+ ascii: true
2296
+ },
2297
+ catalog_id: {
2298
+ type: "string",
2299
+ maxLength: 255
2300
+ },
2301
+ strategy: {
2302
+ type: "string",
2303
+ maxLength: 16
2304
+ },
2305
+ channel_id: {
2306
+ ...CHANNEL_ID,
2307
+ references: {
2308
+ model: "channels",
2309
+ onDelete: "none"
2310
+ }
2311
+ },
2312
+ channel_key: {
2313
+ type: "string",
2314
+ maxLength: 1400,
2315
+ ascii: true
2316
+ },
2317
+ platform: {
2318
+ type: "string",
2319
+ maxLength: 16
2320
+ },
2321
+ fingerprint_hash: {
2322
+ type: "string",
2323
+ maxLength: 255,
2324
+ required: false
2325
+ },
2326
+ generation: { type: "integer" },
2327
+ payload: { type: "string" },
2328
+ catalog_hash: {
2329
+ type: "string",
2330
+ maxLength: 71
2331
+ },
2332
+ byte_size: { type: "integer" },
2333
+ is_tombstone: { type: "boolean" },
2334
+ updated_at_ms: { type: "integer" }
2335
+ }, {
2336
+ key: ["scope_key"],
2337
+ indexes: { all: {
2338
+ eq: [],
2339
+ sort: ["scope_key"]
2340
+ } }
2341
+ });
2342
+ const channels = defineTable({
2343
+ id: CHANNEL_ID,
2344
+ name: {
2345
+ type: "string",
2346
+ maxLength: 255,
2347
+ unique: true
2348
+ }
2349
+ }, {
2350
+ key: ["id"],
2351
+ indexes: { all: {
2352
+ eq: [],
2353
+ sort: ["name"]
2354
+ } }
2355
+ });
2356
+ /**
2357
+ * The native builds `native-build android` and `native-build ios` track, newest first
2358
+ * in each scope: a platform, a channel, and the fingerprint or app version
2359
+ * the build compiled in. `min_bundle_id` names the build's built-in bundle,
2360
+ * a `bundles` row no release publishes. Its references keep no counters, so
2361
+ * adding this table changes no existing one; a channel with builds is
2362
+ * refused deletion through `byChannel`. `build_key` is the native build key
2363
+ * of every input the artifact was built from (`nbk1:` and a SHA-256), which
2364
+ * a build with the same inputs reuses it by; a build registered without
2365
+ * one is never reused. `build_number` is the artifact's `CFBundleVersion`
2366
+ * or `versionCode`. `submitted_to`, `submitted_at_ms` and
2367
+ * `submitted_version` record the store track a build went to, when, and
2368
+ * the store's label of the build; they stay null for a build no store took.
2369
+ */
2370
+ const nativeBuilds = defineTable({
2371
+ id: {
2372
+ type: "string",
2373
+ maxLength: 36
2374
+ },
2375
+ scope_key: {
2376
+ type: "string",
2377
+ maxLength: 2048,
2378
+ ascii: true
2379
+ },
2380
+ platform: {
2381
+ type: "string",
2382
+ maxLength: 16
2383
+ },
2384
+ channel_id: {
2385
+ ...CHANNEL_ID,
2386
+ references: {
2387
+ model: "channels",
2388
+ onDelete: "none"
2389
+ }
2390
+ },
2391
+ fingerprint_hash: {
2392
+ type: "string",
2393
+ maxLength: 255,
2394
+ required: false
2395
+ },
2396
+ app_version: {
2397
+ type: "string",
2398
+ required: false
2399
+ },
2400
+ min_bundle_id: {
2401
+ type: "string",
2402
+ maxLength: 36,
2403
+ unique: true,
2404
+ references: {
2405
+ model: "bundles",
2406
+ onDelete: "none"
2407
+ }
2408
+ },
2409
+ artifact_kind: {
2410
+ type: "string",
2411
+ maxLength: 16
2412
+ },
2413
+ artifact_storage_uri: { type: "string" },
2414
+ byte_size: { type: "integer" },
2415
+ git_commit_hash: {
2416
+ type: "string",
2417
+ maxLength: 255,
2418
+ required: false
2419
+ },
2420
+ created_at_ms: { type: "integer" },
2421
+ build_key: {
2422
+ type: "string",
2423
+ maxLength: 69,
2424
+ ascii: true,
2425
+ required: false
2426
+ },
2427
+ build_number: {
2428
+ type: "string",
2429
+ maxLength: 64,
2430
+ required: false
2431
+ },
2432
+ submitted_to: {
2433
+ type: "string",
2434
+ maxLength: 16,
2435
+ required: false
2436
+ },
2437
+ submitted_at_ms: {
2438
+ type: "integer",
2439
+ required: false
2440
+ },
2441
+ submitted_version: {
2442
+ type: "string",
2443
+ maxLength: 255,
2444
+ required: false
2445
+ }
2446
+ }, {
2447
+ key: ["id"],
2448
+ indexes: {
2449
+ byScope: {
2450
+ eq: ["scope_key"],
2451
+ sort: ["id"]
2452
+ },
2453
+ byBuildKey: {
2454
+ eq: ["scope_key", "build_key"],
2455
+ sort: ["id"]
2456
+ },
2457
+ byChannel: {
2458
+ eq: ["channel_id"],
2459
+ sort: ["id"],
2460
+ root: { model: "channels" }
2461
+ },
2462
+ all: {
2463
+ eq: [],
2464
+ sort: ["id"]
2465
+ }
2466
+ }
2467
+ });
2069
2468
  /** Core's module: always installed, never a plugin. */
2070
2469
  const coreModule = {
2071
2470
  id: "core",
2072
2471
  schema: {
2073
- bundles: defineTable({
2074
- id: {
2075
- type: "string",
2076
- maxLength: 36
2077
- },
2078
- platform: {
2079
- type: "string",
2080
- maxLength: 16
2081
- },
2082
- git_commit_hash: {
2083
- type: "string",
2084
- maxLength: 255,
2085
- required: false
2086
- },
2087
- metadata: { type: "json" },
2088
- manifest_storage_uri: { type: "string" },
2089
- manifest_file_hash: { type: "string" },
2090
- asset_base_storage_uri: { type: "string" }
2091
- }, {
2092
- key: ["id"],
2093
- indexes: {
2094
- byPlatform: {
2095
- eq: ["platform"],
2096
- sort: ["id"]
2097
- },
2098
- all: {
2099
- eq: [],
2100
- sort: ["id"]
2101
- }
2102
- }
2103
- }),
2104
- bundle_patches: defineTable({
2105
- id: {
2106
- type: "string",
2107
- maxLength: 255
2108
- },
2109
- bundle_id: {
2110
- type: "string",
2111
- maxLength: 36,
2112
- references: {
2113
- model: "bundles",
2114
- onDelete: "cascade"
2115
- }
2116
- },
2117
- base_bundle_id: {
2118
- type: "string",
2119
- maxLength: 36,
2120
- references: {
2121
- model: "bundles",
2122
- onDelete: "cascade"
2123
- }
2124
- },
2125
- base_file_hash: { type: "string" },
2126
- patch_file_hash: { type: "string" },
2127
- patch_storage_uri: { type: "string" },
2128
- byte_size: { type: "integer" },
2129
- order_index: { type: "integer" }
2130
- }, {
2131
- key: ["id"],
2132
- indexes: {
2133
- pair: {
2134
- eq: ["bundle_id", "base_bundle_id"],
2135
- sort: [],
2136
- unique: true
2137
- },
2138
- byBundle: {
2139
- eq: ["bundle_id"],
2140
- sort: ["order_index"],
2141
- root: { model: "bundles" }
2142
- },
2143
- byBase: {
2144
- eq: ["base_bundle_id"],
2145
- sort: ["bundle_id"]
2146
- }
2147
- }
2148
- }),
2149
- releases: defineTable({
2150
- id: {
2151
- type: "string",
2152
- maxLength: 36
2153
- },
2154
- revision: { type: "integer" },
2155
- scope_key: {
2156
- type: "string",
2157
- maxLength: 2048,
2158
- ascii: true
2159
- },
2160
- channel_id: {
2161
- ...CHANNEL_ID,
2162
- references: {
2163
- model: "channels",
2164
- onDelete: "restrict"
2165
- }
2166
- },
2167
- platform: {
2168
- type: "string",
2169
- maxLength: 16
2170
- },
2171
- kind: {
2172
- type: "string",
2173
- maxLength: 16
2174
- },
2175
- bundle_id: {
2176
- type: "string",
2177
- maxLength: 36,
2178
- required: false,
2179
- references: {
2180
- model: "bundles",
2181
- onDelete: "restrict"
2182
- }
2183
- },
2184
- strategy: {
2185
- type: "string",
2186
- maxLength: 16
2187
- },
2188
- target_app_version: {
2189
- type: "string",
2190
- required: false
2191
- },
2192
- fingerprint_hash: {
2193
- type: "string",
2194
- maxLength: 255,
2195
- required: false
2196
- },
2197
- enabled: { type: "boolean" },
2198
- should_force_update: { type: "boolean" },
2199
- message: {
2200
- type: "string",
2201
- required: false
2202
- },
2203
- rollout_cohort_count: { type: "integer" },
2204
- target_cohorts: { type: "json" },
2205
- operation: {
2206
- type: "string",
2207
- maxLength: 16
2208
- },
2209
- source_release_id: {
2210
- type: "string",
2211
- maxLength: 36,
2212
- required: false,
2213
- references: {
2214
- model: "releases",
2215
- onDelete: "none"
2216
- }
2217
- },
2218
- created_at_ms: { type: "integer" },
2219
- updated_at_ms: { type: "integer" }
2220
- }, {
2221
- key: ["id"],
2222
- indexes: {
2223
- byScope: {
2224
- eq: ["scope_key"],
2225
- sort: ["id"],
2226
- root: { model: "release_catalogs" }
2227
- },
2228
- byScopeEnabled: {
2229
- eq: ["scope_key", "enabled"],
2230
- sort: ["id"],
2231
- root: { model: "release_catalogs" }
2232
- },
2233
- byChannelPlatform: {
2234
- eq: ["channel_id", "platform"],
2235
- sort: ["id"]
2236
- },
2237
- byChannelPlatformEnabled: {
2238
- eq: [
2239
- "channel_id",
2240
- "platform",
2241
- "enabled"
2242
- ],
2243
- sort: ["id"]
2244
- },
2245
- byBundle: {
2246
- eq: ["bundle_id"],
2247
- sort: ["id"]
2248
- },
2249
- all: {
2250
- eq: [],
2251
- sort: ["id"]
2252
- }
2253
- }
2254
- }),
2255
- release_catalogs: defineTable({
2256
- scope_key: {
2257
- type: "string",
2258
- maxLength: 2048,
2259
- ascii: true
2260
- },
2261
- catalog_id: {
2262
- type: "string",
2263
- maxLength: 255
2264
- },
2265
- strategy: {
2266
- type: "string",
2267
- maxLength: 16
2268
- },
2269
- channel_id: {
2270
- ...CHANNEL_ID,
2271
- references: {
2272
- model: "channels",
2273
- onDelete: "none"
2274
- }
2275
- },
2276
- channel_key: {
2277
- type: "string",
2278
- maxLength: 1400,
2279
- ascii: true
2280
- },
2281
- platform: {
2282
- type: "string",
2283
- maxLength: 16
2284
- },
2285
- fingerprint_hash: {
2286
- type: "string",
2287
- maxLength: 255,
2288
- required: false
2289
- },
2290
- generation: { type: "integer" },
2291
- payload: { type: "string" },
2292
- catalog_hash: {
2293
- type: "string",
2294
- maxLength: 71
2295
- },
2296
- byte_size: { type: "integer" },
2297
- is_tombstone: { type: "boolean" },
2298
- updated_at_ms: { type: "integer" }
2299
- }, {
2300
- key: ["scope_key"],
2301
- indexes: { all: {
2302
- eq: [],
2303
- sort: ["scope_key"]
2304
- } }
2305
- }),
2306
- channels: defineTable({
2307
- id: CHANNEL_ID,
2308
- name: {
2309
- type: "string",
2310
- maxLength: 255,
2311
- unique: true
2312
- }
2313
- }, {
2314
- key: ["id"],
2315
- indexes: { all: {
2316
- eq: [],
2317
- sort: ["name"]
2318
- } }
2319
- }),
2472
+ bundles,
2473
+ bundle_patches: bundlePatches,
2474
+ releases,
2475
+ release_catalogs: releaseCatalogs,
2476
+ channels,
2320
2477
  bundle_totals: defineAggregate({ platform_key: {
2321
2478
  type: "string",
2322
2479
  maxLength: 16
@@ -2327,7 +2484,8 @@ const coreModule = {
2327
2484
  eq: ["platform_key"],
2328
2485
  sort: []
2329
2486
  } }
2330
- })
2487
+ }),
2488
+ native_builds: nativeBuilds
2331
2489
  }
2332
2490
  };
2333
2491
  //#endregion
@@ -3442,14 +3600,14 @@ const createEngineMigrator = (options) => {
3442
3600
  return {
3443
3601
  getVersion,
3444
3602
  getNameVariants: async () => void 0,
3445
- next: async () => await getVersion() === "1.0.0" ? void 0 : { version: HOT_UPDATER_SCHEMA_VERSION },
3603
+ next: async () => await getVersion() === "1.2.0" ? void 0 : { version: HOT_UPDATER_SCHEMA_VERSION },
3446
3604
  previous: async () => void 0,
3447
3605
  up: makeResult,
3448
3606
  down: async () => {
3449
3607
  throw new Error("No previous schema to migrate to.");
3450
3608
  },
3451
3609
  migrateTo: async (version, migrate) => {
3452
- if (version !== "1.0.0") throw new Error(`Invalid version ${version}`);
3610
+ if (version !== "1.2.0") throw new Error(`Invalid version ${version}`);
3453
3611
  return makeResult(migrate);
3454
3612
  },
3455
3613
  migrateToLatest: makeResult
@@ -4181,9 +4339,9 @@ const createDatabaseEngine = ({ batching, now = Date.now, meter, ...options }) =
4181
4339
  */
4182
4340
  const pluginTablesFix = (database) => {
4183
4341
  const { createMigrator, generateSchema } = database;
4184
- const managed = "A managed server runs only its provider's plugins, and rerunning `hot-updater init --provider <provider>` applies the provider's migrations.";
4342
+ 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.";
4185
4343
  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.";
4186
- 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;
4344
+ 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.`;
4187
4345
  };
4188
4346
  /**
4189
4347
  * The storage engine over a database, as a server runs it: core's tables
@@ -4525,7 +4683,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
4525
4683
  };
4526
4684
  //#endregion
4527
4685
  //#region ../../packages/server/dist/version.mjs
4528
- const HOT_UPDATER_SERVER_VERSION = "1.1.4";
4686
+ const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.1";
4529
4687
  //#endregion
4530
4688
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
4531
4689
  /**
@@ -5318,8 +5476,8 @@ const baseBundleIdsOf = (row, range, bundleId, limit) => {
5318
5476
  //#endregion
5319
5477
  //#region ../../packages/server/dist/core/reads.mjs
5320
5478
  const PAGE$3 = 500;
5321
- const pick = (fields) => (row) => Object.fromEntries(fields.map((field) => [field, row[field]]));
5322
- const toBundleRow = pick([
5479
+ const pick$1 = (fields) => (row) => Object.fromEntries(fields.map((field) => [field, row[field]]));
5480
+ const toBundleRow = pick$1([
5323
5481
  "id",
5324
5482
  "platform",
5325
5483
  "git_commit_hash",
@@ -5328,7 +5486,7 @@ const toBundleRow = pick([
5328
5486
  "manifest_file_hash",
5329
5487
  "asset_base_storage_uri"
5330
5488
  ]);
5331
- const toPatchRow = pick([
5489
+ const toPatchRow = pick$1([
5332
5490
  "id",
5333
5491
  "bundle_id",
5334
5492
  "base_bundle_id",
@@ -5338,7 +5496,7 @@ const toPatchRow = pick([
5338
5496
  "byte_size",
5339
5497
  "order_index"
5340
5498
  ]);
5341
- const toReleaseRow = pick([
5499
+ const toReleaseRow = pick$1([
5342
5500
  "id",
5343
5501
  "revision",
5344
5502
  "scope_key",
@@ -5359,7 +5517,7 @@ const toReleaseRow = pick([
5359
5517
  "created_at_ms",
5360
5518
  "updated_at_ms"
5361
5519
  ]);
5362
- const toCatalogRow = pick([
5520
+ const toCatalogRow = pick$1([
5363
5521
  "scope_key",
5364
5522
  "catalog_id",
5365
5523
  "strategy",
@@ -5374,7 +5532,7 @@ const toCatalogRow = pick([
5374
5532
  "is_tombstone",
5375
5533
  "updated_at_ms"
5376
5534
  ]);
5377
- const toChannelRow = pick(["id", "name"]);
5535
+ const toChannelRow = pick$1(["id", "name"]);
5378
5536
  /** Every page of one read, in order. */
5379
5537
  const drain$1 = async (read) => {
5380
5538
  const rows = [];
@@ -5386,6 +5544,8 @@ const drain$1 = async (read) => {
5386
5544
  } while (cursor !== void 0);
5387
5545
  return rows;
5388
5546
  };
5547
+ /** A keyset page's range: the keys strictly after `after`, in `order`. */
5548
+ const after = (input) => input.after === void 0 ? {} : { range: (input.order ?? "asc") === "asc" ? { gt: input.after } : { lt: input.after } };
5389
5549
  /** Core's reads, each through one declared index or key. */
5390
5550
  const createCoreReads = (db, storage) => {
5391
5551
  /** A bundle row's patches, exactly as many as its reference counter holds. */
@@ -5410,7 +5570,6 @@ const createCoreReads = (db, storage) => {
5410
5570
  childCount: row._refs_bundle_patches_base_bundle_id ?? 0
5411
5571
  };
5412
5572
  };
5413
- const after = (input) => input.after === void 0 ? {} : { range: (input.order ?? "asc") === "asc" ? { gt: input.after } : { lt: input.after } };
5414
5573
  return {
5415
5574
  /** The update check: one point read of its scope's catalog. */
5416
5575
  async getReleaseCatalog(input) {
@@ -5601,9 +5760,13 @@ const insertBundle = (tx, row) => {
5601
5760
  tx.create("bundles", row);
5602
5761
  countBundle(tx, row.platform, 1);
5603
5762
  };
5604
- const updateBundle = (tx, current, set) => {
5763
+ /**
5764
+ * Updates a bundle; a platform change moves it between platform totals,
5765
+ * except for a native build's built-in bundle, which they never count.
5766
+ */
5767
+ const updateBundle = (tx, current, set, { counted = true } = {}) => {
5605
5768
  tx.update("bundles", current, set);
5606
- if (set.platform !== void 0 && set.platform !== current.platform) {
5769
+ if (counted && set.platform !== void 0 && set.platform !== current.platform) {
5607
5770
  countBundle(tx, current.platform, -1);
5608
5771
  countBundle(tx, set.platform, 1);
5609
5772
  }
@@ -5664,7 +5827,7 @@ const insertChannel = async (db, row) => {
5664
5827
  throw error;
5665
5828
  }
5666
5829
  };
5667
- /** Deletes a channel no release uses: 1 read + 1 write. */
5830
+ /** Deletes a channel no release or native build uses: 1 read, 1 query, 1 write. */
5668
5831
  const deleteChannel = (db, id) => db.transaction(async (tx) => {
5669
5832
  const row = await tx.findOne("channels", { id });
5670
5833
  if (row === null) return {
@@ -5675,10 +5838,243 @@ const deleteChannel = (db, id) => db.transaction(async (tx) => {
5675
5838
  deleted: false,
5676
5839
  reason: "not_empty"
5677
5840
  };
5841
+ const { rows: builds } = await tx.findMany("native_builds", {
5842
+ index: "byChannel",
5843
+ where: { channel_id: id },
5844
+ limit: 1
5845
+ });
5846
+ if (builds.length > 0) return {
5847
+ deleted: false,
5848
+ reason: "not_empty"
5849
+ };
5678
5850
  await tx.delete("channels", row);
5679
5851
  return { deleted: true };
5680
5852
  });
5681
5853
  //#endregion
5854
+ //#region ../../packages/server/dist/core/nativeBuilds.mjs
5855
+ /** The artifacts each platform's build produces. */
5856
+ const ARTIFACT_KINDS = {
5857
+ android: ["apk", "aab"],
5858
+ ios: ["ipa", "app"]
5859
+ };
5860
+ /** `native_builds.scope_key`'s bound in core's schema. */
5861
+ const SCOPE_KEY_MAX_LENGTH = 2048;
5862
+ /** The `git_commit_hash` bound of `bundles` and `native_builds`. */
5863
+ const GIT_COMMIT_HASH_MAX_LENGTH = 255;
5864
+ /**
5865
+ * A native build key: `nbk1:` and a SHA-256, as the CLI computes it. The
5866
+ * format is the CLI's convention, implemented here so the registry stores
5867
+ * and looks up nothing else.
5868
+ */
5869
+ const BUILD_KEY = /^nbk1:[0-9a-f]{64}$/;
5870
+ /** A native build or scope core cannot store or look up: the caller's input. */
5871
+ var NativeBuildInputError = class extends TypeError {
5872
+ name = "NativeBuildInputError";
5873
+ };
5874
+ const pick = (row) => ({
5875
+ id: row.id,
5876
+ scope_key: row.scope_key,
5877
+ platform: row.platform,
5878
+ channel_id: row.channel_id,
5879
+ fingerprint_hash: row.fingerprint_hash ?? null,
5880
+ app_version: row.app_version ?? null,
5881
+ min_bundle_id: row.min_bundle_id,
5882
+ artifact_kind: row.artifact_kind,
5883
+ artifact_storage_uri: row.artifact_storage_uri,
5884
+ byte_size: row.byte_size,
5885
+ git_commit_hash: row.git_commit_hash ?? null,
5886
+ created_at_ms: row.created_at_ms,
5887
+ build_key: row.build_key ?? null,
5888
+ build_number: row.build_number ?? null,
5889
+ submitted_to: row.submitted_to ?? null,
5890
+ submitted_at_ms: row.submitted_at_ms ?? null,
5891
+ submitted_version: row.submitted_version ?? null
5892
+ });
5893
+ /** The store track each platform's builds are submitted to. */
5894
+ const SUBMISSION_DESTINATIONS = {
5895
+ android: "play-internal",
5896
+ ios: "testflight"
5897
+ };
5898
+ /** `native_builds.build_number`'s bound in core's schema. */
5899
+ const BUILD_NUMBER_MAX_LENGTH = 64;
5900
+ /** `native_builds.submitted_version`'s bound in core's schema. */
5901
+ const SUBMITTED_VERSION_MAX_LENGTH = 255;
5902
+ /**
5903
+ * A native build scope's key: the Release Catalog scope key of a
5904
+ * fingerprint, or of the app-version strategy followed by the exact app
5905
+ * version the build compiled in. One key serves both strategies, so one
5906
+ * index finds the newest build of either.
5907
+ */
5908
+ const nativeBuildScopeKey = (scope) => {
5909
+ let scopeKey;
5910
+ try {
5911
+ const channelKey = encodeChannelKey(scope.channel);
5912
+ scopeKey = scope.fingerprintHash === null ? `${createReleaseCatalogScopeKey({
5913
+ channelKey,
5914
+ platform: scope.platform,
5915
+ strategy: "APP_VERSION"
5916
+ })}:${encodeURIComponent(scope.appVersion)}` : createReleaseCatalogScopeKey({
5917
+ channelKey,
5918
+ fingerprintHash: scope.fingerprintHash,
5919
+ platform: scope.platform,
5920
+ strategy: "FINGERPRINT"
5921
+ });
5922
+ } catch (error) {
5923
+ throw new NativeBuildInputError(`Invalid native build scope: ${error instanceof Error ? error.message : String(error)}.`);
5924
+ }
5925
+ 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.`);
5926
+ return scopeKey;
5927
+ };
5928
+ /** Whether `bundleId` is a native build's built-in bundle: one unique read. */
5929
+ const isBuiltInBundle = async (db, bundleId) => await db.findOne("native_builds", { min_bundle_id: bundleId }) !== null;
5930
+ const problemOf = ({ id, minBundleId, platform, channel, fingerprintHash, appVersion, gitCommitHash, buildKey, buildNumber, artifact }) => {
5931
+ if (!isUUIDv7(id)) return "id must be a UUIDv7";
5932
+ if (!isUUIDv7(minBundleId)) return "minBundleId must be the UUIDv7 a release build compiles in";
5933
+ if (platform !== "ios" && platform !== "android") return "platform must be ios or android";
5934
+ if (channel.trim().length === 0) return "channel is required";
5935
+ if (gitCommitHash !== null && gitCommitHash.length > GIT_COMMIT_HASH_MAX_LENGTH) return `gitCommitHash must be at most ${GIT_COMMIT_HASH_MAX_LENGTH} characters`;
5936
+ if (fingerprintHash === null === (appVersion === null)) return "exactly one of fingerprintHash and appVersion is required";
5937
+ if (buildKey !== null && !BUILD_KEY.test(buildKey)) return "buildKey must be nbk1: and a SHA-256, or null";
5938
+ 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`;
5939
+ const kinds = ARTIFACT_KINDS[platform];
5940
+ if (!kinds.includes(artifact.kind)) return `artifact.kind of an ${platform} build must be ${kinds.join(" or ")}`;
5941
+ if (!Number.isSafeInteger(artifact.byteSize) || artifact.byteSize < 0) return "artifact.byteSize must be a byte count";
5942
+ return null;
5943
+ };
5944
+ /** The native build registry: core's `native_builds` and the built-in bundle rows. */
5945
+ const createNativeBuildApi = (db, { now = Date.now } = {}) => ({
5946
+ async registerNativeBuild(registration) {
5947
+ const problem = problemOf(registration);
5948
+ if (problem !== null) throw new NativeBuildInputError(`Invalid native build: ${problem}.`);
5949
+ const scopeKey = nativeBuildScopeKey(registration);
5950
+ const channelId = `channel:${encodeChannelKey(registration.channel)}`;
5951
+ const { embeddedBundle } = registration;
5952
+ const row = {
5953
+ id: registration.id,
5954
+ scope_key: scopeKey,
5955
+ platform: registration.platform,
5956
+ channel_id: channelId,
5957
+ fingerprint_hash: registration.fingerprintHash,
5958
+ app_version: registration.appVersion,
5959
+ min_bundle_id: registration.minBundleId,
5960
+ artifact_kind: registration.artifact.kind,
5961
+ artifact_storage_uri: registration.artifact.storageUri,
5962
+ byte_size: registration.artifact.byteSize,
5963
+ git_commit_hash: registration.gitCommitHash,
5964
+ created_at_ms: now(),
5965
+ build_key: registration.buildKey,
5966
+ build_number: registration.buildNumber ?? null,
5967
+ submitted_to: null,
5968
+ submitted_at_ms: null,
5969
+ submitted_version: null
5970
+ };
5971
+ for (let attempt = 1;; attempt += 1) {
5972
+ await insertChannel(db, {
5973
+ id: channelId,
5974
+ name: registration.channel
5975
+ });
5976
+ try {
5977
+ await db.transaction(async (tx) => {
5978
+ tx.create("bundles", {
5979
+ id: registration.minBundleId,
5980
+ platform: registration.platform,
5981
+ git_commit_hash: registration.gitCommitHash,
5982
+ metadata: embeddedBundle.metadata ?? {},
5983
+ manifest_storage_uri: embeddedBundle.manifestStorageUri,
5984
+ manifest_file_hash: embeddedBundle.manifestFileHash,
5985
+ asset_base_storage_uri: embeddedBundle.assetBaseStorageUri
5986
+ });
5987
+ tx.create("native_builds", row);
5988
+ });
5989
+ break;
5990
+ } catch (error) {
5991
+ if (!(error instanceof DatabaseConstraintError)) throw error;
5992
+ if (error.reason === "exists" || error.reason === "unique") throw new DatabaseConstraintError(error.reason, "native_builds");
5993
+ if (attempt < 3 && error.reason === "not_found" && error.model === "channels") continue;
5994
+ throw error;
5995
+ }
5996
+ }
5997
+ return row;
5998
+ },
5999
+ async getNativeBuild(id) {
6000
+ const row = await db.findOne("native_builds", { id });
6001
+ return row === null ? null : pick(row);
6002
+ },
6003
+ async findNativeBuild(scope, options) {
6004
+ const scopeKey = nativeBuildScopeKey(scope);
6005
+ const buildKey = options?.buildKey;
6006
+ if (buildKey !== void 0 && !BUILD_KEY.test(buildKey)) throw new NativeBuildInputError("Invalid native build key: it must be nbk1: and a SHA-256.");
6007
+ const { rows } = buildKey === void 0 ? await db.findMany("native_builds", {
6008
+ index: "byScope",
6009
+ where: { scope_key: scopeKey },
6010
+ order: "desc",
6011
+ limit: 1
6012
+ }) : await db.findMany("native_builds", {
6013
+ index: "byBuildKey",
6014
+ where: {
6015
+ scope_key: scopeKey,
6016
+ build_key: buildKey
6017
+ },
6018
+ order: "desc",
6019
+ limit: 1
6020
+ });
6021
+ return rows[0] === void 0 ? null : pick(rows[0]);
6022
+ },
6023
+ async recordNativeBuildSubmission(id, { destination, version }) {
6024
+ 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.`);
6025
+ return db.transaction(async (tx) => {
6026
+ const row = await tx.findOne("native_builds", { id });
6027
+ if (row === null) return null;
6028
+ const build = pick(row);
6029
+ 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}.`);
6030
+ const set = {
6031
+ submitted_to: destination,
6032
+ submitted_at_ms: now(),
6033
+ submitted_version: version
6034
+ };
6035
+ tx.update("native_builds", row, set);
6036
+ return {
6037
+ ...build,
6038
+ ...set
6039
+ };
6040
+ });
6041
+ },
6042
+ async deleteNativeBuild(id) {
6043
+ return db.transaction(async (tx) => {
6044
+ const row = await tx.findOne("native_builds", { id });
6045
+ if (row === null) return {
6046
+ deleted: false,
6047
+ reason: "not_found"
6048
+ };
6049
+ await tx.delete("native_builds", row);
6050
+ const bundle = await tx.findOne("bundles", { id: row.min_bundle_id });
6051
+ if (bundle !== null) await tx.delete("bundles", bundle);
6052
+ return {
6053
+ deleted: true,
6054
+ nativeBuild: pick(row)
6055
+ };
6056
+ });
6057
+ },
6058
+ async listNativeBuilds(input) {
6059
+ const page = {
6060
+ order: input.order ?? "asc",
6061
+ limit: input.limit
6062
+ };
6063
+ const { rows } = input.channelId === void 0 ? await db.findMany("native_builds", {
6064
+ index: "all",
6065
+ where: {},
6066
+ ...page,
6067
+ ...after(input)
6068
+ }) : await db.findMany("native_builds", {
6069
+ index: "byChannel",
6070
+ where: { channel_id: input.channelId },
6071
+ ...page,
6072
+ ...after(input)
6073
+ });
6074
+ return rows.map(pick);
6075
+ }
6076
+ });
6077
+ //#endregion
5682
6078
  //#region ../../packages/server/dist/core/releases.mjs
5683
6079
  const PAGE$1 = 500;
5684
6080
  const fail$1 = (code, message) => {
@@ -5992,6 +6388,7 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
5992
6388
  if (!("bundleId" in deployment) || platforms.has(deployment.bundleId)) continue;
5993
6389
  const row = await db.findOne("bundles", { id: deployment.bundleId });
5994
6390
  if (row === null) throw new DatabaseBundleNotFoundError(deployment.bundleId);
6391
+ 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.`);
5995
6392
  platforms.set(deployment.bundleId, toBundleRow(row).platform);
5996
6393
  }
5997
6394
  const updatedAtMs = now();
@@ -6187,7 +6584,8 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
6187
6584
  id
6188
6585
  };
6189
6586
  const { id: _id, ...set } = bundleToRow(next);
6190
- updateBundle(tx, current, set);
6587
+ const counted = set.platform === current.platform || !await isBuiltInBundle(tx, id);
6588
+ updateBundle(tx, current, set, { counted });
6191
6589
  if (patches !== void 0) await replaceBundlePatches(tx, current, bundleToPatchRows({
6192
6590
  ...next,
6193
6591
  patches
@@ -6198,7 +6596,9 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
6198
6596
  await db.transaction(async (tx) => {
6199
6597
  for (const id of new Set(ids)) {
6200
6598
  const current = await tx.findOne("bundles", { id });
6201
- if (current !== null) await deleteBundle(tx, current);
6599
+ if (current === null) continue;
6600
+ if (await isBuiltInBundle(tx, id)) throw new DatabaseRowReferencedError();
6601
+ await deleteBundle(tx, current);
6202
6602
  }
6203
6603
  });
6204
6604
  } catch (error) {
@@ -6250,6 +6650,7 @@ const createCoreApi = (db, storage, options = {}) => {
6250
6650
  return {
6251
6651
  ...reads,
6252
6652
  ...createCoreOperations(purgingCachedRoutes(db, options.onCachedRoutesChange), options),
6653
+ ...createNativeBuildApi(db, options),
6253
6654
  /** One read, which checks the schema settings first. */
6254
6655
  ready: async () => {
6255
6656
  await reads.listReleaseCatalogs({ limit: 1 });
@@ -6450,6 +6851,10 @@ const answer = async (operation) => {
6450
6851
  code: "REFERENCED",
6451
6852
  error: error.message
6452
6853
  }, 409);
6854
+ if (error instanceof DatabaseConstraintError && error.model === "native_builds") return json$2({
6855
+ code: "NATIVE_BUILD_EXISTS",
6856
+ error: "A native build with this ID or built-in bundle ID is already registered."
6857
+ }, 409);
6453
6858
  throw error;
6454
6859
  }
6455
6860
  };
@@ -6552,6 +6957,42 @@ const bundleUpdateOf = (value, bundleId) => {
6552
6957
  if (id !== void 0 && id !== bundleId) throw new HandlerBadRequestError("Bundle id mismatch");
6553
6958
  return update;
6554
6959
  };
6960
+ const NATIVE_BUILD_ARTIFACT_KINDS = /* @__PURE__ */ new Set([
6961
+ "apk",
6962
+ "aab",
6963
+ "ipa",
6964
+ "app"
6965
+ ]);
6966
+ /** A native build's scope: `fingerprintHash` or `appVersion`, never both. */
6967
+ const nativeBuildScopeOf = (url) => {
6968
+ const params = url.searchParams;
6969
+ const platform = platformOf(url);
6970
+ const channel = params.get("channel");
6971
+ const fingerprintHash = params.get("fingerprintHash");
6972
+ const appVersion = params.get("appVersion");
6973
+ 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.");
6974
+ return fingerprintHash !== null ? {
6975
+ platform,
6976
+ channel,
6977
+ fingerprintHash,
6978
+ appVersion: null
6979
+ } : {
6980
+ platform,
6981
+ channel,
6982
+ fingerprintHash: null,
6983
+ appVersion
6984
+ };
6985
+ };
6986
+ /** A native build or scope core refuses: a bad request. */
6987
+ const refusingInvalid = async (operation) => {
6988
+ try {
6989
+ return await operation();
6990
+ } catch (error) {
6991
+ if (error instanceof NativeBuildInputError) throw new HandlerBadRequestError(error.message);
6992
+ throw error;
6993
+ }
6994
+ };
6995
+ 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));
6555
6996
  const scopeKeyOf = (params) => decodeMaybe(requireRouteParam(params, "scopeKey")) ?? "";
6556
6997
  /** Every admin route's handler, by the name `ADMIN_ROUTES` mounts it under. */
6557
6998
  const createAdminRouteHandlers = () => ({
@@ -6671,6 +7112,37 @@ const createAdminRouteHandlers = () => ({
6671
7112
  }) });
6672
7113
  }),
6673
7114
  preflightReleaseCatalogRebuild: onCore(async (core, _request, params) => json$2({ data: await core.preflightReleaseCatalogRebuild(scopeKeyOf(params)) })),
7115
+ registerNativeBuild: onCore(async (core, request) => {
7116
+ const input = await body(request);
7117
+ if (!isNativeBuildRegistration(input)) throw new HandlerBadRequestError("Invalid native build");
7118
+ return json$2({ data: await refusingInvalid(() => core.registerNativeBuild(input)) }, 201);
7119
+ }),
7120
+ findNativeBuild: onCore(async (core, request) => {
7121
+ const url = new URL(request.url);
7122
+ const scope = nativeBuildScopeOf(url);
7123
+ const buildKey = url.searchParams.get("buildKey");
7124
+ return json$2({ data: await refusingInvalid(() => buildKey === null ? core.findNativeBuild(scope) : core.findNativeBuild(scope, { buildKey })) });
7125
+ }),
7126
+ getNativeBuild: onCore(async (core, _request, params) => json$2({ data: await core.getNativeBuild(requireRouteParam(params, "id")) })),
7127
+ listNativeBuilds: onCore(async (core, request) => {
7128
+ const url = new URL(request.url);
7129
+ const input = keyset(url);
7130
+ const channelId = url.searchParams.get("channelId");
7131
+ return page(await core.listNativeBuilds({
7132
+ ...input,
7133
+ ...channelId === null ? {} : { channelId }
7134
+ }), input, ({ id }) => id);
7135
+ }),
7136
+ recordNativeBuildSubmission: onCore(async (core, request, params) => {
7137
+ const input = await body(request);
7138
+ 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.");
7139
+ const submission = {
7140
+ destination: input.destination,
7141
+ version: input.version
7142
+ };
7143
+ return json$2({ data: await refusingInvalid(() => core.recordNativeBuildSubmission(requireRouteParam(params, "id"), submission)) });
7144
+ }),
7145
+ deleteNativeBuild: onCore(async (core, _request, params) => json$2({ data: await core.deleteNativeBuild(requireRouteParam(params, "id")) })),
6674
7146
  deleteBundles: onCore(async (core, request) => {
6675
7147
  const { ids } = await body(request);
6676
7148
  if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && id.length > 0)) throw new HandlerBadRequestError("Bundle deletion needs ids");
@@ -6789,6 +7261,36 @@ const ADMIN_ROUTES = [
6789
7261
  method: "GET",
6790
7262
  path: "/base-candidates/:candidateKey",
6791
7263
  handler: "findBaseCandidates"
7264
+ },
7265
+ {
7266
+ method: "POST",
7267
+ path: "/native-builds",
7268
+ handler: "registerNativeBuild"
7269
+ },
7270
+ {
7271
+ method: "GET",
7272
+ path: "/native-builds/latest",
7273
+ handler: "findNativeBuild"
7274
+ },
7275
+ {
7276
+ method: "GET",
7277
+ path: "/native-builds/:id",
7278
+ handler: "getNativeBuild"
7279
+ },
7280
+ {
7281
+ method: "GET",
7282
+ path: "/native-builds",
7283
+ handler: "listNativeBuilds"
7284
+ },
7285
+ {
7286
+ method: "POST",
7287
+ path: "/native-builds/:id/submission",
7288
+ handler: "recordNativeBuildSubmission"
7289
+ },
7290
+ {
7291
+ method: "DELETE",
7292
+ path: "/native-builds/:id",
7293
+ handler: "deleteNativeBuild"
6792
7294
  }
6793
7295
  ];
6794
7296
  //#endregion
@@ -11618,11 +12120,20 @@ const adapterOf = ({ databaseId, collection = FIREBASE_V1_COLLECTION, aggregateB
11618
12120
  collection
11619
12121
  }) });
11620
12122
  };
12123
+ /**
12124
+ * The resource this database reaches, without credentials, under the key
12125
+ * `hot-updater doctor` reads to tell the database `hot-updater init` saved
12126
+ * from another (a convention CLAUDE.md documents).
12127
+ */
12128
+ const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
11621
12129
  /** Hot Updater's database in one Firestore collection, through the storage engine. */
11622
- const firebaseDatabase = (config) => createEngineDatabase({
11623
- name: "firebaseDatabase",
11624
- adapter: adapterOf(config),
11625
- aggregateBatching: config.aggregateBatching ?? {}
12130
+ const firebaseDatabase = (config) => ({
12131
+ ...createEngineDatabase({
12132
+ name: "firebaseDatabase",
12133
+ adapter: adapterOf(config),
12134
+ aggregateBatching: config.aggregateBatching ?? {}
12135
+ }),
12136
+ ...config.projectId === void 0 ? {} : { [DATABASE_TARGET]: `firestore/${config.projectId}/${config.databaseId ?? "(default)"}` }
11626
12137
  });
11627
12138
  //#endregion
11628
12139
  //#region src/firebaseStorage.ts
@@ -11630,7 +12141,8 @@ const firebaseDatabase = (config) => createEngineDatabase({
11630
12141
  const MAX_CACHED_SIGNED_URLS = 1e4;
11631
12142
  const firebaseStorage = (config) => {
11632
12143
  const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
11633
- const bucket = (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
12144
+ let opened;
12145
+ const bucket = () => opened ??= (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
11634
12146
  const getStorageKey = createStorageKeyBuilder(config.basePath);
11635
12147
  /**
11636
12148
  * Signed URLs by object. Without a private key, as in Cloud Functions, each
@@ -11641,7 +12153,7 @@ const firebaseStorage = (config) => {
11641
12153
  const signedUrls = /* @__PURE__ */ new Map();
11642
12154
  const signUrl = async (key) => {
11643
12155
  const expiresAt = Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3;
11644
- const [url] = await bucket.file(key).getSignedUrl({
12156
+ const [url] = await bucket().file(key).getSignedUrl({
11645
12157
  action: "read",
11646
12158
  expires: expiresAt
11647
12159
  });
@@ -11674,11 +12186,11 @@ const firebaseStorage = (config) => {
11674
12186
  protocol: "gs",
11675
12187
  async put({ key, body, contentType }) {
11676
12188
  const storageKey = getStorageKey(key);
11677
- const bytes = new Uint8Array(await new Response(body).arrayBuffer());
11678
- await bucket.file(storageKey).save(bytes, { metadata: {
12189
+ const [{ Readable }, { pipeline }] = await Promise.all([import("node:stream"), import("node:stream/promises")]);
12190
+ await pipeline(Readable.fromWeb(body), bucket().file(storageKey).createWriteStream({ metadata: {
11679
12191
  contentType,
11680
12192
  cacheControl: "public, max-age=31536000, immutable"
11681
- } });
12193
+ } }));
11682
12194
  return { storageUri: createStorageUri({
11683
12195
  bucket: config.storageBucket,
11684
12196
  key: storageKey,
@@ -11688,7 +12200,7 @@ const firebaseStorage = (config) => {
11688
12200
  async get({ storageUri }) {
11689
12201
  const { key } = parseAndValidate(storageUri);
11690
12202
  try {
11691
- const file = bucket.file(key);
12203
+ const file = bucket().file(key);
11692
12204
  const [[body], [metadata]] = await Promise.all([file.download(), file.getMetadata()]);
11693
12205
  const headers = new Headers();
11694
12206
  if (metadata.contentType) headers.set("content-type", metadata.contentType);
@@ -11713,12 +12225,12 @@ const firebaseStorage = (config) => {
11713
12225
  },
11714
12226
  async exists({ storageUri }) {
11715
12227
  const { key } = parseAndValidate(storageUri);
11716
- const [exists] = await bucket.file(key).exists();
12228
+ const [exists] = await bucket().file(key).exists();
11717
12229
  return { exists };
11718
12230
  },
11719
12231
  async delete({ storageUri }) {
11720
12232
  const { key } = parseAndValidate(storageUri);
11721
- await bucket.file(key).delete({ ignoreNotFound: true });
12233
+ await bucket().file(key).delete({ ignoreNotFound: true });
11722
12234
  return { deleted: true };
11723
12235
  }
11724
12236
  });