@hot-updater/firebase 1.0.0-rc.44 → 1.0.0-rc.46

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.
@@ -216,6 +216,13 @@ const isDatabaseJsonValue = (value) => {
216
216
  const isDatabaseJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && isDatabaseJsonValue(value);
217
217
  const isDatabaseMetadataObject = (value) => isDatabaseJsonObject(value) && (!Object.hasOwn(value, "app_version") || typeof value["app_version"] === "string");
218
218
  //#endregion
219
+ //#region ../plugin-core/dist/databaseMetadata.mjs
220
+ const bundleMetadataToRow = (metadata) => {
221
+ if (metadata === void 0) return {};
222
+ if (!isDatabaseMetadataObject(metadata)) throw new DatabaseAdapterInputError("invalid-data");
223
+ return metadata;
224
+ };
225
+ //#endregion
219
226
  //#region ../../packages/protocol/dist/index.mjs
220
227
  const LETTER_DASH_NUMBER$2 = "[a-zA-Z0-9-]";
221
228
  const NUMERIC_IDENTIFIER$2 = String.raw`0|[1-9]\d*`;
@@ -329,7 +336,6 @@ function canonicalizeAppVersion(appVersion) {
329
336
  const version = coerce$1(appVersion);
330
337
  return version ? normalizeFull(version) : null;
331
338
  }
332
- const stripBundleArtifactMetadata = (metadata) => metadata;
333
339
  const getManifestStorageUri = (bundle) => bundle.manifestStorageUri;
334
340
  const getManifestFileHash = (bundle) => bundle.manifestFileHash;
335
341
  const getAssetBaseStorageUri = (bundle) => bundle.assetBaseStorageUri;
@@ -576,14 +582,6 @@ const NIL_UUID = "00000000-0000-0000-0000-000000000000";
576
582
  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}$/;
577
583
  const isUUIDv7 = (value) => typeof value === "string" && UUID_V7_PATTERN.test(value);
578
584
  //#endregion
579
- //#region ../plugin-core/dist/databaseMetadata.mjs
580
- const bundleMetadataToRow = (metadata) => {
581
- const value = stripBundleArtifactMetadata(metadata);
582
- if (value === void 0) return {};
583
- if (!isDatabaseMetadataObject(value)) throw new DatabaseAdapterInputError("invalid-data");
584
- return value;
585
- };
586
- //#endregion
587
585
  //#region ../plugin-core/dist/databaseRows.mjs
588
586
  const releaseRowToRelease = (row) => ({
589
587
  id: row.id,
@@ -641,7 +639,7 @@ const rowToBundle = (row, patchRows = []) => {
641
639
  id: row.id,
642
640
  platform: row.platform,
643
641
  gitCommitHash: row.git_commit_hash,
644
- metadata: stripBundleArtifactMetadata(row.metadata),
642
+ metadata: row.metadata,
645
643
  manifestStorageUri: row.manifest_storage_uri,
646
644
  manifestFileHash: row.manifest_file_hash,
647
645
  assetBaseStorageUri: row.asset_base_storage_uri,
@@ -649,6 +647,35 @@ const rowToBundle = (row, patchRows = []) => {
649
647
  };
650
648
  };
651
649
  //#endregion
650
+ //#region ../plugin-core/dist/uuidv7.mjs
651
+ const extractTimestampFromUUIDv7 = (uuid) => {
652
+ if (!isUUIDv7(uuid)) throw new TypeError("Expected a canonical lowercase UUIDv7");
653
+ const timestampHex = uuid.split("-").join("").slice(0, 12);
654
+ return Number.parseInt(timestampHex, 16);
655
+ };
656
+ function createUUIDv7FromTimestampHex(timestampHex) {
657
+ const randomBytes = /* @__PURE__ */ new Uint8Array(10);
658
+ crypto.getRandomValues(randomBytes);
659
+ const randomHex = Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
660
+ const randA = randomHex.slice(0, 3);
661
+ const randBHex = randomHex.slice(3, 19);
662
+ const versionAndRandA = `7${randA}`;
663
+ const variantAndFirstRandB = (128 | parseInt(randBHex.slice(0, 2), 16) & 63).toString(16).padStart(2, "0");
664
+ return [
665
+ timestampHex.slice(0, 8),
666
+ timestampHex.slice(8, 12),
667
+ versionAndRandA,
668
+ variantAndFirstRandB + randBHex.slice(2, 4),
669
+ randBHex.slice(4, 16)
670
+ ].join("-");
671
+ }
672
+ const createUUIDv7 = () => createUUIDv7FromTimestampHex(Date.now().toString(16).padStart(12, "0"));
673
+ const createUUIDv7After = (floor, nowMs = Date.now()) => {
674
+ const timestamp = Math.max(nowMs, floor === null ? 0 : extractTimestampFromUUIDv7(floor) + 1);
675
+ if (!Number.isSafeInteger(timestamp) || timestamp < 0 || timestamp >= 2 ** 48) throw new RangeError("UUIDv7 timestamp is out of range");
676
+ return createUUIDv7FromTimestampHex(timestamp.toString(16).padStart(12, "0"));
677
+ };
678
+ //#endregion
652
679
  //#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/comparison-DenM3wCn.mjs
653
680
  const LETTER_DASH_NUMBER$1 = "[a-zA-Z0-9-]";
654
681
  const NUMERIC_IDENTIFIER$1 = String.raw`0|[1-9]\d*`;
@@ -995,42 +1022,6 @@ function coerce(value, options = {}) {
995
1022
  return tryParse(`${major}.${match[3] || "0"}.${match[4] || "0"}${options.includePrerelease && match[5] ? `-${match[5]}` : ""}${options.includePrerelease && match[6] ? `+${match[6]}` : ""}`, options);
996
1023
  }
997
1024
  //#endregion
998
- //#region ../plugin-core/dist/semverSatisfies.mjs
999
- const semverSatisfies = (targetAppVersion, currentVersion) => {
1000
- const currentCoerce = coerce(currentVersion);
1001
- if (!currentCoerce) return false;
1002
- return satisfies(currentCoerce, targetAppVersion);
1003
- };
1004
- //#endregion
1005
- //#region ../plugin-core/dist/uuidv7.mjs
1006
- const extractTimestampFromUUIDv7 = (uuid) => {
1007
- if (!isUUIDv7(uuid)) throw new TypeError("Expected a canonical lowercase UUIDv7");
1008
- const timestampHex = uuid.split("-").join("").slice(0, 12);
1009
- return Number.parseInt(timestampHex, 16);
1010
- };
1011
- function createUUIDv7FromTimestampHex(timestampHex) {
1012
- const randomBytes = /* @__PURE__ */ new Uint8Array(10);
1013
- crypto.getRandomValues(randomBytes);
1014
- const randomHex = Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1015
- const randA = randomHex.slice(0, 3);
1016
- const randBHex = randomHex.slice(3, 19);
1017
- const versionAndRandA = `7${randA}`;
1018
- const variantAndFirstRandB = (128 | parseInt(randBHex.slice(0, 2), 16) & 63).toString(16).padStart(2, "0");
1019
- return [
1020
- timestampHex.slice(0, 8),
1021
- timestampHex.slice(8, 12),
1022
- versionAndRandA,
1023
- variantAndFirstRandB + randBHex.slice(2, 4),
1024
- randBHex.slice(4, 16)
1025
- ].join("-");
1026
- }
1027
- const createUUIDv7 = () => createUUIDv7FromTimestampHex(Date.now().toString(16).padStart(12, "0"));
1028
- const createUUIDv7After = (floor, nowMs = Date.now()) => {
1029
- const timestamp = Math.max(nowMs, floor === null ? 0 : extractTimestampFromUUIDv7(floor) + 1);
1030
- if (!Number.isSafeInteger(timestamp) || timestamp < 0 || timestamp >= 2 ** 48) throw new RangeError("UUIDv7 timestamp is out of range");
1031
- return createUUIDv7FromTimestampHex(timestamp.toString(16).padStart(12, "0"));
1032
- };
1033
- //#endregion
1034
1025
  //#region ../plugin-core/dist/releaseCatalogCompiler.mjs
1035
1026
  var ReleaseCatalogCompilationError = class extends Error {
1036
1027
  code;
@@ -1426,6 +1417,13 @@ var ReleaseCatalogMutationError = class extends Error {
1426
1417
  }
1427
1418
  };
1428
1419
  //#endregion
1420
+ //#region ../plugin-core/dist/semverSatisfies.mjs
1421
+ const semverSatisfies = (targetAppVersion, currentVersion) => {
1422
+ const currentCoerce = coerce(currentVersion);
1423
+ if (!currentCoerce) return false;
1424
+ return satisfies(currentCoerce, targetAppVersion);
1425
+ };
1426
+ //#endregion
1429
1427
  //#region ../plugin-core/dist/serverPlugin/definePlugin.mjs
1430
1428
  /** Declares a plugin; Hot Updater's own plugins and third-party ones use the same contract. */
1431
1429
  const definePlugin = (plugin) => plugin;
@@ -2373,9 +2371,6 @@ const getEmptyMigrationResult = () => ({
2373
2371
  execute: async () => {},
2374
2372
  getSQL: () => ""
2375
2373
  });
2376
- const assertSupportedMigrationMode = (options) => {
2377
- if (options.mode === "from-database") throw new Error("Hot Updater migrations support only mode: 'from-schema'.");
2378
- };
2379
2374
  //#endregion
2380
2375
  //#region ../plugin-core/dist/engine/db/settingsMigrator.mjs
2381
2376
  /** `schema.core`, or a v0 database's `version`. */
@@ -3459,7 +3454,6 @@ const resolveSchema = (modules) => {
3459
3454
  const createEngineMigrator = (options) => {
3460
3455
  const getVersion = async () => storedSchemaVersion(await options.readSettings());
3461
3456
  const makeResult = async (migrate = {}) => {
3462
- assertSupportedMigrationMode(migrate);
3463
3457
  const stored = await options.readSettings();
3464
3458
  refusePreEngineDatabase(options.adapterName, stored);
3465
3459
  if (Object.entries(options.settings).every(([key, value]) => stored.get(key) === value)) return getEmptyMigrationResult();
@@ -4566,7 +4560,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
4566
4560
  };
4567
4561
  //#endregion
4568
4562
  //#region ../../packages/server/dist/version.mjs
4569
- const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.44";
4563
+ const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.46";
4570
4564
  //#endregion
4571
4565
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
4572
4566
  /**
@@ -7112,7 +7106,7 @@ const databaseOf = (value) => {
7112
7106
  if (isAdapter(value.adapter)) return value;
7113
7107
  if ("core" in value && "fetchAdmin" in value) throw new HotUpdaterConfigError("standaloneRepository reaches a server's admin API from the CLI and console. createHotUpdater needs the server's own database, such as kyselyAdapter(...).");
7114
7108
  }
7115
- throw new HotUpdaterConfigError("database must be a Hot Updater 1.0 database, such as kyselyAdapter(...) or postgres(...). Upgrade the provider package that created it.");
7109
+ throw new HotUpdaterConfigError("database must be a Hot Updater database adapter, such as kyselyAdapter(...) or postgres(...).");
7116
7110
  };
7117
7111
  /** The configured storage, when it is a storage adapter. */
7118
7112
  const storageOf = (value) => {
@@ -7179,340 +7173,92 @@ function createHotUpdater(options) {
7179
7173
  };
7180
7174
  }
7181
7175
  //#endregion
7182
- //#region src/firebaseInfrastructureNames.ts
7183
- /**
7184
- * The one collection the storage engine keeps every item in. It namespaces
7185
- * the data, so item partitions are table names with no prefix.
7186
- */
7187
- const FIREBASE_V1_COLLECTION = "hot_updater_v1";
7188
- //#endregion
7189
- //#region src/firestoreStore.ts
7190
- /**
7191
- * Firestore's limits: 10 MiB a request and 1 MiB a document; 500 writes a
7192
- * transaction stays a conservative bound. Indexed values over 1,500 bytes are
7193
- * truncated, so longer keys are refused rather than read inconsistently.
7194
- */
7195
- const FIRESTORE_LIMITS = {
7196
- items: 500,
7197
- bytes: 1e7,
7198
- itemBytes: 1e6,
7199
- keyBytes: {
7200
- pk: 1500,
7201
- sk: 1500
7176
+ //#region ../api-keys/dist/server/apiKeys.mjs
7177
+ const API_KEY_HEADER_NAME = "x-api-key";
7178
+ const BASE64URL_32_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
7179
+ const bytesToBase64Url = (bytes) => {
7180
+ let binary = "";
7181
+ for (const byte of bytes) binary += String.fromCharCode(byte);
7182
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
7183
+ };
7184
+ const isApiKey = (value) => {
7185
+ if (!BASE64URL_32_PATTERN.test(value)) return false;
7186
+ try {
7187
+ const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
7188
+ const binary = atob(`${base64}=`);
7189
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
7190
+ return bytes.byteLength === 32 && bytesToBase64Url(bytes) === value;
7191
+ } catch {
7192
+ return false;
7202
7193
  }
7203
7194
  };
7204
- /** `getAll` reads at most this many documents a call. */
7205
- const GET_BATCH = 300;
7206
- /** A document's id: a hash of its key, since keys may hold `/` and outgrow Firestore's 1,500 bytes. */
7207
- const firestoreDocumentId = ({ pk, sk }) => (0, node_crypto.createHash)("sha256").update(JSON.stringify([pk, sk])).digest("hex");
7208
- /**
7209
- * A document is `{ pk, sk, row }`. Row values that are maps or arrays become
7210
- * JSON text inside a map, since Firestore has no nested arrays and does not
7211
- * keep map key order; `row` is exempt from single-field indexing.
7212
- */
7213
- const encode = (value) => value !== null && typeof value === "object" ? { json: JSON.stringify(value) } : value;
7214
- const decode = (value) => value !== null && typeof value === "object" && "json" in value ? JSON.parse(String(value.json)) : value;
7215
- /**
7216
- * The collection's TTL field: a timestamp beside `pk` and `sk` on every item
7217
- * of a row that expires, which Firestore's TTL policy deletes.
7218
- */
7219
- const FIRESTORE_TTL_FIELD = "expireAt";
7220
- const toDocument = (key, row, expiresAt) => ({
7221
- pk: key.pk,
7222
- sk: key.sk,
7223
- row: Object.fromEntries(Object.entries(row).map(([name, value]) => [name, encode(value)])),
7224
- ...expiresAt === void 0 ? {} : { [FIRESTORE_TTL_FIELD]: firebase_admin_firestore.Timestamp.fromMillis(expiresAt) }
7225
- });
7226
- const toRow$1 = (data) => Object.fromEntries(Object.entries(data.row ?? {}).map(([name, value]) => [name, decode(value)]));
7227
- const holds = (snapshot, condition) => {
7228
- if (condition === void 0) return true;
7229
- const data = snapshot?.exists ? snapshot.data() : void 0;
7230
- return "v" in condition ? data !== void 0 && data.row?._v === condition.v : data !== void 0 === condition.exists;
7195
+ const normalizeApiKeyHeaderName = (value = API_KEY_HEADER_NAME) => {
7196
+ if (typeof value !== "string" || value.length === 0) throw new TypeError("apiKeys({ headerName }) must name a valid header.");
7197
+ try {
7198
+ const headers = new Headers();
7199
+ headers.set(value, "");
7200
+ return headers.keys().next().value;
7201
+ } catch {
7202
+ throw new TypeError("apiKeys({ headerName }) must name a valid header.");
7203
+ }
7231
7204
  };
7232
- /** gRPC codes a rerun of the whole write can clear: DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, ABORTED, INTERNAL, UNAVAILABLE. */
7233
- const TRANSIENT = /* @__PURE__ */ new Set([
7234
- 4,
7235
- 8,
7236
- 10,
7237
- 13,
7238
- 14
7239
- ]);
7240
- const codeOf = (error) => error.code;
7241
- /** Firestore reports a missing database as NOT_FOUND; it reads as a missing schema, like SQL's 42P01. */
7242
- const missingDatabase = (error) => {
7243
- throw codeOf(error) === 5 ? Object.assign(new Error(error.message, { cause: error }), { code: "42P01" }) : error;
7205
+ const hashApiKey = async (apiKey) => {
7206
+ if (!isApiKey(apiKey)) throw new TypeError("API key must be a canonical 32-byte base64url value.");
7207
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(apiKey));
7208
+ return bytesToBase64Url(new Uint8Array(digest));
7209
+ };
7210
+ const apiKeyId = () => {
7211
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
7212
+ crypto.getRandomValues(bytes);
7213
+ return `api-${bytesToBase64Url(bytes)}`;
7214
+ };
7215
+ const normalizeApiKeyName = (name) => {
7216
+ const normalized = name.trim();
7217
+ if (normalized.length === 0 || normalized.length > 64 || /[\p{Cc}\p{Cf}]/u.test(normalized)) throw new TypeError("API key names must contain 1-64 visible characters.");
7218
+ return normalized;
7219
+ };
7220
+ const toApiKeyMetadata = ({ hash: _hash, ...metadata }) => Object.freeze(metadata);
7221
+ const registerApiKey = async (input) => {
7222
+ const hash = await hashApiKey(input.apiKey);
7223
+ const createdAtMs = input.createdAtMs ?? Date.now();
7224
+ if (!Number.isSafeInteger(createdAtMs) || createdAtMs < 0) throw new TypeError("API key creation time must be non-negative.");
7225
+ const record = Object.freeze({
7226
+ created_at_ms: createdAtMs,
7227
+ hash,
7228
+ id: apiKeyId(),
7229
+ name: normalizeApiKeyName(input.name),
7230
+ prefix: input.apiKey.slice(0, 6),
7231
+ revoked_at_ms: null,
7232
+ role: "client"
7233
+ });
7234
+ if (await input.apiKeys.create(record) === "existing") {
7235
+ const existing = await input.apiKeys.findByHash(hash);
7236
+ if (existing === null || existing.revoked_at_ms !== null) throw new Error("The API key has been revoked.");
7237
+ return Object.freeze({
7238
+ apiKey: input.apiKey,
7239
+ record: toApiKeyMetadata(existing)
7240
+ });
7241
+ }
7242
+ return Object.freeze({
7243
+ apiKey: input.apiKey,
7244
+ record: toApiKeyMetadata(record)
7245
+ });
7246
+ };
7247
+ const createApiKey = (input) => {
7248
+ const bytes = /* @__PURE__ */ new Uint8Array(32);
7249
+ crypto.getRandomValues(bytes);
7250
+ return registerApiKey({
7251
+ apiKey: bytesToBase64Url(bytes),
7252
+ apiKeys: input.apiKeys,
7253
+ name: input.name
7254
+ });
7244
7255
  };
7245
7256
  /**
7246
- * The key-value store over one Firestore collection of `{ pk, sk, row }`
7247
- * documents, queried through the composite indexes on (pk, sk) in both
7248
- * directions. A write is one transaction that reads only the documents its
7249
- * ops guard.
7250
- */
7251
- const createFirestoreStore = ({ firestore, collection, nativePageSize }) => {
7252
- const items = firestore.collection(collection);
7253
- const reference = (key) => items.doc(firestoreDocumentId(key));
7254
- return {
7255
- id: "firestore",
7256
- limits: FIRESTORE_LIMITS,
7257
- async get(keys) {
7258
- const found = /* @__PURE__ */ new Map();
7259
- const ids = [...new Set(keys.map(firestoreDocumentId))];
7260
- for (let at = 0; at < ids.length; at += GET_BATCH) {
7261
- const snapshots = await firestore.getAll(...ids.slice(at, at + GET_BATCH).map((id) => items.doc(id))).catch(missingDatabase);
7262
- for (const snapshot of snapshots) if (snapshot.exists) found.set(snapshot.id, toRow$1(snapshot.data()));
7263
- }
7264
- return keys.map((key) => found.get(firestoreDocumentId(key)) ?? null);
7265
- },
7266
- async query({ pk, gte, lt, order, limit, after }) {
7267
- let query = items.where("pk", "==", pk);
7268
- if (gte !== void 0) query = query.where("sk", ">=", gte);
7269
- if (lt !== void 0) query = query.where("sk", "<", lt);
7270
- query = query.orderBy("sk", order);
7271
- if (after !== void 0) query = query.startAfter(after);
7272
- const size = Math.min(limit, nativePageSize ?? limit);
7273
- const { docs } = await query.limit(size).get().catch(missingDatabase);
7274
- return {
7275
- items: docs.map((document) => ({
7276
- sk: document.get("sk"),
7277
- value: toRow$1(document.data())
7278
- })),
7279
- more: docs.length === size
7280
- };
7281
- },
7282
- async write(ops) {
7283
- const blind = ops.map((op) => op.type === "add" && (op.condition === void 0 || "exists" in op.condition && op.condition.exists));
7284
- const attempt = (reading) => firestore.runTransaction(async (transaction) => {
7285
- const read = ops.flatMap((op, position) => (op.condition !== void 0 || op.type === "add") && (reading || !blind[position]) ? [position] : []);
7286
- const snapshots = /* @__PURE__ */ new Map();
7287
- if (read.length > 0) {
7288
- const found = await transaction.getAll(...read.map((position) => reference(ops[position].key)));
7289
- read.forEach((position, at) => snapshots.set(position, found[at]));
7290
- }
7291
- const failed = read.find((position) => !holds(snapshots.get(position), ops[position].condition));
7292
- if (failed !== void 0) {
7293
- if (!reading && blind.slice(0, failed).includes(true)) return null;
7294
- return {
7295
- ok: false,
7296
- failedOp: failed
7297
- };
7298
- }
7299
- for (const [position, op] of ops.entries()) {
7300
- const document = reference(op.key);
7301
- if (op.type === "put") transaction.set(document, toDocument(op.key, op.value, op.expiresAt));
7302
- else if (op.type === "delete") transaction.delete(document);
7303
- else if (op.type === "add") {
7304
- if (snapshots.get(position)?.exists === false) {
7305
- const created = { ...op.init };
7306
- for (const [name, delta] of Object.entries(op.by)) created[name] = Number(created[name] ?? 0) + delta;
7307
- transaction.set(document, toDocument(op.key, created, op.expiresAt));
7308
- } else {
7309
- const [[name, delta], ...more] = Object.entries(op.by);
7310
- const ttl = op.expiresAt === void 0 ? [] : [FIRESTORE_TTL_FIELD, firebase_admin_firestore.Timestamp.fromMillis(op.expiresAt)];
7311
- transaction.update(document, new firebase_admin_firestore.FieldPath("row", name), firebase_admin_firestore.FieldValue.increment(delta), ...more.flatMap(([other, by]) => [new firebase_admin_firestore.FieldPath("row", other), firebase_admin_firestore.FieldValue.increment(by)]), ...ttl);
7312
- }
7313
- }
7314
- }
7315
- return { ok: true };
7316
- }, { maxAttempts: 5 });
7317
- try {
7318
- try {
7319
- const result = await attempt(false);
7320
- if (result !== null) return result;
7321
- } catch (error) {
7322
- if (codeOf(error) !== 5 || !blind.includes(true)) throw error;
7323
- }
7324
- return await attempt(true);
7325
- } catch (error) {
7326
- if (TRANSIENT.has(codeOf(error))) return {
7327
- ok: false,
7328
- retry: true
7329
- };
7330
- return missingDatabase(error);
7331
- }
7332
- },
7333
- /** A plain batch, which reads nothing, 500 deletes at a time. */
7334
- async deleteConsumed(keys) {
7335
- for (let at = 0; at < keys.length; at += FIRESTORE_LIMITS.items) {
7336
- const batch = firestore.batch();
7337
- for (const key of keys.slice(at, at + FIRESTORE_LIMITS.items)) batch.delete(reference(key));
7338
- await batch.commit().catch(missingDatabase);
7339
- }
7340
- },
7341
- async dispose() {}
7342
- };
7343
- };
7344
- //#endregion
7345
- //#region src/firebaseDatabase.ts
7346
- const adapterOf = ({ collection = FIREBASE_V1_COLLECTION, aggregateBatching: _, ...appOptions }) => {
7347
- const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(appOptions);
7348
- return createKvAdapter({ store: createFirestoreStore({
7349
- firestore: (0, firebase_admin_firestore.getFirestore)(app),
7350
- collection
7351
- }) });
7352
- };
7353
- /** Hot Updater's database in one Firestore collection, through the storage engine. */
7354
- const firebaseDatabase = (config) => createEngineDatabase({
7355
- name: "firebaseDatabase",
7356
- adapter: adapterOf(config),
7357
- aggregateBatching: config.aggregateBatching ?? {}
7358
- });
7359
- //#endregion
7360
- //#region src/firebaseStorage.ts
7361
- const firebaseStorage = (config) => {
7362
- const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
7363
- const bucket = (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
7364
- const getStorageKey = createStorageKeyBuilder(config.basePath);
7365
- const parseAndValidate = (storageUri) => {
7366
- const parsed = parseStorageUri(storageUri, "gs");
7367
- if (parsed.bucket !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${parsed.bucket}".`);
7368
- return parsed;
7369
- };
7370
- return createStorageAdapter({
7371
- name: "firebaseStorage",
7372
- protocol: "gs",
7373
- async put({ key, body, contentType }) {
7374
- const storageKey = getStorageKey(key);
7375
- const bytes = new Uint8Array(await new Response(body).arrayBuffer());
7376
- await bucket.file(storageKey).save(bytes, { metadata: {
7377
- contentType,
7378
- cacheControl: "public, max-age=31536000, immutable"
7379
- } });
7380
- return { storageUri: createStorageUri({
7381
- bucket: config.storageBucket,
7382
- key: storageKey,
7383
- protocol: "gs"
7384
- }) };
7385
- },
7386
- async get({ storageUri }) {
7387
- const { key } = parseAndValidate(storageUri);
7388
- try {
7389
- const file = bucket.file(key);
7390
- const [[body], [metadata]] = await Promise.all([file.download(), file.getMetadata()]);
7391
- const headers = new Headers();
7392
- if (metadata.contentType) headers.set("content-type", metadata.contentType);
7393
- headers.set("content-length", String(metadata.size ?? body.byteLength));
7394
- return { response: new Response(body, { headers }) };
7395
- } catch (error) {
7396
- if (typeof error === "object" && error !== null && "code" in error && error.code === 404) return { response: null };
7397
- throw error;
7398
- }
7399
- },
7400
- async getDownloadUrl({ storageUri }) {
7401
- const { key } = parseAndValidate(storageUri);
7402
- if (config.cdnUrl) {
7403
- const storageUrl = new URL(storageUri);
7404
- const downloadUrl = new URL(config.cdnUrl);
7405
- downloadUrl.pathname = `${downloadUrl.pathname.replace(/\/+$/, "")}${storageUrl.pathname}`;
7406
- downloadUrl.search = "";
7407
- downloadUrl.hash = "";
7408
- return { url: downloadUrl.toString() };
7409
- }
7410
- const [url] = await bucket.file(key).getSignedUrl({
7411
- action: "read",
7412
- expires: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3
7413
- });
7414
- if (!url) throw new Error("Failed to generate Firebase download URL");
7415
- return { url };
7416
- },
7417
- async exists({ storageUri }) {
7418
- const { key } = parseAndValidate(storageUri);
7419
- const [exists] = await bucket.file(key).exists();
7420
- return { exists };
7421
- },
7422
- async delete({ storageUri }) {
7423
- const { key } = parseAndValidate(storageUri);
7424
- await bucket.file(key).delete({ ignoreNotFound: true });
7425
- return { deleted: true };
7426
- }
7427
- });
7428
- };
7429
- //#endregion
7430
- //#region ../api-keys/dist/server/apiKeys.mjs
7431
- const API_KEY_HEADER_NAME = "x-api-key";
7432
- const BASE64URL_32_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
7433
- const bytesToBase64Url = (bytes) => {
7434
- let binary = "";
7435
- for (const byte of bytes) binary += String.fromCharCode(byte);
7436
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
7437
- };
7438
- const isApiKey = (value) => {
7439
- if (!BASE64URL_32_PATTERN.test(value)) return false;
7440
- try {
7441
- const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
7442
- const binary = atob(`${base64}=`);
7443
- const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
7444
- return bytes.byteLength === 32 && bytesToBase64Url(bytes) === value;
7445
- } catch {
7446
- return false;
7447
- }
7448
- };
7449
- const normalizeApiKeyHeaderName = (value = API_KEY_HEADER_NAME) => {
7450
- if (typeof value !== "string" || value.length === 0) throw new TypeError("apiKeys({ headerName }) must name a valid header.");
7451
- try {
7452
- const headers = new Headers();
7453
- headers.set(value, "");
7454
- return headers.keys().next().value;
7455
- } catch {
7456
- throw new TypeError("apiKeys({ headerName }) must name a valid header.");
7457
- }
7458
- };
7459
- const hashApiKey = async (apiKey) => {
7460
- if (!isApiKey(apiKey)) throw new TypeError("API key must be a canonical 32-byte base64url value.");
7461
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(apiKey));
7462
- return bytesToBase64Url(new Uint8Array(digest));
7463
- };
7464
- const apiKeyId = () => {
7465
- const bytes = /* @__PURE__ */ new Uint8Array(16);
7466
- crypto.getRandomValues(bytes);
7467
- return `api-${bytesToBase64Url(bytes)}`;
7468
- };
7469
- const normalizeApiKeyName = (name) => {
7470
- const normalized = name.trim();
7471
- if (normalized.length === 0 || normalized.length > 64 || /[\p{Cc}\p{Cf}]/u.test(normalized)) throw new TypeError("API key names must contain 1-64 visible characters.");
7472
- return normalized;
7473
- };
7474
- const toApiKeyMetadata = ({ hash: _hash, ...metadata }) => Object.freeze(metadata);
7475
- const registerApiKey = async (input) => {
7476
- const hash = await hashApiKey(input.apiKey);
7477
- const createdAtMs = input.createdAtMs ?? Date.now();
7478
- if (!Number.isSafeInteger(createdAtMs) || createdAtMs < 0) throw new TypeError("API key creation time must be non-negative.");
7479
- const record = Object.freeze({
7480
- created_at_ms: createdAtMs,
7481
- hash,
7482
- id: apiKeyId(),
7483
- name: normalizeApiKeyName(input.name),
7484
- prefix: input.apiKey.slice(0, 6),
7485
- revoked_at_ms: null,
7486
- role: "client"
7487
- });
7488
- if (await input.apiKeys.create(record) === "existing") {
7489
- const existing = await input.apiKeys.findByHash(hash);
7490
- if (existing === null || existing.revoked_at_ms !== null) throw new Error("The API key has been revoked.");
7491
- return Object.freeze({
7492
- apiKey: input.apiKey,
7493
- record: toApiKeyMetadata(existing)
7494
- });
7495
- }
7496
- return Object.freeze({
7497
- apiKey: input.apiKey,
7498
- record: toApiKeyMetadata(record)
7499
- });
7500
- };
7501
- const createApiKey = (input) => {
7502
- const bytes = /* @__PURE__ */ new Uint8Array(32);
7503
- crypto.getRandomValues(bytes);
7504
- return registerApiKey({
7505
- apiKey: bytesToBase64Url(bytes),
7506
- apiKeys: input.apiKeys,
7507
- name: input.name
7508
- });
7509
- };
7510
- /**
7511
- * Provisions the client API key used by managed init.
7512
- *
7513
- * A saved plaintext key is registered idempotently; otherwise a new key is
7514
- * created. Callers must persist or display the returned plaintext because the
7515
- * database stores only its hash.
7257
+ * Provisions the client API key used by managed init.
7258
+ *
7259
+ * A saved plaintext key is registered idempotently; otherwise a new key is
7260
+ * created. Callers must persist or display the returned plaintext because the
7261
+ * database stores only its hash.
7516
7262
  */
7517
7263
  const provisionApiKey = (input) => {
7518
7264
  const existingApiKey = input.existingApiKey?.trim();
@@ -7576,7 +7322,7 @@ const apiKeysCli = (headerName) => ({ clientCredential: {
7576
7322
  } });
7577
7323
  //#endregion
7578
7324
  //#region ../api-keys/dist/server/model.mjs
7579
- const toRow = (row) => ({
7325
+ const toRow$1 = (row) => ({
7580
7326
  id: row.id,
7581
7327
  hash: row.hash,
7582
7328
  name: row.name,
@@ -7601,7 +7347,7 @@ const createApiKeyModel = (db) => ({
7601
7347
  },
7602
7348
  async findByHash(hash) {
7603
7349
  const row = await db.findOne("api_keys", { hash });
7604
- return row === null ? null : toRow(row);
7350
+ return row === null ? null : toRow$1(row);
7605
7351
  },
7606
7352
  /** Every key, newest first, then by id; the whole list is the result. */
7607
7353
  async list() {
@@ -7615,7 +7361,7 @@ const createApiKeyModel = (db) => ({
7615
7361
  limit: 500,
7616
7362
  ...cursor === void 0 ? {} : { cursor }
7617
7363
  });
7618
- rows.push(...page.rows.map(toRow));
7364
+ rows.push(...page.rows.map(toRow$1));
7619
7365
  cursor = page.next;
7620
7366
  } while (cursor !== void 0);
7621
7367
  return rows.sort((left, right) => right.created_at_ms - left.created_at_ms || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
@@ -7624,7 +7370,7 @@ const createApiKeyModel = (db) => ({
7624
7370
  const row = await tx.findOne("api_keys", { id });
7625
7371
  if (row === null) return null;
7626
7372
  tx.update("api_keys", row, { revoked_at_ms: revokedAtMs });
7627
- return toRow({
7373
+ return toRow$1({
7628
7374
  ...row,
7629
7375
  revoked_at_ms: revokedAtMs
7630
7376
  });
@@ -11586,16 +11332,253 @@ const remoteConfig = () => {
11586
11332
  return markOfficial(plugin);
11587
11333
  };
11588
11334
  //#endregion
11589
- //#region src/plugins.ts
11335
+ //#region src/firebaseInfrastructureNames.ts
11590
11336
  /**
11591
- * The plugins the managed Firebase server runs: Insights, API keys on every
11592
- * client route, and Remote Config. Its Cloud Function imports this list.
11337
+ * The one collection the storage engine keeps every item in. It namespaces
11338
+ * the data, so item partitions are table names with no prefix.
11593
11339
  */
11594
- const plugins = [
11595
- insights(),
11596
- apiKeys(),
11597
- remoteConfig()
11598
- ];
11340
+ const FIREBASE_V1_COLLECTION = "hot_updater_v1";
11341
+ //#endregion
11342
+ //#region src/firestoreStore.ts
11343
+ /**
11344
+ * Firestore's limits: 10 MiB a request and 1 MiB a document; 500 writes a
11345
+ * transaction stays a conservative bound. Indexed values over 1,500 bytes are
11346
+ * truncated, so longer keys are refused rather than read inconsistently.
11347
+ */
11348
+ const FIRESTORE_LIMITS = {
11349
+ items: 500,
11350
+ bytes: 1e7,
11351
+ itemBytes: 1e6,
11352
+ keyBytes: {
11353
+ pk: 1500,
11354
+ sk: 1500
11355
+ }
11356
+ };
11357
+ /** `getAll` reads at most this many documents a call. */
11358
+ const GET_BATCH = 300;
11359
+ /** A document's id: a hash of its key, since keys may hold `/` and outgrow Firestore's 1,500 bytes. */
11360
+ const firestoreDocumentId = ({ pk, sk }) => (0, node_crypto.createHash)("sha256").update(JSON.stringify([pk, sk])).digest("hex");
11361
+ /**
11362
+ * A document is `{ pk, sk, row }`. Row values that are maps or arrays become
11363
+ * JSON text inside a map, since Firestore has no nested arrays and does not
11364
+ * keep map key order; `row` is exempt from single-field indexing.
11365
+ */
11366
+ const encode = (value) => value !== null && typeof value === "object" ? { json: JSON.stringify(value) } : value;
11367
+ const decode = (value) => value !== null && typeof value === "object" && "json" in value ? JSON.parse(String(value.json)) : value;
11368
+ /**
11369
+ * The collection's TTL field: a timestamp beside `pk` and `sk` on every item
11370
+ * of a row that expires, which Firestore's TTL policy deletes.
11371
+ */
11372
+ const FIRESTORE_TTL_FIELD = "expireAt";
11373
+ const toDocument = (key, row, expiresAt) => ({
11374
+ pk: key.pk,
11375
+ sk: key.sk,
11376
+ row: Object.fromEntries(Object.entries(row).map(([name, value]) => [name, encode(value)])),
11377
+ ...expiresAt === void 0 ? {} : { [FIRESTORE_TTL_FIELD]: firebase_admin_firestore.Timestamp.fromMillis(expiresAt) }
11378
+ });
11379
+ const toRow = (data) => Object.fromEntries(Object.entries(data.row ?? {}).map(([name, value]) => [name, decode(value)]));
11380
+ const holds = (snapshot, condition) => {
11381
+ if (condition === void 0) return true;
11382
+ const data = snapshot?.exists ? snapshot.data() : void 0;
11383
+ return "v" in condition ? data !== void 0 && data.row?._v === condition.v : data !== void 0 === condition.exists;
11384
+ };
11385
+ /** gRPC codes a rerun of the whole write can clear: DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, ABORTED, INTERNAL, UNAVAILABLE. */
11386
+ const TRANSIENT = /* @__PURE__ */ new Set([
11387
+ 4,
11388
+ 8,
11389
+ 10,
11390
+ 13,
11391
+ 14
11392
+ ]);
11393
+ const codeOf = (error) => error.code;
11394
+ /** Firestore reports a missing database as NOT_FOUND; it reads as a missing schema, like SQL's 42P01. */
11395
+ const missingDatabase = (error) => {
11396
+ throw codeOf(error) === 5 ? Object.assign(new Error(error.message, { cause: error }), { code: "42P01" }) : error;
11397
+ };
11398
+ /**
11399
+ * The key-value store over one Firestore collection of `{ pk, sk, row }`
11400
+ * documents, queried through the composite indexes on (pk, sk) in both
11401
+ * directions. A write is one transaction that reads only the documents its
11402
+ * ops guard.
11403
+ */
11404
+ const createFirestoreStore = ({ firestore, collection, nativePageSize }) => {
11405
+ const items = firestore.collection(collection);
11406
+ const reference = (key) => items.doc(firestoreDocumentId(key));
11407
+ return {
11408
+ id: "firestore",
11409
+ limits: FIRESTORE_LIMITS,
11410
+ async get(keys) {
11411
+ const found = /* @__PURE__ */ new Map();
11412
+ const ids = [...new Set(keys.map(firestoreDocumentId))];
11413
+ for (let at = 0; at < ids.length; at += GET_BATCH) {
11414
+ const snapshots = await firestore.getAll(...ids.slice(at, at + GET_BATCH).map((id) => items.doc(id))).catch(missingDatabase);
11415
+ for (const snapshot of snapshots) if (snapshot.exists) found.set(snapshot.id, toRow(snapshot.data()));
11416
+ }
11417
+ return keys.map((key) => found.get(firestoreDocumentId(key)) ?? null);
11418
+ },
11419
+ async query({ pk, gte, lt, order, limit, after }) {
11420
+ let query = items.where("pk", "==", pk);
11421
+ if (gte !== void 0) query = query.where("sk", ">=", gte);
11422
+ if (lt !== void 0) query = query.where("sk", "<", lt);
11423
+ query = query.orderBy("sk", order);
11424
+ if (after !== void 0) query = query.startAfter(after);
11425
+ const size = Math.min(limit, nativePageSize ?? limit);
11426
+ const { docs } = await query.limit(size).get().catch(missingDatabase);
11427
+ return {
11428
+ items: docs.map((document) => ({
11429
+ sk: document.get("sk"),
11430
+ value: toRow(document.data())
11431
+ })),
11432
+ more: docs.length === size
11433
+ };
11434
+ },
11435
+ async write(ops) {
11436
+ const blind = ops.map((op) => op.type === "add" && (op.condition === void 0 || "exists" in op.condition && op.condition.exists));
11437
+ const attempt = (reading) => firestore.runTransaction(async (transaction) => {
11438
+ const read = ops.flatMap((op, position) => (op.condition !== void 0 || op.type === "add") && (reading || !blind[position]) ? [position] : []);
11439
+ const snapshots = /* @__PURE__ */ new Map();
11440
+ if (read.length > 0) {
11441
+ const found = await transaction.getAll(...read.map((position) => reference(ops[position].key)));
11442
+ read.forEach((position, at) => snapshots.set(position, found[at]));
11443
+ }
11444
+ const failed = read.find((position) => !holds(snapshots.get(position), ops[position].condition));
11445
+ if (failed !== void 0) {
11446
+ if (!reading && blind.slice(0, failed).includes(true)) return null;
11447
+ return {
11448
+ ok: false,
11449
+ failedOp: failed
11450
+ };
11451
+ }
11452
+ for (const [position, op] of ops.entries()) {
11453
+ const document = reference(op.key);
11454
+ if (op.type === "put") transaction.set(document, toDocument(op.key, op.value, op.expiresAt));
11455
+ else if (op.type === "delete") transaction.delete(document);
11456
+ else if (op.type === "add") {
11457
+ if (snapshots.get(position)?.exists === false) {
11458
+ const created = { ...op.init };
11459
+ for (const [name, delta] of Object.entries(op.by)) created[name] = Number(created[name] ?? 0) + delta;
11460
+ transaction.set(document, toDocument(op.key, created, op.expiresAt));
11461
+ } else {
11462
+ const [[name, delta], ...more] = Object.entries(op.by);
11463
+ const ttl = op.expiresAt === void 0 ? [] : [FIRESTORE_TTL_FIELD, firebase_admin_firestore.Timestamp.fromMillis(op.expiresAt)];
11464
+ transaction.update(document, new firebase_admin_firestore.FieldPath("row", name), firebase_admin_firestore.FieldValue.increment(delta), ...more.flatMap(([other, by]) => [new firebase_admin_firestore.FieldPath("row", other), firebase_admin_firestore.FieldValue.increment(by)]), ...ttl);
11465
+ }
11466
+ }
11467
+ }
11468
+ return { ok: true };
11469
+ }, { maxAttempts: 5 });
11470
+ try {
11471
+ try {
11472
+ const result = await attempt(false);
11473
+ if (result !== null) return result;
11474
+ } catch (error) {
11475
+ if (codeOf(error) !== 5 || !blind.includes(true)) throw error;
11476
+ }
11477
+ return await attempt(true);
11478
+ } catch (error) {
11479
+ if (TRANSIENT.has(codeOf(error))) return {
11480
+ ok: false,
11481
+ retry: true
11482
+ };
11483
+ return missingDatabase(error);
11484
+ }
11485
+ },
11486
+ /** A plain batch, which reads nothing, 500 deletes at a time. */
11487
+ async deleteConsumed(keys) {
11488
+ for (let at = 0; at < keys.length; at += FIRESTORE_LIMITS.items) {
11489
+ const batch = firestore.batch();
11490
+ for (const key of keys.slice(at, at + FIRESTORE_LIMITS.items)) batch.delete(reference(key));
11491
+ await batch.commit().catch(missingDatabase);
11492
+ }
11493
+ },
11494
+ async dispose() {}
11495
+ };
11496
+ };
11497
+ //#endregion
11498
+ //#region src/firebaseDatabase.ts
11499
+ const adapterOf = ({ collection = FIREBASE_V1_COLLECTION, aggregateBatching: _, ...appOptions }) => {
11500
+ const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(appOptions);
11501
+ return createKvAdapter({ store: createFirestoreStore({
11502
+ firestore: (0, firebase_admin_firestore.getFirestore)(app),
11503
+ collection
11504
+ }) });
11505
+ };
11506
+ /** Hot Updater's database in one Firestore collection, through the storage engine. */
11507
+ const firebaseDatabase = (config) => createEngineDatabase({
11508
+ name: "firebaseDatabase",
11509
+ adapter: adapterOf(config),
11510
+ aggregateBatching: config.aggregateBatching ?? {}
11511
+ });
11512
+ //#endregion
11513
+ //#region src/firebaseStorage.ts
11514
+ const firebaseStorage = (config) => {
11515
+ const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
11516
+ const bucket = (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
11517
+ const getStorageKey = createStorageKeyBuilder(config.basePath);
11518
+ const parseAndValidate = (storageUri) => {
11519
+ const parsed = parseStorageUri(storageUri, "gs");
11520
+ if (parsed.bucket !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${parsed.bucket}".`);
11521
+ return parsed;
11522
+ };
11523
+ return createStorageAdapter({
11524
+ name: "firebaseStorage",
11525
+ protocol: "gs",
11526
+ async put({ key, body, contentType }) {
11527
+ const storageKey = getStorageKey(key);
11528
+ const bytes = new Uint8Array(await new Response(body).arrayBuffer());
11529
+ await bucket.file(storageKey).save(bytes, { metadata: {
11530
+ contentType,
11531
+ cacheControl: "public, max-age=31536000, immutable"
11532
+ } });
11533
+ return { storageUri: createStorageUri({
11534
+ bucket: config.storageBucket,
11535
+ key: storageKey,
11536
+ protocol: "gs"
11537
+ }) };
11538
+ },
11539
+ async get({ storageUri }) {
11540
+ const { key } = parseAndValidate(storageUri);
11541
+ try {
11542
+ const file = bucket.file(key);
11543
+ const [[body], [metadata]] = await Promise.all([file.download(), file.getMetadata()]);
11544
+ const headers = new Headers();
11545
+ if (metadata.contentType) headers.set("content-type", metadata.contentType);
11546
+ headers.set("content-length", String(metadata.size ?? body.byteLength));
11547
+ return { response: new Response(body, { headers }) };
11548
+ } catch (error) {
11549
+ if (typeof error === "object" && error !== null && "code" in error && error.code === 404) return { response: null };
11550
+ throw error;
11551
+ }
11552
+ },
11553
+ async getDownloadUrl({ storageUri }) {
11554
+ const { key } = parseAndValidate(storageUri);
11555
+ if (config.cdnUrl) {
11556
+ const storageUrl = new URL(storageUri);
11557
+ const downloadUrl = new URL(config.cdnUrl);
11558
+ downloadUrl.pathname = `${downloadUrl.pathname.replace(/\/+$/, "")}${storageUrl.pathname}`;
11559
+ downloadUrl.search = "";
11560
+ downloadUrl.hash = "";
11561
+ return { url: downloadUrl.toString() };
11562
+ }
11563
+ const [url] = await bucket.file(key).getSignedUrl({
11564
+ action: "read",
11565
+ expires: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3
11566
+ });
11567
+ if (!url) throw new Error("Failed to generate Firebase download URL");
11568
+ return { url };
11569
+ },
11570
+ async exists({ storageUri }) {
11571
+ const { key } = parseAndValidate(storageUri);
11572
+ const [exists] = await bucket.file(key).exists();
11573
+ return { exists };
11574
+ },
11575
+ async delete({ storageUri }) {
11576
+ const { key } = parseAndValidate(storageUri);
11577
+ await bucket.file(key).delete({ ignoreNotFound: true });
11578
+ return { deleted: true };
11579
+ }
11580
+ });
11581
+ };
11599
11582
  //#endregion
11600
11583
  //#region firebase/functions/index.ts
11601
11584
  const HOT_UPDATER_BASE_PATH = "/";
@@ -11605,7 +11588,11 @@ const cdnUrl = process.env.HOT_UPDATER_CDN_URL;
11605
11588
  if (!storageBucket) throw new Error("Firebase runtime requires storageBucket to read bundle manifests.");
11606
11589
  const hotUpdater = createHotUpdater({
11607
11590
  database: firebaseDatabase({ ...adminOptions }),
11608
- plugins,
11591
+ plugins: [
11592
+ insights(),
11593
+ apiKeys(),
11594
+ remoteConfig()
11595
+ ],
11609
11596
  storage: firebaseStorage({
11610
11597
  ...adminOptions,
11611
11598
  storageBucket,