@hot-updater/firebase 1.2.0-rc.1 → 1.2.0-rc.11

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.
@@ -4683,7 +4683,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
4683
4683
  };
4684
4684
  //#endregion
4685
4685
  //#region ../../packages/server/dist/version.mjs
4686
- const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.1";
4686
+ const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.11";
4687
4687
  //#endregion
4688
4688
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
4689
4689
  /**
@@ -12139,11 +12139,14 @@ const firebaseDatabase = (config) => ({
12139
12139
  //#region src/firebaseStorage.ts
12140
12140
  /** Signed URLs kept per adapter, enough for a few Bundles' assets. */
12141
12141
  const MAX_CACHED_SIGNED_URLS = 1e4;
12142
+ /** Objects deleted at once: Cloud Storage deletes one object a request. */
12143
+ const DELETE_CONCURRENCY = 16;
12142
12144
  const firebaseStorage = (config) => {
12143
12145
  const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
12144
12146
  let opened;
12145
12147
  const bucket = () => opened ??= (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
12146
12148
  const getStorageKey = createStorageKeyBuilder(config.basePath);
12149
+ const basePrefix = getStorageKey() ? `${getStorageKey()}/` : "";
12147
12150
  /**
12148
12151
  * Signed URLs by object. Without a private key, as in Cloud Functions, each
12149
12152
  * signature is an IAM signBlob request, and an update check returns a URL
@@ -12232,6 +12235,39 @@ const firebaseStorage = (config) => {
12232
12235
  const { key } = parseAndValidate(storageUri);
12233
12236
  await bucket().file(key).delete({ ignoreNotFound: true });
12234
12237
  return { deleted: true };
12238
+ },
12239
+ async listObjects(prefix) {
12240
+ const folder = getStorageKey(prefix ?? "");
12241
+ const [files] = await bucket().getFiles({
12242
+ autoPaginate: true,
12243
+ prefix: folder ? `${folder}/` : ""
12244
+ });
12245
+ const objects = [];
12246
+ for (const file of files) {
12247
+ if (!file.name.startsWith(basePrefix)) continue;
12248
+ let storageUri;
12249
+ try {
12250
+ storageUri = createStorageUri({
12251
+ bucket: config.storageBucket,
12252
+ key: file.name,
12253
+ protocol: "gs"
12254
+ });
12255
+ } catch {
12256
+ continue;
12257
+ }
12258
+ const { size, updated } = file.metadata;
12259
+ objects.push({
12260
+ key: file.name.slice(basePrefix.length),
12261
+ ...updated ? { lastModifiedAt: new Date(updated) } : {},
12262
+ size: Number(size ?? 0),
12263
+ storageUri
12264
+ });
12265
+ }
12266
+ return objects;
12267
+ },
12268
+ async deleteObjects(relativeKeys) {
12269
+ const keys = relativeKeys.map((key) => getStorageKey(key));
12270
+ for (let offset = 0; offset < keys.length; offset += DELETE_CONCURRENCY) await Promise.all(keys.slice(offset, offset + DELETE_CONCURRENCY).map((key) => bucket().file(key).delete({ ignoreNotFound: true })));
12235
12271
  }
12236
12272
  });
12237
12273
  };
package/dist/index.cjs CHANGED
@@ -202,11 +202,14 @@ const firebaseDatabase = (config) => ({
202
202
  //#region src/firebaseStorage.ts
203
203
  /** Signed URLs kept per adapter, enough for a few Bundles' assets. */
204
204
  const MAX_CACHED_SIGNED_URLS = 1e4;
205
+ /** Objects deleted at once: Cloud Storage deletes one object a request. */
206
+ const DELETE_CONCURRENCY = 16;
205
207
  const firebaseStorage = (config) => {
206
208
  const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
207
209
  let opened;
208
210
  const bucket = () => opened ??= (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
209
211
  const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
212
+ const basePrefix = getStorageKey() ? `${getStorageKey()}/` : "";
210
213
  /**
211
214
  * Signed URLs by object. Without a private key, as in Cloud Functions, each
212
215
  * signature is an IAM signBlob request, and an update check returns a URL
@@ -295,6 +298,39 @@ const firebaseStorage = (config) => {
295
298
  const { key } = parseAndValidate(storageUri);
296
299
  await bucket().file(key).delete({ ignoreNotFound: true });
297
300
  return { deleted: true };
301
+ },
302
+ async listObjects(prefix) {
303
+ const folder = getStorageKey(prefix ?? "");
304
+ const [files] = await bucket().getFiles({
305
+ autoPaginate: true,
306
+ prefix: folder ? `${folder}/` : ""
307
+ });
308
+ const objects = [];
309
+ for (const file of files) {
310
+ if (!file.name.startsWith(basePrefix)) continue;
311
+ let storageUri;
312
+ try {
313
+ storageUri = (0, _hot_updater_plugin_core.createStorageUri)({
314
+ bucket: config.storageBucket,
315
+ key: file.name,
316
+ protocol: "gs"
317
+ });
318
+ } catch {
319
+ continue;
320
+ }
321
+ const { size, updated } = file.metadata;
322
+ objects.push({
323
+ key: file.name.slice(basePrefix.length),
324
+ ...updated ? { lastModifiedAt: new Date(updated) } : {},
325
+ size: Number(size ?? 0),
326
+ storageUri
327
+ });
328
+ }
329
+ return objects;
330
+ },
331
+ async deleteObjects(relativeKeys) {
332
+ const keys = relativeKeys.map((key) => getStorageKey(key));
333
+ for (let offset = 0; offset < keys.length; offset += DELETE_CONCURRENCY) await Promise.all(keys.slice(offset, offset + DELETE_CONCURRENCY).map((key) => bucket().file(key).delete({ ignoreNotFound: true })));
298
334
  }
299
335
  });
300
336
  };
@@ -356,23 +392,51 @@ const crc32c = (bytes) => {
356
392
  /** The version IDs the secret's annotation lists. */
357
393
  const trackedVersions = (secret) => (secret.annotations?.[VERSIONS_ANNOTATION] ?? "").split(",").map((entry) => entry.trim()).filter((entry) => VERSION_ID_PATTERN.test(entry));
358
394
  const versionOf = (name) => typeof name === "string" ? name.slice(name.lastIndexOf("/") + 1) : "";
395
+ /** The widest line a message has: the CLI prints no wider. */
396
+ const LINE_WIDTH = 76;
397
+ /** `text`, such as Google's own message, in lines that fit, broken at spaces. */
398
+ const wrap = (text) => {
399
+ const lines = [];
400
+ let line = "";
401
+ for (const word of text.split(/\s+/u).filter(Boolean)) if (line && line.length + 1 + word.length > LINE_WIDTH) {
402
+ lines.push(line);
403
+ line = word;
404
+ } else line = line ? `${line} ${word}` : word;
405
+ if (line) lines.push(line);
406
+ return lines;
407
+ };
408
+ /** An error of one line each, for a terminal. */
409
+ const failure = (lines, cause) => new Error(lines.join("\n"), cause === void 0 ? void 0 : { cause });
359
410
  const hint = (response, permission) => {
360
411
  const { status, error } = response;
361
- if (error.details?.some((detail) => detail.reason === "SERVICE_DISABLED")) return " Enable the Secret Manager API (secretmanager.googleapis.com) in the project: gcloud services enable secretmanager.googleapis.com --project <project>.";
362
- if (status === 401) return " Google rejected the access token: sign in again with `gcloud auth application-default login`, or check the job's Workload Identity Federation.";
363
- if (status === 403) return ` The Google identity needs ${permission}.`;
364
- if (status === 429) return " Secret Manager's quota is exhausted: try again later.";
365
- return "";
412
+ if (error.details?.some((detail) => detail.reason === "SERVICE_DISABLED")) return ["Enable the Secret Manager API in the project:", "gcloud services enable secretmanager.googleapis.com --project <project>"];
413
+ if (status === 401) return [
414
+ "Google rejected the access token.",
415
+ "Sign in again with gcloud auth application-default login.",
416
+ "In CI, check the job's Workload Identity Federation."
417
+ ];
418
+ if (status === 403) return wrap(`The Google identity needs ${permission}.`);
419
+ if (status === 429) return ["Secret Manager's quota is exhausted: try again later."];
420
+ return [];
366
421
  };
367
422
  /**
368
- * An error for a failed request: the HTTP status, Google's status and
369
- * message, which name resources but never a value, and what to fix.
423
+ * An error for a failed request on `resource`, a line each: the HTTP
424
+ * status, the resource, Google's status and message, which name resources
425
+ * but never a value, `notes`, and what to fix.
370
426
  */
371
- const requestError = (action, response, permission) => {
427
+ const requestError = (action, resource, response, permission, notes = []) => {
372
428
  const { status, message } = response.error;
373
429
  const detail = [status, message?.slice(0, 300)].filter(Boolean).join(": ");
374
- return /* @__PURE__ */ new Error(`${ADAPTER_NAME} could not ${action} (${response.status}${detail ? ` ${detail}` : ""}).${hint(response, permission)}`);
430
+ return failure([
431
+ `${ADAPTER_NAME} couldn't ${action} (${response.status}).`,
432
+ resourceLine(resource),
433
+ ...wrap(detail),
434
+ ...notes,
435
+ ...hint(response, permission)
436
+ ]);
375
437
  };
438
+ /** The line that names a secret: its resource name is a line's worth. */
439
+ const resourceLine = (secret) => `Secret: ${secret}`;
376
440
  /** Masks a value in the CLI's output, which loads `@hot-updater/cli-tools`. */
377
441
  const mask = async (value) => {
378
442
  const { registerSecretValue } = await import("@hot-updater/cli-tools");
@@ -411,7 +475,14 @@ const createAccessToken = (credentials, projectId) => {
411
475
  try {
412
476
  return await client.getAccessToken() ?? void 0;
413
477
  } catch (error) {
414
- throw new Error(`${ADAPTER_NAME} found no Google credentials (${error instanceof Error ? error.message.split("\n")[0] : String(error)}). Sign in with \`gcloud auth application-default login\`, use google-github-actions/auth in CI, set GOOGLE_APPLICATION_CREDENTIALS, or set ${ACCESS_TOKEN_ENV_NAMES.join(" or ")} to an access token.`, { cause: error });
478
+ throw failure([
479
+ `${ADAPTER_NAME} found no Google credentials.`,
480
+ ...wrap(error instanceof Error ? error.message.split("\n")[0] : String(error)),
481
+ "On your machine, run gcloud auth application-default login.",
482
+ "In CI, use google-github-actions/auth.",
483
+ "Or set GOOGLE_APPLICATION_CREDENTIALS to a service account key.",
484
+ `Or set ${ACCESS_TOKEN_ENV_NAMES.join(" or ")} to a token.`
485
+ ], error);
415
486
  }
416
487
  };
417
488
  return async () => {
@@ -434,13 +505,17 @@ const createAccessToken = (credentials, projectId) => {
434
505
  const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
435
506
  const accessToken = createAccessToken(credentials, projectId);
436
507
  const projectPath = () => {
437
- if (typeof projectId !== "string" || !PROJECT_PATTERN.test(projectId)) throw new Error(`${ADAPTER_NAME} needs the Google Cloud project ID or number as projectId, and ${JSON.stringify(projectId)} is not one.`);
438
- if (!PREFIX_PATTERN.test(prefix)) throw new Error(`${ADAPTER_NAME}'s prefix takes letters, digits, "-", and "_", and ${JSON.stringify(prefix)} has others.`);
508
+ if (typeof projectId !== "string" || !PROJECT_PATTERN.test(projectId)) throw failure([`${ADAPTER_NAME} can't use the projectId ${JSON.stringify(projectId)}.`, "Set the Google Cloud project's ID or number."]);
509
+ if (!PREFIX_PATTERN.test(prefix)) throw failure([`${ADAPTER_NAME} can't use the prefix ${JSON.stringify(prefix)}.`, "It takes letters, digits, \"-\", and \"_\"."]);
439
510
  return `projects/${projectId}`;
440
511
  };
441
512
  const secretId = (name) => {
442
513
  const id = `${prefix}${name}`;
443
- if (!SECRET_ID_PATTERN.test(id)) throw new Error(`${ADAPTER_NAME} keeps ${JSON.stringify(name)} as the secret ID ${JSON.stringify(id)}, and Secret Manager's IDs take only letters, digits, "-", and "_", up to 255 characters.`);
514
+ if (!SECRET_ID_PATTERN.test(id)) throw failure([
515
+ `${ADAPTER_NAME} can't name a secret ${JSON.stringify(name)}.`,
516
+ `Secret: ${id}`,
517
+ "Secret Manager's IDs take up to 255 letters, digits, \"-\", and \"_\"."
518
+ ]);
444
519
  return id;
445
520
  };
446
521
  const request = async (token, method, path, body) => {
@@ -475,14 +550,18 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
475
550
  const accessLatest = async (token, secret) => {
476
551
  const response = await request(token, "GET", `${secret}/versions/latest:access`);
477
552
  if (response.status === 404) return void 0;
478
- if (!response.ok) throw requestError(`read ${secret}`, response, "roles/secretmanager.secretAccessor on the secret");
553
+ if (!response.ok) throw requestError("read", secret, response, "roles/secretmanager.secretAccessor on the secret");
479
554
  const body = isRecord(response.body) ? response.body : void 0;
480
555
  const payload = body?.payload;
481
556
  const data = isRecord(payload) ? payload.data : void 0;
482
- if (typeof data !== "string") throw new Error(`${ADAPTER_NAME} read no payload for ${secret}.`);
557
+ if (typeof data !== "string") throw failure([`${ADAPTER_NAME} read no payload.`, resourceLine(secret)]);
483
558
  const bytes = Buffer.from(data, "base64");
484
559
  const checksum = isRecord(payload) ? payload.dataCrc32c : void 0;
485
- if (checksum !== void 0 && Number(checksum) !== crc32c(bytes)) throw new Error(`${ADAPTER_NAME} read ${secret} with a checksum that does not match its payload.`);
560
+ if (checksum !== void 0 && Number(checksum) !== crc32c(bytes)) throw failure([
561
+ `${ADAPTER_NAME} can't trust what it read.`,
562
+ resourceLine(secret),
563
+ "Its checksum doesn't match its payload."
564
+ ]);
486
565
  const value = bytes.toString("utf8");
487
566
  await mask(value);
488
567
  return {
@@ -515,7 +594,7 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
515
594
  const readSecret = async (token, secret) => {
516
595
  const response = await request(token, "GET", secret);
517
596
  if (response.status === 404) return void 0;
518
- if (!response.ok) throw requestError(`read ${secret}`, response, "secretmanager.secrets.get on the secret");
597
+ if (!response.ok) throw requestError("read", secret, response, "secretmanager.secrets.get on the secret");
519
598
  return response.body;
520
599
  };
521
600
  const getOrCreateSecret = async (token, parent, id) => {
@@ -528,14 +607,21 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
528
607
  const raced = await readSecret(token, secret);
529
608
  if (raced) return raced;
530
609
  }
531
- throw requestError(`create ${secret}`, created, `secretmanager.secrets.create on ${parent}`);
610
+ throw requestError("create", secret, created, `secretmanager.secrets.create on ${parent}`);
532
611
  };
533
612
  const assertCanSet = async (token, secret) => {
534
613
  const response = await request(token, "POST", `${secret}:testIamPermissions`, { permissions: SET_PERMISSIONS });
535
- if (!response.ok) throw requestError(`check its permissions on ${secret}`, response, SET_PERMISSIONS.join(", "));
614
+ if (!response.ok) throw requestError("check its permissions", secret, response, SET_PERMISSIONS.join(", "));
536
615
  const held = isRecord(response.body) && Array.isArray(response.body.permissions) ? response.body.permissions : [];
537
616
  const missing = SET_PERMISSIONS.filter((name) => !held.includes(name));
538
- if (missing.length > 0) throw new Error(`${ADAPTER_NAME} stores a value as a new version of ${secret} and destroys the versions it added before, and the Google identity lacks ${missing.join(", ")} on it: grant a custom role with secretmanager.secrets.get, secretmanager.secrets.update, secretmanager.versions.add, secretmanager.versions.destroy, and secretmanager.versions.access on it, or roles/secretmanager.admin.`);
617
+ if (missing.length > 0) throw failure([
618
+ `${ADAPTER_NAME} can't store a value.`,
619
+ resourceLine(secret),
620
+ ...wrap(`The Google identity lacks ${missing.join(", ")} on it.`),
621
+ "It adds a version, then destroys the versions it added before.",
622
+ "Grant roles/secretmanager.admin, or a custom role with:",
623
+ ...wrap("secretmanager.secrets.get, secretmanager.secrets.update, secretmanager.versions.add, secretmanager.versions.destroy, secretmanager.versions.access")
624
+ ]);
539
625
  };
540
626
  /**
541
627
  * Destroys `version` of `secret`: true once it is destroyed, or already
@@ -582,7 +668,7 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
582
668
  const parent = projectPath();
583
669
  const id = secretId(name);
584
670
  const secret = `${parent}/secrets/${id}`;
585
- if (typeof value !== "string" || value === "") throw new Error(`${ADAPTER_NAME} cannot store an empty ${secret}.`);
671
+ if (typeof value !== "string" || value === "") throw failure([`${ADAPTER_NAME} can't store an empty value.`, resourceLine(secret)]);
586
672
  await mask(value);
587
673
  const token = await accessToken();
588
674
  const current = await getOrCreateSecret(token, parent, id);
@@ -593,7 +679,7 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
593
679
  dataCrc32c: String(crc32c(data))
594
680
  } });
595
681
  const version = versionOf(isRecord(added.body) ? added.body.name : void 0);
596
- if (!added.ok || !VERSION_ID_PATTERN.test(version)) throw requestError(`add a version to ${secret}`, added, "secretmanager.versions.add on the secret");
682
+ if (!added.ok || !VERSION_ID_PATTERN.test(version)) throw requestError("add a version", secret, added, "secretmanager.versions.add on the secret");
597
683
  const visible = await waitForLatest(token, secret, version);
598
684
  const destroyed = /* @__PURE__ */ new Set();
599
685
  if (visible) {
@@ -603,7 +689,8 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
603
689
  if (results[index]) destroyed.add(entry);
604
690
  });
605
691
  }
606
- const notListed = `list version ${version}, which it stored, in ${secret}'s ${VERSIONS_ANNOTATION} annotation, so the next set will not destroy it`;
692
+ const notListed = `record version ${version}, which it stored`;
693
+ const notDestroyed = `The next set won't destroy it: ${VERSIONS_ANNOTATION} misses it.`;
607
694
  let base = current;
608
695
  let updated;
609
696
  try {
@@ -623,10 +710,20 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
623
710
  if (updated.error.status !== "FAILED_PRECONDITION") break;
624
711
  }
625
712
  } catch (error) {
626
- throw new Error(`${ADAPTER_NAME} could not ${notListed} (${error instanceof Error ? error.message : String(error)}).`, { cause: error });
713
+ throw failure([
714
+ `${ADAPTER_NAME} couldn't ${notListed}.`,
715
+ resourceLine(secret),
716
+ ...wrap(error instanceof Error ? error.message : String(error)),
717
+ notDestroyed
718
+ ], error);
627
719
  }
628
- if (!updated?.ok) throw requestError(notListed, updated, "secretmanager.secrets.update on the secret");
629
- if (!visible) throw new Error(`${ADAPTER_NAME} stored ${secret} as version ${version}, and its latest alias does not resolve to that version yet, so it kept the versions it added before; the next set destroys them.`);
720
+ if (!updated?.ok) throw requestError(notListed, secret, updated, "secretmanager.secrets.update on the secret", [notDestroyed]);
721
+ if (!visible) throw failure([
722
+ `${ADAPTER_NAME} stored version ${version}.`,
723
+ resourceLine(secret),
724
+ "Its latest alias doesn't resolve to that version yet.",
725
+ "It kept the versions it added before: the next set destroys them."
726
+ ]);
630
727
  },
631
728
  describe: (name) => {
632
729
  const id = `${prefix}${name}`;
package/dist/index.d.cts CHANGED
@@ -37,7 +37,7 @@ interface FirebaseStorageConfig extends AppOptions {
37
37
  /** Firebase signed URL lifetime in seconds. @default 3600 */
38
38
  signedUrlExpiresIn?: number;
39
39
  }
40
- declare const firebaseStorage: (config: FirebaseStorageConfig) => StorageAdapterWith<"put" | "get" | "getDownloadUrl" | "exists" | "delete">;
40
+ declare const firebaseStorage: (config: FirebaseStorageConfig) => StorageAdapterWith<"put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects">;
41
41
  //#endregion
42
42
  //#region src/secretManager.d.ts
43
43
  /**
@@ -56,7 +56,7 @@ interface SecretManagerOptions {
56
56
  /** The Google Cloud project ID or number that holds the secrets. */
57
57
  readonly projectId: string;
58
58
  /**
59
- * Put before each name to make its secret ID, so `secret:asc-api-key`
59
+ * Put before each name to make its secret ID, so `secret("asc-api-key")`
60
60
  * reads `hot-updater-asc-api-key`. Letters, digits, `-`, and `_`.
61
61
  *
62
62
  * @default "hot-updater-"
package/dist/index.d.mts CHANGED
@@ -37,7 +37,7 @@ interface FirebaseStorageConfig extends AppOptions {
37
37
  /** Firebase signed URL lifetime in seconds. @default 3600 */
38
38
  signedUrlExpiresIn?: number;
39
39
  }
40
- declare const firebaseStorage: (config: FirebaseStorageConfig) => StorageAdapterWith<"put" | "get" | "getDownloadUrl" | "exists" | "delete">;
40
+ declare const firebaseStorage: (config: FirebaseStorageConfig) => StorageAdapterWith<"put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects">;
41
41
  //#endregion
42
42
  //#region src/secretManager.d.ts
43
43
  /**
@@ -56,7 +56,7 @@ interface SecretManagerOptions {
56
56
  /** The Google Cloud project ID or number that holds the secrets. */
57
57
  readonly projectId: string;
58
58
  /**
59
- * Put before each name to make its secret ID, so `secret:asc-api-key`
59
+ * Put before each name to make its secret ID, so `secret("asc-api-key")`
60
60
  * reads `hot-updater-asc-api-key`. Letters, digits, `-`, and `_`.
61
61
  *
62
62
  * @default "hot-updater-"
package/dist/index.mjs CHANGED
@@ -201,11 +201,14 @@ const firebaseDatabase = (config) => ({
201
201
  //#region src/firebaseStorage.ts
202
202
  /** Signed URLs kept per adapter, enough for a few Bundles' assets. */
203
203
  const MAX_CACHED_SIGNED_URLS = 1e4;
204
+ /** Objects deleted at once: Cloud Storage deletes one object a request. */
205
+ const DELETE_CONCURRENCY = 16;
204
206
  const firebaseStorage = (config) => {
205
207
  const app = getApps().length ? getApp() : initializeApp(config);
206
208
  let opened;
207
209
  const bucket = () => opened ??= getStorage(app).bucket(config.storageBucket);
208
210
  const getStorageKey = createStorageKeyBuilder(config.basePath);
211
+ const basePrefix = getStorageKey() ? `${getStorageKey()}/` : "";
209
212
  /**
210
213
  * Signed URLs by object. Without a private key, as in Cloud Functions, each
211
214
  * signature is an IAM signBlob request, and an update check returns a URL
@@ -294,6 +297,39 @@ const firebaseStorage = (config) => {
294
297
  const { key } = parseAndValidate(storageUri);
295
298
  await bucket().file(key).delete({ ignoreNotFound: true });
296
299
  return { deleted: true };
300
+ },
301
+ async listObjects(prefix) {
302
+ const folder = getStorageKey(prefix ?? "");
303
+ const [files] = await bucket().getFiles({
304
+ autoPaginate: true,
305
+ prefix: folder ? `${folder}/` : ""
306
+ });
307
+ const objects = [];
308
+ for (const file of files) {
309
+ if (!file.name.startsWith(basePrefix)) continue;
310
+ let storageUri;
311
+ try {
312
+ storageUri = createStorageUri({
313
+ bucket: config.storageBucket,
314
+ key: file.name,
315
+ protocol: "gs"
316
+ });
317
+ } catch {
318
+ continue;
319
+ }
320
+ const { size, updated } = file.metadata;
321
+ objects.push({
322
+ key: file.name.slice(basePrefix.length),
323
+ ...updated ? { lastModifiedAt: new Date(updated) } : {},
324
+ size: Number(size ?? 0),
325
+ storageUri
326
+ });
327
+ }
328
+ return objects;
329
+ },
330
+ async deleteObjects(relativeKeys) {
331
+ const keys = relativeKeys.map((key) => getStorageKey(key));
332
+ for (let offset = 0; offset < keys.length; offset += DELETE_CONCURRENCY) await Promise.all(keys.slice(offset, offset + DELETE_CONCURRENCY).map((key) => bucket().file(key).delete({ ignoreNotFound: true })));
297
333
  }
298
334
  });
299
335
  };
@@ -355,23 +391,51 @@ const crc32c = (bytes) => {
355
391
  /** The version IDs the secret's annotation lists. */
356
392
  const trackedVersions = (secret) => (secret.annotations?.[VERSIONS_ANNOTATION] ?? "").split(",").map((entry) => entry.trim()).filter((entry) => VERSION_ID_PATTERN.test(entry));
357
393
  const versionOf = (name) => typeof name === "string" ? name.slice(name.lastIndexOf("/") + 1) : "";
394
+ /** The widest line a message has: the CLI prints no wider. */
395
+ const LINE_WIDTH = 76;
396
+ /** `text`, such as Google's own message, in lines that fit, broken at spaces. */
397
+ const wrap = (text) => {
398
+ const lines = [];
399
+ let line = "";
400
+ for (const word of text.split(/\s+/u).filter(Boolean)) if (line && line.length + 1 + word.length > LINE_WIDTH) {
401
+ lines.push(line);
402
+ line = word;
403
+ } else line = line ? `${line} ${word}` : word;
404
+ if (line) lines.push(line);
405
+ return lines;
406
+ };
407
+ /** An error of one line each, for a terminal. */
408
+ const failure = (lines, cause) => new Error(lines.join("\n"), cause === void 0 ? void 0 : { cause });
358
409
  const hint = (response, permission) => {
359
410
  const { status, error } = response;
360
- if (error.details?.some((detail) => detail.reason === "SERVICE_DISABLED")) return " Enable the Secret Manager API (secretmanager.googleapis.com) in the project: gcloud services enable secretmanager.googleapis.com --project <project>.";
361
- if (status === 401) return " Google rejected the access token: sign in again with `gcloud auth application-default login`, or check the job's Workload Identity Federation.";
362
- if (status === 403) return ` The Google identity needs ${permission}.`;
363
- if (status === 429) return " Secret Manager's quota is exhausted: try again later.";
364
- return "";
411
+ if (error.details?.some((detail) => detail.reason === "SERVICE_DISABLED")) return ["Enable the Secret Manager API in the project:", "gcloud services enable secretmanager.googleapis.com --project <project>"];
412
+ if (status === 401) return [
413
+ "Google rejected the access token.",
414
+ "Sign in again with gcloud auth application-default login.",
415
+ "In CI, check the job's Workload Identity Federation."
416
+ ];
417
+ if (status === 403) return wrap(`The Google identity needs ${permission}.`);
418
+ if (status === 429) return ["Secret Manager's quota is exhausted: try again later."];
419
+ return [];
365
420
  };
366
421
  /**
367
- * An error for a failed request: the HTTP status, Google's status and
368
- * message, which name resources but never a value, and what to fix.
422
+ * An error for a failed request on `resource`, a line each: the HTTP
423
+ * status, the resource, Google's status and message, which name resources
424
+ * but never a value, `notes`, and what to fix.
369
425
  */
370
- const requestError = (action, response, permission) => {
426
+ const requestError = (action, resource, response, permission, notes = []) => {
371
427
  const { status, message } = response.error;
372
428
  const detail = [status, message?.slice(0, 300)].filter(Boolean).join(": ");
373
- return /* @__PURE__ */ new Error(`${ADAPTER_NAME} could not ${action} (${response.status}${detail ? ` ${detail}` : ""}).${hint(response, permission)}`);
429
+ return failure([
430
+ `${ADAPTER_NAME} couldn't ${action} (${response.status}).`,
431
+ resourceLine(resource),
432
+ ...wrap(detail),
433
+ ...notes,
434
+ ...hint(response, permission)
435
+ ]);
374
436
  };
437
+ /** The line that names a secret: its resource name is a line's worth. */
438
+ const resourceLine = (secret) => `Secret: ${secret}`;
375
439
  /** Masks a value in the CLI's output, which loads `@hot-updater/cli-tools`. */
376
440
  const mask = async (value) => {
377
441
  const { registerSecretValue } = await import("@hot-updater/cli-tools");
@@ -410,7 +474,14 @@ const createAccessToken = (credentials, projectId) => {
410
474
  try {
411
475
  return await client.getAccessToken() ?? void 0;
412
476
  } catch (error) {
413
- throw new Error(`${ADAPTER_NAME} found no Google credentials (${error instanceof Error ? error.message.split("\n")[0] : String(error)}). Sign in with \`gcloud auth application-default login\`, use google-github-actions/auth in CI, set GOOGLE_APPLICATION_CREDENTIALS, or set ${ACCESS_TOKEN_ENV_NAMES.join(" or ")} to an access token.`, { cause: error });
477
+ throw failure([
478
+ `${ADAPTER_NAME} found no Google credentials.`,
479
+ ...wrap(error instanceof Error ? error.message.split("\n")[0] : String(error)),
480
+ "On your machine, run gcloud auth application-default login.",
481
+ "In CI, use google-github-actions/auth.",
482
+ "Or set GOOGLE_APPLICATION_CREDENTIALS to a service account key.",
483
+ `Or set ${ACCESS_TOKEN_ENV_NAMES.join(" or ")} to a token.`
484
+ ], error);
414
485
  }
415
486
  };
416
487
  return async () => {
@@ -433,13 +504,17 @@ const createAccessToken = (credentials, projectId) => {
433
504
  const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
434
505
  const accessToken = createAccessToken(credentials, projectId);
435
506
  const projectPath = () => {
436
- if (typeof projectId !== "string" || !PROJECT_PATTERN.test(projectId)) throw new Error(`${ADAPTER_NAME} needs the Google Cloud project ID or number as projectId, and ${JSON.stringify(projectId)} is not one.`);
437
- if (!PREFIX_PATTERN.test(prefix)) throw new Error(`${ADAPTER_NAME}'s prefix takes letters, digits, "-", and "_", and ${JSON.stringify(prefix)} has others.`);
507
+ if (typeof projectId !== "string" || !PROJECT_PATTERN.test(projectId)) throw failure([`${ADAPTER_NAME} can't use the projectId ${JSON.stringify(projectId)}.`, "Set the Google Cloud project's ID or number."]);
508
+ if (!PREFIX_PATTERN.test(prefix)) throw failure([`${ADAPTER_NAME} can't use the prefix ${JSON.stringify(prefix)}.`, "It takes letters, digits, \"-\", and \"_\"."]);
438
509
  return `projects/${projectId}`;
439
510
  };
440
511
  const secretId = (name) => {
441
512
  const id = `${prefix}${name}`;
442
- if (!SECRET_ID_PATTERN.test(id)) throw new Error(`${ADAPTER_NAME} keeps ${JSON.stringify(name)} as the secret ID ${JSON.stringify(id)}, and Secret Manager's IDs take only letters, digits, "-", and "_", up to 255 characters.`);
513
+ if (!SECRET_ID_PATTERN.test(id)) throw failure([
514
+ `${ADAPTER_NAME} can't name a secret ${JSON.stringify(name)}.`,
515
+ `Secret: ${id}`,
516
+ "Secret Manager's IDs take up to 255 letters, digits, \"-\", and \"_\"."
517
+ ]);
443
518
  return id;
444
519
  };
445
520
  const request = async (token, method, path, body) => {
@@ -474,14 +549,18 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
474
549
  const accessLatest = async (token, secret) => {
475
550
  const response = await request(token, "GET", `${secret}/versions/latest:access`);
476
551
  if (response.status === 404) return void 0;
477
- if (!response.ok) throw requestError(`read ${secret}`, response, "roles/secretmanager.secretAccessor on the secret");
552
+ if (!response.ok) throw requestError("read", secret, response, "roles/secretmanager.secretAccessor on the secret");
478
553
  const body = isRecord(response.body) ? response.body : void 0;
479
554
  const payload = body?.payload;
480
555
  const data = isRecord(payload) ? payload.data : void 0;
481
- if (typeof data !== "string") throw new Error(`${ADAPTER_NAME} read no payload for ${secret}.`);
556
+ if (typeof data !== "string") throw failure([`${ADAPTER_NAME} read no payload.`, resourceLine(secret)]);
482
557
  const bytes = Buffer.from(data, "base64");
483
558
  const checksum = isRecord(payload) ? payload.dataCrc32c : void 0;
484
- if (checksum !== void 0 && Number(checksum) !== crc32c(bytes)) throw new Error(`${ADAPTER_NAME} read ${secret} with a checksum that does not match its payload.`);
559
+ if (checksum !== void 0 && Number(checksum) !== crc32c(bytes)) throw failure([
560
+ `${ADAPTER_NAME} can't trust what it read.`,
561
+ resourceLine(secret),
562
+ "Its checksum doesn't match its payload."
563
+ ]);
485
564
  const value = bytes.toString("utf8");
486
565
  await mask(value);
487
566
  return {
@@ -514,7 +593,7 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
514
593
  const readSecret = async (token, secret) => {
515
594
  const response = await request(token, "GET", secret);
516
595
  if (response.status === 404) return void 0;
517
- if (!response.ok) throw requestError(`read ${secret}`, response, "secretmanager.secrets.get on the secret");
596
+ if (!response.ok) throw requestError("read", secret, response, "secretmanager.secrets.get on the secret");
518
597
  return response.body;
519
598
  };
520
599
  const getOrCreateSecret = async (token, parent, id) => {
@@ -527,14 +606,21 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
527
606
  const raced = await readSecret(token, secret);
528
607
  if (raced) return raced;
529
608
  }
530
- throw requestError(`create ${secret}`, created, `secretmanager.secrets.create on ${parent}`);
609
+ throw requestError("create", secret, created, `secretmanager.secrets.create on ${parent}`);
531
610
  };
532
611
  const assertCanSet = async (token, secret) => {
533
612
  const response = await request(token, "POST", `${secret}:testIamPermissions`, { permissions: SET_PERMISSIONS });
534
- if (!response.ok) throw requestError(`check its permissions on ${secret}`, response, SET_PERMISSIONS.join(", "));
613
+ if (!response.ok) throw requestError("check its permissions", secret, response, SET_PERMISSIONS.join(", "));
535
614
  const held = isRecord(response.body) && Array.isArray(response.body.permissions) ? response.body.permissions : [];
536
615
  const missing = SET_PERMISSIONS.filter((name) => !held.includes(name));
537
- if (missing.length > 0) throw new Error(`${ADAPTER_NAME} stores a value as a new version of ${secret} and destroys the versions it added before, and the Google identity lacks ${missing.join(", ")} on it: grant a custom role with secretmanager.secrets.get, secretmanager.secrets.update, secretmanager.versions.add, secretmanager.versions.destroy, and secretmanager.versions.access on it, or roles/secretmanager.admin.`);
616
+ if (missing.length > 0) throw failure([
617
+ `${ADAPTER_NAME} can't store a value.`,
618
+ resourceLine(secret),
619
+ ...wrap(`The Google identity lacks ${missing.join(", ")} on it.`),
620
+ "It adds a version, then destroys the versions it added before.",
621
+ "Grant roles/secretmanager.admin, or a custom role with:",
622
+ ...wrap("secretmanager.secrets.get, secretmanager.secrets.update, secretmanager.versions.add, secretmanager.versions.destroy, secretmanager.versions.access")
623
+ ]);
538
624
  };
539
625
  /**
540
626
  * Destroys `version` of `secret`: true once it is destroyed, or already
@@ -581,7 +667,7 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
581
667
  const parent = projectPath();
582
668
  const id = secretId(name);
583
669
  const secret = `${parent}/secrets/${id}`;
584
- if (typeof value !== "string" || value === "") throw new Error(`${ADAPTER_NAME} cannot store an empty ${secret}.`);
670
+ if (typeof value !== "string" || value === "") throw failure([`${ADAPTER_NAME} can't store an empty value.`, resourceLine(secret)]);
585
671
  await mask(value);
586
672
  const token = await accessToken();
587
673
  const current = await getOrCreateSecret(token, parent, id);
@@ -592,7 +678,7 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
592
678
  dataCrc32c: String(crc32c(data))
593
679
  } });
594
680
  const version = versionOf(isRecord(added.body) ? added.body.name : void 0);
595
- if (!added.ok || !VERSION_ID_PATTERN.test(version)) throw requestError(`add a version to ${secret}`, added, "secretmanager.versions.add on the secret");
681
+ if (!added.ok || !VERSION_ID_PATTERN.test(version)) throw requestError("add a version", secret, added, "secretmanager.versions.add on the secret");
596
682
  const visible = await waitForLatest(token, secret, version);
597
683
  const destroyed = /* @__PURE__ */ new Set();
598
684
  if (visible) {
@@ -602,7 +688,8 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
602
688
  if (results[index]) destroyed.add(entry);
603
689
  });
604
690
  }
605
- const notListed = `list version ${version}, which it stored, in ${secret}'s ${VERSIONS_ANNOTATION} annotation, so the next set will not destroy it`;
691
+ const notListed = `record version ${version}, which it stored`;
692
+ const notDestroyed = `The next set won't destroy it: ${VERSIONS_ANNOTATION} misses it.`;
606
693
  let base = current;
607
694
  let updated;
608
695
  try {
@@ -622,10 +709,20 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
622
709
  if (updated.error.status !== "FAILED_PRECONDITION") break;
623
710
  }
624
711
  } catch (error) {
625
- throw new Error(`${ADAPTER_NAME} could not ${notListed} (${error instanceof Error ? error.message : String(error)}).`, { cause: error });
712
+ throw failure([
713
+ `${ADAPTER_NAME} couldn't ${notListed}.`,
714
+ resourceLine(secret),
715
+ ...wrap(error instanceof Error ? error.message : String(error)),
716
+ notDestroyed
717
+ ], error);
626
718
  }
627
- if (!updated?.ok) throw requestError(notListed, updated, "secretmanager.secrets.update on the secret");
628
- if (!visible) throw new Error(`${ADAPTER_NAME} stored ${secret} as version ${version}, and its latest alias does not resolve to that version yet, so it kept the versions it added before; the next set destroys them.`);
719
+ if (!updated?.ok) throw requestError(notListed, secret, updated, "secretmanager.secrets.update on the secret", [notDestroyed]);
720
+ if (!visible) throw failure([
721
+ `${ADAPTER_NAME} stored version ${version}.`,
722
+ resourceLine(secret),
723
+ "Its latest alias doesn't resolve to that version yet.",
724
+ "It kept the versions it added before: the next set destroys them."
725
+ ]);
629
726
  },
630
727
  describe: (name) => {
631
728
  const id = `${prefix}${name}`;
@@ -7202,15 +7202,22 @@ const prepareFirebaseTemplate = async (firebaseRootDir) => {
7202
7202
  };
7203
7203
  //#endregion
7204
7204
  //#region iac/configTemplate.ts
7205
- const getConfigScaffold = (build) => {
7205
+ /**
7206
+ * The config init writes. With the `resources` init set up, it names them;
7207
+ * without them, as the agent scaffold writes it, it reads them from the
7208
+ * environment. Either way it signs in with each person's own Google
7209
+ * credentials (ADC), so it reads no secret.
7210
+ */
7211
+ const getConfigScaffold = (build, resources) => {
7212
+ const projectId = resources === void 0 ? "process.env.HOT_UPDATER_FIREBASE_PROJECT_ID!" : JSON.stringify(resources.projectId);
7206
7213
  const storageConfig = {
7207
7214
  imports: [{
7208
7215
  pkg: "@hot-updater/firebase",
7209
7216
  named: ["firebaseStorage"]
7210
7217
  }],
7211
7218
  configString: `firebaseStorage({
7212
- projectId: process.env.HOT_UPDATER_FIREBASE_PROJECT_ID!,
7213
- storageBucket: process.env.HOT_UPDATER_FIREBASE_STORAGE_BUCKET!,
7219
+ projectId: ${projectId},
7220
+ storageBucket: ${resources === void 0 ? "process.env.HOT_UPDATER_FIREBASE_STORAGE_BUCKET!" : JSON.stringify(resources.storageBucket)},
7214
7221
  credential,
7215
7222
  })`
7216
7223
  };
@@ -7220,8 +7227,8 @@ const getConfigScaffold = (build) => {
7220
7227
  named: ["firebaseDatabase"]
7221
7228
  }],
7222
7229
  configString: `firebaseDatabase({
7223
- projectId: process.env.HOT_UPDATER_FIREBASE_PROJECT_ID!,
7224
- databaseId: process.env.HOT_UPDATER_FIREBASE_DATABASE_ID,
7230
+ projectId: ${projectId},
7231
+ databaseId: ${resources === void 0 ? "process.env.HOT_UPDATER_FIREBASE_DATABASE_ID" : JSON.stringify(resources.databaseId)},
7225
7232
  credential,
7226
7233
  })`
7227
7234
  };
@@ -7315,7 +7322,11 @@ const setEnv = async ({ applicationCredentials, databaseId, projectId, storageBu
7315
7322
  HOT_UPDATER_FIREBASE_STORAGE_BUCKET: storageBucket
7316
7323
  }, ".env.hotupdater", { preserveKeys: applicationCredentials ? [] : [initProvider.inputs.applicationCredentials.envKey] });
7317
7324
  _hot_updater_cli_tools.p.log.success("Firebase credentials have been successfully configured.");
7318
- await (0, _hot_updater_cli_tools.writeHotUpdaterFiles)(getConfigScaffold(build), {
7325
+ await (0, _hot_updater_cli_tools.writeHotUpdaterFiles)(getConfigScaffold(build, {
7326
+ projectId,
7327
+ storageBucket,
7328
+ databaseId
7329
+ }), {
7319
7330
  cwd: process.cwd(),
7320
7331
  settings: "Firebase"
7321
7332
  });
@@ -7744,9 +7755,9 @@ const checkIfGcloudCliInstalled = async () => {
7744
7755
  return false;
7745
7756
  }
7746
7757
  };
7747
- const runInit = async ({ build, envFile }) => {
7758
+ const runInit = async ({ build, envFile, secretsEnv }) => {
7748
7759
  const nonInteractive = envFile !== void 0;
7749
- const initEnvSources = await (0, _hot_updater_cli_tools.readHotUpdaterInitEnv)(process.cwd(), envFile);
7760
+ const initEnvSources = await (0, _hot_updater_cli_tools.readHotUpdaterInitEnv)(process.cwd(), envFile, secretsEnv);
7750
7761
  const { managedEnv } = initEnvSources;
7751
7762
  const initInputEnv = (0, _hot_updater_cli_tools.getHotUpdaterInitInputEnv)(initEnvSources, nonInteractive);
7752
7763
  const savedInputs = resolveFirebaseInitInputs(initInputEnv);
@@ -86,6 +86,6 @@ declare const mergeIndexes: (originalIndexes: {
86
86
  };
87
87
  /** Points the template's `firebase.json` at the database init sets up. */
88
88
  declare const setFirestoreDatabase: (cwd: string, databaseId: string) => Promise<void>;
89
- declare const runInit: ({ build, envFile }: RunInitOptions) => Promise<void>;
89
+ declare const runInit: ({ build, envFile, secretsEnv }: RunInitOptions) => Promise<void>;
90
90
  //#endregion
91
91
  export { initProvider, mergeIndexes, runInit, setFirestoreDatabase };
@@ -86,6 +86,6 @@ declare const mergeIndexes: (originalIndexes: {
86
86
  };
87
87
  /** Points the template's `firebase.json` at the database init sets up. */
88
88
  declare const setFirestoreDatabase: (cwd: string, databaseId: string) => Promise<void>;
89
- declare const runInit: ({ build, envFile }: RunInitOptions) => Promise<void>;
89
+ declare const runInit: ({ build, envFile, secretsEnv }: RunInitOptions) => Promise<void>;
90
90
  //#endregion
91
91
  export { initProvider, mergeIndexes, runInit, setFirestoreDatabase };
@@ -7195,15 +7195,22 @@ const prepareFirebaseTemplate = async (firebaseRootDir) => {
7195
7195
  };
7196
7196
  //#endregion
7197
7197
  //#region iac/configTemplate.ts
7198
- const getConfigScaffold = (build) => {
7198
+ /**
7199
+ * The config init writes. With the `resources` init set up, it names them;
7200
+ * without them, as the agent scaffold writes it, it reads them from the
7201
+ * environment. Either way it signs in with each person's own Google
7202
+ * credentials (ADC), so it reads no secret.
7203
+ */
7204
+ const getConfigScaffold = (build, resources) => {
7205
+ const projectId = resources === void 0 ? "process.env.HOT_UPDATER_FIREBASE_PROJECT_ID!" : JSON.stringify(resources.projectId);
7199
7206
  const storageConfig = {
7200
7207
  imports: [{
7201
7208
  pkg: "@hot-updater/firebase",
7202
7209
  named: ["firebaseStorage"]
7203
7210
  }],
7204
7211
  configString: `firebaseStorage({
7205
- projectId: process.env.HOT_UPDATER_FIREBASE_PROJECT_ID!,
7206
- storageBucket: process.env.HOT_UPDATER_FIREBASE_STORAGE_BUCKET!,
7212
+ projectId: ${projectId},
7213
+ storageBucket: ${resources === void 0 ? "process.env.HOT_UPDATER_FIREBASE_STORAGE_BUCKET!" : JSON.stringify(resources.storageBucket)},
7207
7214
  credential,
7208
7215
  })`
7209
7216
  };
@@ -7213,8 +7220,8 @@ const getConfigScaffold = (build) => {
7213
7220
  named: ["firebaseDatabase"]
7214
7221
  }],
7215
7222
  configString: `firebaseDatabase({
7216
- projectId: process.env.HOT_UPDATER_FIREBASE_PROJECT_ID!,
7217
- databaseId: process.env.HOT_UPDATER_FIREBASE_DATABASE_ID,
7223
+ projectId: ${projectId},
7224
+ databaseId: ${resources === void 0 ? "process.env.HOT_UPDATER_FIREBASE_DATABASE_ID" : JSON.stringify(resources.databaseId)},
7218
7225
  credential,
7219
7226
  })`
7220
7227
  };
@@ -7308,7 +7315,11 @@ const setEnv = async ({ applicationCredentials, databaseId, projectId, storageBu
7308
7315
  HOT_UPDATER_FIREBASE_STORAGE_BUCKET: storageBucket
7309
7316
  }, ".env.hotupdater", { preserveKeys: applicationCredentials ? [] : [initProvider.inputs.applicationCredentials.envKey] });
7310
7317
  p.log.success("Firebase credentials have been successfully configured.");
7311
- await writeHotUpdaterFiles(getConfigScaffold(build), {
7318
+ await writeHotUpdaterFiles(getConfigScaffold(build, {
7319
+ projectId,
7320
+ storageBucket,
7321
+ databaseId
7322
+ }), {
7312
7323
  cwd: process.cwd(),
7313
7324
  settings: "Firebase"
7314
7325
  });
@@ -7737,9 +7748,9 @@ const checkIfGcloudCliInstalled = async () => {
7737
7748
  return false;
7738
7749
  }
7739
7750
  };
7740
- const runInit = async ({ build, envFile }) => {
7751
+ const runInit = async ({ build, envFile, secretsEnv }) => {
7741
7752
  const nonInteractive = envFile !== void 0;
7742
- const initEnvSources = await readHotUpdaterInitEnv(process.cwd(), envFile);
7753
+ const initEnvSources = await readHotUpdaterInitEnv(process.cwd(), envFile, secretsEnv);
7743
7754
  const { managedEnv } = initEnvSources;
7744
7755
  const initInputEnv = getHotUpdaterInitInputEnv(initEnvSources, nonInteractive);
7745
7756
  const savedInputs = resolveFirebaseInitInputs(initInputEnv);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/firebase",
3
3
  "type": "module",
4
- "version": "1.2.0-rc.1",
4
+ "version": "1.2.0-rc.11",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "types": "dist/index.d.cts",
@@ -38,10 +38,10 @@
38
38
  "dependencies": {
39
39
  "hono": "4.12.34",
40
40
  "google-auth-library": "10.7.0",
41
- "@hot-updater/cli-tools": "1.2.0-rc.1",
42
- "@hot-updater/protocol": "1.2.0-rc.1",
43
- "@hot-updater/plugin-core": "1.2.0-rc.1",
44
- "@hot-updater/server": "1.2.0-rc.1"
41
+ "@hot-updater/cli-tools": "1.2.0-rc.11",
42
+ "@hot-updater/protocol": "1.2.0-rc.11",
43
+ "@hot-updater/plugin-core": "1.2.0-rc.11",
44
+ "@hot-updater/server": "1.2.0-rc.11"
45
45
  },
46
46
  "publishConfig": {
47
47
  "access": "public"
@@ -56,8 +56,8 @@
56
56
  "firebase-tools": "15.29.0",
57
57
  "fkill": "^9.0.0",
58
58
  "mime": "^4.0.4",
59
- "@hot-updater/mock": "1.2.0-rc.1",
60
- "@hot-updater/test-utils": "1.2.0-rc.1"
59
+ "@hot-updater/mock": "1.2.0-rc.11",
60
+ "@hot-updater/test-utils": "1.2.0-rc.11"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "firebase-admin": ">=11.10.0",