wrangler 0.0.0-e3cab74 → 0.0.0-e3ce82b
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.
- package/Cloudflare_CA.pem +18 -0
- package/README.md +27 -14
- package/bin/wrangler.js +49 -5
- package/miniflare-dist/index.mjs +4886 -0
- package/package.json +43 -28
- package/pages/functions/buildPlugin.ts +105 -0
- package/pages/functions/buildWorker.ts +93 -4
- package/pages/functions/filepath-routing.test.ts +136 -25
- package/pages/functions/filepath-routing.ts +14 -3
- package/pages/functions/routes.ts +6 -1
- package/pages/functions/template-plugin.ts +147 -0
- package/pages/functions/template-worker.ts +21 -8
- package/src/__tests__/config-cache.test.ts +36 -0
- package/src/__tests__/configuration.test.ts +1713 -411
- package/src/__tests__/dev.test.tsx +954 -84
- package/src/__tests__/guess-worker-format.test.ts +37 -18
- package/src/__tests__/helpers/mock-account-id.ts +22 -4
- package/src/__tests__/helpers/mock-cfetch.ts +8 -4
- package/src/__tests__/helpers/mock-console.ts +35 -10
- package/src/__tests__/helpers/mock-dialogs.ts +67 -2
- package/src/__tests__/helpers/mock-http-server.ts +46 -0
- package/src/__tests__/helpers/mock-istty.ts +27 -0
- package/src/__tests__/helpers/mock-kv.ts +9 -16
- package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
- package/src/__tests__/helpers/mock-stdin.ts +103 -0
- package/src/__tests__/helpers/run-in-tmp.ts +10 -0
- package/src/__tests__/helpers/write-worker-source.ts +31 -0
- package/src/__tests__/https-options.test.ts +130 -0
- package/src/__tests__/index.test.ts +138 -747
- package/src/__tests__/init.test.ts +1637 -0
- package/src/__tests__/jest.setup.ts +28 -8
- package/src/__tests__/kv.test.ts +598 -258
- package/src/__tests__/logger.test.ts +115 -0
- package/src/__tests__/package-manager.test.ts +154 -7
- package/src/__tests__/pages.test.ts +754 -0
- package/src/__tests__/parse.test.ts +8 -0
- package/src/__tests__/publish.test.ts +4374 -1025
- package/src/__tests__/r2.test.ts +69 -34
- package/src/__tests__/route.test.ts +45 -0
- package/src/__tests__/secret.test.ts +228 -73
- package/src/__tests__/tail.test.ts +146 -20
- package/src/__tests__/user.test.ts +131 -0
- package/src/__tests__/whoami.test.tsx +60 -7
- package/src/abort.d.ts +3 -0
- package/src/bundle.ts +75 -11
- package/src/cfetch/index.ts +21 -4
- package/src/cfetch/internal.ts +29 -20
- package/src/cli.ts +5 -11
- package/src/config/README.md +7 -7
- package/src/config/config.ts +69 -100
- package/src/config/environment.ts +119 -4
- package/src/config/index.ts +177 -4
- package/src/config/validation-helpers.ts +237 -23
- package/src/config/validation.ts +722 -191
- package/src/config-cache.ts +45 -0
- package/src/{api/preview.ts → create-worker-preview.ts} +65 -27
- package/src/{api/form_data.ts → create-worker-upload-form.ts} +55 -18
- package/src/dev/dev-vars.ts +36 -0
- package/src/dev/dev.tsx +401 -0
- package/src/dev/local.tsx +322 -0
- package/src/dev/remote.tsx +237 -0
- package/src/dev/use-esbuild.ts +118 -0
- package/src/dialogs.tsx +64 -6
- package/src/durable.ts +102 -0
- package/src/entry.ts +279 -14
- package/src/environment-variables.ts +3 -1
- package/src/errors.ts +11 -0
- package/src/https-options.ts +123 -0
- package/src/index.tsx +1386 -867
- package/src/inspect.ts +64 -11
- package/src/intl-polyfill.d.ts +139 -0
- package/src/{kv.tsx → kv.ts} +195 -62
- package/src/logger.ts +66 -0
- package/src/miniflare-cli/README.md +30 -0
- package/src/miniflare-cli/enum-keys.ts +17 -0
- package/src/miniflare-cli/index.ts +41 -0
- package/src/miniflare-cli/request-context.ts +40 -0
- package/src/module-collection.ts +94 -6
- package/src/open-in-browser.ts +9 -3
- package/src/package-manager.ts +59 -11
- package/src/pages.tsx +1123 -75
- package/src/parse.ts +26 -6
- package/src/proxy.ts +159 -57
- package/src/publish.ts +581 -228
- package/src/selfsigned.d.ts +29 -0
- package/src/sites.tsx +130 -45
- package/src/tail/index.ts +2 -2
- package/src/tail/printing.ts +13 -8
- package/src/update-check.ts +19 -0
- package/src/user.tsx +275 -110
- package/src/whoami.tsx +5 -3
- package/src/{api/worker.ts → worker.ts} +24 -29
- package/src/zones.ts +73 -0
- package/templates/gitignore +171 -0
- package/templates/new-worker-scheduled.js +17 -0
- package/templates/new-worker-scheduled.ts +32 -0
- package/templates/new-worker.ts +16 -1
- package/templates/static-asset-facade.js +1 -5
- package/wrangler-dist/cli.js +104803 -118375
- package/src/__tests__/helpers/mock-user.ts +0 -27
- package/src/__tests__/logout.test.ts +0 -50
- package/src/__tests__/sentry.test.ts +0 -146
- package/src/dev.tsx +0 -961
- package/src/guess-worker-format.ts +0 -68
- package/src/reporting.ts +0 -115
- package/vendor/@cloudflare/kv-asset-handler/CHANGELOG.md +0 -332
- package/vendor/@cloudflare/kv-asset-handler/LICENSE_APACHE +0 -176
- package/vendor/@cloudflare/kv-asset-handler/LICENSE_MIT +0 -25
- package/vendor/@cloudflare/kv-asset-handler/README.md +0 -245
- package/vendor/@cloudflare/kv-asset-handler/dist/index.d.ts +0 -32
- package/vendor/@cloudflare/kv-asset-handler/dist/index.js +0 -354
- package/vendor/@cloudflare/kv-asset-handler/dist/mocks.d.ts +0 -13
- package/vendor/@cloudflare/kv-asset-handler/dist/mocks.js +0 -148
- package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.d.ts +0 -1
- package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.js +0 -436
- package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.d.ts +0 -1
- package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.js +0 -40
- package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.d.ts +0 -1
- package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.js +0 -42
- package/vendor/@cloudflare/kv-asset-handler/dist/types.d.ts +0 -26
- package/vendor/@cloudflare/kv-asset-handler/dist/types.js +0 -31
- package/vendor/@cloudflare/kv-asset-handler/package.json +0 -52
- package/vendor/@cloudflare/kv-asset-handler/src/index.ts +0 -296
- package/vendor/@cloudflare/kv-asset-handler/src/mocks.ts +0 -136
- package/vendor/@cloudflare/kv-asset-handler/src/test/getAssetFromKV.ts +0 -464
- package/vendor/@cloudflare/kv-asset-handler/src/test/mapRequestToAsset.ts +0 -33
- package/vendor/@cloudflare/kv-asset-handler/src/test/serveSinglePageApp.ts +0 -42
- package/vendor/@cloudflare/kv-asset-handler/src/types.ts +0 -39
- package/vendor/wrangler-mime/CHANGELOG.md +0 -289
- package/vendor/wrangler-mime/LICENSE +0 -21
- package/vendor/wrangler-mime/Mime.js +0 -97
- package/vendor/wrangler-mime/README.md +0 -187
- package/vendor/wrangler-mime/cli.js +0 -46
- package/vendor/wrangler-mime/index.js +0 -4
- package/vendor/wrangler-mime/lite.js +0 -4
- package/vendor/wrangler-mime/package.json +0 -52
- package/vendor/wrangler-mime/types/other.js +0 -1
- package/vendor/wrangler-mime/types/standard.js +0 -1
- package/wrangler-dist/cli.js.map +0 -7
package/src/user.tsx
CHANGED
|
@@ -207,21 +207,25 @@
|
|
|
207
207
|
|
|
208
208
|
import assert from "node:assert";
|
|
209
209
|
import { webcrypto as crypto } from "node:crypto";
|
|
210
|
-
import {
|
|
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
214
|
import url from "node:url";
|
|
215
215
|
import { TextEncoder } from "node:util";
|
|
216
216
|
import TOML from "@iarna/toml";
|
|
217
|
+
import { HostURL } from "@webcontainer/env";
|
|
217
218
|
import { render, Text } from "ink";
|
|
218
219
|
import SelectInput from "ink-select-input";
|
|
219
220
|
import Table from "ink-table";
|
|
220
221
|
import React from "react";
|
|
221
222
|
import { fetch } from "undici";
|
|
222
223
|
import { getCloudflareApiBaseUrl } from "./cfetch";
|
|
224
|
+
import { purgeConfigCaches } from "./config-cache";
|
|
223
225
|
import { getEnvironmentVariableFactory } from "./environment-variables";
|
|
226
|
+
import { logger } from "./logger";
|
|
224
227
|
import openInBrowser from "./open-in-browser";
|
|
228
|
+
import { parseTOML, readFileSync } from "./parse";
|
|
225
229
|
import type { Item as SelectInputItem } from "ink-select-input/build/SelectInput";
|
|
226
230
|
import type { ParsedUrlQuery } from "node:querystring";
|
|
227
231
|
import type { Response } from "undici";
|
|
@@ -229,7 +233,7 @@ import type { Response } from "undici";
|
|
|
229
233
|
/**
|
|
230
234
|
* Try to read the API token from the environment.
|
|
231
235
|
*/
|
|
232
|
-
const getCloudflareAPITokenFromEnv = getEnvironmentVariableFactory({
|
|
236
|
+
export const getCloudflareAPITokenFromEnv = getEnvironmentVariableFactory({
|
|
233
237
|
variableName: "CLOUDFLARE_API_TOKEN",
|
|
234
238
|
deprecatedName: "CF_API_TOKEN",
|
|
235
239
|
});
|
|
@@ -251,17 +255,45 @@ interface PKCECodes {
|
|
|
251
255
|
codeVerifier: string;
|
|
252
256
|
}
|
|
253
257
|
|
|
254
|
-
|
|
255
|
-
|
|
258
|
+
/**
|
|
259
|
+
* The module level state of the authentication flow.
|
|
260
|
+
*/
|
|
261
|
+
interface State extends AuthTokens {
|
|
256
262
|
authorizationCode?: string;
|
|
257
263
|
codeChallenge?: string;
|
|
258
264
|
codeVerifier?: string;
|
|
259
265
|
hasAuthCodeBeenExchangedForAccessToken?: boolean;
|
|
260
|
-
refreshToken?: RefreshToken; // persist
|
|
261
266
|
stateQueryParam?: string;
|
|
262
267
|
scopes?: Scope[];
|
|
263
268
|
}
|
|
264
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
|
+
|
|
265
297
|
interface RefreshToken {
|
|
266
298
|
value: string;
|
|
267
299
|
}
|
|
@@ -285,6 +317,8 @@ const Scopes = {
|
|
|
285
317
|
"workers_scripts:write":
|
|
286
318
|
"See and change Cloudflare Workers scripts, durable objects, subdomains, triggers, and tail data.",
|
|
287
319
|
"workers_tail:read": "See Cloudflare Workers tail and script data.",
|
|
320
|
+
"pages:write":
|
|
321
|
+
"See and change Cloudflare Pages projects, settings and deployments.",
|
|
288
322
|
"zone:read": "Grants read level access to account zone.",
|
|
289
323
|
} as const;
|
|
290
324
|
|
|
@@ -306,64 +340,100 @@ export function validateScopeKeys(
|
|
|
306
340
|
const CLIENT_ID = "54d11594-84e4-41aa-b438-e81b8fa78ee7";
|
|
307
341
|
const AUTH_URL = "https://dash.cloudflare.com/oauth2/auth";
|
|
308
342
|
const TOKEN_URL = "https://dash.cloudflare.com/oauth2/token";
|
|
309
|
-
const CALLBACK_URL = "http://localhost:8976/oauth/callback";
|
|
310
343
|
const REVOKE_URL = "https://dash.cloudflare.com/oauth2/revoke";
|
|
311
344
|
|
|
312
|
-
|
|
313
|
-
|
|
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();
|
|
314
356
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
357
|
+
/**
|
|
358
|
+
* Compute the current auth tokens.
|
|
359
|
+
*/
|
|
360
|
+
function getAuthTokens(config?: UserAuthConfig): AuthTokens {
|
|
319
361
|
// get refreshToken/accessToken from fs if exists
|
|
320
362
|
try {
|
|
321
363
|
// if the environment variable is available, use that
|
|
322
364
|
const apiTokenFromEnv = getCloudflareAPITokenFromEnv();
|
|
323
365
|
if (apiTokenFromEnv) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
366
|
+
return {
|
|
367
|
+
accessToken: {
|
|
368
|
+
value: apiTokenFromEnv,
|
|
369
|
+
expiry: "3021-12-31T23:59:59+00:00",
|
|
370
|
+
},
|
|
327
371
|
};
|
|
328
|
-
initialised = true;
|
|
329
|
-
return;
|
|
330
372
|
}
|
|
331
373
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
);
|
|
337
|
-
const { oauth_token, refresh_token, expiration_time } = toml as {
|
|
338
|
-
oauth_token: string;
|
|
339
|
-
refresh_token: string;
|
|
340
|
-
expiration_time: string;
|
|
341
|
-
};
|
|
374
|
+
// otherwise try loading from the user auth config file.
|
|
375
|
+
const { oauth_token, refresh_token, expiration_time, api_token } =
|
|
376
|
+
config || readAuthConfigFile();
|
|
377
|
+
|
|
342
378
|
if (oauth_token) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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 {};
|
|
347
391
|
}
|
|
348
|
-
} catch
|
|
349
|
-
|
|
350
|
-
// console.error(err);
|
|
392
|
+
} catch {
|
|
393
|
+
return {};
|
|
351
394
|
}
|
|
352
|
-
initialised = true;
|
|
353
395
|
}
|
|
354
396
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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."
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const localAPIToken = getCloudflareAPITokenFromEnv();
|
|
425
|
+
|
|
426
|
+
if (
|
|
427
|
+
!process.stdout.isTTY &&
|
|
428
|
+
!localAPIToken &&
|
|
429
|
+
!LocalState.accessToken?.value
|
|
430
|
+
) {
|
|
358
431
|
throw new Error(
|
|
359
|
-
"
|
|
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."
|
|
360
433
|
);
|
|
361
434
|
}
|
|
362
|
-
}
|
|
363
435
|
|
|
364
|
-
|
|
365
|
-
throwIfNotInitialised();
|
|
366
|
-
return LocalState.accessToken?.value;
|
|
436
|
+
return localAPIToken ?? LocalState.accessToken?.value;
|
|
367
437
|
}
|
|
368
438
|
|
|
369
439
|
interface AccessContext {
|
|
@@ -550,8 +620,8 @@ function isReturningFromAuthServer(query: ParsedUrlQuery): boolean {
|
|
|
550
620
|
|
|
551
621
|
const stateQueryParam = query.state;
|
|
552
622
|
if (stateQueryParam !== state.stateQueryParam) {
|
|
553
|
-
|
|
554
|
-
"
|
|
623
|
+
logger.warn(
|
|
624
|
+
"Received query string parameter doesn't match the one sent! Possible malicious activity somewhere."
|
|
555
625
|
);
|
|
556
626
|
throw new ErrorInvalidReturnedStateParam();
|
|
557
627
|
}
|
|
@@ -576,8 +646,8 @@ export async function getAuthURL(scopes = ScopeKeys): Promise<string> {
|
|
|
576
646
|
`?response_type=code&` +
|
|
577
647
|
`client_id=${encodeURIComponent(CLIENT_ID)}&` +
|
|
578
648
|
`redirect_uri=${encodeURIComponent(CALLBACK_URL)}&` +
|
|
579
|
-
//
|
|
580
|
-
`scope=${encodeURIComponent(scopes
|
|
649
|
+
// we add offline_access manually for every request
|
|
650
|
+
`scope=${encodeURIComponent([...scopes, "offline_access"].join(" "))}&` +
|
|
581
651
|
`state=${stateQueryParam}&` +
|
|
582
652
|
`code_challenge=${encodeURIComponent(codeChallenge)}&` +
|
|
583
653
|
`code_challenge_method=S256`
|
|
@@ -600,7 +670,7 @@ type TokenResponse =
|
|
|
600
670
|
*/
|
|
601
671
|
async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
|
|
602
672
|
if (!LocalState.refreshToken) {
|
|
603
|
-
|
|
673
|
+
logger.warn("No refresh token is present.");
|
|
604
674
|
}
|
|
605
675
|
|
|
606
676
|
const body =
|
|
@@ -615,8 +685,27 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
|
|
|
615
685
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
616
686
|
},
|
|
617
687
|
});
|
|
688
|
+
|
|
618
689
|
if (response.status >= 400) {
|
|
619
|
-
|
|
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
|
+
}
|
|
620
709
|
} else {
|
|
621
710
|
try {
|
|
622
711
|
const json = (await response.json()) as TokenResponse;
|
|
@@ -653,18 +742,6 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
|
|
|
653
742
|
};
|
|
654
743
|
return accessContext;
|
|
655
744
|
} catch (error) {
|
|
656
|
-
switch (error) {
|
|
657
|
-
case "invalid_grant":
|
|
658
|
-
console.warn(
|
|
659
|
-
"Expired! Auth code or refresh token needs to be renewed."
|
|
660
|
-
);
|
|
661
|
-
// alert("Redirecting to auth server to obtain a new auth grant code.");
|
|
662
|
-
// TODO: return refreshAuthCodeOrRefreshToken();
|
|
663
|
-
break;
|
|
664
|
-
default:
|
|
665
|
-
console.error(error);
|
|
666
|
-
break;
|
|
667
|
-
}
|
|
668
745
|
if (typeof error === "string") {
|
|
669
746
|
throw toErrorClass(error);
|
|
670
747
|
} else {
|
|
@@ -681,9 +758,9 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
|
|
|
681
758
|
const { authorizationCode, codeVerifier = "" } = LocalState;
|
|
682
759
|
|
|
683
760
|
if (!codeVerifier) {
|
|
684
|
-
|
|
761
|
+
logger.warn("No code verifier is being sent.");
|
|
685
762
|
} else if (!authorizationCode) {
|
|
686
|
-
|
|
763
|
+
logger.warn("No authorization grant code is being passed.");
|
|
687
764
|
}
|
|
688
765
|
|
|
689
766
|
const body =
|
|
@@ -704,7 +781,7 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
|
|
|
704
781
|
const { error } = (await response.json()) as { error: string };
|
|
705
782
|
// .catch((_) => ({ error: "invalid_json" }));
|
|
706
783
|
if (error === "invalid_grant") {
|
|
707
|
-
|
|
784
|
+
logger.log("Expired! Auth code or refresh token needs to be renewed.");
|
|
708
785
|
// alert("Redirecting to auth server to obtain a new auth grant code.");
|
|
709
786
|
// TODO: return refreshAuthCodeOrRefreshToken();
|
|
710
787
|
}
|
|
@@ -798,46 +875,67 @@ function generateRandomState(lengthOfState: number): string {
|
|
|
798
875
|
.join("");
|
|
799
876
|
}
|
|
800
877
|
|
|
801
|
-
|
|
802
|
-
|
|
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/"), {
|
|
803
884
|
recursive: true,
|
|
804
885
|
});
|
|
805
|
-
|
|
806
|
-
path.join(os.homedir(),
|
|
807
|
-
|
|
808
|
-
oauth_token = "${tokenData.token?.value || ""}"
|
|
809
|
-
refresh_token = "${tokenData.refreshToken?.value}"
|
|
810
|
-
expiration_time = "${tokenData.token?.expiry}"
|
|
811
|
-
`,
|
|
886
|
+
writeFileSync(
|
|
887
|
+
path.join(os.homedir(), USER_AUTH_CONFIG_FILE),
|
|
888
|
+
TOML.stringify(config as TOML.JsonMap),
|
|
812
889
|
{ encoding: "utf-8" }
|
|
813
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;
|
|
814
900
|
}
|
|
815
901
|
|
|
816
902
|
type LoginProps = {
|
|
817
903
|
scopes?: Scope[];
|
|
818
904
|
};
|
|
819
905
|
|
|
820
|
-
export async function loginOrRefreshIfRequired(
|
|
906
|
+
export async function loginOrRefreshIfRequired(
|
|
907
|
+
isInteractive = true
|
|
908
|
+
): Promise<boolean> {
|
|
821
909
|
// TODO: if there already is a token, then try refreshing
|
|
822
910
|
// TODO: ask permission before opening browser
|
|
823
|
-
if (!
|
|
824
|
-
//
|
|
825
|
-
|
|
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());
|
|
826
915
|
} else if (isAccessTokenExpired()) {
|
|
827
|
-
|
|
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
|
+
}
|
|
828
926
|
} else {
|
|
829
927
|
return true;
|
|
830
928
|
}
|
|
831
929
|
}
|
|
832
930
|
|
|
833
931
|
export async function login(props?: LoginProps): Promise<boolean> {
|
|
932
|
+
logger.log("Attempting to login via OAuth...");
|
|
834
933
|
const urlToOpen = await getAuthURL(props?.scopes);
|
|
835
|
-
await openInBrowser(urlToOpen);
|
|
836
934
|
let server: http.Server;
|
|
837
935
|
let loginTimeoutHandle: NodeJS.Timeout;
|
|
838
936
|
const timerPromise = new Promise<boolean>((resolve) => {
|
|
839
937
|
loginTimeoutHandle = setTimeout(() => {
|
|
840
|
-
|
|
938
|
+
logger.error(
|
|
841
939
|
"Timed out waiting for authorization code, please try again."
|
|
842
940
|
);
|
|
843
941
|
server.close();
|
|
@@ -873,7 +971,7 @@ export async function login(props?: LoginProps): Promise<boolean> {
|
|
|
873
971
|
res.end(() => {
|
|
874
972
|
finish(false);
|
|
875
973
|
});
|
|
876
|
-
|
|
974
|
+
logger.error(
|
|
877
975
|
"Error: Consent denied. You must grant consent to Wrangler in order to login.\n" +
|
|
878
976
|
"If you don't want to do this consider passing an API token via the `CLOUDFLARE_API_TOKEN` environment variable"
|
|
879
977
|
);
|
|
@@ -889,8 +987,12 @@ export async function login(props?: LoginProps): Promise<boolean> {
|
|
|
889
987
|
finish(false, new ErrorNoAuthCode());
|
|
890
988
|
return;
|
|
891
989
|
} else {
|
|
892
|
-
const
|
|
893
|
-
|
|
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
|
+
});
|
|
894
996
|
res.writeHead(307, {
|
|
895
997
|
Location:
|
|
896
998
|
"https://welcome.developers.workers.dev/wrangler-oauth-consent-granted",
|
|
@@ -898,9 +1000,9 @@ export async function login(props?: LoginProps): Promise<boolean> {
|
|
|
898
1000
|
res.end(() => {
|
|
899
1001
|
finish(true);
|
|
900
1002
|
});
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
);
|
|
1003
|
+
logger.log(`Successfully logged in.`);
|
|
1004
|
+
|
|
1005
|
+
purgeConfigCaches();
|
|
904
1006
|
|
|
905
1007
|
return;
|
|
906
1008
|
}
|
|
@@ -911,36 +1013,59 @@ export async function login(props?: LoginProps): Promise<boolean> {
|
|
|
911
1013
|
server.listen(8976);
|
|
912
1014
|
});
|
|
913
1015
|
|
|
1016
|
+
await openInBrowser(urlToOpen);
|
|
1017
|
+
|
|
914
1018
|
return Promise.race([timerPromise, loginPromise]);
|
|
915
1019
|
}
|
|
916
1020
|
|
|
917
1021
|
/**
|
|
918
1022
|
* Checks to see if the access token has expired.
|
|
919
1023
|
*/
|
|
920
|
-
|
|
921
|
-
throwIfNotInitialised();
|
|
1024
|
+
function isAccessTokenExpired(): boolean {
|
|
922
1025
|
const { accessToken } = LocalState;
|
|
923
1026
|
return Boolean(accessToken && new Date() >= new Date(accessToken.expiry));
|
|
924
1027
|
}
|
|
925
1028
|
|
|
926
|
-
|
|
927
|
-
throwIfNotInitialised();
|
|
1029
|
+
async function refreshToken(): Promise<boolean> {
|
|
928
1030
|
// refresh
|
|
929
1031
|
try {
|
|
930
|
-
const
|
|
931
|
-
|
|
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 });
|
|
932
1040
|
return true;
|
|
933
1041
|
} catch (err) {
|
|
934
|
-
console.error(err);
|
|
935
1042
|
return false;
|
|
936
1043
|
}
|
|
937
1044
|
}
|
|
938
1045
|
|
|
939
1046
|
export async function logout(): Promise<void> {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
+
);
|
|
944
1069
|
}
|
|
945
1070
|
const body =
|
|
946
1071
|
`client_id=${encodeURIComponent(CLIENT_ID)}&` +
|
|
@@ -955,19 +1080,12 @@ export async function logout(): Promise<void> {
|
|
|
955
1080
|
},
|
|
956
1081
|
});
|
|
957
1082
|
await response.text(); // blank text? would be nice if it was something meaningful
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
);
|
|
961
|
-
// delete the file
|
|
962
|
-
await rm(path.join(os.homedir(), ".wrangler/config/default.toml"));
|
|
963
|
-
console.log(
|
|
964
|
-
`Removing ${os.homedir()}/.wrangler/config/default.toml.. success!`
|
|
965
|
-
);
|
|
1083
|
+
rmSync(path.join(os.homedir(), USER_AUTH_CONFIG_FILE));
|
|
1084
|
+
logger.log(`Successfully logged out.`);
|
|
966
1085
|
}
|
|
967
1086
|
|
|
968
|
-
export function listScopes(): void {
|
|
969
|
-
|
|
970
|
-
console.log("💁 Available scopes:");
|
|
1087
|
+
export function listScopes(message = "💁 Available scopes:"): void {
|
|
1088
|
+
logger.log(message);
|
|
971
1089
|
const data = ScopeKeys.map((scope: Scope) => ({
|
|
972
1090
|
Scope: scope,
|
|
973
1091
|
Description: Scopes[scope],
|
|
@@ -976,7 +1094,9 @@ export function listScopes(): void {
|
|
|
976
1094
|
// TODO: maybe a good idea to show usage here
|
|
977
1095
|
}
|
|
978
1096
|
|
|
979
|
-
export async function getAccountId(
|
|
1097
|
+
export async function getAccountId(
|
|
1098
|
+
isInteractive = true
|
|
1099
|
+
): Promise<string | undefined> {
|
|
980
1100
|
const apiToken = getAPIToken();
|
|
981
1101
|
if (!apiToken) return;
|
|
982
1102
|
|
|
@@ -1007,7 +1127,7 @@ export async function getAccountId() {
|
|
|
1007
1127
|
if (responseJSON.success === true) {
|
|
1008
1128
|
if (responseJSON.result.length === 1) {
|
|
1009
1129
|
accountId = responseJSON.result[0].account.id;
|
|
1010
|
-
} else {
|
|
1130
|
+
} else if (isInteractive) {
|
|
1011
1131
|
accountId = await new Promise((resolve) => {
|
|
1012
1132
|
const accounts = responseJSON.result.map((x) => x.account);
|
|
1013
1133
|
const { unmount } = render(
|
|
@@ -1020,7 +1140,21 @@ export async function getAccountId() {
|
|
|
1020
1140
|
/>
|
|
1021
1141
|
);
|
|
1022
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
|
+
);
|
|
1023
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
|
+
);
|
|
1024
1158
|
}
|
|
1025
1159
|
return accountId;
|
|
1026
1160
|
}
|
|
@@ -1047,3 +1181,34 @@ export function ChooseAccount(props: {
|
|
|
1047
1181
|
</>
|
|
1048
1182
|
);
|
|
1049
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
|
+
}
|
package/src/whoami.tsx
CHANGED
|
@@ -2,10 +2,11 @@ import { Text, render } from "ink";
|
|
|
2
2
|
import Table from "ink-table";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { fetchListResult, fetchResult } from "./cfetch";
|
|
5
|
-
import {
|
|
5
|
+
import { logger } from "./logger";
|
|
6
|
+
import { getAPIToken, getCloudflareAPITokenFromEnv } from "./user";
|
|
6
7
|
|
|
7
8
|
export async function whoami() {
|
|
8
|
-
|
|
9
|
+
logger.log("Getting User settings...");
|
|
9
10
|
const user = await getUserInfo();
|
|
10
11
|
render(<WhoAmI user={user}></WhoAmI>);
|
|
11
12
|
}
|
|
@@ -47,10 +48,11 @@ export interface UserInfo {
|
|
|
47
48
|
|
|
48
49
|
export async function getUserInfo(): Promise<UserInfo | undefined> {
|
|
49
50
|
const apiToken = getAPIToken();
|
|
51
|
+
const apiTokenFromEnv = getCloudflareAPITokenFromEnv();
|
|
50
52
|
return apiToken
|
|
51
53
|
? {
|
|
52
54
|
apiToken,
|
|
53
|
-
authType: "OAuth",
|
|
55
|
+
authType: apiTokenFromEnv ? "API" : "OAuth",
|
|
54
56
|
email: await getEmail(),
|
|
55
57
|
accounts: await getAccounts(),
|
|
56
58
|
}
|