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.
- package/bin/alchemy.mjs +713 -255
- package/bin/commands/login.ts +2 -5
- package/lib/build-date.d.ts +1 -1
- package/lib/build-date.js +1 -1
- package/lib/cloudflare/api.d.ts +9 -5
- package/lib/cloudflare/api.d.ts.map +1 -1
- package/lib/cloudflare/api.js +6 -46
- package/lib/cloudflare/api.js.map +1 -1
- package/lib/cloudflare/auth.d.ts +16 -35
- package/lib/cloudflare/auth.d.ts.map +1 -1
- package/lib/cloudflare/auth.js +54 -24
- package/lib/cloudflare/auth.js.map +1 -1
- package/lib/cloudflare/{auth-wrangler.d.ts → oauth.d.ts} +11 -14
- package/lib/cloudflare/oauth.d.ts.map +1 -0
- package/lib/cloudflare/{auth-wrangler.js → oauth.js} +101 -111
- package/lib/cloudflare/oauth.js.map +1 -0
- package/lib/cloudflare/user.d.ts +1 -1
- package/lib/cloudflare/user.d.ts.map +1 -1
- package/lib/cloudflare/user.js +20 -29
- package/lib/cloudflare/user.js.map +1 -1
- package/lib/cloudflare/worker-metadata.d.ts +6 -0
- package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
- package/lib/cloudflare/worker-metadata.js +1 -0
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +27 -1
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +8 -3
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/zone.d.ts +1 -1
- package/lib/cloudflare/zone.d.ts.map +1 -1
- package/lib/cloudflare/zone.js +38 -56
- package/lib/cloudflare/zone.js.map +1 -1
- package/lib/util/memoize.d.ts +0 -1
- package/lib/util/memoize.d.ts.map +1 -1
- package/lib/util/memoize.js +0 -13
- package/lib/util/memoize.js.map +1 -1
- package/lib/util/neverthrow.d.ts +7 -0
- package/lib/util/neverthrow.d.ts.map +1 -1
- package/lib/util/neverthrow.js +17 -0
- package/lib/util/neverthrow.js.map +1 -1
- package/lib/util/telemetry/constants.js +2 -2
- package/lib/util/telemetry/constants.js.map +1 -1
- package/package.json +2 -1
- package/src/build-date.ts +1 -1
- package/src/cloudflare/api.ts +18 -57
- package/src/cloudflare/auth.ts +84 -68
- package/src/cloudflare/{auth-wrangler.ts → oauth.ts} +156 -169
- package/src/cloudflare/user.ts +33 -51
- package/src/cloudflare/worker-metadata.ts +7 -0
- package/src/cloudflare/worker.ts +40 -4
- package/src/cloudflare/zone.ts +50 -70
- package/src/util/memoize.ts +0 -17
- package/src/util/neverthrow.ts +28 -0
- package/src/util/telemetry/constants.ts +2 -2
- package/lib/cloudflare/auth-wrangler.d.ts.map +0 -1
- package/lib/cloudflare/auth-wrangler.js.map +0 -1
- package/lib/util/xdg-paths.d.ts +0 -9
- package/lib/util/xdg-paths.d.ts.map +0 -1
- package/lib/util/xdg-paths.js +0 -41
- package/lib/util/xdg-paths.js.map +0 -1
- 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
|
|
10
|
-
import {
|
|
11
|
-
|
|
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 =
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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:
|
|
80
|
-
}
|
|
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
|
|
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
|
|
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
|
|
187
|
+
return renderResponse("error");
|
|
293
188
|
}
|
|
294
189
|
ok(tokens.value);
|
|
295
|
-
return
|
|
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,
|
|
205
|
+
return ensure(result, () => {
|
|
311
206
|
clearTimeout(timeout);
|
|
312
|
-
|
|
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
|
+
};
|
package/src/cloudflare/user.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
60
|
-
async (options: CloudflareAuthOptions): Promise<
|
|
59
|
+
export const getCloudflareAccountId = memoize(
|
|
60
|
+
async (options: CloudflareAuthOptions): Promise<string> => {
|
|
61
61
|
const headers = await getCloudflareAuthHeaders(options);
|
|
62
|
-
const accounts = await
|
|
63
|
-
"
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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;
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -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 = (
|
|
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(
|