wrangler 0.0.0-fb43821 → 0.0.0-fb53fda

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 (104) hide show
  1. package/README.md +1 -1
  2. package/bin/wrangler.js +12 -3
  3. package/miniflare-dist/index.mjs +79 -0
  4. package/miniflare-dist/index.mjs.map +7 -0
  5. package/package.json +46 -17
  6. package/pages/functions/buildWorker.ts +62 -0
  7. package/pages/functions/filepath-routing.test.ts +223 -0
  8. package/pages/functions/filepath-routing.ts +181 -0
  9. package/pages/functions/identifiers.ts +78 -0
  10. package/pages/functions/routes.ts +151 -0
  11. package/pages/functions/template-worker.ts +130 -0
  12. package/src/__tests__/configuration.test.ts +2790 -0
  13. package/src/__tests__/dev.test.tsx +532 -0
  14. package/src/__tests__/guess-worker-format.test.ts +68 -0
  15. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  16. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  17. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  18. package/src/__tests__/helpers/mock-account-id.ts +48 -0
  19. package/src/__tests__/helpers/mock-bin.ts +36 -0
  20. package/src/__tests__/helpers/mock-cfetch.ts +207 -0
  21. package/src/__tests__/helpers/mock-console.ts +70 -0
  22. package/src/__tests__/helpers/mock-dialogs.ts +65 -0
  23. package/src/__tests__/helpers/mock-kv.ts +33 -0
  24. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  25. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  26. package/src/__tests__/helpers/run-in-tmp.ts +32 -0
  27. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  28. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  29. package/src/__tests__/https-options.test.ts +127 -0
  30. package/src/__tests__/index.test.ts +770 -209
  31. package/src/__tests__/jest.setup.ts +69 -0
  32. package/src/__tests__/kv.test.ts +1380 -0
  33. package/src/__tests__/logout.test.ts +52 -0
  34. package/src/__tests__/package-manager.test.ts +206 -0
  35. package/src/__tests__/pages.test.ts +82 -0
  36. package/src/__tests__/parse.test.ts +322 -0
  37. package/src/__tests__/publish.test.ts +3431 -0
  38. package/src/__tests__/r2.test.ts +198 -0
  39. package/src/__tests__/route.test.ts +45 -0
  40. package/src/__tests__/secret.test.ts +425 -0
  41. package/src/__tests__/sentry.test.ts +146 -0
  42. package/src/__tests__/tail.test.ts +562 -0
  43. package/src/__tests__/whoami.test.tsx +100 -0
  44. package/src/bundle.ts +142 -0
  45. package/src/cfetch/index.ts +104 -0
  46. package/src/cfetch/internal.ts +139 -0
  47. package/src/cli.ts +16 -7
  48. package/src/config/README.md +107 -0
  49. package/src/config/config.ts +218 -0
  50. package/src/config/diagnostics.ts +80 -0
  51. package/src/config/environment.ts +366 -0
  52. package/src/config/index.ts +62 -0
  53. package/src/config/validation-helpers.ts +554 -0
  54. package/src/config/validation.ts +1191 -0
  55. package/src/{api/preview.ts → create-worker-preview.ts} +63 -25
  56. package/src/create-worker-upload-form.ts +217 -0
  57. package/src/dev/dev.tsx +390 -0
  58. package/src/dev/local.tsx +232 -0
  59. package/src/dev/remote.tsx +203 -0
  60. package/src/dev/use-esbuild.ts +100 -0
  61. package/src/dialogs.tsx +4 -4
  62. package/src/entry.ts +237 -0
  63. package/src/environment-variables.ts +33 -0
  64. package/src/https-options.ts +122 -0
  65. package/src/index.tsx +1417 -915
  66. package/src/inspect.ts +617 -0
  67. package/src/intl-polyfill.d.ts +139 -0
  68. package/src/kv.ts +319 -0
  69. package/src/miniflare-cli/README.md +30 -0
  70. package/src/miniflare-cli/enum-keys.ts +17 -0
  71. package/src/miniflare-cli/index.ts +37 -0
  72. package/src/module-collection.ts +229 -35
  73. package/src/open-in-browser.ts +25 -0
  74. package/src/package-manager.ts +126 -0
  75. package/src/pages.tsx +470 -227
  76. package/src/parse.ts +183 -0
  77. package/src/paths.ts +26 -0
  78. package/src/proxy.ts +440 -0
  79. package/src/publish.ts +292 -290
  80. package/src/r2.ts +50 -0
  81. package/src/reporting.ts +131 -0
  82. package/src/selfsigned.d.ts +29 -0
  83. package/src/sites.tsx +224 -59
  84. package/src/tail/filters.ts +279 -0
  85. package/src/tail/index.ts +299 -0
  86. package/src/tail/printing.ts +62 -0
  87. package/src/user.tsx +301 -203
  88. package/src/whoami.tsx +69 -0
  89. package/src/worker.ts +163 -0
  90. package/templates/new-worker.js +15 -0
  91. package/templates/new-worker.ts +15 -0
  92. package/{static-asset-facade.js → templates/static-asset-facade.js} +0 -0
  93. package/templates/tsconfig.json +105 -0
  94. package/wrangler-dist/cli.js +134241 -116339
  95. package/wrangler-dist/cli.js.map +3 -3
  96. package/src/__tests__/mock-cfetch.js +0 -42
  97. package/src/api/form_data.ts +0 -141
  98. package/src/api/inspect.ts +0 -430
  99. package/src/api/worker.ts +0 -161
  100. package/src/cfetch.ts +0 -72
  101. package/src/config.ts +0 -122
  102. package/src/dev.tsx +0 -776
  103. package/src/kv.tsx +0 -211
  104. 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,43 @@
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";
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,39 +605,44 @@ 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
+ // we add offline_access manually for every request
614
+ `scope=${encodeURIComponent([...scopes, "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
  */
580
635
  async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
581
- const { refreshToken } = LocalState;
582
-
583
- if (!refreshToken) {
636
+ if (!LocalState.refreshToken) {
584
637
  console.warn("No refresh token is present.");
585
638
  }
586
639
 
587
- const url = TOKEN_URL;
588
640
  const body =
589
641
  `grant_type=refresh_token&` +
590
- `refresh_token=${refreshToken?.value}&` +
642
+ `refresh_token=${LocalState.refreshToken?.value}&` +
591
643
  `client_id=${CLIENT_ID}`;
592
644
 
593
- const response = await fetch(url, {
645
+ const response = await fetch(TOKEN_URL, {
594
646
  method: "POST",
595
647
  body,
596
648
  headers: {
@@ -601,13 +653,12 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
601
653
  throw await response.json();
602
654
  } else {
603
655
  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
- };
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;
611
662
  let scopes: Scope[] = [];
612
663
 
613
664
  const accessToken: AccessToken = {
@@ -617,10 +668,9 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
617
668
  LocalState.accessToken = accessToken;
618
669
 
619
670
  if (refresh_token) {
620
- const refreshToken: RefreshToken = {
671
+ LocalState.refreshToken = {
621
672
  value: refresh_token,
622
673
  };
623
- LocalState.refreshToken = refreshToken;
624
674
  }
625
675
 
626
676
  if (scope) {
@@ -636,20 +686,24 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
636
686
  refreshToken: LocalState.refreshToken,
637
687
  };
638
688
  return accessContext;
639
- } catch (err) {
640
- const error = err?.error || "There was a network error.";
689
+ } catch (error) {
641
690
  switch (error) {
642
691
  case "invalid_grant":
643
- console.log(
692
+ console.warn(
644
693
  "Expired! Auth code or refresh token needs to be renewed."
645
694
  );
646
695
  // alert("Redirecting to auth server to obtain a new auth grant code.");
647
696
  // TODO: return refreshAuthCodeOrRefreshToken();
648
697
  break;
649
698
  default:
699
+ console.error(error);
650
700
  break;
651
701
  }
652
- throw toErrorClass(error);
702
+ if (typeof error === "string") {
703
+ throw toErrorClass(error);
704
+ } else {
705
+ throw error;
706
+ }
653
707
  }
654
708
  }
655
709
  }
@@ -666,7 +720,6 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
666
720
  console.warn("No authorization grant code is being passed.");
667
721
  }
668
722
 
669
- const url = TOKEN_URL;
670
723
  const body =
671
724
  `grant_type=authorization_code&` +
672
725
  `code=${encodeURIComponent(authorizationCode || "")}&` +
@@ -674,7 +727,7 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
674
727
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
675
728
  `code_verifier=${codeVerifier}`;
676
729
 
677
- const response = await fetch(url, {
730
+ const response = await fetch(TOKEN_URL, {
678
731
  method: "POST",
679
732
  body,
680
733
  headers: {
@@ -691,13 +744,11 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
691
744
  }
692
745
  throw toErrorClass(error);
693
746
  }
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
- };
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;
701
752
  let scopes: Scope[] = [];
702
753
  LocalState.hasAuthCodeBeenExchangedForAccessToken = true;
703
754
 
@@ -709,10 +760,9 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
709
760
  LocalState.accessToken = accessToken;
710
761
 
711
762
  if (refresh_token) {
712
- const refreshToken: RefreshToken = {
763
+ LocalState.refreshToken = {
713
764
  value: refresh_token,
714
765
  };
715
- LocalState.refreshToken = refreshToken;
716
766
  }
717
767
 
718
768
  if (scope) {
@@ -782,52 +832,68 @@ function generateRandomState(lengthOfState: number): string {
782
832
  .join("");
783
833
  }
784
834
 
785
- async function writeToConfigFile(tokenData: AccessContext) {
786
- await mkdir(path.join(os.homedir(), ".wrangler/config/"), {
835
+ export function writeAuthConfigFile(config: UserAuthConfig) {
836
+ mkdirSync(path.join(os.homedir(), ".wrangler/config/"), {
787
837
  recursive: true,
788
838
  });
789
- await writeFile(
790
- path.join(os.homedir(), ".wrangler/config/default.toml"),
791
- `
792
- oauth_token = "${tokenData.token?.value || ""}"
793
- refresh_token = "${tokenData.refreshToken?.value}"
794
- expiration_time = "${tokenData.token?.expiry}"
795
- `,
839
+ writeFileSync(
840
+ path.join(os.homedir(), USER_AUTH_CONFIG_FILE),
841
+ TOML.stringify(config as TOML.JsonMap),
796
842
  { encoding: "utf-8" }
797
843
  );
798
844
  }
799
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
+
800
853
  type LoginProps = {
801
- scopes?: string[];
854
+ scopes?: Scope[];
802
855
  };
803
856
 
804
- export async function loginOrRefreshIfRequired(): Promise<boolean> {
857
+ export async function loginOrRefreshIfRequired(
858
+ isInteractive = true
859
+ ): Promise<boolean> {
805
860
  // TODO: if there already is a token, then try refreshing
806
861
  // TODO: ask permission before opening browser
807
- if (!LocalState.accessToken) {
808
- // not logged in.
809
- 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());
810
866
  } else if (isAccessTokenExpired()) {
811
- return await refreshToken();
867
+ // We're logged in, but the refresh token seems to have expired,
868
+ // so let's try to refresh it
869
+ const didRefresh = await refreshToken();
870
+ if (didRefresh) {
871
+ // The token was refreshed, so we're done here
872
+ return true;
873
+ } else {
874
+ // If the refresh token isn't valid, then we ask the user to login again
875
+ return isInteractive && (await login());
876
+ }
812
877
  } else {
813
878
  return true;
814
879
  }
815
880
  }
816
881
 
817
882
  export async function login(props?: LoginProps): Promise<boolean> {
883
+ console.log("Attempting to login via OAuth...");
818
884
  const urlToOpen = await getAuthURL(props?.scopes);
819
- await 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;
885
+ await openInBrowser(urlToOpen);
886
+ let server: http.Server;
887
+ let loginTimeoutHandle: NodeJS.Timeout;
824
888
  const timerPromise = new Promise<boolean>((resolve) => {
825
889
  loginTimeoutHandle = setTimeout(() => {
826
- console.error("Timed out waiting for authorization code.");
890
+ console.error(
891
+ "Timed out waiting for authorization code, please try again."
892
+ );
827
893
  server.close();
828
894
  clearTimeout(loginTimeoutHandle);
829
895
  resolve(false);
830
- }, 60000); // wait for 30 seconds for the user to authorize
896
+ }, 60000); // wait for 60 seconds for the user to authorize
831
897
  });
832
898
 
833
899
  const loginPromise = new Promise<boolean>((resolve, reject) => {
@@ -858,8 +924,9 @@ export async function login(props?: LoginProps): Promise<boolean> {
858
924
  finish(false);
859
925
  });
860
926
  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
927
+ "Error: Consent denied. You must grant consent to Wrangler in order to login.\n" +
928
+ "If you don't want to do this consider passing an API token via the `CLOUDFLARE_API_TOKEN` environment variable"
929
+ );
863
930
 
864
931
  return;
865
932
  } else {
@@ -872,8 +939,12 @@ export async function login(props?: LoginProps): Promise<boolean> {
872
939
  finish(false, new ErrorNoAuthCode());
873
940
  return;
874
941
  } else {
875
- const tokenData = await exchangeAuthCodeForAccessToken();
876
- await writeToConfigFile(tokenData);
942
+ const exchange = await exchangeAuthCodeForAccessToken();
943
+ writeAuthConfigFile({
944
+ oauth_token: exchange.token?.value ?? "",
945
+ expiration_time: exchange.token?.expiry,
946
+ refresh_token: exchange.refreshToken?.value,
947
+ });
877
948
  res.writeHead(307, {
878
949
  Location:
879
950
  "https://welcome.developers.workers.dev/wrangler-oauth-consent-granted",
@@ -881,9 +952,7 @@ export async function login(props?: LoginProps): Promise<boolean> {
881
952
  res.end(() => {
882
953
  finish(true);
883
954
  });
884
- console.log(
885
- `Successfully configured. You can find your configuration file at: ${os.homedir()}/.wrangler/config/default.toml`
886
- );
955
+ console.log(`Successfully logged in.`);
887
956
 
888
957
  return;
889
958
  }
@@ -900,18 +969,22 @@ export async function login(props?: LoginProps): Promise<boolean> {
900
969
  /**
901
970
  * Checks to see if the access token has expired.
902
971
  */
903
- export function isAccessTokenExpired(): boolean {
904
- throwIfNotInitialised();
972
+ function isAccessTokenExpired(): boolean {
905
973
  const { accessToken } = LocalState;
906
974
  return Boolean(accessToken && new Date() >= new Date(accessToken.expiry));
907
975
  }
908
976
 
909
- export async function refreshToken(): Promise<boolean> {
910
- throwIfNotInitialised();
977
+ async function refreshToken(): Promise<boolean> {
911
978
  // refresh
912
979
  try {
913
- const refreshed = await exchangeRefreshTokenForAccessToken();
914
- await writeToConfigFile(refreshed);
980
+ const {
981
+ token: { value: oauth_token, expiry: expiration_time } = {
982
+ value: "",
983
+ expiry: "",
984
+ },
985
+ refreshToken: { value: refresh_token } = {},
986
+ } = await exchangeRefreshTokenForAccessToken();
987
+ writeAuthConfigFile({ oauth_token, expiration_time, refresh_token });
915
988
  return true;
916
989
  } catch (err) {
917
990
  console.error(err);
@@ -920,16 +993,33 @@ export async function refreshToken(): Promise<boolean> {
920
993
  }
921
994
 
922
995
  export async function logout(): Promise<void> {
923
- throwIfNotInitialised();
924
- const { refreshToken } = LocalState;
925
- if (!refreshToken) {
926
- console.log("Not logged in, exiting...");
927
- return;
996
+ if (!LocalState.accessToken) {
997
+ if (!LocalState.refreshToken) {
998
+ console.log("Not logged in, exiting...");
999
+ return;
1000
+ }
1001
+
1002
+ const body =
1003
+ `client_id=${encodeURIComponent(CLIENT_ID)}&` +
1004
+ `token_type_hint=refresh_token&` +
1005
+ `token=${encodeURIComponent(LocalState.refreshToken?.value || "")}`;
1006
+
1007
+ const response = await fetch(REVOKE_URL, {
1008
+ method: "POST",
1009
+ body,
1010
+ headers: {
1011
+ "Content-Type": "application/x-www-form-urlencoded",
1012
+ },
1013
+ });
1014
+ await response.text(); // blank text? would be nice if it was something meaningful
1015
+ console.log(
1016
+ "💁 Wrangler is configured with an OAuth token. The token has been successfully revoked"
1017
+ );
928
1018
  }
929
1019
  const body =
930
1020
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
931
1021
  `token_type_hint=refresh_token&` +
932
- `token=${encodeURIComponent(refreshToken?.value || "")}`;
1022
+ `token=${encodeURIComponent(LocalState.refreshToken?.value || "")}`;
933
1023
 
934
1024
  const response = await fetch(REVOKE_URL, {
935
1025
  method: "POST",
@@ -939,38 +1029,34 @@ export async function logout(): Promise<void> {
939
1029
  },
940
1030
  });
941
1031
  await response.text(); // blank text? would be nice if it was something meaningful
942
- console.log(
943
- "💁 Wrangler is configured with an OAuth token. The token has been successfully revoked"
944
- );
945
- // delete the file
946
- await rm(path.join(os.homedir(), ".wrangler/config/default.toml"));
947
- console.log(
948
- `Removing ${os.homedir()}/.wrangler/config/default.toml.. success!`
949
- );
1032
+ rmSync(path.join(os.homedir(), USER_AUTH_CONFIG_FILE));
1033
+ console.log(`Successfully logged out.`);
950
1034
  }
951
1035
 
952
- export function listScopes(): void {
953
- throwIfNotInitialised();
954
- console.log("💁 Available scopes:");
955
- const data = Scopes.map((scope, index) => ({
1036
+ export function listScopes(message = "💁 Available scopes:"): void {
1037
+ console.log(message);
1038
+ const data = ScopeKeys.map((scope: Scope) => ({
956
1039
  Scope: scope,
957
- Description: ScopeDescriptions[index],
1040
+ Description: Scopes[scope],
958
1041
  }));
959
1042
  render(<Table data={data} />);
960
1043
  // TODO: maybe a good idea to show usage here
961
1044
  }
962
1045
 
963
- export async function getAccountId() {
1046
+ export async function getAccountId(
1047
+ isInteractive = true
1048
+ ): Promise<string | undefined> {
964
1049
  const apiToken = getAPIToken();
965
1050
  if (!apiToken) return;
966
1051
 
967
- if (process.env.CF_ACCOUNT_ID) {
968
- return process.env.CF_ACCOUNT_ID;
1052
+ const accountIdFromEnv = getCloudflareAccountIdFromEnv();
1053
+ if (accountIdFromEnv) {
1054
+ return accountIdFromEnv;
969
1055
  }
970
1056
 
971
1057
  let response: Response;
972
1058
  try {
973
- response = await fetch(`${CF_API_BASE_URL}/memberships`, {
1059
+ response = await fetch(`${getCloudflareApiBaseUrl()}/memberships`, {
974
1060
  method: "GET",
975
1061
  headers: {
976
1062
  Authorization: "Bearer " + apiToken,
@@ -978,19 +1064,19 @@ export async function getAccountId() {
978
1064
  });
979
1065
  } catch (err) {
980
1066
  // probably offline
1067
+ return;
981
1068
  }
982
- if (!response) return;
983
- let accountId: string;
984
- // @ts-expect-error need to type this response
985
- const responseJSON: {
1069
+
1070
+ let accountId: string | undefined;
1071
+ const responseJSON = (await response.json()) as {
986
1072
  success: boolean;
987
1073
  result: { id: string; account: { id: string; name: string } }[];
988
- } = await response.json();
1074
+ };
989
1075
 
990
1076
  if (responseJSON.success === true) {
991
1077
  if (responseJSON.result.length === 1) {
992
1078
  accountId = responseJSON.result[0].account.id;
993
- } else {
1079
+ } else if (isInteractive) {
994
1080
  accountId = await new Promise((resolve) => {
995
1081
  const accounts = responseJSON.result.map((x) => x.account);
996
1082
  const { unmount } = render(
@@ -1003,6 +1089,15 @@ export async function getAccountId() {
1003
1089
  />
1004
1090
  );
1005
1091
  });
1092
+ } else {
1093
+ throw new Error(
1094
+ "More than one account available but unable to select one in non-interactive mode.\n" +
1095
+ `Please set the appropriate \`account_id\` in your \`wrangler.toml\` file.\n` +
1096
+ `Available accounts are ("<name>" - "<id>"):\n` +
1097
+ responseJSON.result
1098
+ .map((x) => ` "${x.account.name}" - "${x.account.id}")`)
1099
+ .join("\n")
1100
+ );
1006
1101
  }
1007
1102
  }
1008
1103
  return accountId;
@@ -1014,16 +1109,19 @@ type ChooseAccountItem = {
1014
1109
  };
1015
1110
  export function ChooseAccount(props: {
1016
1111
  accounts: ChooseAccountItem[];
1017
- onSelect: (item) => void;
1112
+ onSelect: (item: SelectInputItem<ChooseAccountItem>) => void;
1018
1113
  }) {
1019
1114
  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
- />
1115
+ <>
1116
+ <Text bold>Select an account from below:</Text>
1117
+ <SelectInput
1118
+ items={props.accounts.map((item) => ({
1119
+ key: item.id,
1120
+ label: item.name,
1121
+ value: item,
1122
+ }))}
1123
+ onSelect={props.onSelect}
1124
+ />
1125
+ </>
1028
1126
  );
1029
1127
  }