wrangler 0.0.0-e2a26a0 → 0.0.0-e3278fa

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 (150) hide show
  1. package/Cloudflare_CA.pem +18 -0
  2. package/README.md +27 -14
  3. package/bin/wrangler.js +49 -5
  4. package/miniflare-dist/index.mjs +4886 -0
  5. package/package.json +51 -27
  6. package/pages/functions/buildPlugin.ts +105 -0
  7. package/pages/functions/buildWorker.ts +93 -4
  8. package/pages/functions/filepath-routing.test.ts +223 -36
  9. package/pages/functions/filepath-routing.ts +78 -120
  10. package/pages/functions/routes.ts +13 -17
  11. package/pages/functions/template-plugin.ts +147 -0
  12. package/pages/functions/template-worker.ts +24 -17
  13. package/src/__tests__/config-cache.test.ts +36 -0
  14. package/src/__tests__/configuration.test.ts +3408 -0
  15. package/src/__tests__/dev.test.tsx +977 -65
  16. package/src/__tests__/guess-worker-format.test.ts +85 -0
  17. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  18. package/src/__tests__/helpers/mock-account-id.ts +26 -8
  19. package/src/__tests__/helpers/mock-bin.ts +4 -1
  20. package/src/__tests__/helpers/mock-cfetch.ts +10 -6
  21. package/src/__tests__/helpers/mock-console.ts +39 -8
  22. package/src/__tests__/helpers/mock-dialogs.ts +67 -2
  23. package/src/__tests__/helpers/mock-http-server.ts +46 -0
  24. package/src/__tests__/helpers/mock-istty.ts +27 -0
  25. package/src/__tests__/helpers/mock-kv.ts +9 -16
  26. package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
  27. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  28. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  29. package/src/__tests__/helpers/run-in-tmp.ts +10 -0
  30. package/src/__tests__/helpers/run-wrangler.ts +5 -3
  31. package/src/__tests__/helpers/write-worker-source.ts +31 -0
  32. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  33. package/src/__tests__/https-options.test.ts +130 -0
  34. package/src/__tests__/index.test.ts +148 -435
  35. package/src/__tests__/init.test.ts +1637 -0
  36. package/src/__tests__/jest.setup.ts +45 -5
  37. package/src/__tests__/kv.test.ts +590 -248
  38. package/src/__tests__/logger.test.ts +115 -0
  39. package/src/__tests__/package-manager.test.ts +156 -9
  40. package/src/__tests__/pages.test.ts +754 -0
  41. package/src/__tests__/parse.test.ts +330 -0
  42. package/src/__tests__/publish.test.ts +5471 -939
  43. package/src/__tests__/r2.test.ts +233 -0
  44. package/src/__tests__/route.test.ts +45 -0
  45. package/src/__tests__/secret.test.ts +362 -91
  46. package/src/__tests__/tail.test.ts +690 -0
  47. package/src/__tests__/user.test.ts +131 -0
  48. package/src/__tests__/whoami.test.tsx +60 -7
  49. package/src/abort.d.ts +3 -0
  50. package/src/bundle.ts +197 -0
  51. package/src/cfetch/index.ts +27 -8
  52. package/src/cfetch/internal.ts +73 -32
  53. package/src/cli.ts +6 -2
  54. package/src/config/README.md +107 -0
  55. package/src/config/config.ts +234 -0
  56. package/src/config/diagnostics.ts +80 -0
  57. package/src/config/environment.ts +420 -0
  58. package/src/config/index.ts +225 -0
  59. package/src/config/validation-helpers.ts +584 -0
  60. package/src/config/validation.ts +1539 -0
  61. package/src/config-cache.ts +45 -0
  62. package/src/create-worker-preview.ts +185 -0
  63. package/src/create-worker-upload-form.ts +254 -0
  64. package/src/dev/dev-vars.ts +36 -0
  65. package/src/dev/dev.tsx +412 -0
  66. package/src/dev/local.tsx +322 -0
  67. package/src/dev/remote.tsx +237 -0
  68. package/src/dev/use-esbuild.ts +118 -0
  69. package/src/dialogs.tsx +64 -6
  70. package/src/durable.ts +102 -0
  71. package/src/entry.ts +322 -0
  72. package/src/environment-variables.ts +35 -0
  73. package/src/errors.ts +11 -0
  74. package/src/https-options.ts +123 -0
  75. package/src/index.tsx +1747 -1002
  76. package/src/inspect.ts +80 -14
  77. package/src/intl-polyfill.d.ts +139 -0
  78. package/src/{kv.tsx → kv.ts} +192 -68
  79. package/src/logger.ts +66 -0
  80. package/src/miniflare-cli/README.md +30 -0
  81. package/src/miniflare-cli/enum-keys.ts +17 -0
  82. package/src/miniflare-cli/index.ts +41 -0
  83. package/src/miniflare-cli/request-context.ts +40 -0
  84. package/src/module-collection.ts +226 -31
  85. package/src/open-in-browser.ts +19 -0
  86. package/src/package-manager.ts +79 -25
  87. package/src/pages.tsx +1131 -84
  88. package/src/parse.ts +202 -0
  89. package/src/proxy.ts +170 -62
  90. package/src/publish.ts +612 -306
  91. package/src/r2.ts +50 -0
  92. package/src/selfsigned.d.ts +29 -0
  93. package/src/sites.tsx +143 -57
  94. package/src/tail/filters.ts +279 -0
  95. package/src/tail/index.ts +299 -0
  96. package/src/tail/printing.ts +67 -0
  97. package/src/update-check.ts +19 -0
  98. package/src/user.tsx +341 -146
  99. package/src/whoami.tsx +5 -3
  100. package/src/{api/worker.ts → worker.ts} +65 -39
  101. package/src/zones.ts +73 -0
  102. package/templates/gitignore +171 -0
  103. package/templates/new-worker-scheduled.js +17 -0
  104. package/templates/new-worker-scheduled.ts +32 -0
  105. package/templates/new-worker.js +1 -1
  106. package/templates/new-worker.ts +17 -2
  107. package/templates/static-asset-facade.js +1 -5
  108. package/templates/tsconfig.json +105 -0
  109. package/wrangler-dist/cli.js +72295 -71006
  110. package/src/__tests__/helpers/mock-user.ts +0 -27
  111. package/src/__tests__/logout.test.ts +0 -50
  112. package/src/api/form_data.ts +0 -134
  113. package/src/api/preview.ts +0 -128
  114. package/src/config.ts +0 -641
  115. package/src/dev.tsx +0 -834
  116. package/src/tail.tsx +0 -73
  117. package/vendor/@cloudflare/kv-asset-handler/CHANGELOG.md +0 -332
  118. package/vendor/@cloudflare/kv-asset-handler/LICENSE_APACHE +0 -176
  119. package/vendor/@cloudflare/kv-asset-handler/LICENSE_MIT +0 -25
  120. package/vendor/@cloudflare/kv-asset-handler/README.md +0 -245
  121. package/vendor/@cloudflare/kv-asset-handler/dist/index.d.ts +0 -32
  122. package/vendor/@cloudflare/kv-asset-handler/dist/index.js +0 -354
  123. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.d.ts +0 -13
  124. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.js +0 -148
  125. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.d.ts +0 -1
  126. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.js +0 -436
  127. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.d.ts +0 -1
  128. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.js +0 -40
  129. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.d.ts +0 -1
  130. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.js +0 -42
  131. package/vendor/@cloudflare/kv-asset-handler/dist/types.d.ts +0 -26
  132. package/vendor/@cloudflare/kv-asset-handler/dist/types.js +0 -31
  133. package/vendor/@cloudflare/kv-asset-handler/package.json +0 -52
  134. package/vendor/@cloudflare/kv-asset-handler/src/index.ts +0 -296
  135. package/vendor/@cloudflare/kv-asset-handler/src/mocks.ts +0 -136
  136. package/vendor/@cloudflare/kv-asset-handler/src/test/getAssetFromKV.ts +0 -464
  137. package/vendor/@cloudflare/kv-asset-handler/src/test/mapRequestToAsset.ts +0 -33
  138. package/vendor/@cloudflare/kv-asset-handler/src/test/serveSinglePageApp.ts +0 -42
  139. package/vendor/@cloudflare/kv-asset-handler/src/types.ts +0 -39
  140. package/vendor/wrangler-mime/CHANGELOG.md +0 -289
  141. package/vendor/wrangler-mime/LICENSE +0 -21
  142. package/vendor/wrangler-mime/Mime.js +0 -97
  143. package/vendor/wrangler-mime/README.md +0 -187
  144. package/vendor/wrangler-mime/cli.js +0 -46
  145. package/vendor/wrangler-mime/index.js +0 -4
  146. package/vendor/wrangler-mime/lite.js +0 -4
  147. package/vendor/wrangler-mime/package.json +0 -52
  148. package/vendor/wrangler-mime/types/other.js +0 -1
  149. package/vendor/wrangler-mime/types/standard.js +0 -1
  150. package/wrangler-dist/cli.js.map +0 -7
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
@@ -207,24 +207,45 @@
207
207
 
208
208
  import assert from "node:assert";
209
209
  import { webcrypto as crypto } from "node:crypto";
210
- import { readFile, writeFile, rm, mkdir } from "node:fs/promises";
210
+ import { writeFileSync, mkdirSync, rmSync } from "node:fs";
211
211
  import http from "node:http";
212
212
  import os from "node:os";
213
213
  import path from "node:path";
214
- import process from "node:process";
215
214
  import url from "node:url";
216
215
  import { TextEncoder } from "node:util";
217
216
  import TOML from "@iarna/toml";
217
+ import { HostURL } from "@webcontainer/env";
218
218
  import { render, Text } from "ink";
219
219
  import SelectInput from "ink-select-input";
220
220
  import Table from "ink-table";
221
- import open from "open";
222
221
  import React from "react";
223
222
  import { fetch } from "undici";
224
- import { CF_API_BASE_URL } from "./cfetch";
223
+ import { getCloudflareApiBaseUrl } from "./cfetch";
224
+ import { purgeConfigCaches } from "./config-cache";
225
+ import { getEnvironmentVariableFactory } from "./environment-variables";
226
+ import { logger } from "./logger";
227
+ import openInBrowser from "./open-in-browser";
228
+ import { parseTOML, readFileSync } from "./parse";
229
+ import type { Item as SelectInputItem } from "ink-select-input/build/SelectInput";
225
230
  import type { ParsedUrlQuery } from "node:querystring";
226
231
  import type { Response } from "undici";
227
232
 
233
+ /**
234
+ * Try to read the API token from the environment.
235
+ */
236
+ export const getCloudflareAPITokenFromEnv = getEnvironmentVariableFactory({
237
+ variableName: "CLOUDFLARE_API_TOKEN",
238
+ deprecatedName: "CF_API_TOKEN",
239
+ });
240
+
241
+ /**
242
+ * Try to read the account ID from the environment.
243
+ */
244
+ const getCloudflareAccountIdFromEnv = getEnvironmentVariableFactory({
245
+ variableName: "CLOUDFLARE_ACCOUNT_ID",
246
+ deprecatedName: "CF_ACCOUNT_ID",
247
+ });
248
+
228
249
  /**
229
250
  * An implementation of rfc6749#section-4.1 and rfc7636.
230
251
  */
@@ -234,17 +255,45 @@ interface PKCECodes {
234
255
  codeVerifier: string;
235
256
  }
236
257
 
237
- interface State {
238
- accessToken?: AccessToken; // persist
258
+ /**
259
+ * The module level state of the authentication flow.
260
+ */
261
+ interface State extends AuthTokens {
239
262
  authorizationCode?: string;
240
263
  codeChallenge?: string;
241
264
  codeVerifier?: string;
242
265
  hasAuthCodeBeenExchangedForAccessToken?: boolean;
243
- refreshToken?: RefreshToken; // persist
244
266
  stateQueryParam?: string;
245
267
  scopes?: Scope[];
246
268
  }
247
269
 
270
+ /**
271
+ * The tokens related to authentication.
272
+ */
273
+ interface AuthTokens {
274
+ accessToken?: AccessToken;
275
+ refreshToken?: RefreshToken;
276
+ /** @deprecated - this field was only provided by the deprecated `wrangler1 config` command. */
277
+ apiToken?: string;
278
+ }
279
+
280
+ /**
281
+ * The path to the config file that holds user authentication data,
282
+ * relative to the user's home directory.
283
+ */
284
+ export const USER_AUTH_CONFIG_FILE = ".wrangler/config/default.toml";
285
+
286
+ /**
287
+ * The data that may be read from the `USER_CONFIG_FILE`.
288
+ */
289
+ export interface UserAuthConfig {
290
+ oauth_token?: string;
291
+ refresh_token?: string;
292
+ expiration_time?: string;
293
+ /** @deprecated - this field was only provided by the deprecated `wrangler1 config` command. */
294
+ api_token?: string;
295
+ }
296
+
248
297
  interface RefreshToken {
249
298
  value: string;
250
299
  }
@@ -268,6 +317,8 @@ const Scopes = {
268
317
  "workers_scripts:write":
269
318
  "See and change Cloudflare Workers scripts, durable objects, subdomains, triggers, and tail data.",
270
319
  "workers_tail:read": "See Cloudflare Workers tail and script data.",
320
+ "pages:write":
321
+ "See and change Cloudflare Pages projects, settings and deployments.",
271
322
  "zone:read": "Grants read level access to account zone.",
272
323
  } as const;
273
324
 
@@ -276,79 +327,113 @@ const Scopes = {
276
327
  *
277
328
  * "offline_access" is automatically included.
278
329
  */
279
- type Scope = keyof typeof Scopes | "offline_access";
330
+ type Scope = keyof typeof Scopes;
280
331
 
281
332
  const ScopeKeys = Object.keys(Scopes) as Scope[];
282
333
 
283
334
  export function validateScopeKeys(
284
335
  scopes: string[]
285
336
  ): scopes is typeof ScopeKeys {
286
- return scopes.every((scope) => Scopes[scope]);
337
+ return scopes.every((scope) => scope in Scopes);
287
338
  }
288
339
 
289
340
  const CLIENT_ID = "54d11594-84e4-41aa-b438-e81b8fa78ee7";
290
341
  const AUTH_URL = "https://dash.cloudflare.com/oauth2/auth";
291
342
  const TOKEN_URL = "https://dash.cloudflare.com/oauth2/token";
292
- const CALLBACK_URL = "http://localhost:8976/oauth/callback";
293
343
  const REVOKE_URL = "https://dash.cloudflare.com/oauth2/revoke";
294
344
 
295
- const LocalState: State = {};
296
- let initialised = false;
345
+ /**
346
+ * To allow OAuth callbacks in environments such as WebContainer we need to
347
+ * create a host URL which only resolves `localhost` to a WebContainer
348
+ * hostname if the process is running in a WebContainer. On local this will
349
+ * be a no-op and it leaves the URL unmodified.
350
+ *
351
+ * @see https://www.npmjs.com/package/@webcontainer/env
352
+ */
353
+ const CALLBACK_URL = HostURL.parse("http://localhost:8976/oauth/callback").href;
354
+
355
+ let LocalState: State = getAuthTokens();
297
356
 
298
- // we do this because we have some async stuff
299
- // TODO: this should just happen in the top level
300
- // and we should figure out how to do top level await
301
- export async function initialise(): Promise<void> {
357
+ /**
358
+ * Compute the current auth tokens.
359
+ */
360
+ function getAuthTokens(config?: UserAuthConfig): AuthTokens {
302
361
  // get refreshToken/accessToken from fs if exists
303
362
  try {
304
- // if CF_API_TOKEN available, use that
305
- if (process.env.CF_API_TOKEN) {
306
- LocalState.accessToken = {
307
- value: process.env.CF_API_TOKEN,
308
- expiry: "3021-12-31T23:59:59+00:00",
363
+ // if the environment variable is available, use that
364
+ const apiTokenFromEnv = getCloudflareAPITokenFromEnv();
365
+ if (apiTokenFromEnv) {
366
+ return {
367
+ accessToken: {
368
+ value: apiTokenFromEnv,
369
+ expiry: "3021-12-31T23:59:59+00:00",
370
+ },
309
371
  };
310
- initialised = true;
311
- return;
312
372
  }
313
373
 
314
- const toml = TOML.parse(
315
- await readFile(path.join(os.homedir(), ".wrangler/config/default.toml"), {
316
- encoding: "utf-8",
317
- })
318
- );
319
- const { oauth_token, refresh_token, expiration_time } = toml as {
320
- oauth_token: string;
321
- refresh_token: string;
322
- expiration_time: string;
323
- };
374
+ // otherwise try loading from the user auth config file.
375
+ const { oauth_token, refresh_token, expiration_time, api_token } =
376
+ config || readAuthConfigFile();
377
+
324
378
  if (oauth_token) {
325
- LocalState.accessToken = { value: oauth_token, expiry: expiration_time };
326
- }
327
- if (refresh_token) {
328
- LocalState.refreshToken = { value: refresh_token };
379
+ return {
380
+ accessToken: {
381
+ value: oauth_token,
382
+ // If there is no `expiration_time` field then set it to an old date, to cause it to expire immediately.
383
+ expiry: expiration_time ?? "2000-01-01:00:00:00+00:00",
384
+ },
385
+ refreshToken: { value: refresh_token ?? "" },
386
+ };
387
+ } else if (api_token) {
388
+ return { apiToken: api_token };
389
+ } else {
390
+ return {};
329
391
  }
330
- } catch (err) {
331
- // no config yet, let's chill
332
- // console.error(err);
392
+ } catch {
393
+ return {};
333
394
  }
334
- initialised = true;
335
395
  }
336
396
 
337
- // ugh. TODO: see fix from above.
338
- function throwIfNotInitialised() {
339
- if (initialised === false) {
340
- throw new Error(
341
- "did you forget to call initialise() from the user module?"
342
- );
343
- }
397
+ /**
398
+ * Run the initialisation of the auth state, in the case that something changed.
399
+ *
400
+ * This runs automatically whenever `writeAuthConfigFile` is run, so generally
401
+ * you won't need to call it yourself.
402
+ */
403
+ export function reinitialiseAuthTokens(): void;
404
+
405
+ /**
406
+ * Reinitialise auth state from an in-memory config, skipping
407
+ * over the part where we write a file and then read it back into memory
408
+ */
409
+ export function reinitialiseAuthTokens(config: UserAuthConfig): void;
410
+
411
+ export function reinitialiseAuthTokens(config?: UserAuthConfig): void {
412
+ LocalState = getAuthTokens(config);
344
413
  }
345
414
 
346
415
  export function getAPIToken(): string | undefined {
347
- if (process.env.CF_API_TOKEN) {
348
- return process.env.CF_API_TOKEN;
416
+ if (LocalState.apiToken) {
417
+ logger.warn(
418
+ "It looks like you have used Wrangler 1's `config` command to login with an API token.\n" +
419
+ "This is no longer supported in the current version of Wrangler.\n" +
420
+ "If you wish to authenticate via an API token then please set the `CLOUDFLARE_API_TOKEN` environment variable."
421
+ );
349
422
  }
350
- throwIfNotInitialised();
351
- return LocalState.accessToken?.value;
423
+
424
+ const localAPIToken = getCloudflareAPITokenFromEnv();
425
+
426
+ if (
427
+ !process.stdout.isTTY &&
428
+ !localAPIToken &&
429
+ !LocalState.accessToken?.value
430
+ ) {
431
+ throw new Error(
432
+ "In a non-interactive environment, it's necessary to set a CLOUDFLARE_API_TOKEN environment variable for wrangler to work. Please go to https://developers.cloudflare.com/api/tokens/create/ for instructions on how to create an api token, and assign its value to CLOUDFLARE_API_TOKEN."
433
+ );
434
+ }
435
+
436
+ return localAPIToken ?? LocalState.accessToken?.value;
352
437
  }
353
438
 
354
439
  interface AccessContext {
@@ -535,8 +620,8 @@ function isReturningFromAuthServer(query: ParsedUrlQuery): boolean {
535
620
 
536
621
  const stateQueryParam = query.state;
537
622
  if (stateQueryParam !== state.stateQueryParam) {
538
- console.warn(
539
- "state query string parameter doesn't match the one sent! Possible malicious activity somewhere."
623
+ logger.warn(
624
+ "Received query string parameter doesn't match the one sent! Possible malicious activity somewhere."
540
625
  );
541
626
  throw new ErrorInvalidReturnedStateParam();
542
627
  }
@@ -561,19 +646,31 @@ export async function getAuthURL(scopes = ScopeKeys): Promise<string> {
561
646
  `?response_type=code&` +
562
647
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
563
648
  `redirect_uri=${encodeURIComponent(CALLBACK_URL)}&` +
564
- `scope=${encodeURIComponent(scopes.concat("offline_access").join(" "))}&` +
649
+ // we add offline_access manually for every request
650
+ `scope=${encodeURIComponent([...scopes, "offline_access"].join(" "))}&` +
565
651
  `state=${stateQueryParam}&` +
566
652
  `code_challenge=${encodeURIComponent(codeChallenge)}&` +
567
653
  `code_challenge_method=S256`
568
654
  );
569
655
  }
570
656
 
657
+ type TokenResponse =
658
+ | {
659
+ access_token: string;
660
+ expires_in: number;
661
+ refresh_token: string;
662
+ scope: string;
663
+ }
664
+ | {
665
+ error: string;
666
+ };
667
+
571
668
  /**
572
669
  * Refresh an access token from the remote service.
573
670
  */
574
671
  async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
575
672
  if (!LocalState.refreshToken) {
576
- console.warn("No refresh token is present.");
673
+ logger.warn("No refresh token is present.");
577
674
  }
578
675
 
579
676
  const body =
@@ -588,17 +685,35 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
588
685
  "Content-Type": "application/x-www-form-urlencoded",
589
686
  },
590
687
  });
688
+
591
689
  if (response.status >= 400) {
592
- throw await response.json();
690
+ let tokenExchangeResErr = undefined;
691
+
692
+ try {
693
+ tokenExchangeResErr = await response.text();
694
+ tokenExchangeResErr = JSON.parse(tokenExchangeResErr);
695
+ } catch (e) {
696
+ // If it can't parse to JSON ignore the error
697
+ }
698
+
699
+ if (tokenExchangeResErr !== undefined) {
700
+ // We will throw the parsed error if it parsed correctly, otherwise we throw an unknown error.
701
+ throw typeof tokenExchangeResErr === "string"
702
+ ? new Error(tokenExchangeResErr)
703
+ : tokenExchangeResErr;
704
+ } else {
705
+ throw new ErrorUnknown(
706
+ "Failed to parse Error from exchangeRefreshTokenForAccessToken"
707
+ );
708
+ }
593
709
  } else {
594
710
  try {
595
- const json = await response.json();
596
- const { access_token, expires_in, refresh_token, scope } = json as {
597
- access_token: string;
598
- expires_in: number;
599
- refresh_token: string;
600
- scope: string;
601
- };
711
+ const json = (await response.json()) as TokenResponse;
712
+ if ("error" in json) {
713
+ throw json.error;
714
+ }
715
+
716
+ const { access_token, expires_in, refresh_token, scope } = json;
602
717
  let scopes: Scope[] = [];
603
718
 
604
719
  const accessToken: AccessToken = {
@@ -626,20 +741,12 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
626
741
  refreshToken: LocalState.refreshToken,
627
742
  };
628
743
  return accessContext;
629
- } catch (err) {
630
- const error = err?.error || "There was a network error.";
631
- switch (error) {
632
- case "invalid_grant":
633
- console.log(
634
- "Expired! Auth code or refresh token needs to be renewed."
635
- );
636
- // alert("Redirecting to auth server to obtain a new auth grant code.");
637
- // TODO: return refreshAuthCodeOrRefreshToken();
638
- break;
639
- default:
640
- break;
744
+ } catch (error) {
745
+ if (typeof error === "string") {
746
+ throw toErrorClass(error);
747
+ } else {
748
+ throw error;
641
749
  }
642
- throw toErrorClass(error);
643
750
  }
644
751
  }
645
752
  }
@@ -651,9 +758,9 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
651
758
  const { authorizationCode, codeVerifier = "" } = LocalState;
652
759
 
653
760
  if (!codeVerifier) {
654
- console.warn("No code verifier is being sent.");
761
+ logger.warn("No code verifier is being sent.");
655
762
  } else if (!authorizationCode) {
656
- console.warn("No authorization grant code is being passed.");
763
+ logger.warn("No authorization grant code is being passed.");
657
764
  }
658
765
 
659
766
  const body =
@@ -674,19 +781,17 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
674
781
  const { error } = (await response.json()) as { error: string };
675
782
  // .catch((_) => ({ error: "invalid_json" }));
676
783
  if (error === "invalid_grant") {
677
- console.log("Expired! Auth code or refresh token needs to be renewed.");
784
+ logger.log("Expired! Auth code or refresh token needs to be renewed.");
678
785
  // alert("Redirecting to auth server to obtain a new auth grant code.");
679
786
  // TODO: return refreshAuthCodeOrRefreshToken();
680
787
  }
681
788
  throw toErrorClass(error);
682
789
  }
683
- const json = await response.json();
684
- const { access_token, expires_in, refresh_token, scope } = json as {
685
- access_token: string;
686
- expires_in: number;
687
- refresh_token: string;
688
- scope: string;
689
- };
790
+ const json = (await response.json()) as TokenResponse;
791
+ if ("error" in json) {
792
+ throw new Error(json.error);
793
+ }
794
+ const { access_token, expires_in, refresh_token, scope } = json;
690
795
  let scopes: Scope[] = [];
691
796
  LocalState.hasAuthCodeBeenExchangedForAccessToken = true;
692
797
 
@@ -770,52 +875,73 @@ function generateRandomState(lengthOfState: number): string {
770
875
  .join("");
771
876
  }
772
877
 
773
- async function writeToConfigFile(tokenData: AccessContext) {
774
- await mkdir(path.join(os.homedir(), ".wrangler/config/"), {
878
+ /**
879
+ * Writes a a wrangler config file (auth credentials) to disk,
880
+ * and updates the user auth state with the new credentials.
881
+ */
882
+ export function writeAuthConfigFile(config: UserAuthConfig) {
883
+ mkdirSync(path.join(os.homedir(), ".wrangler/config/"), {
775
884
  recursive: true,
776
885
  });
777
- await writeFile(
778
- path.join(os.homedir(), ".wrangler/config/default.toml"),
779
- `
780
- oauth_token = "${tokenData.token?.value || ""}"
781
- refresh_token = "${tokenData.refreshToken?.value}"
782
- expiration_time = "${tokenData.token?.expiry}"
783
- `,
886
+ writeFileSync(
887
+ path.join(os.homedir(), USER_AUTH_CONFIG_FILE),
888
+ TOML.stringify(config as TOML.JsonMap),
784
889
  { encoding: "utf-8" }
785
890
  );
891
+
892
+ reinitialiseAuthTokens();
893
+ }
894
+
895
+ export function readAuthConfigFile(): UserAuthConfig {
896
+ const toml = parseTOML(
897
+ readFileSync(path.join(os.homedir(), USER_AUTH_CONFIG_FILE))
898
+ );
899
+ return toml;
786
900
  }
787
901
 
788
902
  type LoginProps = {
789
903
  scopes?: Scope[];
790
904
  };
791
905
 
792
- export async function loginOrRefreshIfRequired(): Promise<boolean> {
906
+ export async function loginOrRefreshIfRequired(
907
+ isInteractive = true
908
+ ): Promise<boolean> {
793
909
  // TODO: if there already is a token, then try refreshing
794
910
  // TODO: ask permission before opening browser
795
- if (!LocalState.accessToken) {
796
- // not logged in.
797
- return await login();
911
+ if (!getAPIToken()) {
912
+ // Not logged in.
913
+ // If we are not interactive, we cannot ask the user to login
914
+ return isInteractive && (await login());
798
915
  } else if (isAccessTokenExpired()) {
799
- return await refreshToken();
916
+ // We're logged in, but the refresh token seems to have expired,
917
+ // so let's try to refresh it
918
+ const didRefresh = await refreshToken();
919
+ if (didRefresh) {
920
+ // The token was refreshed, so we're done here
921
+ return true;
922
+ } else {
923
+ // If the refresh token isn't valid, then we ask the user to login again
924
+ return isInteractive && (await login());
925
+ }
800
926
  } else {
801
927
  return true;
802
928
  }
803
929
  }
804
930
 
805
931
  export async function login(props?: LoginProps): Promise<boolean> {
932
+ logger.log("Attempting to login via OAuth...");
806
933
  const urlToOpen = await getAuthURL(props?.scopes);
807
- await open(urlToOpen);
808
- // TODO: log url only if on system where it's unreliable/unavailable
809
- // console.log(`💁 Opened ${urlToOpen}`);
810
- let server;
811
- let loginTimeoutHandle;
934
+ let server: http.Server;
935
+ let loginTimeoutHandle: NodeJS.Timeout;
812
936
  const timerPromise = new Promise<boolean>((resolve) => {
813
937
  loginTimeoutHandle = setTimeout(() => {
814
- console.error("Timed out waiting for authorization code.");
938
+ logger.error(
939
+ "Timed out waiting for authorization code, please try again."
940
+ );
815
941
  server.close();
816
942
  clearTimeout(loginTimeoutHandle);
817
943
  resolve(false);
818
- }, 60000); // wait for 30 seconds for the user to authorize
944
+ }, 60000); // wait for 60 seconds for the user to authorize
819
945
  });
820
946
 
821
947
  const loginPromise = new Promise<boolean>((resolve, reject) => {
@@ -845,9 +971,10 @@ export async function login(props?: LoginProps): Promise<boolean> {
845
971
  res.end(() => {
846
972
  finish(false);
847
973
  });
848
- console.log(
849
- "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"
850
- ); // TODO: implement wrangler config lol
974
+ logger.error(
975
+ "Error: Consent denied. You must grant consent to Wrangler in order to login.\n" +
976
+ "If you don't want to do this consider passing an API token via the `CLOUDFLARE_API_TOKEN` environment variable"
977
+ );
851
978
 
852
979
  return;
853
980
  } else {
@@ -860,8 +987,12 @@ export async function login(props?: LoginProps): Promise<boolean> {
860
987
  finish(false, new ErrorNoAuthCode());
861
988
  return;
862
989
  } else {
863
- const tokenData = await exchangeAuthCodeForAccessToken();
864
- await writeToConfigFile(tokenData);
990
+ const exchange = await exchangeAuthCodeForAccessToken();
991
+ writeAuthConfigFile({
992
+ oauth_token: exchange.token?.value ?? "",
993
+ expiration_time: exchange.token?.expiry,
994
+ refresh_token: exchange.refreshToken?.value,
995
+ });
865
996
  res.writeHead(307, {
866
997
  Location:
867
998
  "https://welcome.developers.workers.dev/wrangler-oauth-consent-granted",
@@ -869,9 +1000,9 @@ export async function login(props?: LoginProps): Promise<boolean> {
869
1000
  res.end(() => {
870
1001
  finish(true);
871
1002
  });
872
- console.log(
873
- `Successfully configured. You can find your configuration file at: ${os.homedir()}/.wrangler/config/default.toml`
874
- );
1003
+ logger.log(`Successfully logged in.`);
1004
+
1005
+ purgeConfigCaches();
875
1006
 
876
1007
  return;
877
1008
  }
@@ -882,36 +1013,59 @@ export async function login(props?: LoginProps): Promise<boolean> {
882
1013
  server.listen(8976);
883
1014
  });
884
1015
 
1016
+ await openInBrowser(urlToOpen);
1017
+
885
1018
  return Promise.race([timerPromise, loginPromise]);
886
1019
  }
887
1020
 
888
1021
  /**
889
1022
  * Checks to see if the access token has expired.
890
1023
  */
891
- export function isAccessTokenExpired(): boolean {
892
- throwIfNotInitialised();
1024
+ function isAccessTokenExpired(): boolean {
893
1025
  const { accessToken } = LocalState;
894
1026
  return Boolean(accessToken && new Date() >= new Date(accessToken.expiry));
895
1027
  }
896
1028
 
897
- export async function refreshToken(): Promise<boolean> {
898
- throwIfNotInitialised();
1029
+ async function refreshToken(): Promise<boolean> {
899
1030
  // refresh
900
1031
  try {
901
- const refreshed = await exchangeRefreshTokenForAccessToken();
902
- await writeToConfigFile(refreshed);
1032
+ const {
1033
+ token: { value: oauth_token, expiry: expiration_time } = {
1034
+ value: "",
1035
+ expiry: "",
1036
+ },
1037
+ refreshToken: { value: refresh_token } = {},
1038
+ } = await exchangeRefreshTokenForAccessToken();
1039
+ writeAuthConfigFile({ oauth_token, expiration_time, refresh_token });
903
1040
  return true;
904
1041
  } catch (err) {
905
- console.error(err);
906
1042
  return false;
907
1043
  }
908
1044
  }
909
1045
 
910
1046
  export async function logout(): Promise<void> {
911
- throwIfNotInitialised();
912
- if (!LocalState.refreshToken) {
913
- console.log("Not logged in, exiting...");
914
- return;
1047
+ if (!LocalState.accessToken) {
1048
+ if (!LocalState.refreshToken) {
1049
+ logger.log("Not logged in, exiting...");
1050
+ return;
1051
+ }
1052
+
1053
+ const body =
1054
+ `client_id=${encodeURIComponent(CLIENT_ID)}&` +
1055
+ `token_type_hint=refresh_token&` +
1056
+ `token=${encodeURIComponent(LocalState.refreshToken?.value || "")}`;
1057
+
1058
+ const response = await fetch(REVOKE_URL, {
1059
+ method: "POST",
1060
+ body,
1061
+ headers: {
1062
+ "Content-Type": "application/x-www-form-urlencoded",
1063
+ },
1064
+ });
1065
+ await response.text(); // blank text? would be nice if it was something meaningful
1066
+ logger.log(
1067
+ "💁 Wrangler is configured with an OAuth token. The token has been successfully revoked"
1068
+ );
915
1069
  }
916
1070
  const body =
917
1071
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
@@ -926,20 +1080,13 @@ export async function logout(): Promise<void> {
926
1080
  },
927
1081
  });
928
1082
  await response.text(); // blank text? would be nice if it was something meaningful
929
- console.log(
930
- "💁 Wrangler is configured with an OAuth token. The token has been successfully revoked"
931
- );
932
- // delete the file
933
- await rm(path.join(os.homedir(), ".wrangler/config/default.toml"));
934
- console.log(
935
- `Removing ${os.homedir()}/.wrangler/config/default.toml.. success!`
936
- );
1083
+ rmSync(path.join(os.homedir(), USER_AUTH_CONFIG_FILE));
1084
+ logger.log(`Successfully logged out.`);
937
1085
  }
938
1086
 
939
- export function listScopes(): void {
940
- throwIfNotInitialised();
941
- console.log("💁 Available scopes:");
942
- const data = ScopeKeys.map((scope) => ({
1087
+ export function listScopes(message = "💁 Available scopes:"): void {
1088
+ logger.log(message);
1089
+ const data = ScopeKeys.map((scope: Scope) => ({
943
1090
  Scope: scope,
944
1091
  Description: Scopes[scope],
945
1092
  }));
@@ -947,17 +1094,20 @@ export function listScopes(): void {
947
1094
  // TODO: maybe a good idea to show usage here
948
1095
  }
949
1096
 
950
- export async function getAccountId() {
1097
+ export async function getAccountId(
1098
+ isInteractive = true
1099
+ ): Promise<string | undefined> {
951
1100
  const apiToken = getAPIToken();
952
1101
  if (!apiToken) return;
953
1102
 
954
- if (process.env.CF_ACCOUNT_ID) {
955
- return process.env.CF_ACCOUNT_ID;
1103
+ const accountIdFromEnv = getCloudflareAccountIdFromEnv();
1104
+ if (accountIdFromEnv) {
1105
+ return accountIdFromEnv;
956
1106
  }
957
1107
 
958
1108
  let response: Response;
959
1109
  try {
960
- response = await fetch(`${CF_API_BASE_URL}/memberships`, {
1110
+ response = await fetch(`${getCloudflareApiBaseUrl()}/memberships`, {
961
1111
  method: "GET",
962
1112
  headers: {
963
1113
  Authorization: "Bearer " + apiToken,
@@ -977,7 +1127,7 @@ export async function getAccountId() {
977
1127
  if (responseJSON.success === true) {
978
1128
  if (responseJSON.result.length === 1) {
979
1129
  accountId = responseJSON.result[0].account.id;
980
- } else {
1130
+ } else if (isInteractive) {
981
1131
  accountId = await new Promise((resolve) => {
982
1132
  const accounts = responseJSON.result.map((x) => x.account);
983
1133
  const { unmount } = render(
@@ -990,7 +1140,21 @@ export async function getAccountId() {
990
1140
  />
991
1141
  );
992
1142
  });
1143
+ } else {
1144
+ throw new Error(
1145
+ "More than one account available but unable to select one in non-interactive mode.\n" +
1146
+ `Please set the appropriate \`account_id\` in your \`wrangler.toml\` file.\n` +
1147
+ `Available accounts are ("<name>" - "<id>"):\n` +
1148
+ responseJSON.result
1149
+ .map((x) => ` "${x.account.name}" - "${x.account.id}")`)
1150
+ .join("\n")
1151
+ );
993
1152
  }
1153
+ } else {
1154
+ if (!isInteractive)
1155
+ throw new Error(
1156
+ `Failed to automatically retrieve account IDs for the logged in user. In a non-interactive environment, it is mandatory to specify an account ID, either by assigning its value to CLOUDFLARE_ACCOUNT_ID, or as \`account_id\` in your \`wrangler.toml\` file.`
1157
+ );
994
1158
  }
995
1159
  return accountId;
996
1160
  }
@@ -1001,7 +1165,7 @@ type ChooseAccountItem = {
1001
1165
  };
1002
1166
  export function ChooseAccount(props: {
1003
1167
  accounts: ChooseAccountItem[];
1004
- onSelect: (item) => void;
1168
+ onSelect: (item: SelectInputItem<ChooseAccountItem>) => void;
1005
1169
  }) {
1006
1170
  return (
1007
1171
  <>
@@ -1017,3 +1181,34 @@ export function ChooseAccount(props: {
1017
1181
  </>
1018
1182
  );
1019
1183
  }
1184
+
1185
+ /**
1186
+ * Ensure that a user is logged in, and a valid account_id is available.
1187
+ */
1188
+ export async function requireAuth(config: {
1189
+ account_id?: string;
1190
+ }): Promise<string> {
1191
+ const isInteractive = process.stdin.isTTY;
1192
+ const loggedIn = await loginOrRefreshIfRequired(isInteractive);
1193
+ if (!loggedIn) {
1194
+ // didn't login, let's just quit
1195
+ throw new Error("Did not login, quitting...");
1196
+ }
1197
+ const accountId = config.account_id || (await getAccountId(isInteractive));
1198
+ if (!accountId) {
1199
+ throw new Error("No account id found, quitting...");
1200
+ }
1201
+
1202
+ return accountId;
1203
+ }
1204
+
1205
+ /**
1206
+ * Throw an error if there is no API token available.
1207
+ */
1208
+ export function requireApiToken(): string {
1209
+ const authToken = getAPIToken();
1210
+ if (!authToken) {
1211
+ throw new Error("No API token found.");
1212
+ }
1213
+ return authToken;
1214
+ }