wrangler 0.0.0-bf67bf2 → 0.0.0-bfbb0be

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