wrangler 0.0.0-e928f94 → 0.0.0-e95bea2

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 (100) hide show
  1. package/bin/wrangler.js +2 -2
  2. package/package.json +46 -18
  3. package/pages/functions/buildWorker.ts +62 -0
  4. package/pages/functions/filepath-routing.test.ts +123 -0
  5. package/pages/functions/filepath-routing.ts +178 -0
  6. package/pages/functions/identifiers.ts +78 -0
  7. package/pages/functions/routes.ts +151 -0
  8. package/pages/functions/template-worker.ts +130 -0
  9. package/src/__tests__/configuration.test.ts +2217 -0
  10. package/src/__tests__/dev.test.tsx +405 -0
  11. package/src/__tests__/dev2.test.tsx +152 -0
  12. package/src/__tests__/guess-worker-format.test.ts +58 -0
  13. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  14. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  15. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  16. package/src/__tests__/helpers/mock-account-id.ts +44 -0
  17. package/src/__tests__/helpers/mock-bin.ts +36 -0
  18. package/src/__tests__/helpers/mock-cfetch.ts +207 -0
  19. package/src/__tests__/helpers/mock-console.ts +70 -0
  20. package/src/__tests__/helpers/mock-dialogs.ts +65 -0
  21. package/src/__tests__/helpers/mock-kv.ts +40 -0
  22. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  23. package/src/__tests__/helpers/mock-user.ts +27 -0
  24. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  25. package/src/__tests__/helpers/run-in-tmp.ts +29 -0
  26. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  27. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  28. package/src/__tests__/https-options.test.ts +127 -0
  29. package/src/__tests__/index.test.ts +957 -119
  30. package/src/__tests__/jest.setup.ts +69 -0
  31. package/src/__tests__/kv.test.ts +1408 -0
  32. package/src/__tests__/logout.test.ts +50 -0
  33. package/src/__tests__/package-manager.test.ts +206 -0
  34. package/src/__tests__/pages.test.ts +44 -0
  35. package/src/__tests__/parse.test.ts +322 -0
  36. package/src/__tests__/publish.test.ts +2453 -0
  37. package/src/__tests__/r2.test.ts +198 -0
  38. package/src/__tests__/route.test.ts +45 -0
  39. package/src/__tests__/secret.test.ts +425 -0
  40. package/src/__tests__/sentry.test.ts +146 -0
  41. package/src/__tests__/tail.test.ts +564 -0
  42. package/src/__tests__/whoami.test.tsx +86 -0
  43. package/src/bundle.ts +142 -0
  44. package/src/cfetch/index.ts +104 -0
  45. package/src/cfetch/internal.ts +135 -0
  46. package/src/cli.ts +16 -7
  47. package/src/config/README.md +107 -0
  48. package/src/config/config.ts +279 -0
  49. package/src/config/diagnostics.ts +80 -0
  50. package/src/config/environment.ts +305 -0
  51. package/src/config/index.ts +52 -0
  52. package/src/config/validation-helpers.ts +388 -0
  53. package/src/config/validation.ts +1027 -0
  54. package/src/{api/preview.ts → create-worker-preview.ts} +63 -25
  55. package/src/create-worker-upload-form.ts +217 -0
  56. package/src/dev/dev.tsx +357 -0
  57. package/src/dev/local.tsx +241 -0
  58. package/src/dev/remote.tsx +201 -0
  59. package/src/dev/use-esbuild.ts +100 -0
  60. package/src/dialogs.tsx +5 -13
  61. package/src/entry.ts +159 -0
  62. package/src/environment-variables.ts +33 -0
  63. package/src/https-options.ts +122 -0
  64. package/src/index.tsx +1374 -903
  65. package/src/inspect.ts +617 -0
  66. package/src/kv/index.tsx +339 -0
  67. package/src/module-collection.ts +215 -26
  68. package/src/open-in-browser.ts +25 -0
  69. package/src/package-manager.ts +126 -0
  70. package/src/pages.tsx +452 -250
  71. package/src/parse.ts +183 -0
  72. package/src/paths.ts +26 -0
  73. package/src/proxy.ts +438 -0
  74. package/src/publish.ts +300 -292
  75. package/src/r2.ts +50 -0
  76. package/src/reporting.ts +131 -0
  77. package/src/selfsigned.d.ts +29 -0
  78. package/src/sites.tsx +181 -58
  79. package/src/tail/filters.ts +279 -0
  80. package/src/tail/index.ts +299 -0
  81. package/src/tail/printing.ts +62 -0
  82. package/src/user.tsx +166 -130
  83. package/src/whoami.tsx +69 -0
  84. package/src/worker.ts +163 -0
  85. package/templates/new-worker.js +15 -0
  86. package/templates/new-worker.ts +15 -0
  87. package/{static-asset-facade.js → templates/static-asset-facade.js} +0 -0
  88. package/templates/tsconfig.json +105 -0
  89. package/wrangler-dist/cli.js +131958 -115793
  90. package/wrangler-dist/cli.js.map +3 -3
  91. package/src/__tests__/fixtures/init/.gitkeep +0 -0
  92. package/src/__tests__/mock-cfetch.js +0 -46
  93. package/src/api/form_data.ts +0 -141
  94. package/src/api/inspect.ts +0 -441
  95. package/src/api/worker.ts +0 -161
  96. package/src/cfetch.ts +0 -72
  97. package/src/config.ts +0 -122
  98. package/src/dev.tsx +0 -724
  99. package/src/kv.tsx +0 -211
  100. package/src/tail.tsx +0 -71
package/src/user.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  /* Based heavily on code from https://github.com/BitySA/oauth2-auth-code-pkce */
2
2
 
3
- /*
3
+ /*
4
4
 
5
5
  Apache License
6
6
  Version 2.0, January 2004
@@ -205,25 +205,42 @@
205
205
  limitations under the License.
206
206
  */
207
207
 
208
- import React from "react";
209
- import { render } from "ink";
210
- import Table from "ink-table";
211
- import SelectInput from "ink-select-input";
212
- import fetch from "node-fetch";
208
+ import assert from "node:assert";
213
209
  import { webcrypto as crypto } from "node:crypto";
214
- import { TextEncoder } from "node:util";
215
- import open from "open";
216
- import url from "node:url";
217
- import http from "node:http";
218
210
  import { readFile, writeFile, rm, mkdir } from "node:fs/promises";
219
- import path from "node:path";
220
- import process from "node:process";
211
+ import http from "node:http";
221
212
  import os from "node:os";
213
+ import path from "node:path";
214
+ import url from "node:url";
215
+ import { TextEncoder } from "node:util";
222
216
  import TOML from "@iarna/toml";
223
- import assert from "node:assert";
217
+ import { render, Text } from "ink";
218
+ import SelectInput from "ink-select-input";
219
+ import Table from "ink-table";
220
+ import React from "react";
221
+ import { fetch } from "undici";
222
+ import { getCloudflareApiBaseUrl } from "./cfetch";
223
+ import { getEnvironmentVariableFactory } from "./environment-variables";
224
+ import openInBrowser from "./open-in-browser";
225
+ import type { Item as SelectInputItem } from "ink-select-input/build/SelectInput";
224
226
  import type { ParsedUrlQuery } from "node:querystring";
225
- import { CF_API_BASE_URL } from "./cfetch";
226
- import type { Response } from "node-fetch";
227
+ import type { Response } from "undici";
228
+
229
+ /**
230
+ * Try to read the API token from the environment.
231
+ */
232
+ const getCloudflareAPITokenFromEnv = getEnvironmentVariableFactory({
233
+ variableName: "CLOUDFLARE_API_TOKEN",
234
+ deprecatedName: "CF_API_TOKEN",
235
+ });
236
+
237
+ /**
238
+ * Try to read the account ID from the environment.
239
+ */
240
+ const getCloudflareAccountIdFromEnv = getEnvironmentVariableFactory({
241
+ variableName: "CLOUDFLARE_ACCOUNT_ID",
242
+ deprecatedName: "CF_ACCOUNT_ID",
243
+ });
227
244
 
228
245
  /**
229
246
  * An implementation of rfc6749#section-4.1 and rfc7636.
@@ -254,38 +271,39 @@ interface AccessToken {
254
271
  expiry: string;
255
272
  }
256
273
 
257
- type Scope =
258
- | "account:read"
259
- | "user:read"
260
- | "workers:write"
261
- | "workers_kv:write"
262
- | "workers_routes:write"
263
- | "workers_scripts:write"
264
- | "workers_tail:read"
265
- | "zone:read"
266
- | "offline_access"; // this should be included by default
267
-
268
- const Scopes: Scope[] = [
269
- "account:read",
270
- "user:read",
271
- "workers:write",
272
- "workers_kv:write",
273
- "workers_routes:write",
274
- "workers_scripts:write",
275
- "workers_tail:read",
276
- "zone:read",
277
- ];
278
-
279
- const ScopeDescriptions = [
280
- "See your account info such as account details, analytics, and memberships.",
281
- "See your user info such as name, email address, and account memberships.",
282
- "See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.",
283
- "See and change Cloudflare Workers KV Storage data such as keys and namespaces.",
284
- "See and change Cloudflare Workers data such as filters and routes.",
285
- "See and change Cloudflare Workers scripts, durable objects, subdomains, triggers, and tail data.",
286
- "See Cloudflare Workers tail and script data.",
287
- "Grants read level access to account zone.",
288
- ];
274
+ const Scopes = {
275
+ "account:read":
276
+ "See your account info such as account details, analytics, and memberships.",
277
+ "user:read":
278
+ "See your user info such as name, email address, and account memberships.",
279
+ "workers:write":
280
+ "See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.",
281
+ "workers_kv:write":
282
+ "See and change Cloudflare Workers KV Storage data such as keys and namespaces.",
283
+ "workers_routes:write":
284
+ "See and change Cloudflare Workers data such as filters and routes.",
285
+ "workers_scripts:write":
286
+ "See and change Cloudflare Workers scripts, durable objects, subdomains, triggers, and tail data.",
287
+ "workers_tail:read": "See Cloudflare Workers tail and script data.",
288
+ "pages:write":
289
+ "See and change Cloudflare Pages projects, settings and deployments.",
290
+ "zone:read": "Grants read level access to account zone.",
291
+ } as const;
292
+
293
+ /**
294
+ * The possible keys for a Scope.
295
+ *
296
+ * "offline_access" is automatically included.
297
+ */
298
+ type Scope = keyof typeof Scopes;
299
+
300
+ const ScopeKeys = Object.keys(Scopes) as Scope[];
301
+
302
+ export function validateScopeKeys(
303
+ scopes: string[]
304
+ ): scopes is typeof ScopeKeys {
305
+ return scopes.every((scope) => scope in Scopes);
306
+ }
289
307
 
290
308
  const CLIENT_ID = "54d11594-84e4-41aa-b438-e81b8fa78ee7";
291
309
  const AUTH_URL = "https://dash.cloudflare.com/oauth2/auth";
@@ -298,14 +316,15 @@ let initialised = false;
298
316
 
299
317
  // we do this because we have some async stuff
300
318
  // TODO: this should just happen in the top level
301
- // abd we should fiure out how to do top level await
319
+ // and we should figure out how to do top level await
302
320
  export async function initialise(): Promise<void> {
303
- // get refreshtoken/accesstoken from fs if exists
321
+ // get refreshToken/accessToken from fs if exists
304
322
  try {
305
- // if CF_API_TOKEN available, use that
306
- if (process.env.CF_API_TOKEN) {
323
+ // if the environment variable is available, use that
324
+ const apiTokenFromEnv = getCloudflareAPITokenFromEnv();
325
+ if (apiTokenFromEnv) {
307
326
  LocalState.accessToken = {
308
- value: process.env.CF_API_TOKEN,
327
+ value: apiTokenFromEnv,
309
328
  expiry: "3021-12-31T23:59:59+00:00",
310
329
  };
311
330
  initialised = true;
@@ -344,11 +363,7 @@ function throwIfNotInitialised() {
344
363
  }
345
364
  }
346
365
 
347
- export function getAPIToken(): string {
348
- if (process.env.CF_API_TOKEN) {
349
- return process.env.CF_API_TOKEN;
350
- }
351
-
366
+ export function getAPIToken(): string | undefined {
352
367
  throwIfNotInitialised();
353
368
  return LocalState.accessToken?.value;
354
369
  }
@@ -548,7 +563,7 @@ function isReturningFromAuthServer(query: ParsedUrlQuery): boolean {
548
563
  return true;
549
564
  }
550
565
 
551
- export async function getAuthURL(scopes?: string[]): Promise<string> {
566
+ export async function getAuthURL(scopes = ScopeKeys): Promise<string> {
552
567
  const { codeChallenge, codeVerifier } = await generatePKCECodes();
553
568
  const stateQueryParam = generateRandomState(RECOMMENDED_STATE_LENGTH);
554
569
 
@@ -558,39 +573,44 @@ export async function getAuthURL(scopes?: string[]): Promise<string> {
558
573
  stateQueryParam,
559
574
  });
560
575
 
561
- // TODO: verify that the scopes passed are legit
562
-
563
576
  return (
564
577
  AUTH_URL +
565
578
  `?response_type=code&` +
566
579
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
567
580
  `redirect_uri=${encodeURIComponent(CALLBACK_URL)}&` +
568
- `scope=${encodeURIComponent(
569
- (scopes || Scopes).concat("offline_access").join(" ")
570
- )}&` +
581
+ // @ts-expect-error we add offline_access manually
582
+ `scope=${encodeURIComponent(scopes.concat("offline_access").join(" "))}&` +
571
583
  `state=${stateQueryParam}&` +
572
584
  `code_challenge=${encodeURIComponent(codeChallenge)}&` +
573
585
  `code_challenge_method=S256`
574
586
  );
575
587
  }
576
588
 
589
+ type TokenResponse =
590
+ | {
591
+ access_token: string;
592
+ expires_in: number;
593
+ refresh_token: string;
594
+ scope: string;
595
+ }
596
+ | {
597
+ error: string;
598
+ };
599
+
577
600
  /**
578
601
  * Refresh an access token from the remote service.
579
602
  */
580
603
  async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
581
- const { refreshToken } = LocalState;
582
-
583
- if (!refreshToken) {
604
+ if (!LocalState.refreshToken) {
584
605
  console.warn("No refresh token is present.");
585
606
  }
586
607
 
587
- const url = TOKEN_URL;
588
608
  const body =
589
609
  `grant_type=refresh_token&` +
590
- `refresh_token=${refreshToken?.value}&` +
610
+ `refresh_token=${LocalState.refreshToken?.value}&` +
591
611
  `client_id=${CLIENT_ID}`;
592
612
 
593
- const response = await fetch(url, {
613
+ const response = await fetch(TOKEN_URL, {
594
614
  method: "POST",
595
615
  body,
596
616
  headers: {
@@ -601,13 +621,12 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
601
621
  throw await response.json();
602
622
  } else {
603
623
  try {
604
- const json = await response.json();
605
- const { access_token, expires_in, refresh_token, scope } = json as {
606
- access_token: string;
607
- expires_in: number;
608
- refresh_token: string;
609
- scope: string;
610
- };
624
+ const json = (await response.json()) as TokenResponse;
625
+ if ("error" in json) {
626
+ throw json.error;
627
+ }
628
+
629
+ const { access_token, expires_in, refresh_token, scope } = json;
611
630
  let scopes: Scope[] = [];
612
631
 
613
632
  const accessToken: AccessToken = {
@@ -617,10 +636,9 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
617
636
  LocalState.accessToken = accessToken;
618
637
 
619
638
  if (refresh_token) {
620
- const refreshToken: RefreshToken = {
639
+ LocalState.refreshToken = {
621
640
  value: refresh_token,
622
641
  };
623
- LocalState.refreshToken = refreshToken;
624
642
  }
625
643
 
626
644
  if (scope) {
@@ -636,20 +654,24 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
636
654
  refreshToken: LocalState.refreshToken,
637
655
  };
638
656
  return accessContext;
639
- } catch (err) {
640
- const error = err?.error || "There was a network error.";
657
+ } catch (error) {
641
658
  switch (error) {
642
659
  case "invalid_grant":
643
- console.log(
660
+ console.warn(
644
661
  "Expired! Auth code or refresh token needs to be renewed."
645
662
  );
646
663
  // alert("Redirecting to auth server to obtain a new auth grant code.");
647
664
  // TODO: return refreshAuthCodeOrRefreshToken();
648
665
  break;
649
666
  default:
667
+ console.error(error);
650
668
  break;
651
669
  }
652
- throw toErrorClass(error);
670
+ if (typeof error === "string") {
671
+ throw toErrorClass(error);
672
+ } else {
673
+ throw error;
674
+ }
653
675
  }
654
676
  }
655
677
  }
@@ -666,7 +688,6 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
666
688
  console.warn("No authorization grant code is being passed.");
667
689
  }
668
690
 
669
- const url = TOKEN_URL;
670
691
  const body =
671
692
  `grant_type=authorization_code&` +
672
693
  `code=${encodeURIComponent(authorizationCode || "")}&` +
@@ -674,7 +695,7 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
674
695
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
675
696
  `code_verifier=${codeVerifier}`;
676
697
 
677
- const response = await fetch(url, {
698
+ const response = await fetch(TOKEN_URL, {
678
699
  method: "POST",
679
700
  body,
680
701
  headers: {
@@ -691,13 +712,11 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
691
712
  }
692
713
  throw toErrorClass(error);
693
714
  }
694
- const json = await response.json();
695
- const { access_token, expires_in, refresh_token, scope } = json as {
696
- access_token: string;
697
- expires_in: number;
698
- refresh_token: string;
699
- scope: string;
700
- };
715
+ const json = (await response.json()) as TokenResponse;
716
+ if ("error" in json) {
717
+ throw new Error(json.error);
718
+ }
719
+ const { access_token, expires_in, refresh_token, scope } = json;
701
720
  let scopes: Scope[] = [];
702
721
  LocalState.hasAuthCodeBeenExchangedForAccessToken = true;
703
722
 
@@ -709,10 +728,9 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
709
728
  LocalState.accessToken = accessToken;
710
729
 
711
730
  if (refresh_token) {
712
- const refreshToken: RefreshToken = {
731
+ LocalState.refreshToken = {
713
732
  value: refresh_token,
714
733
  };
715
- LocalState.refreshToken = refreshToken;
716
734
  }
717
735
 
718
736
  if (scope) {
@@ -798,15 +816,18 @@ expiration_time = "${tokenData.token?.expiry}"
798
816
  }
799
817
 
800
818
  type LoginProps = {
801
- scopes?: string[];
819
+ scopes?: Scope[];
802
820
  };
803
821
 
804
- export async function loginOrRefreshIfRequired(): Promise<boolean> {
822
+ export async function loginOrRefreshIfRequired(
823
+ isInteractive = true
824
+ ): Promise<boolean> {
805
825
  // TODO: if there already is a token, then try refreshing
806
826
  // TODO: ask permission before opening browser
807
827
  if (!LocalState.accessToken) {
808
- // not logged in.
809
- return await login();
828
+ // Not logged in.
829
+ // If we are not interactive, we cannot ask the user to login
830
+ return isInteractive && (await login());
810
831
  } else if (isAccessTokenExpired()) {
811
832
  return await refreshToken();
812
833
  } else {
@@ -816,18 +837,18 @@ export async function loginOrRefreshIfRequired(): Promise<boolean> {
816
837
 
817
838
  export async function login(props?: LoginProps): Promise<boolean> {
818
839
  const urlToOpen = await getAuthURL(props?.scopes);
819
- open(urlToOpen);
820
- // TODO: log url only if on system where it's unreliable/unavailable
821
- // console.log(`💁 Opened ${urlToOpen}`);
822
- let server;
823
- let loginTimeoutHandle;
840
+ await openInBrowser(urlToOpen);
841
+ let server: http.Server;
842
+ let loginTimeoutHandle: NodeJS.Timeout;
824
843
  const timerPromise = new Promise<boolean>((resolve) => {
825
844
  loginTimeoutHandle = setTimeout(() => {
826
- console.error("Timed out waiting for authorization code.");
845
+ console.error(
846
+ "Timed out waiting for authorization code, please try again."
847
+ );
827
848
  server.close();
828
849
  clearTimeout(loginTimeoutHandle);
829
850
  resolve(false);
830
- }, 60000); // wait for 30 seconds for the user to authorize
851
+ }, 60000); // wait for 60 seconds for the user to authorize
831
852
  });
832
853
 
833
854
  const loginPromise = new Promise<boolean>((resolve, reject) => {
@@ -858,8 +879,9 @@ export async function login(props?: LoginProps): Promise<boolean> {
858
879
  finish(false);
859
880
  });
860
881
  console.log(
861
- "Error: Consent denied. You must grant consent to Wrangler in order to login. If you don't want to do this consider passing an API token with CF_API_TOKEN variable"
862
- ); // TODO: implement wrangler config lol
882
+ "Error: Consent denied. You must grant consent to Wrangler in order to login.\n" +
883
+ "If you don't want to do this consider passing an API token via the `CLOUDFLARE_API_TOKEN` environment variable"
884
+ );
863
885
 
864
886
  return;
865
887
  } else {
@@ -921,15 +943,14 @@ export async function refreshToken(): Promise<boolean> {
921
943
 
922
944
  export async function logout(): Promise<void> {
923
945
  throwIfNotInitialised();
924
- const { refreshToken } = LocalState;
925
- if (!refreshToken) {
946
+ if (!LocalState.refreshToken) {
926
947
  console.log("Not logged in, exiting...");
927
948
  return;
928
949
  }
929
950
  const body =
930
951
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
931
952
  `token_type_hint=refresh_token&` +
932
- `token=${encodeURIComponent(refreshToken?.value || "")}`;
953
+ `token=${encodeURIComponent(LocalState.refreshToken?.value || "")}`;
933
954
 
934
955
  const response = await fetch(REVOKE_URL, {
935
956
  method: "POST",
@@ -952,25 +973,28 @@ export async function logout(): Promise<void> {
952
973
  export function listScopes(): void {
953
974
  throwIfNotInitialised();
954
975
  console.log("💁 Available scopes:");
955
- const data = Scopes.map((scope, index) => ({
976
+ const data = ScopeKeys.map((scope: Scope) => ({
956
977
  Scope: scope,
957
- Description: ScopeDescriptions[index],
978
+ Description: Scopes[scope],
958
979
  }));
959
980
  render(<Table data={data} />);
960
981
  // TODO: maybe a good idea to show usage here
961
982
  }
962
983
 
963
- export async function getAccountId() {
984
+ export async function getAccountId(
985
+ isInteractive = true
986
+ ): Promise<string | undefined> {
964
987
  const apiToken = getAPIToken();
965
988
  if (!apiToken) return;
966
989
 
967
- if (process.env.CF_ACCOUNT_ID) {
968
- return process.env.CF_ACCOUNT_ID;
990
+ const accountIdFromEnv = getCloudflareAccountIdFromEnv();
991
+ if (accountIdFromEnv) {
992
+ return accountIdFromEnv;
969
993
  }
970
994
 
971
995
  let response: Response;
972
996
  try {
973
- response = await fetch(`${CF_API_BASE_URL}/memberships`, {
997
+ response = await fetch(`${getCloudflareApiBaseUrl()}/memberships`, {
974
998
  method: "GET",
975
999
  headers: {
976
1000
  Authorization: "Bearer " + apiToken,
@@ -978,19 +1002,19 @@ export async function getAccountId() {
978
1002
  });
979
1003
  } catch (err) {
980
1004
  // probably offline
1005
+ return;
981
1006
  }
982
- if (!response) return;
983
- let accountId: string;
984
- // @ts-expect-error need to type this response
985
- const responseJSON: {
1007
+
1008
+ let accountId: string | undefined;
1009
+ const responseJSON = (await response.json()) as {
986
1010
  success: boolean;
987
1011
  result: { id: string; account: { id: string; name: string } }[];
988
- } = await response.json();
1012
+ };
989
1013
 
990
1014
  if (responseJSON.success === true) {
991
1015
  if (responseJSON.result.length === 1) {
992
1016
  accountId = responseJSON.result[0].account.id;
993
- } else {
1017
+ } else if (isInteractive) {
994
1018
  accountId = await new Promise((resolve) => {
995
1019
  const accounts = responseJSON.result.map((x) => x.account);
996
1020
  const { unmount } = render(
@@ -1003,6 +1027,15 @@ export async function getAccountId() {
1003
1027
  />
1004
1028
  );
1005
1029
  });
1030
+ } else {
1031
+ throw new Error(
1032
+ "More than one account available but unable to select one in non-interactive mode.\n" +
1033
+ `Please set the appropriate \`account_id\` in your \`wrangler.toml\` file.\n` +
1034
+ `Available accounts are ("<name>" - "<id>"):\n` +
1035
+ responseJSON.result
1036
+ .map((x) => ` "${x.account.name}" - "${x.account.id}")`)
1037
+ .join("\n")
1038
+ );
1006
1039
  }
1007
1040
  }
1008
1041
  return accountId;
@@ -1014,16 +1047,19 @@ type ChooseAccountItem = {
1014
1047
  };
1015
1048
  export function ChooseAccount(props: {
1016
1049
  accounts: ChooseAccountItem[];
1017
- onSelect: (item) => void;
1050
+ onSelect: (item: SelectInputItem<ChooseAccountItem>) => void;
1018
1051
  }) {
1019
1052
  return (
1020
- <SelectInput
1021
- items={props.accounts.map((item) => ({
1022
- key: item.id,
1023
- label: item.name,
1024
- value: item,
1025
- }))}
1026
- onSelect={props.onSelect}
1027
- />
1053
+ <>
1054
+ <Text bold>Select an account from below:</Text>
1055
+ <SelectInput
1056
+ items={props.accounts.map((item) => ({
1057
+ key: item.id,
1058
+ label: item.name,
1059
+ value: item,
1060
+ }))}
1061
+ onSelect={props.onSelect}
1062
+ />
1063
+ </>
1028
1064
  );
1029
1065
  }
package/src/whoami.tsx ADDED
@@ -0,0 +1,69 @@
1
+ import { Text, render } from "ink";
2
+ import Table from "ink-table";
3
+ import React from "react";
4
+ import { fetchListResult, fetchResult } from "./cfetch";
5
+ import { getAPIToken } from "./user";
6
+
7
+ export async function whoami() {
8
+ console.log("Getting User settings...");
9
+ const user = await getUserInfo();
10
+ render(<WhoAmI user={user}></WhoAmI>);
11
+ }
12
+
13
+ export function WhoAmI({ user }: { user: UserInfo | undefined }) {
14
+ return user ? (
15
+ <>
16
+ <Email tokenType={user.authType} email={user.email}></Email>
17
+ <Accounts accounts={user.accounts}></Accounts>
18
+ </>
19
+ ) : (
20
+ <Text>You are not authenticated. Please run `wrangler login`.</Text>
21
+ );
22
+ }
23
+
24
+ function Email(props: { tokenType: string; email: string }) {
25
+ return (
26
+ <Text>
27
+ 👋 You are logged in with an {props.tokenType} Token, associated with the
28
+ email &apos;{props.email}&apos;!
29
+ </Text>
30
+ );
31
+ }
32
+
33
+ function Accounts(props: { accounts: AccountInfo[] }) {
34
+ const accounts = props.accounts.map((account) => ({
35
+ "Account Name": account.name,
36
+ "Account ID": account.id,
37
+ }));
38
+ return <Table data={accounts}></Table>;
39
+ }
40
+
41
+ export interface UserInfo {
42
+ apiToken: string;
43
+ authType: string;
44
+ email: string;
45
+ accounts: AccountInfo[];
46
+ }
47
+
48
+ export async function getUserInfo(): Promise<UserInfo | undefined> {
49
+ const apiToken = getAPIToken();
50
+ return apiToken
51
+ ? {
52
+ apiToken,
53
+ authType: "OAuth",
54
+ email: await getEmail(),
55
+ accounts: await getAccounts(),
56
+ }
57
+ : undefined;
58
+ }
59
+
60
+ async function getEmail(): Promise<string> {
61
+ const { email } = await fetchResult<{ email: string }>("/user");
62
+ return email;
63
+ }
64
+
65
+ type AccountInfo = { name: string; id: string };
66
+
67
+ async function getAccounts(): Promise<AccountInfo[]> {
68
+ return await fetchListResult<AccountInfo>("/accounts");
69
+ }