@hot-updater/vercel 1.0.2 → 1.0.4

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.
@@ -17558,7 +17558,7 @@ const postgres = (config) => {
17558
17558
  };
17559
17559
  //#endregion
17560
17560
  //#region ../../packages/server/dist/version.mjs
17561
- const HOT_UPDATER_SERVER_VERSION = "1.0.2";
17561
+ const HOT_UPDATER_SERVER_VERSION = "1.0.4";
17562
17562
  //#endregion
17563
17563
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
17564
17564
  const createVersionRouteHandlers = (plugins = []) => {
@@ -53404,10 +53404,10 @@ const storeIdOf = (token) => {
53404
53404
  if (prefix !== "vercel" || kind !== "blob" || rw !== "rw" || !id) throw new Error("vercelBlobStorage needs the store's read-write token (vercel_blob_rw_...).");
53405
53405
  return `store_${id}`;
53406
53406
  };
53407
+ const access = "private";
53407
53408
  const vercelBlobStorage = (config) => {
53408
- const { token, access } = config;
53409
+ const { token } = config;
53409
53410
  const storeId = storeIdOf(token);
53410
- const host = `${storeId.slice(6).toLowerCase()}.${access}.blob.vercel-storage.com`;
53411
53411
  const normalizedBasePath = config.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
53412
53412
  const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
53413
53413
  const expiresIn = config.signedUrlExpiresIn ?? 3600;
@@ -53477,7 +53477,6 @@ const vercelBlobStorage = (config) => {
53477
53477
  async getDownloadUrl({ storageUri }) {
53478
53478
  const { key } = parseAndValidate(storageUri);
53479
53479
  const pathname = toPathname(key);
53480
- if (access === "public") return { url: `https://${host}/${pathname.split("/").map(encodeURIComponent).join("/")}` };
53481
53480
  const validUntil = Date.now() + expiresInMs;
53482
53481
  const { presignedUrl } = await presignUrl(await delegationUntil(validUntil), {
53483
53482
  access,
@@ -53564,8 +53563,6 @@ const requireEnv = (name) => {
53564
53563
  if (!value) throw new Error(`${name} is not set on the Vercel project. Run \`hot-updater init\` again to configure it.`);
53565
53564
  return value;
53566
53565
  };
53567
- const access = requireEnv("HOT_UPDATER_VERCEL_BLOB_ACCESS");
53568
- if (access !== "public" && access !== "private") throw new Error("HOT_UPDATER_VERCEL_BLOB_ACCESS must be \"public\" or \"private\".");
53569
53566
  const pool = new import_lib$1.default.Pool({
53570
53567
  connectionString: requireEnv("HOT_UPDATER_VERCEL_DATABASE_URL"),
53571
53568
  idleTimeoutMillis: 5e3,
@@ -53579,10 +53576,7 @@ const hotUpdater = createHotUpdater({
53579
53576
  apiKeys(),
53580
53577
  remoteConfig()
53581
53578
  ],
53582
- storage: vercelBlobStorage({
53583
- access,
53584
- token: requireEnv("HOT_UPDATER_VERCEL_BLOB_READ_WRITE_TOKEN")
53585
- })
53579
+ storage: vercelBlobStorage({ token: requireEnv("HOT_UPDATER_VERCEL_BLOB_READ_WRITE_TOKEN") })
53586
53580
  });
53587
53581
  const handler = async (request) => withoutEncodingVary(await hotUpdater.handlers.client(request));
53588
53582
  //#endregion
package/dist/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
3
3
  let _vercel_blob = require("@vercel/blob");
4
+ let _hot_updater_postgres = require("@hot-updater/postgres");
4
5
  //#region src/vercelBlobStorage.ts
5
6
  const PROTOCOL = "vercel-blob";
6
7
  const ONE_YEAR_IN_SECONDS = 31536e3;
@@ -61,14 +62,15 @@ const storeIdOf = (token) => {
61
62
  if (prefix !== "vercel" || kind !== "blob" || rw !== "rw" || !id) throw new Error("vercelBlobStorage needs the store's read-write token (vercel_blob_rw_...).");
62
63
  return `store_${id}`;
63
64
  };
65
+ /** A private store's objects: only an API key holder's signed URL reaches them. */
66
+ const access = "private";
64
67
  /**
65
- * Bundles in a Vercel Blob store. A public store's objects download from
66
- * their Blob URL; a private store's from a presigned URL that expires.
68
+ * Bundles in a private Vercel Blob store. Devices download from presigned URLs
69
+ * that expire, so the store's objects never sit at a public address.
67
70
  */
68
71
  const vercelBlobStorage = (config) => {
69
- const { token, access } = config;
72
+ const { token } = config;
70
73
  const storeId = storeIdOf(token);
71
- const host = `${storeId.slice(6).toLowerCase()}.${access}.blob.vercel-storage.com`;
72
74
  const normalizedBasePath = config.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
73
75
  const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(normalizedBasePath);
74
76
  const expiresIn = config.signedUrlExpiresIn ?? 3600;
@@ -139,7 +141,6 @@ const vercelBlobStorage = (config) => {
139
141
  async getDownloadUrl({ storageUri }) {
140
142
  const { key } = parseAndValidate(storageUri);
141
143
  const pathname = toPathname(key);
142
- if (access === "public") return { url: `https://${host}/${pathname.split("/").map(encodeURIComponent).join("/")}` };
143
144
  const validUntil = Date.now() + expiresInMs;
144
145
  const { presignedUrl } = await (0, _vercel_blob.presignUrl)(await delegationUntil(validUntil), {
145
146
  access,
@@ -205,4 +206,10 @@ const vercelBlobStorage = (config) => {
205
206
  });
206
207
  };
207
208
  //#endregion
209
+ Object.defineProperty(exports, "postgres", {
210
+ enumerable: true,
211
+ get: function() {
212
+ return _hot_updater_postgres.postgres;
213
+ }
214
+ });
208
215
  exports.vercelBlobStorage = vercelBlobStorage;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { StorageAdapterWith } from "@hot-updater/plugin-core";
2
+ import { PostgresConfig, postgres } from "@hot-updater/postgres";
2
3
  //#region src/vercelBlobStorage.d.ts
3
4
  interface VercelBlobStorageConfig {
4
5
  /**
@@ -6,21 +7,19 @@ interface VercelBlobStorageConfig {
6
7
  * the `BLOB_READ_WRITE_TOKEN` Vercel adds to a project the store connects to.
7
8
  */
8
9
  token: string;
9
- /** The store's access, which Vercel fixes when it creates the store. */
10
- access: "public" | "private";
11
10
  /** Base path where bundles will be stored in the store. */
12
11
  basePath?: string;
13
12
  /**
14
- * Lifetime of a private store's download URLs in seconds, at most seven
15
- * days, Vercel's limit. @default 3600
13
+ * Lifetime of the download URLs in seconds, at most seven days, Vercel's
14
+ * limit. @default 3600
16
15
  */
17
16
  signedUrlExpiresIn?: number;
18
17
  }
19
18
  type VercelBlobStorageOperations = "put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects";
20
19
  /**
21
- * Bundles in a Vercel Blob store. A public store's objects download from
22
- * their Blob URL; a private store's from a presigned URL that expires.
20
+ * Bundles in a private Vercel Blob store. Devices download from presigned URLs
21
+ * that expire, so the store's objects never sit at a public address.
23
22
  */
24
23
  declare const vercelBlobStorage: (config: VercelBlobStorageConfig) => StorageAdapterWith<VercelBlobStorageOperations>;
25
24
  //#endregion
26
- export { VercelBlobStorageConfig, vercelBlobStorage };
25
+ export { type PostgresConfig, VercelBlobStorageConfig, postgres, vercelBlobStorage };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { StorageAdapterWith } from "@hot-updater/plugin-core";
2
+ import { PostgresConfig, postgres } from "@hot-updater/postgres";
2
3
  //#region src/vercelBlobStorage.d.ts
3
4
  interface VercelBlobStorageConfig {
4
5
  /**
@@ -6,21 +7,19 @@ interface VercelBlobStorageConfig {
6
7
  * the `BLOB_READ_WRITE_TOKEN` Vercel adds to a project the store connects to.
7
8
  */
8
9
  token: string;
9
- /** The store's access, which Vercel fixes when it creates the store. */
10
- access: "public" | "private";
11
10
  /** Base path where bundles will be stored in the store. */
12
11
  basePath?: string;
13
12
  /**
14
- * Lifetime of a private store's download URLs in seconds, at most seven
15
- * days, Vercel's limit. @default 3600
13
+ * Lifetime of the download URLs in seconds, at most seven days, Vercel's
14
+ * limit. @default 3600
16
15
  */
17
16
  signedUrlExpiresIn?: number;
18
17
  }
19
18
  type VercelBlobStorageOperations = "put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects";
20
19
  /**
21
- * Bundles in a Vercel Blob store. A public store's objects download from
22
- * their Blob URL; a private store's from a presigned URL that expires.
20
+ * Bundles in a private Vercel Blob store. Devices download from presigned URLs
21
+ * that expire, so the store's objects never sit at a public address.
23
22
  */
24
23
  declare const vercelBlobStorage: (config: VercelBlobStorageConfig) => StorageAdapterWith<VercelBlobStorageOperations>;
25
24
  //#endregion
26
- export { VercelBlobStorageConfig, vercelBlobStorage };
25
+ export { type PostgresConfig, VercelBlobStorageConfig, postgres, vercelBlobStorage };
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createStorageAdapter, createStorageKeyBuilder, createStorageUri, parseStorageUri } from "@hot-updater/plugin-core";
2
2
  import { BlobNotFoundError, del, get, head, issueSignedToken, list, presignUrl, put } from "@vercel/blob";
3
+ import { postgres } from "@hot-updater/postgres";
3
4
  //#region src/vercelBlobStorage.ts
4
5
  const PROTOCOL = "vercel-blob";
5
6
  const ONE_YEAR_IN_SECONDS = 31536e3;
@@ -60,14 +61,15 @@ const storeIdOf = (token) => {
60
61
  if (prefix !== "vercel" || kind !== "blob" || rw !== "rw" || !id) throw new Error("vercelBlobStorage needs the store's read-write token (vercel_blob_rw_...).");
61
62
  return `store_${id}`;
62
63
  };
64
+ /** A private store's objects: only an API key holder's signed URL reaches them. */
65
+ const access = "private";
63
66
  /**
64
- * Bundles in a Vercel Blob store. A public store's objects download from
65
- * their Blob URL; a private store's from a presigned URL that expires.
67
+ * Bundles in a private Vercel Blob store. Devices download from presigned URLs
68
+ * that expire, so the store's objects never sit at a public address.
66
69
  */
67
70
  const vercelBlobStorage = (config) => {
68
- const { token, access } = config;
71
+ const { token } = config;
69
72
  const storeId = storeIdOf(token);
70
- const host = `${storeId.slice(6).toLowerCase()}.${access}.blob.vercel-storage.com`;
71
73
  const normalizedBasePath = config.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
72
74
  const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
73
75
  const expiresIn = config.signedUrlExpiresIn ?? 3600;
@@ -138,7 +140,6 @@ const vercelBlobStorage = (config) => {
138
140
  async getDownloadUrl({ storageUri }) {
139
141
  const { key } = parseAndValidate(storageUri);
140
142
  const pathname = toPathname(key);
141
- if (access === "public") return { url: `https://${host}/${pathname.split("/").map(encodeURIComponent).join("/")}` };
142
143
  const validUntil = Date.now() + expiresInMs;
143
144
  const { presignedUrl } = await presignUrl(await delegationUntil(validUntil), {
144
145
  access,
@@ -204,4 +205,4 @@ const vercelBlobStorage = (config) => {
204
205
  });
205
206
  };
206
207
  //#endregion
207
- export { vercelBlobStorage };
208
+ export { postgres, vercelBlobStorage };
@@ -6276,14 +6276,10 @@ const getConfigScaffold = (build) => {
6276
6276
  }],
6277
6277
  configString: `vercelBlobStorage({
6278
6278
  token: process.env.HOT_UPDATER_VERCEL_BLOB_READ_WRITE_TOKEN!,
6279
- access:
6280
- process.env.HOT_UPDATER_VERCEL_BLOB_ACCESS === "public"
6281
- ? "public"
6282
- : "private",
6283
6279
  })`
6284
6280
  }).setDatabase({
6285
6281
  imports: [{
6286
- pkg: "@hot-updater/postgres",
6282
+ pkg: "@hot-updater/vercel",
6287
6283
  named: ["postgres"]
6288
6284
  }],
6289
6285
  configString: `postgres({
@@ -6310,7 +6306,6 @@ const getConfigScaffold = (build) => {
6310
6306
  const isVercelProjectName = (value) => value !== void 0 && /^[a-z0-9][a-z0-9._-]{0,99}$/.test(value) && !value.includes("---");
6311
6307
  /** A Blob store name: letters, digits, `_`, and `-`. */
6312
6308
  const isVercelBlobStoreName = (value) => value !== void 0 && /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/.test(value);
6313
- const isVercelBlobAccess = (value) => value === "public" || value === "private";
6314
6309
  const isPostgresUrl = (value) => {
6315
6310
  if (value === void 0) return false;
6316
6311
  try {
@@ -6363,16 +6358,6 @@ const initProvider = {
6363
6358
  help: "Blob store ID (store_...), saved by init",
6364
6359
  optional: true
6365
6360
  },
6366
- blobAccess: {
6367
- envKey: "HOT_UPDATER_VERCEL_BLOB_ACCESS",
6368
- help: "The Blob store's access: private (the default) or public",
6369
- optional: true,
6370
- prompt: {
6371
- message: "Make the new Blob store private?",
6372
- type: "confirm"
6373
- },
6374
- validate: isVercelBlobAccess
6375
- },
6376
6361
  blobReadWriteToken: {
6377
6362
  envKey: "HOT_UPDATER_VERCEL_BLOB_READ_WRITE_TOKEN",
6378
6363
  help: "The Blob store's read-write token, which init reads from the project",
@@ -6551,6 +6536,10 @@ const createVercelApi = ({ token, teamId, fetch: fetchImpl = fetch }) => {
6551
6536
  envVarPrefix: input.envVarPrefix
6552
6537
  } });
6553
6538
  },
6539
+ /** Removes a store's connection, and the variables it added, from a project. */
6540
+ async disconnectBlobStore(storeId, connectionId) {
6541
+ await request("DELETE", `/v1/storage/stores/${encodeURIComponent(storeId)}/connections/${encodeURIComponent(connectionId)}`);
6542
+ },
6554
6543
  async listProjectEnv(projectId) {
6555
6544
  return (await paginate(`/v10/projects/${encodeURIComponent(projectId)}/env`, "envs")).flatMap((env) => isRecord(env) && typeof env.id === "string" && typeof env.key === "string" ? [{
6556
6545
  id: env.id,
@@ -6791,7 +6780,6 @@ const inputs = initProvider.inputs;
6791
6780
  const BLOB_ENV_PREFIX = "HOT_UPDATER_VERCEL_BLOB";
6792
6781
  const BLOB_TOKEN_ENV = inputs.blobReadWriteToken.envKey;
6793
6782
  const DATABASE_URL_ENV = inputs.databaseUrl.envKey;
6794
- const BLOB_ACCESS_ENV = inputs.blobAccess.envKey;
6795
6783
  /** Project env vars a Marketplace Postgres database adds, such as Neon's `DATABASE_URL`. */
6796
6784
  const DATABASE_URL_ENV_PATTERN = /(?:^|_)(?:DATABASE_URL|POSTGRES_URL)$/;
6797
6785
  const cancelled = (value) => _hot_updater_cli_tools.p.isCancel(value);
@@ -6849,10 +6837,39 @@ const resolveProjectName = async (savedName, nonInteractive) => {
6849
6837
  validate: (value) => isVercelProjectName(value) ? void 0 : "Use up to 100 lowercase letters, digits, '.', '_', or '-', without '---'."
6850
6838
  }));
6851
6839
  };
6852
- /** The saved store, or one the user selects or creates; undefined to create `name`. */
6840
+ /**
6841
+ * The saved store, or one the user selects or creates; `replaces` is a saved
6842
+ * public store the selection takes the place of.
6843
+ */
6853
6844
  const selectBlobStore = async ({ nonInteractive, savedId, savedName, stores }) => {
6854
- const saved = stores.find((store) => store.id === savedId) ?? (savedId === void 0 ? stores.find((store) => store.name === savedName) : void 0);
6845
+ const found = stores.find((store) => store.id === savedId) ?? (savedId === void 0 ? stores.find((store) => store.name === savedName) : void 0);
6846
+ const publicStore = found?.access === "public" ? found : void 0;
6847
+ const replacement = publicStore && `Blob store ${publicStore.name} is public. Hot Updater keeps bundles in a private store, and the bundles deployed to ${publicStore.name} stop downloading until you deploy them again.`;
6848
+ const saved = publicStore ? void 0 : found;
6849
+ const privateStores = stores.filter((store) => store.access === "private");
6855
6850
  if (nonInteractive) {
6851
+ if (publicStore) {
6852
+ const named = stores.find((store) => store.name === savedName);
6853
+ if (savedName && savedName !== publicStore.name) {
6854
+ if (named?.access === "private") {
6855
+ _hot_updater_cli_tools.p.log.warn(replacement);
6856
+ return {
6857
+ kind: "existing",
6858
+ store: named,
6859
+ replaces: publicStore
6860
+ };
6861
+ }
6862
+ if (!named) {
6863
+ _hot_updater_cli_tools.p.log.warn(replacement);
6864
+ return {
6865
+ kind: "create",
6866
+ name: savedName,
6867
+ replaces: publicStore
6868
+ };
6869
+ }
6870
+ }
6871
+ throw new _hot_updater_cli_tools.InitError(`${replacement} Set ${inputs.blobStoreName.envKey} to a private or new store to replace it.`);
6872
+ }
6856
6873
  if (saved) return {
6857
6874
  kind: "existing",
6858
6875
  store: saved
@@ -6863,42 +6880,43 @@ const selectBlobStore = async ({ nonInteractive, savedId, savedName, stores }) =
6863
6880
  };
6864
6881
  throw new _hot_updater_cli_tools.InitError(`${inputs.blobStoreName.envKey} is required to select the Blob store.`);
6865
6882
  }
6866
- if ((savedId || savedName) && !saved) _hot_updater_cli_tools.p.log.warn("The saved Blob store was not found. Select a store again.");
6883
+ if (publicStore) {
6884
+ _hot_updater_cli_tools.p.log.warn(replacement);
6885
+ if (!exitIfCancelled(await _hot_updater_cli_tools.p.confirm({
6886
+ message: `Replace ${publicStore.name} with a private Blob store?`,
6887
+ initialValue: false
6888
+ }))) process.exit(1);
6889
+ } else if ((savedId || savedName) && !saved) _hot_updater_cli_tools.p.log.warn("The saved Blob store was not found. Select a store again.");
6867
6890
  const createKey = "\0create";
6868
6891
  const selected = exitIfCancelled(await _hot_updater_cli_tools.p.select({
6869
6892
  message: "Select the Blob store for bundles",
6870
- initialValue: saved?.id ?? stores[0]?.id ?? createKey,
6871
- options: [...stores.map((store) => ({
6893
+ initialValue: saved?.id ?? privateStores[0]?.id ?? createKey,
6894
+ options: [...privateStores.map((store) => ({
6872
6895
  value: store.id,
6873
- label: `${store.name} (${store.access}${store.region ? `, ${store.region}` : ""})`
6896
+ label: `${store.name}${store.region ? ` (${store.region})` : ""}`
6874
6897
  })), {
6875
6898
  value: createKey,
6876
6899
  label: "Create a new Blob store"
6877
6900
  }]
6878
6901
  }));
6879
- const store = stores.find((candidate) => candidate.id === selected);
6902
+ const store = privateStores.find((candidate) => candidate.id === selected);
6903
+ const replaces = publicStore ? { replaces: publicStore } : {};
6880
6904
  if (store) return {
6881
6905
  kind: "existing",
6882
- store
6906
+ store,
6907
+ ...replaces
6883
6908
  };
6884
6909
  const prompt = inputs.blobStoreName.prompt;
6885
6910
  return {
6886
6911
  kind: "create",
6887
6912
  name: exitIfCancelled(await _hot_updater_cli_tools.p.text({
6888
- ...(0, _hot_updater_cli_tools.getInitProviderTextPromptValues)(prompt, savedName),
6913
+ ...(0, _hot_updater_cli_tools.getInitProviderTextPromptValues)(prompt, savedName === publicStore?.name ? void 0 : savedName),
6889
6914
  message: prompt.message,
6890
6915
  validate: (value) => isVercelBlobStoreName(value) ? void 0 : "Use up to 32 letters, digits, '_', or '-'."
6891
- }))
6916
+ })),
6917
+ ...replaces
6892
6918
  };
6893
6919
  };
6894
- const resolveNewStoreAccess = async (savedAccess, nonInteractive) => {
6895
- if (isVercelBlobAccess(savedAccess) && nonInteractive) return savedAccess;
6896
- if (nonInteractive) return "private";
6897
- return exitIfCancelled(await _hot_updater_cli_tools.p.confirm({
6898
- message: inputs.blobAccess.prompt.message,
6899
- initialValue: savedAccess !== "public"
6900
- })) ? "private" : "public";
6901
- };
6902
6920
  /**
6903
6921
  * The store's read-write token: from its connection to the project, which
6904
6922
  * init makes when there is none.
@@ -6917,6 +6935,7 @@ const resolveBlobToken = async (api, project, store) => {
6917
6935
  const env = (await api.listProjectEnv(project.id)).find((candidate) => candidate.key === tokenEnv && candidate.target.includes("production"));
6918
6936
  const token = env === void 0 ? void 0 : await api.getProjectEnvValue(project.id, env.id);
6919
6937
  if (!token?.startsWith("vercel_blob_rw_")) throw new _hot_updater_cli_tools.InitError(`Blob store ${store.name} is connected to project ${project.name} without a read-write token for production. Reconnect it from the project's Storage tab with a read-write token, then rerun init.`);
6938
+ if (`store_${token.split("_")[3]}` !== store.id) throw new _hot_updater_cli_tools.InitError(`The production ${tokenEnv} of project ${project.name} belongs to another Blob store, not ${store.name}. Disconnect that store from the project's Storage tab, then rerun init.`);
6920
6939
  return token;
6921
6940
  };
6922
6941
  /**
@@ -7051,7 +7070,6 @@ const runInit = async ({ build, envFile }) => {
7051
7070
  ...saved.blobStoreId ? { savedId: saved.blobStoreId } : {},
7052
7071
  ...saved.blobStoreName ? { savedName: saved.blobStoreName } : {}
7053
7072
  });
7054
- const newStoreAccess = storeSelection.kind === "create" ? await resolveNewStoreAccess(saved.blobAccess, nonInteractive) : void 0;
7055
7073
  let databaseUrl = nonInteractive ? saved.databaseUrl : void 0;
7056
7074
  if (nonInteractive && !isPostgresUrl(databaseUrl)) throw new _hot_updater_cli_tools.InitError(`${DATABASE_URL_ENV} must be a postgres:// or postgresql:// URL.`);
7057
7075
  const project = existingProject ?? await (async () => {
@@ -7068,28 +7086,31 @@ const runInit = async ({ build, envFile }) => {
7068
7086
  ...saved.databaseUrl ? { savedUrl: saved.databaseUrl } : {}
7069
7087
  });
7070
7088
  assertPostgresCanInitialize(await readHotUpdaterTables(databaseUrl));
7089
+ for (const connection of storeSelection.replaces?.connections ?? []) {
7090
+ if (connection.projectId !== project.id) continue;
7091
+ await api.disconnectBlobStore(storeSelection.replaces.id, connection.id);
7092
+ _hot_updater_cli_tools.p.log.info(`Disconnected Blob store ${storeSelection.replaces.name} from ${project.name}`);
7093
+ }
7071
7094
  let store;
7072
7095
  if (storeSelection.kind === "existing") {
7073
7096
  store = storeSelection.store;
7074
7097
  _hot_updater_cli_tools.p.log.info(`Selected Blob store: ${store.name}`);
7075
7098
  } else {
7076
- const access = newStoreAccess ?? "private";
7077
7099
  const { id } = await api.createBlobStore({
7078
- access,
7100
+ access: "private",
7079
7101
  name: storeSelection.name
7080
7102
  });
7081
7103
  store = {
7082
- access,
7104
+ access: "private",
7083
7105
  connections: [],
7084
7106
  id,
7085
7107
  name: storeSelection.name,
7086
7108
  region: void 0
7087
7109
  };
7088
- _hot_updater_cli_tools.p.log.info(`Created Blob store: ${store.name} (${access})`);
7110
+ _hot_updater_cli_tools.p.log.info(`Created Blob store: ${store.name} (private)`);
7089
7111
  }
7090
7112
  const blobToken = await resolveBlobToken(api, project, store);
7091
7113
  const resolvedInputs = {
7092
- blobAccess: store.access,
7093
7114
  blobReadWriteToken: blobToken,
7094
7115
  blobStoreId: store.id,
7095
7116
  blobStoreName: store.name,
@@ -7124,7 +7145,6 @@ const runInit = async ({ build, envFile }) => {
7124
7145
  title: "Setting the Function's environment variables...",
7125
7146
  task: async () => {
7126
7147
  await api.setProductionEnv(project.id, DATABASE_URL_ENV, databaseUrl);
7127
- await api.setProductionEnv(project.id, BLOB_ACCESS_ENV, store.access);
7128
7148
  await syncFunctionBlobToken(api, project, store, blobToken);
7129
7149
  return "Set the Function's environment variables";
7130
7150
  }
@@ -43,16 +43,6 @@ declare const initProvider: {
43
43
  readonly help: "Blob store ID (store_...), saved by init";
44
44
  readonly optional: true;
45
45
  };
46
- readonly blobAccess: {
47
- readonly envKey: "HOT_UPDATER_VERCEL_BLOB_ACCESS";
48
- readonly help: "The Blob store's access: private (the default) or public";
49
- readonly optional: true;
50
- readonly prompt: {
51
- readonly message: "Make the new Blob store private?";
52
- readonly type: "confirm";
53
- };
54
- readonly validate: (value: string | undefined) => value is "public" | "private";
55
- };
56
46
  readonly blobReadWriteToken: {
57
47
  readonly envKey: "HOT_UPDATER_VERCEL_BLOB_READ_WRITE_TOKEN";
58
48
  readonly help: "The Blob store's read-write token, which init reads from the project";
@@ -43,16 +43,6 @@ declare const initProvider: {
43
43
  readonly help: "Blob store ID (store_...), saved by init";
44
44
  readonly optional: true;
45
45
  };
46
- readonly blobAccess: {
47
- readonly envKey: "HOT_UPDATER_VERCEL_BLOB_ACCESS";
48
- readonly help: "The Blob store's access: private (the default) or public";
49
- readonly optional: true;
50
- readonly prompt: {
51
- readonly message: "Make the new Blob store private?";
52
- readonly type: "confirm";
53
- };
54
- readonly validate: (value: string | undefined) => value is "public" | "private";
55
- };
56
46
  readonly blobReadWriteToken: {
57
47
  readonly envKey: "HOT_UPDATER_VERCEL_BLOB_READ_WRITE_TOKEN";
58
48
  readonly help: "The Blob store's read-write token, which init reads from the project";
@@ -6270,14 +6270,10 @@ const getConfigScaffold = (build) => {
6270
6270
  }],
6271
6271
  configString: `vercelBlobStorage({
6272
6272
  token: process.env.HOT_UPDATER_VERCEL_BLOB_READ_WRITE_TOKEN!,
6273
- access:
6274
- process.env.HOT_UPDATER_VERCEL_BLOB_ACCESS === "public"
6275
- ? "public"
6276
- : "private",
6277
6273
  })`
6278
6274
  }).setDatabase({
6279
6275
  imports: [{
6280
- pkg: "@hot-updater/postgres",
6276
+ pkg: "@hot-updater/vercel",
6281
6277
  named: ["postgres"]
6282
6278
  }],
6283
6279
  configString: `postgres({
@@ -6304,7 +6300,6 @@ const getConfigScaffold = (build) => {
6304
6300
  const isVercelProjectName = (value) => value !== void 0 && /^[a-z0-9][a-z0-9._-]{0,99}$/.test(value) && !value.includes("---");
6305
6301
  /** A Blob store name: letters, digits, `_`, and `-`. */
6306
6302
  const isVercelBlobStoreName = (value) => value !== void 0 && /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/.test(value);
6307
- const isVercelBlobAccess = (value) => value === "public" || value === "private";
6308
6303
  const isPostgresUrl = (value) => {
6309
6304
  if (value === void 0) return false;
6310
6305
  try {
@@ -6357,16 +6352,6 @@ const initProvider = {
6357
6352
  help: "Blob store ID (store_...), saved by init",
6358
6353
  optional: true
6359
6354
  },
6360
- blobAccess: {
6361
- envKey: "HOT_UPDATER_VERCEL_BLOB_ACCESS",
6362
- help: "The Blob store's access: private (the default) or public",
6363
- optional: true,
6364
- prompt: {
6365
- message: "Make the new Blob store private?",
6366
- type: "confirm"
6367
- },
6368
- validate: isVercelBlobAccess
6369
- },
6370
6355
  blobReadWriteToken: {
6371
6356
  envKey: "HOT_UPDATER_VERCEL_BLOB_READ_WRITE_TOKEN",
6372
6357
  help: "The Blob store's read-write token, which init reads from the project",
@@ -6545,6 +6530,10 @@ const createVercelApi = ({ token, teamId, fetch: fetchImpl = fetch }) => {
6545
6530
  envVarPrefix: input.envVarPrefix
6546
6531
  } });
6547
6532
  },
6533
+ /** Removes a store's connection, and the variables it added, from a project. */
6534
+ async disconnectBlobStore(storeId, connectionId) {
6535
+ await request("DELETE", `/v1/storage/stores/${encodeURIComponent(storeId)}/connections/${encodeURIComponent(connectionId)}`);
6536
+ },
6548
6537
  async listProjectEnv(projectId) {
6549
6538
  return (await paginate(`/v10/projects/${encodeURIComponent(projectId)}/env`, "envs")).flatMap((env) => isRecord(env) && typeof env.id === "string" && typeof env.key === "string" ? [{
6550
6539
  id: env.id,
@@ -6785,7 +6774,6 @@ const inputs = initProvider.inputs;
6785
6774
  const BLOB_ENV_PREFIX = "HOT_UPDATER_VERCEL_BLOB";
6786
6775
  const BLOB_TOKEN_ENV = inputs.blobReadWriteToken.envKey;
6787
6776
  const DATABASE_URL_ENV = inputs.databaseUrl.envKey;
6788
- const BLOB_ACCESS_ENV = inputs.blobAccess.envKey;
6789
6777
  /** Project env vars a Marketplace Postgres database adds, such as Neon's `DATABASE_URL`. */
6790
6778
  const DATABASE_URL_ENV_PATTERN = /(?:^|_)(?:DATABASE_URL|POSTGRES_URL)$/;
6791
6779
  const cancelled = (value) => p.isCancel(value);
@@ -6843,10 +6831,39 @@ const resolveProjectName = async (savedName, nonInteractive) => {
6843
6831
  validate: (value) => isVercelProjectName(value) ? void 0 : "Use up to 100 lowercase letters, digits, '.', '_', or '-', without '---'."
6844
6832
  }));
6845
6833
  };
6846
- /** The saved store, or one the user selects or creates; undefined to create `name`. */
6834
+ /**
6835
+ * The saved store, or one the user selects or creates; `replaces` is a saved
6836
+ * public store the selection takes the place of.
6837
+ */
6847
6838
  const selectBlobStore = async ({ nonInteractive, savedId, savedName, stores }) => {
6848
- const saved = stores.find((store) => store.id === savedId) ?? (savedId === void 0 ? stores.find((store) => store.name === savedName) : void 0);
6839
+ const found = stores.find((store) => store.id === savedId) ?? (savedId === void 0 ? stores.find((store) => store.name === savedName) : void 0);
6840
+ const publicStore = found?.access === "public" ? found : void 0;
6841
+ const replacement = publicStore && `Blob store ${publicStore.name} is public. Hot Updater keeps bundles in a private store, and the bundles deployed to ${publicStore.name} stop downloading until you deploy them again.`;
6842
+ const saved = publicStore ? void 0 : found;
6843
+ const privateStores = stores.filter((store) => store.access === "private");
6849
6844
  if (nonInteractive) {
6845
+ if (publicStore) {
6846
+ const named = stores.find((store) => store.name === savedName);
6847
+ if (savedName && savedName !== publicStore.name) {
6848
+ if (named?.access === "private") {
6849
+ p.log.warn(replacement);
6850
+ return {
6851
+ kind: "existing",
6852
+ store: named,
6853
+ replaces: publicStore
6854
+ };
6855
+ }
6856
+ if (!named) {
6857
+ p.log.warn(replacement);
6858
+ return {
6859
+ kind: "create",
6860
+ name: savedName,
6861
+ replaces: publicStore
6862
+ };
6863
+ }
6864
+ }
6865
+ throw new InitError(`${replacement} Set ${inputs.blobStoreName.envKey} to a private or new store to replace it.`);
6866
+ }
6850
6867
  if (saved) return {
6851
6868
  kind: "existing",
6852
6869
  store: saved
@@ -6857,42 +6874,43 @@ const selectBlobStore = async ({ nonInteractive, savedId, savedName, stores }) =
6857
6874
  };
6858
6875
  throw new InitError(`${inputs.blobStoreName.envKey} is required to select the Blob store.`);
6859
6876
  }
6860
- if ((savedId || savedName) && !saved) p.log.warn("The saved Blob store was not found. Select a store again.");
6877
+ if (publicStore) {
6878
+ p.log.warn(replacement);
6879
+ if (!exitIfCancelled(await p.confirm({
6880
+ message: `Replace ${publicStore.name} with a private Blob store?`,
6881
+ initialValue: false
6882
+ }))) process.exit(1);
6883
+ } else if ((savedId || savedName) && !saved) p.log.warn("The saved Blob store was not found. Select a store again.");
6861
6884
  const createKey = "\0create";
6862
6885
  const selected = exitIfCancelled(await p.select({
6863
6886
  message: "Select the Blob store for bundles",
6864
- initialValue: saved?.id ?? stores[0]?.id ?? createKey,
6865
- options: [...stores.map((store) => ({
6887
+ initialValue: saved?.id ?? privateStores[0]?.id ?? createKey,
6888
+ options: [...privateStores.map((store) => ({
6866
6889
  value: store.id,
6867
- label: `${store.name} (${store.access}${store.region ? `, ${store.region}` : ""})`
6890
+ label: `${store.name}${store.region ? ` (${store.region})` : ""}`
6868
6891
  })), {
6869
6892
  value: createKey,
6870
6893
  label: "Create a new Blob store"
6871
6894
  }]
6872
6895
  }));
6873
- const store = stores.find((candidate) => candidate.id === selected);
6896
+ const store = privateStores.find((candidate) => candidate.id === selected);
6897
+ const replaces = publicStore ? { replaces: publicStore } : {};
6874
6898
  if (store) return {
6875
6899
  kind: "existing",
6876
- store
6900
+ store,
6901
+ ...replaces
6877
6902
  };
6878
6903
  const prompt = inputs.blobStoreName.prompt;
6879
6904
  return {
6880
6905
  kind: "create",
6881
6906
  name: exitIfCancelled(await p.text({
6882
- ...getInitProviderTextPromptValues(prompt, savedName),
6907
+ ...getInitProviderTextPromptValues(prompt, savedName === publicStore?.name ? void 0 : savedName),
6883
6908
  message: prompt.message,
6884
6909
  validate: (value) => isVercelBlobStoreName(value) ? void 0 : "Use up to 32 letters, digits, '_', or '-'."
6885
- }))
6910
+ })),
6911
+ ...replaces
6886
6912
  };
6887
6913
  };
6888
- const resolveNewStoreAccess = async (savedAccess, nonInteractive) => {
6889
- if (isVercelBlobAccess(savedAccess) && nonInteractive) return savedAccess;
6890
- if (nonInteractive) return "private";
6891
- return exitIfCancelled(await p.confirm({
6892
- message: inputs.blobAccess.prompt.message,
6893
- initialValue: savedAccess !== "public"
6894
- })) ? "private" : "public";
6895
- };
6896
6914
  /**
6897
6915
  * The store's read-write token: from its connection to the project, which
6898
6916
  * init makes when there is none.
@@ -6911,6 +6929,7 @@ const resolveBlobToken = async (api, project, store) => {
6911
6929
  const env = (await api.listProjectEnv(project.id)).find((candidate) => candidate.key === tokenEnv && candidate.target.includes("production"));
6912
6930
  const token = env === void 0 ? void 0 : await api.getProjectEnvValue(project.id, env.id);
6913
6931
  if (!token?.startsWith("vercel_blob_rw_")) throw new InitError(`Blob store ${store.name} is connected to project ${project.name} without a read-write token for production. Reconnect it from the project's Storage tab with a read-write token, then rerun init.`);
6932
+ if (`store_${token.split("_")[3]}` !== store.id) throw new InitError(`The production ${tokenEnv} of project ${project.name} belongs to another Blob store, not ${store.name}. Disconnect that store from the project's Storage tab, then rerun init.`);
6914
6933
  return token;
6915
6934
  };
6916
6935
  /**
@@ -7045,7 +7064,6 @@ const runInit = async ({ build, envFile }) => {
7045
7064
  ...saved.blobStoreId ? { savedId: saved.blobStoreId } : {},
7046
7065
  ...saved.blobStoreName ? { savedName: saved.blobStoreName } : {}
7047
7066
  });
7048
- const newStoreAccess = storeSelection.kind === "create" ? await resolveNewStoreAccess(saved.blobAccess, nonInteractive) : void 0;
7049
7067
  let databaseUrl = nonInteractive ? saved.databaseUrl : void 0;
7050
7068
  if (nonInteractive && !isPostgresUrl(databaseUrl)) throw new InitError(`${DATABASE_URL_ENV} must be a postgres:// or postgresql:// URL.`);
7051
7069
  const project = existingProject ?? await (async () => {
@@ -7062,28 +7080,31 @@ const runInit = async ({ build, envFile }) => {
7062
7080
  ...saved.databaseUrl ? { savedUrl: saved.databaseUrl } : {}
7063
7081
  });
7064
7082
  assertPostgresCanInitialize(await readHotUpdaterTables(databaseUrl));
7083
+ for (const connection of storeSelection.replaces?.connections ?? []) {
7084
+ if (connection.projectId !== project.id) continue;
7085
+ await api.disconnectBlobStore(storeSelection.replaces.id, connection.id);
7086
+ p.log.info(`Disconnected Blob store ${storeSelection.replaces.name} from ${project.name}`);
7087
+ }
7065
7088
  let store;
7066
7089
  if (storeSelection.kind === "existing") {
7067
7090
  store = storeSelection.store;
7068
7091
  p.log.info(`Selected Blob store: ${store.name}`);
7069
7092
  } else {
7070
- const access = newStoreAccess ?? "private";
7071
7093
  const { id } = await api.createBlobStore({
7072
- access,
7094
+ access: "private",
7073
7095
  name: storeSelection.name
7074
7096
  });
7075
7097
  store = {
7076
- access,
7098
+ access: "private",
7077
7099
  connections: [],
7078
7100
  id,
7079
7101
  name: storeSelection.name,
7080
7102
  region: void 0
7081
7103
  };
7082
- p.log.info(`Created Blob store: ${store.name} (${access})`);
7104
+ p.log.info(`Created Blob store: ${store.name} (private)`);
7083
7105
  }
7084
7106
  const blobToken = await resolveBlobToken(api, project, store);
7085
7107
  const resolvedInputs = {
7086
- blobAccess: store.access,
7087
7108
  blobReadWriteToken: blobToken,
7088
7109
  blobStoreId: store.id,
7089
7110
  blobStoreName: store.name,
@@ -7118,7 +7139,6 @@ const runInit = async ({ build, envFile }) => {
7118
7139
  title: "Setting the Function's environment variables...",
7119
7140
  task: async () => {
7120
7141
  await api.setProductionEnv(project.id, DATABASE_URL_ENV, databaseUrl);
7121
- await api.setProductionEnv(project.id, BLOB_ACCESS_ENV, store.access);
7122
7142
  await syncFunctionBlobToken(api, project, store, blobToken);
7123
7143
  return "Set the Function's environment variables";
7124
7144
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/vercel",
3
3
  "type": "module",
4
- "version": "1.0.2",
4
+ "version": "1.0.4",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
@@ -41,11 +41,11 @@
41
41
  "@vercel/cli-auth": "0.3.10",
42
42
  "@vercel/cli-config": "0.3.1",
43
43
  "pg": "8.13.1",
44
- "@hot-updater/cli-tools": "1.0.2",
45
- "@hot-updater/plugin-core": "1.0.2",
46
- "@hot-updater/postgres": "1.0.2",
47
- "@hot-updater/protocol": "1.0.2",
48
- "@hot-updater/server": "1.0.2"
44
+ "@hot-updater/cli-tools": "1.0.4",
45
+ "@hot-updater/plugin-core": "1.0.4",
46
+ "@hot-updater/postgres": "1.0.4",
47
+ "@hot-updater/protocol": "1.0.4",
48
+ "@hot-updater/server": "1.0.4"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^20",
@@ -54,7 +54,7 @@
54
54
  "execa": "9.5.2",
55
55
  "kysely": "0.28.17",
56
56
  "vitest": "4.1.4",
57
- "@hot-updater/test-utils": "1.0.2"
57
+ "@hot-updater/test-utils": "1.0.4"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "tsdown",