@hot-updater/azure 1.2.0-rc.12 → 1.2.0-rc.13

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.
@@ -6104,6 +6104,10 @@ const nativeBuilds = defineTable({
6104
6104
  type: "string",
6105
6105
  maxLength: 255,
6106
6106
  required: false
6107
+ },
6108
+ store_build: {
6109
+ type: "boolean",
6110
+ required: false
6107
6111
  }
6108
6112
  }, {
6109
6113
  key: ["id"],
@@ -6116,6 +6120,11 @@ const nativeBuilds = defineTable({
6116
6120
  eq: ["scope_key", "build_key"],
6117
6121
  sort: ["id"]
6118
6122
  },
6123
+ byStoreBuild: {
6124
+ eq: ["scope_key", "store_build"],
6125
+ sort: [],
6126
+ unique: true
6127
+ },
6119
6128
  byChannel: {
6120
6129
  eq: ["channel_id"],
6121
6130
  sort: ["id"],
@@ -6311,10 +6320,7 @@ const LOG_TABLES = [
6311
6320
  const LEASE_TABLE = "aggregate_lease";
6312
6321
  const LEASE = { id: "compaction" };
6313
6322
  const LEASE_MS = 3e4;
6314
- const WINDOW_MS$1 = {
6315
- log: 6e4,
6316
- memory: 15e3
6317
- };
6323
+ const WINDOW_MS$1 = 6e4;
6318
6324
  const LOG_PAGE = 200;
6319
6325
  const GROUPS = {
6320
6326
  commit: 1,
@@ -6322,7 +6328,6 @@ const GROUPS = {
6322
6328
  flush: 256
6323
6329
  };
6324
6330
  const ATTEMPTS = 5;
6325
- const MAX_BUFFERED = 1e4;
6326
6331
  const log$1 = defineTable({
6327
6332
  id: {
6328
6333
  type: "string",
@@ -6423,8 +6428,8 @@ const rewrite = (change, key, current) => {
6423
6428
  previous: current
6424
6429
  };
6425
6430
  };
6426
- const compile = async (adapter, changes, shard) => {
6427
- const keys = changes.map(({ model, key }) => [...key.slice(0, -1), shard(model)]);
6431
+ const compile = async (adapter, changes) => {
6432
+ const keys = changes.map(({ key }) => [...key.slice(0, -1), 0]);
6428
6433
  const reads = /* @__PURE__ */ new Map();
6429
6434
  changes.forEach((change, at) => {
6430
6435
  if (rewrites(change)) reads.set(change.model, [...reads.get(change.model) ?? [], at]);
@@ -6470,7 +6475,7 @@ const encode$1 = async (changes) => {
6470
6475
  const zipped = await deflate(json);
6471
6476
  return zipped !== void 0 && zipped.length < json.length ? `z${zipped}` : json;
6472
6477
  };
6473
- const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "log", windowMs = WINDOW_MS$1[mode] }, now }) => {
6478
+ const createAggregateBatches = ({ engine, adapter, schema, batching: { windowMs = WINDOW_MS$1 }, now }) => {
6474
6479
  if (!schema.models.has(LEASE_TABLE)) throw new DatabaseTransactionError("An engine that batches aggregates needs aggregateBatchingModule in its schema.");
6475
6480
  const holder = crypto.randomUUID().replaceAll("-", "");
6476
6481
  const table = (name) => schema.models.get(name).table;
@@ -6497,61 +6502,6 @@ const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "l
6497
6502
  }
6498
6503
  return changes;
6499
6504
  };
6500
- const apply = async (changes, shard) => {
6501
- let rest = changes;
6502
- let ops = [];
6503
- let read = true;
6504
- for (let failures = 0; rest.length > 0 && failures < ATTEMPTS;) try {
6505
- if (read) ops = await compile(adapter, rest, shard);
6506
- let size = rest.length;
6507
- const chunk = () => ops.slice(0, size).filter(isOp);
6508
- while (size > 1 && !adapter.fits(chunk())) size = Math.ceil(size / 2);
6509
- let result = { ok: true };
6510
- if (chunk().length > 0) result = await adapter.write(chunk()).catch((error) => {
6511
- warn("flush")(error);
6512
- return { ok: true };
6513
- });
6514
- read = !result.ok && !("retry" in result);
6515
- if (result.ok) {
6516
- rest = rest.slice(size);
6517
- ops = ops.slice(size);
6518
- failures = 0;
6519
- } else {
6520
- failures += 1;
6521
- if (!read) await sleep$1(failures);
6522
- }
6523
- } catch (error) {
6524
- warn("flush")(error);
6525
- return rest;
6526
- }
6527
- return rest;
6528
- };
6529
- let buffer = /* @__PURE__ */ new Map();
6530
- let timer;
6531
- let flushing;
6532
- const shards = /* @__PURE__ */ new Map();
6533
- const processShard = (model) => {
6534
- if (!shards.has(model)) shards.set(model, Math.floor(Math.random() * shapeOf(model).shards));
6535
- return shards.get(model);
6536
- };
6537
- const schedule = () => {
6538
- if (timer !== void 0 || buffer.size === 0) return;
6539
- timer = setTimeout(() => {
6540
- timer = void 0;
6541
- flushBuffer();
6542
- }, windowMs);
6543
- timer.unref?.();
6544
- };
6545
- const flushBuffer = () => flushing ??= (async () => {
6546
- clearTimeout(timer);
6547
- timer = void 0;
6548
- const pending = [...buffer.values()];
6549
- buffer = /* @__PURE__ */ new Map();
6550
- mergeAll(buffer, await apply(pending, processShard));
6551
- })().catch(warn("flush")).finally(() => {
6552
- flushing = void 0;
6553
- schedule();
6554
- });
6555
6505
  let nextDue = 0;
6556
6506
  let compacting;
6557
6507
  const known = /* @__PURE__ */ new Map();
@@ -6673,7 +6623,7 @@ const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "l
6673
6623
  more
6674
6624
  };
6675
6625
  };
6676
- const rowsFor = async (changes) => (await compile(adapter, changes, () => 0)).filter(isOp);
6626
+ const rowsFor = async (changes) => (await compile(adapter, changes)).filter(isOp);
6677
6627
  const applyGroup = async (held, logs) => {
6678
6628
  for (let count = logs.length; count > 0; count = Math.floor(count / 2)) {
6679
6629
  const batch = logs.slice(0, count);
@@ -6753,19 +6703,6 @@ const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "l
6753
6703
  }))).then((pages) => pages.some((page) => page.rows.length > 0)).finally(() => {
6754
6704
  checking = void 0;
6755
6705
  });
6756
- const committed = async (changes) => {
6757
- if (mode === "log") {
6758
- if (now() >= nextDue) await compact(false, GROUPS.commit);
6759
- return;
6760
- }
6761
- mergeAll(buffer, changes.values());
6762
- schedule();
6763
- if (buffer.size >= MAX_BUFFERED) await flushBuffer();
6764
- };
6765
- const flush = async () => {
6766
- await flushBuffer();
6767
- if (mode === "log" && await hasPending()) await compact(true, GROUPS.flush);
6768
- };
6769
6706
  return {
6770
6707
  async transaction(fn) {
6771
6708
  let changes = /* @__PURE__ */ new Map();
@@ -6791,7 +6728,7 @@ const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "l
6791
6728
  aggregate
6792
6729
  });
6793
6730
  if (failure) throw failure.error;
6794
- if (mode === "log" && attempt.size > 0) {
6731
+ if (attempt.size > 0) {
6795
6732
  const id = `${now().toString(36).padStart(9, "0")}${holder.slice(0, 8)}${crypto.randomUUID().slice(0, 8)}`;
6796
6733
  const shard = Math.floor(Math.random() * LOG_TABLES.length);
6797
6734
  const merged = mergeAll(/* @__PURE__ */ new Map(), attempt.values());
@@ -6805,24 +6742,18 @@ const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "l
6805
6742
  open = false;
6806
6743
  }
6807
6744
  });
6808
- if (changes.size > 0) await committed(changes);
6745
+ if (changes.size > 0 && now() >= nextDue) await compact(false, GROUPS.commit);
6809
6746
  return result;
6810
6747
  },
6811
6748
  async beforeRead(name) {
6812
6749
  if (!isBatched(schema.models.get(name))) return;
6813
- if (buffer.size > 0 || flushing !== void 0) await flushBuffer();
6814
- if (mode === "log") {
6815
- if (await hasPending().catch((error) => {
6816
- warn("read")(error);
6817
- return false;
6818
- })) await compact(true, GROUPS.read);
6819
- }
6750
+ if (await hasPending().catch((error) => {
6751
+ warn("read")(error);
6752
+ return false;
6753
+ })) await compact(true, GROUPS.read);
6820
6754
  },
6821
- flush,
6822
- async dispose() {
6823
- clearTimeout(timer);
6824
- timer = void 0;
6825
- await flush();
6755
+ async flush() {
6756
+ if (await hasPending()) await compact(true, GROUPS.flush);
6826
6757
  }
6827
6758
  };
6828
6759
  };
@@ -7788,9 +7719,6 @@ const createDatabaseEngine = ({ batching, now = Date.now, meter, ...options }) =
7788
7719
  flush: async () => {
7789
7720
  await batches?.flush();
7790
7721
  },
7791
- dispose: async () => {
7792
- await batches?.dispose();
7793
- },
7794
7722
  database(module) {
7795
7723
  const name = (model) => module.namespace ? `${module.namespace}_${model}` : model;
7796
7724
  return {
@@ -7858,8 +7786,7 @@ const createEngine = (database, { plugins = [], now = Date.now } = {}) => {
7858
7786
  if (module === void 0) throw new HotUpdaterConfigError(`Plugin "${plugin.id}" is not one of this engine's plugins.`);
7859
7787
  return engine.database(module);
7860
7788
  },
7861
- flush: () => engine.flush(),
7862
- dispose: () => engine.dispose()
7789
+ flush: () => engine.flush()
7863
7790
  };
7864
7791
  };
7865
7792
  //#endregion
@@ -8118,7 +8045,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
8118
8045
  };
8119
8046
  //#endregion
8120
8047
  //#region ../../packages/server/dist/version.mjs
8121
- const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.12";
8048
+ const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.13";
8122
8049
  //#endregion
8123
8050
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
8124
8051
  const createVersionRouteHandlers = (plugins = []) => {
@@ -9226,8 +9153,12 @@ const pick = (row) => ({
9226
9153
  build_number: row.build_number ?? null,
9227
9154
  submitted_to: row.submitted_to ?? null,
9228
9155
  submitted_at_ms: row.submitted_at_ms ?? null,
9229
- submitted_version: row.submitted_version ?? null
9156
+ submitted_version: row.submitted_version ?? null,
9157
+ store_build: row.store_build === true ? true : null
9230
9158
  });
9159
+ const SANDBOX_CHANNEL_PREFIX = "sandbox-";
9160
+ const OLDER_BUILDS_PAGE = 20;
9161
+ const MARK_ATTEMPTS = 8;
9231
9162
  const SUBMISSION_DESTINATIONS = {
9232
9163
  android: "play-internal",
9233
9164
  ios: "testflight"
@@ -9293,7 +9224,8 @@ const createNativeBuildApi = (db, { now = Date.now } = {}) => ({
9293
9224
  build_number: registration.buildNumber ?? null,
9294
9225
  submitted_to: null,
9295
9226
  submitted_at_ms: null,
9296
- submitted_version: null
9227
+ submitted_version: null,
9228
+ store_build: null
9297
9229
  };
9298
9230
  for (let attempt = 1;; attempt += 1) {
9299
9231
  await insertChannel(db, {
@@ -9331,21 +9263,45 @@ const createNativeBuildApi = (db, { now = Date.now } = {}) => ({
9331
9263
  const scopeKey = nativeBuildScopeKey(scope);
9332
9264
  const buildKey = options?.buildKey;
9333
9265
  if (buildKey !== void 0 && !BUILD_KEY.test(buildKey)) throw new NativeBuildInputError("Invalid native build key: it must be nbk1: and a SHA-256.");
9334
- const { rows } = buildKey === void 0 ? await db.findMany("native_builds", {
9335
- index: "byScope",
9336
- where: { scope_key: scopeKey },
9337
- order: "desc",
9338
- limit: 1
9339
- }) : await db.findMany("native_builds", {
9340
- index: "byBuildKey",
9341
- where: {
9266
+ if (options?.storeBuild) {
9267
+ const marked = await db.findOne("native_builds", {
9342
9268
  scope_key: scopeKey,
9343
- build_key: buildKey
9344
- },
9345
- order: "desc",
9346
- limit: 1
9347
- });
9348
- return rows[0] === void 0 ? null : pick(rows[0]);
9269
+ store_build: true
9270
+ });
9271
+ return marked === null ? null : pick(marked);
9272
+ }
9273
+ const read = (limit, before) => {
9274
+ const page = {
9275
+ order: "desc",
9276
+ limit,
9277
+ ...before === void 0 ? {} : { range: { lt: before } }
9278
+ };
9279
+ return buildKey === void 0 ? db.findMany("native_builds", {
9280
+ index: "byScope",
9281
+ where: { scope_key: scopeKey },
9282
+ ...page
9283
+ }) : db.findMany("native_builds", {
9284
+ index: "byBuildKey",
9285
+ where: {
9286
+ scope_key: scopeKey,
9287
+ build_key: buildKey
9288
+ },
9289
+ ...page
9290
+ });
9291
+ };
9292
+ const { rows } = await read(1);
9293
+ const newest = rows[0];
9294
+ if (newest === void 0) return null;
9295
+ const bundleId = options?.builtInOlderThan;
9296
+ if (bundleId === void 0 || newest.min_bundle_id < bundleId) return pick(newest);
9297
+ for (let before = newest.id;;) {
9298
+ const { rows: older } = await read(OLDER_BUILDS_PAGE, before);
9299
+ const usable = older.find(({ min_bundle_id }) => min_bundle_id < bundleId);
9300
+ if (usable !== void 0) return pick(usable);
9301
+ const last = older.at(-1);
9302
+ if (older.length < OLDER_BUILDS_PAGE || last === void 0) return null;
9303
+ before = last.id;
9304
+ }
9349
9305
  },
9350
9306
  async recordNativeBuildSubmission(id, { destination, version }) {
9351
9307
  if (version.trim().length === 0 || version.length > SUBMITTED_VERSION_MAX_LENGTH) throw new NativeBuildInputError(`Invalid native build submission: version must be the store's label of the build, up to ${SUBMITTED_VERSION_MAX_LENGTH} characters.`);
@@ -9366,6 +9322,49 @@ const createNativeBuildApi = (db, { now = Date.now } = {}) => ({
9366
9322
  };
9367
9323
  });
9368
9324
  },
9325
+ async markNativeBuild(id, { storeBuild }) {
9326
+ for (let attempt = 1;; attempt += 1) try {
9327
+ return await db.transaction(async (tx) => {
9328
+ const row = await tx.findOne("native_builds", { id });
9329
+ if (row === null) return null;
9330
+ const build = pick(row);
9331
+ if (!storeBuild) {
9332
+ if (build.store_build !== null) tx.update("native_builds", row, { store_build: null });
9333
+ return {
9334
+ nativeBuild: {
9335
+ ...build,
9336
+ store_build: null
9337
+ },
9338
+ previous: null
9339
+ };
9340
+ }
9341
+ if (decodeChannelKey(build.channel_id.slice(8)).startsWith(SANDBOX_CHANNEL_PREFIX)) throw new NativeBuildInputError("A build on a sandbox channel can't be the store build.");
9342
+ if (build.store_build !== null) return {
9343
+ nativeBuild: build,
9344
+ previous: null
9345
+ };
9346
+ const holder = await tx.findOne("native_builds", {
9347
+ scope_key: build.scope_key,
9348
+ store_build: true
9349
+ });
9350
+ if (holder !== null) tx.update("native_builds", holder, { store_build: null });
9351
+ tx.update("native_builds", row, { store_build: true });
9352
+ return {
9353
+ nativeBuild: {
9354
+ ...build,
9355
+ store_build: true
9356
+ },
9357
+ previous: holder === null ? null : {
9358
+ ...pick(holder),
9359
+ store_build: null
9360
+ }
9361
+ };
9362
+ });
9363
+ } catch (error) {
9364
+ if (attempt < MARK_ATTEMPTS && error instanceof DatabaseConstraintError && error.reason === "unique") continue;
9365
+ throw error;
9366
+ }
9367
+ },
9369
9368
  async deleteNativeBuild(id) {
9370
9369
  return db.transaction(async (tx) => {
9371
9370
  const row = await tx.findOne("native_builds", { id });
@@ -10392,7 +10391,13 @@ const createAdminRouteHandlers = () => ({
10392
10391
  const url = new URL(request.url);
10393
10392
  const scope = nativeBuildScopeOf(url);
10394
10393
  const buildKey = url.searchParams.get("buildKey");
10395
- return json$2({ data: await refusingInvalid(() => buildKey === null ? core.findNativeBuild(scope) : core.findNativeBuild(scope, { buildKey })) });
10394
+ const builtInOlderThan = url.searchParams.get("builtInOlderThan");
10395
+ const options = {
10396
+ ...buildKey === null ? {} : { buildKey },
10397
+ ...url.searchParams.get("storeBuild") === "true" ? { storeBuild: true } : {},
10398
+ ...builtInOlderThan === null ? {} : { builtInOlderThan }
10399
+ };
10400
+ return json$2({ data: await refusingInvalid(() => Object.keys(options).length === 0 ? core.findNativeBuild(scope) : core.findNativeBuild(scope, options)) });
10396
10401
  }),
10397
10402
  getNativeBuild: onCore(async (core, _request, params) => json$2({ data: await core.getNativeBuild(requireRouteParam(params, "id")) })),
10398
10403
  listNativeBuilds: onCore(async (core, request) => {
@@ -10413,6 +10418,12 @@ const createAdminRouteHandlers = () => ({
10413
10418
  };
10414
10419
  return json$2({ data: await refusingInvalid(() => core.recordNativeBuildSubmission(requireRouteParam(params, "id"), submission)) });
10415
10420
  }),
10421
+ markNativeBuild: onCore(async (core, request, params) => {
10422
+ const input = await body$1(request);
10423
+ if (!isRecord$5(input) || typeof input.storeBuild !== "boolean") throw new HandlerBadRequestError("A native build's store build mark needs storeBuild, true or false.");
10424
+ const mark = { storeBuild: input.storeBuild };
10425
+ return json$2({ data: await refusingInvalid(() => core.markNativeBuild(requireRouteParam(params, "id"), mark)) });
10426
+ }),
10416
10427
  deleteNativeBuild: onCore(async (core, _request, params) => json$2({ data: await core.deleteNativeBuild(requireRouteParam(params, "id")) })),
10417
10428
  deleteBundles: onCore(async (core, request) => {
10418
10429
  const { ids } = await body$1(request);
@@ -10557,6 +10568,11 @@ const ADMIN_ROUTES = [
10557
10568
  path: "/native-builds/:id/submission",
10558
10569
  handler: "recordNativeBuildSubmission"
10559
10570
  },
10571
+ {
10572
+ method: "POST",
10573
+ path: "/native-builds/:id/store-build",
10574
+ handler: "markNativeBuild"
10575
+ },
10560
10576
  {
10561
10577
  method: "DELETE",
10562
10578
  path: "/native-builds/:id",
package/dist/index.cjs CHANGED
@@ -811,23 +811,23 @@ const NO_CREDENTIAL_ERRORS = /* @__PURE__ */ new Set([
811
811
  "AuthenticationRequiredError",
812
812
  "CredentialUnavailableError"
813
813
  ]);
814
- const SIGN_IN_HINT = [
815
- "In CI, sign in with azure/login.",
816
- "On your machine, run az login.",
817
- "Or set AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET."
818
- ];
819
- /** The widest line a message has: the CLI prints no wider. */
820
- const LINE_WIDTH = 76;
821
- /** `text`, such as Azure's own message, in lines that fit, broken at spaces. */
822
- const wrap = (text) => {
823
- const lines = [];
824
- let line = "";
825
- for (const word of text.split(/\s+/u).filter(Boolean)) if (line && line.length + 1 + word.length > LINE_WIDTH) {
826
- lines.push(line);
827
- line = word;
828
- } else line = line ? `${line} ${word}` : word;
829
- if (line) lines.push(line);
830
- return lines;
814
+ /** The variables `DefaultAzureCredential` signs in with first, if complete. */
815
+ const environmentCredentialVariables = () => {
816
+ const { env } = process;
817
+ if (!env["AZURE_TENANT_ID"] || !env["AZURE_CLIENT_ID"]) return void 0;
818
+ if (env["AZURE_CLIENT_SECRET"]) return "AZURE_CLIENT_SECRET";
819
+ if (env["AZURE_CLIENT_CERTIFICATE_PATH"]) return "AZURE_CLIENT_CERTIFICATE_PATH";
820
+ if (env["AZURE_USERNAME"] && env["AZURE_PASSWORD"]) return "AZURE_PASSWORD";
821
+ };
822
+ /**
823
+ * The one step that can fix the identity in use: `az login` changes nothing
824
+ * while the environment's identity comes first.
825
+ */
826
+ const signInHint = (credential) => {
827
+ if (credential !== void 0) return [`Check the credential passed to ${NAME}().`];
828
+ const variable = environmentCredentialVariables();
829
+ if (variable) return [`Fix or unset ${variable}, AZURE_TENANT_ID, and AZURE_CLIENT_ID.`];
830
+ return process.env["CI"] ? ["Sign in with azure/login."] : ["Sign in:", "az login"];
831
831
  };
832
832
  /** An error of one line each, for a terminal. */
833
833
  const failure = (lines, cause) => new Error(lines.join("\n"), cause === void 0 ? void 0 : { cause });
@@ -940,56 +940,21 @@ const keyVault = ({ credential, prefix = "hot-updater-", vaultName, vaultUrl })
940
940
  const name = errorName(error);
941
941
  const status = statusOf(error);
942
942
  const code = innerCode(error) ?? error?.code;
943
- const detail = wrap(`${code || name || "Error"}${status ? ` (${status})` : ""}: ${errorMessage(error)}.`);
944
- const secret = `Secret: ${id}`;
945
- if (NO_CREDENTIAL_ERRORS.has(name)) return failure([
946
- `${NAME} found no Azure identity to ${verb} a secret.`,
947
- secret,
948
- ...wrap(`${errorMessage(error)}.`),
949
- ...SIGN_IN_HINT
950
- ], error);
951
- if (status === 401) return failure([
952
- `Azure Key Vault refused the sign-in to ${verb} a secret.`,
953
- secret,
954
- ...detail,
955
- "Sign in again.",
956
- ...SIGN_IN_HINT
957
- ], error);
958
- if (code === "SecretDisabled") return failure([
959
- `Azure Key Vault can't ${verb} a secret.`,
960
- secret,
961
- "Its current version is disabled. Enable it, or store a new value."
962
- ], error);
963
- if (status === 403) return failure([
964
- `Azure Key Vault refused to ${verb} a secret.`,
965
- secret,
966
- ...detail,
967
- `The Azure identity needs the ${role} role on the vault.`,
968
- `With access policies, it needs the secret permissions ${role === READ_ROLE ? "get" : "get and set"}.`,
969
- "A firewall may also refuse this network."
970
- ], error);
971
- if (code === "ObjectIsBeingDeleted" || /being deleted/iu.test(errorMessage(error))) return failure([
972
- `Azure Key Vault can't ${verb} a secret: it is being deleted.`,
973
- secret,
974
- "Try again in a minute."
975
- ], error);
943
+ if (NO_CREDENTIAL_ERRORS.has(name)) return failure([`${NAME} found no Azure identity.`, ...signInHint(credential)], error);
944
+ if (status === 401 || name === "AuthenticationError") return failure(["Azure rejected the sign-in.", ...signInHint(credential)], error);
945
+ if (/\bENOTFOUND\b/u.test(`${code} ${errorMessage(error)}`) && errorMessage(error).includes(vault().hostname)) return failure([`${NAME} can't find ${vault().hostname}.`, `Check ${vaultUrl === void 0 ? "vaultName" : "vaultUrl"}.`], error);
946
+ if (code === "SecretDisabled") return failure([`${id}'s current version is disabled. Enable it, or store a new value.`], error);
947
+ if (status === 403) return failure([`Azure Key Vault refused to ${verb} ${id}.`, code === "ForbiddenByFirewall" ? "Its firewall refuses this network." : code === "ForbiddenByPolicy" ? `The identity needs the secret permissions ${role === READ_ROLE ? "get" : "get and set"}.` : `The identity needs the ${role} role on the vault.`], error);
948
+ if (code === "ObjectIsBeingDeleted" || /being deleted/iu.test(errorMessage(error))) return failure([`${id} is being deleted. Try again in a minute.`], error);
976
949
  if (status === 409 || code === "ObjectIsDeletedButRecoverable") {
977
950
  const vaultName = vaultLabel();
978
951
  return failure([
979
- `Azure Key Vault can't ${verb} a secret: it was deleted.`,
980
- secret,
981
- "Recover it, or purge it:",
982
- `az keyvault secret recover --vault-name ${vaultName} \\`,
983
- ` --name ${id}`,
984
- `az keyvault secret purge --vault-name ${vaultName} \\`,
985
- ` --name ${id}`
952
+ `${id} was deleted. Recover it, or purge it:`,
953
+ `az keyvault secret recover --vault-name ${vaultName} --name ${id}`,
954
+ `az keyvault secret purge --vault-name ${vaultName} --name ${id}`
986
955
  ], error);
987
956
  }
988
- return failure([
989
- `Azure Key Vault failed to ${verb} a secret.`,
990
- secret,
991
- ...detail
992
- ], error);
957
+ return failure([`Azure Key Vault failed to ${verb} ${id}.`, `${code || name || "Error"}${status ? ` (${status})` : ""}: ${errorMessage(error)}.`], error);
993
958
  };
994
959
  const read = async (client, id) => {
995
960
  let value;
package/dist/index.d.cts CHANGED
@@ -58,9 +58,8 @@ type CosmosDatabaseConfig = Omit<CosmosClientOptions, "consistencyLevel" | "conn
58
58
  readonly containerId?: string;
59
59
  /**
60
60
  * Cosmos DB bills each item a write touches, so Insights' aggregates are
61
- * batched: through log rows a compaction merges (the default), or
62
- * `{ mode: "memory" }` on a long-lived server. `false` commits them with
63
- * each event.
61
+ * batched: through log rows a compaction merges (the default). `false` commits
62
+ * them with each event.
64
63
  */
65
64
  readonly aggregateBatching?: EngineDatabase["aggregateBatching"] | false;
66
65
  };
package/dist/index.d.mts CHANGED
@@ -58,9 +58,8 @@ type CosmosDatabaseConfig = Omit<CosmosClientOptions, "consistencyLevel" | "conn
58
58
  readonly containerId?: string;
59
59
  /**
60
60
  * Cosmos DB bills each item a write touches, so Insights' aggregates are
61
- * batched: through log rows a compaction merges (the default), or
62
- * `{ mode: "memory" }` on a long-lived server. `false` commits them with
63
- * each event.
61
+ * batched: through log rows a compaction merges (the default). `false` commits
62
+ * them with each event.
64
63
  */
65
64
  readonly aggregateBatching?: EngineDatabase["aggregateBatching"] | false;
66
65
  };
package/dist/index.mjs CHANGED
@@ -810,23 +810,23 @@ const NO_CREDENTIAL_ERRORS = /* @__PURE__ */ new Set([
810
810
  "AuthenticationRequiredError",
811
811
  "CredentialUnavailableError"
812
812
  ]);
813
- const SIGN_IN_HINT = [
814
- "In CI, sign in with azure/login.",
815
- "On your machine, run az login.",
816
- "Or set AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET."
817
- ];
818
- /** The widest line a message has: the CLI prints no wider. */
819
- const LINE_WIDTH = 76;
820
- /** `text`, such as Azure's own message, in lines that fit, broken at spaces. */
821
- const wrap = (text) => {
822
- const lines = [];
823
- let line = "";
824
- for (const word of text.split(/\s+/u).filter(Boolean)) if (line && line.length + 1 + word.length > LINE_WIDTH) {
825
- lines.push(line);
826
- line = word;
827
- } else line = line ? `${line} ${word}` : word;
828
- if (line) lines.push(line);
829
- return lines;
813
+ /** The variables `DefaultAzureCredential` signs in with first, if complete. */
814
+ const environmentCredentialVariables = () => {
815
+ const { env } = process;
816
+ if (!env["AZURE_TENANT_ID"] || !env["AZURE_CLIENT_ID"]) return void 0;
817
+ if (env["AZURE_CLIENT_SECRET"]) return "AZURE_CLIENT_SECRET";
818
+ if (env["AZURE_CLIENT_CERTIFICATE_PATH"]) return "AZURE_CLIENT_CERTIFICATE_PATH";
819
+ if (env["AZURE_USERNAME"] && env["AZURE_PASSWORD"]) return "AZURE_PASSWORD";
820
+ };
821
+ /**
822
+ * The one step that can fix the identity in use: `az login` changes nothing
823
+ * while the environment's identity comes first.
824
+ */
825
+ const signInHint = (credential) => {
826
+ if (credential !== void 0) return [`Check the credential passed to ${NAME}().`];
827
+ const variable = environmentCredentialVariables();
828
+ if (variable) return [`Fix or unset ${variable}, AZURE_TENANT_ID, and AZURE_CLIENT_ID.`];
829
+ return process.env["CI"] ? ["Sign in with azure/login."] : ["Sign in:", "az login"];
830
830
  };
831
831
  /** An error of one line each, for a terminal. */
832
832
  const failure = (lines, cause) => new Error(lines.join("\n"), cause === void 0 ? void 0 : { cause });
@@ -939,56 +939,21 @@ const keyVault = ({ credential, prefix = "hot-updater-", vaultName, vaultUrl })
939
939
  const name = errorName(error);
940
940
  const status = statusOf(error);
941
941
  const code = innerCode(error) ?? error?.code;
942
- const detail = wrap(`${code || name || "Error"}${status ? ` (${status})` : ""}: ${errorMessage(error)}.`);
943
- const secret = `Secret: ${id}`;
944
- if (NO_CREDENTIAL_ERRORS.has(name)) return failure([
945
- `${NAME} found no Azure identity to ${verb} a secret.`,
946
- secret,
947
- ...wrap(`${errorMessage(error)}.`),
948
- ...SIGN_IN_HINT
949
- ], error);
950
- if (status === 401) return failure([
951
- `Azure Key Vault refused the sign-in to ${verb} a secret.`,
952
- secret,
953
- ...detail,
954
- "Sign in again.",
955
- ...SIGN_IN_HINT
956
- ], error);
957
- if (code === "SecretDisabled") return failure([
958
- `Azure Key Vault can't ${verb} a secret.`,
959
- secret,
960
- "Its current version is disabled. Enable it, or store a new value."
961
- ], error);
962
- if (status === 403) return failure([
963
- `Azure Key Vault refused to ${verb} a secret.`,
964
- secret,
965
- ...detail,
966
- `The Azure identity needs the ${role} role on the vault.`,
967
- `With access policies, it needs the secret permissions ${role === READ_ROLE ? "get" : "get and set"}.`,
968
- "A firewall may also refuse this network."
969
- ], error);
970
- if (code === "ObjectIsBeingDeleted" || /being deleted/iu.test(errorMessage(error))) return failure([
971
- `Azure Key Vault can't ${verb} a secret: it is being deleted.`,
972
- secret,
973
- "Try again in a minute."
974
- ], error);
942
+ if (NO_CREDENTIAL_ERRORS.has(name)) return failure([`${NAME} found no Azure identity.`, ...signInHint(credential)], error);
943
+ if (status === 401 || name === "AuthenticationError") return failure(["Azure rejected the sign-in.", ...signInHint(credential)], error);
944
+ if (/\bENOTFOUND\b/u.test(`${code} ${errorMessage(error)}`) && errorMessage(error).includes(vault().hostname)) return failure([`${NAME} can't find ${vault().hostname}.`, `Check ${vaultUrl === void 0 ? "vaultName" : "vaultUrl"}.`], error);
945
+ if (code === "SecretDisabled") return failure([`${id}'s current version is disabled. Enable it, or store a new value.`], error);
946
+ if (status === 403) return failure([`Azure Key Vault refused to ${verb} ${id}.`, code === "ForbiddenByFirewall" ? "Its firewall refuses this network." : code === "ForbiddenByPolicy" ? `The identity needs the secret permissions ${role === READ_ROLE ? "get" : "get and set"}.` : `The identity needs the ${role} role on the vault.`], error);
947
+ if (code === "ObjectIsBeingDeleted" || /being deleted/iu.test(errorMessage(error))) return failure([`${id} is being deleted. Try again in a minute.`], error);
975
948
  if (status === 409 || code === "ObjectIsDeletedButRecoverable") {
976
949
  const vaultName = vaultLabel();
977
950
  return failure([
978
- `Azure Key Vault can't ${verb} a secret: it was deleted.`,
979
- secret,
980
- "Recover it, or purge it:",
981
- `az keyvault secret recover --vault-name ${vaultName} \\`,
982
- ` --name ${id}`,
983
- `az keyvault secret purge --vault-name ${vaultName} \\`,
984
- ` --name ${id}`
951
+ `${id} was deleted. Recover it, or purge it:`,
952
+ `az keyvault secret recover --vault-name ${vaultName} --name ${id}`,
953
+ `az keyvault secret purge --vault-name ${vaultName} --name ${id}`
985
954
  ], error);
986
955
  }
987
- return failure([
988
- `Azure Key Vault failed to ${verb} a secret.`,
989
- secret,
990
- ...detail
991
- ], error);
956
+ return failure([`Azure Key Vault failed to ${verb} ${id}.`, `${code || name || "Error"}${status ? ` (${status})` : ""}: ${errorMessage(error)}.`], error);
992
957
  };
993
958
  const read = async (client, id) => {
994
959
  let value;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/azure",
3
3
  "type": "module",
4
- "version": "1.2.0-rc.12",
4
+ "version": "1.2.0-rc.13",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "engines": {
7
7
  "node": "^22.23.4 || ^24.22.1 || >=26.12.1"
@@ -44,10 +44,10 @@
44
44
  "@azure/identity": "4.14.0",
45
45
  "@azure/keyvault-secrets": "4.11.2",
46
46
  "@azure/storage-blob": "12.34.0",
47
- "@hot-updater/cli-tools": "1.2.0-rc.12",
48
- "@hot-updater/plugin-core": "1.2.0-rc.12",
49
- "@hot-updater/protocol": "1.2.0-rc.12",
50
- "@hot-updater/server": "1.2.0-rc.12"
47
+ "@hot-updater/cli-tools": "1.2.0-rc.13",
48
+ "@hot-updater/plugin-core": "1.2.0-rc.13",
49
+ "@hot-updater/protocol": "1.2.0-rc.13",
50
+ "@hot-updater/server": "1.2.0-rc.13"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@azure/functions": "4.16.5",
@@ -55,7 +55,7 @@
55
55
  "execa": "9.5.2",
56
56
  "jszip": "^3.10.1",
57
57
  "vitest": "4.1.4",
58
- "@hot-updater/test-utils": "1.2.0-rc.12"
58
+ "@hot-updater/test-utils": "1.2.0-rc.13"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "tsdown",