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
@@ -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
- Self,
7
- type Binding,
8
- type Bindings,
9
- type WorkerBindingService,
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
- case "ai_gateway": {
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
- (options.hyperdrives ??= {})[key] = binding.dev.origin.unencrypted;
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 (typeof binding === "string" || binding === Self) {
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 { MiniflareWorkerProxy } from "./miniflare-worker-proxy.ts";
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
- const proxy = new MiniflareWorkerProxy({
44
- name: input.name,
45
- port: input.port ?? (await findOpenPort()),
46
- miniflare,
47
- });
48
- this.localProxies.set(input.name, proxy);
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 ${proxy.url}`,
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 proxy.url;
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 MiniflareWorkerProxyOptions {
10
- name: string;
11
- port: number;
12
- miniflare: miniflare.Miniflare;
7
+ export interface MiniflareWorkerProxy {
8
+ url: URL;
9
+ close: () => Promise<void>;
13
10
  }
14
11
 
15
- export class MiniflareWorkerProxy {
16
- private server = http.createServer();
17
- private wss = new WebSocketServer({ noServer: true });
18
-
19
- constructor(private readonly options: MiniflareWorkerProxyOptions) {
20
- this.server.on("upgrade", async (req, socket, head) => {
21
- await this.handleUpgrade(req, socket, head);
22
- });
23
- this.server.on("request", async (req, res) => {
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
- private async handleUpgrade(
48
- req: http.IncomingMessage,
49
- socket: Stream.Duplex,
50
- head: Buffer,
51
- ) {
52
- const server = await this.createServerWebSocket(req);
53
- if (!server) {
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
- this.wss.handleUpgrade(req, socket, head, (client) => {
58
- client.on("message", (event, binary) => server.send(event, { binary }));
59
- client.on("close", (code, reason) => server.close(code, reason));
60
- server.on("message", (event, binary) => client.send(event, { binary }));
61
- server.on("close", (code, reason) => client.close(code, reason));
62
- this.wss.emit("connection", client, req);
63
- });
64
- }
65
-
66
- private async createServerWebSocket(req: http.IncomingMessage) {
67
- const target = await this.options.miniflare.unsafeGetDirectURL(
68
- this.options.name,
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
- return null;
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
- const controller = new AbortController();
83
- return await Promise.race([
84
- once(server, "open", { signal: controller.signal }).then(() => server),
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
- private async handleRequest(
96
- req: http.IncomingMessage,
97
- res: http.ServerResponse,
98
- ) {
99
- const worker = await this.options.miniflare.getWorker(this.options.name);
100
- if (!worker) {
101
- res.statusCode = 503;
102
- res.end(`[Alchemy] The worker "${this.options.name}" is not running.`);
103
- return;
104
- }
105
- try {
106
- const response = await worker.fetch(toMiniflareRequest(req));
107
- writeMiniflareResponseToNode(response, res);
108
- } catch (rawError) {
109
- const message =
110
- rawError instanceof Error
111
- ? (rawError.stack ?? rawError.message)
112
- : String(rawError);
113
- res.statusCode = 500;
114
- res.end(
115
- `[Alchemy] The worker "${this.options.name}" threw an error:\n\n${message}`,
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 toMiniflareRequest = (req: http.IncomingMessage) => {
102
+ const parseIncomingMessage = (req: http.IncomingMessage): RequestInfo => {
122
103
  const method = req.method ?? "GET";
123
- const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
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 new miniflare.Request(url, {
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
+ }
@@ -33,6 +33,9 @@ export async function Nextjs<const B extends Bindings>(
33
33
  );
34
34
  }
35
35
  return await Website(id, {
36
+ bundle: {
37
+ minify: true,
38
+ },
36
39
  ...props,
37
40
  entrypoint: props.entrypoint ?? ".open-next/worker.js",
38
41
  wrangler: {
@@ -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
- Self,
8
- type Bindings,
9
- type WorkerBindingDurableObjectNamespace,
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,