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

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