@hot-updater/cloudflare 1.2.0-rc.12 → 1.2.0-rc.14

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/cloudflare",
3
3
  "type": "module",
4
- "version": "1.2.0-rc.12",
4
+ "version": "1.2.0-rc.14",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
@@ -52,11 +52,11 @@
52
52
  "cloudflare": "4.2.0",
53
53
  "hono": "4.12.34",
54
54
  "uuidv7": "^1.0.2",
55
- "@hot-updater/protocol": "1.2.0-rc.12",
56
- "@hot-updater/cli-tools": "1.2.0-rc.12",
57
- "@hot-updater/plugin-core": "1.2.0-rc.12",
58
- "@hot-updater/storage-s3": "1.2.0-rc.12",
59
- "@hot-updater/server": "1.2.0-rc.12"
55
+ "@hot-updater/cli-tools": "1.2.0-rc.14",
56
+ "@hot-updater/plugin-core": "1.2.0-rc.14",
57
+ "@hot-updater/server": "1.2.0-rc.14",
58
+ "@hot-updater/storage-s3": "1.2.0-rc.14",
59
+ "@hot-updater/protocol": "1.2.0-rc.14"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@aws-sdk/client-s3": "3.1066.0",
@@ -75,7 +75,7 @@
75
75
  "vitest": "4.1.4",
76
76
  "wrangler": "^4.69.0",
77
77
  "xdg-app-paths": "^8.3.0",
78
- "@hot-updater/test-utils": "1.2.0-rc.12"
78
+ "@hot-updater/test-utils": "1.2.0-rc.14"
79
79
  },
80
80
  "scripts": {
81
81
  "build": "tsdown && pnpm build:worker",
package/sql/bundles.sql CHANGED
@@ -36,12 +36,14 @@ CREATE UNIQUE INDEX IF NOT EXISTS "channels_name" ON "channels" ("name");
36
36
 
37
37
  CREATE TABLE IF NOT EXISTS "bundle_totals" ("platform_key" TEXT NOT NULL, "_shard" INTEGER NOT NULL, "bundles" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("platform_key", "_shard"));
38
38
 
39
- CREATE TABLE IF NOT EXISTS "native_builds" ("id" TEXT NOT NULL, "scope_key" TEXT NOT NULL, "platform" TEXT NOT NULL, "channel_id" TEXT NOT NULL, "fingerprint_hash" TEXT, "app_version" TEXT, "min_bundle_id" TEXT NOT NULL, "artifact_kind" TEXT NOT NULL, "artifact_storage_uri" TEXT NOT NULL, "byte_size" INTEGER NOT NULL, "git_commit_hash" TEXT, "created_at_ms" INTEGER NOT NULL, "build_key" TEXT, "build_number" TEXT, "submitted_to" TEXT, "submitted_at_ms" INTEGER, "submitted_version" TEXT, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
39
+ CREATE TABLE IF NOT EXISTS "native_builds" ("id" TEXT NOT NULL, "scope_key" TEXT NOT NULL, "platform" TEXT NOT NULL, "channel_id" TEXT NOT NULL, "fingerprint_hash" TEXT, "app_version" TEXT, "min_bundle_id" TEXT NOT NULL, "artifact_kind" TEXT NOT NULL, "artifact_storage_uri" TEXT NOT NULL, "byte_size" INTEGER NOT NULL, "git_commit_hash" TEXT, "created_at_ms" INTEGER NOT NULL, "build_key" TEXT, "build_number" TEXT, "submitted_to" TEXT, "submitted_at_ms" INTEGER, "submitted_version" TEXT, "store_build" INTEGER, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
40
40
 
41
41
  CREATE INDEX IF NOT EXISTS "native_builds_byScope" ON "native_builds" ("scope_key", "id");
42
42
 
43
43
  CREATE INDEX IF NOT EXISTS "native_builds_byBuildKey" ON "native_builds" ("scope_key", "build_key", "id");
44
44
 
45
+ CREATE UNIQUE INDEX IF NOT EXISTS "native_builds_byStoreBuild" ON "native_builds" ("scope_key", "store_build");
46
+
45
47
  CREATE INDEX IF NOT EXISTS "native_builds_byChannel" ON "native_builds" ("channel_id", "id");
46
48
 
47
49
  CREATE UNIQUE INDEX IF NOT EXISTS "native_builds_min_bundle_id" ON "native_builds" ("min_bundle_id");
@@ -1 +1 @@
1
- This folder contains the built output assets for the worker "hot-updater" generated at 2026-10-30T04:39:59.029Z.
1
+ This folder contains the built output assets for the worker "hot-updater" generated at 2026-11-03T17:15:32.516Z.
@@ -2600,6 +2600,10 @@ var nativeBuilds = defineTable({
2600
2600
  type: "string",
2601
2601
  maxLength: 255,
2602
2602
  required: false
2603
+ },
2604
+ store_build: {
2605
+ type: "boolean",
2606
+ required: false
2603
2607
  }
2604
2608
  }, {
2605
2609
  key: ["id"],
@@ -2612,6 +2616,11 @@ var nativeBuilds = defineTable({
2612
2616
  eq: ["scope_key", "build_key"],
2613
2617
  sort: ["id"]
2614
2618
  },
2619
+ byStoreBuild: {
2620
+ eq: ["scope_key", "store_build"],
2621
+ sort: [],
2622
+ unique: true
2623
+ },
2615
2624
  byChannel: {
2616
2625
  eq: ["channel_id"],
2617
2626
  sort: ["id"],
@@ -2813,10 +2822,7 @@ var LOG_TABLES = [
2813
2822
  var LEASE_TABLE = "aggregate_lease";
2814
2823
  var LEASE = { id: "compaction" };
2815
2824
  var LEASE_MS = 3e4;
2816
- var WINDOW_MS = {
2817
- log: 6e4,
2818
- memory: 15e3
2819
- };
2825
+ var WINDOW_MS = 6e4;
2820
2826
  var LOG_PAGE = 200;
2821
2827
  var GROUPS = {
2822
2828
  commit: 1,
@@ -2824,7 +2830,6 @@ var GROUPS = {
2824
2830
  flush: 256
2825
2831
  };
2826
2832
  var ATTEMPTS = 5;
2827
- var MAX_BUFFERED = 1e4;
2828
2833
  var log = defineTable({
2829
2834
  id: {
2830
2835
  type: "string",
@@ -2926,8 +2931,8 @@ var rewrite2 = /* @__PURE__ */ __name((change, key, current) => {
2926
2931
  previous: current
2927
2932
  };
2928
2933
  }, "rewrite");
2929
- var compile = /* @__PURE__ */ __name(async (adapter, changes, shard) => {
2930
- const keys = changes.map(({ model, key }) => [...key.slice(0, -1), shard(model)]);
2934
+ var compile = /* @__PURE__ */ __name(async (adapter, changes) => {
2935
+ const keys = changes.map(({ key }) => [...key.slice(0, -1), 0]);
2931
2936
  const reads = /* @__PURE__ */ new Map();
2932
2937
  changes.forEach((change, at) => {
2933
2938
  if (rewrites(change)) reads.set(change.model, [...reads.get(change.model) ?? [], at]);
@@ -2973,7 +2978,7 @@ var encode2 = /* @__PURE__ */ __name(async (changes) => {
2973
2978
  const zipped = await deflate(json4);
2974
2979
  return zipped !== void 0 && zipped.length < json4.length ? `z${zipped}` : json4;
2975
2980
  }, "encode");
2976
- var createAggregateBatches = /* @__PURE__ */ __name(({ engine, adapter, schema, batching: { mode = "log", windowMs = WINDOW_MS[mode] }, now }) => {
2981
+ var createAggregateBatches = /* @__PURE__ */ __name(({ engine, adapter, schema, batching: { windowMs = WINDOW_MS }, now }) => {
2977
2982
  if (!schema.models.has(LEASE_TABLE)) throw new DatabaseTransactionError("An engine that batches aggregates needs aggregateBatchingModule in its schema.");
2978
2983
  const holder = crypto.randomUUID().replaceAll("-", "");
2979
2984
  const table = /* @__PURE__ */ __name((name) => schema.models.get(name).table, "table");
@@ -3000,61 +3005,6 @@ var createAggregateBatches = /* @__PURE__ */ __name(({ engine, adapter, schema,
3000
3005
  }
3001
3006
  return changes;
3002
3007
  }, "decode");
3003
- const apply = /* @__PURE__ */ __name(async (changes, shard) => {
3004
- let rest = changes;
3005
- let ops = [];
3006
- let read = true;
3007
- for (let failures = 0; rest.length > 0 && failures < ATTEMPTS; ) try {
3008
- if (read) ops = await compile(adapter, rest, shard);
3009
- let size = rest.length;
3010
- const chunk = /* @__PURE__ */ __name(() => ops.slice(0, size).filter(isOp), "chunk");
3011
- while (size > 1 && !adapter.fits(chunk())) size = Math.ceil(size / 2);
3012
- let result = { ok: true };
3013
- if (chunk().length > 0) result = await adapter.write(chunk()).catch((error) => {
3014
- warn("flush")(error);
3015
- return { ok: true };
3016
- });
3017
- read = !result.ok && !("retry" in result);
3018
- if (result.ok) {
3019
- rest = rest.slice(size);
3020
- ops = ops.slice(size);
3021
- failures = 0;
3022
- } else {
3023
- failures += 1;
3024
- if (!read) await sleep(failures);
3025
- }
3026
- } catch (error) {
3027
- warn("flush")(error);
3028
- return rest;
3029
- }
3030
- return rest;
3031
- }, "apply");
3032
- let buffer = /* @__PURE__ */ new Map();
3033
- let timer;
3034
- let flushing;
3035
- const shards = /* @__PURE__ */ new Map();
3036
- const processShard = /* @__PURE__ */ __name((model) => {
3037
- if (!shards.has(model)) shards.set(model, Math.floor(Math.random() * shapeOf(model).shards));
3038
- return shards.get(model);
3039
- }, "processShard");
3040
- const schedule = /* @__PURE__ */ __name(() => {
3041
- if (timer !== void 0 || buffer.size === 0) return;
3042
- timer = setTimeout(() => {
3043
- timer = void 0;
3044
- flushBuffer();
3045
- }, windowMs);
3046
- timer.unref?.();
3047
- }, "schedule");
3048
- const flushBuffer = /* @__PURE__ */ __name(() => flushing ??= (async () => {
3049
- clearTimeout(timer);
3050
- timer = void 0;
3051
- const pending = [...buffer.values()];
3052
- buffer = /* @__PURE__ */ new Map();
3053
- mergeAll(buffer, await apply(pending, processShard));
3054
- })().catch(warn("flush")).finally(() => {
3055
- flushing = void 0;
3056
- schedule();
3057
- }), "flushBuffer");
3058
3008
  let nextDue = 0;
3059
3009
  let compacting;
3060
3010
  const known = /* @__PURE__ */ new Map();
@@ -3176,7 +3126,7 @@ var createAggregateBatches = /* @__PURE__ */ __name(({ engine, adapter, schema,
3176
3126
  more
3177
3127
  };
3178
3128
  }, "readLogs");
3179
- const rowsFor = /* @__PURE__ */ __name(async (changes) => (await compile(adapter, changes, () => 0)).filter(isOp), "rowsFor");
3129
+ const rowsFor = /* @__PURE__ */ __name(async (changes) => (await compile(adapter, changes)).filter(isOp), "rowsFor");
3180
3130
  const applyGroup = /* @__PURE__ */ __name(async (held, logs) => {
3181
3131
  for (let count = logs.length; count > 0; count = Math.floor(count / 2)) {
3182
3132
  const batch = logs.slice(0, count);
@@ -3256,19 +3206,6 @@ var createAggregateBatches = /* @__PURE__ */ __name(({ engine, adapter, schema,
3256
3206
  }))).then((pages) => pages.some((page2) => page2.rows.length > 0)).finally(() => {
3257
3207
  checking = void 0;
3258
3208
  }), "hasPending");
3259
- const committed = /* @__PURE__ */ __name(async (changes) => {
3260
- if (mode === "log") {
3261
- if (now() >= nextDue) await compact(false, GROUPS.commit);
3262
- return;
3263
- }
3264
- mergeAll(buffer, changes.values());
3265
- schedule();
3266
- if (buffer.size >= MAX_BUFFERED) await flushBuffer();
3267
- }, "committed");
3268
- const flush = /* @__PURE__ */ __name(async () => {
3269
- await flushBuffer();
3270
- if (mode === "log" && await hasPending()) await compact(true, GROUPS.flush);
3271
- }, "flush");
3272
3209
  return {
3273
3210
  async transaction(fn) {
3274
3211
  let changes = /* @__PURE__ */ new Map();
@@ -3294,7 +3231,7 @@ var createAggregateBatches = /* @__PURE__ */ __name(({ engine, adapter, schema,
3294
3231
  aggregate
3295
3232
  });
3296
3233
  if (failure) throw failure.error;
3297
- if (mode === "log" && attempt.size > 0) {
3234
+ if (attempt.size > 0) {
3298
3235
  const id = `${now().toString(36).padStart(9, "0")}${holder.slice(0, 8)}${crypto.randomUUID().slice(0, 8)}`;
3299
3236
  const shard = Math.floor(Math.random() * LOG_TABLES.length);
3300
3237
  const merged = mergeAll(/* @__PURE__ */ new Map(), attempt.values());
@@ -3308,24 +3245,18 @@ var createAggregateBatches = /* @__PURE__ */ __name(({ engine, adapter, schema,
3308
3245
  open = false;
3309
3246
  }
3310
3247
  });
3311
- if (changes.size > 0) await committed(changes);
3248
+ if (changes.size > 0 && now() >= nextDue) await compact(false, GROUPS.commit);
3312
3249
  return result;
3313
3250
  },
3314
3251
  async beforeRead(name) {
3315
3252
  if (!isBatched(schema.models.get(name))) return;
3316
- if (buffer.size > 0 || flushing !== void 0) await flushBuffer();
3317
- if (mode === "log") {
3318
- if (await hasPending().catch((error) => {
3319
- warn("read")(error);
3320
- return false;
3321
- })) await compact(true, GROUPS.read);
3322
- }
3253
+ if (await hasPending().catch((error) => {
3254
+ warn("read")(error);
3255
+ return false;
3256
+ })) await compact(true, GROUPS.read);
3323
3257
  },
3324
- flush,
3325
- async dispose() {
3326
- clearTimeout(timer);
3327
- timer = void 0;
3328
- await flush();
3258
+ async flush() {
3259
+ if (await hasPending()) await compact(true, GROUPS.flush);
3329
3260
  }
3330
3261
  };
3331
3262
  }, "createAggregateBatches");
@@ -4294,13 +4225,10 @@ var createDatabaseEngine = /* @__PURE__ */ __name(({ batching, now = Date.now, m
4294
4225
  }, "barePage");
4295
4226
  return {
4296
4227
  ...engine,
4297
- /** Applies batched aggregate changes still pending: this process's buffer and the log. */
4228
+ /** Applies the batched aggregate changes still in the log. */
4298
4229
  flush: /* @__PURE__ */ __name(async () => {
4299
4230
  await batches?.flush();
4300
4231
  }, "flush"),
4301
- dispose: /* @__PURE__ */ __name(async () => {
4302
- await batches?.dispose();
4303
- }, "dispose"),
4304
4232
  database(module) {
4305
4233
  const name = /* @__PURE__ */ __name((model) => module.namespace ? `${module.namespace}_${model}` : model, "name");
4306
4234
  return {
@@ -4368,8 +4296,7 @@ var createEngine = /* @__PURE__ */ __name((database, { plugins = [], now = Date.
4368
4296
  if (module === void 0) throw new HotUpdaterConfigError(`Plugin "${plugin.id}" is not one of this engine's plugins.`);
4369
4297
  return engine.database(module);
4370
4298
  }, "database"),
4371
- flush: /* @__PURE__ */ __name(() => engine.flush(), "flush"),
4372
- dispose: /* @__PURE__ */ __name(() => engine.dispose(), "dispose")
4299
+ flush: /* @__PURE__ */ __name(() => engine.flush(), "flush")
4373
4300
  };
4374
4301
  }, "createEngine");
4375
4302
 
@@ -4632,7 +4559,7 @@ var createSqlAdapter = /* @__PURE__ */ __name(({ executor, tablePrefix = "", max
4632
4559
  }, "createSqlAdapter");
4633
4560
 
4634
4561
  // ../../packages/server/dist/package.mjs
4635
- var version = "1.2.0-rc.12";
4562
+ var version = "1.2.0-rc.14";
4636
4563
 
4637
4564
  // ../../packages/server/dist/version.mjs
4638
4565
  var HOT_UPDATER_SERVER_VERSION = version;
@@ -5807,8 +5734,12 @@ var pick2 = /* @__PURE__ */ __name((row) => ({
5807
5734
  build_number: row.build_number ?? null,
5808
5735
  submitted_to: row.submitted_to ?? null,
5809
5736
  submitted_at_ms: row.submitted_at_ms ?? null,
5810
- submitted_version: row.submitted_version ?? null
5737
+ submitted_version: row.submitted_version ?? null,
5738
+ store_build: row.store_build === true ? true : null
5811
5739
  }), "pick");
5740
+ var SANDBOX_CHANNEL_PREFIX = "sandbox-";
5741
+ var OLDER_BUILDS_PAGE = 20;
5742
+ var MARK_ATTEMPTS = 8;
5812
5743
  var SUBMISSION_DESTINATIONS = {
5813
5744
  android: "play-internal",
5814
5745
  ios: "testflight"
@@ -5874,7 +5805,8 @@ var createNativeBuildApi = /* @__PURE__ */ __name((db, { now = Date.now } = {})
5874
5805
  build_number: registration.buildNumber ?? null,
5875
5806
  submitted_to: null,
5876
5807
  submitted_at_ms: null,
5877
- submitted_version: null
5808
+ submitted_version: null,
5809
+ store_build: null
5878
5810
  };
5879
5811
  for (let attempt = 1; ; attempt += 1) {
5880
5812
  await insertChannel(db, {
@@ -5912,21 +5844,45 @@ var createNativeBuildApi = /* @__PURE__ */ __name((db, { now = Date.now } = {})
5912
5844
  const scopeKey = nativeBuildScopeKey(scope);
5913
5845
  const buildKey = options?.buildKey;
5914
5846
  if (buildKey !== void 0 && !BUILD_KEY.test(buildKey)) throw new NativeBuildInputError("Invalid native build key: it must be nbk1: and a SHA-256.");
5915
- const { rows } = buildKey === void 0 ? await db.findMany("native_builds", {
5916
- index: "byScope",
5917
- where: { scope_key: scopeKey },
5918
- order: "desc",
5919
- limit: 1
5920
- }) : await db.findMany("native_builds", {
5921
- index: "byBuildKey",
5922
- where: {
5847
+ if (options?.storeBuild) {
5848
+ const marked = await db.findOne("native_builds", {
5923
5849
  scope_key: scopeKey,
5924
- build_key: buildKey
5925
- },
5926
- order: "desc",
5927
- limit: 1
5928
- });
5929
- return rows[0] === void 0 ? null : pick2(rows[0]);
5850
+ store_build: true
5851
+ });
5852
+ return marked === null ? null : pick2(marked);
5853
+ }
5854
+ const read = /* @__PURE__ */ __name((limit, before) => {
5855
+ const page2 = {
5856
+ order: "desc",
5857
+ limit,
5858
+ ...before === void 0 ? {} : { range: { lt: before } }
5859
+ };
5860
+ return buildKey === void 0 ? db.findMany("native_builds", {
5861
+ index: "byScope",
5862
+ where: { scope_key: scopeKey },
5863
+ ...page2
5864
+ }) : db.findMany("native_builds", {
5865
+ index: "byBuildKey",
5866
+ where: {
5867
+ scope_key: scopeKey,
5868
+ build_key: buildKey
5869
+ },
5870
+ ...page2
5871
+ });
5872
+ }, "read");
5873
+ const { rows } = await read(1);
5874
+ const newest = rows[0];
5875
+ if (newest === void 0) return null;
5876
+ const bundleId = options?.builtInOlderThan;
5877
+ if (bundleId === void 0 || newest.min_bundle_id < bundleId) return pick2(newest);
5878
+ for (let before = newest.id; ; ) {
5879
+ const { rows: older } = await read(OLDER_BUILDS_PAGE, before);
5880
+ const usable = older.find(({ min_bundle_id }) => min_bundle_id < bundleId);
5881
+ if (usable !== void 0) return pick2(usable);
5882
+ const last = older.at(-1);
5883
+ if (older.length < OLDER_BUILDS_PAGE || last === void 0) return null;
5884
+ before = last.id;
5885
+ }
5930
5886
  },
5931
5887
  async recordNativeBuildSubmission(id, { destination, version: version2 }) {
5932
5888
  if (version2.trim().length === 0 || version2.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.`);
@@ -5947,6 +5903,49 @@ var createNativeBuildApi = /* @__PURE__ */ __name((db, { now = Date.now } = {})
5947
5903
  };
5948
5904
  });
5949
5905
  },
5906
+ async markNativeBuild(id, { storeBuild }) {
5907
+ for (let attempt = 1; ; attempt += 1) try {
5908
+ return await db.transaction(async (tx) => {
5909
+ const row = await tx.findOne("native_builds", { id });
5910
+ if (row === null) return null;
5911
+ const build = pick2(row);
5912
+ if (!storeBuild) {
5913
+ if (build.store_build !== null) tx.update("native_builds", row, { store_build: null });
5914
+ return {
5915
+ nativeBuild: {
5916
+ ...build,
5917
+ store_build: null
5918
+ },
5919
+ previous: null
5920
+ };
5921
+ }
5922
+ 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.");
5923
+ if (build.store_build !== null) return {
5924
+ nativeBuild: build,
5925
+ previous: null
5926
+ };
5927
+ const holder = await tx.findOne("native_builds", {
5928
+ scope_key: build.scope_key,
5929
+ store_build: true
5930
+ });
5931
+ if (holder !== null) tx.update("native_builds", holder, { store_build: null });
5932
+ tx.update("native_builds", row, { store_build: true });
5933
+ return {
5934
+ nativeBuild: {
5935
+ ...build,
5936
+ store_build: true
5937
+ },
5938
+ previous: holder === null ? null : {
5939
+ ...pick2(holder),
5940
+ store_build: null
5941
+ }
5942
+ };
5943
+ });
5944
+ } catch (error) {
5945
+ if (attempt < MARK_ATTEMPTS && error instanceof DatabaseConstraintError && error.reason === "unique") continue;
5946
+ throw error;
5947
+ }
5948
+ },
5950
5949
  async deleteNativeBuild(id) {
5951
5950
  return db.transaction(async (tx) => {
5952
5951
  const row = await tx.findOne("native_builds", { id });
@@ -6984,7 +6983,13 @@ var createAdminRouteHandlers = /* @__PURE__ */ __name(() => ({
6984
6983
  const url = new URL(request.url);
6985
6984
  const scope = nativeBuildScopeOf(url);
6986
6985
  const buildKey = url.searchParams.get("buildKey");
6987
- return json({ data: await refusingInvalid(() => buildKey === null ? core.findNativeBuild(scope) : core.findNativeBuild(scope, { buildKey })) });
6986
+ const builtInOlderThan = url.searchParams.get("builtInOlderThan");
6987
+ const options = {
6988
+ ...buildKey === null ? {} : { buildKey },
6989
+ ...url.searchParams.get("storeBuild") === "true" ? { storeBuild: true } : {},
6990
+ ...builtInOlderThan === null ? {} : { builtInOlderThan }
6991
+ };
6992
+ return json({ data: await refusingInvalid(() => Object.keys(options).length === 0 ? core.findNativeBuild(scope) : core.findNativeBuild(scope, options)) });
6988
6993
  }),
6989
6994
  getNativeBuild: onCore(async (core, _request, params) => json({ data: await core.getNativeBuild(requireRouteParam(params, "id")) })),
6990
6995
  listNativeBuilds: onCore(async (core, request) => {
@@ -7005,6 +7010,12 @@ var createAdminRouteHandlers = /* @__PURE__ */ __name(() => ({
7005
7010
  };
7006
7011
  return json({ data: await refusingInvalid(() => core.recordNativeBuildSubmission(requireRouteParam(params, "id"), submission)) });
7007
7012
  }),
7013
+ markNativeBuild: onCore(async (core, request, params) => {
7014
+ const input = await body(request);
7015
+ if (!isRecord3(input) || typeof input.storeBuild !== "boolean") throw new HandlerBadRequestError("A native build's store build mark needs storeBuild, true or false.");
7016
+ const mark = { storeBuild: input.storeBuild };
7017
+ return json({ data: await refusingInvalid(() => core.markNativeBuild(requireRouteParam(params, "id"), mark)) });
7018
+ }),
7008
7019
  deleteNativeBuild: onCore(async (core, _request, params) => json({ data: await core.deleteNativeBuild(requireRouteParam(params, "id")) })),
7009
7020
  deleteBundles: onCore(async (core, request) => {
7010
7021
  const { ids } = await body(request);
@@ -7149,6 +7160,11 @@ var ADMIN_ROUTES = [
7149
7160
  path: "/native-builds/:id/submission",
7150
7161
  handler: "recordNativeBuildSubmission"
7151
7162
  },
7163
+ {
7164
+ method: "POST",
7165
+ path: "/native-builds/:id/store-build",
7166
+ handler: "markNativeBuild"
7167
+ },
7152
7168
  {
7153
7169
  method: "DELETE",
7154
7170
  path: "/native-builds/:id",