wrangler 0.0.0-e22f9d7 → 0.0.0-e273e09

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 (154) hide show
  1. package/Cloudflare_CA.pem +18 -0
  2. package/README.md +27 -14
  3. package/bin/wrangler.js +48 -4
  4. package/miniflare-dist/index.mjs +4886 -0
  5. package/package.json +60 -31
  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 -28
  9. package/pages/functions/filepath-routing.ts +100 -132
  10. package/pages/functions/routes.ts +16 -18
  11. package/pages/functions/template-plugin.ts +147 -0
  12. package/pages/functions/template-worker.ts +37 -38
  13. package/src/__tests__/config-cache.test.ts +36 -0
  14. package/src/__tests__/configuration.test.ts +3487 -0
  15. package/src/__tests__/dev.test.tsx +977 -61
  16. package/src/__tests__/guess-worker-format.test.ts +85 -0
  17. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  18. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  19. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  20. package/src/__tests__/helpers/mock-account-id.ts +48 -0
  21. package/src/__tests__/helpers/mock-bin.ts +36 -0
  22. package/src/__tests__/{mock-cfetch.ts → helpers/mock-cfetch.ts} +56 -16
  23. package/src/__tests__/helpers/mock-console.ts +87 -0
  24. package/src/__tests__/helpers/mock-dialogs.ts +130 -0
  25. package/src/__tests__/helpers/mock-http-server.ts +46 -0
  26. package/src/__tests__/helpers/mock-istty.ts +27 -0
  27. package/src/__tests__/helpers/mock-kv.ts +33 -0
  28. package/src/__tests__/helpers/mock-oauth-flow.ts +255 -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 +39 -0
  32. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  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 +179 -209
  37. package/src/__tests__/init.test.ts +1637 -0
  38. package/src/__tests__/jest.setup.ts +61 -6
  39. package/src/__tests__/kv.test.ts +842 -472
  40. package/src/__tests__/logger.test.ts +115 -0
  41. package/src/__tests__/package-manager.test.ts +353 -0
  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 +5755 -0
  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 +481 -0
  48. package/src/__tests__/tail.test.ts +690 -0
  49. package/src/__tests__/user.test.ts +131 -0
  50. package/src/__tests__/whoami.test.tsx +139 -0
  51. package/src/abort.d.ts +3 -0
  52. package/src/bundle.ts +197 -0
  53. package/src/cfetch/index.ts +40 -21
  54. package/src/cfetch/internal.ts +102 -29
  55. package/src/cli.ts +6 -2
  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 +420 -0
  60. package/src/config/index.ts +225 -0
  61. package/src/config/validation-helpers.ts +584 -0
  62. package/src/config/validation.ts +1548 -0
  63. package/src/config-cache.ts +45 -0
  64. package/src/create-worker-preview.ts +185 -0
  65. package/src/create-worker-upload-form.ts +254 -0
  66. package/src/dev/dev-vars.ts +36 -0
  67. package/src/dev/dev.tsx +401 -0
  68. package/src/dev/local.tsx +326 -0
  69. package/src/dev/remote.tsx +237 -0
  70. package/src/dev/use-esbuild.ts +118 -0
  71. package/src/dialogs.tsx +66 -8
  72. package/src/durable.ts +102 -0
  73. package/src/entry.ts +322 -0
  74. package/src/environment-variables.ts +35 -0
  75. package/src/errors.ts +11 -0
  76. package/src/https-options.ts +123 -0
  77. package/src/index.tsx +1848 -976
  78. package/src/inspect.ts +156 -33
  79. package/src/intl-polyfill.d.ts +139 -0
  80. package/src/{kv.tsx → kv.ts} +199 -67
  81. package/src/logger.ts +66 -0
  82. package/src/miniflare-cli/README.md +30 -0
  83. package/src/miniflare-cli/enum-keys.ts +17 -0
  84. package/src/miniflare-cli/index.ts +41 -0
  85. package/src/miniflare-cli/request-context.ts +40 -0
  86. package/src/module-collection.ts +229 -35
  87. package/src/open-in-browser.ts +19 -0
  88. package/src/package-manager.ts +174 -0
  89. package/src/pages.tsx +1162 -100
  90. package/src/parse.ts +203 -0
  91. package/src/paths.ts +26 -0
  92. package/src/proxy.ts +227 -51
  93. package/src/publish.ts +633 -276
  94. package/src/r2.ts +50 -0
  95. package/src/selfsigned.d.ts +29 -0
  96. package/src/sites.tsx +270 -62
  97. package/src/tail/filters.ts +279 -0
  98. package/src/tail/index.ts +299 -0
  99. package/src/tail/printing.ts +67 -0
  100. package/src/update-check.ts +19 -0
  101. package/src/user.tsx +382 -195
  102. package/src/whoami.tsx +71 -0
  103. package/src/{api/worker.ts → worker.ts} +66 -40
  104. package/src/zones.ts +73 -0
  105. package/templates/gitignore +171 -0
  106. package/templates/new-worker-scheduled.js +17 -0
  107. package/templates/new-worker-scheduled.ts +32 -0
  108. package/templates/new-worker.js +15 -0
  109. package/templates/new-worker.ts +30 -0
  110. package/{static-asset-facade.js → templates/static-asset-facade.js} +1 -5
  111. package/templates/tsconfig.json +105 -0
  112. package/wrangler-dist/cli.js +106332 -103741
  113. package/src/__tests__/mock-dialogs.ts +0 -65
  114. package/src/__tests__/run-in-tmp.ts +0 -19
  115. package/src/__tests__/run-wrangler.ts +0 -32
  116. package/src/api/form_data.ts +0 -131
  117. package/src/api/preview.ts +0 -128
  118. package/src/config.ts +0 -487
  119. package/src/dev.tsx +0 -784
  120. package/src/tail.tsx +0 -73
  121. package/vendor/@cloudflare/kv-asset-handler/CHANGELOG.md +0 -332
  122. package/vendor/@cloudflare/kv-asset-handler/LICENSE_APACHE +0 -176
  123. package/vendor/@cloudflare/kv-asset-handler/LICENSE_MIT +0 -25
  124. package/vendor/@cloudflare/kv-asset-handler/README.md +0 -245
  125. package/vendor/@cloudflare/kv-asset-handler/dist/index.d.ts +0 -32
  126. package/vendor/@cloudflare/kv-asset-handler/dist/index.js +0 -354
  127. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.d.ts +0 -13
  128. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.js +0 -148
  129. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.d.ts +0 -1
  130. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.js +0 -436
  131. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.d.ts +0 -1
  132. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.js +0 -40
  133. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.d.ts +0 -1
  134. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.js +0 -42
  135. package/vendor/@cloudflare/kv-asset-handler/dist/types.d.ts +0 -26
  136. package/vendor/@cloudflare/kv-asset-handler/dist/types.js +0 -31
  137. package/vendor/@cloudflare/kv-asset-handler/package.json +0 -52
  138. package/vendor/@cloudflare/kv-asset-handler/src/index.ts +0 -296
  139. package/vendor/@cloudflare/kv-asset-handler/src/mocks.ts +0 -136
  140. package/vendor/@cloudflare/kv-asset-handler/src/test/getAssetFromKV.ts +0 -464
  141. package/vendor/@cloudflare/kv-asset-handler/src/test/mapRequestToAsset.ts +0 -33
  142. package/vendor/@cloudflare/kv-asset-handler/src/test/serveSinglePageApp.ts +0 -42
  143. package/vendor/@cloudflare/kv-asset-handler/src/types.ts +0 -39
  144. package/vendor/wrangler-mime/CHANGELOG.md +0 -289
  145. package/vendor/wrangler-mime/LICENSE +0 -21
  146. package/vendor/wrangler-mime/Mime.js +0 -97
  147. package/vendor/wrangler-mime/README.md +0 -187
  148. package/vendor/wrangler-mime/cli.js +0 -46
  149. package/vendor/wrangler-mime/index.js +0 -4
  150. package/vendor/wrangler-mime/lite.js +0 -4
  151. package/vendor/wrangler-mime/package.json +0 -52
  152. package/vendor/wrangler-mime/types/other.js +0 -1
  153. package/vendor/wrangler-mime/types/standard.js +0 -1
  154. 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, Text } 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
+ 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
+ });
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,105 +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;
354
+
355
+ let LocalState: State = getAuthTokens();
298
356
 
299
- // we do this because we have some async stuff
300
- // TODO: this should just happen in the top level
301
- // and we should figure out how to do top level await
302
- export async function initialise(): Promise<void> {
357
+ /**
358
+ * Compute the current auth tokens.
359
+ */
360
+ function getAuthTokens(config?: UserAuthConfig): AuthTokens {
303
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
- // `throwIfNotInitialised()` ensures that the accessToken is guaranteed to be defined.
354
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
355
- return LocalState.accessToken!.value;
436
+ return localAPIToken ?? LocalState.accessToken?.value;
356
437
  }
357
438
 
358
439
  interface AccessContext {
@@ -539,8 +620,8 @@ function isReturningFromAuthServer(query: ParsedUrlQuery): boolean {
539
620
 
540
621
  const stateQueryParam = query.state;
541
622
  if (stateQueryParam !== state.stateQueryParam) {
542
- console.warn(
543
- "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."
544
625
  );
545
626
  throw new ErrorInvalidReturnedStateParam();
546
627
  }
@@ -550,7 +631,7 @@ function isReturningFromAuthServer(query: ParsedUrlQuery): boolean {
550
631
  return true;
551
632
  }
552
633
 
553
- export async function getAuthURL(scopes?: string[]): Promise<string> {
634
+ export async function getAuthURL(scopes = ScopeKeys): Promise<string> {
554
635
  const { codeChallenge, codeVerifier } = await generatePKCECodes();
555
636
  const stateQueryParam = generateRandomState(RECOMMENDED_STATE_LENGTH);
556
637
 
@@ -560,28 +641,36 @@ export async function getAuthURL(scopes?: string[]): Promise<string> {
560
641
  stateQueryParam,
561
642
  });
562
643
 
563
- // TODO: verify that the scopes passed are legit
564
-
565
644
  return (
566
645
  AUTH_URL +
567
646
  `?response_type=code&` +
568
647
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
569
648
  `redirect_uri=${encodeURIComponent(CALLBACK_URL)}&` +
570
- `scope=${encodeURIComponent(
571
- (scopes || Scopes).concat("offline_access").join(" ")
572
- )}&` +
649
+ // we add offline_access manually for every request
650
+ `scope=${encodeURIComponent([...scopes, "offline_access"].join(" "))}&` +
573
651
  `state=${stateQueryParam}&` +
574
652
  `code_challenge=${encodeURIComponent(codeChallenge)}&` +
575
653
  `code_challenge_method=S256`
576
654
  );
577
655
  }
578
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
+
579
668
  /**
580
669
  * Refresh an access token from the remote service.
581
670
  */
582
671
  async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
583
672
  if (!LocalState.refreshToken) {
584
- console.warn("No refresh token is present.");
673
+ logger.warn("No refresh token is present.");
585
674
  }
586
675
 
587
676
  const body =
@@ -596,17 +685,35 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
596
685
  "Content-Type": "application/x-www-form-urlencoded",
597
686
  },
598
687
  });
688
+
599
689
  if (response.status >= 400) {
600
- 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
+ }
601
709
  } else {
602
710
  try {
603
- const json = await response.json();
604
- const { access_token, expires_in, refresh_token, scope } = json as {
605
- access_token: string;
606
- expires_in: number;
607
- refresh_token: string;
608
- scope: string;
609
- };
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;
610
717
  let scopes: Scope[] = [];
611
718
 
612
719
  const accessToken: AccessToken = {
@@ -634,20 +741,12 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
634
741
  refreshToken: LocalState.refreshToken,
635
742
  };
636
743
  return accessContext;
637
- } catch (err) {
638
- const error = err?.error || "There was a network error.";
639
- switch (error) {
640
- case "invalid_grant":
641
- console.log(
642
- "Expired! Auth code or refresh token needs to be renewed."
643
- );
644
- // alert("Redirecting to auth server to obtain a new auth grant code.");
645
- // TODO: return refreshAuthCodeOrRefreshToken();
646
- break;
647
- default:
648
- break;
744
+ } catch (error) {
745
+ if (typeof error === "string") {
746
+ throw toErrorClass(error);
747
+ } else {
748
+ throw error;
649
749
  }
650
- throw toErrorClass(error);
651
750
  }
652
751
  }
653
752
  }
@@ -659,9 +758,9 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
659
758
  const { authorizationCode, codeVerifier = "" } = LocalState;
660
759
 
661
760
  if (!codeVerifier) {
662
- console.warn("No code verifier is being sent.");
761
+ logger.warn("No code verifier is being sent.");
663
762
  } else if (!authorizationCode) {
664
- console.warn("No authorization grant code is being passed.");
763
+ logger.warn("No authorization grant code is being passed.");
665
764
  }
666
765
 
667
766
  const body =
@@ -682,19 +781,17 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
682
781
  const { error } = (await response.json()) as { error: string };
683
782
  // .catch((_) => ({ error: "invalid_json" }));
684
783
  if (error === "invalid_grant") {
685
- 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.");
686
785
  // alert("Redirecting to auth server to obtain a new auth grant code.");
687
786
  // TODO: return refreshAuthCodeOrRefreshToken();
688
787
  }
689
788
  throw toErrorClass(error);
690
789
  }
691
- const json = await response.json();
692
- const { access_token, expires_in, refresh_token, scope } = json as {
693
- access_token: string;
694
- expires_in: number;
695
- refresh_token: string;
696
- scope: string;
697
- };
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;
698
795
  let scopes: Scope[] = [];
699
796
  LocalState.hasAuthCodeBeenExchangedForAccessToken = true;
700
797
 
@@ -778,52 +875,73 @@ function generateRandomState(lengthOfState: number): string {
778
875
  .join("");
779
876
  }
780
877
 
781
- async function writeToConfigFile(tokenData: AccessContext) {
782
- 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/"), {
783
884
  recursive: true,
784
885
  });
785
- await writeFile(
786
- path.join(os.homedir(), ".wrangler/config/default.toml"),
787
- `
788
- oauth_token = "${tokenData.token?.value || ""}"
789
- refresh_token = "${tokenData.refreshToken?.value}"
790
- expiration_time = "${tokenData.token?.expiry}"
791
- `,
886
+ writeFileSync(
887
+ path.join(os.homedir(), USER_AUTH_CONFIG_FILE),
888
+ TOML.stringify(config as TOML.JsonMap),
792
889
  { encoding: "utf-8" }
793
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;
794
900
  }
795
901
 
796
902
  type LoginProps = {
797
- scopes?: string[];
903
+ scopes?: Scope[];
798
904
  };
799
905
 
800
- export async function loginOrRefreshIfRequired(): Promise<boolean> {
906
+ export async function loginOrRefreshIfRequired(
907
+ isInteractive = true
908
+ ): Promise<boolean> {
801
909
  // TODO: if there already is a token, then try refreshing
802
910
  // TODO: ask permission before opening browser
803
- if (!LocalState.accessToken) {
804
- // not logged in.
805
- 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());
806
915
  } else if (isAccessTokenExpired()) {
807
- 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
+ }
808
926
  } else {
809
927
  return true;
810
928
  }
811
929
  }
812
930
 
813
931
  export async function login(props?: LoginProps): Promise<boolean> {
932
+ logger.log("Attempting to login via OAuth...");
814
933
  const urlToOpen = await getAuthURL(props?.scopes);
815
- await open(urlToOpen);
816
- // TODO: log url only if on system where it's unreliable/unavailable
817
- // console.log(`💁 Opened ${urlToOpen}`);
818
- let server;
819
- let loginTimeoutHandle;
934
+ let server: http.Server;
935
+ let loginTimeoutHandle: NodeJS.Timeout;
820
936
  const timerPromise = new Promise<boolean>((resolve) => {
821
937
  loginTimeoutHandle = setTimeout(() => {
822
- console.error("Timed out waiting for authorization code.");
938
+ logger.error(
939
+ "Timed out waiting for authorization code, please try again."
940
+ );
823
941
  server.close();
824
942
  clearTimeout(loginTimeoutHandle);
825
943
  resolve(false);
826
- }, 60000); // wait for 30 seconds for the user to authorize
944
+ }, 60000); // wait for 60 seconds for the user to authorize
827
945
  });
828
946
 
829
947
  const loginPromise = new Promise<boolean>((resolve, reject) => {
@@ -853,9 +971,10 @@ export async function login(props?: LoginProps): Promise<boolean> {
853
971
  res.end(() => {
854
972
  finish(false);
855
973
  });
856
- console.log(
857
- "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"
858
- ); // 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
+ );
859
978
 
860
979
  return;
861
980
  } else {
@@ -868,8 +987,12 @@ export async function login(props?: LoginProps): Promise<boolean> {
868
987
  finish(false, new ErrorNoAuthCode());
869
988
  return;
870
989
  } else {
871
- const tokenData = await exchangeAuthCodeForAccessToken();
872
- 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
+ });
873
996
  res.writeHead(307, {
874
997
  Location:
875
998
  "https://welcome.developers.workers.dev/wrangler-oauth-consent-granted",
@@ -877,9 +1000,9 @@ export async function login(props?: LoginProps): Promise<boolean> {
877
1000
  res.end(() => {
878
1001
  finish(true);
879
1002
  });
880
- console.log(
881
- `Successfully configured. You can find your configuration file at: ${os.homedir()}/.wrangler/config/default.toml`
882
- );
1003
+ logger.log(`Successfully logged in.`);
1004
+
1005
+ purgeConfigCaches();
883
1006
 
884
1007
  return;
885
1008
  }
@@ -890,36 +1013,59 @@ export async function login(props?: LoginProps): Promise<boolean> {
890
1013
  server.listen(8976);
891
1014
  });
892
1015
 
1016
+ await openInBrowser(urlToOpen);
1017
+
893
1018
  return Promise.race([timerPromise, loginPromise]);
894
1019
  }
895
1020
 
896
1021
  /**
897
1022
  * Checks to see if the access token has expired.
898
1023
  */
899
- export function isAccessTokenExpired(): boolean {
900
- throwIfNotInitialised();
1024
+ function isAccessTokenExpired(): boolean {
901
1025
  const { accessToken } = LocalState;
902
1026
  return Boolean(accessToken && new Date() >= new Date(accessToken.expiry));
903
1027
  }
904
1028
 
905
- export async function refreshToken(): Promise<boolean> {
906
- throwIfNotInitialised();
1029
+ async function refreshToken(): Promise<boolean> {
907
1030
  // refresh
908
1031
  try {
909
- const refreshed = await exchangeRefreshTokenForAccessToken();
910
- 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 });
911
1040
  return true;
912
1041
  } catch (err) {
913
- console.error(err);
914
1042
  return false;
915
1043
  }
916
1044
  }
917
1045
 
918
1046
  export async function logout(): Promise<void> {
919
- throwIfNotInitialised();
920
- if (!LocalState.refreshToken) {
921
- console.log("Not logged in, exiting...");
922
- 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
+ );
923
1069
  }
924
1070
  const body =
925
1071
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
@@ -934,38 +1080,34 @@ export async function logout(): Promise<void> {
934
1080
  },
935
1081
  });
936
1082
  await response.text(); // blank text? would be nice if it was something meaningful
937
- console.log(
938
- "💁 Wrangler is configured with an OAuth token. The token has been successfully revoked"
939
- );
940
- // delete the file
941
- await rm(path.join(os.homedir(), ".wrangler/config/default.toml"));
942
- console.log(
943
- `Removing ${os.homedir()}/.wrangler/config/default.toml.. success!`
944
- );
1083
+ rmSync(path.join(os.homedir(), USER_AUTH_CONFIG_FILE));
1084
+ logger.log(`Successfully logged out.`);
945
1085
  }
946
1086
 
947
- export function listScopes(): void {
948
- throwIfNotInitialised();
949
- console.log("💁 Available scopes:");
950
- 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) => ({
951
1090
  Scope: scope,
952
- Description: ScopeDescriptions[index],
1091
+ Description: Scopes[scope],
953
1092
  }));
954
1093
  render(<Table data={data} />);
955
1094
  // TODO: maybe a good idea to show usage here
956
1095
  }
957
1096
 
958
- export async function getAccountId() {
1097
+ export async function getAccountId(
1098
+ isInteractive = true
1099
+ ): Promise<string | undefined> {
959
1100
  const apiToken = getAPIToken();
960
1101
  if (!apiToken) return;
961
1102
 
962
- if (process.env.CF_ACCOUNT_ID) {
963
- return process.env.CF_ACCOUNT_ID;
1103
+ const accountIdFromEnv = getCloudflareAccountIdFromEnv();
1104
+ if (accountIdFromEnv) {
1105
+ return accountIdFromEnv;
964
1106
  }
965
1107
 
966
1108
  let response: Response;
967
1109
  try {
968
- response = await fetch(`${CF_API_BASE_URL}/memberships`, {
1110
+ response = await fetch(`${getCloudflareApiBaseUrl()}/memberships`, {
969
1111
  method: "GET",
970
1112
  headers: {
971
1113
  Authorization: "Bearer " + apiToken,
@@ -985,7 +1127,7 @@ export async function getAccountId() {
985
1127
  if (responseJSON.success === true) {
986
1128
  if (responseJSON.result.length === 1) {
987
1129
  accountId = responseJSON.result[0].account.id;
988
- } else {
1130
+ } else if (isInteractive) {
989
1131
  accountId = await new Promise((resolve) => {
990
1132
  const accounts = responseJSON.result.map((x) => x.account);
991
1133
  const { unmount } = render(
@@ -998,7 +1140,21 @@ export async function getAccountId() {
998
1140
  />
999
1141
  );
1000
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
+ );
1001
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
+ );
1002
1158
  }
1003
1159
  return accountId;
1004
1160
  }
@@ -1009,7 +1165,7 @@ type ChooseAccountItem = {
1009
1165
  };
1010
1166
  export function ChooseAccount(props: {
1011
1167
  accounts: ChooseAccountItem[];
1012
- onSelect: (item) => void;
1168
+ onSelect: (item: SelectInputItem<ChooseAccountItem>) => void;
1013
1169
  }) {
1014
1170
  return (
1015
1171
  <>
@@ -1025,3 +1181,34 @@ export function ChooseAccount(props: {
1025
1181
  </>
1026
1182
  );
1027
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
+ }