alchemy 0.57.2 → 0.59.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 (136) hide show
  1. package/bin/alchemy.js +1 -1
  2. package/lib/apply.js +3 -3
  3. package/lib/apply.js.map +1 -1
  4. package/lib/cloudflare/api-error.js +1 -1
  5. package/lib/cloudflare/api-error.js.map +1 -1
  6. package/lib/cloudflare/api-response.d.ts +19 -0
  7. package/lib/cloudflare/api-response.d.ts.map +1 -1
  8. package/lib/cloudflare/api-response.js.map +1 -1
  9. package/lib/cloudflare/api.d.ts.map +1 -1
  10. package/lib/cloudflare/api.js +10 -2
  11. package/lib/cloudflare/api.js.map +1 -1
  12. package/lib/cloudflare/assets.d.ts +1 -1
  13. package/lib/cloudflare/assets.d.ts.map +1 -1
  14. package/lib/cloudflare/bucket.d.ts +21 -3
  15. package/lib/cloudflare/bucket.d.ts.map +1 -1
  16. package/lib/cloudflare/bucket.js +83 -53
  17. package/lib/cloudflare/bucket.js.map +1 -1
  18. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  19. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  20. package/lib/cloudflare/container.d.ts +8 -0
  21. package/lib/cloudflare/container.d.ts.map +1 -1
  22. package/lib/cloudflare/container.js +7 -1
  23. package/lib/cloudflare/container.js.map +1 -1
  24. package/lib/cloudflare/custom-domain.d.ts +8 -0
  25. package/lib/cloudflare/custom-domain.d.ts.map +1 -1
  26. package/lib/cloudflare/custom-domain.js +11 -0
  27. package/lib/cloudflare/custom-domain.js.map +1 -1
  28. package/lib/cloudflare/d1-database.d.ts +15 -1
  29. package/lib/cloudflare/d1-database.d.ts.map +1 -1
  30. package/lib/cloudflare/d1-database.js +24 -17
  31. package/lib/cloudflare/d1-database.js.map +1 -1
  32. package/lib/cloudflare/d1-import.d.ts.map +1 -1
  33. package/lib/cloudflare/d1-import.js.map +1 -1
  34. package/lib/cloudflare/hyperdrive.d.ts +37 -9
  35. package/lib/cloudflare/hyperdrive.d.ts.map +1 -1
  36. package/lib/cloudflare/hyperdrive.js +91 -5
  37. package/lib/cloudflare/hyperdrive.js.map +1 -1
  38. package/lib/cloudflare/index.d.ts +1 -0
  39. package/lib/cloudflare/index.d.ts.map +1 -1
  40. package/lib/cloudflare/index.js +1 -0
  41. package/lib/cloudflare/index.js.map +1 -1
  42. package/lib/cloudflare/kv-namespace.d.ts +23 -4
  43. package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
  44. package/lib/cloudflare/kv-namespace.js +70 -58
  45. package/lib/cloudflare/kv-namespace.js.map +1 -1
  46. package/lib/cloudflare/miniflare/build-worker-options.d.ts +2 -0
  47. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  48. package/lib/cloudflare/miniflare/build-worker-options.js +9 -7
  49. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  50. package/lib/cloudflare/miniflare/delete.d.ts +21 -0
  51. package/lib/cloudflare/miniflare/delete.d.ts.map +1 -0
  52. package/lib/cloudflare/miniflare/delete.js +91 -0
  53. package/lib/cloudflare/miniflare/delete.js.map +1 -0
  54. package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
  55. package/lib/cloudflare/miniflare/miniflare-controller.js +26 -9
  56. package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
  57. package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts +2 -0
  58. package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
  59. package/lib/cloudflare/miniflare/remote-binding-proxy.js +3 -4
  60. package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
  61. package/lib/cloudflare/pipeline.d.ts +3 -0
  62. package/lib/cloudflare/pipeline.d.ts.map +1 -1
  63. package/lib/cloudflare/pipeline.js +14 -1
  64. package/lib/cloudflare/pipeline.js.map +1 -1
  65. package/lib/cloudflare/queue-consumer.d.ts +8 -0
  66. package/lib/cloudflare/queue-consumer.d.ts.map +1 -1
  67. package/lib/cloudflare/queue-consumer.js +15 -4
  68. package/lib/cloudflare/queue-consumer.js.map +1 -1
  69. package/lib/cloudflare/queue.d.ts +22 -4
  70. package/lib/cloudflare/queue.d.ts.map +1 -1
  71. package/lib/cloudflare/queue.js +32 -9
  72. package/lib/cloudflare/queue.js.map +1 -1
  73. package/lib/cloudflare/route.d.ts +8 -0
  74. package/lib/cloudflare/route.d.ts.map +1 -1
  75. package/lib/cloudflare/route.js +13 -0
  76. package/lib/cloudflare/route.js.map +1 -1
  77. package/lib/cloudflare/tunnel.d.ts +448 -0
  78. package/lib/cloudflare/tunnel.d.ts.map +1 -0
  79. package/lib/cloudflare/tunnel.js +467 -0
  80. package/lib/cloudflare/tunnel.js.map +1 -0
  81. package/lib/cloudflare/worker-subdomain.d.ts +8 -0
  82. package/lib/cloudflare/worker-subdomain.d.ts.map +1 -1
  83. package/lib/cloudflare/worker-subdomain.js +9 -1
  84. package/lib/cloudflare/worker-subdomain.js.map +1 -1
  85. package/lib/cloudflare/worker.d.ts +7 -1
  86. package/lib/cloudflare/worker.d.ts.map +1 -1
  87. package/lib/cloudflare/worker.js +405 -375
  88. package/lib/cloudflare/worker.js.map +1 -1
  89. package/lib/cloudflare/wrangler.json.d.ts +1 -1
  90. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  91. package/lib/cloudflare/wrangler.json.js +24 -12
  92. package/lib/cloudflare/wrangler.json.js.map +1 -1
  93. package/lib/destroy.d.ts.map +1 -1
  94. package/lib/destroy.js +4 -8
  95. package/lib/destroy.js.map +1 -1
  96. package/lib/esbuild/bundle.d.ts +1 -1
  97. package/lib/secret.d.ts +18 -1
  98. package/lib/secret.d.ts.map +1 -1
  99. package/lib/secret.js +25 -0
  100. package/lib/secret.js.map +1 -1
  101. package/package.json +1 -1
  102. package/src/apply.ts +5 -6
  103. package/src/cloudflare/api-error.ts +2 -2
  104. package/src/cloudflare/api-response.ts +22 -0
  105. package/src/cloudflare/api.ts +13 -2
  106. package/src/cloudflare/bucket.ts +126 -68
  107. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  108. package/src/cloudflare/container.ts +18 -1
  109. package/src/cloudflare/custom-domain.ts +22 -0
  110. package/src/cloudflare/d1-database.ts +44 -19
  111. package/src/cloudflare/d1-import.ts +1 -14
  112. package/src/cloudflare/hyperdrive.ts +170 -14
  113. package/src/cloudflare/index.ts +1 -0
  114. package/src/cloudflare/kv-namespace.ts +117 -73
  115. package/src/cloudflare/miniflare/build-worker-options.ts +12 -8
  116. package/src/cloudflare/miniflare/delete.ts +136 -0
  117. package/src/cloudflare/miniflare/miniflare-controller.ts +26 -9
  118. package/src/cloudflare/miniflare/remote-binding-proxy.ts +5 -4
  119. package/src/cloudflare/pipeline.ts +17 -1
  120. package/src/cloudflare/queue-consumer.ts +27 -5
  121. package/src/cloudflare/queue.ts +62 -12
  122. package/src/cloudflare/route.ts +24 -0
  123. package/src/cloudflare/tunnel.ts +911 -0
  124. package/src/cloudflare/worker-subdomain.ts +29 -6
  125. package/src/cloudflare/worker.ts +484 -465
  126. package/src/cloudflare/wrangler.json.ts +34 -13
  127. package/src/destroy.ts +4 -8
  128. package/src/secret.ts +30 -1
  129. package/templates/astro/README.md +2 -0
  130. package/templates/nuxt/README.md +2 -0
  131. package/templates/react-router/README.md +2 -0
  132. package/templates/rwsdk/README.md +2 -0
  133. package/templates/sveltekit/README.md +2 -0
  134. package/templates/tanstack-start/README.md +2 -0
  135. package/templates/typescript/README.md +2 -0
  136. package/templates/vite/README.md +2 -0
@@ -1,6 +1,8 @@
1
+ import { alchemy } from "../alchemy.ts";
1
2
  import type { Context } from "../context.ts";
2
3
  import { Resource } from "../resource.ts";
3
- import type { Secret } from "../secret.ts";
4
+ import { Scope } from "../scope.ts";
5
+ import { Secret } from "../secret.ts";
4
6
  import { logger } from "../util/logger.ts";
5
7
  import { handleApiError } from "./api-error.ts";
6
8
  import { createCloudflareApi, type CloudflareApiOptions } from "./api.ts";
@@ -8,7 +10,7 @@ import { createCloudflareApi, type CloudflareApiOptions } from "./api.ts";
8
10
  /**
9
11
  * Origin configuration for a PostgreSQL or MySQL database connection
10
12
  */
11
- export interface HyperdriveOrigin {
13
+ export interface HyperdrivePublicOrigin {
12
14
  /**
13
15
  * Database name
14
16
  */
@@ -23,7 +25,7 @@ export interface HyperdriveOrigin {
23
25
  * Database password
24
26
  * Use alchemy.secret() to securely store this value
25
27
  */
26
- password: Secret;
28
+ password: string | Secret;
27
29
 
28
30
  /**
29
31
  * Database port
@@ -56,7 +58,7 @@ export interface HyperdriveOriginWithAccess {
56
58
  * Access client secret
57
59
  * Use alchemy.secret() to securely store this value
58
60
  */
59
- access_client_secret: Secret;
61
+ access_client_secret: string | Secret;
60
62
 
61
63
  /**
62
64
  * Database host
@@ -118,6 +120,16 @@ export interface HyperdriveMtls {
118
120
  sslmode?: "verify-ca" | "verify-full";
119
121
  }
120
122
 
123
+ export type HyperdriveOriginInput =
124
+ | string
125
+ | Secret
126
+ | HyperdrivePublicOrigin
127
+ | HyperdriveOriginWithAccess;
128
+
129
+ export type HyperdriveOrigin =
130
+ | HyperdrivePublicOrigin
131
+ | HyperdriveOriginWithAccess;
132
+
121
133
  /**
122
134
  * Properties for creating or updating a Cloudflare Hyperdrive.
123
135
  */
@@ -130,7 +142,7 @@ export interface HyperdriveProps extends CloudflareApiOptions {
130
142
  /**
131
143
  * Database connection origin configuration
132
144
  */
133
- origin: HyperdriveOrigin | HyperdriveOriginWithAccess;
145
+ origin: HyperdriveOriginInput;
134
146
 
135
147
  /**
136
148
  * Caching configuration
@@ -148,6 +160,14 @@ export interface HyperdriveProps extends CloudflareApiOptions {
148
160
  * @internal
149
161
  */
150
162
  hyperdriveId?: string;
163
+
164
+ dev?: {
165
+ /**
166
+ * The database connection origin configuration for local development
167
+ * @default origin
168
+ */
169
+ origin?: HyperdriveOriginInput;
170
+ };
151
171
  }
152
172
 
153
173
  /**
@@ -156,7 +176,7 @@ export interface HyperdriveProps extends CloudflareApiOptions {
156
176
  */
157
177
  export interface Hyperdrive
158
178
  extends Resource<"cloudflare::Hyperdrive">,
159
- Omit<HyperdriveProps, "origin"> {
179
+ Omit<HyperdriveProps, "origin" | "dev"> {
160
180
  /**
161
181
  * The ID of the resource
162
182
  */
@@ -170,7 +190,18 @@ export interface Hyperdrive
170
190
  /**
171
191
  * Database connection origin configuration
172
192
  */
173
- origin: HyperdriveOrigin | HyperdriveOriginWithAccess;
193
+ origin: HyperdrivePublicOrigin | HyperdriveOriginWithAccess;
194
+
195
+ /**
196
+ * Local development configuration
197
+ * @internal
198
+ */
199
+ dev: {
200
+ /**
201
+ * The connection string to use for local development
202
+ */
203
+ origin: Secret;
204
+ };
174
205
 
175
206
  /**
176
207
  * Resource type identifier for binding.
@@ -257,19 +288,66 @@ export interface Hyperdrive
257
288
  * }
258
289
  * });
259
290
  */
260
- export const Hyperdrive = Resource(
291
+ export async function Hyperdrive(
292
+ id: string,
293
+ props: HyperdriveProps,
294
+ ): Promise<Hyperdrive> {
295
+ const origin = normalizeHyperdriveOrigin(props.origin);
296
+ const dev = {
297
+ origin: toConnectionString(
298
+ normalizeHyperdriveOrigin(props.dev?.origin ?? origin),
299
+ ),
300
+ force: Scope.current.local,
301
+ };
302
+ return await _Hyperdrive(id, {
303
+ ...props,
304
+ origin,
305
+ dev,
306
+ });
307
+ }
308
+
309
+ /**
310
+ * Internal properties for creating or updating a Cloudflare Hyperdrive config.
311
+ * @internal
312
+ */
313
+ interface InternalHyperdriveProps extends CloudflareApiOptions {
314
+ name: string;
315
+ hyperdriveId?: string;
316
+ origin: HyperdriveOrigin;
317
+ caching?: HyperdriveCaching;
318
+ mtls?: HyperdriveMtls;
319
+ dev: {
320
+ origin: Secret;
321
+ force?: boolean;
322
+ };
323
+ }
324
+
325
+ const _Hyperdrive = Resource(
261
326
  "cloudflare::Hyperdrive",
262
327
  async function (
263
328
  this: Context<Hyperdrive>,
264
329
  id: string,
265
- props: HyperdriveProps,
330
+ props: InternalHyperdriveProps,
266
331
  ): Promise<Hyperdrive> {
332
+ const hyperdriveId = props.hyperdriveId || this.output?.hyperdriveId;
333
+
334
+ if (this.scope.local) {
335
+ return this({
336
+ id,
337
+ hyperdriveId: hyperdriveId || "",
338
+ name: props.name,
339
+ origin: props.origin,
340
+ caching: props.caching,
341
+ mtls: props.mtls,
342
+ dev: props.dev,
343
+ type: "hyperdrive",
344
+ });
345
+ }
267
346
  const api = await createCloudflareApi(props);
268
347
  const configsPath = `/accounts/${api.accountId}/hyperdrive/configs`;
269
348
 
270
349
  // For create operations, we don't have a hyperdriveId yet
271
350
  // For update/delete operations, we need to use the hyperdriveId from props or output
272
- const hyperdriveId = props.hyperdriveId || this.output?.hyperdriveId;
273
351
  const configPath = hyperdriveId
274
352
  ? `${configsPath}/${hyperdriveId}`
275
353
  : `${configsPath}`;
@@ -353,9 +431,10 @@ export const Hyperdrive = Resource(
353
431
  id,
354
432
  hyperdriveId: apiResource.id, // Store the Cloudflare-assigned UUID
355
433
  name: apiResource.name,
356
- origin: props.origin, // Keep the original origin with secrets
434
+ origin: props.origin,
357
435
  caching: apiResource.caching,
358
436
  mtls: apiResource.mtls,
437
+ dev: props.dev,
359
438
  type: "hyperdrive",
360
439
  });
361
440
  },
@@ -364,7 +443,7 @@ export const Hyperdrive = Resource(
364
443
  /**
365
444
  * Prepare the request body by unwrapping secret values
366
445
  */
367
- function prepareRequestBody(props: HyperdriveProps): any {
446
+ function prepareRequestBody(props: InternalHyperdriveProps): any {
368
447
  const requestBody: any = { ...props };
369
448
 
370
449
  // Remove internal props
@@ -375,16 +454,93 @@ function prepareRequestBody(props: HyperdriveProps): any {
375
454
  // Regular origin with password
376
455
  requestBody.origin = {
377
456
  ...props.origin,
378
- password: props.origin.password.unencrypted,
457
+ password: Secret.unwrap(props.origin.password),
379
458
  scheme: props.origin.scheme ?? "postgres",
380
459
  };
381
460
  } else if ("access_client_secret" in props.origin) {
382
461
  // Origin with access client secret
383
462
  requestBody.origin = {
384
463
  ...props.origin,
385
- access_client_secret: props.origin.access_client_secret.unencrypted,
464
+ access_client_secret: Secret.unwrap(props.origin.access_client_secret),
386
465
  };
387
466
  }
388
467
 
389
468
  return requestBody;
390
469
  }
470
+
471
+ /**
472
+ * Converts a HyperdriveOriginInput to a HyperdriveOrigin.
473
+ * This includes:
474
+ * - parsing the origin from a string
475
+ * - ensuring the scheme is "postgres" or "mysql"
476
+ * - normalizing the port to a number with default values
477
+ * - wrapping secrets in a Secret object
478
+ * @internal - Exported for testing
479
+ */
480
+ export const normalizeHyperdriveOrigin = (
481
+ input: HyperdriveOriginInput,
482
+ ): Required<HyperdrivePublicOrigin> | Required<HyperdriveOriginWithAccess> => {
483
+ const origin = Secret.unwrap(input);
484
+ if (typeof origin === "string") {
485
+ const url = new URL(origin);
486
+ const scheme = normalizeScheme(url.protocol.slice(0, -1));
487
+ return {
488
+ scheme,
489
+ user: url.username,
490
+ password: alchemy.secret(url.password),
491
+ host: url.hostname,
492
+ port: normalizePort(scheme, url.port),
493
+ database: url.pathname.slice(1),
494
+ };
495
+ }
496
+ const scheme = normalizeScheme(origin.scheme);
497
+ return {
498
+ ...origin,
499
+ ...("password" in origin && {
500
+ password: Secret.wrap(origin.password),
501
+ }),
502
+ ...("access_client_secret" in origin && {
503
+ access_client_secret: Secret.wrap(origin.access_client_secret),
504
+ }),
505
+ scheme,
506
+ port: normalizePort(scheme, origin.port),
507
+ };
508
+ };
509
+
510
+ const normalizeScheme = (scheme: string | undefined) => {
511
+ if (!scheme || scheme === "postgres" || scheme === "postgresql") {
512
+ return "postgres";
513
+ }
514
+ if (scheme === "mysql" || scheme === "mysql2") {
515
+ return "mysql";
516
+ }
517
+ throw new Error(
518
+ `Unsupported database connection scheme "${scheme}" for Hyperdrive (expected "postgres" or "mysql")`,
519
+ );
520
+ };
521
+
522
+ const normalizePort = (
523
+ scheme: "postgres" | "mysql",
524
+ port: string | number | undefined,
525
+ ) => {
526
+ if (typeof port === "number") {
527
+ return port;
528
+ }
529
+ if (port) {
530
+ return Number.parseInt(port);
531
+ }
532
+ return scheme === "postgres" ? 5432 : 3306;
533
+ };
534
+
535
+ const toConnectionString = (
536
+ origin:
537
+ | Required<HyperdrivePublicOrigin>
538
+ | Required<HyperdriveOriginWithAccess>,
539
+ ) => {
540
+ const password = Secret.unwrap(
541
+ "password" in origin ? origin.password : origin.access_client_secret,
542
+ );
543
+ return new Secret(
544
+ `${origin.scheme}://${origin.user}:${password}@${origin.host}:${origin.port}/${origin.database}`,
545
+ );
546
+ };
@@ -50,6 +50,7 @@ export * from "./secrets-store.ts";
50
50
  export * from "./state.ts";
51
51
  export * from "./sveltekit/sveltekit.ts";
52
52
  export * from "./tanstack-start/tanstack-start.ts";
53
+ export * from "./tunnel.ts";
53
54
  export * from "./vectorize-index.ts";
54
55
  export * from "./vectorize-metadata-index.ts";
55
56
  export * from "./version-metadata.ts";
@@ -1,13 +1,18 @@
1
1
  import type { Context } from "../context.ts";
2
2
  import { Resource, ResourceKind } from "../resource.ts";
3
- import { logger } from "../util/logger.ts";
3
+ import { Scope } from "../scope.ts";
4
4
  import { withExponentialBackoff } from "../util/retry.ts";
5
- import { handleApiError } from "./api-error.ts";
5
+ import { CloudflareApiError, handleApiError } from "./api-error.ts";
6
+ import {
7
+ extractCloudflareResult,
8
+ type CloudflareApiErrorPayload,
9
+ } from "./api-response.ts";
6
10
  import {
7
11
  createCloudflareApi,
8
12
  type CloudflareApi,
9
13
  type CloudflareApiOptions,
10
14
  } from "./api.ts";
15
+ import { deleteMiniflareBinding } from "./miniflare/delete.ts";
11
16
 
12
17
  /**
13
18
  * Properties for creating or updating a KV Namespace
@@ -49,6 +54,12 @@ export interface KVNamespaceProps extends CloudflareApiOptions {
49
54
  * @default false
50
55
  */
51
56
  remote?: boolean;
57
+
58
+ /**
59
+ * Whether to force the KV namespace to be created or updated
60
+ * @default false
61
+ */
62
+ force?: boolean;
52
63
  };
53
64
  }
54
65
 
@@ -91,7 +102,7 @@ export function isKVNamespace(resource: Resource): resource is KVNamespace {
91
102
  */
92
103
  export interface KVNamespace
93
104
  extends Resource<"cloudflare::KVNamespace">,
94
- Omit<KVNamespaceProps, "delete"> {
105
+ Omit<KVNamespaceProps, "delete" | "dev"> {
95
106
  type: "kv_namespace";
96
107
  /**
97
108
  * The ID of the namespace
@@ -107,6 +118,22 @@ export interface KVNamespace
107
118
  * Time at which the namespace was last modified
108
119
  */
109
120
  modifiedAt: number;
121
+
122
+ /**
123
+ * Development mode properties
124
+ * @internal
125
+ */
126
+ dev: {
127
+ /**
128
+ * The ID of the KV namespace in development mode
129
+ */
130
+ id: string;
131
+
132
+ /**
133
+ * Whether the KV namespace is running remotely
134
+ */
135
+ remote: boolean;
136
+ };
110
137
  }
111
138
 
112
139
  /**
@@ -163,84 +190,76 @@ export interface KVNamespace
163
190
  * delete: false
164
191
  * });
165
192
  */
166
- export const KVNamespace = Resource(
193
+ export async function KVNamespace(
194
+ id: string,
195
+ props: KVNamespaceProps = {},
196
+ ): Promise<KVNamespace> {
197
+ return await _KVNamespace(id, {
198
+ ...props,
199
+ dev: {
200
+ ...(props.dev ?? {}),
201
+ force: Scope.current.local,
202
+ },
203
+ });
204
+ }
205
+
206
+ const _KVNamespace = Resource(
167
207
  "cloudflare::KVNamespace",
168
208
  async function (
169
209
  this: Context<KVNamespace>,
170
210
  id: string,
171
211
  props: KVNamespaceProps,
172
212
  ): Promise<KVNamespace> {
173
- // Create Cloudflare API client with automatic account discovery
174
- const api = await createCloudflareApi(props);
175
-
176
213
  const title = props.title ?? id;
214
+ const local = this.scope.local && !props.dev?.remote;
215
+ const dev = {
216
+ id: this.output?.dev?.id ?? this.output?.namespaceId ?? id,
217
+ remote: props.dev?.remote ?? false,
218
+ };
219
+
220
+ if (local) {
221
+ return this({
222
+ type: "kv_namespace",
223
+ namespaceId: this.output?.namespaceId ?? "",
224
+ title,
225
+ values: props.values,
226
+ dev,
227
+ createdAt: this.output?.createdAt ?? Date.now(),
228
+ modifiedAt: Date.now(),
229
+ });
230
+ }
231
+
232
+ const api = await createCloudflareApi(props);
177
233
 
178
234
  if (this.phase === "delete") {
179
- // For delete operations, we need to check if the namespace ID exists in the output
180
- const namespaceId = this.output?.namespaceId;
181
- if (namespaceId && props.delete !== false) {
182
- await deleteKVNamespace(api, namespaceId);
235
+ if (this.output.dev?.id) {
236
+ await deleteMiniflareBinding("kv", this.output.dev.id);
237
+ }
238
+ if (this.output.namespaceId && props.delete !== false) {
239
+ await deleteKVNamespace(api, this.output.namespaceId);
183
240
  }
184
-
185
- // Return minimal output for deleted state
186
241
  return this.destroy();
187
242
  }
188
- // For create or update operations
189
- // If this.phase is "update", we expect this.output to exist
190
- let namespaceId =
191
- this.phase === "update" ? this.output?.namespaceId || "" : "";
192
- let createdAt =
193
- this.phase === "update"
194
- ? this.output?.createdAt || Date.now()
195
- : Date.now();
196
-
197
- if (this.phase === "update" && namespaceId) {
198
- // Can't update a KV namespace title directly, just work with existing ID
199
- } else {
200
- try {
201
- // Try to create the KV namespace
202
- const { id } = await createKVNamespace(api, {
203
- ...props,
204
- title,
205
- });
206
- createdAt = Date.now();
207
- namespaceId = id;
208
- } catch (error) {
209
- // Check if this is a "namespace already exists" error and adopt is enabled
210
- if (
211
- props.adopt &&
212
- error instanceof Error &&
213
- error.message.includes("already exists")
214
- ) {
215
- logger.log(`Namespace '${title}' already exists, adopting it`);
216
- // Find the existing namespace by title
217
- const existingNamespace = await findKVNamespaceByTitle(api, title);
218
-
219
- if (!existingNamespace) {
220
- throw new Error(
221
- `Failed to find existing namespace '${title}' for adoption`,
222
- );
223
- }
224
243
 
225
- // Use the existing namespace ID
226
- namespaceId = existingNamespace.id;
227
- createdAt = existingNamespace.createdAt || Date.now();
228
- } else {
229
- // Re-throw the error if adopt is false or it's not a "namespace already exists" error
230
- throw error;
231
- }
232
- }
244
+ let result: { namespaceId: string; createdAt: number };
245
+ if (this.phase === "create" || !this.output.namespaceId) {
246
+ result = await createKVNamespace(api, {
247
+ ...props,
248
+ title,
249
+ });
250
+ } else {
251
+ result = this.output;
233
252
  }
234
253
 
235
- await insertKVRecords(api, namespaceId, props);
254
+ await insertKVRecords(api, result.namespaceId, props);
236
255
 
237
256
  return this({
238
257
  type: "kv_namespace",
239
- namespaceId,
240
- title: props.title,
258
+ namespaceId: result.namespaceId,
259
+ title,
241
260
  values: props.values,
242
- dev: props.dev,
243
- createdAt: createdAt,
261
+ dev,
262
+ createdAt: result.createdAt,
244
263
  modifiedAt: Date.now(),
245
264
  });
246
265
  },
@@ -251,19 +270,44 @@ export async function createKVNamespace(
251
270
  props: KVNamespaceProps & {
252
271
  title: string;
253
272
  },
254
- ): Promise<{ id: string }> {
255
- const createResponse = await api.post(
256
- `/accounts/${api.accountId}/storage/kv/namespaces`,
257
- {
258
- title: props.title,
259
- },
260
- );
273
+ ): Promise<{ namespaceId: string; createdAt: number }> {
274
+ try {
275
+ const { id } = await extractCloudflareResult<{
276
+ id: string;
277
+ title: string;
278
+ beta?: boolean;
279
+ supports_url_encoding?: boolean;
280
+ }>(
281
+ `create kv namespace "${props.title}"`,
282
+ api.post(`/accounts/${api.accountId}/storage/kv/namespaces`, {
283
+ title: props.title,
284
+ }),
285
+ );
286
+ return { namespaceId: id, createdAt: Date.now() };
287
+ } catch (error) {
288
+ if (
289
+ error instanceof CloudflareApiError &&
290
+ (error.errorData as CloudflareApiErrorPayload[]).some(
291
+ (e) => e.code === 10014,
292
+ ) &&
293
+ props.adopt
294
+ ) {
295
+ const existingNamespace = await findKVNamespaceByTitle(api, props.title);
296
+
297
+ if (!existingNamespace) {
298
+ throw new Error(
299
+ `Failed to find existing namespace '${props.title}' for adoption`,
300
+ );
301
+ }
261
302
 
262
- if (!createResponse.ok) {
263
- await handleApiError(createResponse, "create", "kv_namespace", props.title);
303
+ return {
304
+ namespaceId: existingNamespace.id,
305
+ createdAt: existingNamespace.createdAt ?? Date.now(),
306
+ };
307
+ } else {
308
+ throw error;
309
+ }
264
310
  }
265
-
266
- return { id: ((await createResponse.json()) as any).result.id };
267
311
  }
268
312
 
269
313
  export async function deleteKVNamespace(
@@ -1,6 +1,7 @@
1
1
  import * as miniflare from "miniflare";
2
2
  import { assertNever } from "../../util/assert-never.ts";
3
3
  import type { HTTPServer } from "../../util/http.ts";
4
+ import type { CloudflareApi } from "../api.ts";
4
5
  import {
5
6
  Self,
6
7
  type Binding,
@@ -14,6 +15,7 @@ import type { AssetsConfig } from "../worker.ts";
14
15
  import { createRemoteProxyWorker } from "./remote-binding-proxy.ts";
15
16
 
16
17
  export interface MiniflareWorkerInput {
18
+ api: CloudflareApi;
17
19
  id: string;
18
20
  name: string;
19
21
  compatibilityDate: string | undefined;
@@ -143,7 +145,7 @@ export const buildWorkerOptions = async (
143
145
  raw: true,
144
146
  });
145
147
  } else {
146
- (options.d1Databases ??= {})[key] = binding.id;
148
+ (options.d1Databases ??= {})[key] = binding.dev.id;
147
149
  }
148
150
  break;
149
151
  }
@@ -165,7 +167,8 @@ export const buildWorkerOptions = async (
165
167
  break;
166
168
  }
167
169
  case "hyperdrive": {
168
- throw new Error("Hyperdrive bindings are not supported in local mode");
170
+ (options.hyperdrives ??= {})[key] = binding.dev.origin.unencrypted;
171
+ break;
169
172
  }
170
173
  case "images": {
171
174
  if (isRemoteBinding(binding)) {
@@ -182,17 +185,17 @@ export const buildWorkerOptions = async (
182
185
  break;
183
186
  }
184
187
  case "kv_namespace": {
185
- const namespaceId =
186
- "namespaceId" in binding ? binding.namespaceId : binding.id;
187
188
  if (isRemoteBinding(binding)) {
188
189
  remoteBindings.push({
189
190
  type: "kv_namespace",
190
191
  name: key,
191
- namespace_id: namespaceId,
192
+ namespace_id:
193
+ "namespaceId" in binding ? binding.namespaceId : binding.id,
192
194
  raw: true,
193
195
  });
194
196
  } else {
195
- (options.kvNamespaces ??= {})[key] = namespaceId;
197
+ (options.kvNamespaces ??= {})[key] =
198
+ "dev" in binding ? binding.dev.id : binding.id;
196
199
  }
197
200
  break;
198
201
  }
@@ -217,7 +220,7 @@ export const buildWorkerOptions = async (
217
220
  break;
218
221
  }
219
222
  case "pipeline": {
220
- (options.pipelines ??= {})[key] = binding.id;
223
+ (options.pipelines ??= {})[key] = binding.name;
221
224
  break;
222
225
  }
223
226
  case "ratelimit": {
@@ -249,7 +252,7 @@ export const buildWorkerOptions = async (
249
252
  raw: true,
250
253
  });
251
254
  } else {
252
- (options.r2Buckets ??= {})[key] = binding.name;
255
+ (options.r2Buckets ??= {})[key] = binding.dev.id;
253
256
  }
254
257
  break;
255
258
  }
@@ -329,6 +332,7 @@ export const buildWorkerOptions = async (
329
332
  }
330
333
  if (remoteBindings.length > 0) {
331
334
  const remoteProxy = await createRemoteProxyWorker({
335
+ api: input.api,
332
336
  name: input.name,
333
337
  bindings: remoteBindings,
334
338
  });