@hot-updater/firebase 0.36.6 → 1.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  import { createRequire } from "node:module";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
- import { ConfigBuilder, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, MissingInitInputsError, assertInitProviderInputs, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, readHotUpdaterInitEnv, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolvePackageVersion, transformEnv, transformTemplate, writeHotUpdaterConfig } from "@hot-updater/cli-tools";
4
+ import { ConfigBuilder, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, InitError, MissingInitInputsError, assertInfrastructureGenerationAtUrl, assertInitProviderInputs, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, formatApiKeyNote, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, readHotUpdaterInitEnv, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolvePackageVersion, transformEnv, transformTemplate, writeHotUpdaterConfig } from "@hot-updater/cli-tools";
5
+ import { provisionApiKey } from "@hot-updater/server";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
7
8
  import { StringDecoder } from "node:string_decoder";
@@ -17,7 +18,12 @@ import { appendFileSync, createReadStream, createWriteStream, readFileSync, stat
17
18
  import { finished } from "node:stream/promises";
18
19
  import { Duplex, PassThrough, Readable, Transform, Writable, getDefaultHighWaterMark } from "node:stream";
19
20
  import { Buffer as Buffer$1 } from "node:buffer";
20
- import fs$1 from "node:fs/promises";
21
+ import { applicationDefault, cert, deleteApp, getApp, getApps, initializeApp } from "firebase-admin/app";
22
+ import { createDatabasePlugin, isDatabaseMetadataObject } from "@hot-updater/plugin-core";
23
+ import { createDatabasePluginAdapter } from "@hot-updater/plugin-core/internal";
24
+ import { getFirestore } from "firebase-admin/firestore";
25
+ import fs$1 from "fs/promises";
26
+ import fs$2 from "node:fs/promises";
21
27
  //#region \0rolldown/runtime.js
22
28
  var __create = Object.create;
23
29
  var __defProp = Object.defineProperty;
@@ -1317,7 +1323,7 @@ const handleCommand = (filePath, rawArguments, rawOptions) => {
1317
1323
  var require_windows = /* @__PURE__ */ __commonJSMin(((exports, module) => {
1318
1324
  module.exports = isexe;
1319
1325
  isexe.sync = sync;
1320
- var fs$4 = __require("fs");
1326
+ var fs$5 = __require("fs");
1321
1327
  function checkPathExt(path, options) {
1322
1328
  var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
1323
1329
  if (!pathext) return true;
@@ -1334,12 +1340,12 @@ var require_windows = /* @__PURE__ */ __commonJSMin(((exports, module) => {
1334
1340
  return checkPathExt(path, options);
1335
1341
  }
1336
1342
  function isexe(path, options, cb) {
1337
- fs$4.stat(path, function(er, stat) {
1343
+ fs$5.stat(path, function(er, stat) {
1338
1344
  cb(er, er ? false : checkStat(stat, path, options));
1339
1345
  });
1340
1346
  }
1341
1347
  function sync(path, options) {
1342
- return checkStat(fs$4.statSync(path), path, options);
1348
+ return checkStat(fs$5.statSync(path), path, options);
1343
1349
  }
1344
1350
  }));
1345
1351
  //#endregion
@@ -1347,14 +1353,14 @@ var require_windows = /* @__PURE__ */ __commonJSMin(((exports, module) => {
1347
1353
  var require_mode = /* @__PURE__ */ __commonJSMin(((exports, module) => {
1348
1354
  module.exports = isexe;
1349
1355
  isexe.sync = sync;
1350
- var fs$3 = __require("fs");
1356
+ var fs$4 = __require("fs");
1351
1357
  function isexe(path, options, cb) {
1352
- fs$3.stat(path, function(er, stat) {
1358
+ fs$4.stat(path, function(er, stat) {
1353
1359
  cb(er, er ? false : checkStat(stat, options));
1354
1360
  });
1355
1361
  }
1356
1362
  function sync(path, options) {
1357
- return checkStat(fs$3.statSync(path), options);
1363
+ return checkStat(fs$4.statSync(path), options);
1358
1364
  }
1359
1365
  function checkStat(stat, options) {
1360
1366
  return stat.isFile() && checkMode(stat, options);
@@ -1569,16 +1575,16 @@ var require_shebang_command = /* @__PURE__ */ __commonJSMin(((exports, module) =
1569
1575
  //#endregion
1570
1576
  //#region ../../node_modules/.pnpm/cross-spawn@7.0.6/node_modules/cross-spawn/lib/util/readShebang.js
1571
1577
  var require_readShebang = /* @__PURE__ */ __commonJSMin(((exports, module) => {
1572
- const fs$2 = __require("fs");
1578
+ const fs$3 = __require("fs");
1573
1579
  const shebangCommand = require_shebang_command();
1574
1580
  function readShebang(command) {
1575
1581
  const size = 150;
1576
1582
  const buffer = Buffer.alloc(size);
1577
1583
  let fd;
1578
1584
  try {
1579
- fd = fs$2.openSync(command, "r");
1580
- fs$2.readSync(fd, buffer, 0, size, 0);
1581
- fs$2.closeSync(fd);
1585
+ fd = fs$3.openSync(command, "r");
1586
+ fs$3.readSync(fd, buffer, 0, size, 0);
1587
+ fs$3.closeSync(fd);
1582
1588
  } catch (e) {}
1583
1589
  return shebangCommand(buffer.toString());
1584
1590
  }
@@ -6706,6 +6712,777 @@ createExeca(mapNode);
6706
6712
  createExeca(mapScriptAsync, {}, deepScriptOptions, setScriptSync);
6707
6713
  const { sendMessage, getOneMessage, getEachMessage, getCancelSignal } = getIpcExport();
6708
6714
  //#endregion
6715
+ //#region src/firebaseDatabaseParserShared.ts
6716
+ var FirebaseDatabaseDataError = class extends Error {
6717
+ name = "FirebaseDatabaseDataError";
6718
+ constructor(source) {
6719
+ super(`Invalid Firebase database data at "${source}".`);
6720
+ this.source = source;
6721
+ }
6722
+ };
6723
+ const record = (value, source) => {
6724
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new FirebaseDatabaseDataError(source);
6725
+ return value;
6726
+ };
6727
+ const property = (value, key) => Reflect.get(value, key);
6728
+ const string = (value, source) => {
6729
+ if (typeof value !== "string") throw new FirebaseDatabaseDataError(source);
6730
+ return value;
6731
+ };
6732
+ const nullableString = (value, source) => {
6733
+ if (value === null || value === void 0) return null;
6734
+ return string(value, source);
6735
+ };
6736
+ const boolean = (value, source) => {
6737
+ if (typeof value !== "boolean") throw new FirebaseDatabaseDataError(source);
6738
+ return value;
6739
+ };
6740
+ const number = (value, source) => {
6741
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new FirebaseDatabaseDataError(source);
6742
+ return value;
6743
+ };
6744
+ const byteSize = (value, source) => {
6745
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new FirebaseDatabaseDataError(source);
6746
+ return value;
6747
+ };
6748
+ const stringArray = (value, source) => {
6749
+ if (value === null || value === void 0) return null;
6750
+ if (!Array.isArray(value)) throw new FirebaseDatabaseDataError(source);
6751
+ return value.map((item) => string(item, source));
6752
+ };
6753
+ const platform$1 = (value, source) => {
6754
+ if (value === "android" || value === "ios") return value;
6755
+ throw new FirebaseDatabaseDataError(source);
6756
+ };
6757
+ //#endregion
6758
+ //#region src/firebaseDatabaseParser.ts
6759
+ const metadata = (value, source) => {
6760
+ if (!isDatabaseMetadataObject(value)) throw new FirebaseDatabaseDataError(source);
6761
+ return value;
6762
+ };
6763
+ const requiredNullableString = (input, key, source) => {
6764
+ if (!Object.hasOwn(input, key)) throw new FirebaseDatabaseDataError(source);
6765
+ return nullableString(property(input, key), source);
6766
+ };
6767
+ const parseFirebaseBundleRow = (value, source) => {
6768
+ const input = record(value, source);
6769
+ return {
6770
+ id: string(property(input, "id"), source),
6771
+ platform: platform$1(property(input, "platform"), source),
6772
+ file_hash: string(property(input, "file_hash"), source),
6773
+ git_commit_hash: nullableString(property(input, "git_commit_hash"), source),
6774
+ storage_uri: string(property(input, "storage_uri"), source),
6775
+ archive_byte_size: byteSize(property(input, "archive_byte_size"), source),
6776
+ metadata: metadata(property(input, "metadata"), source),
6777
+ manifest_storage_uri: nullableString(property(input, "manifest_storage_uri"), source),
6778
+ manifest_file_hash: nullableString(property(input, "manifest_file_hash"), source),
6779
+ asset_base_storage_uri: nullableString(property(input, "asset_base_storage_uri"), source)
6780
+ };
6781
+ };
6782
+ const parseFirebaseChannelRow = (value, source) => {
6783
+ const input = record(value, source);
6784
+ return {
6785
+ id: string(property(input, "id"), source),
6786
+ name: string(property(input, "name"), source)
6787
+ };
6788
+ };
6789
+ const parseFirebasePatchRow = (value, source) => {
6790
+ const input = record(value, source);
6791
+ return {
6792
+ id: string(property(input, "id"), source),
6793
+ bundle_id: string(property(input, "bundle_id"), source),
6794
+ base_bundle_id: string(property(input, "base_bundle_id"), source),
6795
+ base_file_hash: string(property(input, "base_file_hash"), source),
6796
+ patch_file_hash: string(property(input, "patch_file_hash"), source),
6797
+ patch_storage_uri: string(property(input, "patch_storage_uri"), source),
6798
+ byte_size: byteSize(property(input, "byte_size"), source),
6799
+ order_index: number(property(input, "order_index"), source)
6800
+ };
6801
+ };
6802
+ const parseFirebaseBundleEventRow = (value, source) => {
6803
+ const input = record(value, source);
6804
+ const type = string(property(input, "type"), source);
6805
+ const fromBundleId = requiredNullableString(input, "from_bundle_id", source);
6806
+ const updateStrategy = requiredNullableString(input, "update_strategy", source);
6807
+ if (!((type === "UPDATE_APPLIED" || type === "RECOVERED" || type === "RELEASE_ADOPTED") && typeof fromBundleId === "string" && (updateStrategy === "fingerprint" || updateStrategy === "appVersion") || type === "UNCHANGED" && fromBundleId === null && updateStrategy === null)) throw new FirebaseDatabaseDataError(source);
6808
+ return {
6809
+ id: string(property(input, "id"), source),
6810
+ type,
6811
+ install_id: string(property(input, "install_id"), source),
6812
+ user_id: nullableString(property(input, "user_id"), source),
6813
+ username: nullableString(property(input, "username"), source),
6814
+ from_release_id: requiredNullableString(input, "from_release_id", source),
6815
+ from_bundle_id: fromBundleId,
6816
+ to_release_id: requiredNullableString(input, "to_release_id", source),
6817
+ to_bundle_id: string(property(input, "to_bundle_id"), source),
6818
+ platform: platform$1(property(input, "platform"), source),
6819
+ app_version: string(property(input, "app_version"), source),
6820
+ channel: string(property(input, "channel"), source),
6821
+ cohort: string(property(input, "cohort"), source),
6822
+ update_strategy: updateStrategy,
6823
+ fingerprint_hash: nullableString(property(input, "fingerprint_hash"), source),
6824
+ sdk_version: nullableString(property(input, "sdk_version"), source),
6825
+ received_at_ms: number(property(input, "received_at_ms"), source)
6826
+ };
6827
+ };
6828
+ const parseFirebaseApiKeyRow = (value, source) => {
6829
+ const input = record(value, source);
6830
+ const role = string(property(input, "role"), source);
6831
+ if (role !== "client") throw new FirebaseDatabaseDataError(source);
6832
+ const revokedAt = property(input, "revoked_at_ms");
6833
+ return {
6834
+ id: string(property(input, "id"), source),
6835
+ hash: string(property(input, "hash"), source),
6836
+ name: string(property(input, "name"), source),
6837
+ prefix: string(property(input, "prefix"), source),
6838
+ role,
6839
+ created_at_ms: number(property(input, "created_at_ms"), source),
6840
+ revoked_at_ms: revokedAt === null ? null : number(revokedAt, source)
6841
+ };
6842
+ };
6843
+ const parseFirebaseReleaseRow = (value, source) => {
6844
+ const input = record(value, source);
6845
+ const kind = string(property(input, "kind"), source);
6846
+ const strategy = string(property(input, "strategy"), source);
6847
+ const operation = string(property(input, "operation"), source);
6848
+ const targetCohorts = stringArray(property(input, "target_cohorts"), source);
6849
+ if (kind !== "BUNDLE" && kind !== "EMBEDDED" || strategy !== "APP_VERSION" && strategy !== "FINGERPRINT" || operation !== "DEPLOY" && operation !== "PROMOTE" && operation !== "ROLLBACK" || targetCohorts === null) throw new FirebaseDatabaseDataError(source);
6850
+ return {
6851
+ id: string(property(input, "id"), source),
6852
+ revision: number(property(input, "revision"), source),
6853
+ scope_key: string(property(input, "scope_key"), source),
6854
+ channel_id: string(property(input, "channel_id"), source),
6855
+ platform: platform$1(property(input, "platform"), source),
6856
+ kind,
6857
+ bundle_id: nullableString(property(input, "bundle_id"), source),
6858
+ strategy,
6859
+ target_app_version: nullableString(property(input, "target_app_version"), source),
6860
+ fingerprint_hash: nullableString(property(input, "fingerprint_hash"), source),
6861
+ enabled: boolean(property(input, "enabled"), source),
6862
+ should_force_update: boolean(property(input, "should_force_update"), source),
6863
+ message: nullableString(property(input, "message"), source),
6864
+ rollout_cohort_count: number(property(input, "rollout_cohort_count"), source),
6865
+ target_cohorts: targetCohorts,
6866
+ operation,
6867
+ source_release_id: nullableString(property(input, "source_release_id"), source),
6868
+ created_at_ms: number(property(input, "created_at_ms"), source),
6869
+ updated_at_ms: number(property(input, "updated_at_ms"), source)
6870
+ };
6871
+ };
6872
+ const parseFirebaseReleaseCatalogRow = (value, source) => {
6873
+ const input = record(value, source);
6874
+ const strategy = string(property(input, "strategy"), source);
6875
+ if (strategy !== "APP_VERSION" && strategy !== "FINGERPRINT") throw new FirebaseDatabaseDataError(source);
6876
+ return {
6877
+ scope_key: string(property(input, "scope_key"), source),
6878
+ catalog_id: string(property(input, "catalog_id"), source),
6879
+ strategy,
6880
+ channel_id: string(property(input, "channel_id"), source),
6881
+ channel_key: string(property(input, "channel_key"), source),
6882
+ platform: platform$1(property(input, "platform"), source),
6883
+ fingerprint_hash: nullableString(property(input, "fingerprint_hash"), source),
6884
+ generation: number(property(input, "generation"), source),
6885
+ payload: string(property(input, "payload"), source),
6886
+ catalog_hash: string(property(input, "catalog_hash"), source),
6887
+ byte_size: number(property(input, "byte_size"), source),
6888
+ is_tombstone: boolean(property(input, "is_tombstone"), source),
6889
+ updated_at_ms: number(property(input, "updated_at_ms"), source)
6890
+ };
6891
+ };
6892
+ //#endregion
6893
+ //#region src/firebaseDatabaseQuery.ts
6894
+ const compare = (left, right) => {
6895
+ if (typeof left === "number" && typeof right === "number") return left - right;
6896
+ return String(left).localeCompare(String(right));
6897
+ };
6898
+ const normalizeStringComparison = (actual, expected, mode) => {
6899
+ if (typeof actual !== "string") return null;
6900
+ return mode === "insensitive" ? [actual.toLocaleLowerCase(), expected.toLocaleLowerCase()] : [actual, expected];
6901
+ };
6902
+ const matchesCondition = (row, condition) => {
6903
+ const actual = Reflect.get(row, condition.field);
6904
+ const expected = condition.value;
6905
+ switch (condition.operator ?? "eq") {
6906
+ case "eq": {
6907
+ if (typeof expected !== "string") return actual === expected;
6908
+ const comparison = normalizeStringComparison(actual, expected, "mode" in condition ? condition.mode : void 0);
6909
+ return comparison !== null && comparison[0] === comparison[1];
6910
+ }
6911
+ case "ne": {
6912
+ if (actual === null || actual === void 0) return false;
6913
+ if (typeof expected !== "string") return actual !== expected;
6914
+ const comparison = normalizeStringComparison(actual, expected, "mode" in condition ? condition.mode : void 0);
6915
+ return comparison === null || comparison[0] !== comparison[1];
6916
+ }
6917
+ case "gt":
6918
+ if (actual === null || actual === void 0) return false;
6919
+ return compare(actual, expected) > 0;
6920
+ case "gte":
6921
+ if (actual === null || actual === void 0) return false;
6922
+ return compare(actual, expected) >= 0;
6923
+ case "lt":
6924
+ if (actual === null || actual === void 0) return false;
6925
+ return compare(actual, expected) < 0;
6926
+ case "lte":
6927
+ if (actual === null || actual === void 0) return false;
6928
+ return compare(actual, expected) <= 0;
6929
+ case "in":
6930
+ if (!Array.isArray(expected)) return false;
6931
+ return expected.some((candidate) => candidate === actual);
6932
+ case "not_in": {
6933
+ if (!Array.isArray(expected)) return false;
6934
+ const values = expected;
6935
+ return values.length === 0 || actual !== null && actual !== void 0 && values.every((candidate) => candidate !== actual);
6936
+ }
6937
+ case "contains": {
6938
+ if (typeof expected !== "string") return false;
6939
+ const comparison = normalizeStringComparison(actual, expected, "mode" in condition ? condition.mode : void 0);
6940
+ return comparison?.[0].includes(comparison[1]) ?? false;
6941
+ }
6942
+ case "starts_with": {
6943
+ if (typeof expected !== "string") return false;
6944
+ const comparison = normalizeStringComparison(actual, expected, "mode" in condition ? condition.mode : void 0);
6945
+ return comparison?.[0].startsWith(comparison[1]) ?? false;
6946
+ }
6947
+ case "ends_with": {
6948
+ if (typeof expected !== "string") return false;
6949
+ const comparison = normalizeStringComparison(actual, expected, "mode" in condition ? condition.mode : void 0);
6950
+ return comparison?.[0].endsWith(comparison[1]) ?? false;
6951
+ }
6952
+ }
6953
+ };
6954
+ const matchesFirebaseDatabaseWhere = (row, where) => {
6955
+ const first = where?.[0];
6956
+ if (!first) return true;
6957
+ let result = matchesCondition(row, first);
6958
+ for (const condition of where.slice(1)) {
6959
+ const current = matchesCondition(row, condition);
6960
+ result = condition.connector === "OR" ? result || current : result && current;
6961
+ }
6962
+ return result;
6963
+ };
6964
+ const queryFirebaseDatabaseRows = (rows, input) => {
6965
+ const filtered = rows.filter((row) => matchesFirebaseDatabaseWhere(row, input.where));
6966
+ const orderBy = input.orderBy;
6967
+ if (orderBy !== void 0) filtered.sort((left, right) => {
6968
+ for (const clause of orderBy) {
6969
+ const leftValue = Reflect.get(left, clause.field);
6970
+ const rightValue = Reflect.get(right, clause.field);
6971
+ if (leftValue === rightValue) continue;
6972
+ if (clause.nulls !== void 0 && (leftValue === null || rightValue === null)) {
6973
+ const nullComparison = leftValue === null ? -1 : 1;
6974
+ return clause.nulls === "first" ? nullComparison : -nullComparison;
6975
+ }
6976
+ const comparison = compare(leftValue, rightValue);
6977
+ if (comparison !== 0) return clause.direction === "asc" ? comparison : -comparison;
6978
+ }
6979
+ return 0;
6980
+ });
6981
+ const distinctOn = input.distinctOn;
6982
+ if (distinctOn === void 0) return filtered.slice(input.offset, input.offset + input.limit);
6983
+ const seen = /* @__PURE__ */ new Set();
6984
+ return filtered.filter((row) => {
6985
+ const key = JSON.stringify(distinctOn.fields.map((field) => Reflect.get(row, field)));
6986
+ if (seen.has(key)) return false;
6987
+ seen.add(key);
6988
+ return true;
6989
+ }).slice(input.offset, input.offset + input.limit);
6990
+ };
6991
+ //#endregion
6992
+ //#region src/firebaseDatabaseState.ts
6993
+ var FirebaseDatabaseConstraintError = class extends Error {
6994
+ name = "FirebaseDatabaseConstraintError";
6995
+ constructor(constraint) {
6996
+ super(`Firebase database constraint failed: ${constraint}`);
6997
+ this.constraint = constraint;
6998
+ }
6999
+ };
7000
+ const cloneFirebaseDatabaseSnapshot = (snapshot) => ({
7001
+ bundles: new Map(snapshot.bundles),
7002
+ bundlePatches: new Map(snapshot.bundlePatches),
7003
+ bundleEvents: new Map(snapshot.bundleEvents),
7004
+ channels: new Map(snapshot.channels),
7005
+ apiKeys: new Map(snapshot.apiKeys),
7006
+ releaseCatalogs: new Map(snapshot.releaseCatalogs),
7007
+ releases: new Map(snapshot.releases)
7008
+ });
7009
+ const requireUnique = (rows, id, model) => {
7010
+ if (rows.has(id)) throw new FirebaseDatabaseConstraintError(`${model}.id.unique`);
7011
+ };
7012
+ const distinctCount = (rows, fields) => {
7013
+ if (fields === void 0) return rows.length;
7014
+ return new Set(rows.map((row) => JSON.stringify(fields.map((field) => Reflect.get(row, field))))).size;
7015
+ };
7016
+ const createFirebaseDatabaseState = (snapshot) => ({
7017
+ async create(input) {
7018
+ switch (input.model) {
7019
+ case "bundles":
7020
+ requireUnique(snapshot.bundles, input.data.id, input.model);
7021
+ snapshot.bundles.set(input.data.id, input.data);
7022
+ return input.data;
7023
+ case "bundle_patches":
7024
+ requireUnique(snapshot.bundlePatches, input.data.id, input.model);
7025
+ if (!snapshot.bundles.has(input.data.bundle_id)) throw new FirebaseDatabaseConstraintError("bundle_patches.bundle_id.foreign-key");
7026
+ if (!snapshot.bundles.has(input.data.base_bundle_id)) throw new FirebaseDatabaseConstraintError("bundle_patches.base_bundle_id.foreign-key");
7027
+ snapshot.bundlePatches.set(input.data.id, input.data);
7028
+ return input.data;
7029
+ case "bundle_events":
7030
+ requireUnique(snapshot.bundleEvents, input.data.id, input.model);
7031
+ snapshot.bundleEvents.set(input.data.id, input.data);
7032
+ return input.data;
7033
+ case "releases":
7034
+ requireUnique(snapshot.releases, input.data.id, input.model);
7035
+ if (!snapshot.channels.has(input.data.channel_id)) throw new FirebaseDatabaseConstraintError("releases.channel_id.foreign-key");
7036
+ if (input.data.bundle_id !== null && !snapshot.bundles.has(input.data.bundle_id)) throw new FirebaseDatabaseConstraintError("releases.bundle_id.foreign-key");
7037
+ snapshot.releases.set(input.data.id, input.data);
7038
+ return input.data;
7039
+ case "release_catalogs":
7040
+ if (snapshot.releaseCatalogs.has(input.data.scope_key)) throw new FirebaseDatabaseConstraintError("release_catalogs.scope_key.unique");
7041
+ snapshot.releaseCatalogs.set(input.data.scope_key, input.data);
7042
+ return input.data;
7043
+ case "channels": {
7044
+ const existing = [...snapshot.channels.values()].find(({ name }) => name === input.data.name);
7045
+ if (existing && input.onConflict === "ignore") return existing;
7046
+ requireUnique(snapshot.channels, input.data.id, input.model);
7047
+ if (existing) throw new FirebaseDatabaseConstraintError("channels.name.unique");
7048
+ snapshot.channels.set(input.data.id, input.data);
7049
+ return input.data;
7050
+ }
7051
+ case "api_keys": {
7052
+ const existing = [...snapshot.apiKeys.values()].find(({ hash }) => hash === input.data.hash);
7053
+ if (existing && input.onConflict === "ignore") return existing;
7054
+ requireUnique(snapshot.apiKeys, input.data.id, input.model);
7055
+ if (existing) throw new FirebaseDatabaseConstraintError("api_keys.hash.unique");
7056
+ snapshot.apiKeys.set(input.data.id, input.data);
7057
+ return input.data;
7058
+ }
7059
+ }
7060
+ },
7061
+ async update(input) {
7062
+ if (input.model === "api_keys") {
7063
+ const current = [...snapshot.apiKeys.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where));
7064
+ if (!current) return null;
7065
+ const updated = {
7066
+ ...current,
7067
+ ...input.update
7068
+ };
7069
+ snapshot.apiKeys.set(current.id, updated);
7070
+ return updated;
7071
+ }
7072
+ if (input.model === "releases") {
7073
+ const current = [...snapshot.releases.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where));
7074
+ if (!current) return null;
7075
+ const updated = {
7076
+ ...current,
7077
+ ...input.update
7078
+ };
7079
+ snapshot.releases.set(current.id, updated);
7080
+ return updated;
7081
+ }
7082
+ if (input.model === "release_catalogs") {
7083
+ const current = [...snapshot.releaseCatalogs.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where));
7084
+ if (!current) return null;
7085
+ const updated = {
7086
+ ...current,
7087
+ ...input.update
7088
+ };
7089
+ snapshot.releaseCatalogs.set(current.scope_key, updated);
7090
+ return updated;
7091
+ }
7092
+ const current = [...snapshot.bundles.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where));
7093
+ if (!current) return null;
7094
+ const updated = {
7095
+ ...current,
7096
+ ...input.update
7097
+ };
7098
+ snapshot.bundles.set(current.id, updated);
7099
+ return updated;
7100
+ },
7101
+ async delete(input) {
7102
+ if (input.model === "channels") {
7103
+ for (const row of snapshot.channels.values()) if (matchesFirebaseDatabaseWhere(row, input.where)) snapshot.channels.delete(row.id);
7104
+ return;
7105
+ }
7106
+ if (input.model === "bundle_patches") {
7107
+ for (const row of snapshot.bundlePatches.values()) if (matchesFirebaseDatabaseWhere(row, input.where)) snapshot.bundlePatches.delete(row.id);
7108
+ return;
7109
+ }
7110
+ if (input.model === "releases") {
7111
+ for (const row of snapshot.releases.values()) if (matchesFirebaseDatabaseWhere(row, input.where)) snapshot.releases.delete(row.id);
7112
+ return;
7113
+ }
7114
+ const removedIds = new Set([...snapshot.bundles.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)).map(({ id }) => id));
7115
+ for (const id of removedIds) snapshot.bundles.delete(id);
7116
+ for (const patch of snapshot.bundlePatches.values()) if (removedIds.has(patch.bundle_id) || removedIds.has(patch.base_bundle_id)) snapshot.bundlePatches.delete(patch.id);
7117
+ },
7118
+ async count(input) {
7119
+ switch (input.model) {
7120
+ case "bundles": return distinctCount([...snapshot.bundles.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
7121
+ case "bundle_patches": return distinctCount([...snapshot.bundlePatches.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
7122
+ case "releases": return distinctCount([...snapshot.releases.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
7123
+ }
7124
+ },
7125
+ async findOne(input) {
7126
+ switch (input.model) {
7127
+ case "bundles": return [...snapshot.bundles.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
7128
+ case "api_keys": return [...snapshot.apiKeys.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
7129
+ case "channels": return [...snapshot.channels.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
7130
+ case "bundle_patches": return [...snapshot.bundlePatches.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
7131
+ case "releases": return [...snapshot.releases.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
7132
+ case "release_catalogs": return [...snapshot.releaseCatalogs.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
7133
+ }
7134
+ },
7135
+ async findMany(input) {
7136
+ switch (input.model) {
7137
+ case "bundles": return queryFirebaseDatabaseRows([...snapshot.bundles.values()], input);
7138
+ case "bundle_patches": return queryFirebaseDatabaseRows([...snapshot.bundlePatches.values()], input);
7139
+ case "bundle_events": return queryFirebaseDatabaseRows([...snapshot.bundleEvents.values()], input);
7140
+ case "channels": return queryFirebaseDatabaseRows([...snapshot.channels.values()], input);
7141
+ case "api_keys": return queryFirebaseDatabaseRows([...snapshot.apiKeys.values()], input);
7142
+ case "releases": return queryFirebaseDatabaseRows([...snapshot.releases.values()], input);
7143
+ case "release_catalogs": return queryFirebaseDatabaseRows([...snapshot.releaseCatalogs.values()], input);
7144
+ }
7145
+ }
7146
+ });
7147
+ //#endregion
7148
+ //#region src/firebaseInfrastructureNames.ts
7149
+ const FIREBASE_V1_FUNCTION_NAME = "hot-updater-v1";
7150
+ const FIREBASE_V1_COLLECTION_NAMES = {
7151
+ apiKeys: "hot_updater_v1_api_keys",
7152
+ bundleEvents: "hot_updater_v1_bundle_events",
7153
+ bundlePatches: "hot_updater_v1_bundle_patches",
7154
+ bundles: "hot_updater_v1_bundles",
7155
+ channels: "hot_updater_v1_channels",
7156
+ releaseCatalogs: "hot_updater_v1_release_catalogs",
7157
+ releases: "hot_updater_v1_releases",
7158
+ settings: "hot_updater_v1_private_settings"
7159
+ };
7160
+ //#endregion
7161
+ //#region src/firebaseDatabasePersistence.ts
7162
+ var FirebaseDatabaseAdapterVersionError = class extends Error {
7163
+ name = "FirebaseDatabaseAdapterVersionError";
7164
+ constructor(version) {
7165
+ super(`Unsupported Firebase database adapter version: ${String(version)}`);
7166
+ this.version = version;
7167
+ }
7168
+ };
7169
+ const createFirebaseDatabaseCollections = (db) => ({
7170
+ bundles: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundles),
7171
+ bundlePatches: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundlePatches),
7172
+ bundleEvents: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundleEvents),
7173
+ channels: db.collection(FIREBASE_V1_COLLECTION_NAMES.channels),
7174
+ apiKeys: db.collection(FIREBASE_V1_COLLECTION_NAMES.apiKeys),
7175
+ releaseCatalogs: db.collection(FIREBASE_V1_COLLECTION_NAMES.releaseCatalogs),
7176
+ releases: db.collection(FIREBASE_V1_COLLECTION_NAMES.releases),
7177
+ settings: db.collection(FIREBASE_V1_COLLECTION_NAMES.settings)
7178
+ });
7179
+ const firebaseChannelDocumentId = (name) => `name_${Buffer.from(name, "utf8").toString("base64url")}`;
7180
+ const firebaseChannelIdDocumentId = (id) => `channel_id_${Buffer.from(id, "utf8").toString("base64url")}`;
7181
+ const requireFirebaseDocumentKey = (model, documentId, row) => {
7182
+ if (documentId !== ("id" in row ? row.id : row.scope_key)) throw new FirebaseDatabaseConstraintError(`${model}.id.document-key`);
7183
+ return row;
7184
+ };
7185
+ const documentMap = (model, documents) => {
7186
+ const rows = /* @__PURE__ */ new Map();
7187
+ for (const { row } of documents) {
7188
+ const key = "id" in row ? row.id : row.scope_key;
7189
+ if (rows.has(key)) throw new FirebaseDatabaseConstraintError(`${model}.id.unique`);
7190
+ rows.set(key, row);
7191
+ }
7192
+ for (const { document, row } of documents) requireFirebaseDocumentKey(model, document.id, row);
7193
+ return rows;
7194
+ };
7195
+ const bundleMap = (snapshot) => documentMap("bundles", snapshot.docs.map((document) => ({
7196
+ document,
7197
+ row: parseFirebaseBundleRow(document.data(), `bundles/${document.id}`)
7198
+ })));
7199
+ const patchMap = (snapshot) => documentMap("bundle_patches", snapshot.docs.map((document) => ({
7200
+ document,
7201
+ row: parseFirebasePatchRow(document.data(), `bundle_patches/${document.id}`)
7202
+ })));
7203
+ const eventMap = (snapshot) => documentMap("bundle_events", snapshot.docs.map((document) => ({
7204
+ document,
7205
+ row: parseFirebaseBundleEventRow(document.data(), `bundle_events/${document.id}`)
7206
+ })));
7207
+ const channelMap = (snapshot) => {
7208
+ const rows = /* @__PURE__ */ new Map();
7209
+ const names = /* @__PURE__ */ new Set();
7210
+ for (const document of snapshot.docs) {
7211
+ const row = parseFirebaseChannelRow(document.data(), `channels/${document.id}`);
7212
+ if (document.id !== firebaseChannelDocumentId(row.name)) throw new FirebaseDatabaseConstraintError("channels.name.document-key");
7213
+ if (rows.has(row.id)) throw new FirebaseDatabaseConstraintError("channels.id.unique");
7214
+ if (names.has(row.name)) throw new FirebaseDatabaseConstraintError("channels.name.unique");
7215
+ rows.set(row.id, row);
7216
+ names.add(row.name);
7217
+ }
7218
+ return rows;
7219
+ };
7220
+ const loadFirebaseChannels = async (collections) => [...channelMap(await collections.channels.get()).values()];
7221
+ const apiKeyMap = (snapshot) => documentMap("api_keys", snapshot.docs.map((document) => ({
7222
+ document,
7223
+ row: parseFirebaseApiKeyRow(document.data(), `api_keys/${document.id}`)
7224
+ })));
7225
+ const releaseMap = (snapshot) => documentMap("releases", snapshot.docs.map((document) => ({
7226
+ document,
7227
+ row: parseFirebaseReleaseRow(document.data(), `releases/${document.id}`)
7228
+ })));
7229
+ const releaseCatalogMap = (snapshot) => documentMap("release_catalogs", snapshot.docs.map((document) => ({
7230
+ document,
7231
+ row: parseFirebaseReleaseCatalogRow(document.data(), `release_catalogs/${document.id}`)
7232
+ })));
7233
+ const toSnapshot = (documents) => {
7234
+ return {
7235
+ bundles: bundleMap(documents[0]),
7236
+ bundlePatches: patchMap(documents[1]),
7237
+ bundleEvents: eventMap(documents[2]),
7238
+ channels: channelMap(documents[3]),
7239
+ apiKeys: apiKeyMap(documents[4]),
7240
+ releases: releaseMap(documents[5]),
7241
+ releaseCatalogs: releaseCatalogMap(documents[6])
7242
+ };
7243
+ };
7244
+ const loadFirebaseDatabaseSnapshot = async (collections) => {
7245
+ const [bundles, patches, events, channels, apiKeys, releases, releaseCatalogs] = await Promise.all([
7246
+ collections.bundles.get(),
7247
+ collections.bundlePatches.get(),
7248
+ collections.bundleEvents.get(),
7249
+ collections.channels.get(),
7250
+ collections.apiKeys.get(),
7251
+ collections.releases.get(),
7252
+ collections.releaseCatalogs.get()
7253
+ ]);
7254
+ return toSnapshot([
7255
+ bundles,
7256
+ patches,
7257
+ events,
7258
+ channels,
7259
+ apiKeys,
7260
+ releases,
7261
+ releaseCatalogs
7262
+ ]);
7263
+ };
7264
+ const loadFirebaseTransactionSnapshot = async (transaction, collections) => {
7265
+ const [bundles, patches, events, channels, apiKeys, releases, releaseCatalogs] = await Promise.all([
7266
+ transaction.get(collections.bundles),
7267
+ transaction.get(collections.bundlePatches),
7268
+ transaction.get(collections.bundleEvents),
7269
+ transaction.get(collections.channels),
7270
+ transaction.get(collections.apiKeys),
7271
+ transaction.get(collections.releases),
7272
+ transaction.get(collections.releaseCatalogs)
7273
+ ]);
7274
+ return toSnapshot([
7275
+ bundles,
7276
+ patches,
7277
+ events,
7278
+ channels,
7279
+ apiKeys,
7280
+ releases,
7281
+ releaseCatalogs
7282
+ ]);
7283
+ };
7284
+ const persistCollection = ({ transaction, collection, before, after, documentId }) => {
7285
+ for (const [id, row] of before) if (!after.has(id)) transaction.delete(collection.doc(documentId(row)));
7286
+ for (const [id, row] of after) if (JSON.stringify(before.get(id)) !== JSON.stringify(row)) transaction.set(collection.doc(documentId(row)), row, { merge: true });
7287
+ };
7288
+ const persistFirebaseDatabaseSnapshot = ({ transaction, collections, before, after }) => {
7289
+ persistCollection({
7290
+ transaction,
7291
+ collection: collections.bundles,
7292
+ before: before.bundles,
7293
+ after: after.bundles,
7294
+ documentId: (row) => row.id
7295
+ });
7296
+ persistCollection({
7297
+ transaction,
7298
+ collection: collections.bundlePatches,
7299
+ before: before.bundlePatches,
7300
+ after: after.bundlePatches,
7301
+ documentId: (row) => row.id
7302
+ });
7303
+ persistCollection({
7304
+ transaction,
7305
+ collection: collections.bundleEvents,
7306
+ before: before.bundleEvents,
7307
+ after: after.bundleEvents,
7308
+ documentId: (row) => row.id
7309
+ });
7310
+ persistCollection({
7311
+ transaction,
7312
+ collection: collections.channels,
7313
+ before: before.channels,
7314
+ after: after.channels,
7315
+ documentId: (row) => firebaseChannelDocumentId(row.name)
7316
+ });
7317
+ for (const [id] of before.channels) if (!after.channels.has(id)) transaction.delete(collections.settings.doc(firebaseChannelIdDocumentId(id)));
7318
+ for (const [id, row] of after.channels) if (JSON.stringify(before.channels.get(id)) !== JSON.stringify(row)) transaction.set(collections.settings.doc(firebaseChannelIdDocumentId(id)), row);
7319
+ persistCollection({
7320
+ transaction,
7321
+ collection: collections.apiKeys,
7322
+ before: before.apiKeys,
7323
+ after: after.apiKeys,
7324
+ documentId: (row) => row.id
7325
+ });
7326
+ persistCollection({
7327
+ transaction,
7328
+ collection: collections.releases,
7329
+ before: before.releases,
7330
+ after: after.releases,
7331
+ documentId: (row) => row.id
7332
+ });
7333
+ persistCollection({
7334
+ transaction,
7335
+ collection: collections.releaseCatalogs,
7336
+ before: before.releaseCatalogs,
7337
+ after: after.releaseCatalogs,
7338
+ documentId: (row) => row.scope_key
7339
+ });
7340
+ };
7341
+ const migrateFirebaseDatabase = async (_db, collections) => {
7342
+ const versionDocument = collections.settings.doc("database_adapter_version");
7343
+ const version = await versionDocument.get();
7344
+ const adapterVersion = version.data()?.version;
7345
+ if (adapterVersion === 4) return;
7346
+ if (version.exists) throw new FirebaseDatabaseAdapterVersionError(adapterVersion);
7347
+ if ((await Promise.all([
7348
+ collections.bundles.limit(1).get(),
7349
+ collections.bundlePatches.limit(1).get(),
7350
+ collections.channels.limit(1).get(),
7351
+ collections.releases.limit(1).get(),
7352
+ collections.releaseCatalogs.limit(1).get()
7353
+ ])).some((snapshot) => !snapshot.empty)) throw new FirebaseDatabaseAdapterVersionError("v0");
7354
+ try {
7355
+ await versionDocument.create({ version: 4 });
7356
+ } catch (error) {
7357
+ if ((await versionDocument.get()).data()?.version !== 4) throw error;
7358
+ }
7359
+ };
7360
+ //#endregion
7361
+ //#region src/firebaseDatabase.ts
7362
+ const exactId = (input) => {
7363
+ if (input.where?.length !== 1) return void 0;
7364
+ const [condition] = input.where;
7365
+ return condition.field === "id" && (condition.operator === void 0 || condition.operator === "eq") && typeof condition.value === "string" ? condition.value : void 0;
7366
+ };
7367
+ const firebaseDatabase = (config) => {
7368
+ const adapter = createDatabasePluginAdapter("firebaseDatabase", (() => {
7369
+ const db = getFirestore(getApps().length ? getApp() : initializeApp(config));
7370
+ const collections = createFirebaseDatabaseCollections(db);
7371
+ let migration;
7372
+ const ensureMigrated = () => {
7373
+ migration ??= migrateFirebaseDatabase(db, collections).catch((error) => {
7374
+ migration = void 0;
7375
+ throw error;
7376
+ });
7377
+ return migration;
7378
+ };
7379
+ const mutate = async (operation) => {
7380
+ await ensureMigrated();
7381
+ return db.runTransaction(async (transaction) => {
7382
+ const before = await loadFirebaseTransactionSnapshot(transaction, collections);
7383
+ const after = cloneFirebaseDatabaseSnapshot(before);
7384
+ const result = await operation(createFirebaseDatabaseState(after));
7385
+ persistFirebaseDatabaseSnapshot({
7386
+ transaction,
7387
+ collections,
7388
+ before,
7389
+ after
7390
+ });
7391
+ return result;
7392
+ });
7393
+ };
7394
+ const read = async (operation) => {
7395
+ await ensureMigrated();
7396
+ return operation(createFirebaseDatabaseState(await loadFirebaseDatabaseSnapshot(collections)));
7397
+ };
7398
+ return {
7399
+ create: (input) => mutate((database) => database.create(input)),
7400
+ update: (input) => mutate((database) => database.update(input)),
7401
+ delete: (input) => mutate((database) => database.delete(input)),
7402
+ count: (input) => read((database) => database.count(input)),
7403
+ findOne: async (input) => {
7404
+ const id = exactId(input);
7405
+ if (id === void 0) return read((database) => database.findOne(input));
7406
+ await ensureMigrated();
7407
+ switch (input.model) {
7408
+ case "bundles": {
7409
+ const document = await collections.bundles.doc(id).get();
7410
+ return document.exists ? requireFirebaseDocumentKey("bundles", document.id, parseFirebaseBundleRow(document.data(), `bundles/${document.id}`)) : null;
7411
+ }
7412
+ case "bundle_patches": {
7413
+ const document = await collections.bundlePatches.doc(id).get();
7414
+ return document.exists ? requireFirebaseDocumentKey("bundle_patches", document.id, parseFirebasePatchRow(document.data(), `bundle_patches/${document.id}`)) : null;
7415
+ }
7416
+ case "api_keys": {
7417
+ const document = await collections.apiKeys.doc(id).get();
7418
+ return document.exists ? requireFirebaseDocumentKey("api_keys", document.id, parseFirebaseApiKeyRow(document.data(), `api_keys/${document.id}`)) : null;
7419
+ }
7420
+ default: return read((database) => database.findOne(input));
7421
+ }
7422
+ },
7423
+ findMany: async (input) => {
7424
+ if (input.model !== "channels") return read((database) => database.findMany(input));
7425
+ await ensureMigrated();
7426
+ return queryFirebaseDatabaseRows(await loadFirebaseChannels(collections), input);
7427
+ },
7428
+ insertChannel: async (input) => {
7429
+ await ensureMigrated();
7430
+ return db.runTransaction(async (transaction) => {
7431
+ const reference = collections.channels.doc(firebaseChannelDocumentId(input.row.name));
7432
+ const idReference = collections.settings.doc(firebaseChannelIdDocumentId(input.row.id));
7433
+ const [document, idDocument] = await transaction.getAll(reference, idReference);
7434
+ if (idDocument.exists) {
7435
+ const row = parseFirebaseChannelRow(idDocument.data(), `${FIREBASE_V1_COLLECTION_NAMES.settings}/${idDocument.id}`);
7436
+ if (row.id !== input.row.id || row.name !== input.row.name) throw new FirebaseDatabaseConstraintError("channels.id.registry");
7437
+ }
7438
+ if (idDocument.exists && !document.exists) throw new FirebaseDatabaseConstraintError("channels.id.unique");
7439
+ if (document.exists) {
7440
+ const row = parseFirebaseChannelRow(document.data(), `channels/${document.id}`);
7441
+ if (document.id !== firebaseChannelDocumentId(row.name)) throw new FirebaseDatabaseConstraintError("channels.name.document-key");
7442
+ return {
7443
+ row,
7444
+ inserted: false
7445
+ };
7446
+ }
7447
+ transaction.create(reference, input.row);
7448
+ transaction.create(idReference, input.row);
7449
+ return {
7450
+ row: input.row,
7451
+ inserted: true
7452
+ };
7453
+ });
7454
+ },
7455
+ deleteChannel: async ({ id }) => {
7456
+ await ensureMigrated();
7457
+ return db.runTransaction(async (transaction) => {
7458
+ const idReference = collections.settings.doc(firebaseChannelIdDocumentId(id));
7459
+ const idDocument = await transaction.get(idReference);
7460
+ if (!idDocument.exists) return {
7461
+ deleted: false,
7462
+ reason: "not_found"
7463
+ };
7464
+ const row = parseFirebaseChannelRow(idDocument.data(), `${FIREBASE_V1_COLLECTION_NAMES.settings}/${idDocument.id}`);
7465
+ const reference = collections.channels.doc(firebaseChannelDocumentId(row.name));
7466
+ if (!(await transaction.get(reference)).exists || row.id !== id) throw new FirebaseDatabaseConstraintError("channels.id.registry");
7467
+ if (!(await transaction.get(collections.releases.where("channel_id", "==", id).limit(1))).empty) return {
7468
+ deleted: false,
7469
+ reason: "not_empty"
7470
+ };
7471
+ transaction.delete(reference);
7472
+ transaction.delete(idReference);
7473
+ return { deleted: true };
7474
+ });
7475
+ },
7476
+ transaction: (callback) => mutate(callback)
7477
+ };
7478
+ })());
7479
+ return createDatabasePlugin({
7480
+ name: "firebaseDatabase",
7481
+ models: adapter.models,
7482
+ commit: adapter.commit
7483
+ });
7484
+ };
7485
+ //#endregion
6709
7486
  //#region iac/init/index.ts
6710
7487
  const isFirebaseRegion = (value) => value !== void 0 && /^[a-z]+(?:-[a-z]+)+[0-9]+$/.test(value);
6711
7488
  const isFirebaseProjectId = (value) => value !== void 0 && /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/.test(value);
@@ -6759,6 +7536,53 @@ const inputFirebaseApplicationCredentials = async ({ applicationCredentials, non
6759
7536
  return credentialsPath || void 0;
6760
7537
  };
6761
7538
  //#endregion
7539
+ //#region iac/firebaseInfrastructureState.ts
7540
+ const resolveFirebaseInfrastructureState = ({ adapterVersion, hasData }) => {
7541
+ if (adapterVersion === 4) return "v1";
7542
+ if (adapterVersion !== void 0 || hasData) return "incompatible";
7543
+ return "fresh";
7544
+ };
7545
+ const assertFirebaseInfrastructureCanInitialize = async ({ applicationCredentials, projectId }) => {
7546
+ const app = initializeApp({
7547
+ credential: applicationCredentials ? cert(JSON.parse(await fs$1.readFile(applicationCredentials, "utf8"))) : applicationDefault(),
7548
+ projectId
7549
+ }, `hot-updater-init-${projectId}-${Date.now()}`);
7550
+ try {
7551
+ const db = getFirestore(app);
7552
+ const version = await db.collection(FIREBASE_V1_COLLECTION_NAMES.settings).doc("database_adapter_version").get();
7553
+ const collections = await Promise.all([
7554
+ FIREBASE_V1_COLLECTION_NAMES.bundles,
7555
+ FIREBASE_V1_COLLECTION_NAMES.bundlePatches,
7556
+ FIREBASE_V1_COLLECTION_NAMES.channels,
7557
+ FIREBASE_V1_COLLECTION_NAMES.releaseCatalogs
7558
+ ].map((name) => db.collection(name).limit(1).get()));
7559
+ if (resolveFirebaseInfrastructureState({
7560
+ adapterVersion: version.exists ? version.data()?.version : void 0,
7561
+ hasData: collections.some((snapshot) => !snapshot.empty)
7562
+ }) === "incompatible") throw new InitError(`Firebase v1 infrastructure in project ${projectId} is incomplete or uses an unsupported database version.`);
7563
+ } catch (error) {
7564
+ if (error instanceof InitError) throw error;
7565
+ const code = typeof error === "object" && error !== null ? Reflect.get(error, "code") : void 0;
7566
+ if (code === 5 || code === "not-found") return;
7567
+ throw error;
7568
+ } finally {
7569
+ await deleteApp(app);
7570
+ }
7571
+ };
7572
+ const assertFirebaseFunctionCanInitialize = async ({ fetchImpl, functions }) => {
7573
+ const existingFunctions = functions.filter(({ id }) => id === FIREBASE_V1_FUNCTION_NAME);
7574
+ for (const existingFunction of existingFunctions) {
7575
+ if (!existingFunction.uri) throw new InitError(`Could not verify the Firebase infrastructure generation at Function ${FIREBASE_V1_FUNCTION_NAME}: endpoint URL was not reported.`);
7576
+ const versionUrl = new URL("version", `${existingFunction.uri.replace(/\/$/u, "")}/`).toString();
7577
+ await assertInfrastructureGenerationAtUrl({
7578
+ fetchImpl,
7579
+ provider: "Firebase",
7580
+ resource: `Function ${FIREBASE_V1_FUNCTION_NAME}`,
7581
+ versionUrl
7582
+ });
7583
+ }
7584
+ };
7585
+ //#endregion
6762
7586
  //#region iac/firebaseInitInputs.ts
6763
7587
  const resolveFirebaseCredentialsPath = (credentialsPath, { cwd = process.cwd(), homeDir = os.homedir(), pathApi = path$1 }) => {
6764
7588
  const homeRelativePath = credentialsPath === "~" ? "" : credentialsPath.startsWith("~/") || credentialsPath.startsWith("~\\") ? credentialsPath.slice(2) : void 0;
@@ -6888,7 +7712,7 @@ const resolveFirebaseRegion = async ({ cwd, discoverExistingProject = true, nonI
6888
7712
  if (functionsList.exitCode === 0) {
6889
7713
  const parsed = JSON.parse(functionsList.stdout);
6890
7714
  if (typeof parsed === "object" && parsed !== null && "result" in parsed && Array.isArray(parsed.result)) {
6891
- for (const entry of parsed.result) if (typeof entry === "object" && entry !== null && "id" in entry && entry.id === "hot-updater" && "region" in entry && typeof entry.region === "string") {
7715
+ for (const entry of parsed.result) if (typeof entry === "object" && entry !== null && "id" in entry && entry.id === "hot-updater-v1" && "region" in entry && typeof entry.region === "string") {
6892
7716
  discoveredRegion = entry.region;
6893
7717
  break;
6894
7718
  }
@@ -6915,7 +7739,7 @@ const resolveFirebaseRegion = async ({ cwd, discoverExistingProject = true, nonI
6915
7739
  //#region iac/prepareTemplate.ts
6916
7740
  const ensureExists = async (targetPath, description) => {
6917
7741
  try {
6918
- await fs$1.access(targetPath);
7742
+ await fs$2.access(targetPath);
6919
7743
  } catch {
6920
7744
  throw new Error(`Missing Firebase ${description} at ${targetPath}`);
6921
7745
  }
@@ -6924,7 +7748,7 @@ const normalizeFunctionsPackage = async (functionsDir) => {
6924
7748
  const templatePackagePath = path$1.join(functionsDir, "_package.json");
6925
7749
  const packageJsonPath = path$1.join(functionsDir, "package.json");
6926
7750
  try {
6927
- await fs$1.rename(templatePackagePath, packageJsonPath);
7751
+ await fs$2.rename(templatePackagePath, packageJsonPath);
6928
7752
  } catch (error) {
6929
7753
  if (error.code !== "ENOENT") throw error;
6930
7754
  await ensureExists(packageJsonPath, "functions package.json");
@@ -6937,7 +7761,7 @@ const prepareFirebaseTemplate = async (firebaseRootDir) => {
6937
7761
  await ensureExists(builtFunctionsDir, "functions build directory");
6938
7762
  const { tmpDir, removeTmpDir } = await copyDirToTmp(publicDir);
6939
7763
  const functionsDir = path$1.join(tmpDir, "functions");
6940
- await fs$1.cp(builtFunctionsDir, functionsDir, { recursive: true });
7764
+ await fs$2.cp(builtFunctionsDir, functionsDir, { recursive: true });
6941
7765
  await normalizeFunctionsPackage(functionsDir);
6942
7766
  return {
6943
7767
  tmpDir,
@@ -7210,10 +8034,16 @@ function App() {
7210
8034
  return null; // Replace with your app root
7211
8035
  }
7212
8036
 
7213
- export default HotUpdater.wrap({
8037
+ HotUpdater.init({
7214
8038
  baseURL: "%%source%%",
7215
- updateStrategy: "appVersion", // or "fingerprint"
7216
- })(App);`;
8039
+ requestHeaders: {
8040
+ "x-api-key": %%apiKey%%,
8041
+ },
8042
+ });
8043
+
8044
+ // Call HotUpdater.checkForUpdate({ updateStrategy: "appVersion" })
8045
+ // when your app is ready to check.
8046
+ export default App;`;
7217
8047
  const getFirebaseRuntimePackageInfo = () => {
7218
8048
  const firebasePackageRoot = path.dirname(__require.resolve("@hot-updater/firebase/package.json"));
7219
8049
  return {
@@ -7234,6 +8064,71 @@ const syncFunctionsPackageJson = async (functionsDir) => {
7234
8064
  await fs.promises.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
7235
8065
  return runtimePackageInfo;
7236
8066
  };
8067
+ const commandErrorMessage = (error) => {
8068
+ if (error instanceof ExecaError) {
8069
+ const output = error.stderr || error.stdout;
8070
+ if (output !== void 0 && output !== null) {
8071
+ const text = String(output).trim();
8072
+ if (!text) return error.message;
8073
+ try {
8074
+ const parsed = JSON.parse(text);
8075
+ if (typeof parsed === "object" && parsed !== null) {
8076
+ const message = Reflect.get(parsed, "error");
8077
+ if (typeof message === "string") return message;
8078
+ }
8079
+ } catch {
8080
+ return text;
8081
+ }
8082
+ return text;
8083
+ }
8084
+ }
8085
+ return error instanceof Error ? error.message : String(error);
8086
+ };
8087
+ const ensureCloudFunctionsApiEnabled = async (projectId, cliEnv) => {
8088
+ await p.tasks([{
8089
+ title: "Checking Cloud Functions API",
8090
+ task: async () => {
8091
+ try {
8092
+ await execa("gcloud", [
8093
+ "services",
8094
+ "enable",
8095
+ "cloudfunctions.googleapis.com",
8096
+ `--project=${projectId}`,
8097
+ "--quiet"
8098
+ ], { env: cliEnv });
8099
+ } catch (error) {
8100
+ throw new InitError(`Could not enable the Cloud Functions API for Firebase project ${projectId}: ${commandErrorMessage(error)}`);
8101
+ }
8102
+ return "Cloud Functions API is enabled";
8103
+ }
8104
+ }]);
8105
+ };
8106
+ const listFirebaseFunctions = async (cwd, projectId, nonInteractive, cliEnv) => {
8107
+ let functionsList;
8108
+ try {
8109
+ functionsList = await execa("npx", [
8110
+ "firebase",
8111
+ "functions:list",
8112
+ "--json",
8113
+ "--project",
8114
+ projectId,
8115
+ ...nonInteractive ? ["--non-interactive"] : []
8116
+ ], {
8117
+ cwd,
8118
+ env: cliEnv
8119
+ });
8120
+ } catch (error) {
8121
+ throw new InitError(`Could not list Firebase Functions for project ${projectId}: ${commandErrorMessage(error)}. Run npx firebase functions:list --project ${projectId} --debug for details.`);
8122
+ }
8123
+ let body;
8124
+ try {
8125
+ body = JSON.parse(functionsList.stdout);
8126
+ } catch {
8127
+ throw new InitError("Firebase functions list response was invalid.");
8128
+ }
8129
+ if (typeof body !== "object" || body === null || !Array.isArray(Reflect.get(body, "result"))) throw new InitError("Firebase functions list response was invalid.");
8130
+ return Reflect.get(body, "result");
8131
+ };
7237
8132
  function normalizeIndex(index) {
7238
8133
  return {
7239
8134
  collectionGroup: index.collectionGroup,
@@ -7298,7 +8193,7 @@ const deployFunctions = async (cwd, nonInteractive = false, cliEnv) => {
7298
8193
  "firebase",
7299
8194
  "deploy",
7300
8195
  "--only",
7301
- "functions",
8196
+ `functions:${FIREBASE_V1_FUNCTION_NAME}`,
7302
8197
  ...nonInteractive ? ["--non-interactive"] : []
7303
8198
  ], {
7304
8199
  cwd,
@@ -7311,21 +8206,26 @@ const deployFunctions = async (cwd, nonInteractive = false, cliEnv) => {
7311
8206
  process.exit(1);
7312
8207
  }
7313
8208
  };
7314
- const printTemplate = async (projectId, region, cliEnv) => {
8209
+ const printTemplate = async (apiKey, projectId, region, cliEnv) => {
7315
8210
  try {
7316
- const { stdout } = await execa("gcloud", [
8211
+ const describedFunction = await execa("gcloud", [
7317
8212
  "functions",
7318
8213
  "describe",
7319
- "hot-updater",
8214
+ FIREBASE_V1_FUNCTION_NAME,
7320
8215
  "--project",
7321
8216
  projectId,
7322
8217
  "--region",
7323
8218
  region,
7324
8219
  "--format=json"
7325
8220
  ], { env: cliEnv });
7326
- const parsedData = JSON.parse(stdout);
7327
- const functionUrl = `${parsedData?.serviceConfig?.uri ?? parsedData.url}/api/check-update`;
7328
- p.note(transformTemplate(SOURCE_TEMPLATE, { source: functionUrl }));
8221
+ const functionUrl = JSON.parse(describedFunction.stdout).serviceConfig?.uri;
8222
+ if (!functionUrl) throw new InitError(`Firebase Function ${FIREBASE_V1_FUNCTION_NAME} did not report an endpoint URL.`);
8223
+ p.note(transformTemplate(SOURCE_TEMPLATE, {
8224
+ apiKey: JSON.stringify(apiKey),
8225
+ source: functionUrl
8226
+ }));
8227
+ p.note(formatApiKeyNote(apiKey), "API Key");
8228
+ p.log.message("Store this API key separately in a secure place.");
7329
8229
  } catch (error) {
7330
8230
  if (error instanceof ExecaError) p.log.error(error.stderr || error.stdout || error.message);
7331
8231
  else if (error instanceof Error) p.log.error(error.message);
@@ -7344,7 +8244,8 @@ const runInit = async ({ build, envFile }) => {
7344
8244
  const nonInteractive = envFile !== void 0;
7345
8245
  const initEnvSources = await readHotUpdaterInitEnv(process.cwd(), envFile);
7346
8246
  const { managedEnv } = initEnvSources;
7347
- const savedInputs = resolveFirebaseInitInputs(getHotUpdaterInitInputEnv(initEnvSources, nonInteractive));
8247
+ const initInputEnv = getHotUpdaterInitInputEnv(initEnvSources, nonInteractive);
8248
+ const savedInputs = resolveFirebaseInitInputs(initInputEnv);
7348
8249
  assertFirebaseNonInteractiveInputs(savedInputs, nonInteractive);
7349
8250
  let applicationCredentials = savedInputs.applicationCredentials;
7350
8251
  const cliEnv = nonInteractive ? getFirebaseCliEnv(applicationCredentials) : void 0;
@@ -7365,6 +8266,14 @@ const runInit = async ({ build, envFile }) => {
7365
8266
  });
7366
8267
  return cliEnv;
7367
8268
  });
8269
+ if (initializeVariable.status === "ready") {
8270
+ await assertFirebaseInfrastructureCanInitialize({
8271
+ applicationCredentials,
8272
+ projectId: initializeVariable.projectId
8273
+ });
8274
+ await ensureCloudFunctionsApiEnabled(initializeVariable.projectId, cliEnv);
8275
+ await assertFirebaseFunctionCanInitialize({ functions: await listFirebaseFunctions(tmpDir, initializeVariable.projectId, nonInteractive, cliEnv) });
8276
+ }
7368
8277
  const currentRegion = await resolveFirebaseRegion({
7369
8278
  cwd: tmpDir,
7370
8279
  discoverExistingProject: initializeVariable.status === "ready",
@@ -7421,22 +8330,31 @@ const runInit = async ({ build, envFile }) => {
7421
8330
  }
7422
8331
  }]);
7423
8332
  await deployFirestore(tmpDir, nonInteractive, cliEnv);
8333
+ const credential = applicationCredentials ? cert(JSON.parse(await fs.promises.readFile(applicationCredentials, "utf-8"))) : applicationDefault();
8334
+ const existingApps = new Set(getApps());
8335
+ const databasePlugin = firebaseDatabase({
8336
+ credential,
8337
+ projectId: initializeVariable.projectId
8338
+ });
8339
+ let apiKey;
8340
+ try {
8341
+ apiKey = (await provisionApiKey({
8342
+ apiKeys: databasePlugin.models.apiKeys,
8343
+ existingApiKey: initInputEnv.HOT_UPDATER_API_KEY,
8344
+ name: "Firebase init"
8345
+ })).apiKey;
8346
+ await makeEnv({ HOT_UPDATER_API_KEY: apiKey });
8347
+ } finally {
8348
+ await databasePlugin.dispose?.();
8349
+ await Promise.all(getApps().filter((app) => !existingApps.has(app)).map((app) => deleteApp(app)));
8350
+ }
7424
8351
  await deployFunctions(tmpDir, nonInteractive, cliEnv);
7425
8352
  await p.tasks([{
7426
8353
  title: "Check IAM policy",
7427
8354
  async task(message) {
7428
- const functionsList = await execa("npx", [
7429
- "firebase",
7430
- "functions:list",
7431
- "--json",
7432
- ...nonInteractive ? ["--non-interactive"] : []
7433
- ], {
7434
- cwd: tmpDir,
7435
- env: cliEnv
7436
- });
7437
- const account = (JSON.parse(functionsList.stdout).result || []).find((fn) => fn.id === "hot-updater")?.serviceAccount;
8355
+ const account = (await listFirebaseFunctions(tmpDir, initializeVariable.projectId, nonInteractive, cliEnv)).find((fn) => fn.id === FIREBASE_V1_FUNCTION_NAME)?.serviceAccount;
7438
8356
  if (!account) {
7439
- p.log.error("hot-updater function not found");
8357
+ p.log.error(`${FIREBASE_V1_FUNCTION_NAME} function not found`);
7440
8358
  await removeTmpDir();
7441
8359
  process.exit(1);
7442
8360
  }
@@ -7473,7 +8391,7 @@ const runInit = async ({ build, envFile }) => {
7473
8391
  await removeTmpDir();
7474
8392
  process.exit(1);
7475
8393
  }
7476
- await printTemplate(initializeVariable.projectId, currentRegion, cliEnv);
8394
+ await printTemplate(apiKey, initializeVariable.projectId, currentRegion, cliEnv);
7477
8395
  await removeTmpDir();
7478
8396
  p.log.message(`Next step: ${link("https://hot-updater.dev/docs/managed/firebase#step-3-generated-configurations")}`);
7479
8397
  if (!applicationCredentials) p.log.message("Next step: Change GOOGLE_APPLICATION_CREDENTIALS=your-credentials.json in .env.hotupdater");