alchemy 0.93.11 → 0.94.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 (83) hide show
  1. package/README.md +1 -1
  2. package/bin/alchemy.js +103 -103
  3. package/lib/aws/control/client.d.ts.map +1 -1
  4. package/lib/aws/control/client.js +2 -1
  5. package/lib/aws/control/client.js.map +1 -1
  6. package/lib/aws/control/resource.d.ts.map +1 -1
  7. package/lib/aws/control/resource.js +46 -5
  8. package/lib/aws/control/resource.js.map +1 -1
  9. package/lib/aws/ec2/utils.d.ts.map +1 -1
  10. package/lib/aws/ec2/utils.js +5 -3
  11. package/lib/aws/ec2/utils.js.map +1 -1
  12. package/lib/aws/utils.d.ts +8 -1
  13. package/lib/aws/utils.d.ts.map +1 -1
  14. package/lib/aws/utils.js +14 -1
  15. package/lib/aws/utils.js.map +1 -1
  16. package/lib/cloudflare/artifacts.d.ts +54 -0
  17. package/lib/cloudflare/artifacts.d.ts.map +1 -0
  18. package/lib/cloudflare/artifacts.js +14 -0
  19. package/lib/cloudflare/artifacts.js.map +1 -0
  20. package/lib/cloudflare/bindings.d.ts +16 -4
  21. package/lib/cloudflare/bindings.d.ts.map +1 -1
  22. package/lib/cloudflare/bindings.js.map +1 -1
  23. package/lib/cloudflare/bound.d.ts +2 -1
  24. package/lib/cloudflare/bound.d.ts.map +1 -1
  25. package/lib/cloudflare/bucket-custom-domain.d.ts.map +1 -1
  26. package/lib/cloudflare/bucket-custom-domain.js +22 -4
  27. package/lib/cloudflare/bucket-custom-domain.js.map +1 -1
  28. package/lib/cloudflare/d1-database.d.ts.map +1 -1
  29. package/lib/cloudflare/d1-database.js +8 -2
  30. package/lib/cloudflare/d1-database.js.map +1 -1
  31. package/lib/cloudflare/hyperdrive.d.ts +8 -0
  32. package/lib/cloudflare/hyperdrive.d.ts.map +1 -1
  33. package/lib/cloudflare/hyperdrive.js +3 -1
  34. package/lib/cloudflare/hyperdrive.js.map +1 -1
  35. package/lib/cloudflare/index.d.ts +1 -0
  36. package/lib/cloudflare/index.d.ts.map +1 -1
  37. package/lib/cloudflare/index.js +1 -0
  38. package/lib/cloudflare/index.js.map +1 -1
  39. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  40. package/lib/cloudflare/miniflare/build-worker-options.js +3 -0
  41. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  42. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
  43. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +3 -1
  44. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
  45. package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
  46. package/lib/cloudflare/miniflare/remote-binding-proxy.js +16 -30
  47. package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
  48. package/lib/cloudflare/worker-metadata.d.ts +11 -0
  49. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  50. package/lib/cloudflare/worker-metadata.js +20 -1
  51. package/lib/cloudflare/worker-metadata.js.map +1 -1
  52. package/lib/cloudflare/worker.d.ts +37 -0
  53. package/lib/cloudflare/worker.d.ts.map +1 -1
  54. package/lib/cloudflare/worker.js +1 -1
  55. package/lib/cloudflare/worker.js.map +1 -1
  56. package/lib/cloudflare/wrangler.json.js +11 -0
  57. package/lib/cloudflare/wrangler.json.js.map +1 -1
  58. package/lib/cloudflare/zone.js +1 -1
  59. package/lib/cloudflare/zone.js.map +1 -1
  60. package/lib/scope.js +2 -2
  61. package/lib/scope.js.map +1 -1
  62. package/package.json +4 -4
  63. package/src/aws/control/client.ts +2 -1
  64. package/src/aws/control/resource.ts +67 -8
  65. package/src/aws/ec2/utils.ts +5 -3
  66. package/src/aws/utils.ts +20 -2
  67. package/src/cloudflare/artifacts.ts +69 -0
  68. package/src/cloudflare/bindings.ts +17 -2
  69. package/src/cloudflare/bound.ts +68 -65
  70. package/src/cloudflare/bucket-custom-domain.ts +33 -7
  71. package/src/cloudflare/d1-database.ts +12 -2
  72. package/src/cloudflare/hyperdrive.ts +13 -1
  73. package/src/cloudflare/index.ts +1 -0
  74. package/src/cloudflare/miniflare/build-worker-options.ts +5 -0
  75. package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +3 -1
  76. package/src/cloudflare/miniflare/remote-binding-proxy.ts +20 -43
  77. package/src/cloudflare/worker-metadata.ts +33 -1
  78. package/src/cloudflare/worker.ts +41 -1
  79. package/src/cloudflare/wrangler.json.ts +19 -0
  80. package/src/cloudflare/zone.ts +1 -1
  81. package/src/scope.ts +2 -2
  82. package/workers/cloudflare-state-store.js +57 -57
  83. package/workers/tunnel-proxy.js +1 -1
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.93.11",
3
+ "version": "0.94.0",
4
4
  "homepage": "https://alchemy.run",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Sam Goodwin <sam@alchemy.run>",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/alchemy-run/alchemy.git",
9
+ "url": "git+https://github.com/alchemy-run/alchemy-async.git",
10
10
  "directory": "alchemy"
11
11
  },
12
12
  "bin": {
@@ -179,7 +179,7 @@
179
179
  "dependencies": {
180
180
  "@aws-sdk/credential-providers": "^3.0.0",
181
181
  "@cloudflare/unenv-preset": "2.7.7",
182
- "@cloudflare/workers-types": "^4.20260602.1",
182
+ "@cloudflare/workers-types": "^5.20260708.1",
183
183
  "@iarna/toml": "^2.2.5",
184
184
  "@octokit/rest": "^21.1.1",
185
185
  "@smithy/node-config-provider": "^4.0.0",
@@ -195,7 +195,7 @@
195
195
  "glob": "^10.0.0",
196
196
  "jszip": "^3.0.0",
197
197
  "libsodium-wrappers": "^0.8.0",
198
- "miniflare": "4.20260424.0",
198
+ "miniflare": "4.20260708.1",
199
199
  "neverthrow": "^8.2.0",
200
200
  "open": "^10.1.2",
201
201
  "openapi-types": "^12.1.3",
@@ -3,6 +3,7 @@ import { loadConfig } from "@smithy/node-config-provider";
3
3
  import { AwsClient } from "aws4fetch";
4
4
  import { logger } from "../../util/logger.ts";
5
5
  import { safeFetch } from "../../util/safe-fetch.ts";
6
+ import { awsEndpoint } from "../utils.ts";
6
7
  import {
7
8
  AlreadyExistsError,
8
9
  CloudControlError,
@@ -355,7 +356,7 @@ export class CloudControlClient {
355
356
  while (true) {
356
357
  try {
357
358
  const args = [
358
- `https://cloudcontrolapi.${this.region}.amazonaws.com/?Action=${action}&Version=2021-09-30`,
359
+ `${awsEndpoint("cloudcontrolapi", this.region)}/?Action=${action}&Version=2021-09-30`,
359
360
  {
360
361
  method: "POST",
361
362
  headers: {
@@ -6,7 +6,11 @@ import {
6
6
  type Provider,
7
7
  } from "../../resource.ts";
8
8
  import { logger } from "../../util/logger.ts";
9
- import { createCloudControlClient, type ProgressEvent } from "./client.ts";
9
+ import {
10
+ createCloudControlClient,
11
+ type CloudControlClient,
12
+ type ProgressEvent,
13
+ } from "./client.ts";
10
14
  import {
11
15
  AlreadyExistsError,
12
16
  ConcurrentOperationError,
@@ -297,8 +301,20 @@ async function CloudControlLifecycle(
297
301
  props.typeName,
298
302
  this.output.id,
299
303
  );
304
+ if (!currentResource) {
305
+ // The physical resource no longer exists (deleted or detached out-of-band),
306
+ // so there is nothing to patch. Falling through to UpdateResource would fail
307
+ // on types with no update support (e.g. AWS::EC2::VolumeAttachment, whose
308
+ // properties are all Immutable). Recreate via replacement instead. Force the
309
+ // replacement (delete-before-create) because the old resource is already gone
310
+ // (delete tolerates NotFoundError) and a deferred delete would destroy the
311
+ // new resource if it reuses the same identifier (e.g. an unchanged BucketName).
312
+ logger.log(
313
+ `Resource ${id} no longer exists (empty read-back on update), requiring replacement`,
314
+ );
315
+ return this.replace(true);
316
+ }
300
317
  if (
301
- currentResource &&
302
318
  hasImmutablePropertyChanges(
303
319
  props.typeName,
304
320
  currentResource,
@@ -328,10 +344,12 @@ async function CloudControlLifecycle(
328
344
  );
329
345
  } catch (error) {
330
346
  if (error instanceof AlreadyExistsError && props.adopt) {
331
- const resource = (await client.getResource(
347
+ const resource = await getResourceForAdoption(
348
+ client,
332
349
  props.typeName,
333
- error.progressEvent.Identifier!,
334
- ))!;
350
+ error.progressEvent.Identifier,
351
+ error,
352
+ );
335
353
 
336
354
  response = await updateResourceWithPatch(
337
355
  client,
@@ -357,10 +375,11 @@ async function CloudControlLifecycle(
357
375
  const concurrentResult = await client.poll(error.requestToken);
358
376
 
359
377
  // The concurrent operation succeeded, now adopt the resource
360
- const resource = (await client.getResource(
378
+ const resource = await getResourceForAdoption(
379
+ client,
361
380
  props.typeName,
362
- concurrentResult.Identifier!,
363
- ))!;
381
+ concurrentResult.Identifier,
382
+ );
364
383
 
365
384
  // Apply our desired state as a patch to the existing resource
366
385
  response = await updateResourceWithPatch(
@@ -401,6 +420,46 @@ async function CloudControlLifecycle(
401
420
  };
402
421
  }
403
422
 
423
+ /**
424
+ * Read back a resource that Cloud Control reported as already existing so it
425
+ * can be adopted. The read-back is retried briefly because some services are
426
+ * eventually consistent between the create conflict and GetResource.
427
+ *
428
+ * If the resource still cannot be read back, adoption is impossible — most
429
+ * commonly the name is taken in a globally shared namespace but not visible
430
+ * in this account (e.g. an S3 bucket owned by another AWS account, or a
431
+ * recently deleted name still held by the service). Cloud Control reports
432
+ * both as AlreadyExists, so fail with an actionable error instead of letting
433
+ * `undefined` flow into the patch computation.
434
+ */
435
+ async function getResourceForAdoption(
436
+ client: CloudControlClient,
437
+ typeName: string,
438
+ identifier: string | undefined,
439
+ cause?: Error,
440
+ ): Promise<Record<string, any>> {
441
+ if (identifier) {
442
+ let delay = 500;
443
+ for (let attempt = 0; attempt < 5; attempt++) {
444
+ if (attempt > 0) {
445
+ await new Promise((resolve) => setTimeout(resolve, delay));
446
+ delay *= 2;
447
+ }
448
+ const resource = await client.getResource(typeName, identifier);
449
+ if (resource !== undefined) {
450
+ return resource;
451
+ }
452
+ }
453
+ }
454
+ throw new Error(
455
+ `Resource ${typeName} "${identifier}" already exists but could not be read back for adoption. ` +
456
+ "It may belong to a different AWS account (e.g. S3 bucket names are globally unique), " +
457
+ "exist in a different region, or its name may still be reserved by a recent deletion. " +
458
+ "Choose a different name or wait for the name to become available.",
459
+ { cause },
460
+ );
461
+ }
462
+
404
463
  async function updateResourceWithPatch(
405
464
  client: any,
406
465
  typeName: string,
@@ -6,7 +6,7 @@ import { logger } from "../../util/logger.ts";
6
6
  import { flattenParams } from "../../util/params.ts";
7
7
  import type { AwsClientProps } from "../client-props.ts";
8
8
  import { resolveAwsCredentials } from "../credentials.ts";
9
- import { getRegion } from "../utils.ts";
9
+ import { awsEndpoint, getRegion } from "../utils.ts";
10
10
 
11
11
  /**
12
12
  * Shared EC2 utilities for all EC2 resources
@@ -100,8 +100,10 @@ export async function callEC2Api<T>(
100
100
  // Try the API call, and retry once with fresh credentials on auth failure
101
101
  for (let attempt = 1; attempt <= 2; attempt++) {
102
102
  try {
103
- // Use the client's region instead of the global region
104
- const url = `https://ec2.${client.region}.amazonaws.com/`;
103
+ // Use the client's region instead of the global region.
104
+ // Honour AWS_ENDPOINT_URL / AWS_ENDPOINT_URL_EC2 for emulators
105
+ // (LocalStack, moto, etc.) — matches AWS SDK v3 behaviour.
106
+ const url = `${awsEndpoint("ec2", client.region ?? "us-east-1")}/`;
105
107
 
106
108
  const body = new URLSearchParams({
107
109
  Action: action,
package/src/aws/utils.ts CHANGED
@@ -14,13 +14,29 @@ export const getRegion: Provider<string> = loadConfig({
14
14
  default: "us-east-1",
15
15
  });
16
16
 
17
+ /**
18
+ * Resolve the endpoint URL for an aws4fetch service request, honouring the
19
+ * same `AWS_ENDPOINT_URL_<SERVICE>` / `AWS_ENDPOINT_URL` env vars as the AWS
20
+ * SDK / CLI so aws4fetch-based resources can be pointed at LocalStack, moto,
21
+ * or any other emulator.
22
+ */
23
+ export function awsEndpoint(service: string, region: string): string {
24
+ return (
25
+ process.env[
26
+ `AWS_ENDPOINT_URL_${service.toUpperCase().replace(/-/g, "_")}`
27
+ ] ??
28
+ process.env.AWS_ENDPOINT_URL ??
29
+ `https://${service}.${region}.amazonaws.com`
30
+ ).replace(/\/+$/, "");
31
+ }
32
+
17
33
  /**
18
34
  * AWS Service Configuration
19
35
  */
20
36
  export interface AwsServiceConfig {
21
37
  service: string;
22
38
  version: string;
23
- endpoint: (region: string) => string;
39
+ endpoint?: (region: string) => string;
24
40
  }
25
41
 
26
42
  /**
@@ -63,7 +79,9 @@ export function getAwsApiCaller<T = any>(
63
79
  for (let attempt = 1; attempt <= 2; attempt++) {
64
80
  try {
65
81
  const region = await getRegion();
66
- const url = config.endpoint(region);
82
+ const url = config.endpoint
83
+ ? config.endpoint(region)
84
+ : `${awsEndpoint(config.service, region)}/`;
67
85
 
68
86
  const body = new URLSearchParams({
69
87
  Action: action,
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Properties for creating an Artifacts binding.
3
+ *
4
+ * Artifacts namespaces are created implicitly by Cloudflare when the first
5
+ * repository is created in the namespace, so this binding does not provision
6
+ * or delete a namespace resource.
7
+ */
8
+ export interface ArtifactsProps {
9
+ /**
10
+ * Artifacts namespace name.
11
+ */
12
+ namespace: string;
13
+
14
+ dev?: {
15
+ /**
16
+ * Whether to run the Artifacts binding remotely in local development.
17
+ *
18
+ * Artifacts do not have a local emulator, so generated Wrangler config
19
+ * defaults this binding to remote mode.
20
+ *
21
+ * @default true
22
+ */
23
+ remote?: boolean;
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Cloudflare Artifacts binding.
29
+ *
30
+ * Artifacts namespaces are implicit containers for repositories. Use this
31
+ * helper to bind a Worker to a namespace; repository lifecycle should be
32
+ * managed through the Artifacts runtime API, REST API, or Git endpoint.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { Artifacts, Worker } from "alchemy/cloudflare";
37
+ *
38
+ * await Worker("site-editor", {
39
+ * entrypoint: "./src/worker.ts",
40
+ * bindings: {
41
+ * SITE_ARTIFACTS: Artifacts({ namespace: "my-sites" }),
42
+ * },
43
+ * });
44
+ * ```
45
+ *
46
+ * @see https://developers.cloudflare.com/artifacts/
47
+ */
48
+ export interface Artifacts {
49
+ type: "artifacts";
50
+ namespace: string;
51
+ dev?: {
52
+ remote?: boolean;
53
+ };
54
+ }
55
+
56
+ export function Artifacts(props: string | ArtifactsProps): Artifacts {
57
+ if (typeof props === "string") {
58
+ return {
59
+ type: "artifacts",
60
+ namespace: props,
61
+ };
62
+ }
63
+
64
+ return {
65
+ type: "artifacts",
66
+ namespace: props.namespace,
67
+ dev: props.dev,
68
+ };
69
+ }
@@ -8,6 +8,7 @@ import type { Ai } from "./ai.ts";
8
8
  import type { AiSearchNamespace } from "./ai-search-namespace.ts";
9
9
  import type { AiSearch } from "./ai-search.ts";
10
10
  import type { AnalyticsEngineDataset } from "./analytics-engine.ts";
11
+ import type { Artifacts } from "./artifacts.ts";
11
12
  import type { Assets } from "./assets.ts";
12
13
  import type { Bound } from "./bound.ts";
13
14
  import type { BrowserRendering } from "./browser-rendering.ts";
@@ -54,6 +55,7 @@ export type Binding =
54
55
  | AiSearch
55
56
  | AiSearchNamespace
56
57
  | Assets
58
+ | Artifacts
57
59
  | Container
58
60
  | CloudflareSecret
59
61
  | CloudflareSecretRef
@@ -127,6 +129,7 @@ export type WorkerBindingSpec =
127
129
  | WorkerBindingAiSearch
128
130
  | WorkerBindingAiSearchNamespace
129
131
  | WorkerBindingAnalyticsEngine
132
+ | WorkerBindingArtifacts
130
133
  | WorkerBindingAssets
131
134
  | WorkerBindingBrowserRendering
132
135
  | WorkerBindingD1
@@ -206,6 +209,18 @@ export interface WorkerBindingAnalyticsEngine {
206
209
  dataset: string;
207
210
  }
208
211
 
212
+ /**
213
+ * Artifacts binding type
214
+ */
215
+ export interface WorkerBindingArtifacts {
216
+ /** The name of the binding */
217
+ name: string;
218
+ /** Type identifier for Artifacts binding */
219
+ type: "artifacts";
220
+ /** Artifacts namespace name */
221
+ namespace: string;
222
+ }
223
+
209
224
  /**
210
225
  * Assets binding type
211
226
  */
@@ -295,8 +310,8 @@ export interface WorkerBindingJson {
295
310
  name: string;
296
311
  /** Type identifier for JSON binding */
297
312
  type: "json";
298
- /** JSON value */
299
- json: string;
313
+ /** JSON value, sent raw (not stringified) so the runtime injects native types */
314
+ json: unknown;
300
315
  }
301
316
 
302
317
  /**
@@ -5,6 +5,7 @@ import type { Ai as _Ai } from "./ai.ts";
5
5
  import type { AiSearchNamespace as _AiSearchNamespace } from "./ai-search-namespace.ts";
6
6
  import type { AiSearch as _AiSearch } from "./ai-search.ts";
7
7
  import type { AnalyticsEngineDataset as _AnalyticsEngineDataset } from "./analytics-engine.ts";
8
+ import type { Artifacts as _Artifacts } from "./artifacts.ts";
8
9
  import type { Assets } from "./assets.ts";
9
10
  import type { Binding, Json, Self } from "./bindings.ts";
10
11
  import type { BrowserRendering } from "./browser-rendering.ts";
@@ -61,69 +62,71 @@ export type Bound<T extends Binding> = T extends _AiSearch
61
62
  ? BoundWorker<RPC>
62
63
  : T extends _Worker<any, infer RPC> | WorkerRef<infer RPC>
63
64
  ? BoundWorker<RPC>
64
- : T extends { type: "service" }
65
- ? Service
66
- : T extends _R2Bucket
67
- ? R2Bucket
68
- : T extends _Hyperdrive | HyperdriveRef
69
- ? Hyperdrive
70
- : T extends Secret
71
- ? string
72
- : T extends CloudflareSecret | CloudflareSecretRef
73
- ? SecretsStoreSecret
74
- : T extends _EmailSender
75
- ? SendEmail
76
- : T extends SecretKey
77
- ? CryptoKey
78
- : T extends Assets
79
- ? Service
80
- : T extends _Workflow<infer P>
81
- ? Workflow<P>
82
- : T extends _D1Database
83
- ? D1Database
84
- : T extends DispatchNamespace
85
- ? DispatchNamespace
86
- : T extends _WorkerLoader
87
- ? WorkerLoader
88
- : T extends _VectorizeIndex
89
- ? VectorizeIndex
90
- : T extends _Queue<infer Body>
91
- ? Queue<Body>
92
- : T extends _AnalyticsEngineDataset
93
- ? AnalyticsEngineDataset
94
- : T extends _Pipeline<infer R>
95
- ? Pipeline<R>
96
- : T extends _RateLimit
97
- ? RateLimit
98
- : T extends string
99
- ? T
100
- : T extends BrowserRendering
101
- ? BrowserRun
102
- : T extends _Ai<infer M>
103
- ? Ai<M>
104
- : T extends _Images
105
- ? ImagesBinding
106
- : T extends _VersionMetadata
107
- ? WorkerVersionMetadata
108
- : T extends
109
- | _Worker.DevDomain
110
- | _Worker.DevUrl
111
- ? string
112
- : T extends Self
113
- ? Service
114
- : T extends Json<
115
- infer T
116
- >
117
- ? T
118
- : T extends _Container<
119
- infer Obj
65
+ : T extends _Artifacts
66
+ ? Artifacts
67
+ : T extends { type: "service" }
68
+ ? Service
69
+ : T extends _R2Bucket
70
+ ? R2Bucket
71
+ : T extends _Hyperdrive | HyperdriveRef
72
+ ? Hyperdrive
73
+ : T extends Secret
74
+ ? string
75
+ : T extends CloudflareSecret | CloudflareSecretRef
76
+ ? SecretsStoreSecret
77
+ : T extends _EmailSender
78
+ ? SendEmail
79
+ : T extends SecretKey
80
+ ? CryptoKey
81
+ : T extends Assets
82
+ ? Service
83
+ : T extends _Workflow<infer P>
84
+ ? Workflow<P>
85
+ : T extends _D1Database
86
+ ? D1Database
87
+ : T extends DispatchNamespace
88
+ ? DispatchNamespace
89
+ : T extends _WorkerLoader
90
+ ? WorkerLoader
91
+ : T extends _VectorizeIndex
92
+ ? VectorizeIndex
93
+ : T extends _Queue<infer Body>
94
+ ? Queue<Body>
95
+ : T extends _AnalyticsEngineDataset
96
+ ? AnalyticsEngineDataset
97
+ : T extends _Pipeline<infer R>
98
+ ? Pipeline<R>
99
+ : T extends _RateLimit
100
+ ? RateLimit
101
+ : T extends string
102
+ ? T
103
+ : T extends BrowserRendering
104
+ ? BrowserRun
105
+ : T extends _Ai<infer M>
106
+ ? Ai<M>
107
+ : T extends _Images
108
+ ? ImagesBinding
109
+ : T extends _VersionMetadata
110
+ ? WorkerVersionMetadata
111
+ : T extends
112
+ | _Worker.DevDomain
113
+ | _Worker.DevUrl
114
+ ? string
115
+ : T extends Self
116
+ ? Service
117
+ : T extends Json<
118
+ infer T
120
119
  >
121
- ? DurableObjectNamespace<
122
- Obj &
123
- Rpc.DurableObjectBranded
124
- >
125
- : T extends _VpcService
126
- ? Fetcher
127
- : T extends undefined
128
- ? undefined
129
- : Service;
120
+ ? T
121
+ : T extends _Container<
122
+ infer Obj
123
+ >
124
+ ? DurableObjectNamespace<
125
+ Obj &
126
+ Rpc.DurableObjectBranded
127
+ >
128
+ : T extends _VpcService
129
+ ? Fetcher
130
+ : T extends undefined
131
+ ? undefined
132
+ : Service;
@@ -142,17 +142,24 @@ export const R2BucketCustomDomain = Resource(
142
142
  props.jurisdiction,
143
143
  payload,
144
144
  ).catch(async (error) => {
145
- if (
146
- error instanceof CloudflareApiError &&
147
- error.status === 409 &&
148
- adopt
149
- ) {
150
- return await updateBucketCustomDomain(
145
+ if (error instanceof CloudflareApiError && error.status === 409) {
146
+ const existing = await getBucketCustomDomain(
151
147
  api,
152
148
  props.bucketName,
149
+ payload.domain,
153
150
  props.jurisdiction,
154
- payload,
155
151
  );
152
+ if (existing && customDomainMatches(existing, payload)) {
153
+ return;
154
+ }
155
+ if (adopt) {
156
+ return await updateBucketCustomDomain(
157
+ api,
158
+ props.bucketName,
159
+ props.jurisdiction,
160
+ payload,
161
+ );
162
+ }
156
163
  }
157
164
  throw error;
158
165
  });
@@ -190,6 +197,25 @@ export const R2BucketCustomDomain = Resource(
190
197
  },
191
198
  );
192
199
 
200
+ function customDomainMatches(
201
+ existing: R2BucketCustomDomain,
202
+ payload: R2BucketCustomDomain,
203
+ ) {
204
+ return (
205
+ existing.domain === payload.domain &&
206
+ existing.zoneId === payload.zoneId &&
207
+ existing.enabled === payload.enabled &&
208
+ (existing.minTLS ?? undefined) === (payload.minTLS ?? undefined) &&
209
+ arraysEqual(existing.ciphers, payload.ciphers)
210
+ );
211
+ }
212
+
213
+ function arraysEqual(a: string[] | undefined, b: string[] | undefined) {
214
+ if (a === b) return true;
215
+ if (!a || !b || a.length !== b.length) return false;
216
+ return a.every((v, i) => v === b[i]);
217
+ }
218
+
193
219
  async function createBucketCustomDomain(
194
220
  api: CloudflareApi,
195
221
  bucket: string,
@@ -307,11 +307,21 @@ const _D1Database = Resource(
307
307
 
308
308
  if (local) {
309
309
  if (props.migrationsFiles?.length || props.importFiles?.length) {
310
+ const localMigrationsFiles = props.migrationsDir
311
+ ? await listSqlFiles(props.migrationsDir)
312
+ : [];
313
+ const localImportFiles = props.importFiles?.length
314
+ ? await Promise.all(
315
+ props.importFiles.map((file) =>
316
+ readSqlFile(this.scope.rootDir, file.id),
317
+ ),
318
+ )
319
+ : [];
310
320
  await applyLocalD1Migrations({
311
321
  databaseId: dev.id,
312
322
  migrationsTable: props.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE,
313
- migrations: props.migrationsFiles ?? [],
314
- imports: props.importFiles ?? [],
323
+ migrations: localMigrationsFiles,
324
+ imports: localImportFiles,
315
325
  rootDir: this.scope.rootDir,
316
326
  });
317
327
  }
@@ -47,6 +47,11 @@ export interface HyperdrivePublicOrigin {
47
47
  */
48
48
  scheme?: "postgres" | "mysql";
49
49
 
50
+ /**
51
+ * Connection query string, without the leading "?"
52
+ */
53
+ query?: string;
54
+
50
55
  /**
51
56
  * Database user
52
57
  */
@@ -90,6 +95,11 @@ export interface HyperdriveOriginWithAccess {
90
95
  */
91
96
  scheme?: "postgres" | "mysql";
92
97
 
98
+ /**
99
+ * Connection query string, without the leading "?"
100
+ */
101
+ query?: string;
102
+
93
103
  /**
94
104
  * Database user
95
105
  */
@@ -610,6 +620,7 @@ export const normalizeHyperdriveOrigin = (
610
620
  host: url.hostname,
611
621
  port: normalizePort(scheme, url.port),
612
622
  database: url.pathname.slice(1),
623
+ query: url.search?.slice(1) ?? "",
613
624
  };
614
625
  }
615
626
  const scheme = normalizeScheme(origin.scheme);
@@ -623,6 +634,7 @@ export const normalizeHyperdriveOrigin = (
623
634
  }),
624
635
  scheme,
625
636
  port: normalizePort(scheme, origin.port),
637
+ query: origin.query ?? "",
626
638
  };
627
639
  };
628
640
 
@@ -660,6 +672,6 @@ const toConnectionString = (
660
672
  "password" in origin ? origin.password : origin.access_client_secret,
661
673
  );
662
674
  return new Secret(
663
- `${origin.scheme}://${origin.user}:${password}@${origin.host}:${origin.port}/${origin.database}`,
675
+ `${origin.scheme}://${origin.user}:${password}@${origin.host}:${origin.port}/${origin.database}${origin.query ? `?${origin.query}` : ""}`,
664
676
  );
665
677
  };
@@ -20,6 +20,7 @@ export * from "./api-gateway-operation.ts";
20
20
  export * from "./api-schema.ts";
21
21
  export * from "./api-shield.ts";
22
22
  export * from "./api.ts";
23
+ export * from "./artifacts.ts";
23
24
  export * from "./assets.ts";
24
25
  export * from "./astro/astro.ts";
25
26
  export * from "./bindings.ts";
@@ -167,6 +167,11 @@ export const buildWorkerOptions = async (
167
167
  };
168
168
  break;
169
169
  }
170
+ case "artifacts": {
171
+ throw new Error(
172
+ "Artifacts bindings are not supported in Alchemy local dev yet. Deploy the Worker or use generated Wrangler config with remote Artifacts bindings.",
173
+ );
174
+ }
170
175
  case "assets": {
171
176
  options.assets = {
172
177
  binding: key,
@@ -249,7 +249,9 @@ const writeMiniflareResponseToNode = (
249
249
  });
250
250
 
251
251
  if (res.body) {
252
- Readable.fromWeb(res.body).pipe(out, { end: true });
252
+ const body = Readable.fromWeb(res.body);
253
+ body.on("error", () => out.destroy());
254
+ body.pipe(out, { end: true });
253
255
  } else {
254
256
  out.end();
255
257
  }