@hot-updater/cloudflare 0.36.7 → 1.0.0-rc.0

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.
Files changed (49) hide show
  1. package/dist/iac/index.cjs +988 -10
  2. package/dist/iac/index.mjs +991 -14
  3. package/dist/index.cjs +919 -6989
  4. package/dist/index.d.cts +11 -47
  5. package/dist/index.d.mts +11 -47
  6. package/dist/index.mjs +917 -7006
  7. package/dist/worker/index.cjs +898 -336
  8. package/dist/worker/index.d.cts +18 -33
  9. package/dist/worker/index.d.mts +18 -33
  10. package/dist/worker/index.mjs +899 -331
  11. package/package.json +7 -10
  12. package/sql/bundles.sql +183 -35
  13. package/src/d1Database.spec.ts +143 -392
  14. package/src/d1Database.ts +55 -650
  15. package/src/d1Implementation.spec.ts +252 -0
  16. package/src/d1Implementation.ts +742 -0
  17. package/src/d1Query.spec.ts +46 -0
  18. package/src/d1Query.ts +89 -0
  19. package/src/d1Rows.spec.ts +111 -0
  20. package/src/d1Rows.ts +326 -0
  21. package/src/d1Sql.spec.ts +30 -0
  22. package/src/d1Sql.ts +175 -0
  23. package/src/r2S3Storage.ts +93 -137
  24. package/src/r2Storage.spec.ts +120 -218
  25. package/src/r2Storage.ts +13 -61
  26. package/src/r2WorkerStorage.spec.ts +161 -50
  27. package/src/r2WorkerStorage.ts +85 -118
  28. package/src/worker/d1Database.spec.ts +33 -0
  29. package/src/worker/d1Database.ts +52 -0
  30. package/src/worker/index.ts +3 -31
  31. package/worker/dist/README.md +1 -1
  32. package/worker/dist/index.js +5968 -4701
  33. package/worker/dist/index.js.map +4 -4
  34. package/worker/migrations/0001_hot-updater_1.0.0.sql +197 -0
  35. package/worker/migrations/0002_hot-updater_artifact_byte_sizes.sql +11 -0
  36. package/worker/src/databaseMigration.integration.spec.ts +114 -0
  37. package/worker/src/databasePlugins.integration.spec.ts +292 -0
  38. package/worker/src/index.integration.spec.ts +102 -294
  39. package/worker/src/index.ts +14 -66
  40. package/worker/src/storagePlugin.integration.spec.ts +59 -0
  41. package/worker/wrangler.json +5 -2
  42. package/src/cloudflareWorkerDatabase.spec.ts +0 -260
  43. package/src/cloudflareWorkerDatabase.ts +0 -651
  44. package/src/r2WranglerStorage.ts +0 -193
  45. package/worker/migrations/0001_hot-updater_init.sql +0 -16
  46. package/worker/migrations/0002_hot-updater_0.13.0.sql +0 -8
  47. package/worker/migrations/0003_hot-updater_0.18.0.sql +0 -38
  48. package/worker/migrations/0004_hot-updater_0.29.0.sql +0 -11
  49. package/worker/migrations/0005_hot-updater_0.31.0.sql +0 -24
@@ -29,7 +29,11 @@ fs_promises = __toESM(fs_promises);
29
29
  let path = require("path");
30
30
  path = __toESM(path);
31
31
  let _hot_updater_cli_tools = require("@hot-updater/cli-tools");
32
+ let _hot_updater_server = require("@hot-updater/server");
32
33
  let cloudflare = require("cloudflare");
34
+ cloudflare = __toESM(cloudflare);
35
+ let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
36
+ let _hot_updater_plugin_core_internal = require("@hot-updater/plugin-core/internal");
33
37
  let node_url = require("node:url");
34
38
  let node_child_process = require("node:child_process");
35
39
  let node_string_decoder = require("node:string_decoder");
@@ -53,6 +57,895 @@ let node_buffer = require("node:buffer");
53
57
  let cloudflare_error = require("cloudflare/error");
54
58
  let os = require("os");
55
59
  os = __toESM(os);
60
+ //#region src/d1Rows.ts
61
+ var InvalidD1RowError = class extends Error {
62
+ name = "InvalidD1RowError";
63
+ model;
64
+ constructor(model) {
65
+ super(`D1 returned an invalid ${model} row`);
66
+ this.model = model;
67
+ }
68
+ };
69
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
70
+ const stringValue = (row, field, model) => {
71
+ const value = row[field];
72
+ if (typeof value !== "string") throw new InvalidD1RowError(model);
73
+ return value;
74
+ };
75
+ const nullableString = (row, field, model) => {
76
+ const value = row[field];
77
+ if (value === null) return null;
78
+ if (typeof value !== "string") throw new InvalidD1RowError(model);
79
+ return value;
80
+ };
81
+ const numberValue = (row, field, model) => {
82
+ const value = row[field];
83
+ if (typeof value !== "number") throw new InvalidD1RowError(model);
84
+ return value;
85
+ };
86
+ const byteSizeValue = (row, field, model) => {
87
+ const value = row[field];
88
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new InvalidD1RowError(model);
89
+ return value;
90
+ };
91
+ const booleanValue = (row, field, model) => {
92
+ const value = row[field];
93
+ if (value === true || value === 1) return true;
94
+ if (value === false || value === 0) return false;
95
+ throw new InvalidD1RowError(model);
96
+ };
97
+ const jsonValue = (value, model) => {
98
+ if (typeof value !== "string") throw new InvalidD1RowError(model);
99
+ try {
100
+ return JSON.parse(value);
101
+ } catch (error) {
102
+ if (error instanceof SyntaxError) throw new InvalidD1RowError(model);
103
+ throw error;
104
+ }
105
+ };
106
+ const metadata = (row) => {
107
+ const value = jsonValue(row["metadata"], "bundles");
108
+ if (!(0, _hot_updater_plugin_core.isDatabaseMetadataObject)(value)) throw new InvalidD1RowError("bundles");
109
+ return value;
110
+ };
111
+ const bundleRow = (row) => {
112
+ const platform = stringValue(row, "platform", "bundles");
113
+ if (platform !== "ios" && platform !== "android") throw new InvalidD1RowError("bundles");
114
+ return {
115
+ id: stringValue(row, "id", "bundles"),
116
+ platform,
117
+ file_hash: stringValue(row, "file_hash", "bundles"),
118
+ git_commit_hash: nullableString(row, "git_commit_hash", "bundles"),
119
+ storage_uri: stringValue(row, "storage_uri", "bundles"),
120
+ archive_byte_size: row["archive_byte_size"] === void 0 ? 0 : byteSizeValue(row, "archive_byte_size", "bundles"),
121
+ metadata: metadata(row),
122
+ manifest_storage_uri: nullableString(row, "manifest_storage_uri", "bundles"),
123
+ manifest_file_hash: nullableString(row, "manifest_file_hash", "bundles"),
124
+ asset_base_storage_uri: nullableString(row, "asset_base_storage_uri", "bundles")
125
+ };
126
+ };
127
+ const patchRow = (row) => ({
128
+ id: stringValue(row, "id", "bundle_patches"),
129
+ bundle_id: stringValue(row, "bundle_id", "bundle_patches"),
130
+ base_bundle_id: stringValue(row, "base_bundle_id", "bundle_patches"),
131
+ base_file_hash: stringValue(row, "base_file_hash", "bundle_patches"),
132
+ patch_file_hash: stringValue(row, "patch_file_hash", "bundle_patches"),
133
+ patch_storage_uri: stringValue(row, "patch_storage_uri", "bundle_patches"),
134
+ byte_size: byteSizeValue(row, "byte_size", "bundle_patches"),
135
+ order_index: numberValue(row, "order_index", "bundle_patches")
136
+ });
137
+ const channelRow = (row) => ({
138
+ id: stringValue(row, "id", "channels"),
139
+ name: stringValue(row, "name", "channels")
140
+ });
141
+ const eventRow = (row) => {
142
+ const type = stringValue(row, "type", "bundle_events");
143
+ const platform = stringValue(row, "platform", "bundle_events");
144
+ const fromBundleId = nullableString(row, "from_bundle_id", "bundle_events");
145
+ const updateStrategy = nullableString(row, "update_strategy", "bundle_events");
146
+ if (platform !== "ios" && platform !== "android" || !((type === "UPDATE_APPLIED" || type === "RECOVERED" || type === "RELEASE_ADOPTED") && typeof fromBundleId === "string" && (updateStrategy === "fingerprint" || updateStrategy === "appVersion") || type === "UNCHANGED" && fromBundleId === null && updateStrategy === null)) throw new InvalidD1RowError("bundle_events");
147
+ return {
148
+ id: stringValue(row, "id", "bundle_events"),
149
+ type,
150
+ install_id: stringValue(row, "install_id", "bundle_events"),
151
+ user_id: nullableString(row, "user_id", "bundle_events"),
152
+ username: nullableString(row, "username", "bundle_events"),
153
+ from_release_id: nullableString(row, "from_release_id", "bundle_events"),
154
+ from_bundle_id: fromBundleId,
155
+ to_release_id: nullableString(row, "to_release_id", "bundle_events"),
156
+ to_bundle_id: stringValue(row, "to_bundle_id", "bundle_events"),
157
+ platform,
158
+ app_version: stringValue(row, "app_version", "bundle_events"),
159
+ channel: stringValue(row, "channel", "bundle_events"),
160
+ cohort: stringValue(row, "cohort", "bundle_events"),
161
+ update_strategy: updateStrategy,
162
+ fingerprint_hash: nullableString(row, "fingerprint_hash", "bundle_events"),
163
+ sdk_version: nullableString(row, "sdk_version", "bundle_events"),
164
+ received_at_ms: numberValue(row, "received_at_ms", "bundle_events")
165
+ };
166
+ };
167
+ const apiKeyRow = (row) => {
168
+ const role = stringValue(row, "role", "api_keys");
169
+ if (role !== "client") throw new InvalidD1RowError("api_keys");
170
+ const revokedAt = row["revoked_at_ms"];
171
+ return {
172
+ id: stringValue(row, "id", "api_keys"),
173
+ hash: stringValue(row, "hash", "api_keys"),
174
+ name: stringValue(row, "name", "api_keys"),
175
+ prefix: stringValue(row, "prefix", "api_keys"),
176
+ role,
177
+ created_at_ms: numberValue(row, "created_at_ms", "api_keys"),
178
+ revoked_at_ms: revokedAt === null ? null : numberValue(row, "revoked_at_ms", "api_keys")
179
+ };
180
+ };
181
+ const releaseTargetCohorts = (row) => {
182
+ const value = jsonValue(row["target_cohorts"], "releases");
183
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new InvalidD1RowError("releases");
184
+ return value.filter((item) => typeof item === "string");
185
+ };
186
+ const releaseRow = (row) => {
187
+ const platform = stringValue(row, "platform", "releases");
188
+ const kind = stringValue(row, "kind", "releases");
189
+ const strategy = stringValue(row, "strategy", "releases");
190
+ const operation = stringValue(row, "operation", "releases");
191
+ if (platform !== "ios" && platform !== "android" || kind !== "BUNDLE" && kind !== "EMBEDDED" || strategy !== "APP_VERSION" && strategy !== "FINGERPRINT" || operation !== "DEPLOY" && operation !== "PROMOTE" && operation !== "ROLLBACK") throw new InvalidD1RowError("releases");
192
+ return {
193
+ id: stringValue(row, "id", "releases"),
194
+ revision: numberValue(row, "revision", "releases"),
195
+ scope_key: stringValue(row, "scope_key", "releases"),
196
+ channel_id: stringValue(row, "channel_id", "releases"),
197
+ platform,
198
+ kind,
199
+ bundle_id: nullableString(row, "bundle_id", "releases"),
200
+ strategy,
201
+ target_app_version: nullableString(row, "target_app_version", "releases"),
202
+ fingerprint_hash: nullableString(row, "fingerprint_hash", "releases"),
203
+ enabled: booleanValue(row, "enabled", "releases"),
204
+ should_force_update: booleanValue(row, "should_force_update", "releases"),
205
+ message: nullableString(row, "message", "releases"),
206
+ rollout_cohort_count: numberValue(row, "rollout_cohort_count", "releases"),
207
+ target_cohorts: releaseTargetCohorts(row),
208
+ operation,
209
+ source_release_id: nullableString(row, "source_release_id", "releases"),
210
+ created_at_ms: numberValue(row, "created_at_ms", "releases"),
211
+ updated_at_ms: numberValue(row, "updated_at_ms", "releases")
212
+ };
213
+ };
214
+ const releaseCatalogRow = (row) => {
215
+ const platform = stringValue(row, "platform", "release_catalogs");
216
+ const strategy = stringValue(row, "strategy", "release_catalogs");
217
+ if (platform !== "ios" && platform !== "android" || strategy !== "APP_VERSION" && strategy !== "FINGERPRINT") throw new InvalidD1RowError("release_catalogs");
218
+ return {
219
+ scope_key: stringValue(row, "scope_key", "release_catalogs"),
220
+ catalog_id: stringValue(row, "catalog_id", "release_catalogs"),
221
+ strategy,
222
+ channel_id: stringValue(row, "channel_id", "release_catalogs"),
223
+ channel_key: stringValue(row, "channel_key", "release_catalogs"),
224
+ platform,
225
+ fingerprint_hash: nullableString(row, "fingerprint_hash", "release_catalogs"),
226
+ generation: numberValue(row, "generation", "release_catalogs"),
227
+ payload: stringValue(row, "payload", "release_catalogs"),
228
+ catalog_hash: stringValue(row, "catalog_hash", "release_catalogs"),
229
+ byte_size: numberValue(row, "byte_size", "release_catalogs"),
230
+ is_tombstone: booleanValue(row, "is_tombstone", "release_catalogs"),
231
+ updated_at_ms: numberValue(row, "updated_at_ms", "release_catalogs")
232
+ };
233
+ };
234
+ function parseD1Row(model, value) {
235
+ if (!isRecord(value)) throw new InvalidD1RowError(model);
236
+ switch (model) {
237
+ case "bundles": return bundleRow(value);
238
+ case "bundle_patches": return patchRow(value);
239
+ case "channels": return channelRow(value);
240
+ case "bundle_events": return eventRow(value);
241
+ case "api_keys": return apiKeyRow(value);
242
+ case "releases": return releaseRow(value);
243
+ case "release_catalogs": return releaseCatalogRow(value);
244
+ }
245
+ }
246
+ //#endregion
247
+ //#region src/d1Sql.ts
248
+ var InvalidD1PredicateError = class extends Error {
249
+ name = "InvalidD1PredicateError";
250
+ };
251
+ const encodeD1Value = (value) => {
252
+ const encoded = JSON.stringify(value);
253
+ if (encoded === void 0) throw new TypeError("D1 values must be JSON-serializable.");
254
+ return encoded;
255
+ };
256
+ const bind = (value) => ({
257
+ sql: "json_extract(?, '$')",
258
+ params: [encodeD1Value(value)]
259
+ });
260
+ const escapeLikePattern = (value) => value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
261
+ const stringPredicate = (condition, operator) => {
262
+ if (typeof condition.value !== "string") throw new InvalidD1PredicateError();
263
+ if (condition.mode !== "insensitive") {
264
+ const parameter = bind(condition.value);
265
+ const column = `${condition.field} COLLATE BINARY`;
266
+ const value = `${parameter.sql} COLLATE BINARY`;
267
+ if (operator === "contains" || operator === "starts_with") return {
268
+ sql: `instr(${column}, ${value}) ${operator === "contains" ? "> 0" : "= 1"}`,
269
+ params: parameter.params
270
+ };
271
+ return {
272
+ sql: `substr(${column}, length(${condition.field}) - length(${parameter.sql}) + 1) = ${value}`,
273
+ params: [...parameter.params, ...parameter.params]
274
+ };
275
+ }
276
+ const literal = escapeLikePattern(condition.value);
277
+ const parameter = bind(operator === "contains" ? `%${literal}%` : operator === "starts_with" ? `${literal}%` : `%${literal}`);
278
+ return {
279
+ sql: `lower(${condition.field}) LIKE lower(${parameter.sql}) ESCAPE '\\'`,
280
+ params: parameter.params
281
+ };
282
+ };
283
+ const predicate = (condition) => {
284
+ const operator = condition.operator ?? "eq";
285
+ switch (operator) {
286
+ case "eq":
287
+ case "ne": {
288
+ if (condition.value === null) return {
289
+ sql: `${condition.field} IS ${operator === "ne" ? "NOT " : ""}NULL`,
290
+ params: []
291
+ };
292
+ const parameter = bind(condition.value);
293
+ const insensitive = "mode" in condition && condition.mode === "insensitive";
294
+ const column = insensitive ? `lower(${condition.field})` : condition.field;
295
+ const value = insensitive ? `lower(${parameter.sql})` : parameter.sql;
296
+ return {
297
+ sql: `${column} ${operator === "eq" ? "=" : "<>"} ${value}`,
298
+ params: parameter.params
299
+ };
300
+ }
301
+ case "gt":
302
+ case "gte":
303
+ case "lt":
304
+ case "lte": {
305
+ const sqlOperator = {
306
+ gt: ">",
307
+ gte: ">=",
308
+ lt: "<",
309
+ lte: "<="
310
+ }[operator];
311
+ const parameter = bind(condition.value);
312
+ return {
313
+ sql: `${condition.field} ${sqlOperator} ${parameter.sql}`,
314
+ params: parameter.params
315
+ };
316
+ }
317
+ case "in":
318
+ case "not_in":
319
+ if (!Array.isArray(condition.value)) throw new InvalidD1PredicateError();
320
+ if (condition.value.length === 0) return {
321
+ sql: operator === "not_in" ? "1 = 1" : "1 = 0",
322
+ params: []
323
+ };
324
+ return {
325
+ sql: `${condition.field} ${operator === "not_in" ? "NOT " : ""}IN (SELECT value FROM json_each(?))`,
326
+ params: [encodeD1Value(condition.value)]
327
+ };
328
+ case "contains":
329
+ case "starts_with":
330
+ case "ends_with": return stringPredicate(condition, operator);
331
+ }
332
+ };
333
+ const buildD1Where = (where) => {
334
+ const [first, ...rest] = where ?? [];
335
+ if (first === void 0) return {
336
+ sql: "",
337
+ params: []
338
+ };
339
+ const initial = predicate(first);
340
+ let sql = initial.sql;
341
+ const params = [...initial.params];
342
+ for (const condition of rest) {
343
+ const next = predicate(condition);
344
+ sql = `(${sql} ${condition.connector === "OR" ? "OR" : "AND"} ${next.sql})`;
345
+ params.push(...next.params);
346
+ }
347
+ return {
348
+ sql: ` WHERE ${sql}`,
349
+ params
350
+ };
351
+ };
352
+ const buildD1Order = (orderBy) => orderBy === void 0 ? "" : ` ORDER BY ${orderBy.map((clause) => `${clause.field} ${clause.direction.toUpperCase()}${clause.nulls ? ` NULLS ${clause.nulls.toUpperCase()}` : ""}`).join(", ")}`;
353
+ const d1Placeholders = (count) => Array.from({ length: count }, () => "json_extract(?, '$')").join(", ");
354
+ const encodeD1Values = (values) => values.map(encodeD1Value);
355
+ //#endregion
356
+ //#region src/d1Query.ts
357
+ const d1TableNames = {
358
+ bundles: "bundles",
359
+ bundle_patches: "bundle_patches",
360
+ channels: "channels",
361
+ bundle_events: "bundle_events",
362
+ api_keys: "api_keys",
363
+ release_catalogs: "release_catalogs",
364
+ releases: "releases"
365
+ };
366
+ var D1QueryResultError = class extends Error {
367
+ name = "D1QueryResultError";
368
+ operation = "count";
369
+ constructor(result) {
370
+ super("D1 count query returned a malformed first row");
371
+ this.result = result;
372
+ }
373
+ };
374
+ const countD1Rows = async (executor, input) => {
375
+ const where = buildD1Where(input.where);
376
+ const table = d1TableNames[input.model];
377
+ const source = input.distinct === void 0 ? `${table}${where.sql}` : `(SELECT DISTINCT ${input.distinct.join(", ")} FROM ${table}${where.sql}) AS distinct_rows`;
378
+ const row = (await executor.query(`SELECT COUNT(*) AS count FROM ${source}`, where.params))[0];
379
+ if (typeof row !== "object" || row === null || Array.isArray(row)) throw new D1QueryResultError(row);
380
+ const count = Reflect.get(row, "count");
381
+ if (typeof count !== "number" || !Number.isFinite(count)) throw new D1QueryResultError(row);
382
+ return count;
383
+ };
384
+ const findManyD1Rows = async (executor, input) => {
385
+ const where = buildD1Where(input.where);
386
+ const orderBy = input.orderBy;
387
+ const order = buildD1Order(orderBy);
388
+ const table = d1TableNames[input.model];
389
+ const source = input.distinctOn === void 0 ? `SELECT * FROM ${table}${where.sql}${order}` : `SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY ${input.distinctOn.fields.join(", ")}${order}) AS __hot_updater_rank FROM ${table}${where.sql}) AS distinct_rows WHERE __hot_updater_rank = 1${order}`;
390
+ const pageParams = encodeD1Values([input.limit, input.offset]);
391
+ const rows = await executor.query(`${source} LIMIT json_extract(?, '$') OFFSET json_extract(?, '$')`, [...where.params, ...pageParams]);
392
+ switch (input.model) {
393
+ case "bundles": return rows.map((row) => parseD1Row("bundles", row));
394
+ case "bundle_patches": return rows.map((row) => parseD1Row("bundle_patches", row));
395
+ case "channels": return rows.map((row) => parseD1Row("channels", row));
396
+ case "bundle_events": return rows.map((row) => parseD1Row("bundle_events", row));
397
+ case "api_keys": return rows.map((row) => parseD1Row("api_keys", row));
398
+ case "releases": return rows.map((row) => parseD1Row("releases", row));
399
+ case "release_catalogs": return rows.map((row) => parseD1Row("release_catalogs", row));
400
+ }
401
+ };
402
+ //#endregion
403
+ //#region src/d1Implementation.ts
404
+ const EXPECTATION_CONFLICT_MARKER = "HOT_UPDATER_COMMIT_EXPECTATION_CONFLICT";
405
+ const expectationGuard = (expectations) => {
406
+ const clauses = [];
407
+ const params = [];
408
+ for (const expectation of expectations) {
409
+ const isRelease = expectation.model === "releases";
410
+ const table = isRelease ? "releases" : "release_catalogs";
411
+ const keyField = isRelease ? "id" : "scope_key";
412
+ const versionField = isRelease ? "revision" : "generation";
413
+ const key = isRelease ? expectation.id : expectation.scopeKey;
414
+ const version = isRelease ? expectation.revision : expectation.generation;
415
+ if (version === null) {
416
+ clauses.push(`NOT EXISTS (SELECT 1 FROM ${table} WHERE ${keyField} = json_extract(?, '$'))`);
417
+ params.push(...encodeD1Values([key]));
418
+ } else {
419
+ clauses.push(`EXISTS (SELECT 1 FROM ${table} WHERE ${keyField} = json_extract(?, '$') AND ${versionField} = json_extract(?, '$'))`);
420
+ params.push(...encodeD1Values([key, version]));
421
+ }
422
+ }
423
+ return {
424
+ sql: clauses.join(" AND ") || "1",
425
+ params
426
+ };
427
+ };
428
+ const readVersion = (row, field) => {
429
+ if (typeof row !== "object" || row === null) return null;
430
+ const value = Reflect.get(row, field);
431
+ return typeof value === "number" ? value : null;
432
+ };
433
+ const expectationConflict = async (executor, expectations) => {
434
+ for (const expectation of expectations) {
435
+ const isRelease = expectation.model === "releases";
436
+ const table = isRelease ? "releases" : "release_catalogs";
437
+ const keyField = isRelease ? "id" : "scope_key";
438
+ const versionField = isRelease ? "revision" : "generation";
439
+ const key = isRelease ? expectation.id : expectation.scopeKey;
440
+ const expectedVersion = isRelease ? expectation.revision : expectation.generation;
441
+ const actualVersion = readVersion((await executor.query(`SELECT ${versionField} FROM ${table} WHERE ${keyField} = json_extract(?, '$') LIMIT 1`, encodeD1Values([key])))[0], versionField);
442
+ if (actualVersion !== expectedVersion) return {
443
+ committed: false,
444
+ conflict: {
445
+ actualVersion,
446
+ changeIndex: -1,
447
+ expectedVersion,
448
+ key,
449
+ model: expectation.model,
450
+ reason: "version_conflict"
451
+ }
452
+ };
453
+ }
454
+ return null;
455
+ };
456
+ const isExpectationConflictError = (error) => {
457
+ const message = error instanceof Error ? error.message : String(error);
458
+ return message.includes(EXPECTATION_CONFLICT_MARKER) || message.toLowerCase().includes("malformed json");
459
+ };
460
+ const bundleValues = (row) => [
461
+ row.id,
462
+ row.platform,
463
+ row.file_hash,
464
+ row.git_commit_hash,
465
+ row.storage_uri,
466
+ row.archive_byte_size,
467
+ row.metadata,
468
+ row.manifest_storage_uri,
469
+ row.manifest_file_hash,
470
+ row.asset_base_storage_uri
471
+ ];
472
+ const patchValues = (row) => [
473
+ row.id,
474
+ row.bundle_id,
475
+ row.base_bundle_id,
476
+ row.base_file_hash,
477
+ row.patch_file_hash,
478
+ row.patch_storage_uri,
479
+ row.byte_size,
480
+ row.order_index
481
+ ];
482
+ const channelValues = (row) => [row.id, row.name];
483
+ const insertQuery = (input, guard, conflictMode = "returnExisting") => {
484
+ let columns;
485
+ let values;
486
+ switch (input.model) {
487
+ case "bundles":
488
+ columns = [
489
+ "id",
490
+ "platform",
491
+ "file_hash",
492
+ "git_commit_hash",
493
+ "storage_uri",
494
+ "archive_byte_size",
495
+ "metadata",
496
+ "manifest_storage_uri",
497
+ "manifest_file_hash",
498
+ "asset_base_storage_uri"
499
+ ];
500
+ values = bundleValues(input.data);
501
+ break;
502
+ case "bundle_patches":
503
+ columns = [
504
+ "id",
505
+ "bundle_id",
506
+ "base_bundle_id",
507
+ "base_file_hash",
508
+ "patch_file_hash",
509
+ "patch_storage_uri",
510
+ "byte_size",
511
+ "order_index"
512
+ ];
513
+ values = patchValues(input.data);
514
+ break;
515
+ case "channels":
516
+ columns = ["id", "name"];
517
+ values = channelValues(input.data);
518
+ break;
519
+ case "bundle_events":
520
+ case "api_keys":
521
+ case "release_catalogs":
522
+ case "releases":
523
+ columns = Object.keys(input.data);
524
+ values = Object.values(input.data);
525
+ break;
526
+ }
527
+ const conflict = input.onConflict !== "ignore" ? "" : input.model === "channels" ? conflictMode === "ignore" ? " ON CONFLICT(name) DO NOTHING" : " ON CONFLICT(name) DO UPDATE SET name = excluded.name" : input.model === "api_keys" ? conflictMode === "ignore" ? " ON CONFLICT(hash) DO NOTHING" : " ON CONFLICT(hash) DO UPDATE SET hash = excluded.hash" : "";
528
+ return {
529
+ sql: `INSERT INTO ${d1TableNames[input.model]} (${columns.join(", ")}) ${guard === void 0 ? `VALUES (${d1Placeholders(values.length)})` : `SELECT ${d1Placeholders(values.length)} WHERE ${guard.sql}`}${conflict} RETURNING *`,
530
+ params: [...encodeD1Values(values), ...guard?.params ?? []]
531
+ };
532
+ };
533
+ const updateEntries = (update) => Object.entries(update);
534
+ const updateQuery = (input, guard) => {
535
+ const entries = updateEntries(input.update);
536
+ const where = buildD1Where(input.where);
537
+ if (entries.length === 0) return {
538
+ sql: `SELECT * FROM ${d1TableNames[input.model]}${where.sql}${guard ? ` AND ${guard.sql}` : ""} LIMIT 1`,
539
+ params: [...where.params, ...guard?.params ?? []]
540
+ };
541
+ const assignments = entries.map(([field]) => `${field} = json_extract(?, '$')`).join(", ");
542
+ return {
543
+ sql: `UPDATE ${d1TableNames[input.model]} SET ${assignments}${where.sql}${guard ? ` AND ${guard.sql}` : ""} RETURNING *`,
544
+ params: [
545
+ ...encodeD1Values(entries.map(([, value]) => value)),
546
+ ...where.params,
547
+ ...guard?.params ?? []
548
+ ]
549
+ };
550
+ };
551
+ const deleteQuery = (input, guard) => {
552
+ const where = buildD1Where(input.where);
553
+ return {
554
+ sql: `DELETE FROM ${d1TableNames[input.model]}${where.sql}${guard ? ` AND ${guard.sql}` : ""}`,
555
+ params: [...where.params, ...guard?.params ?? []]
556
+ };
557
+ };
558
+ const insertedKey = (change) => {
559
+ if (change.operation !== "insert") return void 0;
560
+ if (change.model === "bundles") return `bundles:${change.row.id}`;
561
+ if (change.model === "apiKeys") return `api_keys:${change.row.id}`;
562
+ if (change.model === "releases") return `releases:${change.row.id}`;
563
+ };
564
+ const requiredRow = (change, changeIndex) => {
565
+ if (change.operation !== "update") return void 0;
566
+ if (change.model === "bundles") return {
567
+ model: "bundles",
568
+ id: change.where.id,
569
+ changeIndex
570
+ };
571
+ if (change.model === "apiKeys") return {
572
+ model: "api_keys",
573
+ id: change.where.id,
574
+ changeIndex
575
+ };
576
+ if (change.model === "releases") return {
577
+ model: "releases",
578
+ id: change.where.id,
579
+ changeIndex
580
+ };
581
+ };
582
+ const channelDeletePrecondition = (changes, changeIndex, channelId) => {
583
+ const releaseEffects = /* @__PURE__ */ new Map();
584
+ for (const change of changes.slice(0, changeIndex)) {
585
+ if (change.model !== "releases") continue;
586
+ switch (change.operation) {
587
+ case "insert":
588
+ releaseEffects.set(change.row.id, change.row.channel_id);
589
+ break;
590
+ case "update": break;
591
+ case "delete":
592
+ releaseEffects.set(change.where.id, null);
593
+ break;
594
+ }
595
+ }
596
+ return {
597
+ id: channelId,
598
+ excludedReleaseIds: [...releaseEffects.keys()],
599
+ addedReferenceReleaseIds: [...releaseEffects].filter(([, finalChannelId]) => finalChannelId === channelId).map(([releaseId]) => releaseId)
600
+ };
601
+ };
602
+ const changeQuery = (change, guard) => {
603
+ switch (change.model) {
604
+ case "bundles": switch (change.operation) {
605
+ case "insert": return insertQuery({
606
+ model: "bundles",
607
+ data: change.row
608
+ }, guard);
609
+ case "update": return updateQuery({
610
+ model: "bundles",
611
+ where: [{
612
+ field: "id",
613
+ value: change.where.id
614
+ }],
615
+ update: change.update
616
+ }, guard);
617
+ case "delete": return deleteQuery({
618
+ model: "bundles",
619
+ where: [{
620
+ field: "id",
621
+ value: change.where.id
622
+ }]
623
+ }, guard);
624
+ }
625
+ case "bundlePatches": return change.operation === "insert" ? insertQuery({
626
+ model: "bundle_patches",
627
+ data: change.row
628
+ }, guard) : deleteQuery({
629
+ model: "bundle_patches",
630
+ where: [{
631
+ field: "bundle_id",
632
+ value: change.where.bundleId
633
+ }]
634
+ }, guard);
635
+ case "releases": switch (change.operation) {
636
+ case "insert": return insertQuery({
637
+ model: "releases",
638
+ data: change.row
639
+ }, guard);
640
+ case "update": return updateQuery({
641
+ model: "releases",
642
+ where: [{
643
+ field: "id",
644
+ value: change.where.id
645
+ }],
646
+ update: change.update
647
+ }, guard);
648
+ case "delete": return deleteQuery({
649
+ model: "releases",
650
+ where: [{
651
+ field: "id",
652
+ value: change.where.id
653
+ }]
654
+ }, guard);
655
+ }
656
+ case "releaseCatalogs": return {
657
+ sql: `INSERT INTO release_catalogs (${Object.keys(change.row).join(", ")}) SELECT ${d1Placeholders(Object.keys(change.row).length)} WHERE ${guard.sql} ON CONFLICT(scope_key) DO UPDATE SET ${Object.keys(change.row).filter((field) => field !== "scope_key").map((field) => `${field} = excluded.${field}`).join(", ")} RETURNING *`,
658
+ params: [...encodeD1Values(Object.values(change.row)), ...guard.params]
659
+ };
660
+ case "channels": return change.operation === "insert" ? insertQuery({
661
+ model: "channels",
662
+ data: change.row,
663
+ onConflict: change.onConflict
664
+ }, guard, "ignore") : {
665
+ sql: `DELETE FROM channels WHERE id = json_extract(?, '$') AND NOT EXISTS (SELECT 1 FROM releases WHERE channel_id = json_extract(?, '$')) AND ${guard.sql}`,
666
+ params: [...encodeD1Values([change.where.id, change.where.id]), ...guard.params]
667
+ };
668
+ case "analytics": return insertQuery({
669
+ model: "bundle_events",
670
+ data: change.row
671
+ }, guard);
672
+ case "apiKeys": return change.operation === "insert" ? insertQuery({
673
+ model: "api_keys",
674
+ data: change.row,
675
+ onConflict: change.onConflict
676
+ }, guard, "ignore") : updateQuery({
677
+ model: "api_keys",
678
+ where: [{
679
+ field: "id",
680
+ value: change.where.id
681
+ }],
682
+ update: { revoked_at_ms: change.update.revokedAtMs }
683
+ }, guard);
684
+ }
685
+ };
686
+ const createCommitPlan = (input) => {
687
+ const checks = [];
688
+ const statements = [];
689
+ const expectations = input.expectations ?? [];
690
+ if (expectations.length > 0) {
691
+ const guard = expectationGuard(expectations);
692
+ statements.push({
693
+ sql: `SELECT CASE WHEN ${guard.sql} THEN 1 ELSE json_extract('${EXPECTATION_CONFLICT_MARKER}', '$') END AS expectation_guard`,
694
+ params: guard.params
695
+ });
696
+ }
697
+ const requiredRows = [];
698
+ const channelDeletes = [];
699
+ const inserted = /* @__PURE__ */ new Set();
700
+ for (const [changeIndex, change] of input.changes.entries()) {
701
+ const required = requiredRow(change, changeIndex);
702
+ if (required !== void 0) {
703
+ if (!inserted.has(`${required.model}:${required.id}`)) {
704
+ requiredRows.push(required);
705
+ checks.push({
706
+ changeIndex,
707
+ resultIndex: statements.length,
708
+ conflictWhen: "empty",
709
+ reason: "not_found"
710
+ });
711
+ statements.push({
712
+ sql: `SELECT id FROM ${d1TableNames[required.model]} WHERE id = json_extract(?, '$') LIMIT 1`,
713
+ params: encodeD1Values([required.id])
714
+ });
715
+ }
716
+ }
717
+ if (change.model === "channels" && change.operation === "delete") {
718
+ const precondition = channelDeletePrecondition(input.changes, changeIndex, change.where.id);
719
+ channelDeletes.push(precondition);
720
+ checks.push({
721
+ changeIndex,
722
+ resultIndex: statements.length,
723
+ conflictWhen: "nonempty",
724
+ reason: "referenced"
725
+ });
726
+ statements.push({
727
+ sql: "SELECT id FROM releases WHERE channel_id = json_extract(?, '$') AND id NOT IN (SELECT value FROM json_each(?)) UNION ALL SELECT value AS id FROM json_each(?) LIMIT 1",
728
+ params: encodeD1Values([
729
+ precondition.id,
730
+ precondition.excludedReleaseIds,
731
+ precondition.addedReferenceReleaseIds
732
+ ])
733
+ });
734
+ }
735
+ const key = insertedKey(change);
736
+ if (key !== void 0) inserted.add(key);
737
+ }
738
+ const guard = {
739
+ sql: `NOT EXISTS (
740
+ SELECT 1 FROM json_each(?) AS required
741
+ WHERE NOT EXISTS (
742
+ SELECT 1 FROM bundles
743
+ WHERE json_extract(required.value, '$.model') = 'bundles'
744
+ AND bundles.id = json_extract(required.value, '$.id')
745
+ UNION ALL
746
+ SELECT 1 FROM api_keys
747
+ WHERE json_extract(required.value, '$.model') = 'api_keys'
748
+ AND api_keys.id = json_extract(required.value, '$.id')
749
+ UNION ALL
750
+ SELECT 1 FROM channels
751
+ WHERE json_extract(required.value, '$.model') = 'channels'
752
+ AND channels.id = json_extract(required.value, '$.id')
753
+ UNION ALL
754
+ SELECT 1 FROM releases
755
+ WHERE json_extract(required.value, '$.model') = 'releases'
756
+ AND releases.id = json_extract(required.value, '$.id')
757
+ )
758
+ ) AND NOT EXISTS (
759
+ SELECT 1 FROM json_each(?) AS channel_delete
760
+ WHERE EXISTS (
761
+ SELECT 1 FROM releases
762
+ WHERE releases.channel_id = json_extract(channel_delete.value, '$.id')
763
+ AND releases.id NOT IN (
764
+ SELECT value FROM json_each(
765
+ json_extract(channel_delete.value, '$.excludedReleaseIds')
766
+ )
767
+ )
768
+ UNION ALL
769
+ SELECT value FROM json_each(
770
+ json_extract(channel_delete.value, '$.addedReferenceReleaseIds')
771
+ )
772
+ )
773
+ )`,
774
+ params: encodeD1Values([requiredRows.map(({ model, id }) => ({
775
+ model,
776
+ id
777
+ })), channelDeletes])
778
+ };
779
+ statements.push(...input.changes.map((change) => changeQuery(change, guard)));
780
+ return {
781
+ checks,
782
+ statements
783
+ };
784
+ };
785
+ const resultForPlan = (plan, results) => {
786
+ const missing = plan.checks.find(({ conflictWhen, resultIndex }) => conflictWhen === "empty" ? (results[resultIndex]?.length ?? 0) === 0 : (results[resultIndex]?.length ?? 0) > 0);
787
+ return missing === void 0 ? { committed: true } : {
788
+ committed: false,
789
+ conflict: {
790
+ changeIndex: missing.changeIndex,
791
+ reason: missing.reason
792
+ }
793
+ };
794
+ };
795
+ const insertChannel = async (executor, input) => {
796
+ const insert = insertQuery({
797
+ model: "channels",
798
+ data: input.row,
799
+ onConflict: "ignore"
800
+ }, void 0, "ignore");
801
+ const select = {
802
+ sql: "SELECT id, name FROM channels WHERE name = json_extract(?, '$') LIMIT 1",
803
+ params: encodeD1Values([input.row.name])
804
+ };
805
+ const [insertedRows = [], canonicalRows = []] = await executor.batch([insert, select]);
806
+ return {
807
+ row: parseD1Row("channels", canonicalRows[0]),
808
+ inserted: insertedRows.length > 0
809
+ };
810
+ };
811
+ const deleteChannel = async (executor, input) => {
812
+ const [channelRows = [], referenceRows = [], deletedRows = []] = await executor.batch([
813
+ {
814
+ sql: "SELECT id FROM channels WHERE id = json_extract(?, '$') LIMIT 1",
815
+ params: encodeD1Values([input.id])
816
+ },
817
+ {
818
+ sql: "SELECT id FROM releases WHERE channel_id = json_extract(?, '$') LIMIT 1",
819
+ params: encodeD1Values([input.id])
820
+ },
821
+ {
822
+ sql: "DELETE FROM channels WHERE id = json_extract(?, '$') AND NOT EXISTS (SELECT 1 FROM releases WHERE channel_id = json_extract(?, '$')) RETURNING id",
823
+ params: encodeD1Values([input.id, input.id])
824
+ }
825
+ ]);
826
+ if (channelRows.length === 0) return {
827
+ deleted: false,
828
+ reason: "not_found"
829
+ };
830
+ if (referenceRows.length > 0) return {
831
+ deleted: false,
832
+ reason: "not_empty"
833
+ };
834
+ return deletedRows.length > 0 ? { deleted: true } : {
835
+ deleted: false,
836
+ reason: "not_empty"
837
+ };
838
+ };
839
+ const createD1Implementation = (executor) => ({
840
+ async create(input) {
841
+ const query = insertQuery(input);
842
+ const rows = await executor.query(query.sql, query.params);
843
+ switch (input.model) {
844
+ case "bundles": return parseD1Row("bundles", rows[0]);
845
+ case "bundle_patches": return parseD1Row("bundle_patches", rows[0]);
846
+ case "channels": return parseD1Row("channels", rows[0]);
847
+ case "bundle_events": return parseD1Row("bundle_events", rows[0]);
848
+ case "api_keys": return parseD1Row("api_keys", rows[0]);
849
+ case "releases": return parseD1Row("releases", rows[0]);
850
+ case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
851
+ }
852
+ },
853
+ async update(input) {
854
+ const query = updateQuery(input);
855
+ const rows = await executor.query(query.sql, query.params);
856
+ if (rows[0] === void 0) return null;
857
+ switch (input.model) {
858
+ case "bundles": return parseD1Row("bundles", rows[0]);
859
+ case "api_keys": return parseD1Row("api_keys", rows[0]);
860
+ case "releases": return parseD1Row("releases", rows[0]);
861
+ case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
862
+ }
863
+ },
864
+ async delete(input) {
865
+ const query = deleteQuery(input);
866
+ await executor.query(query.sql, query.params);
867
+ },
868
+ count: (input) => countD1Rows(executor, input),
869
+ async findOne(input) {
870
+ const where = buildD1Where(input.where);
871
+ const rows = await executor.query(`SELECT * FROM ${d1TableNames[input.model]}${where.sql} LIMIT 1`, where.params);
872
+ if (rows[0] === void 0) return null;
873
+ switch (input.model) {
874
+ case "bundles": return parseD1Row("bundles", rows[0]);
875
+ case "bundle_patches": return parseD1Row("bundle_patches", rows[0]);
876
+ case "channels": return parseD1Row("channels", rows[0]);
877
+ case "api_keys": return parseD1Row("api_keys", rows[0]);
878
+ case "releases": return parseD1Row("releases", rows[0]);
879
+ case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
880
+ }
881
+ },
882
+ findMany: (input) => findManyD1Rows(executor, input),
883
+ insertChannel: (input) => insertChannel(executor, input),
884
+ deleteChannel: (input) => deleteChannel(executor, input),
885
+ async commit(input) {
886
+ if (input.changes.length === 0) return { committed: true };
887
+ const expectations = input.expectations ?? [];
888
+ const conflict = await expectationConflict(executor, expectations);
889
+ if (conflict !== null) return conflict;
890
+ const plan = createCommitPlan(input);
891
+ try {
892
+ return resultForPlan(plan, await executor.batch(plan.statements));
893
+ } catch (error) {
894
+ if (expectations.length === 0 || !isExpectationConflictError(error)) throw error;
895
+ return await expectationConflict(executor, expectations) ?? {
896
+ committed: false,
897
+ conflict: {
898
+ actualVersion: expectations[0].model === "releases" ? expectations[0].revision : expectations[0].generation,
899
+ changeIndex: -1,
900
+ expectedVersion: expectations[0].model === "releases" ? expectations[0].revision : expectations[0].generation,
901
+ key: expectations[0].model === "releases" ? expectations[0].id : expectations[0].scopeKey,
902
+ model: expectations[0].model,
903
+ reason: "version_conflict"
904
+ }
905
+ };
906
+ }
907
+ }
908
+ });
909
+ //#endregion
910
+ //#region src/d1Database.ts
911
+ const d1Database = (config) => {
912
+ const cloudflare$1 = new cloudflare.default({ apiToken: config.cloudflareApiToken });
913
+ const execute = async (statements) => {
914
+ const first = statements[0];
915
+ if (first === void 0) return [];
916
+ const body = statements.length === 1 ? {
917
+ account_id: config.accountId,
918
+ sql: first.sql,
919
+ params: [...first.params]
920
+ } : {
921
+ account_id: config.accountId,
922
+ batch: statements.map(({ sql, params }) => ({
923
+ sql,
924
+ params: [...params]
925
+ }))
926
+ };
927
+ const page = await cloudflare$1.d1.database.query(config.databaseId, { ...body });
928
+ const results = [];
929
+ for await (const resultPage of page.iterPages()) for (const result of resultPage.result) results.push(result.results ?? []);
930
+ return results;
931
+ };
932
+ const adapter = (0, _hot_updater_plugin_core_internal.createDatabasePluginAdapter)("d1Database", createD1Implementation({
933
+ async query(sql, params) {
934
+ return (await execute([{
935
+ sql,
936
+ params
937
+ }])).flat();
938
+ },
939
+ batch: execute
940
+ }));
941
+ return (0, _hot_updater_plugin_core.createDatabasePlugin)({
942
+ name: "d1Database",
943
+ models: adapter.models,
944
+ commit: adapter.commit,
945
+ ...adapter.dispose ? { dispose: adapter.dispose } : {}
946
+ });
947
+ };
948
+ //#endregion
56
949
  //#region ../../node_modules/.pnpm/is-plain-obj@4.1.0/node_modules/is-plain-obj/index.js
57
950
  function isPlainObject(value) {
58
951
  if (typeof value !== "object" || value === null) return false;
@@ -11033,6 +11926,26 @@ const resolveCloudflareInfrastructureApiToken = async ({ cwd, nonInteractive })
11033
11926
  return authenticatedToken;
11034
11927
  };
11035
11928
  //#endregion
11929
+ //#region iac/cloudflareInfrastructureState.ts
11930
+ const resolveCloudflareInfrastructureState = (tableNames) => {
11931
+ if (tableNames.includes("release_catalogs")) return "v1";
11932
+ if (tableNames.includes("bundles")) return "v0";
11933
+ return "fresh";
11934
+ };
11935
+ const assertCloudflareInfrastructureCanInitialize = (tableNames, databaseName) => {
11936
+ if (resolveCloudflareInfrastructureState(tableNames) === "v0") throw new _hot_updater_cli_tools.LegacyInfrastructureError("Cloudflare", `D1 database ${databaseName}`);
11937
+ };
11938
+ const assertCloudflareWorkerCanInitialize = async ({ fetchImpl, scriptNames, workerName, workersSubdomain }) => {
11939
+ if (!scriptNames.includes(workerName)) return;
11940
+ await (0, _hot_updater_cli_tools.assertInfrastructureGenerationAtUrl)({
11941
+ fetchImpl,
11942
+ legacyStatuses: [400, 404],
11943
+ provider: "Cloudflare",
11944
+ resource: `Worker ${workerName}`,
11945
+ versionUrl: `https://${workerName}.${workersSubdomain}.workers.dev/version`
11946
+ });
11947
+ };
11948
+ //#endregion
11036
11949
  //#region iac/init/index.ts
11037
11950
  const initProvider = {
11038
11951
  label: "Cloudflare D1 + R2 + Worker",
@@ -11236,10 +12149,16 @@ function App() {
11236
12149
  return null; // Replace with your app root
11237
12150
  }
11238
12151
 
11239
- export default HotUpdater.wrap({
12152
+ HotUpdater.init({
11240
12153
  baseURL: "%%source%%",
11241
- updateStrategy: "appVersion", // or "fingerprint"
11242
- })(App);`;
12154
+ requestHeaders: {
12155
+ "x-api-key": %%apiKey%%,
12156
+ },
12157
+ });
12158
+
12159
+ // Call HotUpdater.checkForUpdate({ updateStrategy: "appVersion" })
12160
+ // when your app is ready to check.
12161
+ export default App;`;
11243
12162
  const deployWorker = async (apiToken, accountId, { credentialSource, d1DatabaseId, d1DatabaseName, nonInteractive, r2BucketName, workerName }) => {
11244
12163
  const cwd = (0, _hot_updater_cli_tools.getCwd)();
11245
12164
  const cloudflarePackagePath = require.resolve("@hot-updater/cloudflare/package.json", { paths: [cwd] });
@@ -11256,7 +12175,7 @@ const deployWorker = async (apiToken, accountId, { credentialSource, d1DatabaseI
11256
12175
  binding: "BUCKET",
11257
12176
  bucket_name: r2BucketName
11258
12177
  }];
11259
- wranglerConfig.vars = { JWT_SECRET: crypto.default.randomBytes(32).toString("hex") };
12178
+ wranglerConfig.vars = { BUCKET_NAME: r2BucketName };
11260
12179
  await fs_promises.default.writeFile(path.default.join(workerRoot, "wrangler.json"), JSON.stringify(wranglerConfig, null, 2));
11261
12180
  const wrangler = await createWrangler({
11262
12181
  stdio: "inherit",
@@ -11274,6 +12193,16 @@ const deployWorker = async (apiToken, accountId, { credentialSource, d1DatabaseI
11274
12193
  }
11275
12194
  await wrangler("d1", "migrations", "apply", d1DatabaseName, "--remote");
11276
12195
  await wrangler("deploy", "--name", workerName);
12196
+ const secretCommand = createWrangler({
12197
+ stdio: "pipe",
12198
+ cloudflareApiToken: apiToken,
12199
+ cwd: workerRoot,
12200
+ accountId,
12201
+ nonInteractive
12202
+ })("secret", "put", "STORAGE_DOWNLOAD_URL_SIGNING_KEY", "--name", workerName);
12203
+ if (!secretCommand.stdin) throw new Error("Failed to open Wrangler secret input.");
12204
+ secretCommand.stdin.end(crypto.default.randomBytes(32).toString("hex"));
12205
+ await secretCommand;
11277
12206
  return workerName;
11278
12207
  } catch (error) {
11279
12208
  if (error instanceof Error) throw toCloudflareDeploymentError(error, credentialSource);
@@ -11287,7 +12216,8 @@ const runInit = async ({ build, envFile }) => {
11287
12216
  const nonInteractive = envFile !== void 0;
11288
12217
  const initEnvSources = await (0, _hot_updater_cli_tools.readHotUpdaterInitEnv)(cwd, envFile);
11289
12218
  const { managedEnv } = initEnvSources;
11290
- const existingInputs = resolveCloudflareInitInputs((0, _hot_updater_cli_tools.getHotUpdaterInitInputEnv)(initEnvSources, nonInteractive));
12219
+ const initInputEnv = (0, _hot_updater_cli_tools.getHotUpdaterInitInputEnv)(initEnvSources, nonInteractive);
12220
+ const existingInputs = resolveCloudflareInitInputs(initInputEnv);
11291
12221
  assertCloudflareNonInteractiveInputs(existingInputs, nonInteractive);
11292
12222
  const { accessKeyId: existingR2AccessKeyId, accountId: existingAccountId, apiToken: existingApiToken, bucketName: existingBucketName, d1DatabaseId: existingD1DatabaseId, d1DatabaseName: existingD1DatabaseName, r2Private: savedPrivateSetting, secretAccessKey: existingR2SecretAccessKey, workerName: existingWorkerName } = existingInputs;
11293
12223
  const infrastructureCredentialSource = { kind: "wrangler-oauth" };
@@ -11492,6 +12422,37 @@ const runInit = async ({ build, envFile }) => {
11492
12422
  verifyUserToken: () => credentialClient.user.tokens.verify()
11493
12423
  })
11494
12424
  });
12425
+ const queryD1 = async (sql) => {
12426
+ if (!selectedD1DatabaseId) return [];
12427
+ const page = await cf.d1.database.query(selectedD1DatabaseId, {
12428
+ account_id: accountId,
12429
+ sql
12430
+ });
12431
+ const rows = [];
12432
+ for await (const resultPage of page.iterPages()) for (const result of resultPage.result) rows.push(...result.results ?? []);
12433
+ return rows;
12434
+ };
12435
+ if (!createD1Database) assertCloudflareInfrastructureCanInitialize((await queryD1(`
12436
+ SELECT name
12437
+ FROM sqlite_schema
12438
+ WHERE type = 'table'
12439
+ AND name IN ('bundles', 'release_catalogs')
12440
+ ORDER BY name
12441
+ `)).flatMap(({ name }) => typeof name === "string" ? [name] : []), d1DatabaseName);
12442
+ const workerScripts = await runCloudflareApiRequest({
12443
+ request: () => cf.workers.scripts.list({ account_id: accountId }),
12444
+ source: infrastructureCredentialSource
12445
+ });
12446
+ const subdomains = await runCloudflareApiRequest({
12447
+ request: () => cf.workers.subdomains.get({ account_id: accountId }),
12448
+ source: infrastructureCredentialSource
12449
+ });
12450
+ if (!subdomains.subdomain) throw new Error("Cloudflare Workers subdomain is required to configure the storage download URL.");
12451
+ await assertCloudflareWorkerCanInitialize({
12452
+ scriptNames: workerScripts.result.flatMap(({ id }) => typeof id === "string" ? [id] : []),
12453
+ workerName,
12454
+ workersSubdomain: subdomains.subdomain
12455
+ });
11495
12456
  const resolvedInputs = {
11496
12457
  ...existingInputs,
11497
12458
  accessKeyId,
@@ -11563,10 +12524,6 @@ const runInit = async ({ build, envFile }) => {
11563
12524
  [initProvider.inputs.d1DatabaseId.envKey]: selectedD1DatabaseId,
11564
12525
  [initProvider.inputs.d1DatabaseName.envKey]: d1DatabaseName
11565
12526
  });
11566
- const subdomains = await runCloudflareApiRequest({
11567
- request: () => cf.workers.subdomains.get({ account_id: accountId }),
11568
- source: infrastructureCredentialSource
11569
- });
11570
12527
  await deployWorker(infrastructureApiToken, accountId, {
11571
12528
  credentialSource: infrastructureCredentialSource,
11572
12529
  d1DatabaseId: selectedD1DatabaseId,
@@ -11575,12 +12532,33 @@ const runInit = async ({ build, envFile }) => {
11575
12532
  r2BucketName: selectedBucketName,
11576
12533
  workerName
11577
12534
  });
12535
+ const databasePlugin = d1Database({
12536
+ accountId,
12537
+ cloudflareApiToken: apiToken,
12538
+ databaseId: selectedD1DatabaseId
12539
+ });
12540
+ let apiKey;
12541
+ try {
12542
+ apiKey = (await (0, _hot_updater_server.provisionApiKey)({
12543
+ apiKeys: databasePlugin.models.apiKeys,
12544
+ existingApiKey: initInputEnv.HOT_UPDATER_API_KEY,
12545
+ name: "Cloudflare init"
12546
+ })).apiKey;
12547
+ await (0, _hot_updater_cli_tools.makeEnv)({ HOT_UPDATER_API_KEY: apiKey });
12548
+ } finally {
12549
+ await databasePlugin.dispose?.();
12550
+ }
11578
12551
  const configWriteResult = await (0, _hot_updater_cli_tools.writeHotUpdaterConfig)(getConfigScaffold(build));
11579
12552
  _hot_updater_cli_tools.p.log.success("Generated '.env.hotupdater' file with Cloudflare settings.");
11580
12553
  if (configWriteResult.status === "created") _hot_updater_cli_tools.p.log.success("Generated 'hot-updater.config.ts' file with Cloudflare settings.");
11581
12554
  else if (configWriteResult.status === "merged") _hot_updater_cli_tools.p.log.success("Updated 'hot-updater.config.ts' file with Cloudflare settings.");
11582
12555
  else _hot_updater_cli_tools.p.log.warn(`Kept existing 'hot-updater.config.ts' unchanged: ${configWriteResult.reason}`);
11583
- if (subdomains.subdomain) _hot_updater_cli_tools.p.note((0, _hot_updater_cli_tools.transformTemplate)(SOURCE_TEMPLATE, { source: `https://${workerName}.${subdomains.subdomain}.workers.dev/api/check-update` }));
12556
+ if (subdomains.subdomain) _hot_updater_cli_tools.p.note((0, _hot_updater_cli_tools.transformTemplate)(SOURCE_TEMPLATE, {
12557
+ apiKey: JSON.stringify(apiKey),
12558
+ source: `https://${workerName}.${subdomains.subdomain}.workers.dev`
12559
+ }));
12560
+ _hot_updater_cli_tools.p.note((0, _hot_updater_cli_tools.formatApiKeyNote)(apiKey), "API Key");
12561
+ _hot_updater_cli_tools.p.log.message("Store this API key separately in a secure place.");
11584
12562
  _hot_updater_cli_tools.p.log.message(`Next step: ${(0, _hot_updater_cli_tools.link)("https://hot-updater.dev/docs/managed/cloudflare#step-4-add-hotupdater-to-your-project")}`);
11585
12563
  _hot_updater_cli_tools.p.log.success("Done! 🎉");
11586
12564
  };