wrangler 0.0.0-e2a26a0 → 0.0.0-e322475

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