wrangler 0.0.0-e6733a3 → 0.0.0-e88a54e

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 (99) hide show
  1. package/bin/wrangler.js +2 -2
  2. package/miniflare-dist/index.mjs +79 -0
  3. package/miniflare-dist/index.mjs.map +7 -0
  4. package/package.json +35 -16
  5. package/pages/functions/buildWorker.ts +1 -1
  6. package/pages/functions/filepath-routing.test.ts +212 -28
  7. package/pages/functions/filepath-routing.ts +91 -131
  8. package/pages/functions/routes.ts +11 -18
  9. package/pages/functions/template-worker.ts +19 -33
  10. package/src/__tests__/configuration.test.ts +2796 -0
  11. package/src/__tests__/dev.test.tsx +488 -58
  12. package/src/__tests__/dev2.test.tsx +154 -0
  13. package/src/__tests__/guess-worker-format.test.ts +58 -0
  14. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  15. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  16. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  17. package/src/__tests__/helpers/mock-account-id.ts +48 -0
  18. package/src/__tests__/helpers/mock-bin.ts +36 -0
  19. package/src/__tests__/{mock-cfetch.ts → helpers/mock-cfetch.ts} +55 -19
  20. package/src/__tests__/helpers/mock-console.ts +70 -0
  21. package/src/__tests__/{mock-dialogs.ts → helpers/mock-dialogs.ts} +1 -1
  22. package/src/__tests__/helpers/mock-kv.ts +33 -0
  23. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  24. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  25. package/src/__tests__/helpers/run-in-tmp.ts +32 -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 +779 -75
  30. package/src/__tests__/jest.setup.ts +53 -6
  31. package/src/__tests__/kv.test.ts +738 -454
  32. package/src/__tests__/logout.test.ts +52 -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 +2704 -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 +100 -0
  43. package/src/bundle.ts +142 -0
  44. package/src/cfetch/index.ts +21 -19
  45. package/src/cfetch/internal.ts +94 -24
  46. package/src/cli.ts +10 -0
  47. package/src/config/README.md +107 -0
  48. package/src/config/config.ts +218 -0
  49. package/src/config/diagnostics.ts +80 -0
  50. package/src/config/environment.ts +366 -0
  51. package/src/config/index.ts +62 -0
  52. package/src/config/validation-helpers.ts +486 -0
  53. package/src/config/validation.ts +1090 -0
  54. package/src/{api/preview.ts → create-worker-preview.ts} +61 -23
  55. package/src/create-worker-upload-form.ts +217 -0
  56. package/src/dev/dev.tsx +364 -0
  57. package/src/dev/local.tsx +232 -0
  58. package/src/dev/remote.tsx +203 -0
  59. package/src/dev/use-esbuild.ts +100 -0
  60. package/src/dialogs.tsx +2 -2
  61. package/src/entry.ts +208 -0
  62. package/src/environment-variables.ts +33 -0
  63. package/src/https-options.ts +122 -0
  64. package/src/index.tsx +1198 -799
  65. package/src/inspect.ts +159 -66
  66. package/src/{kv.tsx → kv.ts} +117 -65
  67. package/src/miniflare-cli/README.md +30 -0
  68. package/src/miniflare-cli/enum-keys.ts +17 -0
  69. package/src/miniflare-cli/index.ts +37 -0
  70. package/src/module-collection.ts +229 -35
  71. package/src/open-in-browser.ts +25 -0
  72. package/src/package-manager.ts +126 -0
  73. package/src/pages.tsx +58 -42
  74. package/src/parse.ts +183 -0
  75. package/src/paths.ts +26 -0
  76. package/src/proxy.ts +224 -78
  77. package/src/publish.ts +281 -292
  78. package/src/r2.ts +50 -0
  79. package/src/reporting.ts +131 -0
  80. package/src/selfsigned.d.ts +29 -0
  81. package/src/sites.tsx +224 -58
  82. package/src/tail/filters.ts +279 -0
  83. package/src/tail/index.ts +299 -0
  84. package/src/tail/printing.ts +62 -0
  85. package/src/user.tsx +273 -180
  86. package/src/whoami.tsx +69 -0
  87. package/src/{api/worker.ts → worker.ts} +45 -37
  88. package/templates/new-worker.js +15 -0
  89. package/templates/new-worker.ts +15 -0
  90. package/{static-asset-facade.js → templates/static-asset-facade.js} +0 -0
  91. package/templates/tsconfig.json +105 -0
  92. package/wrangler-dist/cli.js +130612 -112486
  93. package/wrangler-dist/cli.js.map +3 -3
  94. package/src/__tests__/run-in-tmp.ts +0 -19
  95. package/src/__tests__/run-wrangler.ts +0 -32
  96. package/src/api/form_data.ts +0 -131
  97. package/src/config.ts +0 -487
  98. package/src/dev.tsx +0 -771
  99. package/src/tail.tsx +0 -73
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,43 @@
205
205
  limitations under the License.
206
206
  */
207
207
 
208
- import React from "react";
209
- import { render, Text } 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";
210
+ import { writeFileSync, mkdirSync, rmSync } from "node:fs";
217
211
  import http from "node:http";
218
- import { readFile, writeFile, rm, mkdir } from "node:fs/promises";
219
- import path from "node:path";
220
- import process from "node:process";
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 { readFileSync } from "./parse";
226
+ import type { Item as SelectInputItem } from "ink-select-input/build/SelectInput";
224
227
  import type { ParsedUrlQuery } from "node:querystring";
225
- import { CF_API_BASE_URL } from "./cfetch";
226
- import type { Response } from "node-fetch";
228
+ import type { Response } from "undici";
229
+
230
+ /**
231
+ * Try to read the API token from the environment.
232
+ */
233
+ const getCloudflareAPITokenFromEnv = getEnvironmentVariableFactory({
234
+ variableName: "CLOUDFLARE_API_TOKEN",
235
+ deprecatedName: "CF_API_TOKEN",
236
+ });
237
+
238
+ /**
239
+ * Try to read the account ID from the environment.
240
+ */
241
+ const getCloudflareAccountIdFromEnv = getEnvironmentVariableFactory({
242
+ variableName: "CLOUDFLARE_ACCOUNT_ID",
243
+ deprecatedName: "CF_ACCOUNT_ID",
244
+ });
227
245
 
228
246
  /**
229
247
  * An implementation of rfc6749#section-4.1 and rfc7636.
@@ -234,17 +252,45 @@ interface PKCECodes {
234
252
  codeVerifier: string;
235
253
  }
236
254
 
237
- interface State {
238
- accessToken?: AccessToken; // persist
255
+ /**
256
+ * The module level state of the authentication flow.
257
+ */
258
+ interface State extends AuthTokens {
239
259
  authorizationCode?: string;
240
260
  codeChallenge?: string;
241
261
  codeVerifier?: string;
242
262
  hasAuthCodeBeenExchangedForAccessToken?: boolean;
243
- refreshToken?: RefreshToken; // persist
244
263
  stateQueryParam?: string;
245
264
  scopes?: Scope[];
246
265
  }
247
266
 
267
+ /**
268
+ * The tokens related to authentication.
269
+ */
270
+ interface AuthTokens {
271
+ accessToken?: AccessToken;
272
+ refreshToken?: RefreshToken;
273
+ /** @deprecated - this field was only provided by the deprecated `wrangler1 config` command. */
274
+ apiToken?: string;
275
+ }
276
+
277
+ /**
278
+ * The path to the config file that holds user authentication data,
279
+ * relative to the user's home directory.
280
+ */
281
+ export const USER_AUTH_CONFIG_FILE = ".wrangler/config/default.toml";
282
+
283
+ /**
284
+ * The data that may be read from the `USER_CONFIG_FILE`.
285
+ */
286
+ export interface UserAuthConfig {
287
+ oauth_token?: string;
288
+ refresh_token?: string;
289
+ expiration_time?: string;
290
+ /** @deprecated - this field was only provided by the deprecated `wrangler1 config` command. */
291
+ api_token?: string;
292
+ }
293
+
248
294
  interface RefreshToken {
249
295
  value: string;
250
296
  }
@@ -254,38 +300,39 @@ interface AccessToken {
254
300
  expiry: string;
255
301
  }
256
302
 
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
- ];
303
+ const Scopes = {
304
+ "account:read":
305
+ "See your account info such as account details, analytics, and memberships.",
306
+ "user:read":
307
+ "See your user info such as name, email address, and account memberships.",
308
+ "workers:write":
309
+ "See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.",
310
+ "workers_kv:write":
311
+ "See and change Cloudflare Workers KV Storage data such as keys and namespaces.",
312
+ "workers_routes:write":
313
+ "See and change Cloudflare Workers data such as filters and routes.",
314
+ "workers_scripts:write":
315
+ "See and change Cloudflare Workers scripts, durable objects, subdomains, triggers, and tail data.",
316
+ "workers_tail:read": "See Cloudflare Workers tail and script data.",
317
+ "pages:write":
318
+ "See and change Cloudflare Pages projects, settings and deployments.",
319
+ "zone:read": "Grants read level access to account zone.",
320
+ } as const;
321
+
322
+ /**
323
+ * The possible keys for a Scope.
324
+ *
325
+ * "offline_access" is automatically included.
326
+ */
327
+ type Scope = keyof typeof Scopes;
328
+
329
+ const ScopeKeys = Object.keys(Scopes) as Scope[];
330
+
331
+ export function validateScopeKeys(
332
+ scopes: string[]
333
+ ): scopes is typeof ScopeKeys {
334
+ return scopes.every((scope) => scope in Scopes);
335
+ }
289
336
 
290
337
  const CLIENT_ID = "54d11594-84e4-41aa-b438-e81b8fa78ee7";
291
338
  const AUTH_URL = "https://dash.cloudflare.com/oauth2/auth";
@@ -293,63 +340,63 @@ const TOKEN_URL = "https://dash.cloudflare.com/oauth2/token";
293
340
  const CALLBACK_URL = "http://localhost:8976/oauth/callback";
294
341
  const REVOKE_URL = "https://dash.cloudflare.com/oauth2/revoke";
295
342
 
296
- const LocalState: State = {};
297
- let initialised = false;
343
+ let LocalState: State = getAuthTokens();
298
344
 
299
- // we do this because we have some async stuff
300
- // TODO: this should just happen in the top level
301
- // abd we should fiure out how to do top level await
302
- export async function initialise(): Promise<void> {
303
- // get refreshtoken/accesstoken from fs if exists
345
+ /**
346
+ * Compute the current auth tokens.
347
+ */
348
+ function getAuthTokens(): AuthTokens {
349
+ // get refreshToken/accessToken from fs if exists
304
350
  try {
305
- // if CF_API_TOKEN available, use that
306
- if (process.env.CF_API_TOKEN) {
307
- LocalState.accessToken = {
308
- value: process.env.CF_API_TOKEN,
309
- expiry: "3021-12-31T23:59:59+00:00",
351
+ // if the environment variable is available, use that
352
+ const apiTokenFromEnv = getCloudflareAPITokenFromEnv();
353
+ if (apiTokenFromEnv) {
354
+ return {
355
+ accessToken: {
356
+ value: apiTokenFromEnv,
357
+ expiry: "3021-12-31T23:59:59+00:00",
358
+ },
310
359
  };
311
- initialised = true;
312
- return;
313
360
  }
314
361
 
315
- const toml = TOML.parse(
316
- await readFile(path.join(os.homedir(), ".wrangler/config/default.toml"), {
317
- encoding: "utf-8",
318
- })
319
- );
320
- const { oauth_token, refresh_token, expiration_time } = toml as {
321
- oauth_token: string;
322
- refresh_token: string;
323
- expiration_time: string;
324
- };
362
+ // otherwise try loading from the user auth config file.
363
+ const { oauth_token, refresh_token, expiration_time, api_token } =
364
+ readAuthConfigFile();
365
+
325
366
  if (oauth_token) {
326
- LocalState.accessToken = { value: oauth_token, expiry: expiration_time };
327
- }
328
- if (refresh_token) {
329
- LocalState.refreshToken = { value: refresh_token };
367
+ return {
368
+ accessToken: {
369
+ value: oauth_token,
370
+ // If there is no `expiration_time` field then set it to an old date, to cause it to expire immediately.
371
+ expiry: expiration_time ?? "2000-01-01:00:00:00+00:00",
372
+ },
373
+ refreshToken: { value: refresh_token ?? "" },
374
+ };
375
+ } else if (api_token) {
376
+ return { apiToken: api_token };
377
+ } else {
378
+ return {};
330
379
  }
331
- } catch (err) {
332
- // no config yet, let's chill
333
- // console.error(err);
380
+ } catch {
381
+ return {};
334
382
  }
335
- initialised = true;
336
383
  }
337
384
 
338
- // ugh. TODO: see fix from above.
339
- function throwIfNotInitialised() {
340
- if (initialised === false) {
341
- throw new Error(
342
- "did you forget to call initialise() from the user module?"
343
- );
344
- }
385
+ /**
386
+ * Run the initialisation of the auth state, in the case that something changed.
387
+ */
388
+ export function reinitialiseAuthTokens(): void {
389
+ LocalState = getAuthTokens();
345
390
  }
346
391
 
347
- export function getAPIToken(): string {
348
- if (process.env.CF_API_TOKEN) {
349
- return process.env.CF_API_TOKEN;
392
+ export function getAPIToken(): string | undefined {
393
+ if (LocalState.apiToken) {
394
+ console.warn(
395
+ "It looks like you have used Wrangler 1's `config` command to login with an API token.\n" +
396
+ "This is no longer supported in the current version of Wrangler.\n" +
397
+ "If you wish to authenticate via an API token then please set the `CLOUDFLARE_API_TOKEN` environment variable."
398
+ );
350
399
  }
351
-
352
- throwIfNotInitialised();
353
400
  return LocalState.accessToken?.value;
354
401
  }
355
402
 
@@ -548,7 +595,7 @@ function isReturningFromAuthServer(query: ParsedUrlQuery): boolean {
548
595
  return true;
549
596
  }
550
597
 
551
- export async function getAuthURL(scopes?: string[]): Promise<string> {
598
+ export async function getAuthURL(scopes = ScopeKeys): Promise<string> {
552
599
  const { codeChallenge, codeVerifier } = await generatePKCECodes();
553
600
  const stateQueryParam = generateRandomState(RECOMMENDED_STATE_LENGTH);
554
601
 
@@ -558,22 +605,30 @@ export async function getAuthURL(scopes?: string[]): Promise<string> {
558
605
  stateQueryParam,
559
606
  });
560
607
 
561
- // TODO: verify that the scopes passed are legit
562
-
563
608
  return (
564
609
  AUTH_URL +
565
610
  `?response_type=code&` +
566
611
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
567
612
  `redirect_uri=${encodeURIComponent(CALLBACK_URL)}&` +
568
- `scope=${encodeURIComponent(
569
- (scopes || Scopes).concat("offline_access").join(" ")
570
- )}&` +
613
+ // @ts-expect-error we add offline_access manually
614
+ `scope=${encodeURIComponent(scopes.concat("offline_access").join(" "))}&` +
571
615
  `state=${stateQueryParam}&` +
572
616
  `code_challenge=${encodeURIComponent(codeChallenge)}&` +
573
617
  `code_challenge_method=S256`
574
618
  );
575
619
  }
576
620
 
621
+ type TokenResponse =
622
+ | {
623
+ access_token: string;
624
+ expires_in: number;
625
+ refresh_token: string;
626
+ scope: string;
627
+ }
628
+ | {
629
+ error: string;
630
+ };
631
+
577
632
  /**
578
633
  * Refresh an access token from the remote service.
579
634
  */
@@ -598,13 +653,12 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
598
653
  throw await response.json();
599
654
  } else {
600
655
  try {
601
- const json = await response.json();
602
- const { access_token, expires_in, refresh_token, scope } = json as {
603
- access_token: string;
604
- expires_in: number;
605
- refresh_token: string;
606
- scope: string;
607
- };
656
+ const json = (await response.json()) as TokenResponse;
657
+ if ("error" in json) {
658
+ throw json.error;
659
+ }
660
+
661
+ const { access_token, expires_in, refresh_token, scope } = json;
608
662
  let scopes: Scope[] = [];
609
663
 
610
664
  const accessToken: AccessToken = {
@@ -632,20 +686,24 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
632
686
  refreshToken: LocalState.refreshToken,
633
687
  };
634
688
  return accessContext;
635
- } catch (err) {
636
- const error = err?.error || "There was a network error.";
689
+ } catch (error) {
637
690
  switch (error) {
638
691
  case "invalid_grant":
639
- console.log(
692
+ console.warn(
640
693
  "Expired! Auth code or refresh token needs to be renewed."
641
694
  );
642
695
  // alert("Redirecting to auth server to obtain a new auth grant code.");
643
696
  // TODO: return refreshAuthCodeOrRefreshToken();
644
697
  break;
645
698
  default:
699
+ console.error(error);
646
700
  break;
647
701
  }
648
- throw toErrorClass(error);
702
+ if (typeof error === "string") {
703
+ throw toErrorClass(error);
704
+ } else {
705
+ throw error;
706
+ }
649
707
  }
650
708
  }
651
709
  }
@@ -686,13 +744,11 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
686
744
  }
687
745
  throw toErrorClass(error);
688
746
  }
689
- const json = await response.json();
690
- const { access_token, expires_in, refresh_token, scope } = json as {
691
- access_token: string;
692
- expires_in: number;
693
- refresh_token: string;
694
- scope: string;
695
- };
747
+ const json = (await response.json()) as TokenResponse;
748
+ if ("error" in json) {
749
+ throw new Error(json.error);
750
+ }
751
+ const { access_token, expires_in, refresh_token, scope } = json;
696
752
  let scopes: Scope[] = [];
697
753
  LocalState.hasAuthCodeBeenExchangedForAccessToken = true;
698
754
 
@@ -776,31 +832,37 @@ function generateRandomState(lengthOfState: number): string {
776
832
  .join("");
777
833
  }
778
834
 
779
- async function writeToConfigFile(tokenData: AccessContext) {
780
- await mkdir(path.join(os.homedir(), ".wrangler/config/"), {
835
+ export function writeAuthConfigFile(config: UserAuthConfig) {
836
+ mkdirSync(path.join(os.homedir(), ".wrangler/config/"), {
781
837
  recursive: true,
782
838
  });
783
- await writeFile(
784
- path.join(os.homedir(), ".wrangler/config/default.toml"),
785
- `
786
- oauth_token = "${tokenData.token?.value || ""}"
787
- refresh_token = "${tokenData.refreshToken?.value}"
788
- expiration_time = "${tokenData.token?.expiry}"
789
- `,
839
+ writeFileSync(
840
+ path.join(os.homedir(), USER_AUTH_CONFIG_FILE),
841
+ TOML.stringify(config as TOML.JsonMap),
790
842
  { encoding: "utf-8" }
791
843
  );
792
844
  }
793
845
 
846
+ function readAuthConfigFile(): UserAuthConfig {
847
+ const toml = TOML.parse(
848
+ readFileSync(path.join(os.homedir(), USER_AUTH_CONFIG_FILE))
849
+ );
850
+ return toml;
851
+ }
852
+
794
853
  type LoginProps = {
795
- scopes?: string[];
854
+ scopes?: Scope[];
796
855
  };
797
856
 
798
- export async function loginOrRefreshIfRequired(): Promise<boolean> {
857
+ export async function loginOrRefreshIfRequired(
858
+ isInteractive = true
859
+ ): Promise<boolean> {
799
860
  // TODO: if there already is a token, then try refreshing
800
861
  // TODO: ask permission before opening browser
801
- if (!LocalState.accessToken) {
802
- // not logged in.
803
- return await login();
862
+ if (!getAPIToken()) {
863
+ // Not logged in.
864
+ // If we are not interactive, we cannot ask the user to login
865
+ return isInteractive && (await login());
804
866
  } else if (isAccessTokenExpired()) {
805
867
  return await refreshToken();
806
868
  } else {
@@ -809,19 +871,20 @@ export async function loginOrRefreshIfRequired(): Promise<boolean> {
809
871
  }
810
872
 
811
873
  export async function login(props?: LoginProps): Promise<boolean> {
874
+ console.log("Attempting to login via OAuth...");
812
875
  const urlToOpen = await getAuthURL(props?.scopes);
813
- await open(urlToOpen);
814
- // TODO: log url only if on system where it's unreliable/unavailable
815
- // console.log(`💁 Opened ${urlToOpen}`);
816
- let server;
817
- let loginTimeoutHandle;
876
+ await openInBrowser(urlToOpen);
877
+ let server: http.Server;
878
+ let loginTimeoutHandle: NodeJS.Timeout;
818
879
  const timerPromise = new Promise<boolean>((resolve) => {
819
880
  loginTimeoutHandle = setTimeout(() => {
820
- console.error("Timed out waiting for authorization code.");
881
+ console.error(
882
+ "Timed out waiting for authorization code, please try again."
883
+ );
821
884
  server.close();
822
885
  clearTimeout(loginTimeoutHandle);
823
886
  resolve(false);
824
- }, 60000); // wait for 30 seconds for the user to authorize
887
+ }, 60000); // wait for 60 seconds for the user to authorize
825
888
  });
826
889
 
827
890
  const loginPromise = new Promise<boolean>((resolve, reject) => {
@@ -852,8 +915,9 @@ export async function login(props?: LoginProps): Promise<boolean> {
852
915
  finish(false);
853
916
  });
854
917
  console.log(
855
- "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"
856
- ); // TODO: implement wrangler config lol
918
+ "Error: Consent denied. You must grant consent to Wrangler in order to login.\n" +
919
+ "If you don't want to do this consider passing an API token via the `CLOUDFLARE_API_TOKEN` environment variable"
920
+ );
857
921
 
858
922
  return;
859
923
  } else {
@@ -866,8 +930,12 @@ export async function login(props?: LoginProps): Promise<boolean> {
866
930
  finish(false, new ErrorNoAuthCode());
867
931
  return;
868
932
  } else {
869
- const tokenData = await exchangeAuthCodeForAccessToken();
870
- await writeToConfigFile(tokenData);
933
+ const exchange = await exchangeAuthCodeForAccessToken();
934
+ writeAuthConfigFile({
935
+ oauth_token: exchange.token?.value ?? "",
936
+ expiration_time: exchange.token?.expiry,
937
+ refresh_token: exchange.refreshToken?.value,
938
+ });
871
939
  res.writeHead(307, {
872
940
  Location:
873
941
  "https://welcome.developers.workers.dev/wrangler-oauth-consent-granted",
@@ -875,9 +943,7 @@ export async function login(props?: LoginProps): Promise<boolean> {
875
943
  res.end(() => {
876
944
  finish(true);
877
945
  });
878
- console.log(
879
- `Successfully configured. You can find your configuration file at: ${os.homedir()}/.wrangler/config/default.toml`
880
- );
946
+ console.log(`Successfully logged in.`);
881
947
 
882
948
  return;
883
949
  }
@@ -894,18 +960,22 @@ export async function login(props?: LoginProps): Promise<boolean> {
894
960
  /**
895
961
  * Checks to see if the access token has expired.
896
962
  */
897
- export function isAccessTokenExpired(): boolean {
898
- throwIfNotInitialised();
963
+ function isAccessTokenExpired(): boolean {
899
964
  const { accessToken } = LocalState;
900
965
  return Boolean(accessToken && new Date() >= new Date(accessToken.expiry));
901
966
  }
902
967
 
903
- export async function refreshToken(): Promise<boolean> {
904
- throwIfNotInitialised();
968
+ async function refreshToken(): Promise<boolean> {
905
969
  // refresh
906
970
  try {
907
- const refreshed = await exchangeRefreshTokenForAccessToken();
908
- await writeToConfigFile(refreshed);
971
+ const {
972
+ token: { value: oauth_token, expiry: expiration_time } = {
973
+ value: "",
974
+ expiry: "",
975
+ },
976
+ refreshToken: { value: refresh_token } = {},
977
+ } = await exchangeRefreshTokenForAccessToken();
978
+ writeAuthConfigFile({ oauth_token, expiration_time, refresh_token });
909
979
  return true;
910
980
  } catch (err) {
911
981
  console.error(err);
@@ -914,10 +984,28 @@ export async function refreshToken(): Promise<boolean> {
914
984
  }
915
985
 
916
986
  export async function logout(): Promise<void> {
917
- throwIfNotInitialised();
918
- if (!LocalState.refreshToken) {
919
- console.log("Not logged in, exiting...");
920
- return;
987
+ if (!LocalState.accessToken) {
988
+ if (!LocalState.refreshToken) {
989
+ console.log("Not logged in, exiting...");
990
+ return;
991
+ }
992
+
993
+ const body =
994
+ `client_id=${encodeURIComponent(CLIENT_ID)}&` +
995
+ `token_type_hint=refresh_token&` +
996
+ `token=${encodeURIComponent(LocalState.refreshToken?.value || "")}`;
997
+
998
+ const response = await fetch(REVOKE_URL, {
999
+ method: "POST",
1000
+ body,
1001
+ headers: {
1002
+ "Content-Type": "application/x-www-form-urlencoded",
1003
+ },
1004
+ });
1005
+ await response.text(); // blank text? would be nice if it was something meaningful
1006
+ console.log(
1007
+ "💁 Wrangler is configured with an OAuth token. The token has been successfully revoked"
1008
+ );
921
1009
  }
922
1010
  const body =
923
1011
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
@@ -932,38 +1020,34 @@ export async function logout(): Promise<void> {
932
1020
  },
933
1021
  });
934
1022
  await response.text(); // blank text? would be nice if it was something meaningful
935
- console.log(
936
- "💁 Wrangler is configured with an OAuth token. The token has been successfully revoked"
937
- );
938
- // delete the file
939
- await rm(path.join(os.homedir(), ".wrangler/config/default.toml"));
940
- console.log(
941
- `Removing ${os.homedir()}/.wrangler/config/default.toml.. success!`
942
- );
1023
+ rmSync(path.join(os.homedir(), USER_AUTH_CONFIG_FILE));
1024
+ console.log(`Successfully logged out.`);
943
1025
  }
944
1026
 
945
- export function listScopes(): void {
946
- throwIfNotInitialised();
947
- console.log("💁 Available scopes:");
948
- const data = Scopes.map((scope, index) => ({
1027
+ export function listScopes(message = "💁 Available scopes:"): void {
1028
+ console.log(message);
1029
+ const data = ScopeKeys.map((scope: Scope) => ({
949
1030
  Scope: scope,
950
- Description: ScopeDescriptions[index],
1031
+ Description: Scopes[scope],
951
1032
  }));
952
1033
  render(<Table data={data} />);
953
1034
  // TODO: maybe a good idea to show usage here
954
1035
  }
955
1036
 
956
- export async function getAccountId() {
1037
+ export async function getAccountId(
1038
+ isInteractive = true
1039
+ ): Promise<string | undefined> {
957
1040
  const apiToken = getAPIToken();
958
1041
  if (!apiToken) return;
959
1042
 
960
- if (process.env.CF_ACCOUNT_ID) {
961
- return process.env.CF_ACCOUNT_ID;
1043
+ const accountIdFromEnv = getCloudflareAccountIdFromEnv();
1044
+ if (accountIdFromEnv) {
1045
+ return accountIdFromEnv;
962
1046
  }
963
1047
 
964
1048
  let response: Response;
965
1049
  try {
966
- response = await fetch(`${CF_API_BASE_URL}/memberships`, {
1050
+ response = await fetch(`${getCloudflareApiBaseUrl()}/memberships`, {
967
1051
  method: "GET",
968
1052
  headers: {
969
1053
  Authorization: "Bearer " + apiToken,
@@ -971,19 +1055,19 @@ export async function getAccountId() {
971
1055
  });
972
1056
  } catch (err) {
973
1057
  // probably offline
1058
+ return;
974
1059
  }
975
- if (!response) return;
976
- let accountId: string;
977
- // @ts-expect-error need to type this response
978
- const responseJSON: {
1060
+
1061
+ let accountId: string | undefined;
1062
+ const responseJSON = (await response.json()) as {
979
1063
  success: boolean;
980
1064
  result: { id: string; account: { id: string; name: string } }[];
981
- } = await response.json();
1065
+ };
982
1066
 
983
1067
  if (responseJSON.success === true) {
984
1068
  if (responseJSON.result.length === 1) {
985
1069
  accountId = responseJSON.result[0].account.id;
986
- } else {
1070
+ } else if (isInteractive) {
987
1071
  accountId = await new Promise((resolve) => {
988
1072
  const accounts = responseJSON.result.map((x) => x.account);
989
1073
  const { unmount } = render(
@@ -996,6 +1080,15 @@ export async function getAccountId() {
996
1080
  />
997
1081
  );
998
1082
  });
1083
+ } else {
1084
+ throw new Error(
1085
+ "More than one account available but unable to select one in non-interactive mode.\n" +
1086
+ `Please set the appropriate \`account_id\` in your \`wrangler.toml\` file.\n` +
1087
+ `Available accounts are ("<name>" - "<id>"):\n` +
1088
+ responseJSON.result
1089
+ .map((x) => ` "${x.account.name}" - "${x.account.id}")`)
1090
+ .join("\n")
1091
+ );
999
1092
  }
1000
1093
  }
1001
1094
  return accountId;
@@ -1007,7 +1100,7 @@ type ChooseAccountItem = {
1007
1100
  };
1008
1101
  export function ChooseAccount(props: {
1009
1102
  accounts: ChooseAccountItem[];
1010
- onSelect: (item) => void;
1103
+ onSelect: (item: SelectInputItem<ChooseAccountItem>) => void;
1011
1104
  }) {
1012
1105
  return (
1013
1106
  <>