alchemy 2.0.0-beta.2 → 2.0.0-beta.4
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-effect.js → alchemy.js} +171 -171
- package/bin/alchemy.js.map +1 -0
- package/bin/{alchemy-effect.sh → alchemy.sh} +2 -2
- package/lib/cli/index.d.ts.map +1 -1
- package/lib/cli/index.js +167 -167
- package/lib/cli/index.js.map +1 -1
- package/package.json +54 -45
- package/src/AWS/AGENTS.md +10 -10
- package/src/AWS/Assets.ts +1 -1
- package/src/AWS/AuthProvider.ts +392 -0
- package/src/AWS/Credentials.ts +0 -1
- package/src/AWS/DynamoDB/Table.ts +63 -10
- package/src/AWS/EC2/VpcEndpoint.ts +4 -4
- package/src/AWS/EC2/hosted.ts +1 -1
- package/src/AWS/ECS/Task.ts +1 -1
- package/src/AWS/Kinesis/Stream.ts +44 -8
- package/src/AWS/Lambda/Function.ts +218 -10
- package/src/AWS/S3/Bucket.ts +26 -19
- package/src/AWS/S3/BucketNotifications.ts +1 -1
- package/src/AWS/SNS/Topic.ts +47 -10
- package/src/AWS/SQS/Queue.ts +66 -0
- package/src/Auth/AuthProvider.ts +33 -0
- package/src/Auth/Credentials.ts +56 -0
- package/src/Auth/Env.ts +45 -0
- package/src/Auth/Profile.ts +72 -0
- package/src/Auth/index.ts +2 -0
- package/src/Cloudflare/Auth/AuthProvider.ts +670 -0
- package/src/Cloudflare/Auth/OAuthClient.ts +315 -0
- package/src/Cloudflare/Container/Container.ts +122 -0
- package/src/Cloudflare/Container/ContainerApplication.ts +6 -3
- package/src/Cloudflare/Container/ContainerBinding.ts +2 -4
- package/src/Cloudflare/Container/StartContainer.ts +1 -1
- package/src/Cloudflare/D1/D1Database.ts +23 -4
- package/src/Cloudflare/KV/KVNamespace.ts +25 -0
- package/src/Cloudflare/Providers.ts +1 -6
- package/src/Cloudflare/R2/R2Bucket.ts +45 -2
- package/src/Cloudflare/Website/StaticSite.ts +101 -15
- package/src/Cloudflare/Website/Vite.ts +86 -20
- package/src/Cloudflare/Workers/Assets.ts +170 -207
- package/src/Cloudflare/Workers/DurableObjectNamespace.ts +629 -0
- package/src/Cloudflare/Workers/DurableObjectState.ts +63 -0
- package/src/Cloudflare/Workers/DurableObjectStorage.ts +256 -0
- package/src/Cloudflare/Workers/DynamicWorkerLoader.ts +66 -7
- package/src/Cloudflare/Workers/InferEnv.ts +11 -7
- package/src/Cloudflare/Workers/Rpc.ts +13 -2
- package/src/Cloudflare/Workers/ScheduledEvents.ts +185 -0
- package/src/Cloudflare/Workers/WebSocket.ts +1 -1
- package/src/Cloudflare/Workers/Worker.ts +572 -67
- package/src/Cloudflare/Workers/Workflow.ts +53 -11
- package/src/Cloudflare/Workers/index.ts +4 -1
- package/src/Construct.ts +2 -2
- package/src/GitHub/Comment.ts +224 -0
- package/src/GitHub/Secret.ts +257 -0
- package/src/GitHub/Variable.ts +166 -0
- package/src/GitHub/index.ts +3 -0
- package/src/Kubernetes/client.ts +2 -2
- package/src/Output.ts +1 -1
- package/src/Platform.ts +10 -5
- package/src/Provider.ts +1 -1
- package/src/Resource.ts +4 -4
- package/src/Test/Vitest.ts +4 -3
- package/src/Util/Clank.ts +77 -0
- package/src/Util/PlatformServices.ts +21 -0
- package/src/Util/dedent.ts +59 -0
- package/src/Util/index.ts +1 -0
- package/bin/alchemy-effect.js.map +0 -1
- package/src/Cloudflare/Workers/DurableObject.ts +0 -527
- package/src/Daemon/Client.ts +0 -116
- package/src/Daemon/Config.ts +0 -28
- package/src/Daemon/Errors.ts +0 -48
- package/src/Daemon/Lock.ts +0 -162
- package/src/Daemon/ProcessRegistry.ts +0 -284
- package/src/Daemon/RpcSchema.ts +0 -43
- package/src/Daemon/RpcServer.ts +0 -231
- package/src/Daemon/index.ts +0 -27
- package/src/Spawn.ts +0 -23
- /package/bin/{alchemy-effect.ts → alchemy.ts} +0 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import * as cfAccounts from "@distilled.cloud/cloudflare/accounts";
|
|
2
|
+
import * as CfCredentialsModule from "@distilled.cloud/cloudflare/Credentials";
|
|
3
|
+
import * as Console from "effect/Console";
|
|
4
|
+
import * as Duration from "effect/Duration";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as FileSystem from "effect/FileSystem";
|
|
7
|
+
import * as Layer from "effect/Layer";
|
|
8
|
+
import * as Match from "effect/Match";
|
|
9
|
+
import * as Path from "effect/Path";
|
|
10
|
+
import * as Redacted from "effect/Redacted";
|
|
11
|
+
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
12
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
13
|
+
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
|
|
14
|
+
import { AuthError, type AuthProvider } from "../../Auth/AuthProvider.ts";
|
|
15
|
+
import {
|
|
16
|
+
deleteCredentials,
|
|
17
|
+
displayRedacted,
|
|
18
|
+
readCredentials,
|
|
19
|
+
writeCredentials,
|
|
20
|
+
} from "../../Auth/Credentials.ts";
|
|
21
|
+
import { getEnv, getEnvRedacted, retryOnce } from "../../Auth/Env.ts";
|
|
22
|
+
import * as Clank from "../../Util/Clank.ts";
|
|
23
|
+
import * as OAuthClient from "./OAuthClient.ts";
|
|
24
|
+
|
|
25
|
+
export type CloudflareAuthConfig =
|
|
26
|
+
| { method: "env"; accountId?: string }
|
|
27
|
+
| { method: "stored"; credentialType: "apiToken"; accountId?: string }
|
|
28
|
+
| { method: "stored"; credentialType: "apiKey"; accountId?: string }
|
|
29
|
+
| { method: "oauth"; scopes: string[]; accountId?: string };
|
|
30
|
+
|
|
31
|
+
const options: Array<{
|
|
32
|
+
value: CloudflareAuthConfig["method"];
|
|
33
|
+
label: string;
|
|
34
|
+
hint?: string;
|
|
35
|
+
}> = [
|
|
36
|
+
{
|
|
37
|
+
value: "oauth",
|
|
38
|
+
label: "OAuth",
|
|
39
|
+
hint: "recommended — browser-based login with automatic token refresh",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
value: "env",
|
|
43
|
+
label: "Environment Variables",
|
|
44
|
+
hint: "CLOUDFLARE_API_TOKEN or CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
value: "stored",
|
|
48
|
+
label: "Stored",
|
|
49
|
+
hint: "stored in ~/.alchemy/credentials",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export type CloudflareStoredCredentials =
|
|
54
|
+
| { type: "apiToken"; apiToken: string }
|
|
55
|
+
| { type: "apiKey"; apiKey: string; email: string };
|
|
56
|
+
|
|
57
|
+
export type CloudflareResolvedCredentials =
|
|
58
|
+
| {
|
|
59
|
+
type: "apiToken";
|
|
60
|
+
apiToken: Redacted.Redacted<string>;
|
|
61
|
+
source: { type: CloudflareAuthConfig["method"]; details?: string };
|
|
62
|
+
}
|
|
63
|
+
| {
|
|
64
|
+
type: "apiKey";
|
|
65
|
+
apiKey: Redacted.Redacted<string>;
|
|
66
|
+
email: Redacted.Redacted<string>;
|
|
67
|
+
source: { type: CloudflareAuthConfig["method"]; details?: string };
|
|
68
|
+
}
|
|
69
|
+
| {
|
|
70
|
+
type: "oauth";
|
|
71
|
+
accessToken: Redacted.Redacted<string>;
|
|
72
|
+
expires: number;
|
|
73
|
+
source: { type: CloudflareAuthConfig["method"]; details?: string };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const CloudflareAuth = Effect.gen(function* () {
|
|
77
|
+
const context = yield* Effect.context<
|
|
78
|
+
FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner
|
|
79
|
+
>();
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
name: "Cloudflare",
|
|
83
|
+
|
|
84
|
+
configure: (profileName) =>
|
|
85
|
+
configureCredentials(profileName).pipe(Effect.provide(context)),
|
|
86
|
+
|
|
87
|
+
logout: (profileName, config) =>
|
|
88
|
+
logout(profileName, config).pipe(Effect.provide(context)),
|
|
89
|
+
|
|
90
|
+
login: (profileName, config) =>
|
|
91
|
+
login(profileName, config).pipe(Effect.provide(context)),
|
|
92
|
+
|
|
93
|
+
prettyPrint: (profileName, config) =>
|
|
94
|
+
prettyPrint(profileName, config).pipe(Effect.provide(context)),
|
|
95
|
+
|
|
96
|
+
read: (profileName, config) =>
|
|
97
|
+
resolveCredentials(profileName, config).pipe(Effect.provide(context)),
|
|
98
|
+
} satisfies AuthProvider<CloudflareAuthConfig, CloudflareResolvedCredentials>;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const resolveCredentials = (
|
|
102
|
+
profileName: string,
|
|
103
|
+
config: CloudflareAuthConfig,
|
|
104
|
+
) =>
|
|
105
|
+
Match.value(config).pipe(
|
|
106
|
+
Match.when(
|
|
107
|
+
{ method: "env" },
|
|
108
|
+
Effect.fnUntraced(function* () {
|
|
109
|
+
const apiToken = yield* getEnvRedacted("CLOUDFLARE_API_TOKEN");
|
|
110
|
+
if (apiToken) {
|
|
111
|
+
return {
|
|
112
|
+
type: "apiToken" as const,
|
|
113
|
+
apiToken,
|
|
114
|
+
source: { type: "env" },
|
|
115
|
+
} as CloudflareResolvedCredentials;
|
|
116
|
+
}
|
|
117
|
+
const apiKey = yield* getEnvRedacted("CLOUDFLARE_API_KEY");
|
|
118
|
+
const email = yield* getEnvRedacted("CLOUDFLARE_EMAIL");
|
|
119
|
+
if (apiKey && email) {
|
|
120
|
+
return {
|
|
121
|
+
type: "apiKey" as const,
|
|
122
|
+
apiKey,
|
|
123
|
+
email,
|
|
124
|
+
source: { type: "env" },
|
|
125
|
+
} as CloudflareResolvedCredentials;
|
|
126
|
+
}
|
|
127
|
+
return yield* new AuthError({
|
|
128
|
+
message:
|
|
129
|
+
"Cloudflare env credentials not found. Set CLOUDFLARE_API_TOKEN or CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL.",
|
|
130
|
+
});
|
|
131
|
+
}),
|
|
132
|
+
),
|
|
133
|
+
Match.when({ method: "stored" }, () =>
|
|
134
|
+
readCredentials<CloudflareStoredCredentials>(
|
|
135
|
+
profileName,
|
|
136
|
+
"cf-stored",
|
|
137
|
+
).pipe(
|
|
138
|
+
Effect.flatMap((creds) =>
|
|
139
|
+
creds == null
|
|
140
|
+
? Effect.fail(
|
|
141
|
+
new AuthError({
|
|
142
|
+
message:
|
|
143
|
+
"Cloudflare stored credentials not found. Run: alchemy-effect login --configure",
|
|
144
|
+
}),
|
|
145
|
+
)
|
|
146
|
+
: Effect.succeed(
|
|
147
|
+
Match.value(creds).pipe(
|
|
148
|
+
Match.when({ type: "apiToken" }, (c) => ({
|
|
149
|
+
type: "apiToken" as const,
|
|
150
|
+
apiToken: Redacted.make(c.apiToken),
|
|
151
|
+
source: { type: "stored" as const },
|
|
152
|
+
})),
|
|
153
|
+
Match.when({ type: "apiKey" }, (c) => ({
|
|
154
|
+
type: "apiKey" as const,
|
|
155
|
+
apiKey: Redacted.make(c.apiKey),
|
|
156
|
+
email: Redacted.make(c.email),
|
|
157
|
+
source: { type: "stored" as const },
|
|
158
|
+
})),
|
|
159
|
+
Match.exhaustive,
|
|
160
|
+
) as CloudflareResolvedCredentials,
|
|
161
|
+
),
|
|
162
|
+
),
|
|
163
|
+
),
|
|
164
|
+
),
|
|
165
|
+
Match.when({ method: "oauth" }, () =>
|
|
166
|
+
readCredentials<OAuthClient.OAuthCredentials>(
|
|
167
|
+
profileName,
|
|
168
|
+
"cf-oauth",
|
|
169
|
+
).pipe(
|
|
170
|
+
Effect.flatMap((creds) =>
|
|
171
|
+
creds == null || creds.type !== "oauth"
|
|
172
|
+
? Effect.fail(
|
|
173
|
+
new AuthError({
|
|
174
|
+
message:
|
|
175
|
+
"Cloudflare OAuth credentials not found. Run: alchemy-effect login",
|
|
176
|
+
}),
|
|
177
|
+
)
|
|
178
|
+
: Effect.succeed({
|
|
179
|
+
type: "oauth" as const,
|
|
180
|
+
accessToken: Redacted.make(creds.access),
|
|
181
|
+
expires: creds.expires,
|
|
182
|
+
source: { type: "oauth" as const },
|
|
183
|
+
} as CloudflareResolvedCredentials),
|
|
184
|
+
),
|
|
185
|
+
),
|
|
186
|
+
),
|
|
187
|
+
Match.exhaustive,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const logout = (profileName: string, config: CloudflareAuthConfig) =>
|
|
191
|
+
Match.value(config).pipe(
|
|
192
|
+
Match.when({ method: "env" }, () => Effect.void),
|
|
193
|
+
Match.when({ method: "stored" }, () =>
|
|
194
|
+
deleteCredentials(profileName, "cf-stored").pipe(
|
|
195
|
+
Effect.andThen(Clank.success("Cloudflare: stored credentials removed")),
|
|
196
|
+
),
|
|
197
|
+
),
|
|
198
|
+
Match.when({ method: "oauth" }, () =>
|
|
199
|
+
readCredentials<OAuthClient.OAuthCredentials>(
|
|
200
|
+
profileName,
|
|
201
|
+
"cf-oauth",
|
|
202
|
+
).pipe(
|
|
203
|
+
Effect.tap((creds) =>
|
|
204
|
+
creds?.type === "oauth"
|
|
205
|
+
? OAuthClient.revoke(creds).pipe(
|
|
206
|
+
Effect.catchTag("OAuthError", (err) =>
|
|
207
|
+
Clank.warn(
|
|
208
|
+
`Cloudflare: could not revoke OAuth token: ${err.errorDescription}`,
|
|
209
|
+
),
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
: Effect.void,
|
|
213
|
+
),
|
|
214
|
+
Effect.andThen(deleteCredentials(profileName, "cf-oauth")),
|
|
215
|
+
Effect.andThen(Clank.success("Cloudflare: OAuth credentials removed.")),
|
|
216
|
+
),
|
|
217
|
+
),
|
|
218
|
+
Match.exhaustive,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const login = (profileName: string, config: CloudflareAuthConfig) =>
|
|
222
|
+
Match.value(config)
|
|
223
|
+
.pipe(
|
|
224
|
+
Match.when({ method: "env" }, () => Effect.void),
|
|
225
|
+
Match.when({ method: "stored" }, () =>
|
|
226
|
+
readCredentials<CloudflareStoredCredentials>(
|
|
227
|
+
profileName,
|
|
228
|
+
"cf-stored",
|
|
229
|
+
).pipe(
|
|
230
|
+
Effect.flatMap((creds) =>
|
|
231
|
+
creds == null ? loginStored(profileName) : Effect.void,
|
|
232
|
+
),
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
Match.when({ method: "oauth" }, (c) =>
|
|
236
|
+
Effect.gen(function* () {
|
|
237
|
+
const creds = yield* readCredentials<OAuthClient.OAuthCredentials>(
|
|
238
|
+
profileName,
|
|
239
|
+
"cf-oauth",
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
if (creds?.type === "oauth" && creds.expires > Date.now() + 10_000) {
|
|
243
|
+
yield* Clank.info("Cloudflare: OAuth credentials are still valid.");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (creds?.type === "oauth") {
|
|
248
|
+
yield* Clank.info("Cloudflare: refreshing OAuth credentials...");
|
|
249
|
+
yield* OAuthClient.refresh(creds).pipe(
|
|
250
|
+
Effect.flatMap((refreshed) =>
|
|
251
|
+
writeCredentials(profileName, "cf-oauth", refreshed).pipe(
|
|
252
|
+
Effect.andThen(
|
|
253
|
+
Clank.success("Cloudflare: OAuth credentials refreshed."),
|
|
254
|
+
),
|
|
255
|
+
),
|
|
256
|
+
),
|
|
257
|
+
Effect.catchTag("OAuthError", () =>
|
|
258
|
+
oauthLogin(profileName, c.scopes).pipe(Effect.asVoid),
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
yield* oauthLogin(profileName, c.scopes);
|
|
265
|
+
}),
|
|
266
|
+
),
|
|
267
|
+
Match.exhaustive,
|
|
268
|
+
)
|
|
269
|
+
.pipe(
|
|
270
|
+
Effect.mapError(
|
|
271
|
+
(e) => new AuthError({ message: "login failed", cause: e }),
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const configureCredentials = (profileName: string) =>
|
|
276
|
+
Clank.select({
|
|
277
|
+
message: "Cloudflare authentication method",
|
|
278
|
+
options,
|
|
279
|
+
})
|
|
280
|
+
.pipe(
|
|
281
|
+
Effect.flatMap((method) =>
|
|
282
|
+
Match.value(method).pipe(
|
|
283
|
+
Match.when("env", () => Effect.succeed({ method: "env" as const })),
|
|
284
|
+
Match.when("oauth", () => configureOAuth(profileName)),
|
|
285
|
+
Match.when("stored", () => loginStored(profileName)),
|
|
286
|
+
Match.exhaustive,
|
|
287
|
+
),
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
.pipe(
|
|
291
|
+
Effect.mapError(
|
|
292
|
+
(e) =>
|
|
293
|
+
new AuthError({
|
|
294
|
+
message: "failed to configure credentials",
|
|
295
|
+
cause: e,
|
|
296
|
+
}),
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const configureOAuth = Effect.fnUntraced(function* (profileName: string) {
|
|
301
|
+
const scopes = yield* promptOAuthScopes();
|
|
302
|
+
|
|
303
|
+
const oauthCreds = yield* oauthLogin(profileName, scopes);
|
|
304
|
+
|
|
305
|
+
const accountId = yield* selectAccount(oauthCreds.access).pipe(
|
|
306
|
+
Effect.mapError(
|
|
307
|
+
(e) =>
|
|
308
|
+
new AuthError({
|
|
309
|
+
message: "Cloudflare: could not list accounts",
|
|
310
|
+
cause: e,
|
|
311
|
+
}),
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
method: "oauth" as const,
|
|
317
|
+
scopes,
|
|
318
|
+
accountId,
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const loginStored = Effect.fnUntraced(function* (profileName: string) {
|
|
323
|
+
const credentialType = yield* Clank.select({
|
|
324
|
+
message: "Cloudflare credential type",
|
|
325
|
+
options: [
|
|
326
|
+
{ value: "apiToken" as const, label: "API Token", hint: "recommended" },
|
|
327
|
+
{ value: "apiKey" as const, label: "API Key + Email" },
|
|
328
|
+
],
|
|
329
|
+
}).pipe(retryOnce);
|
|
330
|
+
|
|
331
|
+
return yield* Match.value(credentialType).pipe(
|
|
332
|
+
Match.when("apiToken", () =>
|
|
333
|
+
Effect.gen(function* () {
|
|
334
|
+
const apiToken = yield* Clank.password({
|
|
335
|
+
message: "Cloudflare API Token",
|
|
336
|
+
validate: (v) => (v.length === 0 ? "Required" : undefined),
|
|
337
|
+
}).pipe(retryOnce);
|
|
338
|
+
|
|
339
|
+
yield* writeCredentials<CloudflareStoredCredentials>(
|
|
340
|
+
profileName,
|
|
341
|
+
"cf-stored",
|
|
342
|
+
{ type: "apiToken", apiToken },
|
|
343
|
+
);
|
|
344
|
+
yield* Clank.success("Cloudflare: credentials saved.");
|
|
345
|
+
const accountId = yield* promptAccountId();
|
|
346
|
+
return {
|
|
347
|
+
method: "stored" as const,
|
|
348
|
+
credentialType: "apiToken" as const,
|
|
349
|
+
...(accountId ? { accountId } : {}),
|
|
350
|
+
};
|
|
351
|
+
}),
|
|
352
|
+
),
|
|
353
|
+
Match.when("apiKey", () =>
|
|
354
|
+
Effect.gen(function* () {
|
|
355
|
+
const apiKey = yield* Clank.text({
|
|
356
|
+
message: "Cloudflare API Key",
|
|
357
|
+
validate: (v) => (v.length === 0 ? "Required" : undefined),
|
|
358
|
+
}).pipe(retryOnce);
|
|
359
|
+
|
|
360
|
+
const email = yield* Clank.text({
|
|
361
|
+
message: "Cloudflare Email",
|
|
362
|
+
validate: (v) => (v.length === 0 ? "Required" : undefined),
|
|
363
|
+
}).pipe(retryOnce);
|
|
364
|
+
|
|
365
|
+
yield* writeCredentials<CloudflareStoredCredentials>(
|
|
366
|
+
profileName,
|
|
367
|
+
"cf-stored",
|
|
368
|
+
{ type: "apiKey", apiKey, email },
|
|
369
|
+
);
|
|
370
|
+
yield* Clank.success("Cloudflare: credentials saved.");
|
|
371
|
+
const accountId = yield* promptAccountId();
|
|
372
|
+
return {
|
|
373
|
+
method: "stored" as const,
|
|
374
|
+
credentialType: "apiKey" as const,
|
|
375
|
+
...(accountId ? { accountId } : {}),
|
|
376
|
+
};
|
|
377
|
+
}),
|
|
378
|
+
),
|
|
379
|
+
Match.exhaustive,
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const oauthLogin = (profileName: string, scopes: string[]) =>
|
|
384
|
+
Effect.gen(function* () {
|
|
385
|
+
const authorization = OAuthClient.authorize([...scopes, "offline_access"]);
|
|
386
|
+
|
|
387
|
+
yield* Clank.info("Cloudflare: opening browser for OAuth login...");
|
|
388
|
+
yield* Clank.info(authorization.url);
|
|
389
|
+
yield* Clank.openUrl(authorization.url).pipe(
|
|
390
|
+
Effect.catch(() =>
|
|
391
|
+
Clank.warn(
|
|
392
|
+
"Cloudflare: could not open browser automatically. Please open the URL above manually.",
|
|
393
|
+
),
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
yield* Clank.info(
|
|
397
|
+
"Cloudflare: waiting for authorization (up to 5 minutes)...",
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const credentials = yield* OAuthClient.callback(authorization);
|
|
401
|
+
yield* writeCredentials(profileName, "cf-oauth", credentials);
|
|
402
|
+
yield* Clank.success("Cloudflare: OAuth credentials saved.");
|
|
403
|
+
return credentials;
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const promptAccountId = () =>
|
|
407
|
+
getEnv("CLOUDFLARE_ACCOUNT_ID").pipe(
|
|
408
|
+
Effect.flatMap((envAccountId) =>
|
|
409
|
+
Clank.text({
|
|
410
|
+
message: "Cloudflare Account ID (Enter to skip)",
|
|
411
|
+
placeholder: envAccountId ?? "",
|
|
412
|
+
defaultValue: envAccountId ?? "",
|
|
413
|
+
}).pipe(retryOnce),
|
|
414
|
+
),
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const promptOAuthScopes = () =>
|
|
418
|
+
Clank.confirm({
|
|
419
|
+
message: "Customize OAuth scopes? (default covers typical use cases)",
|
|
420
|
+
initialValue: false,
|
|
421
|
+
}).pipe(
|
|
422
|
+
retryOnce,
|
|
423
|
+
Effect.flatMap((customize) => {
|
|
424
|
+
if (!customize) return Effect.succeed([...DEFAULT_SCOPES]);
|
|
425
|
+
return Clank.multiselect({
|
|
426
|
+
message: "Select OAuth scopes",
|
|
427
|
+
initialValues: DEFAULT_SCOPES as string[],
|
|
428
|
+
options: Object.entries(ALL_SCOPES).map(([value, hint]) => ({
|
|
429
|
+
value: value as string,
|
|
430
|
+
label: value,
|
|
431
|
+
hint,
|
|
432
|
+
})),
|
|
433
|
+
required: true,
|
|
434
|
+
}).pipe(
|
|
435
|
+
Effect.map((s) => s as string[]),
|
|
436
|
+
retryOnce,
|
|
437
|
+
);
|
|
438
|
+
}),
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const withOAuthCredentials = <A, E>(
|
|
442
|
+
accessToken: string,
|
|
443
|
+
effect: Effect.Effect<
|
|
444
|
+
A,
|
|
445
|
+
E,
|
|
446
|
+
CfCredentialsModule.Credentials | HttpClient.HttpClient
|
|
447
|
+
>,
|
|
448
|
+
): Effect.Effect<A, E> =>
|
|
449
|
+
Effect.provide(
|
|
450
|
+
effect,
|
|
451
|
+
Layer.mergeAll(
|
|
452
|
+
CfCredentialsModule.fromOAuth({
|
|
453
|
+
load: Effect.succeed({ accessToken }),
|
|
454
|
+
refresh: () =>
|
|
455
|
+
Effect.die("refresh not expected during account selection"),
|
|
456
|
+
}),
|
|
457
|
+
FetchHttpClient.layer,
|
|
458
|
+
),
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const selectAccount = (accessToken: string) =>
|
|
462
|
+
Effect.gen(function* () {
|
|
463
|
+
const list = yield* cfAccounts.listAccounts;
|
|
464
|
+
const response = yield* list({});
|
|
465
|
+
const accounts = response.result;
|
|
466
|
+
if (accounts.length === 0) {
|
|
467
|
+
yield* new AuthError({
|
|
468
|
+
message: "Cloudflare: no accounts found for this credential.",
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (accounts.length === 1) {
|
|
472
|
+
const account = accounts[0]!;
|
|
473
|
+
yield* Clank.info(
|
|
474
|
+
`Cloudflare: using account: ${account.name} (${account.id})`,
|
|
475
|
+
);
|
|
476
|
+
return account.id;
|
|
477
|
+
}
|
|
478
|
+
return yield* Clank.select({
|
|
479
|
+
message: "Select a Cloudflare account",
|
|
480
|
+
options: accounts.map((a) => ({
|
|
481
|
+
value: a.id,
|
|
482
|
+
label: a.name,
|
|
483
|
+
hint: a.id,
|
|
484
|
+
})),
|
|
485
|
+
}).pipe(retryOnce);
|
|
486
|
+
}).pipe((e) => withOAuthCredentials(accessToken, e));
|
|
487
|
+
|
|
488
|
+
const prettyPrint = (profileName: string, config: CloudflareAuthConfig) =>
|
|
489
|
+
resolveCredentials(profileName, config).pipe(
|
|
490
|
+
Effect.tap((creds) => {
|
|
491
|
+
const sourceStr = creds.source.details
|
|
492
|
+
? `${creds.source.type} - ${creds.source.details}`
|
|
493
|
+
: creds.source.type;
|
|
494
|
+
return Match.value(creds).pipe(
|
|
495
|
+
Match.when({ type: "apiToken" }, (c) =>
|
|
496
|
+
Effect.all([
|
|
497
|
+
Console.log(` apiToken: ${displayRedacted(c.apiToken, 9)}`),
|
|
498
|
+
Console.log(` source: ${sourceStr}`),
|
|
499
|
+
]),
|
|
500
|
+
),
|
|
501
|
+
Match.when({ type: "apiKey" }, (c) =>
|
|
502
|
+
Effect.all([
|
|
503
|
+
Console.log(` apiKey: ${displayRedacted(c.apiKey)}`),
|
|
504
|
+
Console.log(` email: ${displayRedacted(c.email)}`),
|
|
505
|
+
Console.log(` source: ${sourceStr}`),
|
|
506
|
+
]),
|
|
507
|
+
),
|
|
508
|
+
Match.when({ type: "oauth" }, (c) => {
|
|
509
|
+
const remainingMs = c.expires - Date.now();
|
|
510
|
+
const expiresAt = new Date(c.expires).toISOString();
|
|
511
|
+
const expiresStr =
|
|
512
|
+
remainingMs <= 0
|
|
513
|
+
? `expired (${expiresAt})`
|
|
514
|
+
: `in ${Duration.format(Duration.millis(remainingMs))} (${expiresAt})`;
|
|
515
|
+
return Effect.all([
|
|
516
|
+
Console.log(` accessToken: ${displayRedacted(c.accessToken)}`),
|
|
517
|
+
Console.log(` expires: ${expiresStr}`),
|
|
518
|
+
Console.log(` source: ${sourceStr}`),
|
|
519
|
+
]);
|
|
520
|
+
}),
|
|
521
|
+
Match.exhaustive,
|
|
522
|
+
);
|
|
523
|
+
}),
|
|
524
|
+
Effect.andThen(
|
|
525
|
+
getEnv("CLOUDFLARE_ACCOUNT_ID").pipe(
|
|
526
|
+
Effect.flatMap((envAccountId) => {
|
|
527
|
+
const resolved = config.accountId ?? envAccountId;
|
|
528
|
+
return Console.log(
|
|
529
|
+
` accountId: ${resolved ?? "(not set — configure or set CLOUDFLARE_ACCOUNT_ID)"}`,
|
|
530
|
+
);
|
|
531
|
+
}),
|
|
532
|
+
),
|
|
533
|
+
),
|
|
534
|
+
Effect.catch((e) =>
|
|
535
|
+
Console.error(` Failed to retrieve credentials: ${e}`),
|
|
536
|
+
),
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
export const ALL_SCOPES = {
|
|
540
|
+
"access:read":
|
|
541
|
+
"See Cloudflare Access data such as zones, applications, certificates, device postures, groups, identity providers, login counts, organizations, policies, service tokens, and users",
|
|
542
|
+
"access:write":
|
|
543
|
+
"See and change Cloudflare Access data such as zones, applications, certificates, device postures, groups, identity providers, login counts, organizations, policies, service tokens, and users",
|
|
544
|
+
"account:read":
|
|
545
|
+
"See your account info such as account details, analytics, and memberships",
|
|
546
|
+
"agw:read": "Grants read level access to Agents Gateway",
|
|
547
|
+
"agw:run": "Grants run level access to Agents Gateway",
|
|
548
|
+
"agw:write": "Grants read and write level access to Agents Gateway",
|
|
549
|
+
"ai:read": "Grants read level access to Workers AI",
|
|
550
|
+
"ai:write": "Grants write level access to Workers AI",
|
|
551
|
+
"aiaudit:read": "Grants read level access to AI Audit",
|
|
552
|
+
"aiaudit:write": "Grants write level access to AI Audit",
|
|
553
|
+
"aig:read": "Grants read level access to AI Gateway",
|
|
554
|
+
"aig:write": "Grants write level access to AI Gateway",
|
|
555
|
+
"auditlogs:read": "View Cloudflare Account Audit Logs",
|
|
556
|
+
"browser:read": "Grants read level access to Browser Rendering",
|
|
557
|
+
"browser:write": "Grants write level access to Browser Rendering",
|
|
558
|
+
"cfone:read": "Grants read level access to Cloudforce One data",
|
|
559
|
+
"cfone:write": "Grants write level access to Cloudforce One data",
|
|
560
|
+
"cloudchamber:write": "See and make changes to Cloudchamber",
|
|
561
|
+
"connectivity:admin":
|
|
562
|
+
"See, change, and bind to Connectivity Directory services, including creating services targeting Cloudflare Tunnel",
|
|
563
|
+
"connectivity:bind":
|
|
564
|
+
"read, list, and bind to Connectivity Directory services, as well as read and list Cloudflare Tunnels",
|
|
565
|
+
"connectivity:read":
|
|
566
|
+
"See Connectivity Directory services and Cloudflare Tunnels",
|
|
567
|
+
"constellation:write":
|
|
568
|
+
"Grants write access to Constellation configuration and models",
|
|
569
|
+
"containers:write": "See and make changes to Workers Containers",
|
|
570
|
+
"d1:write": "See and make changes to D1",
|
|
571
|
+
"dex:read": "Grants read level access to Cloudflare DEX",
|
|
572
|
+
"dex:write": "Grants write level access to Cloudflare DEX",
|
|
573
|
+
"dns_analytics:read": "Grants read level access to Cloudflare DNS Analytics",
|
|
574
|
+
"dns_records:edit": "Grants edit level access to dns records",
|
|
575
|
+
"dns_records:read": "Grants read level access to dns records",
|
|
576
|
+
"dns_settings:read": "Grants read level access to Cloudflare DNS Settings",
|
|
577
|
+
"firstpartytags:write":
|
|
578
|
+
"Can see, edit and publish Google tag gateway configuration.",
|
|
579
|
+
"lb:edit": "Grants edit level access to lb and lb pools",
|
|
580
|
+
"lb:read": "Grants read level access to lb and lb pools",
|
|
581
|
+
"logpush:read": "See Cloudflare Logpush data",
|
|
582
|
+
"logpush:write": "See and change Cloudflare Logpush data",
|
|
583
|
+
"notification:read": "View Cloudflare Notifications",
|
|
584
|
+
"notification:write": "View and Modify Cloudflare Notifications",
|
|
585
|
+
"pages:read": "See Cloudflare Pages projects, settings and deployments",
|
|
586
|
+
"pages:write":
|
|
587
|
+
"See and change Cloudflare Pages projects, settings and deployments",
|
|
588
|
+
"pipelines:read": "Grants read level access to Cloudflare Pipelines",
|
|
589
|
+
"pipelines:setup":
|
|
590
|
+
"Grants permission to generate R2 tokens for Workers Pipelines",
|
|
591
|
+
"pipelines:write": "Grants write level access to Cloudflare Pipelines",
|
|
592
|
+
"query_cache:write": "See and make changes to Hyperdrive",
|
|
593
|
+
"queues:write": "See and change Cloudflare Queues settings and data",
|
|
594
|
+
"r2_catalog:write": "Grants write level access to R2 Data Catalog",
|
|
595
|
+
"radar:read": "Grants access to read Cloudflare Radar data",
|
|
596
|
+
"ai-search:read": "Grants read level access to AI Search",
|
|
597
|
+
"ai-search:write": "Grants write level access to AI Search",
|
|
598
|
+
"ai-search:run": "Grants run level access to AI Search",
|
|
599
|
+
"secrets_store:read": "Grants read level access to Secrets Store",
|
|
600
|
+
"secrets_store:write": "Grants write level access to Secrets Store",
|
|
601
|
+
"ssl_certs:write":
|
|
602
|
+
"Grants read and write access to SSL MTLS certificates or Certificate Store",
|
|
603
|
+
"sso-connector:read": "See Cloudflare SSO connectors",
|
|
604
|
+
"sso-connector:write":
|
|
605
|
+
"See Cloudflare SSO connectors to toggle activation and deactivation of SSO",
|
|
606
|
+
"teams:pii": "See personally identifiable Cloudflare Teams data",
|
|
607
|
+
"teams:read":
|
|
608
|
+
"See Cloudflare Teams data such as zones, gateway, and argo tunnel details",
|
|
609
|
+
"teams:secure_location":
|
|
610
|
+
"See all DNS Location data but can only change secure DNS Locations",
|
|
611
|
+
"teams:write":
|
|
612
|
+
"See and change Cloudflare Teams data such as zones, gateway, and argo tunnel details",
|
|
613
|
+
"url_scanner:read": "Grants read level access to URL Scanner",
|
|
614
|
+
"url_scanner:write": "Grants write level access to URL Scanner",
|
|
615
|
+
"user:read":
|
|
616
|
+
"See your user info such as name, email address, and account memberships",
|
|
617
|
+
"vectorize:write": "See and make changes to Vectorize",
|
|
618
|
+
"workers:read":
|
|
619
|
+
"See Cloudflare Workers data such as zones, KV storage, R2 storage, scripts, and routes",
|
|
620
|
+
"workers:write":
|
|
621
|
+
"See and change Cloudflare Workers data such as zones, KV storage, R2 storage, scripts, and routes",
|
|
622
|
+
"workers_builds:read":
|
|
623
|
+
"See Cloudflare Workers Builds data such as builds, build configuration, and build logs",
|
|
624
|
+
"workers_builds:write":
|
|
625
|
+
"See and change Cloudflare Workers Builds data such as builds, build configuration, and build logs",
|
|
626
|
+
"workers_kv:write":
|
|
627
|
+
"See and change Cloudflare Workers KV Storage data such as keys and namespaces",
|
|
628
|
+
"workers_observability:read":
|
|
629
|
+
"Grants read access to Cloudflare Workers Observability",
|
|
630
|
+
"workers_observability_telemetry:write":
|
|
631
|
+
"Grants write access to Cloudflare Workers Observability Telemetry API",
|
|
632
|
+
"workers_routes:write":
|
|
633
|
+
"See and change Cloudflare Workers data such as filters and routes",
|
|
634
|
+
"workers_scripts:write":
|
|
635
|
+
"See and change Cloudflare Workers scripts, durable objects, subdomains, triggers, and tail data",
|
|
636
|
+
"workers_tail:read": "See Cloudflare Workers tail and script data",
|
|
637
|
+
"zone:read": "Grants read level access to account zone",
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
export const DEFAULT_SCOPES = [
|
|
641
|
+
"account:read",
|
|
642
|
+
"ai-search:write",
|
|
643
|
+
"ai-search:run",
|
|
644
|
+
"ai:write",
|
|
645
|
+
"cloudchamber:write",
|
|
646
|
+
"connectivity:admin",
|
|
647
|
+
"containers:write",
|
|
648
|
+
"d1:write",
|
|
649
|
+
"pages:write",
|
|
650
|
+
"pipelines:write",
|
|
651
|
+
"queues:write",
|
|
652
|
+
"secrets_store:write",
|
|
653
|
+
"ssl_certs:write",
|
|
654
|
+
"user:read",
|
|
655
|
+
"vectorize:write",
|
|
656
|
+
"workers_kv:write",
|
|
657
|
+
"workers_routes:write",
|
|
658
|
+
"workers_scripts:write",
|
|
659
|
+
"workers_tail:read",
|
|
660
|
+
"workers:write",
|
|
661
|
+
"zone:read",
|
|
662
|
+
];
|
|
663
|
+
|
|
664
|
+
export const OAUTH_CLIENT_ID = "6d8c2255-0773-45f6-b376-2914632e6f91";
|
|
665
|
+
export const OAUTH_REDIRECT_URI = "http://localhost:9976/auth/callback";
|
|
666
|
+
export const OAUTH_ENDPOINTS = {
|
|
667
|
+
authorize: "https://dash.cloudflare.com/oauth2/authorize",
|
|
668
|
+
token: "https://dash.cloudflare.com/oauth2/token",
|
|
669
|
+
revoke: "https://dash.cloudflare.com/oauth2/revoke",
|
|
670
|
+
};
|