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,12 +1,12 @@
1
- import { alchemy } from "../alchemy.ts";
2
1
  import type { Secret } from "../secret.ts";
3
2
  import { withExponentialBackoff } from "../util/retry.ts";
4
3
  import { safeFetch } from "../util/safe-fetch.ts";
5
4
  import {
6
5
  getCloudflareAuthHeaders,
6
+ normalizeAuthOptions,
7
7
  type CloudflareAuthOptions,
8
8
  } from "./auth.ts";
9
- import { getCloudflareAccounts, getUserEmailFromApiKey } from "./user.ts";
9
+ import { getCloudflareAccountId } from "./user.ts";
10
10
 
11
11
  /**
12
12
  * Options for Cloudflare API requests
@@ -42,6 +42,12 @@ export interface CloudflareApiOptions {
42
42
  email?: string;
43
43
  }
44
44
 
45
+ /** Used to propagate normalized auth options from a parent resource to `createCloudflareApi` in a child resource */
46
+ export type InternalCloudflareApiOptions = CloudflareAuthOptions & {
47
+ baseUrl?: string;
48
+ accountId: string;
49
+ };
50
+
45
51
  /**
46
52
  * Creates a CloudflareApi instance with automatic account ID discovery if not provided
47
53
  *
@@ -49,44 +55,18 @@ export interface CloudflareApiOptions {
49
55
  * @returns Promise resolving to a CloudflareApi instance
50
56
  */
51
57
  export async function createCloudflareApi(
52
- options: Partial<CloudflareApiOptions> = {},
58
+ options: Partial<CloudflareApiOptions> | InternalCloudflareApiOptions = {},
53
59
  ): Promise<CloudflareApi> {
54
- const apiKey =
55
- options.apiKey ??
56
- (process.env.CLOUDFLARE_API_KEY
57
- ? alchemy.secret(process.env.CLOUDFLARE_API_KEY)
58
- : undefined);
59
- const apiToken =
60
- options.apiToken ??
61
- (process.env.CLOUDFLARE_API_TOKEN
62
- ? alchemy.secret(process.env.CLOUDFLARE_API_TOKEN)
63
- : undefined);
64
- let email = options.email ?? process.env.CLOUDFLARE_EMAIL;
65
- if (apiKey && !email) {
66
- email = await getUserEmailFromApiKey(apiKey.unencrypted);
67
- }
60
+ const authOptions = await normalizeAuthOptions(options);
68
61
  const accountId =
69
62
  options.accountId ??
70
63
  process.env.CLOUDFLARE_ACCOUNT_ID ??
71
64
  process.env.CF_ACCOUNT_ID ??
72
- (
73
- await getCloudflareAccounts({
74
- apiKey,
75
- apiToken,
76
- email,
77
- } as CloudflareAuthOptions)
78
- )[0]?.id;
79
- if (accountId === undefined) {
80
- throw new Error(
81
- "Either accountId or CLOUDFLARE_ACCOUNT_ID must be provided",
82
- );
83
- }
65
+ (await getCloudflareAccountId(authOptions));
84
66
  return new CloudflareApi({
85
67
  baseUrl: options.baseUrl,
86
68
  accountId,
87
- email,
88
- apiKey,
89
- apiToken,
69
+ authOptions,
90
70
  });
91
71
  }
92
72
 
@@ -96,9 +76,6 @@ export async function createCloudflareApi(
96
76
  export class CloudflareApi {
97
77
  public readonly accountId: string;
98
78
  public readonly baseUrl: string;
99
- public readonly apiKey: Secret | undefined;
100
- public readonly apiToken: Secret | undefined;
101
- public readonly email: string | undefined;
102
79
  public readonly authOptions: CloudflareAuthOptions;
103
80
 
104
81
  /**
@@ -108,30 +85,14 @@ export class CloudflareApi {
108
85
  *
109
86
  * @param options API options
110
87
  */
111
- constructor(
112
- options: CloudflareApiOptions & {
113
- accountId: string;
114
- },
115
- ) {
88
+ constructor(options: {
89
+ baseUrl?: string;
90
+ accountId: string;
91
+ authOptions: CloudflareAuthOptions;
92
+ }) {
116
93
  this.accountId = options.accountId;
117
94
  this.baseUrl = options.baseUrl ?? "https://api.cloudflare.com/client/v4";
118
- this.apiKey = options.apiKey;
119
- this.apiToken = options.apiToken;
120
- this.email = options.email;
121
-
122
- if (this.apiKey && this.apiToken) {
123
- throw new Error("'apiKey' and 'apiToken' cannot both be provided");
124
- } else if (this.apiKey && !this.email) {
125
- throw new Error("'email' must be provided if 'apiKey' is provided");
126
- }
127
- this.authOptions = this.apiKey
128
- ? {
129
- apiKey: this.apiKey,
130
- email: this.email!,
131
- }
132
- : {
133
- apiToken: this.apiToken,
134
- };
95
+ this.authOptions = options.authOptions;
135
96
  }
136
97
 
137
98
  /**
@@ -1,85 +1,101 @@
1
+ import { alchemy } from "../alchemy.ts";
1
2
  import type { Secret } from "../secret.ts";
2
- import { getRefreshedWranglerConfig } from "./auth-wrangler.ts";
3
- /**
4
- * Authentication options for Cloudflare API
5
- */
6
- export type CloudflareAuthOptions =
7
- | CloudflareApiKeyAuthOptions
8
- | CloudflareApiTokenAuthOptions;
9
-
10
- export type CloudflareApiKeyAuthOptions = {
11
- /**
12
- * API Key to use with API Key
13
- */
14
- apiKey: Secret;
15
- /**
16
- * Email to use with API Key
17
- * If not provided, will attempt to discover from Cloudflare API
18
- */
19
- email: string;
3
+ import type { CloudflareApiOptions } from "./api.ts";
4
+ import { getRefreshedWranglerConfig } from "./oauth.ts";
5
+ import { getUserEmailFromApiKey } from "./user.ts";
20
6
 
21
- /**
22
- * API Token to use with API Key
23
- */
24
- apiToken?: undefined;
25
- };
7
+ export interface CloudflareApiTokenAuthOptions {
8
+ type: "api-token";
9
+ apiToken: Secret<string>;
10
+ }
26
11
 
27
- export function isCloudflareApiKeyAuthOptions(
28
- options: CloudflareAuthOptions | undefined,
29
- ): options is CloudflareApiKeyAuthOptions {
30
- return options !== undefined && options.apiKey !== undefined;
12
+ export interface CloudflareApiKeyAuthOptions {
13
+ type: "api-key";
14
+ apiKey: Secret;
15
+ apiEmail: string;
31
16
  }
32
17
 
33
- export type CloudflareApiTokenAuthOptions = {
34
- /**
35
- * API Token to use with API Key
36
- */
37
- apiToken?: Secret;
18
+ export interface CloudflareWranglerAuthOptions {
19
+ type: "wrangler";
20
+ }
38
21
 
39
- /**
40
- * API Key to use with API Token
41
- */
42
- apiKey?: undefined;
22
+ export type CloudflareAuthOptions =
23
+ | CloudflareApiTokenAuthOptions
24
+ | CloudflareApiKeyAuthOptions
25
+ | CloudflareWranglerAuthOptions;
43
26
 
44
- /**
45
- * Email to use with API Token
46
- */
47
- email?: undefined;
27
+ export const isCloudflareAuthOptions = (
28
+ input?: CloudflareApiOptions | CloudflareAuthOptions,
29
+ ): input is CloudflareAuthOptions => {
30
+ return (
31
+ input !== undefined &&
32
+ "type" in input &&
33
+ (input.type === "api-token" ||
34
+ input.type === "api-key" ||
35
+ input.type === "wrangler")
36
+ );
48
37
  };
49
38
 
50
- export function isCloudflareApiTokenAuthOptions(
51
- options: CloudflareAuthOptions | undefined,
52
- ): options is CloudflareApiTokenAuthOptions {
53
- return options !== undefined && options.apiToken !== undefined;
54
- }
39
+ export async function normalizeAuthOptions(
40
+ input?: CloudflareApiOptions | CloudflareAuthOptions,
41
+ ): Promise<CloudflareAuthOptions> {
42
+ if (isCloudflareAuthOptions(input)) {
43
+ return input;
44
+ }
55
45
 
56
- export async function getCloudflareAuthHeaders(
57
- options: CloudflareAuthOptions | undefined,
58
- ): Promise<Record<string, string>> {
59
- if (isCloudflareApiKeyAuthOptions(options)) {
60
- // Global API Key
61
- return {
62
- "X-Auth-Key": options.apiKey.unencrypted,
63
- "X-Auth-Email": options.email,
64
- };
65
- } else if (isCloudflareApiTokenAuthOptions(options)) {
66
- // API Token
46
+ const email = async (apiKey: Secret<string>) =>
47
+ input?.email ??
48
+ process.env.CLOUDFLARE_EMAIL ??
49
+ (await getUserEmailFromApiKey(apiKey.unencrypted));
50
+
51
+ if (input?.apiKey) {
67
52
  return {
68
- Authorization: `Bearer ${options.apiToken?.unencrypted}`,
53
+ type: "api-key",
54
+ apiKey: input.apiKey,
55
+ apiEmail: await email(input.apiKey),
69
56
  };
70
57
  }
71
- // Wrangler OAuth Token
72
- const wranglerConfig = await getRefreshedWranglerConfig();
73
- if (wranglerConfig.isOk() && wranglerConfig.value) {
58
+ if (input?.apiToken) {
59
+ return { type: "api-token", apiToken: input.apiToken };
60
+ }
61
+ if (process.env.CLOUDFLARE_API_KEY) {
62
+ const key = alchemy.secret(process.env.CLOUDFLARE_API_KEY);
63
+ return { type: "api-key", apiKey: key, apiEmail: await email(key) };
64
+ }
65
+ if (process.env.CLOUDFLARE_API_TOKEN) {
74
66
  return {
75
- Authorization: `Bearer ${wranglerConfig.value.oauth_token}`,
67
+ type: "api-token",
68
+ apiToken: alchemy.secret(process.env.CLOUDFLARE_API_TOKEN),
76
69
  };
77
- } else if (wranglerConfig.isErr()) {
78
- throw new Error(
79
- `Cloudflare authentication failed: ${wranglerConfig.error.message}`,
80
- );
81
70
  }
82
- throw new Error(
83
- "Cloudflare authentication required. Did you forget to login with `wrangler login` or set CLOUDFLARE_API_TOKEN, CLOUDFLARE_API_KEY?",
84
- );
71
+ return { type: "wrangler" };
72
+ }
73
+
74
+ export async function getCloudflareAuthHeaders(
75
+ authOptions: CloudflareAuthOptions,
76
+ ): Promise<Record<string, string>> {
77
+ switch (authOptions.type) {
78
+ case "api-token":
79
+ return { Authorization: `Bearer ${authOptions.apiToken.unencrypted}` };
80
+ case "api-key":
81
+ return {
82
+ "X-Auth-Key": authOptions.apiKey.unencrypted,
83
+ "X-Auth-Email": authOptions.apiEmail,
84
+ };
85
+ case "wrangler": {
86
+ const wranglerConfig = await getRefreshedWranglerConfig();
87
+ if (wranglerConfig.isErr()) {
88
+ throw new Error(
89
+ [
90
+ wranglerConfig.error.message,
91
+ "Please run `alchemy login`, or set either CLOUDFLARE_API_TOKEN or CLOUDFLARE_API_KEY in your environment.",
92
+ "Learn more: https://alchemy.run/guides/cloudflare/",
93
+ ].join("\n"),
94
+ );
95
+ }
96
+ return {
97
+ Authorization: `Bearer ${wranglerConfig.value.oauth_token}`,
98
+ };
99
+ }
100
+ }
85
101
  }