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.
- package/bin/alchemy.js +10 -4
- package/bin/services/find-workspace-root.ts +1 -1
- package/lib/alchemy.d.ts +6 -0
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +1 -0
- package/lib/alchemy.js.map +1 -1
- package/lib/cloudflare/bindings.d.ts +17 -5
- package/lib/cloudflare/bindings.d.ts.map +1 -1
- package/lib/cloudflare/bindings.js +3 -1
- package/lib/cloudflare/bindings.js.map +1 -1
- package/lib/cloudflare/bound.d.ts +4 -3
- package/lib/cloudflare/bound.d.ts.map +1 -1
- package/lib/cloudflare/bundle/internal-worker-bundle.d.ts +1 -1
- package/lib/cloudflare/bundle/internal-worker-bundle.d.ts.map +1 -1
- package/lib/cloudflare/bundle/internal-worker-bundle.js.map +1 -1
- package/lib/cloudflare/hyperdrive-ref.d.ts +23 -0
- package/lib/cloudflare/hyperdrive-ref.d.ts.map +1 -0
- package/lib/cloudflare/hyperdrive-ref.js +52 -0
- package/lib/cloudflare/hyperdrive-ref.js.map +1 -0
- package/lib/cloudflare/index.d.ts +2 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +2 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.d.ts +2 -1
- package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.js +11 -6
- package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-controller.d.ts +3 -1
- package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-controller.js +24 -9
- package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts +24 -13
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +95 -79
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
- package/lib/cloudflare/miniflare/tunnel.d.ts +18 -0
- package/lib/cloudflare/miniflare/tunnel.d.ts.map +1 -0
- package/lib/cloudflare/miniflare/tunnel.js +79 -0
- package/lib/cloudflare/miniflare/tunnel.js.map +1 -0
- package/lib/cloudflare/next.d.ts.map +1 -1
- package/lib/cloudflare/next.js +3 -0
- package/lib/cloudflare/next.js.map +1 -1
- package/lib/cloudflare/quick-tunnel.d.ts +1 -1
- package/lib/cloudflare/secret-ref.d.ts +38 -0
- package/lib/cloudflare/secret-ref.d.ts.map +1 -0
- package/lib/cloudflare/secret-ref.js +38 -0
- package/lib/cloudflare/secret-ref.js.map +1 -0
- package/lib/cloudflare/worker-metadata.d.ts +1 -1
- package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
- package/lib/cloudflare/worker-metadata.js +7 -9
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +16 -1
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +12 -0
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/wrangler.json.d.ts +2 -1
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +4 -6
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/llms.cloudflare.txt +1 -1
- package/lib/scope.d.ts +10 -1
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +3 -1
- package/lib/scope.js.map +1 -1
- package/lib/util/idempotent-spawn.d.ts.map +1 -1
- package/lib/util/idempotent-spawn.js +5 -8
- package/lib/util/idempotent-spawn.js.map +1 -1
- package/package.json +1 -1
- package/src/alchemy.ts +7 -0
- package/src/cloudflare/bindings.ts +21 -4
- package/src/cloudflare/bound.ts +50 -52
- package/src/cloudflare/bundle/internal-worker-bundle.ts +2 -1
- package/src/cloudflare/hyperdrive-ref.ts +98 -0
- package/src/cloudflare/index.ts +2 -0
- package/src/cloudflare/miniflare/build-worker-options.ts +21 -12
- package/src/cloudflare/miniflare/miniflare-controller.ts +27 -12
- package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +122 -102
- package/src/cloudflare/miniflare/tunnel.ts +116 -0
- package/src/cloudflare/next.ts +3 -0
- package/src/cloudflare/secret-ref.ts +56 -0
- package/src/cloudflare/worker-metadata.ts +14 -12
- package/src/cloudflare/worker.ts +32 -1
- package/src/cloudflare/wrangler.json.ts +13 -11
- package/src/llms.cloudflare.txt +1 -1
- package/src/scope.ts +12 -1
- package/src/util/idempotent-spawn.ts +5 -6
- package/templates/rwsdk/.cursor/rules/alchemy_cloudflare.mdc +1 -1
- package/templates/rwsdk/package.json +3 -3
- package/workers/tunnel-proxy.js +110 -0
- package/workers/tunnel-proxy.ts +121 -0
package/src/cloudflare/worker.ts
CHANGED
|
@@ -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:
|
|
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: {
|
|
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
|
|
780
|
+
? binding.dev?.origin.unencrypted
|
|
779
781
|
: undefined,
|
|
780
782
|
});
|
|
781
783
|
} else if (binding.type === "pipeline") {
|
package/src/llms.cloudflare.txt
CHANGED
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
239
|
+
clearInterval(tick);
|
|
241
240
|
await fh.close().catch(() => {});
|
|
242
241
|
await persist();
|
|
243
242
|
};
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"worker:run": "rw-scripts worker-run"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@prisma/adapter-d1": "~6.
|
|
31
|
-
"@prisma/client": "~6.
|
|
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.
|
|
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
|
+
`;
|