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
|
@@ -2,12 +2,11 @@ import * as miniflare from "miniflare";
|
|
|
2
2
|
import { assertNever } from "../../util/assert-never.ts";
|
|
3
3
|
import type { HTTPServer } from "../../util/http.ts";
|
|
4
4
|
import type { CloudflareApi } from "../api.ts";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
type WorkerBindingSpec,
|
|
5
|
+
import type {
|
|
6
|
+
Binding,
|
|
7
|
+
Bindings,
|
|
8
|
+
WorkerBindingService,
|
|
9
|
+
WorkerBindingSpec,
|
|
11
10
|
} from "../bindings.ts";
|
|
12
11
|
import { isQueueEventSource, type EventSource } from "../event-source.ts";
|
|
13
12
|
import type { WorkerBundle, WorkerBundleSource } from "../worker-bundle.ts";
|
|
@@ -25,11 +24,11 @@ export interface MiniflareWorkerInput {
|
|
|
25
24
|
assets: AssetsConfig | undefined;
|
|
26
25
|
bundle: WorkerBundleSource;
|
|
27
26
|
port: number | undefined;
|
|
27
|
+
tunnel: boolean | undefined;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
type RemoteOnlyBindingType =
|
|
31
31
|
| "ai"
|
|
32
|
-
| "ai_gateway"
|
|
33
32
|
| "browser"
|
|
34
33
|
| "dispatch_namespace"
|
|
35
34
|
| "mtls_certificate"
|
|
@@ -92,13 +91,18 @@ export const buildWorkerOptions = async (
|
|
|
92
91
|
(options.bindings ??= {})[key] = binding;
|
|
93
92
|
continue;
|
|
94
93
|
}
|
|
95
|
-
if (binding === Self) {
|
|
94
|
+
if (binding.type === "cloudflare::Worker::Self") {
|
|
96
95
|
(options.serviceBindings ??= {})[key] = miniflare.kCurrentWorker;
|
|
97
96
|
continue;
|
|
98
97
|
}
|
|
99
98
|
switch (binding.type) {
|
|
100
|
-
case "ai":
|
|
101
|
-
|
|
99
|
+
case "ai": {
|
|
100
|
+
const existing = remoteBindings.find((b) => b.type === "ai");
|
|
101
|
+
if (existing) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Workers cannot have multiple AI bindings. Binding "${key}" conflicts with "${existing.name}".`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
102
106
|
remoteBindings.push({
|
|
103
107
|
type: "ai",
|
|
104
108
|
name: key,
|
|
@@ -180,7 +184,9 @@ export const buildWorkerOptions = async (
|
|
|
180
184
|
break;
|
|
181
185
|
}
|
|
182
186
|
case "hyperdrive": {
|
|
183
|
-
(
|
|
187
|
+
if (binding.dev) {
|
|
188
|
+
(options.hyperdrives ??= {})[key] = binding.dev.origin.unencrypted;
|
|
189
|
+
}
|
|
184
190
|
break;
|
|
185
191
|
}
|
|
186
192
|
case "images": {
|
|
@@ -459,7 +465,10 @@ const normalizeBundle = (bundle: WorkerBundle) => {
|
|
|
459
465
|
};
|
|
460
466
|
|
|
461
467
|
const isRemoteBinding = (binding: Binding) => {
|
|
462
|
-
if (
|
|
468
|
+
if (
|
|
469
|
+
typeof binding === "string" ||
|
|
470
|
+
binding.type === "cloudflare::Worker::Self"
|
|
471
|
+
) {
|
|
463
472
|
return false;
|
|
464
473
|
}
|
|
465
474
|
return (
|
|
@@ -10,8 +10,12 @@ import {
|
|
|
10
10
|
buildWorkerOptions,
|
|
11
11
|
type MiniflareWorkerInput,
|
|
12
12
|
} from "./build-worker-options.ts";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
createMiniflareWorkerProxy,
|
|
15
|
+
type MiniflareWorkerProxy,
|
|
16
|
+
} from "./miniflare-worker-proxy.ts";
|
|
14
17
|
import { getDefaultPersistPath } from "./paths.ts";
|
|
18
|
+
import { createTunnel, type Tunnel } from "./tunnel.ts";
|
|
15
19
|
|
|
16
20
|
declare global {
|
|
17
21
|
var ALCHEMY_MINIFLARE_CONTROLLER: MiniflareController | undefined;
|
|
@@ -21,6 +25,7 @@ export class MiniflareController {
|
|
|
21
25
|
abort = new AbortController();
|
|
22
26
|
miniflare: miniflare.Miniflare | undefined;
|
|
23
27
|
options = new Map<string, miniflare.WorkerOptions>();
|
|
28
|
+
tunnel: Tunnel | undefined;
|
|
24
29
|
localProxies = new Map<string, MiniflareWorkerProxy>();
|
|
25
30
|
remoteProxies = new Map<string, HTTPServer>();
|
|
26
31
|
mutex = new AsyncMutex();
|
|
@@ -40,21 +45,32 @@ export class MiniflareController {
|
|
|
40
45
|
assert(first.value, "First value is undefined");
|
|
41
46
|
this.options.set(input.name, first.value);
|
|
42
47
|
const miniflare = await this.update();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
let url: URL;
|
|
49
|
+
if (input.tunnel) {
|
|
50
|
+
this.tunnel ??= await createTunnel(miniflare);
|
|
51
|
+
url = await this.tunnel.configureWorker({
|
|
52
|
+
api: input.api,
|
|
53
|
+
name: input.name,
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
const proxy = await createMiniflareWorkerProxy({
|
|
57
|
+
port: input.port ?? (await findOpenPort()),
|
|
58
|
+
getWorkerName: () => input.name,
|
|
59
|
+
miniflare,
|
|
60
|
+
mode: "local",
|
|
61
|
+
});
|
|
62
|
+
this.localProxies.set(input.name, proxy);
|
|
63
|
+
url = proxy.url;
|
|
64
|
+
}
|
|
49
65
|
void this.watch(input.id, watcher);
|
|
50
66
|
logger.task(input.id, {
|
|
51
|
-
message: `Ready at ${
|
|
67
|
+
message: `Ready at ${url}`,
|
|
52
68
|
status: "success",
|
|
53
69
|
resource: input.id,
|
|
54
70
|
prefix: "dev",
|
|
55
71
|
prefixColor: "cyanBright",
|
|
56
72
|
});
|
|
57
|
-
return
|
|
73
|
+
return url.toString();
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
private async watch(
|
|
@@ -76,9 +92,7 @@ export class MiniflareController {
|
|
|
76
92
|
|
|
77
93
|
private async update() {
|
|
78
94
|
return await this.mutex.lock(async () => {
|
|
79
|
-
const options: miniflare.MiniflareOptions
|
|
80
|
-
workers: miniflare.WorkerOptions[];
|
|
81
|
-
} = {
|
|
95
|
+
const options: miniflare.MiniflareOptions = {
|
|
82
96
|
workers: [],
|
|
83
97
|
defaultPersistRoot: path.resolve(
|
|
84
98
|
getDefaultPersistPath(Scope.current.rootDir),
|
|
@@ -147,6 +161,7 @@ export class MiniflareController {
|
|
|
147
161
|
this.abort.abort();
|
|
148
162
|
await Promise.all([
|
|
149
163
|
this.miniflare?.dispose(),
|
|
164
|
+
this.tunnel?.close(),
|
|
150
165
|
...this.localProxies.values().map((proxy) => proxy.close()),
|
|
151
166
|
...this.remoteProxies.values().map((proxy) => proxy.close()),
|
|
152
167
|
]);
|
|
@@ -1,126 +1,108 @@
|
|
|
1
1
|
import * as miniflare from "miniflare";
|
|
2
2
|
import { once } from "node:events";
|
|
3
3
|
import http from "node:http";
|
|
4
|
-
import type Stream from "node:stream";
|
|
5
4
|
import { Readable } from "node:stream";
|
|
6
5
|
import { WebSocket, WebSocketServer } from "ws";
|
|
7
|
-
import { logger } from "../../util/logger.ts";
|
|
8
6
|
|
|
9
|
-
interface
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
miniflare: miniflare.Miniflare;
|
|
7
|
+
export interface MiniflareWorkerProxy {
|
|
8
|
+
url: URL;
|
|
9
|
+
close: () => Promise<void>;
|
|
13
10
|
}
|
|
14
11
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
await this.handleRequest(req, res);
|
|
25
|
-
});
|
|
26
|
-
this.server.listen(this.options.port);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
get ready() {
|
|
30
|
-
if (!this.server.listening) {
|
|
31
|
-
return once(this.server, "listening");
|
|
32
|
-
}
|
|
33
|
-
return Promise.resolve();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
get url() {
|
|
37
|
-
return `http://localhost:${this.options.port}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async close() {
|
|
41
|
-
await Promise.all([
|
|
42
|
-
new Promise((resolve) => this.wss.close(resolve)),
|
|
43
|
-
new Promise((resolve) => this.server.close(resolve)),
|
|
44
|
-
]);
|
|
45
|
-
}
|
|
12
|
+
export async function createMiniflareWorkerProxy(options: {
|
|
13
|
+
port: number;
|
|
14
|
+
transformRequest?: (request: RequestInfo) => void;
|
|
15
|
+
getWorkerName: (request: miniflare.Request) => string;
|
|
16
|
+
miniflare: miniflare.Miniflare;
|
|
17
|
+
mode: "local" | "remote";
|
|
18
|
+
}) {
|
|
19
|
+
const server = http.createServer();
|
|
20
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
46
21
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
22
|
+
server.on("upgrade", async (req, socket, head) => {
|
|
23
|
+
try {
|
|
24
|
+
const server = await createServerWebSocket(req);
|
|
25
|
+
wss.handleUpgrade(req, socket, head, (client) => {
|
|
26
|
+
client.on("message", (event, binary) => server.send(event, { binary }));
|
|
27
|
+
client.on("close", (code, reason) => server.close(code, reason));
|
|
28
|
+
server.on("message", (event, binary) => client.send(event, { binary }));
|
|
29
|
+
server.on("close", (code, reason) => client.close(code, reason));
|
|
30
|
+
wss.emit("connection", client, req);
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error(error);
|
|
54
34
|
socket.destroy();
|
|
55
|
-
return;
|
|
56
35
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
70
|
-
if (!target) {
|
|
71
|
-
logger.error(
|
|
72
|
-
`[Alchemy] Websocket connection failed: The worker "${this.options.name}" is not running.`,
|
|
36
|
+
});
|
|
37
|
+
server.on("request", async (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
const request = toMiniflareRequest(req);
|
|
40
|
+
const name = options.getWorkerName(request);
|
|
41
|
+
const worker = await options.miniflare.getWorker(name);
|
|
42
|
+
const response = await worker.fetch(request);
|
|
43
|
+
writeMiniflareResponseToNode(response, res);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(error);
|
|
46
|
+
const response = MiniflareWorkerProxyError.fromUnknown(error).toResponse(
|
|
47
|
+
options.mode,
|
|
73
48
|
);
|
|
74
|
-
|
|
49
|
+
writeMiniflareResponseToNode(response, res);
|
|
75
50
|
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const toMiniflareRequest = (req: http.IncomingMessage): miniflare.Request => {
|
|
54
|
+
const info = parseIncomingMessage(req);
|
|
55
|
+
options.transformRequest?.(info);
|
|
56
|
+
return new miniflare.Request(info.url, {
|
|
57
|
+
method: info.method,
|
|
58
|
+
headers: info.headers,
|
|
59
|
+
body: info.body,
|
|
60
|
+
redirect: info.redirect,
|
|
61
|
+
duplex: info.duplex,
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const createServerWebSocket = async (req: http.IncomingMessage) => {
|
|
66
|
+
const name = options.getWorkerName(toMiniflareRequest(req));
|
|
67
|
+
const target = await options.miniflare.unsafeGetDirectURL(name);
|
|
76
68
|
const url = new URL(req.url ?? "/", target);
|
|
77
69
|
url.protocol = url.protocol.replace("http", "ws");
|
|
78
70
|
const protocols = req.headers["sec-websocket-protocol"]
|
|
79
71
|
?.split(",")
|
|
80
72
|
.map((p) => p.trim());
|
|
81
73
|
const server = new WebSocket(url, protocols);
|
|
82
|
-
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
once(server, "close", { signal: controller.signal }).then((args) => {
|
|
86
|
-
const [code, reason] = args as [number, string];
|
|
87
|
-
logger.error(
|
|
88
|
-
`[Alchemy] Websocket connection failed for worker "${this.options.name}": ${code} ${reason}`,
|
|
89
|
-
);
|
|
90
|
-
return null;
|
|
91
|
-
}),
|
|
92
|
-
]).finally(() => controller.abort());
|
|
93
|
-
}
|
|
74
|
+
await once(server, "open");
|
|
75
|
+
return server;
|
|
76
|
+
};
|
|
94
77
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
}
|
|
78
|
+
server.listen(options.port);
|
|
79
|
+
await once(server, "listening");
|
|
80
|
+
const url = new URL(`http://localhost:${options.port}`);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
url,
|
|
84
|
+
close: async () => {
|
|
85
|
+
await Promise.all([
|
|
86
|
+
new Promise((resolve) => server.close(resolve)),
|
|
87
|
+
new Promise((resolve) => wss.close(resolve)),
|
|
88
|
+
]);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface RequestInfo {
|
|
94
|
+
method: string;
|
|
95
|
+
url: URL;
|
|
96
|
+
headers: miniflare.Headers;
|
|
97
|
+
body: miniflare.BodyInit | undefined;
|
|
98
|
+
redirect: "manual";
|
|
99
|
+
duplex: "half" | undefined;
|
|
119
100
|
}
|
|
120
101
|
|
|
121
|
-
const
|
|
102
|
+
const parseIncomingMessage = (req: http.IncomingMessage): RequestInfo => {
|
|
122
103
|
const method = req.method ?? "GET";
|
|
123
|
-
const
|
|
104
|
+
const protocol = req.headers["x-forwarded-proto"] ?? "http";
|
|
105
|
+
const url = new URL(req.url ?? "/", `${protocol}://${req.headers.host}`);
|
|
124
106
|
const headers = new miniflare.Headers();
|
|
125
107
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
126
108
|
if (Array.isArray(value)) {
|
|
@@ -135,13 +117,14 @@ const toMiniflareRequest = (req: http.IncomingMessage) => {
|
|
|
135
117
|
["GET", "HEAD", "OPTIONS"].includes(method) || !req.readable
|
|
136
118
|
? undefined
|
|
137
119
|
: Readable.toWeb(req);
|
|
138
|
-
return
|
|
120
|
+
return {
|
|
139
121
|
method,
|
|
122
|
+
url,
|
|
140
123
|
headers,
|
|
141
124
|
body,
|
|
142
125
|
redirect: "manual",
|
|
143
126
|
duplex: body ? "half" : undefined,
|
|
144
|
-
}
|
|
127
|
+
};
|
|
145
128
|
};
|
|
146
129
|
|
|
147
130
|
const writeMiniflareResponseToNode = (
|
|
@@ -159,3 +142,40 @@ const writeMiniflareResponseToNode = (
|
|
|
159
142
|
out.end();
|
|
160
143
|
}
|
|
161
144
|
};
|
|
145
|
+
|
|
146
|
+
export class MiniflareWorkerProxyError extends Error {
|
|
147
|
+
constructor(
|
|
148
|
+
message: string,
|
|
149
|
+
readonly status: number,
|
|
150
|
+
options?: ErrorOptions,
|
|
151
|
+
) {
|
|
152
|
+
super(message, options);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
toResponse(mode: "local" | "remote"): miniflare.Response {
|
|
156
|
+
let text = `[Alchemy] ${this.message}`;
|
|
157
|
+
if (mode === "local" && this.cause) {
|
|
158
|
+
const cause =
|
|
159
|
+
this.cause instanceof Error
|
|
160
|
+
? (this.cause.stack ?? this.cause.message)
|
|
161
|
+
: String(this.cause);
|
|
162
|
+
text += `\n\n${cause}`;
|
|
163
|
+
}
|
|
164
|
+
return new miniflare.Response(text, {
|
|
165
|
+
status: this.status,
|
|
166
|
+
headers: {
|
|
167
|
+
"Content-Type": "text/plain",
|
|
168
|
+
"Alchemy-Error": this.message,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static fromUnknown(error: unknown): MiniflareWorkerProxyError {
|
|
174
|
+
if (error instanceof MiniflareWorkerProxyError) {
|
|
175
|
+
return error;
|
|
176
|
+
}
|
|
177
|
+
return new MiniflareWorkerProxyError("An unknown error occurred.", 500, {
|
|
178
|
+
cause: error,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type * as miniflare from "miniflare";
|
|
2
|
+
import { Scope } from "../../scope.ts";
|
|
3
|
+
import { findOpenPort } from "../../util/find-open-port.ts";
|
|
4
|
+
import type { CloudflareApi } from "../api.ts";
|
|
5
|
+
import { getInternalWorkerBundle } from "../bundle/internal-worker-bundle.ts";
|
|
6
|
+
import {
|
|
7
|
+
enableWorkerSubdomain,
|
|
8
|
+
getAccountSubdomain,
|
|
9
|
+
getWorkerSubdomain,
|
|
10
|
+
} from "../worker-subdomain.ts";
|
|
11
|
+
import { putWorker } from "../worker.ts";
|
|
12
|
+
import {
|
|
13
|
+
createMiniflareWorkerProxy,
|
|
14
|
+
MiniflareWorkerProxyError,
|
|
15
|
+
} from "./miniflare-worker-proxy.ts";
|
|
16
|
+
|
|
17
|
+
export interface Tunnel {
|
|
18
|
+
/**
|
|
19
|
+
* Enables tunneling for a local worker.
|
|
20
|
+
* Returns a `workers.dev` URL that can be used to access the worker.
|
|
21
|
+
*/
|
|
22
|
+
configureWorker: (input: {
|
|
23
|
+
api: CloudflareApi;
|
|
24
|
+
name: string;
|
|
25
|
+
}) => Promise<URL>;
|
|
26
|
+
/**
|
|
27
|
+
* Closes the tunnel.
|
|
28
|
+
*/
|
|
29
|
+
close: () => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function createTunnel(
|
|
33
|
+
miniflare: miniflare.Miniflare,
|
|
34
|
+
): Promise<Tunnel> {
|
|
35
|
+
const workers = new Set<string>(); // used to avoid exposing workers that are not running with tunneling enabled
|
|
36
|
+
const proxy = await createMiniflareWorkerProxy({
|
|
37
|
+
port: await findOpenPort(9977), // alchemy auth uses 9976, so one above that
|
|
38
|
+
miniflare,
|
|
39
|
+
mode: "remote",
|
|
40
|
+
transformRequest: (request) => {
|
|
41
|
+
if (request.url.origin === remote.origin) {
|
|
42
|
+
request.url.protocol = proxy.url.protocol;
|
|
43
|
+
request.url.host = proxy.url.host;
|
|
44
|
+
request.url.port = proxy.url.port;
|
|
45
|
+
request.headers.set("host", proxy.url.host);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
getWorkerName: (request) => {
|
|
49
|
+
const name = request.headers.get("alchemy-worker-name");
|
|
50
|
+
if (!name) {
|
|
51
|
+
throw new MiniflareWorkerProxyError(
|
|
52
|
+
"Worker name is missing from request headers. This indicates a bug in Alchemy.",
|
|
53
|
+
530,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (!workers.has(name)) {
|
|
57
|
+
throw new MiniflareWorkerProxyError(
|
|
58
|
+
`The worker "${name}" is not running with tunneling enabled.`,
|
|
59
|
+
530,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return name;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const remoteUrlString = await Scope.current.spawn("tunnel", {
|
|
66
|
+
processName: `cloudflared-${proxy.url.port}`,
|
|
67
|
+
cmd: `cloudflared tunnel --url ${proxy.url.toString()}`,
|
|
68
|
+
quiet: !process.env.DEBUG,
|
|
69
|
+
extract: (line) => {
|
|
70
|
+
const match = line.match(/https:\/\/([^\s]+)\.trycloudflare\.com/);
|
|
71
|
+
if (match) {
|
|
72
|
+
return `https://${match[1]}.trycloudflare.com`;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const remote = new URL(remoteUrlString);
|
|
77
|
+
return {
|
|
78
|
+
configureWorker: async (input) => {
|
|
79
|
+
workers.add(input.name);
|
|
80
|
+
return await createTunnelProxyWorker({
|
|
81
|
+
api: input.api,
|
|
82
|
+
name: input.name,
|
|
83
|
+
host: remote.host,
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
close: async () => {
|
|
87
|
+
await proxy.close();
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function createTunnelProxyWorker(input: {
|
|
93
|
+
api: CloudflareApi;
|
|
94
|
+
name: string;
|
|
95
|
+
host: string;
|
|
96
|
+
}) {
|
|
97
|
+
const script = await getInternalWorkerBundle("tunnel-proxy");
|
|
98
|
+
const [accountSubdomain, workerSubdomainStatus] = await Promise.all([
|
|
99
|
+
getAccountSubdomain(input.api),
|
|
100
|
+
getWorkerSubdomain(input.api, input.name),
|
|
101
|
+
putWorker(input.api, {
|
|
102
|
+
workerName: input.name,
|
|
103
|
+
scriptBundle: script.bundle,
|
|
104
|
+
compatibilityDate: "2025-09-01",
|
|
105
|
+
compatibilityFlags: [],
|
|
106
|
+
bindings: {
|
|
107
|
+
WORKER_NAME: input.name,
|
|
108
|
+
TUNNEL_HOST: input.host,
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
]);
|
|
112
|
+
if (!workerSubdomainStatus.enabled) {
|
|
113
|
+
await enableWorkerSubdomain(input.api, input.name);
|
|
114
|
+
}
|
|
115
|
+
return new URL(`https://${input.name}.${accountSubdomain}.workers.dev`);
|
|
116
|
+
}
|
package/src/cloudflare/next.ts
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createCloudflareApi, type CloudflareApiOptions } from "./api.ts";
|
|
2
|
+
import { findSecretsStoreByName, SecretsStore } from "./secrets-store.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Properties for referencing an existing Secret in a Secrets Store (without managing its value)
|
|
6
|
+
*/
|
|
7
|
+
export interface SecretRefProps extends CloudflareApiOptions {
|
|
8
|
+
name: string;
|
|
9
|
+
store?: SecretsStore;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type SecretRef = {
|
|
13
|
+
type: "secrets_store_secret";
|
|
14
|
+
name: string;
|
|
15
|
+
storeId: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* SecretRef references an existing secret by name in a Secrets Store.
|
|
20
|
+
*
|
|
21
|
+
* It does not create or update the secret value – use {@link Secret} for that.
|
|
22
|
+
*
|
|
23
|
+
* Behavior:
|
|
24
|
+
* - Resolves the target secrets store ID (defaults to Cloudflare's `default_secrets_store`, creating/adopting if needed)
|
|
25
|
+
* - Exposes a binding of type `secrets_store_secret` usable in Worker bindings
|
|
26
|
+
* - Update with a different name will trigger resource replacement
|
|
27
|
+
* - Delete is a no-op in Cloudflare (resource removed from state only)
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Reference an existing secret in the default store and bind to a Worker
|
|
31
|
+
* const apiKeyRef = await SecretRef("api-key-ref", { name: "API_KEY" });
|
|
32
|
+
* const worker = await Worker("my-worker", {
|
|
33
|
+
* entrypoint: "./src/worker.ts",
|
|
34
|
+
* url: true,
|
|
35
|
+
* bindings: {
|
|
36
|
+
* API_KEY: apiKeyRef,
|
|
37
|
+
* },
|
|
38
|
+
* });
|
|
39
|
+
*/
|
|
40
|
+
export async function SecretRef(props: SecretRefProps): Promise<SecretRef> {
|
|
41
|
+
const api = await createCloudflareApi(props);
|
|
42
|
+
|
|
43
|
+
const storeId =
|
|
44
|
+
props.store?.id ??
|
|
45
|
+
(await findSecretsStoreByName(api, SecretsStore.Default))?.id;
|
|
46
|
+
|
|
47
|
+
if (!storeId) {
|
|
48
|
+
throw new Error(`Secrets store ${SecretsStore.Default} not found`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
type: "secrets_store_secret",
|
|
53
|
+
name: props.name,
|
|
54
|
+
storeId,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -3,11 +3,10 @@ import { logger } from "../util/logger.ts";
|
|
|
3
3
|
import { memoize } from "../util/memoize.ts";
|
|
4
4
|
import { extractCloudflareResult } from "./api-response.ts";
|
|
5
5
|
import type { CloudflareApi } from "./api.ts";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
type WorkerBindingSpec,
|
|
6
|
+
import type {
|
|
7
|
+
Bindings,
|
|
8
|
+
WorkerBindingDurableObjectNamespace,
|
|
9
|
+
WorkerBindingSpec,
|
|
11
10
|
} from "./bindings.ts";
|
|
12
11
|
import { isContainer, type Container } from "./container.ts";
|
|
13
12
|
import {
|
|
@@ -392,11 +391,12 @@ export async function prepareWorkerMetadata(
|
|
|
392
391
|
name: bindingName,
|
|
393
392
|
text: binding,
|
|
394
393
|
});
|
|
395
|
-
} else if (binding === Self) {
|
|
394
|
+
} else if (binding.type === "cloudflare::Worker::Self") {
|
|
396
395
|
meta.bindings.push({
|
|
397
396
|
type: "service",
|
|
398
397
|
name: bindingName,
|
|
399
398
|
service: props.workerName,
|
|
399
|
+
entrypoint: binding.__entrypoint__,
|
|
400
400
|
});
|
|
401
401
|
} else if (binding.type === "d1") {
|
|
402
402
|
meta.bindings.push({
|
|
@@ -416,6 +416,8 @@ export async function prepareWorkerMetadata(
|
|
|
416
416
|
type: "service",
|
|
417
417
|
name: bindingName,
|
|
418
418
|
service: "service" in binding ? binding.service : binding.name,
|
|
419
|
+
entrypoint:
|
|
420
|
+
"__entrypoint__" in binding ? binding.__entrypoint__ : undefined,
|
|
419
421
|
});
|
|
420
422
|
} else if (binding.type === "durable_object_namespace") {
|
|
421
423
|
meta.bindings.push({
|
|
@@ -491,12 +493,6 @@ export async function prepareWorkerMetadata(
|
|
|
491
493
|
name: bindingName,
|
|
492
494
|
index_name: binding.name,
|
|
493
495
|
});
|
|
494
|
-
} else if (binding.type === "ai_gateway") {
|
|
495
|
-
// AI Gateway binding - just needs the name property
|
|
496
|
-
meta.bindings.push({
|
|
497
|
-
type: "ai",
|
|
498
|
-
name: bindingName,
|
|
499
|
-
});
|
|
500
496
|
} else if (binding.type === "hyperdrive") {
|
|
501
497
|
// Hyperdrive binding
|
|
502
498
|
meta.bindings.push({
|
|
@@ -510,6 +506,12 @@ export async function prepareWorkerMetadata(
|
|
|
510
506
|
name: bindingName,
|
|
511
507
|
});
|
|
512
508
|
} else if (binding.type === "ai") {
|
|
509
|
+
const existing = meta.bindings.find((b) => b.type === "ai");
|
|
510
|
+
if (existing) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`Workers cannot have multiple AI bindings. Binding "${bindingName}" conflicts with "${existing.name}".`,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
513
515
|
meta.bindings.push({
|
|
514
516
|
type: "ai",
|
|
515
517
|
name: bindingName,
|