alchemy 0.46.0 → 0.47.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 (61) hide show
  1. package/bin/alchemy.mjs +713 -255
  2. package/bin/commands/login.ts +2 -5
  3. package/lib/build-date.d.ts +1 -1
  4. package/lib/build-date.js +1 -1
  5. package/lib/cloudflare/api.d.ts +9 -5
  6. package/lib/cloudflare/api.d.ts.map +1 -1
  7. package/lib/cloudflare/api.js +6 -46
  8. package/lib/cloudflare/api.js.map +1 -1
  9. package/lib/cloudflare/auth.d.ts +16 -35
  10. package/lib/cloudflare/auth.d.ts.map +1 -1
  11. package/lib/cloudflare/auth.js +54 -24
  12. package/lib/cloudflare/auth.js.map +1 -1
  13. package/lib/cloudflare/{auth-wrangler.d.ts → oauth.d.ts} +11 -14
  14. package/lib/cloudflare/oauth.d.ts.map +1 -0
  15. package/lib/cloudflare/{auth-wrangler.js → oauth.js} +101 -111
  16. package/lib/cloudflare/oauth.js.map +1 -0
  17. package/lib/cloudflare/user.d.ts +1 -1
  18. package/lib/cloudflare/user.d.ts.map +1 -1
  19. package/lib/cloudflare/user.js +20 -29
  20. package/lib/cloudflare/user.js.map +1 -1
  21. package/lib/cloudflare/worker-metadata.d.ts +6 -0
  22. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  23. package/lib/cloudflare/worker-metadata.js +1 -0
  24. package/lib/cloudflare/worker-metadata.js.map +1 -1
  25. package/lib/cloudflare/worker.d.ts +27 -1
  26. package/lib/cloudflare/worker.d.ts.map +1 -1
  27. package/lib/cloudflare/worker.js +8 -3
  28. package/lib/cloudflare/worker.js.map +1 -1
  29. package/lib/cloudflare/zone.d.ts +1 -1
  30. package/lib/cloudflare/zone.d.ts.map +1 -1
  31. package/lib/cloudflare/zone.js +38 -56
  32. package/lib/cloudflare/zone.js.map +1 -1
  33. package/lib/util/memoize.d.ts +0 -1
  34. package/lib/util/memoize.d.ts.map +1 -1
  35. package/lib/util/memoize.js +0 -13
  36. package/lib/util/memoize.js.map +1 -1
  37. package/lib/util/neverthrow.d.ts +7 -0
  38. package/lib/util/neverthrow.d.ts.map +1 -1
  39. package/lib/util/neverthrow.js +17 -0
  40. package/lib/util/neverthrow.js.map +1 -1
  41. package/lib/util/telemetry/constants.js +2 -2
  42. package/lib/util/telemetry/constants.js.map +1 -1
  43. package/package.json +2 -1
  44. package/src/build-date.ts +1 -1
  45. package/src/cloudflare/api.ts +18 -57
  46. package/src/cloudflare/auth.ts +84 -68
  47. package/src/cloudflare/{auth-wrangler.ts → oauth.ts} +156 -169
  48. package/src/cloudflare/user.ts +33 -51
  49. package/src/cloudflare/worker-metadata.ts +7 -0
  50. package/src/cloudflare/worker.ts +40 -4
  51. package/src/cloudflare/zone.ts +50 -70
  52. package/src/util/memoize.ts +0 -17
  53. package/src/util/neverthrow.ts +28 -0
  54. package/src/util/telemetry/constants.ts +2 -2
  55. package/lib/cloudflare/auth-wrangler.d.ts.map +0 -1
  56. package/lib/cloudflare/auth-wrangler.js.map +0 -1
  57. package/lib/util/xdg-paths.d.ts +0 -9
  58. package/lib/util/xdg-paths.d.ts.map +0 -1
  59. package/lib/util/xdg-paths.js +0 -41
  60. package/lib/util/xdg-paths.js.map +0 -1
  61. package/src/util/xdg-paths.ts +0 -48
@@ -1,18 +1,24 @@
1
1
  import kleur from "kleur";
2
- import { err, ok, ResultAsync } from "neverthrow";
2
+ import { err, ok, okAsync, ResultAsync } from "neverthrow";
3
+ import assert from "node:assert";
3
4
  import crypto from "node:crypto";
4
5
  import fs from "node:fs/promises";
5
6
  import os from "node:os";
6
7
  import path from "node:path";
7
8
  import open from "open";
9
+ import xdgAppPaths from "xdg-app-paths";
8
10
  import { HTTPServer } from "../util/http-server.ts";
9
- import { memoize, memoizeSync } from "../util/memoize.ts";
10
- import { detached, ensure, singleFlight } from "../util/neverthrow.ts";
11
- import { createXdgAppPaths } from "../util/xdg-paths.ts";
11
+ import { memoize } from "../util/memoize.ts";
12
+ import {
13
+ detached,
14
+ ensure,
15
+ fetchJSON,
16
+ HTTPError,
17
+ singleFlight,
18
+ } from "../util/neverthrow.ts";
12
19
 
13
20
  const CLIENT_ID = "54d11594-84e4-41aa-b438-e81b8fa78ee7";
14
21
  const REDIRECT_URI = "http://localhost:8976/oauth/callback";
15
- const ALCHEMY_BASE_URL = "https://alchemy.run";
16
22
  export const DEFAULT_SCOPES = [
17
23
  "account:read",
18
24
  "user:read",
@@ -32,6 +38,7 @@ export const DEFAULT_SCOPES = [
32
38
  "containers:write",
33
39
  "cloudchamber:write",
34
40
  ];
41
+ const ALCHEMY_BASE_URL = "https://alchemy.run";
35
42
 
36
43
  interface WranglerConfig {
37
44
  oauth_token: string;
@@ -40,8 +47,30 @@ interface WranglerConfig {
40
47
  scopes: string[];
41
48
  }
42
49
 
50
+ let cached: WranglerConfig | undefined;
51
+
52
+ export const getRefreshedWranglerConfig = singleFlight(() =>
53
+ readWranglerConfig()
54
+ .andThen((config) => {
55
+ if (config.expiration_time.getTime() > Date.now() + 10 * 1000) {
56
+ return ok(config);
57
+ }
58
+ return fetchToken({
59
+ grant_type: "refresh_token",
60
+ refresh_token: config.refresh_token,
61
+ client_id: CLIENT_ID,
62
+ });
63
+ })
64
+ .orElse((error) => {
65
+ if (isInteractive()) {
66
+ return wranglerLogin();
67
+ }
68
+ return err(error);
69
+ }),
70
+ );
71
+
43
72
  const getWranglerConfigPath = memoize(async () => {
44
- const xdgConfigDir = createXdgAppPaths(".wrangler").config();
73
+ const xdgConfigDir = xdgAppPaths(".wrangler").config();
45
74
  const legacyConfigDir = path.join(os.homedir(), ".wrangler");
46
75
  const configDir = (await fs
47
76
  .stat(legacyConfigDir)
@@ -53,38 +82,35 @@ const getWranglerConfigPath = memoize(async () => {
53
82
  });
54
83
 
55
84
  const readWranglerConfig = () => {
56
- return ResultAsync.fromSafePromise(getWranglerConfigPath())
57
- .andThen((path) =>
58
- ResultAsync.fromPromise(
59
- fs.readFile(path, "utf-8").then(async (text) => {
60
- const toml = await import("@iarna/toml");
61
- return toml.parse(text.replace(/\r\n/g, "\n"));
62
- }),
63
- () => new Error("Failed to read Wrangler config."),
64
- ),
65
- )
66
- .andThen((config) => {
67
- if (
68
- typeof config.oauth_token !== "string" ||
69
- typeof config.refresh_token !== "string" ||
70
- !config.expiration_time ||
71
- !Array.isArray(config.scopes)
72
- ) {
73
- return err(new Error("Invalid Wrangler config."));
74
- }
75
- return ok({
85
+ if (cached) {
86
+ return okAsync(cached);
87
+ }
88
+ return ResultAsync.fromPromise(
89
+ getWranglerConfigPath().then(async (path): Promise<WranglerConfig> => {
90
+ const text = await fs.readFile(path, "utf-8");
91
+ const toml = await import("@iarna/toml");
92
+ const config = toml.parse(text.replace(/\r\n/g, "\n"));
93
+ assert(typeof config.oauth_token === "string");
94
+ assert(typeof config.refresh_token === "string");
95
+ assert(!!config.expiration_time);
96
+ assert(Array.isArray(config.scopes));
97
+ return {
76
98
  oauth_token: config.oauth_token,
77
99
  refresh_token: config.refresh_token,
78
100
  expiration_time: new Date(config.expiration_time as any),
79
- scopes: Array.from(config.scopes as string[]),
80
- } as WranglerConfig);
81
- });
101
+ scopes: config.scopes as string[],
102
+ };
103
+ }),
104
+ () => new Error("Cloudflare credentials are missing or invalid."),
105
+ );
82
106
  };
83
107
 
84
108
  const writeWranglerConfig = (config: WranglerConfig) => {
109
+ cached = config;
85
110
  return ResultAsync.fromSafePromise(getWranglerConfigPath()).map(
86
111
  async (configPath) => {
87
112
  const toml = await import("@iarna/toml");
113
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
88
114
  await fs.writeFile(
89
115
  configPath,
90
116
  toml.stringify({
@@ -98,140 +124,6 @@ const writeWranglerConfig = (config: WranglerConfig) => {
98
124
  );
99
125
  };
100
126
 
101
- const fetchToken = (
102
- body:
103
- | {
104
- grant_type: "refresh_token";
105
- refresh_token: string;
106
- client_id: string;
107
- }
108
- | {
109
- grant_type: "authorization_code";
110
- code: string;
111
- code_verifier: string;
112
- client_id: string;
113
- redirect_uri: string;
114
- },
115
- ) => {
116
- return ResultAsync.fromPromise(
117
- fetch("https://dash.cloudflare.com/oauth2/token", {
118
- method: "POST",
119
- headers: {
120
- "Content-Type": "application/x-www-form-urlencoded",
121
- },
122
- body: new URLSearchParams(body),
123
- }),
124
- () =>
125
- new OAuthError({
126
- error: "fetch_error",
127
- error_description: "Failed to fetch OAuth token.",
128
- status_code: 0,
129
- }),
130
- )
131
- .andThen((res) =>
132
- ResultAsync.fromPromise(
133
- res.json(),
134
- () =>
135
- new OAuthError({
136
- error: "invalid_response",
137
- error_description: "The server returned an invalid response.",
138
- status_code: res.status,
139
- }),
140
- ).andThen((json) =>
141
- res.ok ? ok(json as OAuthTokens) : err(new OAuthError(json as any)),
142
- ),
143
- )
144
- .map(
145
- (tokens): WranglerConfig => ({
146
- oauth_token: tokens.access_token,
147
- refresh_token: tokens.refresh_token,
148
- expiration_time: new Date(Date.now() + tokens.expires_in * 1000),
149
- scopes: tokens.scope.split(" "),
150
- }),
151
- )
152
- .andTee((config) => writeWranglerConfig(config));
153
- };
154
-
155
- interface OAuthTokens {
156
- access_token: string;
157
- refresh_token: string;
158
- expires_in: number;
159
- scope: string;
160
- }
161
-
162
- export const getRefreshedWranglerConfig = singleFlight(
163
- memoizeSync(
164
- () => {
165
- return readWranglerConfig()
166
- .andThen((config) => {
167
- if (config.expiration_time.getTime() > Date.now() + 10 * 1000) {
168
- return ok(config);
169
- }
170
- return fetchToken({
171
- grant_type: "refresh_token",
172
- refresh_token: config.refresh_token,
173
- client_id: CLIENT_ID,
174
- });
175
- })
176
- .orElse((error) => {
177
- if (isInteractive()) {
178
- return wranglerLogin();
179
- }
180
- if (error instanceof OAuthError) {
181
- return err(error);
182
- }
183
- return ok(null);
184
- });
185
- },
186
- () => Math.floor(Date.now() / 5000).toString(),
187
- ),
188
- );
189
-
190
- const isInteractive = () => {
191
- return process.stdin.isTTY && process.stdout.isTTY && !process.env.CI;
192
- };
193
-
194
- const generateAuthorizationURL = (scopes: string[]) => {
195
- const state = crypto.randomBytes(32).toString("base64url");
196
- const verifier = crypto.randomBytes(96).toString("base64url");
197
- const challenge = crypto
198
- .createHash("sha256")
199
- .update(verifier)
200
- .digest("base64url");
201
- const url = new URL("https://dash.cloudflare.com/oauth2/auth");
202
- url.search = new URLSearchParams({
203
- response_type: "code",
204
- client_id: CLIENT_ID,
205
- redirect_uri: REDIRECT_URI,
206
- scope: [...scopes, "offline_access"].join(" "),
207
- state,
208
- code_challenge: challenge,
209
- code_challenge_method: "S256",
210
- }).toString();
211
- return { url: url.toString(), state, verifier };
212
- };
213
-
214
- class OAuthError extends Error {
215
- readonly error: string;
216
- readonly error_verbose?: string;
217
- readonly error_hint?: string;
218
- readonly status_code: number;
219
- constructor(params: {
220
- error: string;
221
- error_verbose?: string;
222
- error_description: string;
223
- error_hint?: string;
224
- status_code: number;
225
- }) {
226
- console.dir(params, { depth: null });
227
- super(params.error_description);
228
- this.error = params.error;
229
- this.error_verbose = params.error_verbose;
230
- this.error_hint = params.error_hint;
231
- this.status_code = params.status_code;
232
- }
233
- }
234
-
235
127
  export const wranglerLogin = (
236
128
  scopes: string[] = DEFAULT_SCOPES,
237
129
  log: (message: string) => void = console.log, // using console.log instead of logger.log to avoid bundling hell with CLI
@@ -249,6 +141,9 @@ export const wranglerLogin = (
249
141
  );
250
142
  open(challenge.url);
251
143
  const { result, ok, err } = detached<WranglerConfig, OAuthError>();
144
+ const renderResponse = async (type: "success" | "error") => {
145
+ return Response.redirect(`${ALCHEMY_BASE_URL}/auth/${type}`);
146
+ };
252
147
  const server = new HTTPServer({
253
148
  port: 8976,
254
149
  fetch: async (request) => {
@@ -268,7 +163,7 @@ export const wranglerLogin = (
268
163
  status_code: 400,
269
164
  }),
270
165
  );
271
- return Response.redirect(`${ALCHEMY_BASE_URL}/auth/error`);
166
+ return renderResponse("error");
272
167
  }
273
168
  if (!code || !state || state !== challenge.state) {
274
169
  err(
@@ -278,7 +173,7 @@ export const wranglerLogin = (
278
173
  status_code: 400,
279
174
  }),
280
175
  );
281
- return Response.redirect(`${ALCHEMY_BASE_URL}/auth/error`);
176
+ return renderResponse("error");
282
177
  }
283
178
  const tokens = await fetchToken({
284
179
  grant_type: "authorization_code",
@@ -289,10 +184,10 @@ export const wranglerLogin = (
289
184
  });
290
185
  if (tokens.isErr()) {
291
186
  err(tokens.error);
292
- return Response.redirect(`${ALCHEMY_BASE_URL}/auth/error`);
187
+ return renderResponse("error");
293
188
  }
294
189
  ok(tokens.value);
295
- return Response.redirect(`${ALCHEMY_BASE_URL}/auth/success`);
190
+ return renderResponse("success");
296
191
  },
297
192
  });
298
193
  const timeout = setTimeout(
@@ -307,8 +202,100 @@ export const wranglerLogin = (
307
202
  },
308
203
  1000 * 60 * 5,
309
204
  );
310
- return ensure(result, async () => {
205
+ return ensure(result, () => {
311
206
  clearTimeout(timeout);
312
- await server.stop();
207
+ void server.stop();
313
208
  });
314
209
  };
210
+
211
+ const generateAuthorizationURL = (scopes: string[]) => {
212
+ const state = crypto.randomBytes(32).toString("base64url");
213
+ const verifier = crypto.randomBytes(96).toString("base64url");
214
+ const challenge = crypto
215
+ .createHash("sha256")
216
+ .update(verifier)
217
+ .digest("base64url");
218
+ const url = new URL("https://dash.cloudflare.com/oauth2/auth");
219
+ url.search = new URLSearchParams({
220
+ response_type: "code",
221
+ client_id: CLIENT_ID,
222
+ redirect_uri: "http://localhost:8976/oauth/callback",
223
+ scope: [...scopes, "offline_access"].join(" "),
224
+ state,
225
+ code_challenge: challenge,
226
+ code_challenge_method: "S256",
227
+ }).toString();
228
+ return { url: url.toString(), state, verifier };
229
+ };
230
+
231
+ interface OAuthTokens {
232
+ access_token: string;
233
+ refresh_token: string;
234
+ expires_in: number;
235
+ scope: string;
236
+ }
237
+
238
+ interface OAuthErrorParams {
239
+ error: string;
240
+ error_description: string;
241
+ error_verbose?: string;
242
+ error_hint?: string;
243
+ status_code: number;
244
+ }
245
+
246
+ class OAuthError extends Error {
247
+ constructor(params: OAuthErrorParams) {
248
+ super(
249
+ `Cloudflare authentication failed with error ${params.error} (${params.status_code}): ${params.error_description}`,
250
+ );
251
+ }
252
+ }
253
+
254
+ const fetchToken = (
255
+ body:
256
+ | {
257
+ grant_type: "refresh_token";
258
+ refresh_token: string;
259
+ client_id: string;
260
+ }
261
+ | {
262
+ grant_type: "authorization_code";
263
+ code: string;
264
+ code_verifier: string;
265
+ client_id: string;
266
+ redirect_uri: string;
267
+ },
268
+ ) => {
269
+ return fetchJSON<OAuthTokens, OAuthErrorParams>(
270
+ "https://dash.cloudflare.com/oauth2/token",
271
+ {
272
+ method: "POST",
273
+ headers: {
274
+ "Content-Type": "application/x-www-form-urlencoded",
275
+ },
276
+ body: new URLSearchParams(body),
277
+ },
278
+ )
279
+ .map(
280
+ (tokens): WranglerConfig => ({
281
+ oauth_token: tokens.access_token,
282
+ refresh_token: tokens.refresh_token,
283
+ expiration_time: new Date(Date.now() + tokens.expires_in * 1000),
284
+ scopes: tokens.scope.split(" "),
285
+ }),
286
+ )
287
+ .mapErr((error) =>
288
+ error instanceof HTTPError
289
+ ? new OAuthError({
290
+ error: "http_error",
291
+ error_description: error.message,
292
+ status_code: error.status,
293
+ })
294
+ : new OAuthError(error),
295
+ )
296
+ .andTee((config) => writeWranglerConfig(config));
297
+ };
298
+
299
+ const isInteractive = () => {
300
+ return process.stdin.isTTY && process.stdout.isTTY && !process.env.CI;
301
+ };
@@ -1,7 +1,7 @@
1
1
  import type { Secret } from "../secret.ts";
2
2
  import { logger } from "../util/logger.ts";
3
3
  import { memoize } from "../util/memoize.ts";
4
- import { CloudflareApiError } from "./api-error.ts";
4
+ import { extractCloudflareResult } from "./api-response.ts";
5
5
  import {
6
6
  getCloudflareAuthHeaders,
7
7
  type CloudflareAuthOptions,
@@ -56,66 +56,48 @@ export interface CloudflareOrganization {
56
56
  roles: string[];
57
57
  }
58
58
 
59
- export const getCloudflareAccounts = memoize(
60
- async (options: CloudflareAuthOptions): Promise<CloudflareAccount[]> => {
59
+ export const getCloudflareAccountId = memoize(
60
+ async (options: CloudflareAuthOptions): Promise<string> => {
61
61
  const headers = await getCloudflareAuthHeaders(options);
62
- const accounts = await fetch(
63
- "https://api.cloudflare.com/client/v4/accounts",
64
- {
62
+ const accounts = await extractCloudflareResult<CloudflareAccount[]>(
63
+ "get accounts for authorized user",
64
+ fetch("https://api.cloudflare.com/client/v4/accounts", {
65
65
  headers,
66
- },
66
+ }),
67
67
  );
68
- if (accounts.ok) {
69
- return ((await accounts.json()) as any).result;
70
- } else {
71
- throw new CloudflareApiError(
72
- `Failed to get accounts for authorized user, please make sure you're authenticated (see: https://alchemy.run/guides/cloudflare/) or explicitly set the Cloudflare Account ID (see: https://alchemy.run/guides/cloudflare/#account-id)`,
73
- accounts,
68
+ if (!accounts[0]) {
69
+ throw new Error(
70
+ "No accounts found for authorized user. Please make sure you're authenticated (see: https://alchemy.run/guides/cloudflare/) or explicitly set the Cloudflare Account ID (see: https://alchemy.run/guides/cloudflare/#account-id)",
71
+ );
72
+ }
73
+ if (accounts.length > 1) {
74
+ logger.warnOnce(
75
+ [
76
+ "Multiple Cloudflare accounts found for authorized user:",
77
+ accounts.map((a, i) => `${i + 1}: ${a.name} (${a.id})`).join("\n"),
78
+ "The first account will be used by default. To use a different account, explicitly set the Cloudflare Account ID (see: https://alchemy.run/guides/cloudflare/#account-id)",
79
+ ].join("\n"),
74
80
  );
75
81
  }
82
+ return accounts[0].id;
76
83
  },
77
84
  );
78
85
 
79
86
  export const getUserEmailFromApiKey = memoize(
80
87
  async (apiKey: string): Promise<string> => {
81
- try {
82
- // Call the /user endpoint to get user information
83
- const response = await fetch(
84
- "https://api.cloudflare.com/client/v4/user",
85
- {
86
- headers: {
87
- "Content-Type": "application/json",
88
- "X-Auth-Key": apiKey,
89
- },
88
+ const result = await extractCloudflareResult<{
89
+ id: string;
90
+ name: string;
91
+ email: string;
92
+ }>(
93
+ "automatically discover email for API Key authentication",
94
+ fetch("https://api.cloudflare.com/client/v4/user", {
95
+ headers: {
96
+ "Content-Type": "application/json",
97
+ "X-Auth-Key": apiKey,
90
98
  },
91
- );
92
-
93
- if (!response.ok) {
94
- throw new Error(
95
- `Failed to get user information: ${response.status} ${response.statusText}`,
96
- );
97
- }
98
-
99
- const data = (await response.json()) as {
100
- success: boolean;
101
- result: {
102
- id: string;
103
- email: string;
104
- name: string;
105
- [key: string]: any;
106
- };
107
- };
108
-
109
- if (!data.success || !data.result || !data.result.email) {
110
- throw new Error("Cloudflare API did not return valid user information");
111
- }
112
-
113
- return data.result.email;
114
- } catch (error) {
115
- logger.error("Error retrieving email from Cloudflare API:", error);
116
- throw new Error(
117
- "Failed to automatically discover email for API Key authentication",
118
- );
119
- }
99
+ }),
100
+ );
101
+ return result.email;
120
102
  },
121
103
  );
@@ -197,6 +197,9 @@ export interface WorkerMetadata {
197
197
  suspended: boolean;
198
198
  }[];
199
199
  containers?: { class_name: string }[];
200
+ placement?: {
201
+ mode: "smart";
202
+ };
200
203
  }
201
204
 
202
205
  export async function prepareWorkerMetadata(
@@ -213,6 +216,9 @@ export async function prepareWorkerMetadata(
213
216
  };
214
217
  tags?: string[];
215
218
  unstable_cacheWorkerSettings?: boolean;
219
+ placement?: {
220
+ mode: "smart";
221
+ };
216
222
  },
217
223
  ): Promise<WorkerMetadata> {
218
224
  const oldSettings = await (props.unstable_cacheWorkerSettings
@@ -336,6 +342,7 @@ export async function prepareWorkerMetadata(
336
342
  transferred_classes: [],
337
343
  new_sqlite_classes: [],
338
344
  },
345
+ placement: props.placement,
339
346
  };
340
347
 
341
348
  const assetUploadResult = props.assetUploadResult;
@@ -28,6 +28,7 @@ import { CloudflareApiError, handleApiError } from "./api-error.ts";
28
28
  import {
29
29
  type CloudflareApi,
30
30
  type CloudflareApiOptions,
31
+ type InternalCloudflareApiOptions,
31
32
  createCloudflareApi,
32
33
  } from "./api.ts";
33
34
  import type { Assets } from "./assets.ts";
@@ -88,6 +89,10 @@ import { WorkerSubdomain, disableWorkerSubdomain } from "./worker-subdomain.ts";
88
89
  import { createTail } from "./worker-tail.ts";
89
90
  import { Workflow, isWorkflow, upsertWorkflow } from "./workflow.ts";
90
91
 
92
+ // Previous versions of `Worker` used the `Bundle` resource.
93
+ // This import is here to avoid errors when destroying the `Bundle` resource.
94
+ import "../esbuild/bundle.ts";
95
+
91
96
  /**
92
97
  * Configuration options for static assets
93
98
  */
@@ -176,6 +181,8 @@ export interface BaseWorkerProps<
176
181
  * Environment variables to attach to the worker
177
182
  *
178
183
  * These will be converted to plain_text bindings
184
+ *
185
+ * @deprecated - use `bindings` instead
179
186
  */
180
187
  env?: {
181
188
  [key: string]: string;
@@ -364,6 +371,24 @@ export interface BaseWorkerProps<
364
371
  url: string;
365
372
  cwd?: string;
366
373
  };
374
+
375
+ /**
376
+ * Smart placement configuration for the worker.
377
+ *
378
+ * Controls how Cloudflare places the worker across its network for optimal performance.
379
+ *
380
+ * When omitted, smart placement is disabled (default behavior).
381
+ */
382
+ placement?: {
383
+ /**
384
+ * The placement mode for the worker.
385
+ *
386
+ * - "smart": Automatically optimize placement based on performance metrics
387
+ *
388
+ * @default undefined (smart placement disabled)
389
+ */
390
+ mode: "smart";
391
+ };
367
392
  }
368
393
 
369
394
  export interface InlineWorkerProps<
@@ -558,6 +583,13 @@ export type Worker<
558
583
  * Version label for this worker deployment
559
584
  */
560
585
  version?: string;
586
+
587
+ /**
588
+ * Smart placement configuration for the worker
589
+ */
590
+ placement?: {
591
+ mode: "smart";
592
+ };
561
593
  };
562
594
 
563
595
  /**
@@ -1086,6 +1118,8 @@ export const _Worker = Resource(
1086
1118
  assets: props.assets,
1087
1119
  // Include cron triggers in the output
1088
1120
  crons: props.crons,
1121
+ // Include placement configuration in the output
1122
+ placement: props.placement,
1089
1123
  // phantom property
1090
1124
  Env: undefined!,
1091
1125
  } as unknown as Worker<B>);
@@ -1378,6 +1412,8 @@ export const _Worker = Resource(
1378
1412
  namespace: props.namespace,
1379
1413
  // Include version information in the output
1380
1414
  version: props.version,
1415
+ // Include placement configuration in the output
1416
+ placement: props.placement,
1381
1417
  // phantom property
1382
1418
  Env: undefined!,
1383
1419
  } as unknown as Worker<B>);
@@ -1487,12 +1523,12 @@ const normalizeExportBindings = (
1487
1523
  );
1488
1524
  };
1489
1525
 
1490
- const normalizeApiOptions = (api: CloudflareApi): CloudflareApiOptions => ({
1526
+ const normalizeApiOptions = (
1527
+ api: CloudflareApi,
1528
+ ): InternalCloudflareApiOptions => ({
1491
1529
  accountId: api.accountId,
1492
- apiKey: api.apiKey,
1493
- apiToken: api.apiToken,
1494
- email: api.email,
1495
1530
  baseUrl: api.baseUrl,
1531
+ ...api.authOptions,
1496
1532
  });
1497
1533
 
1498
1534
  async function provisionContainers(