alchemy 0.66.0 → 0.67.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 (90) hide show
  1. package/bin/alchemy.js +10 -4
  2. package/bin/services/find-workspace-root.ts +1 -1
  3. package/lib/alchemy.d.ts +6 -0
  4. package/lib/alchemy.d.ts.map +1 -1
  5. package/lib/alchemy.js +1 -0
  6. package/lib/alchemy.js.map +1 -1
  7. package/lib/cloudflare/bindings.d.ts +17 -5
  8. package/lib/cloudflare/bindings.d.ts.map +1 -1
  9. package/lib/cloudflare/bindings.js +3 -1
  10. package/lib/cloudflare/bindings.js.map +1 -1
  11. package/lib/cloudflare/bound.d.ts +4 -3
  12. package/lib/cloudflare/bound.d.ts.map +1 -1
  13. package/lib/cloudflare/bundle/internal-worker-bundle.d.ts +1 -1
  14. package/lib/cloudflare/bundle/internal-worker-bundle.d.ts.map +1 -1
  15. package/lib/cloudflare/bundle/internal-worker-bundle.js.map +1 -1
  16. package/lib/cloudflare/hyperdrive-ref.d.ts +23 -0
  17. package/lib/cloudflare/hyperdrive-ref.d.ts.map +1 -0
  18. package/lib/cloudflare/hyperdrive-ref.js +52 -0
  19. package/lib/cloudflare/hyperdrive-ref.js.map +1 -0
  20. package/lib/cloudflare/index.d.ts +2 -0
  21. package/lib/cloudflare/index.d.ts.map +1 -1
  22. package/lib/cloudflare/index.js +2 -0
  23. package/lib/cloudflare/index.js.map +1 -1
  24. package/lib/cloudflare/miniflare/build-worker-options.d.ts +2 -1
  25. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  26. package/lib/cloudflare/miniflare/build-worker-options.js +11 -6
  27. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  28. package/lib/cloudflare/miniflare/miniflare-controller.d.ts +3 -1
  29. package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
  30. package/lib/cloudflare/miniflare/miniflare-controller.js +24 -9
  31. package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
  32. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts +24 -13
  33. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
  34. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +95 -79
  35. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
  36. package/lib/cloudflare/miniflare/tunnel.d.ts +18 -0
  37. package/lib/cloudflare/miniflare/tunnel.d.ts.map +1 -0
  38. package/lib/cloudflare/miniflare/tunnel.js +79 -0
  39. package/lib/cloudflare/miniflare/tunnel.js.map +1 -0
  40. package/lib/cloudflare/next.d.ts.map +1 -1
  41. package/lib/cloudflare/next.js +3 -0
  42. package/lib/cloudflare/next.js.map +1 -1
  43. package/lib/cloudflare/quick-tunnel.d.ts +1 -1
  44. package/lib/cloudflare/secret-ref.d.ts +38 -0
  45. package/lib/cloudflare/secret-ref.d.ts.map +1 -0
  46. package/lib/cloudflare/secret-ref.js +38 -0
  47. package/lib/cloudflare/secret-ref.js.map +1 -0
  48. package/lib/cloudflare/worker-metadata.d.ts +1 -1
  49. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  50. package/lib/cloudflare/worker-metadata.js +7 -9
  51. package/lib/cloudflare/worker-metadata.js.map +1 -1
  52. package/lib/cloudflare/worker.d.ts +16 -1
  53. package/lib/cloudflare/worker.d.ts.map +1 -1
  54. package/lib/cloudflare/worker.js +12 -0
  55. package/lib/cloudflare/worker.js.map +1 -1
  56. package/lib/cloudflare/wrangler.json.d.ts +2 -1
  57. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  58. package/lib/cloudflare/wrangler.json.js +4 -6
  59. package/lib/cloudflare/wrangler.json.js.map +1 -1
  60. package/lib/llms.cloudflare.txt +1 -1
  61. package/lib/scope.d.ts +10 -1
  62. package/lib/scope.d.ts.map +1 -1
  63. package/lib/scope.js +3 -1
  64. package/lib/scope.js.map +1 -1
  65. package/lib/util/idempotent-spawn.d.ts.map +1 -1
  66. package/lib/util/idempotent-spawn.js +5 -8
  67. package/lib/util/idempotent-spawn.js.map +1 -1
  68. package/package.json +1 -1
  69. package/src/alchemy.ts +7 -0
  70. package/src/cloudflare/bindings.ts +21 -4
  71. package/src/cloudflare/bound.ts +50 -52
  72. package/src/cloudflare/bundle/internal-worker-bundle.ts +2 -1
  73. package/src/cloudflare/hyperdrive-ref.ts +98 -0
  74. package/src/cloudflare/index.ts +2 -0
  75. package/src/cloudflare/miniflare/build-worker-options.ts +21 -12
  76. package/src/cloudflare/miniflare/miniflare-controller.ts +27 -12
  77. package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +122 -102
  78. package/src/cloudflare/miniflare/tunnel.ts +116 -0
  79. package/src/cloudflare/next.ts +3 -0
  80. package/src/cloudflare/secret-ref.ts +56 -0
  81. package/src/cloudflare/worker-metadata.ts +14 -12
  82. package/src/cloudflare/worker.ts +32 -1
  83. package/src/cloudflare/wrangler.json.ts +13 -11
  84. package/src/llms.cloudflare.txt +1 -1
  85. package/src/scope.ts +12 -1
  86. package/src/util/idempotent-spawn.ts +5 -6
  87. package/templates/rwsdk/.cursor/rules/alchemy_cloudflare.mdc +1 -1
  88. package/templates/rwsdk/package.json +3 -3
  89. package/workers/tunnel-proxy.js +110 -0
  90. package/workers/tunnel-proxy.ts +121 -0
@@ -15,6 +15,7 @@ import {
15
15
  import type { Assets } from "./assets.ts";
16
16
  import type {
17
17
  Bindings,
18
+ Self,
18
19
  WorkerBindingDurableObjectNamespace,
19
20
  WorkerBindingSpec,
20
21
  } from "./bindings.ts";
@@ -58,6 +59,8 @@ import { Workflow, isWorkflow, upsertWorkflow } from "./workflow.ts";
58
59
  // Previous versions of `Worker` used the `Bundle` resource.
59
60
  // This import is here to avoid errors when destroying the `Bundle` resource.
60
61
  import "../esbuild/bundle.ts";
62
+ import { Scope } from "../scope.ts";
63
+ import type { WorkerRef } from "./worker-ref.ts";
61
64
 
62
65
  /**
63
66
  * Configuration options for static assets
@@ -330,11 +333,19 @@ export interface BaseWorkerProps<
330
333
  * @default false
331
334
  */
332
335
  remote?: boolean;
336
+ /**
337
+ * Whether to expose the worker via a Cloudflare Tunnel.
338
+ *
339
+ * @default false
340
+ */
341
+ tunnel?: boolean;
333
342
  url?: undefined;
334
343
  }
335
344
  | {
336
345
  url: string;
337
346
  remote?: undefined;
347
+ tunnel?: undefined;
348
+ port?: undefined;
338
349
  };
339
350
 
340
351
  /**
@@ -699,6 +710,25 @@ export function Worker<const B extends Bindings>(
699
710
  return _Worker(id, props as WorkerProps<B>);
700
711
  }
701
712
 
713
+ Worker.experimentalEntrypoint = <RPC extends Rpc.WorkerEntrypointBranded>(
714
+ worker: Worker | WorkerRef | Self,
715
+ entrypoint: string,
716
+ ) => {
717
+ if (Scope.getScope()?.local) {
718
+ logger.warn(
719
+ "Worker.experimentalEntrypoint is not supported in local development. See: https://github.com/cloudflare/workers-sdk/issues/10681",
720
+ );
721
+ }
722
+ return {
723
+ ...worker,
724
+ // we rename the entrypoint in order to prevent collisions with entrypoint on Worker
725
+ __entrypoint__: entrypoint,
726
+ } as (Worker | WorkerRef) & {
727
+ __entrypoint__?: string;
728
+ __rpc__?: RPC;
729
+ };
730
+ };
731
+
702
732
  const _Worker = Resource(
703
733
  "cloudflare::Worker",
704
734
  {
@@ -854,7 +884,8 @@ const _Worker = Resource(
854
884
  eventSources: props.eventSources,
855
885
  assets: props.assets,
856
886
  bundle,
857
- port: (props.dev as { port?: number } | undefined)?.port,
887
+ port: props.dev?.port,
888
+ tunnel: props.dev?.tunnel ?? this.scope.tunnel,
858
889
  });
859
890
  this.onCleanup(() => controller.dispose());
860
891
  }
@@ -5,11 +5,7 @@ import { Resource } from "../resource.ts";
5
5
  import { Scope } from "../scope.ts";
6
6
  import { isSecret } from "../secret.ts";
7
7
  import { assertNever } from "../util/assert-never.ts";
8
- import {
9
- Self,
10
- type Bindings,
11
- type WorkerBindingRateLimit,
12
- } from "./bindings.ts";
8
+ import type { Bindings, WorkerBindingRateLimit } from "./bindings.ts";
13
9
  import type { R2BucketJurisdiction } from "./bucket.ts";
14
10
  import type { DurableObjectNamespace } from "./durable-object-namespace.ts";
15
11
  import type { EventSource } from "./event-source.ts";
@@ -371,6 +367,7 @@ export interface WranglerJsonSpec {
371
367
  binding: string;
372
368
  service: string;
373
369
  environment?: string;
370
+ entrypoint?: string;
374
371
  }[];
375
372
 
376
373
  /**
@@ -534,8 +531,12 @@ function processBindings(
534
531
  preview_bucket_name: string;
535
532
  jurisdiction?: R2BucketJurisdiction;
536
533
  }[] = [];
537
- const services: { binding: string; service: string; environment?: string }[] =
538
- [];
534
+ const services: {
535
+ binding: string;
536
+ service: string;
537
+ environment?: string;
538
+ entrypoint?: string;
539
+ }[] = [];
539
540
  const secrets: string[] = [];
540
541
  const workflows: {
541
542
  name: string;
@@ -625,17 +626,20 @@ function processBindings(
625
626
  } else if (writeSecrets && isSecret(binding)) {
626
627
  spec.vars ??= {};
627
628
  spec.vars[bindingName] = binding as any;
628
- } else if (binding === Self) {
629
+ } else if (binding.type === "cloudflare::Worker::Self") {
629
630
  // Self(service) binding
630
631
  services.push({
631
632
  binding: bindingName,
632
633
  service: workerName,
634
+ entrypoint: binding.__entrypoint__,
633
635
  });
634
636
  } else if (binding.type === "service") {
635
637
  // Service binding
636
638
  services.push({
637
639
  binding: bindingName,
638
640
  service: "name" in binding ? binding.name : binding.service,
641
+ entrypoint:
642
+ "__entrypoint__" in binding ? binding.__entrypoint__ : undefined,
639
643
  });
640
644
  } else if (binding.type === "kv_namespace") {
641
645
  // KV Namespace binding
@@ -759,8 +763,6 @@ function processBindings(
759
763
  binding: bindingName,
760
764
  dataset: binding.dataset,
761
765
  });
762
- } else if (binding.type === "ai_gateway") {
763
- // no-op
764
766
  } else if (binding.type === "version_metadata") {
765
767
  if (spec.version_metadata) {
766
768
  throw new Error(
@@ -775,7 +777,7 @@ function processBindings(
775
777
  binding: bindingName,
776
778
  id: binding.hyperdriveId,
777
779
  localConnectionString: writeSecrets
778
- ? binding.dev.origin.unencrypted
780
+ ? binding.dev?.origin.unencrypted
779
781
  : undefined,
780
782
  });
781
783
  } else if (binding.type === "pipeline") {
@@ -269,7 +269,7 @@ const worker = await Worker("my-worker", {
269
269
  ### 2. Enable Development Compatibility
270
270
  ```typescript
271
271
  // Generate wrangler.json for local development
272
- await WranglerJson("wrangler.json", {
272
+ await WranglerJson({
273
273
  worker,
274
274
  });
275
275
  ```
package/src/scope.ts CHANGED
@@ -57,6 +57,12 @@ export interface ScopeOptions extends ProviderCredentials {
57
57
  * @default - `true` if ran with `alchemy dev`, `alchemy watch`, `bun --watch ./alchemy.run.ts`
58
58
  */
59
59
  watch?: boolean;
60
+ /**
61
+ * Whether to create a tunnel for supported resources.
62
+ *
63
+ * @default false
64
+ */
65
+ tunnel?: boolean;
60
66
  /**
61
67
  * Apply updates to resources even if there are no changes.
62
68
  *
@@ -186,6 +192,7 @@ export class Scope {
186
192
  public readonly phase: Phase;
187
193
  public readonly local: boolean;
188
194
  public readonly watch: boolean;
195
+ public readonly tunnel: boolean;
189
196
  public readonly force: boolean;
190
197
  public readonly adopt: boolean;
191
198
  public readonly destroyStrategy: DestroyStrategy;
@@ -225,6 +232,7 @@ export class Scope {
225
232
  phase,
226
233
  local,
227
234
  watch,
235
+ tunnel,
228
236
  force,
229
237
  destroyStrategy,
230
238
  telemetryClient,
@@ -284,6 +292,7 @@ export class Scope {
284
292
 
285
293
  this.local = local ?? this.parent?.local ?? false;
286
294
  this.watch = watch ?? this.parent?.watch ?? false;
295
+ this.tunnel = tunnel ?? this.parent?.tunnel ?? false;
287
296
  this.force = force ?? this.parent?.force ?? false;
288
297
  this.adopt = adopt ?? this.parent?.adopt ?? false;
289
298
  this.destroyStrategy =
@@ -327,7 +336,9 @@ export class Scope {
327
336
  >(
328
337
  // TODO(sam): validate uniqueness? Ensure a flat .logs/${id}.log dir? Or nest in scope dirs?
329
338
  id: string,
330
- options: Omit<IdempotentSpawnOptions, "log" | "stateFile">,
339
+ options: Omit<IdempotentSpawnOptions, "log" | "stateFile"> & {
340
+ extract?: E;
341
+ },
331
342
  ): Promise<E extends undefined ? undefined : string> {
332
343
  const logsDir = path.join(this.dotAlchemy, "logs");
333
344
  const pidsDir = path.join(this.dotAlchemy, "pids");
@@ -161,7 +161,7 @@ export async function idempotentSpawn({
161
161
  {
162
162
  write,
163
163
  chunkSize = 64 * 1024,
164
- // tickMs = 100,
164
+ tickMs = 100,
165
165
  }: {
166
166
  stateKey: string;
167
167
  write: (buf: Buffer) => boolean;
@@ -229,15 +229,14 @@ export async function idempotentSpawn({
229
229
  await drain();
230
230
  });
231
231
 
232
- // TODO(sam): do we need this?
233
- // const tick = setInterval(() => {
234
- // drain().catch(() => {});
235
- // }, tickMs);
232
+ const tick = setInterval(() => {
233
+ drain().catch(() => {});
234
+ }, tickMs);
236
235
 
237
236
  return async function stop() {
238
237
  closed = true;
239
238
  watcher.close();
240
- // clearInterval(tick);
239
+ clearInterval(tick);
241
240
  await fh.close().catch(() => {});
242
241
  await persist();
243
242
  };
@@ -274,7 +274,7 @@ const worker = await Worker("my-worker", {
274
274
  ### 2. Enable Development Compatibility
275
275
  ```typescript
276
276
  // Generate wrangler.json for local development
277
- await WranglerJson("wrangler.json", {
277
+ await WranglerJson({
278
278
  worker,
279
279
  });
280
280
  ```
@@ -27,8 +27,8 @@
27
27
  "worker:run": "rw-scripts worker-run"
28
28
  },
29
29
  "dependencies": {
30
- "@prisma/adapter-d1": "~6.8.2",
31
- "@prisma/client": "~6.8.2",
30
+ "@prisma/adapter-d1": "~6.16.1",
31
+ "@prisma/client": "~6.16.1",
32
32
  "@simplewebauthn/browser": "^13.1.0",
33
33
  "@simplewebauthn/server": "^13.1.1",
34
34
  "rwsdk": "^0.1.35"
@@ -39,7 +39,7 @@
39
39
  "@types/node": "^22.14.0",
40
40
  "@types/react": "^19.1.2",
41
41
  "@types/react-dom": "^19.1.2",
42
- "prisma": "~6.8.2",
42
+ "prisma": "~6.16.1",
43
43
  "typescript": "^5.8.3",
44
44
  "vibe-rules": "^0.2.31",
45
45
  "alchemy": "workspace:*",
@@ -0,0 +1,110 @@
1
+ // workers/tunnel-proxy.ts
2
+ var tunnel_proxy_default = {
3
+ async fetch(request, env) {
4
+ const url = new URL(request.url);
5
+ url.host = env.TUNNEL_HOST;
6
+ const headers = new Headers(request.headers);
7
+ headers.set("alchemy-worker-name", env.WORKER_NAME);
8
+ const response = await fetch(url, {
9
+ method: request.method,
10
+ headers,
11
+ body: request.body,
12
+ redirect: "manual"
13
+ });
14
+ if (response.status === 530) {
15
+ const error = await extractResponseError(response);
16
+ const headers2 = new Headers(response.headers);
17
+ headers2.set("Content-Type", "text/html");
18
+ return new Response(
19
+ renderErrorHtml({
20
+ name: env.WORKER_NAME,
21
+ error,
22
+ rayId: response.headers.get("cf-ray") ?? "Unknown"
23
+ }),
24
+ {
25
+ status: 530,
26
+ headers: headers2
27
+ }
28
+ );
29
+ }
30
+ return response;
31
+ }
32
+ };
33
+ var extractResponseError = async (response) => {
34
+ const error = response.headers.get("Alchemy-Error");
35
+ if (error) {
36
+ return error;
37
+ }
38
+ let code;
39
+ let message;
40
+ const { promise, resolve } = Promise.withResolvers();
41
+ const rewriter = new HTMLRewriter();
42
+ rewriter.on("h1", {
43
+ text(text) {
44
+ if (code) return;
45
+ const match = text.text.match(/(\d+)/);
46
+ if (match) {
47
+ code = match[1];
48
+ }
49
+ }
50
+ });
51
+ rewriter.on("h2", {
52
+ text(text) {
53
+ if (message) return;
54
+ message = text.text;
55
+ }
56
+ });
57
+ rewriter.onDocument({
58
+ end() {
59
+ resolve([code, message].filter(Boolean).join(": ") || "Unknown");
60
+ }
61
+ });
62
+ await rewriter.transform(response).text();
63
+ return promise;
64
+ };
65
+ var renderErrorHtml = (props) => `
66
+ <!DOCTYPE html>
67
+ <html lang="en">
68
+
69
+ <head>
70
+ <title>Alchemy | Tunnel Error</title>
71
+ <meta charset="utf-8">
72
+ <meta name="viewport" content="width=device-width, initial-scale=1">
73
+ <link rel="shortcut icon" href="https://alchemy.run/potion.png" type="image/png" />
74
+ <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"><\/script>
75
+ </head>
76
+
77
+ <body class="font-sans bg-slate-100 text-slate-950">
78
+ <div class="flex flex-col items-center justify-center h-screen">
79
+ <div class="border-t-3 border-red-600 px-5 py-7 bg-white gap-3 flex flex-col w-full max-w-lg">
80
+ <h1 class="text-2xl font-semibold">530: Tunnel Error</h1>
81
+ <p class="text-slate-800">The worker "${props.name}" could not be reached.</p>
82
+ <p class="text-slate-800">Please ensure that <code
83
+ class="bg-slate-100 text-slate-800 p-1 rounded-md text-sm">alchemy dev</code>
84
+ is running and tunnel mode is enabled for the worker.</p>
85
+ <div class="flex flex-col gap-2 border-l-2 border-slate-300 pl-3 my-1.5">
86
+ <div class="text-sm flex flex-col gap-1">
87
+ <div class="text-slate-500">Error:</div>
88
+ <div class="text-slate-800 font-mono">${props.error}</div>
89
+ </div>
90
+ <div class="text-sm flex flex-col gap-1">
91
+ <div class="text-slate-500">Ray ID:</div>
92
+ <div class="text-slate-800 font-mono">${props.rayId}</div>
93
+ </div>
94
+ </div>
95
+ <p class="text-slate-800"><a href="https://alchemy.run/docs/guides/cloudflare-tunnel"
96
+ class="text-slate-800 underline underline-offset-2 hover:no-underline" target="_blank">Learn more about tunnel
97
+ mode in
98
+ Alchemy</a>.</p>
99
+ </div>
100
+ <div class="bg-slate-200 px-5 py-3 flex flex-col w-full max-w-lg">
101
+ <p class="text-sm text-slate-500">Alchemy ${"0.67.0"}</p>
102
+ </div>
103
+ </div>
104
+ </body>
105
+
106
+ </html>
107
+ `;
108
+ export {
109
+ tunnel_proxy_default as default
110
+ };
@@ -0,0 +1,121 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+
3
+ interface Env {
4
+ WORKER_NAME: string;
5
+ TUNNEL_HOST: string;
6
+ }
7
+
8
+ declare const ALCHEMY_VERSION: string; // injected during `bun run build:workers`
9
+
10
+ export default {
11
+ async fetch(request: Request, env: Env) {
12
+ const url = new URL(request.url);
13
+ url.host = env.TUNNEL_HOST;
14
+ const headers = new Headers(request.headers);
15
+ headers.set("alchemy-worker-name", env.WORKER_NAME);
16
+ const response = await fetch(url, {
17
+ method: request.method,
18
+ headers,
19
+ body: request.body,
20
+ redirect: "manual",
21
+ });
22
+ if (response.status === 530) {
23
+ const error = await extractResponseError(response);
24
+ const headers = new Headers(response.headers);
25
+ headers.set("Content-Type", "text/html");
26
+ return new Response(
27
+ renderErrorHtml({
28
+ name: env.WORKER_NAME,
29
+ error,
30
+ rayId: response.headers.get("cf-ray") ?? "Unknown",
31
+ }),
32
+ {
33
+ status: 530,
34
+ headers,
35
+ },
36
+ );
37
+ }
38
+ return response;
39
+ },
40
+ } as ExportedHandler<Env>;
41
+
42
+ const extractResponseError = async (response: Response) => {
43
+ const error = response.headers.get("Alchemy-Error");
44
+ if (error) {
45
+ return error;
46
+ }
47
+ let code: string | undefined;
48
+ let message: string | undefined;
49
+ const { promise, resolve } = Promise.withResolvers<string>();
50
+ const rewriter = new HTMLRewriter();
51
+ rewriter.on("h1", {
52
+ text(text) {
53
+ if (code) return;
54
+ const match = text.text.match(/(\d+)/);
55
+ if (match) {
56
+ code = match[1];
57
+ }
58
+ },
59
+ });
60
+ rewriter.on("h2", {
61
+ text(text) {
62
+ if (message) return;
63
+ message = text.text;
64
+ },
65
+ });
66
+ rewriter.onDocument({
67
+ end() {
68
+ resolve([code, message].filter(Boolean).join(": ") || "Unknown");
69
+ },
70
+ });
71
+ await rewriter.transform(response).text(); // consume response body so rewriter runs
72
+ return promise;
73
+ };
74
+
75
+ const renderErrorHtml = (props: {
76
+ name: string;
77
+ error: string;
78
+ rayId: string;
79
+ }) => `
80
+ <!DOCTYPE html>
81
+ <html lang="en">
82
+
83
+ <head>
84
+ <title>Alchemy | Tunnel Error</title>
85
+ <meta charset="utf-8">
86
+ <meta name="viewport" content="width=device-width, initial-scale=1">
87
+ <link rel="shortcut icon" href="https://alchemy.run/potion.png" type="image/png" />
88
+ <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
89
+ </head>
90
+
91
+ <body class="font-sans bg-slate-100 text-slate-950">
92
+ <div class="flex flex-col items-center justify-center h-screen">
93
+ <div class="border-t-3 border-red-600 px-5 py-7 bg-white gap-3 flex flex-col w-full max-w-lg">
94
+ <h1 class="text-2xl font-semibold">530: Tunnel Error</h1>
95
+ <p class="text-slate-800">The worker "${props.name}" could not be reached.</p>
96
+ <p class="text-slate-800">Please ensure that <code
97
+ class="bg-slate-100 text-slate-800 p-1 rounded-md text-sm">alchemy dev</code>
98
+ is running and tunnel mode is enabled for the worker.</p>
99
+ <div class="flex flex-col gap-2 border-l-2 border-slate-300 pl-3 my-1.5">
100
+ <div class="text-sm flex flex-col gap-1">
101
+ <div class="text-slate-500">Error:</div>
102
+ <div class="text-slate-800 font-mono">${props.error}</div>
103
+ </div>
104
+ <div class="text-sm flex flex-col gap-1">
105
+ <div class="text-slate-500">Ray ID:</div>
106
+ <div class="text-slate-800 font-mono">${props.rayId}</div>
107
+ </div>
108
+ </div>
109
+ <p class="text-slate-800"><a href="https://alchemy.run/docs/guides/cloudflare-tunnel"
110
+ class="text-slate-800 underline underline-offset-2 hover:no-underline" target="_blank">Learn more about tunnel
111
+ mode in
112
+ Alchemy</a>.</p>
113
+ </div>
114
+ <div class="bg-slate-200 px-5 py-3 flex flex-col w-full max-w-lg">
115
+ <p class="text-sm text-slate-500">Alchemy ${ALCHEMY_VERSION}</p>
116
+ </div>
117
+ </div>
118
+ </body>
119
+
120
+ </html>
121
+ `;