wrangler 0.0.0-ece06ea → 0.0.0-ed08df4

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 (121) hide show
  1. package/Cloudflare_CA.pem +18 -0
  2. package/README.md +27 -14
  3. package/bin/wrangler.js +34 -2
  4. package/miniflare-dist/index.mjs +4886 -0
  5. package/package.json +71 -36
  6. package/pages/functions/buildPlugin.ts +92 -0
  7. package/pages/functions/buildWorker.ts +138 -0
  8. package/pages/functions/filepath-routing.test.ts +234 -0
  9. package/pages/functions/filepath-routing.ts +189 -0
  10. package/pages/functions/identifiers.ts +78 -0
  11. package/pages/functions/routes.ts +156 -0
  12. package/pages/functions/template-plugin.ts +147 -0
  13. package/pages/functions/template-worker.ts +143 -0
  14. package/src/__tests__/config-cache.test.ts +36 -0
  15. package/src/__tests__/configuration.test.ts +3125 -0
  16. package/src/__tests__/dev.test.tsx +829 -0
  17. package/src/__tests__/guess-worker-format.test.ts +85 -0
  18. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  19. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  20. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  21. package/src/__tests__/helpers/mock-account-id.ts +48 -0
  22. package/src/__tests__/helpers/mock-bin.ts +36 -0
  23. package/src/__tests__/helpers/mock-cfetch.ts +211 -0
  24. package/src/__tests__/helpers/mock-console.ts +87 -0
  25. package/src/__tests__/helpers/mock-dialogs.ts +90 -0
  26. package/src/__tests__/helpers/mock-http-server.ts +46 -0
  27. package/src/__tests__/helpers/mock-istty.ts +27 -0
  28. package/src/__tests__/helpers/mock-kv.ts +33 -0
  29. package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
  30. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  31. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  32. package/src/__tests__/helpers/run-in-tmp.ts +39 -0
  33. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  34. package/src/__tests__/helpers/write-worker-source.ts +31 -0
  35. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  36. package/src/__tests__/https-options.test.ts +130 -0
  37. package/src/__tests__/index.test.ts +221 -132
  38. package/src/__tests__/init.test.ts +1516 -0
  39. package/src/__tests__/jest.setup.ts +77 -0
  40. package/src/__tests__/kv.test.ts +1481 -0
  41. package/src/__tests__/logger.test.ts +115 -0
  42. package/src/__tests__/package-manager.test.ts +206 -0
  43. package/src/__tests__/pages.test.ts +327 -0
  44. package/src/__tests__/parse.test.ts +326 -0
  45. package/src/__tests__/publish.test.ts +4996 -0
  46. package/src/__tests__/r2.test.ts +210 -0
  47. package/src/__tests__/route.test.ts +45 -0
  48. package/src/__tests__/secret.test.ts +446 -0
  49. package/src/__tests__/tail.test.ts +690 -0
  50. package/src/__tests__/user.test.ts +131 -0
  51. package/src/__tests__/whoami.test.tsx +105 -0
  52. package/src/bundle.ts +169 -0
  53. package/src/cfetch/index.ts +104 -0
  54. package/src/cfetch/internal.ts +140 -0
  55. package/src/cli.ts +10 -7
  56. package/src/config/README.md +107 -0
  57. package/src/config/config.ts +234 -0
  58. package/src/config/diagnostics.ts +80 -0
  59. package/src/config/environment.ts +394 -0
  60. package/src/config/index.ts +63 -0
  61. package/src/config/validation-helpers.ts +575 -0
  62. package/src/config/validation.ts +1424 -0
  63. package/src/config-cache.ts +45 -0
  64. package/src/create-worker-preview.ts +175 -0
  65. package/src/create-worker-upload-form.ts +240 -0
  66. package/src/dev/dev-vars.ts +36 -0
  67. package/src/dev/dev.tsx +421 -0
  68. package/src/dev/local.tsx +312 -0
  69. package/src/dev/remote.tsx +213 -0
  70. package/src/dev/use-esbuild.ts +118 -0
  71. package/src/dialogs.tsx +22 -20
  72. package/src/entry.ts +322 -0
  73. package/src/environment-variables.ts +35 -0
  74. package/src/errors.ts +11 -0
  75. package/src/https-options.ts +123 -0
  76. package/src/index.tsx +1869 -1020
  77. package/src/inspect.ts +623 -0
  78. package/src/intl-polyfill.d.ts +139 -0
  79. package/src/kv.ts +344 -0
  80. package/src/logger.ts +66 -0
  81. package/src/miniflare-cli/README.md +30 -0
  82. package/src/miniflare-cli/enum-keys.ts +17 -0
  83. package/src/miniflare-cli/index.ts +41 -0
  84. package/src/miniflare-cli/request-context.ts +40 -0
  85. package/src/module-collection.ts +258 -0
  86. package/src/open-in-browser.ts +26 -0
  87. package/src/package-manager.ts +127 -0
  88. package/src/pages.tsx +1756 -218
  89. package/src/parse.ts +185 -0
  90. package/src/paths.ts +26 -0
  91. package/src/proxy.ts +449 -0
  92. package/src/publish.ts +377 -262
  93. package/src/r2.ts +50 -0
  94. package/src/selfsigned.d.ts +29 -0
  95. package/src/sites.tsx +237 -59
  96. package/src/tail/filters.ts +279 -0
  97. package/src/tail/index.ts +299 -0
  98. package/src/tail/printing.ts +67 -0
  99. package/src/update-check.ts +19 -0
  100. package/src/user.tsx +395 -221
  101. package/src/whoami.tsx +70 -0
  102. package/src/worker.ts +172 -0
  103. package/templates/gitignore +171 -0
  104. package/templates/new-worker.js +15 -0
  105. package/templates/new-worker.ts +15 -0
  106. package/{static-asset-facade.js → templates/static-asset-facade.js} +1 -1
  107. package/templates/tsconfig.json +105 -0
  108. package/wrangler-dist/cli.js +108921 -132528
  109. package/src/__tests__/fixtures/init/.gitkeep +0 -0
  110. package/src/__tests__/mock-cfetch.js +0 -46
  111. package/src/api/form_data.ts +0 -158
  112. package/src/api/inspect.ts +0 -441
  113. package/src/api/preview.ts +0 -123
  114. package/src/api/worker.ts +0 -161
  115. package/src/cfetch.ts +0 -72
  116. package/src/config.ts +0 -124
  117. package/src/dev.tsx +0 -736
  118. package/src/kv.tsx +0 -211
  119. package/src/tail.tsx +0 -71
  120. package/src/util/fetch.ts +0 -74
  121. 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
@@ -205,25 +205,46 @@
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 { HostURL } from "@webcontainer/env";
218
+ import { render, Text } from "ink";
219
+ import SelectInput from "ink-select-input";
220
+ import Table from "ink-table";
221
+ import React from "react";
222
+ import { fetch } from "undici";
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";
224
230
  import type { ParsedUrlQuery } from "node:querystring";
225
- import { CF_API_BASE_URL } from "./cfetch";
226
- import type { Response } from "node-fetch";
231
+ import type { Response } from "undici";
232
+
233
+ /**
234
+ * Try to read the API token from the environment.
235
+ */
236
+ 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
+ });
227
248
 
228
249
  /**
229
250
  * An implementation of rfc6749#section-4.1 and rfc7636.
@@ -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
  }
@@ -254,103 +303,137 @@ interface AccessToken {
254
303
  expiry: string;
255
304
  }
256
305
 
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
- ];
306
+ const Scopes = {
307
+ "account:read":
308
+ "See your account info such as account details, analytics, and memberships.",
309
+ "user:read":
310
+ "See your user info such as name, email address, and account memberships.",
311
+ "workers:write":
312
+ "See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.",
313
+ "workers_kv:write":
314
+ "See and change Cloudflare Workers KV Storage data such as keys and namespaces.",
315
+ "workers_routes:write":
316
+ "See and change Cloudflare Workers data such as filters and routes.",
317
+ "workers_scripts:write":
318
+ "See and change Cloudflare Workers scripts, durable objects, subdomains, triggers, and tail data.",
319
+ "workers_tail:read": "See Cloudflare Workers tail and script data.",
320
+ "pages:write":
321
+ "See and change Cloudflare Pages projects, settings and deployments.",
322
+ "zone:read": "Grants read level access to account zone.",
323
+ } as const;
324
+
325
+ /**
326
+ * The possible keys for a Scope.
327
+ *
328
+ * "offline_access" is automatically included.
329
+ */
330
+ type Scope = keyof typeof Scopes;
331
+
332
+ const ScopeKeys = Object.keys(Scopes) as Scope[];
333
+
334
+ export function validateScopeKeys(
335
+ scopes: string[]
336
+ ): scopes is typeof ScopeKeys {
337
+ return scopes.every((scope) => scope in Scopes);
338
+ }
289
339
 
290
340
  const CLIENT_ID = "54d11594-84e4-41aa-b438-e81b8fa78ee7";
291
341
  const AUTH_URL = "https://dash.cloudflare.com/oauth2/auth";
292
342
  const TOKEN_URL = "https://dash.cloudflare.com/oauth2/token";
293
- const CALLBACK_URL = "http://localhost:8976/oauth/callback";
294
343
  const REVOKE_URL = "https://dash.cloudflare.com/oauth2/revoke";
295
344
 
296
- const LocalState: State = {};
297
- 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;
298
354
 
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
355
+ let LocalState: State = getAuthTokens();
356
+
357
+ /**
358
+ * Compute the current auth tokens.
359
+ */
360
+ function getAuthTokens(config?: UserAuthConfig): AuthTokens {
361
+ // get refreshToken/accessToken from fs if exists
304
362
  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",
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
+ },
310
371
  };
311
- initialised = true;
312
- return;
313
372
  }
314
373
 
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
- };
374
+ // otherwise try loading from the user auth config file.
375
+ const { oauth_token, refresh_token, expiration_time, api_token } =
376
+ config || readAuthConfigFile();
377
+
325
378
  if (oauth_token) {
326
- LocalState.accessToken = { value: oauth_token, expiry: expiration_time };
327
- }
328
- if (refresh_token) {
329
- 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 {};
330
391
  }
331
- } catch (err) {
332
- // no config yet, let's chill
333
- // console.error(err);
392
+ } catch {
393
+ return {};
334
394
  }
335
- initialised = true;
336
395
  }
337
396
 
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?"
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);
413
+ }
414
+
415
+ export function getAPIToken(): string | undefined {
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."
343
421
  );
344
422
  }
345
- }
346
423
 
347
- export function getAPIToken(): string {
348
- if (process.env.CF_API_TOKEN) {
349
- return process.env.CF_API_TOKEN;
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
+ );
350
434
  }
351
435
 
352
- throwIfNotInitialised();
353
- return LocalState.accessToken?.value;
436
+ return localAPIToken ?? LocalState.accessToken?.value;
354
437
  }
355
438
 
356
439
  interface AccessContext {
@@ -537,8 +620,8 @@ function isReturningFromAuthServer(query: ParsedUrlQuery): boolean {
537
620
 
538
621
  const stateQueryParam = query.state;
539
622
  if (stateQueryParam !== state.stateQueryParam) {
540
- console.warn(
541
- "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."
542
625
  );
543
626
  throw new ErrorInvalidReturnedStateParam();
544
627
  }
@@ -548,7 +631,7 @@ function isReturningFromAuthServer(query: ParsedUrlQuery): boolean {
548
631
  return true;
549
632
  }
550
633
 
551
- export async function getAuthURL(scopes?: string[]): Promise<string> {
634
+ export async function getAuthURL(scopes = ScopeKeys): Promise<string> {
552
635
  const { codeChallenge, codeVerifier } = await generatePKCECodes();
553
636
  const stateQueryParam = generateRandomState(RECOMMENDED_STATE_LENGTH);
554
637
 
@@ -558,56 +641,79 @@ export async function getAuthURL(scopes?: string[]): Promise<string> {
558
641
  stateQueryParam,
559
642
  });
560
643
 
561
- // TODO: verify that the scopes passed are legit
562
-
563
644
  return (
564
645
  AUTH_URL +
565
646
  `?response_type=code&` +
566
647
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
567
648
  `redirect_uri=${encodeURIComponent(CALLBACK_URL)}&` +
568
- `scope=${encodeURIComponent(
569
- (scopes || Scopes).concat("offline_access").join(" ")
570
- )}&` +
649
+ // we add offline_access manually for every request
650
+ `scope=${encodeURIComponent([...scopes, "offline_access"].join(" "))}&` +
571
651
  `state=${stateQueryParam}&` +
572
652
  `code_challenge=${encodeURIComponent(codeChallenge)}&` +
573
653
  `code_challenge_method=S256`
574
654
  );
575
655
  }
576
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
+
577
668
  /**
578
669
  * Refresh an access token from the remote service.
579
670
  */
580
671
  async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
581
- const { refreshToken } = LocalState;
582
-
583
- if (!refreshToken) {
584
- console.warn("No refresh token is present.");
672
+ if (!LocalState.refreshToken) {
673
+ logger.warn("No refresh token is present.");
585
674
  }
586
675
 
587
- const url = TOKEN_URL;
588
676
  const body =
589
677
  `grant_type=refresh_token&` +
590
- `refresh_token=${refreshToken?.value}&` +
678
+ `refresh_token=${LocalState.refreshToken?.value}&` +
591
679
  `client_id=${CLIENT_ID}`;
592
680
 
593
- const response = await fetch(url, {
681
+ const response = await fetch(TOKEN_URL, {
594
682
  method: "POST",
595
683
  body,
596
684
  headers: {
597
685
  "Content-Type": "application/x-www-form-urlencoded",
598
686
  },
599
687
  });
688
+
600
689
  if (response.status >= 400) {
601
- 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
+ }
602
709
  } else {
603
710
  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
- };
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;
611
717
  let scopes: Scope[] = [];
612
718
 
613
719
  const accessToken: AccessToken = {
@@ -617,10 +723,9 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
617
723
  LocalState.accessToken = accessToken;
618
724
 
619
725
  if (refresh_token) {
620
- const refreshToken: RefreshToken = {
726
+ LocalState.refreshToken = {
621
727
  value: refresh_token,
622
728
  };
623
- LocalState.refreshToken = refreshToken;
624
729
  }
625
730
 
626
731
  if (scope) {
@@ -636,20 +741,12 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
636
741
  refreshToken: LocalState.refreshToken,
637
742
  };
638
743
  return accessContext;
639
- } catch (err) {
640
- const error = err?.error || "There was a network error.";
641
- switch (error) {
642
- case "invalid_grant":
643
- console.log(
644
- "Expired! Auth code or refresh token needs to be renewed."
645
- );
646
- // alert("Redirecting to auth server to obtain a new auth grant code.");
647
- // TODO: return refreshAuthCodeOrRefreshToken();
648
- break;
649
- default:
650
- break;
744
+ } catch (error) {
745
+ if (typeof error === "string") {
746
+ throw toErrorClass(error);
747
+ } else {
748
+ throw error;
651
749
  }
652
- throw toErrorClass(error);
653
750
  }
654
751
  }
655
752
  }
@@ -661,12 +758,11 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
661
758
  const { authorizationCode, codeVerifier = "" } = LocalState;
662
759
 
663
760
  if (!codeVerifier) {
664
- console.warn("No code verifier is being sent.");
761
+ logger.warn("No code verifier is being sent.");
665
762
  } else if (!authorizationCode) {
666
- console.warn("No authorization grant code is being passed.");
763
+ logger.warn("No authorization grant code is being passed.");
667
764
  }
668
765
 
669
- const url = TOKEN_URL;
670
766
  const body =
671
767
  `grant_type=authorization_code&` +
672
768
  `code=${encodeURIComponent(authorizationCode || "")}&` +
@@ -674,7 +770,7 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
674
770
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
675
771
  `code_verifier=${codeVerifier}`;
676
772
 
677
- const response = await fetch(url, {
773
+ const response = await fetch(TOKEN_URL, {
678
774
  method: "POST",
679
775
  body,
680
776
  headers: {
@@ -685,19 +781,17 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
685
781
  const { error } = (await response.json()) as { error: string };
686
782
  // .catch((_) => ({ error: "invalid_json" }));
687
783
  if (error === "invalid_grant") {
688
- 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.");
689
785
  // alert("Redirecting to auth server to obtain a new auth grant code.");
690
786
  // TODO: return refreshAuthCodeOrRefreshToken();
691
787
  }
692
788
  throw toErrorClass(error);
693
789
  }
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
- };
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;
701
795
  let scopes: Scope[] = [];
702
796
  LocalState.hasAuthCodeBeenExchangedForAccessToken = true;
703
797
 
@@ -709,10 +803,9 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
709
803
  LocalState.accessToken = accessToken;
710
804
 
711
805
  if (refresh_token) {
712
- const refreshToken: RefreshToken = {
806
+ LocalState.refreshToken = {
713
807
  value: refresh_token,
714
808
  };
715
- LocalState.refreshToken = refreshToken;
716
809
  }
717
810
 
718
811
  if (scope) {
@@ -782,52 +875,73 @@ function generateRandomState(lengthOfState: number): string {
782
875
  .join("");
783
876
  }
784
877
 
785
- async function writeToConfigFile(tokenData: AccessContext) {
786
- 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/"), {
787
884
  recursive: true,
788
885
  });
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
- `,
886
+ writeFileSync(
887
+ path.join(os.homedir(), USER_AUTH_CONFIG_FILE),
888
+ TOML.stringify(config as TOML.JsonMap),
796
889
  { encoding: "utf-8" }
797
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;
798
900
  }
799
901
 
800
902
  type LoginProps = {
801
- scopes?: string[];
903
+ scopes?: Scope[];
802
904
  };
803
905
 
804
- export async function loginOrRefreshIfRequired(): Promise<boolean> {
906
+ export async function loginOrRefreshIfRequired(
907
+ isInteractive = true
908
+ ): Promise<boolean> {
805
909
  // TODO: if there already is a token, then try refreshing
806
910
  // TODO: ask permission before opening browser
807
- if (!LocalState.accessToken) {
808
- // not logged in.
809
- 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());
810
915
  } else if (isAccessTokenExpired()) {
811
- 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
+ }
812
926
  } else {
813
927
  return true;
814
928
  }
815
929
  }
816
930
 
817
931
  export async function login(props?: LoginProps): Promise<boolean> {
932
+ logger.log("Attempting to login via OAuth...");
818
933
  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;
934
+ let server: http.Server;
935
+ let loginTimeoutHandle: NodeJS.Timeout;
824
936
  const timerPromise = new Promise<boolean>((resolve) => {
825
937
  loginTimeoutHandle = setTimeout(() => {
826
- console.error("Timed out waiting for authorization code.");
938
+ logger.error(
939
+ "Timed out waiting for authorization code, please try again."
940
+ );
827
941
  server.close();
828
942
  clearTimeout(loginTimeoutHandle);
829
943
  resolve(false);
830
- }, 60000); // wait for 30 seconds for the user to authorize
944
+ }, 60000); // wait for 60 seconds for the user to authorize
831
945
  });
832
946
 
833
947
  const loginPromise = new Promise<boolean>((resolve, reject) => {
@@ -857,9 +971,10 @@ export async function login(props?: LoginProps): Promise<boolean> {
857
971
  res.end(() => {
858
972
  finish(false);
859
973
  });
860
- 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
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
+ );
863
978
 
864
979
  return;
865
980
  } else {
@@ -872,8 +987,12 @@ export async function login(props?: LoginProps): Promise<boolean> {
872
987
  finish(false, new ErrorNoAuthCode());
873
988
  return;
874
989
  } else {
875
- const tokenData = await exchangeAuthCodeForAccessToken();
876
- 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
+ });
877
996
  res.writeHead(307, {
878
997
  Location:
879
998
  "https://welcome.developers.workers.dev/wrangler-oauth-consent-granted",
@@ -881,9 +1000,9 @@ export async function login(props?: LoginProps): Promise<boolean> {
881
1000
  res.end(() => {
882
1001
  finish(true);
883
1002
  });
884
- console.log(
885
- `Successfully configured. You can find your configuration file at: ${os.homedir()}/.wrangler/config/default.toml`
886
- );
1003
+ logger.log(`Successfully logged in.`);
1004
+
1005
+ purgeConfigCaches();
887
1006
 
888
1007
  return;
889
1008
  }
@@ -894,42 +1013,64 @@ export async function login(props?: LoginProps): Promise<boolean> {
894
1013
  server.listen(8976);
895
1014
  });
896
1015
 
1016
+ await openInBrowser(urlToOpen);
1017
+
897
1018
  return Promise.race([timerPromise, loginPromise]);
898
1019
  }
899
1020
 
900
1021
  /**
901
1022
  * Checks to see if the access token has expired.
902
1023
  */
903
- export function isAccessTokenExpired(): boolean {
904
- throwIfNotInitialised();
1024
+ function isAccessTokenExpired(): boolean {
905
1025
  const { accessToken } = LocalState;
906
1026
  return Boolean(accessToken && new Date() >= new Date(accessToken.expiry));
907
1027
  }
908
1028
 
909
- export async function refreshToken(): Promise<boolean> {
910
- throwIfNotInitialised();
1029
+ async function refreshToken(): Promise<boolean> {
911
1030
  // refresh
912
1031
  try {
913
- const refreshed = await exchangeRefreshTokenForAccessToken();
914
- 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 });
915
1040
  return true;
916
1041
  } catch (err) {
917
- console.error(err);
918
1042
  return false;
919
1043
  }
920
1044
  }
921
1045
 
922
1046
  export async function logout(): Promise<void> {
923
- throwIfNotInitialised();
924
- const { refreshToken } = LocalState;
925
- if (!refreshToken) {
926
- console.log("Not logged in, exiting...");
927
- 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
+ );
928
1069
  }
929
1070
  const body =
930
1071
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
931
1072
  `token_type_hint=refresh_token&` +
932
- `token=${encodeURIComponent(refreshToken?.value || "")}`;
1073
+ `token=${encodeURIComponent(LocalState.refreshToken?.value || "")}`;
933
1074
 
934
1075
  const response = await fetch(REVOKE_URL, {
935
1076
  method: "POST",
@@ -939,38 +1080,34 @@ export async function logout(): Promise<void> {
939
1080
  },
940
1081
  });
941
1082
  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
- );
1083
+ rmSync(path.join(os.homedir(), USER_AUTH_CONFIG_FILE));
1084
+ logger.log(`Successfully logged out.`);
950
1085
  }
951
1086
 
952
- export function listScopes(): void {
953
- throwIfNotInitialised();
954
- console.log("💁 Available scopes:");
955
- const data = Scopes.map((scope, index) => ({
1087
+ export function listScopes(message = "💁 Available scopes:"): void {
1088
+ logger.log(message);
1089
+ const data = ScopeKeys.map((scope: Scope) => ({
956
1090
  Scope: scope,
957
- Description: ScopeDescriptions[index],
1091
+ Description: Scopes[scope],
958
1092
  }));
959
1093
  render(<Table data={data} />);
960
1094
  // TODO: maybe a good idea to show usage here
961
1095
  }
962
1096
 
963
- export async function getAccountId() {
1097
+ export async function getAccountId(
1098
+ isInteractive = true
1099
+ ): Promise<string | undefined> {
964
1100
  const apiToken = getAPIToken();
965
1101
  if (!apiToken) return;
966
1102
 
967
- if (process.env.CF_ACCOUNT_ID) {
968
- return process.env.CF_ACCOUNT_ID;
1103
+ const accountIdFromEnv = getCloudflareAccountIdFromEnv();
1104
+ if (accountIdFromEnv) {
1105
+ return accountIdFromEnv;
969
1106
  }
970
1107
 
971
1108
  let response: Response;
972
1109
  try {
973
- response = await fetch(`${CF_API_BASE_URL}/memberships`, {
1110
+ response = await fetch(`${getCloudflareApiBaseUrl()}/memberships`, {
974
1111
  method: "GET",
975
1112
  headers: {
976
1113
  Authorization: "Bearer " + apiToken,
@@ -978,19 +1115,19 @@ export async function getAccountId() {
978
1115
  });
979
1116
  } catch (err) {
980
1117
  // probably offline
1118
+ return;
981
1119
  }
982
- if (!response) return;
983
- let accountId: string;
984
- // @ts-expect-error need to type this response
985
- const responseJSON: {
1120
+
1121
+ let accountId: string | undefined;
1122
+ const responseJSON = (await response.json()) as {
986
1123
  success: boolean;
987
1124
  result: { id: string; account: { id: string; name: string } }[];
988
- } = await response.json();
1125
+ };
989
1126
 
990
1127
  if (responseJSON.success === true) {
991
1128
  if (responseJSON.result.length === 1) {
992
1129
  accountId = responseJSON.result[0].account.id;
993
- } else {
1130
+ } else if (isInteractive) {
994
1131
  accountId = await new Promise((resolve) => {
995
1132
  const accounts = responseJSON.result.map((x) => x.account);
996
1133
  const { unmount } = render(
@@ -1003,7 +1140,21 @@ export async function getAccountId() {
1003
1140
  />
1004
1141
  );
1005
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
+ );
1006
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
+ );
1007
1158
  }
1008
1159
  return accountId;
1009
1160
  }
@@ -1014,16 +1165,39 @@ type ChooseAccountItem = {
1014
1165
  };
1015
1166
  export function ChooseAccount(props: {
1016
1167
  accounts: ChooseAccountItem[];
1017
- onSelect: (item) => void;
1168
+ onSelect: (item: SelectInputItem<ChooseAccountItem>) => void;
1018
1169
  }) {
1019
1170
  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
- />
1171
+ <>
1172
+ <Text bold>Select an account from below:</Text>
1173
+ <SelectInput
1174
+ items={props.accounts.map((item) => ({
1175
+ key: item.id,
1176
+ label: item.name,
1177
+ value: item,
1178
+ }))}
1179
+ onSelect={props.onSelect}
1180
+ />
1181
+ </>
1028
1182
  );
1029
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
+ }