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

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.
@@ -4566,7 +4566,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
4566
4566
  };
4567
4567
  //#endregion
4568
4568
  //#region ../../packages/server/dist/version.mjs
4569
- const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.44";
4569
+ const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.45";
4570
4570
  //#endregion
4571
4571
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
4572
4572
  /**
@@ -7179,254 +7179,6 @@ function createHotUpdater(options) {
7179
7179
  };
7180
7180
  }
7181
7181
  //#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
7202
- }
7203
- };
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;
7231
- };
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;
7244
- };
7245
- /**
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
7182
  //#region ../api-keys/dist/server/apiKeys.mjs
7431
7183
  const API_KEY_HEADER_NAME = "x-api-key";
7432
7184
  const BASE64URL_32_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
@@ -7576,7 +7328,7 @@ const apiKeysCli = (headerName) => ({ clientCredential: {
7576
7328
  } });
7577
7329
  //#endregion
7578
7330
  //#region ../api-keys/dist/server/model.mjs
7579
- const toRow = (row) => ({
7331
+ const toRow$1 = (row) => ({
7580
7332
  id: row.id,
7581
7333
  hash: row.hash,
7582
7334
  name: row.name,
@@ -7601,7 +7353,7 @@ const createApiKeyModel = (db) => ({
7601
7353
  },
7602
7354
  async findByHash(hash) {
7603
7355
  const row = await db.findOne("api_keys", { hash });
7604
- return row === null ? null : toRow(row);
7356
+ return row === null ? null : toRow$1(row);
7605
7357
  },
7606
7358
  /** Every key, newest first, then by id; the whole list is the result. */
7607
7359
  async list() {
@@ -7615,7 +7367,7 @@ const createApiKeyModel = (db) => ({
7615
7367
  limit: 500,
7616
7368
  ...cursor === void 0 ? {} : { cursor }
7617
7369
  });
7618
- rows.push(...page.rows.map(toRow));
7370
+ rows.push(...page.rows.map(toRow$1));
7619
7371
  cursor = page.next;
7620
7372
  } while (cursor !== void 0);
7621
7373
  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 +7376,7 @@ const createApiKeyModel = (db) => ({
7624
7376
  const row = await tx.findOne("api_keys", { id });
7625
7377
  if (row === null) return null;
7626
7378
  tx.update("api_keys", row, { revoked_at_ms: revokedAtMs });
7627
- return toRow({
7379
+ return toRow$1({
7628
7380
  ...row,
7629
7381
  revoked_at_ms: revokedAtMs
7630
7382
  });
@@ -11586,16 +11338,253 @@ const remoteConfig = () => {
11586
11338
  return markOfficial(plugin);
11587
11339
  };
11588
11340
  //#endregion
11589
- //#region src/plugins.ts
11341
+ //#region src/firebaseInfrastructureNames.ts
11590
11342
  /**
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.
11343
+ * The one collection the storage engine keeps every item in. It namespaces
11344
+ * the data, so item partitions are table names with no prefix.
11593
11345
  */
11594
- const plugins = [
11595
- insights(),
11596
- apiKeys(),
11597
- remoteConfig()
11598
- ];
11346
+ const FIREBASE_V1_COLLECTION = "hot_updater_v1";
11347
+ //#endregion
11348
+ //#region src/firestoreStore.ts
11349
+ /**
11350
+ * Firestore's limits: 10 MiB a request and 1 MiB a document; 500 writes a
11351
+ * transaction stays a conservative bound. Indexed values over 1,500 bytes are
11352
+ * truncated, so longer keys are refused rather than read inconsistently.
11353
+ */
11354
+ const FIRESTORE_LIMITS = {
11355
+ items: 500,
11356
+ bytes: 1e7,
11357
+ itemBytes: 1e6,
11358
+ keyBytes: {
11359
+ pk: 1500,
11360
+ sk: 1500
11361
+ }
11362
+ };
11363
+ /** `getAll` reads at most this many documents a call. */
11364
+ const GET_BATCH = 300;
11365
+ /** A document's id: a hash of its key, since keys may hold `/` and outgrow Firestore's 1,500 bytes. */
11366
+ const firestoreDocumentId = ({ pk, sk }) => (0, node_crypto.createHash)("sha256").update(JSON.stringify([pk, sk])).digest("hex");
11367
+ /**
11368
+ * A document is `{ pk, sk, row }`. Row values that are maps or arrays become
11369
+ * JSON text inside a map, since Firestore has no nested arrays and does not
11370
+ * keep map key order; `row` is exempt from single-field indexing.
11371
+ */
11372
+ const encode = (value) => value !== null && typeof value === "object" ? { json: JSON.stringify(value) } : value;
11373
+ const decode = (value) => value !== null && typeof value === "object" && "json" in value ? JSON.parse(String(value.json)) : value;
11374
+ /**
11375
+ * The collection's TTL field: a timestamp beside `pk` and `sk` on every item
11376
+ * of a row that expires, which Firestore's TTL policy deletes.
11377
+ */
11378
+ const FIRESTORE_TTL_FIELD = "expireAt";
11379
+ const toDocument = (key, row, expiresAt) => ({
11380
+ pk: key.pk,
11381
+ sk: key.sk,
11382
+ row: Object.fromEntries(Object.entries(row).map(([name, value]) => [name, encode(value)])),
11383
+ ...expiresAt === void 0 ? {} : { [FIRESTORE_TTL_FIELD]: firebase_admin_firestore.Timestamp.fromMillis(expiresAt) }
11384
+ });
11385
+ const toRow = (data) => Object.fromEntries(Object.entries(data.row ?? {}).map(([name, value]) => [name, decode(value)]));
11386
+ const holds = (snapshot, condition) => {
11387
+ if (condition === void 0) return true;
11388
+ const data = snapshot?.exists ? snapshot.data() : void 0;
11389
+ return "v" in condition ? data !== void 0 && data.row?._v === condition.v : data !== void 0 === condition.exists;
11390
+ };
11391
+ /** gRPC codes a rerun of the whole write can clear: DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, ABORTED, INTERNAL, UNAVAILABLE. */
11392
+ const TRANSIENT = /* @__PURE__ */ new Set([
11393
+ 4,
11394
+ 8,
11395
+ 10,
11396
+ 13,
11397
+ 14
11398
+ ]);
11399
+ const codeOf = (error) => error.code;
11400
+ /** Firestore reports a missing database as NOT_FOUND; it reads as a missing schema, like SQL's 42P01. */
11401
+ const missingDatabase = (error) => {
11402
+ throw codeOf(error) === 5 ? Object.assign(new Error(error.message, { cause: error }), { code: "42P01" }) : error;
11403
+ };
11404
+ /**
11405
+ * The key-value store over one Firestore collection of `{ pk, sk, row }`
11406
+ * documents, queried through the composite indexes on (pk, sk) in both
11407
+ * directions. A write is one transaction that reads only the documents its
11408
+ * ops guard.
11409
+ */
11410
+ const createFirestoreStore = ({ firestore, collection, nativePageSize }) => {
11411
+ const items = firestore.collection(collection);
11412
+ const reference = (key) => items.doc(firestoreDocumentId(key));
11413
+ return {
11414
+ id: "firestore",
11415
+ limits: FIRESTORE_LIMITS,
11416
+ async get(keys) {
11417
+ const found = /* @__PURE__ */ new Map();
11418
+ const ids = [...new Set(keys.map(firestoreDocumentId))];
11419
+ for (let at = 0; at < ids.length; at += GET_BATCH) {
11420
+ const snapshots = await firestore.getAll(...ids.slice(at, at + GET_BATCH).map((id) => items.doc(id))).catch(missingDatabase);
11421
+ for (const snapshot of snapshots) if (snapshot.exists) found.set(snapshot.id, toRow(snapshot.data()));
11422
+ }
11423
+ return keys.map((key) => found.get(firestoreDocumentId(key)) ?? null);
11424
+ },
11425
+ async query({ pk, gte, lt, order, limit, after }) {
11426
+ let query = items.where("pk", "==", pk);
11427
+ if (gte !== void 0) query = query.where("sk", ">=", gte);
11428
+ if (lt !== void 0) query = query.where("sk", "<", lt);
11429
+ query = query.orderBy("sk", order);
11430
+ if (after !== void 0) query = query.startAfter(after);
11431
+ const size = Math.min(limit, nativePageSize ?? limit);
11432
+ const { docs } = await query.limit(size).get().catch(missingDatabase);
11433
+ return {
11434
+ items: docs.map((document) => ({
11435
+ sk: document.get("sk"),
11436
+ value: toRow(document.data())
11437
+ })),
11438
+ more: docs.length === size
11439
+ };
11440
+ },
11441
+ async write(ops) {
11442
+ const blind = ops.map((op) => op.type === "add" && (op.condition === void 0 || "exists" in op.condition && op.condition.exists));
11443
+ const attempt = (reading) => firestore.runTransaction(async (transaction) => {
11444
+ const read = ops.flatMap((op, position) => (op.condition !== void 0 || op.type === "add") && (reading || !blind[position]) ? [position] : []);
11445
+ const snapshots = /* @__PURE__ */ new Map();
11446
+ if (read.length > 0) {
11447
+ const found = await transaction.getAll(...read.map((position) => reference(ops[position].key)));
11448
+ read.forEach((position, at) => snapshots.set(position, found[at]));
11449
+ }
11450
+ const failed = read.find((position) => !holds(snapshots.get(position), ops[position].condition));
11451
+ if (failed !== void 0) {
11452
+ if (!reading && blind.slice(0, failed).includes(true)) return null;
11453
+ return {
11454
+ ok: false,
11455
+ failedOp: failed
11456
+ };
11457
+ }
11458
+ for (const [position, op] of ops.entries()) {
11459
+ const document = reference(op.key);
11460
+ if (op.type === "put") transaction.set(document, toDocument(op.key, op.value, op.expiresAt));
11461
+ else if (op.type === "delete") transaction.delete(document);
11462
+ else if (op.type === "add") {
11463
+ if (snapshots.get(position)?.exists === false) {
11464
+ const created = { ...op.init };
11465
+ for (const [name, delta] of Object.entries(op.by)) created[name] = Number(created[name] ?? 0) + delta;
11466
+ transaction.set(document, toDocument(op.key, created, op.expiresAt));
11467
+ } else {
11468
+ const [[name, delta], ...more] = Object.entries(op.by);
11469
+ const ttl = op.expiresAt === void 0 ? [] : [FIRESTORE_TTL_FIELD, firebase_admin_firestore.Timestamp.fromMillis(op.expiresAt)];
11470
+ 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);
11471
+ }
11472
+ }
11473
+ }
11474
+ return { ok: true };
11475
+ }, { maxAttempts: 5 });
11476
+ try {
11477
+ try {
11478
+ const result = await attempt(false);
11479
+ if (result !== null) return result;
11480
+ } catch (error) {
11481
+ if (codeOf(error) !== 5 || !blind.includes(true)) throw error;
11482
+ }
11483
+ return await attempt(true);
11484
+ } catch (error) {
11485
+ if (TRANSIENT.has(codeOf(error))) return {
11486
+ ok: false,
11487
+ retry: true
11488
+ };
11489
+ return missingDatabase(error);
11490
+ }
11491
+ },
11492
+ /** A plain batch, which reads nothing, 500 deletes at a time. */
11493
+ async deleteConsumed(keys) {
11494
+ for (let at = 0; at < keys.length; at += FIRESTORE_LIMITS.items) {
11495
+ const batch = firestore.batch();
11496
+ for (const key of keys.slice(at, at + FIRESTORE_LIMITS.items)) batch.delete(reference(key));
11497
+ await batch.commit().catch(missingDatabase);
11498
+ }
11499
+ },
11500
+ async dispose() {}
11501
+ };
11502
+ };
11503
+ //#endregion
11504
+ //#region src/firebaseDatabase.ts
11505
+ const adapterOf = ({ collection = FIREBASE_V1_COLLECTION, aggregateBatching: _, ...appOptions }) => {
11506
+ const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(appOptions);
11507
+ return createKvAdapter({ store: createFirestoreStore({
11508
+ firestore: (0, firebase_admin_firestore.getFirestore)(app),
11509
+ collection
11510
+ }) });
11511
+ };
11512
+ /** Hot Updater's database in one Firestore collection, through the storage engine. */
11513
+ const firebaseDatabase = (config) => createEngineDatabase({
11514
+ name: "firebaseDatabase",
11515
+ adapter: adapterOf(config),
11516
+ aggregateBatching: config.aggregateBatching ?? {}
11517
+ });
11518
+ //#endregion
11519
+ //#region src/firebaseStorage.ts
11520
+ const firebaseStorage = (config) => {
11521
+ const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
11522
+ const bucket = (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
11523
+ const getStorageKey = createStorageKeyBuilder(config.basePath);
11524
+ const parseAndValidate = (storageUri) => {
11525
+ const parsed = parseStorageUri(storageUri, "gs");
11526
+ if (parsed.bucket !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${parsed.bucket}".`);
11527
+ return parsed;
11528
+ };
11529
+ return createStorageAdapter({
11530
+ name: "firebaseStorage",
11531
+ protocol: "gs",
11532
+ async put({ key, body, contentType }) {
11533
+ const storageKey = getStorageKey(key);
11534
+ const bytes = new Uint8Array(await new Response(body).arrayBuffer());
11535
+ await bucket.file(storageKey).save(bytes, { metadata: {
11536
+ contentType,
11537
+ cacheControl: "public, max-age=31536000, immutable"
11538
+ } });
11539
+ return { storageUri: createStorageUri({
11540
+ bucket: config.storageBucket,
11541
+ key: storageKey,
11542
+ protocol: "gs"
11543
+ }) };
11544
+ },
11545
+ async get({ storageUri }) {
11546
+ const { key } = parseAndValidate(storageUri);
11547
+ try {
11548
+ const file = bucket.file(key);
11549
+ const [[body], [metadata]] = await Promise.all([file.download(), file.getMetadata()]);
11550
+ const headers = new Headers();
11551
+ if (metadata.contentType) headers.set("content-type", metadata.contentType);
11552
+ headers.set("content-length", String(metadata.size ?? body.byteLength));
11553
+ return { response: new Response(body, { headers }) };
11554
+ } catch (error) {
11555
+ if (typeof error === "object" && error !== null && "code" in error && error.code === 404) return { response: null };
11556
+ throw error;
11557
+ }
11558
+ },
11559
+ async getDownloadUrl({ storageUri }) {
11560
+ const { key } = parseAndValidate(storageUri);
11561
+ if (config.cdnUrl) {
11562
+ const storageUrl = new URL(storageUri);
11563
+ const downloadUrl = new URL(config.cdnUrl);
11564
+ downloadUrl.pathname = `${downloadUrl.pathname.replace(/\/+$/, "")}${storageUrl.pathname}`;
11565
+ downloadUrl.search = "";
11566
+ downloadUrl.hash = "";
11567
+ return { url: downloadUrl.toString() };
11568
+ }
11569
+ const [url] = await bucket.file(key).getSignedUrl({
11570
+ action: "read",
11571
+ expires: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3
11572
+ });
11573
+ if (!url) throw new Error("Failed to generate Firebase download URL");
11574
+ return { url };
11575
+ },
11576
+ async exists({ storageUri }) {
11577
+ const { key } = parseAndValidate(storageUri);
11578
+ const [exists] = await bucket.file(key).exists();
11579
+ return { exists };
11580
+ },
11581
+ async delete({ storageUri }) {
11582
+ const { key } = parseAndValidate(storageUri);
11583
+ await bucket.file(key).delete({ ignoreNotFound: true });
11584
+ return { deleted: true };
11585
+ }
11586
+ });
11587
+ };
11599
11588
  //#endregion
11600
11589
  //#region firebase/functions/index.ts
11601
11590
  const HOT_UPDATER_BASE_PATH = "/";
@@ -11605,7 +11594,11 @@ const cdnUrl = process.env.HOT_UPDATER_CDN_URL;
11605
11594
  if (!storageBucket) throw new Error("Firebase runtime requires storageBucket to read bundle manifests.");
11606
11595
  const hotUpdater = createHotUpdater({
11607
11596
  database: firebaseDatabase({ ...adminOptions }),
11608
- plugins,
11597
+ plugins: [
11598
+ insights(),
11599
+ apiKeys(),
11600
+ remoteConfig()
11601
+ ],
11609
11602
  storage: firebaseStorage({
11610
11603
  ...adminOptions,
11611
11604
  storageBucket,
package/dist/index.cjs CHANGED
@@ -4,7 +4,6 @@ let firebase_admin_app = require("firebase-admin/app");
4
4
  let firebase_admin_firestore = require("firebase-admin/firestore");
5
5
  let node_crypto = require("node:crypto");
6
6
  let firebase_admin_storage = require("firebase-admin/storage");
7
- let _hot_updater_server_plugins = require("@hot-updater/server/plugins");
8
7
  //#region src/firebaseInfrastructureNames.ts
9
8
  /**
10
9
  * The one collection the storage engine keeps every item in. It namespaces
@@ -260,18 +259,6 @@ const firebaseStorage = (config) => {
260
259
  });
261
260
  };
262
261
  //#endregion
263
- //#region src/plugins.ts
264
- /**
265
- * The plugins the managed Firebase server runs: Insights, API keys on every
266
- * client route, and Remote Config. Its Cloud Function imports this list.
267
- */
268
- const plugins = [
269
- (0, _hot_updater_server_plugins.insights)(),
270
- (0, _hot_updater_server_plugins.apiKeys)(),
271
- (0, _hot_updater_server_plugins.remoteConfig)()
272
- ];
273
- //#endregion
274
262
  exports.firebaseDatabase = firebaseDatabase;
275
263
  exports.firebaseStorage = firebaseStorage;
276
264
  exports.migrateFirebaseDatabase = migrateFirebaseDatabase;
277
- exports.plugins = plugins;