wrangler 0.0.0-ece06ea → 0.0.0-ed4f2ef

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 (109) hide show
  1. package/README.md +3 -3
  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 +49 -22
  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 +2874 -0
  13. package/src/__tests__/dev.test.tsx +588 -0
  14. package/src/__tests__/guess-worker-format.test.ts +83 -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-oauth-flow.ts +178 -0
  25. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  26. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  27. package/src/__tests__/helpers/run-in-tmp.ts +32 -0
  28. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  29. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  30. package/src/__tests__/https-options.test.ts +127 -0
  31. package/src/__tests__/index.test.ts +953 -119
  32. package/src/__tests__/jest.setup.ts +74 -0
  33. package/src/__tests__/kv.test.ts +1380 -0
  34. package/src/__tests__/package-manager.test.ts +206 -0
  35. package/src/__tests__/pages.test.ts +180 -0
  36. package/src/__tests__/parse.test.ts +322 -0
  37. package/src/__tests__/publish.test.ts +3946 -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 +638 -0
  43. package/src/__tests__/user.test.ts +83 -0
  44. package/src/__tests__/whoami.test.tsx +95 -0
  45. package/src/bundle.ts +155 -0
  46. package/src/cfetch/index.ts +104 -0
  47. package/src/cfetch/internal.ts +139 -0
  48. package/src/cli.ts +18 -7
  49. package/src/config/README.md +107 -0
  50. package/src/config/config.ts +229 -0
  51. package/src/config/diagnostics.ts +80 -0
  52. package/src/config/environment.ts +371 -0
  53. package/src/config/index.ts +62 -0
  54. package/src/config/validation-helpers.ts +557 -0
  55. package/src/config/validation.ts +1240 -0
  56. package/src/create-worker-preview.ts +173 -0
  57. package/src/create-worker-upload-form.ts +240 -0
  58. package/src/dev/dev.tsx +413 -0
  59. package/src/dev/local.tsx +287 -0
  60. package/src/dev/remote.tsx +209 -0
  61. package/src/dev/use-esbuild.ts +109 -0
  62. package/src/dialogs.tsx +6 -14
  63. package/src/entry.ts +256 -0
  64. package/src/environment-variables.ts +33 -0
  65. package/src/errors.ts +11 -0
  66. package/src/https-options.ts +122 -0
  67. package/src/index.tsx +1503 -959
  68. package/src/inspect.ts +624 -0
  69. package/src/intl-polyfill.d.ts +139 -0
  70. package/src/kv.ts +319 -0
  71. package/src/miniflare-cli/README.md +30 -0
  72. package/src/miniflare-cli/enum-keys.ts +17 -0
  73. package/src/miniflare-cli/index.ts +38 -0
  74. package/src/module-collection.ts +257 -0
  75. package/src/open-in-browser.ts +25 -0
  76. package/src/package-manager.ts +126 -0
  77. package/src/pages.tsx +1074 -212
  78. package/src/parse.ts +183 -0
  79. package/src/paths.ts +26 -0
  80. package/src/proxy.ts +455 -0
  81. package/src/publish.ts +326 -295
  82. package/src/r2.ts +50 -0
  83. package/src/reporting.ts +131 -0
  84. package/src/selfsigned.d.ts +29 -0
  85. package/src/sites.tsx +224 -59
  86. package/src/tail/filters.ts +279 -0
  87. package/src/tail/index.ts +299 -0
  88. package/src/tail/printing.ts +62 -0
  89. package/src/user.tsx +340 -203
  90. package/src/whoami.tsx +69 -0
  91. package/src/worker.ts +172 -0
  92. package/templates/new-worker.js +15 -0
  93. package/templates/new-worker.ts +15 -0
  94. package/{static-asset-facade.js → templates/static-asset-facade.js} +1 -1
  95. package/templates/tsconfig.json +105 -0
  96. package/wrangler-dist/cli.js +136988 -131174
  97. package/wrangler-dist/cli.js.map +3 -3
  98. package/src/__tests__/fixtures/init/.gitkeep +0 -0
  99. package/src/__tests__/mock-cfetch.js +0 -46
  100. package/src/api/form_data.ts +0 -158
  101. package/src/api/inspect.ts +0 -441
  102. package/src/api/preview.ts +0 -123
  103. package/src/api/worker.ts +0 -161
  104. package/src/cfetch.ts +0 -72
  105. package/src/config.ts +0 -124
  106. package/src/dev.tsx +0 -736
  107. package/src/kv.tsx +0 -211
  108. package/src/tail.tsx +0 -71
  109. package/src/util/fetch.ts +0 -74
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,44 @@
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 { Config } from "./config";
227
+ import type { Item as SelectInputItem } from "ink-select-input/build/SelectInput";
224
228
  import type { ParsedUrlQuery } from "node:querystring";
225
- import { CF_API_BASE_URL } from "./cfetch";
226
- import type { Response } from "node-fetch";
229
+ import type { Response } from "undici";
230
+
231
+ /**
232
+ * Try to read the API token from the environment.
233
+ */
234
+ const getCloudflareAPITokenFromEnv = getEnvironmentVariableFactory({
235
+ variableName: "CLOUDFLARE_API_TOKEN",
236
+ deprecatedName: "CF_API_TOKEN",
237
+ });
238
+
239
+ /**
240
+ * Try to read the account ID from the environment.
241
+ */
242
+ const getCloudflareAccountIdFromEnv = getEnvironmentVariableFactory({
243
+ variableName: "CLOUDFLARE_ACCOUNT_ID",
244
+ deprecatedName: "CF_ACCOUNT_ID",
245
+ });
227
246
 
228
247
  /**
229
248
  * An implementation of rfc6749#section-4.1 and rfc7636.
@@ -234,17 +253,45 @@ interface PKCECodes {
234
253
  codeVerifier: string;
235
254
  }
236
255
 
237
- interface State {
238
- accessToken?: AccessToken; // persist
256
+ /**
257
+ * The module level state of the authentication flow.
258
+ */
259
+ interface State extends AuthTokens {
239
260
  authorizationCode?: string;
240
261
  codeChallenge?: string;
241
262
  codeVerifier?: string;
242
263
  hasAuthCodeBeenExchangedForAccessToken?: boolean;
243
- refreshToken?: RefreshToken; // persist
244
264
  stateQueryParam?: string;
245
265
  scopes?: Scope[];
246
266
  }
247
267
 
268
+ /**
269
+ * The tokens related to authentication.
270
+ */
271
+ interface AuthTokens {
272
+ accessToken?: AccessToken;
273
+ refreshToken?: RefreshToken;
274
+ /** @deprecated - this field was only provided by the deprecated `wrangler1 config` command. */
275
+ apiToken?: string;
276
+ }
277
+
278
+ /**
279
+ * The path to the config file that holds user authentication data,
280
+ * relative to the user's home directory.
281
+ */
282
+ export const USER_AUTH_CONFIG_FILE = ".wrangler/config/default.toml";
283
+
284
+ /**
285
+ * The data that may be read from the `USER_CONFIG_FILE`.
286
+ */
287
+ export interface UserAuthConfig {
288
+ oauth_token?: string;
289
+ refresh_token?: string;
290
+ expiration_time?: string;
291
+ /** @deprecated - this field was only provided by the deprecated `wrangler1 config` command. */
292
+ api_token?: string;
293
+ }
294
+
248
295
  interface RefreshToken {
249
296
  value: string;
250
297
  }
@@ -254,38 +301,39 @@ interface AccessToken {
254
301
  expiry: string;
255
302
  }
256
303
 
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
- ];
304
+ const Scopes = {
305
+ "account:read":
306
+ "See your account info such as account details, analytics, and memberships.",
307
+ "user:read":
308
+ "See your user info such as name, email address, and account memberships.",
309
+ "workers:write":
310
+ "See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.",
311
+ "workers_kv:write":
312
+ "See and change Cloudflare Workers KV Storage data such as keys and namespaces.",
313
+ "workers_routes:write":
314
+ "See and change Cloudflare Workers data such as filters and routes.",
315
+ "workers_scripts:write":
316
+ "See and change Cloudflare Workers scripts, durable objects, subdomains, triggers, and tail data.",
317
+ "workers_tail:read": "See Cloudflare Workers tail and script data.",
318
+ "pages:write":
319
+ "See and change Cloudflare Pages projects, settings and deployments.",
320
+ "zone:read": "Grants read level access to account zone.",
321
+ } as const;
322
+
323
+ /**
324
+ * The possible keys for a Scope.
325
+ *
326
+ * "offline_access" is automatically included.
327
+ */
328
+ type Scope = keyof typeof Scopes;
329
+
330
+ const ScopeKeys = Object.keys(Scopes) as Scope[];
331
+
332
+ export function validateScopeKeys(
333
+ scopes: string[]
334
+ ): scopes is typeof ScopeKeys {
335
+ return scopes.every((scope) => scope in Scopes);
336
+ }
289
337
 
290
338
  const CLIENT_ID = "54d11594-84e4-41aa-b438-e81b8fa78ee7";
291
339
  const AUTH_URL = "https://dash.cloudflare.com/oauth2/auth";
@@ -293,63 +341,74 @@ const TOKEN_URL = "https://dash.cloudflare.com/oauth2/token";
293
341
  const CALLBACK_URL = "http://localhost:8976/oauth/callback";
294
342
  const REVOKE_URL = "https://dash.cloudflare.com/oauth2/revoke";
295
343
 
296
- const LocalState: State = {};
297
- let initialised = false;
344
+ let LocalState: State = getAuthTokens();
298
345
 
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
346
+ /**
347
+ * Compute the current auth tokens.
348
+ */
349
+ function getAuthTokens(config?: UserAuthConfig): AuthTokens {
350
+ // get refreshToken/accessToken from fs if exists
304
351
  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",
352
+ // if the environment variable is available, use that
353
+ const apiTokenFromEnv = getCloudflareAPITokenFromEnv();
354
+ if (apiTokenFromEnv) {
355
+ return {
356
+ accessToken: {
357
+ value: apiTokenFromEnv,
358
+ expiry: "3021-12-31T23:59:59+00:00",
359
+ },
310
360
  };
311
- initialised = true;
312
- return;
313
361
  }
314
362
 
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
- };
363
+ // otherwise try loading from the user auth config file.
364
+ const { oauth_token, refresh_token, expiration_time, api_token } =
365
+ config || readAuthConfigFile();
366
+
325
367
  if (oauth_token) {
326
- LocalState.accessToken = { value: oauth_token, expiry: expiration_time };
327
- }
328
- if (refresh_token) {
329
- LocalState.refreshToken = { value: refresh_token };
368
+ return {
369
+ accessToken: {
370
+ value: oauth_token,
371
+ // If there is no `expiration_time` field then set it to an old date, to cause it to expire immediately.
372
+ expiry: expiration_time ?? "2000-01-01:00:00:00+00:00",
373
+ },
374
+ refreshToken: { value: refresh_token ?? "" },
375
+ };
376
+ } else if (api_token) {
377
+ return { apiToken: api_token };
378
+ } else {
379
+ return {};
330
380
  }
331
- } catch (err) {
332
- // no config yet, let's chill
333
- // console.error(err);
381
+ } catch {
382
+ return {};
334
383
  }
335
- initialised = true;
336
384
  }
337
385
 
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
- }
386
+ /**
387
+ * Run the initialisation of the auth state, in the case that something changed.
388
+ *
389
+ * This runs automatically whenever `writeAuthConfigFile` is run, so generally
390
+ * you won't need to call it yourself.
391
+ */
392
+ export function reinitialiseAuthTokens(): void;
393
+
394
+ /**
395
+ * Reinitialise auth state from an in-memory config, skipping
396
+ * over the part where we write a file and then read it back into memory
397
+ */
398
+ export function reinitialiseAuthTokens(config: UserAuthConfig): void;
399
+
400
+ export function reinitialiseAuthTokens(config?: UserAuthConfig): void {
401
+ LocalState = getAuthTokens(config);
345
402
  }
346
403
 
347
- export function getAPIToken(): string {
348
- if (process.env.CF_API_TOKEN) {
349
- return process.env.CF_API_TOKEN;
404
+ export function getAPIToken(): string | undefined {
405
+ if (LocalState.apiToken) {
406
+ console.warn(
407
+ "It looks like you have used Wrangler 1's `config` command to login with an API token.\n" +
408
+ "This is no longer supported in the current version of Wrangler.\n" +
409
+ "If you wish to authenticate via an API token then please set the `CLOUDFLARE_API_TOKEN` environment variable."
410
+ );
350
411
  }
351
-
352
- throwIfNotInitialised();
353
412
  return LocalState.accessToken?.value;
354
413
  }
355
414
 
@@ -548,7 +607,7 @@ function isReturningFromAuthServer(query: ParsedUrlQuery): boolean {
548
607
  return true;
549
608
  }
550
609
 
551
- export async function getAuthURL(scopes?: string[]): Promise<string> {
610
+ export async function getAuthURL(scopes = ScopeKeys): Promise<string> {
552
611
  const { codeChallenge, codeVerifier } = await generatePKCECodes();
553
612
  const stateQueryParam = generateRandomState(RECOMMENDED_STATE_LENGTH);
554
613
 
@@ -558,39 +617,44 @@ export async function getAuthURL(scopes?: string[]): Promise<string> {
558
617
  stateQueryParam,
559
618
  });
560
619
 
561
- // TODO: verify that the scopes passed are legit
562
-
563
620
  return (
564
621
  AUTH_URL +
565
622
  `?response_type=code&` +
566
623
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
567
624
  `redirect_uri=${encodeURIComponent(CALLBACK_URL)}&` +
568
- `scope=${encodeURIComponent(
569
- (scopes || Scopes).concat("offline_access").join(" ")
570
- )}&` +
625
+ // we add offline_access manually for every request
626
+ `scope=${encodeURIComponent([...scopes, "offline_access"].join(" "))}&` +
571
627
  `state=${stateQueryParam}&` +
572
628
  `code_challenge=${encodeURIComponent(codeChallenge)}&` +
573
629
  `code_challenge_method=S256`
574
630
  );
575
631
  }
576
632
 
633
+ type TokenResponse =
634
+ | {
635
+ access_token: string;
636
+ expires_in: number;
637
+ refresh_token: string;
638
+ scope: string;
639
+ }
640
+ | {
641
+ error: string;
642
+ };
643
+
577
644
  /**
578
645
  * Refresh an access token from the remote service.
579
646
  */
580
647
  async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
581
- const { refreshToken } = LocalState;
582
-
583
- if (!refreshToken) {
648
+ if (!LocalState.refreshToken) {
584
649
  console.warn("No refresh token is present.");
585
650
  }
586
651
 
587
- const url = TOKEN_URL;
588
652
  const body =
589
653
  `grant_type=refresh_token&` +
590
- `refresh_token=${refreshToken?.value}&` +
654
+ `refresh_token=${LocalState.refreshToken?.value}&` +
591
655
  `client_id=${CLIENT_ID}`;
592
656
 
593
- const response = await fetch(url, {
657
+ const response = await fetch(TOKEN_URL, {
594
658
  method: "POST",
595
659
  body,
596
660
  headers: {
@@ -601,13 +665,12 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
601
665
  throw await response.json();
602
666
  } else {
603
667
  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
- };
668
+ const json = (await response.json()) as TokenResponse;
669
+ if ("error" in json) {
670
+ throw json.error;
671
+ }
672
+
673
+ const { access_token, expires_in, refresh_token, scope } = json;
611
674
  let scopes: Scope[] = [];
612
675
 
613
676
  const accessToken: AccessToken = {
@@ -617,10 +680,9 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
617
680
  LocalState.accessToken = accessToken;
618
681
 
619
682
  if (refresh_token) {
620
- const refreshToken: RefreshToken = {
683
+ LocalState.refreshToken = {
621
684
  value: refresh_token,
622
685
  };
623
- LocalState.refreshToken = refreshToken;
624
686
  }
625
687
 
626
688
  if (scope) {
@@ -636,20 +698,24 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
636
698
  refreshToken: LocalState.refreshToken,
637
699
  };
638
700
  return accessContext;
639
- } catch (err) {
640
- const error = err?.error || "There was a network error.";
701
+ } catch (error) {
641
702
  switch (error) {
642
703
  case "invalid_grant":
643
- console.log(
704
+ console.warn(
644
705
  "Expired! Auth code or refresh token needs to be renewed."
645
706
  );
646
707
  // alert("Redirecting to auth server to obtain a new auth grant code.");
647
708
  // TODO: return refreshAuthCodeOrRefreshToken();
648
709
  break;
649
710
  default:
711
+ console.error(error);
650
712
  break;
651
713
  }
652
- throw toErrorClass(error);
714
+ if (typeof error === "string") {
715
+ throw toErrorClass(error);
716
+ } else {
717
+ throw error;
718
+ }
653
719
  }
654
720
  }
655
721
  }
@@ -666,7 +732,6 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
666
732
  console.warn("No authorization grant code is being passed.");
667
733
  }
668
734
 
669
- const url = TOKEN_URL;
670
735
  const body =
671
736
  `grant_type=authorization_code&` +
672
737
  `code=${encodeURIComponent(authorizationCode || "")}&` +
@@ -674,7 +739,7 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
674
739
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
675
740
  `code_verifier=${codeVerifier}`;
676
741
 
677
- const response = await fetch(url, {
742
+ const response = await fetch(TOKEN_URL, {
678
743
  method: "POST",
679
744
  body,
680
745
  headers: {
@@ -691,13 +756,11 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
691
756
  }
692
757
  throw toErrorClass(error);
693
758
  }
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
- };
759
+ const json = (await response.json()) as TokenResponse;
760
+ if ("error" in json) {
761
+ throw new Error(json.error);
762
+ }
763
+ const { access_token, expires_in, refresh_token, scope } = json;
701
764
  let scopes: Scope[] = [];
702
765
  LocalState.hasAuthCodeBeenExchangedForAccessToken = true;
703
766
 
@@ -709,10 +772,9 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
709
772
  LocalState.accessToken = accessToken;
710
773
 
711
774
  if (refresh_token) {
712
- const refreshToken: RefreshToken = {
775
+ LocalState.refreshToken = {
713
776
  value: refresh_token,
714
777
  };
715
- LocalState.refreshToken = refreshToken;
716
778
  }
717
779
 
718
780
  if (scope) {
@@ -782,52 +844,73 @@ function generateRandomState(lengthOfState: number): string {
782
844
  .join("");
783
845
  }
784
846
 
785
- async function writeToConfigFile(tokenData: AccessContext) {
786
- await mkdir(path.join(os.homedir(), ".wrangler/config/"), {
847
+ /**
848
+ * Writes a a wrangler config file (auth credentials) to disk,
849
+ * and updates the user auth state with the new credentials.
850
+ */
851
+ export function writeAuthConfigFile(config: UserAuthConfig) {
852
+ mkdirSync(path.join(os.homedir(), ".wrangler/config/"), {
787
853
  recursive: true,
788
854
  });
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
- `,
855
+ writeFileSync(
856
+ path.join(os.homedir(), USER_AUTH_CONFIG_FILE),
857
+ TOML.stringify(config as TOML.JsonMap),
796
858
  { encoding: "utf-8" }
797
859
  );
860
+
861
+ reinitialiseAuthTokens();
862
+ }
863
+
864
+ export function readAuthConfigFile(): UserAuthConfig {
865
+ const toml = TOML.parse(
866
+ readFileSync(path.join(os.homedir(), USER_AUTH_CONFIG_FILE))
867
+ );
868
+ return toml;
798
869
  }
799
870
 
800
871
  type LoginProps = {
801
- scopes?: string[];
872
+ scopes?: Scope[];
802
873
  };
803
874
 
804
- export async function loginOrRefreshIfRequired(): Promise<boolean> {
875
+ export async function loginOrRefreshIfRequired(
876
+ isInteractive = true
877
+ ): Promise<boolean> {
805
878
  // TODO: if there already is a token, then try refreshing
806
879
  // TODO: ask permission before opening browser
807
- if (!LocalState.accessToken) {
808
- // not logged in.
809
- return await login();
880
+ if (!getAPIToken()) {
881
+ // Not logged in.
882
+ // If we are not interactive, we cannot ask the user to login
883
+ return isInteractive && (await login());
810
884
  } else if (isAccessTokenExpired()) {
811
- return await refreshToken();
885
+ // We're logged in, but the refresh token seems to have expired,
886
+ // so let's try to refresh it
887
+ const didRefresh = await refreshToken();
888
+ if (didRefresh) {
889
+ // The token was refreshed, so we're done here
890
+ return true;
891
+ } else {
892
+ // If the refresh token isn't valid, then we ask the user to login again
893
+ return isInteractive && (await login());
894
+ }
812
895
  } else {
813
896
  return true;
814
897
  }
815
898
  }
816
899
 
817
900
  export async function login(props?: LoginProps): Promise<boolean> {
901
+ console.log("Attempting to login via OAuth...");
818
902
  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;
903
+ let server: http.Server;
904
+ let loginTimeoutHandle: NodeJS.Timeout;
824
905
  const timerPromise = new Promise<boolean>((resolve) => {
825
906
  loginTimeoutHandle = setTimeout(() => {
826
- console.error("Timed out waiting for authorization code.");
907
+ console.error(
908
+ "Timed out waiting for authorization code, please try again."
909
+ );
827
910
  server.close();
828
911
  clearTimeout(loginTimeoutHandle);
829
912
  resolve(false);
830
- }, 60000); // wait for 30 seconds for the user to authorize
913
+ }, 60000); // wait for 60 seconds for the user to authorize
831
914
  });
832
915
 
833
916
  const loginPromise = new Promise<boolean>((resolve, reject) => {
@@ -858,8 +941,9 @@ export async function login(props?: LoginProps): Promise<boolean> {
858
941
  finish(false);
859
942
  });
860
943
  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
944
+ "Error: Consent denied. You must grant consent to Wrangler in order to login.\n" +
945
+ "If you don't want to do this consider passing an API token via the `CLOUDFLARE_API_TOKEN` environment variable"
946
+ );
863
947
 
864
948
  return;
865
949
  } else {
@@ -872,8 +956,12 @@ export async function login(props?: LoginProps): Promise<boolean> {
872
956
  finish(false, new ErrorNoAuthCode());
873
957
  return;
874
958
  } else {
875
- const tokenData = await exchangeAuthCodeForAccessToken();
876
- await writeToConfigFile(tokenData);
959
+ const exchange = await exchangeAuthCodeForAccessToken();
960
+ writeAuthConfigFile({
961
+ oauth_token: exchange.token?.value ?? "",
962
+ expiration_time: exchange.token?.expiry,
963
+ refresh_token: exchange.refreshToken?.value,
964
+ });
877
965
  res.writeHead(307, {
878
966
  Location:
879
967
  "https://welcome.developers.workers.dev/wrangler-oauth-consent-granted",
@@ -881,9 +969,7 @@ export async function login(props?: LoginProps): Promise<boolean> {
881
969
  res.end(() => {
882
970
  finish(true);
883
971
  });
884
- console.log(
885
- `Successfully configured. You can find your configuration file at: ${os.homedir()}/.wrangler/config/default.toml`
886
- );
972
+ console.log(`Successfully logged in.`);
887
973
 
888
974
  return;
889
975
  }
@@ -894,24 +980,30 @@ export async function login(props?: LoginProps): Promise<boolean> {
894
980
  server.listen(8976);
895
981
  });
896
982
 
983
+ await openInBrowser(urlToOpen);
984
+
897
985
  return Promise.race([timerPromise, loginPromise]);
898
986
  }
899
987
 
900
988
  /**
901
989
  * Checks to see if the access token has expired.
902
990
  */
903
- export function isAccessTokenExpired(): boolean {
904
- throwIfNotInitialised();
991
+ function isAccessTokenExpired(): boolean {
905
992
  const { accessToken } = LocalState;
906
993
  return Boolean(accessToken && new Date() >= new Date(accessToken.expiry));
907
994
  }
908
995
 
909
- export async function refreshToken(): Promise<boolean> {
910
- throwIfNotInitialised();
996
+ async function refreshToken(): Promise<boolean> {
911
997
  // refresh
912
998
  try {
913
- const refreshed = await exchangeRefreshTokenForAccessToken();
914
- await writeToConfigFile(refreshed);
999
+ const {
1000
+ token: { value: oauth_token, expiry: expiration_time } = {
1001
+ value: "",
1002
+ expiry: "",
1003
+ },
1004
+ refreshToken: { value: refresh_token } = {},
1005
+ } = await exchangeRefreshTokenForAccessToken();
1006
+ writeAuthConfigFile({ oauth_token, expiration_time, refresh_token });
915
1007
  return true;
916
1008
  } catch (err) {
917
1009
  console.error(err);
@@ -920,16 +1012,33 @@ export async function refreshToken(): Promise<boolean> {
920
1012
  }
921
1013
 
922
1014
  export async function logout(): Promise<void> {
923
- throwIfNotInitialised();
924
- const { refreshToken } = LocalState;
925
- if (!refreshToken) {
926
- console.log("Not logged in, exiting...");
927
- return;
1015
+ if (!LocalState.accessToken) {
1016
+ if (!LocalState.refreshToken) {
1017
+ console.log("Not logged in, exiting...");
1018
+ return;
1019
+ }
1020
+
1021
+ const body =
1022
+ `client_id=${encodeURIComponent(CLIENT_ID)}&` +
1023
+ `token_type_hint=refresh_token&` +
1024
+ `token=${encodeURIComponent(LocalState.refreshToken?.value || "")}`;
1025
+
1026
+ const response = await fetch(REVOKE_URL, {
1027
+ method: "POST",
1028
+ body,
1029
+ headers: {
1030
+ "Content-Type": "application/x-www-form-urlencoded",
1031
+ },
1032
+ });
1033
+ await response.text(); // blank text? would be nice if it was something meaningful
1034
+ console.log(
1035
+ "💁 Wrangler is configured with an OAuth token. The token has been successfully revoked"
1036
+ );
928
1037
  }
929
1038
  const body =
930
1039
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
931
1040
  `token_type_hint=refresh_token&` +
932
- `token=${encodeURIComponent(refreshToken?.value || "")}`;
1041
+ `token=${encodeURIComponent(LocalState.refreshToken?.value || "")}`;
933
1042
 
934
1043
  const response = await fetch(REVOKE_URL, {
935
1044
  method: "POST",
@@ -939,38 +1048,34 @@ export async function logout(): Promise<void> {
939
1048
  },
940
1049
  });
941
1050
  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
- );
1051
+ rmSync(path.join(os.homedir(), USER_AUTH_CONFIG_FILE));
1052
+ console.log(`Successfully logged out.`);
950
1053
  }
951
1054
 
952
- export function listScopes(): void {
953
- throwIfNotInitialised();
954
- console.log("💁 Available scopes:");
955
- const data = Scopes.map((scope, index) => ({
1055
+ export function listScopes(message = "💁 Available scopes:"): void {
1056
+ console.log(message);
1057
+ const data = ScopeKeys.map((scope: Scope) => ({
956
1058
  Scope: scope,
957
- Description: ScopeDescriptions[index],
1059
+ Description: Scopes[scope],
958
1060
  }));
959
1061
  render(<Table data={data} />);
960
1062
  // TODO: maybe a good idea to show usage here
961
1063
  }
962
1064
 
963
- export async function getAccountId() {
1065
+ export async function getAccountId(
1066
+ isInteractive = true
1067
+ ): Promise<string | undefined> {
964
1068
  const apiToken = getAPIToken();
965
1069
  if (!apiToken) return;
966
1070
 
967
- if (process.env.CF_ACCOUNT_ID) {
968
- return process.env.CF_ACCOUNT_ID;
1071
+ const accountIdFromEnv = getCloudflareAccountIdFromEnv();
1072
+ if (accountIdFromEnv) {
1073
+ return accountIdFromEnv;
969
1074
  }
970
1075
 
971
1076
  let response: Response;
972
1077
  try {
973
- response = await fetch(`${CF_API_BASE_URL}/memberships`, {
1078
+ response = await fetch(`${getCloudflareApiBaseUrl()}/memberships`, {
974
1079
  method: "GET",
975
1080
  headers: {
976
1081
  Authorization: "Bearer " + apiToken,
@@ -978,19 +1083,19 @@ export async function getAccountId() {
978
1083
  });
979
1084
  } catch (err) {
980
1085
  // probably offline
1086
+ return;
981
1087
  }
982
- if (!response) return;
983
- let accountId: string;
984
- // @ts-expect-error need to type this response
985
- const responseJSON: {
1088
+
1089
+ let accountId: string | undefined;
1090
+ const responseJSON = (await response.json()) as {
986
1091
  success: boolean;
987
1092
  result: { id: string; account: { id: string; name: string } }[];
988
- } = await response.json();
1093
+ };
989
1094
 
990
1095
  if (responseJSON.success === true) {
991
1096
  if (responseJSON.result.length === 1) {
992
1097
  accountId = responseJSON.result[0].account.id;
993
- } else {
1098
+ } else if (isInteractive) {
994
1099
  accountId = await new Promise((resolve) => {
995
1100
  const accounts = responseJSON.result.map((x) => x.account);
996
1101
  const { unmount } = render(
@@ -1003,6 +1108,15 @@ export async function getAccountId() {
1003
1108
  />
1004
1109
  );
1005
1110
  });
1111
+ } else {
1112
+ throw new Error(
1113
+ "More than one account available but unable to select one in non-interactive mode.\n" +
1114
+ `Please set the appropriate \`account_id\` in your \`wrangler.toml\` file.\n` +
1115
+ `Available accounts are ("<name>" - "<id>"):\n` +
1116
+ responseJSON.result
1117
+ .map((x) => ` "${x.account.name}" - "${x.account.id}")`)
1118
+ .join("\n")
1119
+ );
1006
1120
  }
1007
1121
  }
1008
1122
  return accountId;
@@ -1014,16 +1128,39 @@ type ChooseAccountItem = {
1014
1128
  };
1015
1129
  export function ChooseAccount(props: {
1016
1130
  accounts: ChooseAccountItem[];
1017
- onSelect: (item) => void;
1131
+ onSelect: (item: SelectInputItem<ChooseAccountItem>) => void;
1018
1132
  }) {
1019
1133
  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
- />
1134
+ <>
1135
+ <Text bold>Select an account from below:</Text>
1136
+ <SelectInput
1137
+ items={props.accounts.map((item) => ({
1138
+ key: item.id,
1139
+ label: item.name,
1140
+ value: item,
1141
+ }))}
1142
+ onSelect={props.onSelect}
1143
+ />
1144
+ </>
1028
1145
  );
1029
1146
  }
1147
+
1148
+ /**
1149
+ * Ensure that a user is logged in, and a valid account_id is available.
1150
+ */
1151
+ export async function requireAuth(
1152
+ config: Config,
1153
+ isInteractive = true
1154
+ ): Promise<string> {
1155
+ const loggedIn = await loginOrRefreshIfRequired(isInteractive);
1156
+ if (!loggedIn) {
1157
+ // didn't login, let's just quit
1158
+ throw new Error("Did not login, quitting...");
1159
+ }
1160
+ const accountId = config.account_id || (await getAccountId(isInteractive));
1161
+ if (!accountId) {
1162
+ throw new Error("No account id found, quitting...");
1163
+ }
1164
+
1165
+ return accountId;
1166
+ }