wrangler 0.0.0-e3cab74 → 0.0.0-e3e3243
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/README.md +1 -1
- package/bin/wrangler.js +10 -1
- package/miniflare-dist/index.mjs +79 -0
- package/miniflare-dist/index.mjs.map +7 -0
- package/package.json +10 -7
- package/pages/functions/filepath-routing.test.ts +125 -25
- package/pages/functions/filepath-routing.ts +6 -3
- package/src/__tests__/configuration.test.ts +959 -324
- package/src/__tests__/dev.test.tsx +512 -84
- package/src/__tests__/guess-worker-format.test.ts +19 -17
- package/src/__tests__/helpers/mock-account-id.ts +22 -4
- package/src/__tests__/helpers/mock-cfetch.ts +3 -3
- package/src/__tests__/helpers/mock-console.ts +17 -9
- package/src/__tests__/helpers/mock-kv.ts +9 -16
- package/src/__tests__/helpers/mock-stdin.ts +103 -0
- package/src/__tests__/helpers/run-in-tmp.ts +3 -0
- package/src/__tests__/https-options.test.ts +127 -0
- package/src/__tests__/index.test.ts +128 -11
- package/src/__tests__/jest.setup.ts +16 -4
- package/src/__tests__/kv.test.ts +364 -125
- package/src/__tests__/logout.test.ts +15 -13
- package/src/__tests__/pages.test.ts +82 -0
- package/src/__tests__/publish.test.ts +1838 -735
- package/src/__tests__/r2.test.ts +4 -4
- package/src/__tests__/route.test.ts +45 -0
- package/src/__tests__/secret.test.ts +167 -68
- package/src/__tests__/tail.test.ts +2 -4
- package/src/__tests__/whoami.test.tsx +21 -7
- package/src/bundle.ts +27 -5
- package/src/cfetch/internal.ts +11 -5
- package/src/config/README.md +6 -6
- package/src/config/config.ts +54 -101
- package/src/config/environment.ts +67 -1
- package/src/config/index.ts +13 -3
- package/src/config/validation-helpers.ts +207 -23
- package/src/config/validation.ts +344 -129
- package/src/{api/preview.ts → create-worker-preview.ts} +39 -20
- package/src/{api/form_data.ts → create-worker-upload-form.ts} +1 -1
- package/src/dev/dev.tsx +392 -0
- package/src/dev/local.tsx +232 -0
- package/src/dev/remote.tsx +203 -0
- package/src/dev/use-esbuild.ts +104 -0
- package/src/entry.ts +192 -9
- package/src/https-options.ts +122 -0
- package/src/index.tsx +577 -515
- package/src/inspect.ts +9 -1
- package/src/intl-polyfill.d.ts +139 -0
- package/src/{kv.tsx → kv.ts} +97 -53
- package/src/miniflare-cli/README.md +30 -0
- package/src/miniflare-cli/enum-keys.ts +17 -0
- package/src/miniflare-cli/index.ts +37 -0
- package/src/module-collection.ts +90 -2
- package/src/open-in-browser.ts +14 -2
- package/src/pages.tsx +26 -10
- package/src/proxy.ts +104 -40
- package/src/publish.ts +53 -71
- package/src/reporting.ts +25 -9
- package/src/selfsigned.d.ts +29 -0
- package/src/sites.tsx +68 -29
- package/src/user.tsx +163 -85
- package/src/{api/worker.ts → worker.ts} +1 -24
- package/wrangler-dist/cli.js +5933 -3842
- package/wrangler-dist/cli.js.map +3 -3
- package/src/__tests__/helpers/mock-user.ts +0 -27
- package/src/dev.tsx +0 -961
- package/src/guess-worker-format.ts +0 -68
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
declare module "selfsigned" {
|
|
2
|
+
import type { pki } from "node-forge";
|
|
3
|
+
|
|
4
|
+
export type Attributes = pki.CertificateField[];
|
|
5
|
+
|
|
6
|
+
export interface Options {
|
|
7
|
+
days?: number;
|
|
8
|
+
keySize?: number;
|
|
9
|
+
extensions?: unknown[];
|
|
10
|
+
algorithm?: string;
|
|
11
|
+
pkcs7?: boolean;
|
|
12
|
+
clientCertificate?: boolean;
|
|
13
|
+
clientCertificateCN?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Certificate {
|
|
17
|
+
private: string;
|
|
18
|
+
public: string;
|
|
19
|
+
cert: string;
|
|
20
|
+
fingerprint: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function generate(attrs?: Attributes, options?: Options): Certificate;
|
|
24
|
+
export function generate(
|
|
25
|
+
attrs: Attributes,
|
|
26
|
+
options: Options,
|
|
27
|
+
callback: (err: Error, certificate: Certificate) => void
|
|
28
|
+
): void;
|
|
29
|
+
}
|
package/src/sites.tsx
CHANGED
|
@@ -7,28 +7,27 @@ import {
|
|
|
7
7
|
listNamespaceKeys,
|
|
8
8
|
listNamespaces,
|
|
9
9
|
putBulkKeyValue,
|
|
10
|
+
deleteBulkKeyValue,
|
|
10
11
|
} from "./kv";
|
|
11
12
|
import type { Config } from "./config";
|
|
13
|
+
import type { KeyValue } from "./kv";
|
|
12
14
|
import type { XXHashAPI } from "xxhash-wasm";
|
|
13
15
|
|
|
14
16
|
/** Paths to always ignore. */
|
|
15
|
-
const ALWAYS_IGNORE = ["node_modules"];
|
|
16
|
-
const HIDDEN_FILES_TO_INCLUDE = [
|
|
17
|
+
const ALWAYS_IGNORE = new Set(["node_modules"]);
|
|
18
|
+
const HIDDEN_FILES_TO_INCLUDE = new Set([
|
|
17
19
|
".well-known", // See https://datatracker.ietf.org/doc/html/rfc8615
|
|
18
|
-
];
|
|
20
|
+
]);
|
|
19
21
|
|
|
20
22
|
async function* getFilesInFolder(dirPath: string): AsyncIterable<string> {
|
|
21
23
|
const files = await readdir(dirPath, { withFileTypes: true });
|
|
22
24
|
for (const file of files) {
|
|
23
25
|
// Skip files that we never want to process.
|
|
24
|
-
if (ALWAYS_IGNORE.
|
|
26
|
+
if (ALWAYS_IGNORE.has(file.name)) {
|
|
25
27
|
continue;
|
|
26
28
|
}
|
|
27
29
|
// Skip hidden files (starting with .) except for some special ones
|
|
28
|
-
if (
|
|
29
|
-
file.name.startsWith(".") &&
|
|
30
|
-
!HIDDEN_FILES_TO_INCLUDE.some((p) => file.name === p)
|
|
31
|
-
) {
|
|
30
|
+
if (file.name.startsWith(".") && !HIDDEN_FILES_TO_INCLUDE.has(file.name)) {
|
|
32
31
|
continue;
|
|
33
32
|
}
|
|
34
33
|
// TODO: follow symlinks??
|
|
@@ -128,36 +127,36 @@ export async function syncAssets(
|
|
|
128
127
|
const keys = new Set(result.map((x) => x.name));
|
|
129
128
|
|
|
130
129
|
const manifest: Record<string, string> = {};
|
|
131
|
-
const
|
|
132
|
-
key: string;
|
|
133
|
-
value: string;
|
|
134
|
-
base64: boolean;
|
|
135
|
-
}[] = [];
|
|
130
|
+
const toUpload: KeyValue[] = [];
|
|
136
131
|
|
|
137
132
|
const include = createPatternMatcher(siteAssets.includePatterns, false);
|
|
138
133
|
const exclude = createPatternMatcher(siteAssets.excludePatterns, true);
|
|
139
134
|
const hasher = await xxhash();
|
|
140
135
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
const assetDirectory = path.join(
|
|
137
|
+
siteAssets.baseDirectory,
|
|
138
|
+
siteAssets.assetDirectory
|
|
139
|
+
);
|
|
140
|
+
for await (const absAssetFile of getFilesInFolder(assetDirectory)) {
|
|
141
|
+
const assetFile = path.relative(siteAssets.baseDirectory, absAssetFile);
|
|
142
|
+
if (!include(assetFile)) {
|
|
144
143
|
continue;
|
|
145
144
|
}
|
|
146
|
-
if (exclude(
|
|
145
|
+
if (exclude(assetFile)) {
|
|
147
146
|
continue;
|
|
148
147
|
}
|
|
149
148
|
|
|
150
|
-
await validateAssetSize(
|
|
151
|
-
console.log(`reading ${
|
|
152
|
-
const content = await readFile(
|
|
149
|
+
await validateAssetSize(absAssetFile, assetFile);
|
|
150
|
+
console.log(`reading ${assetFile}...`);
|
|
151
|
+
const content = await readFile(absAssetFile, "base64");
|
|
153
152
|
|
|
154
|
-
const assetKey =
|
|
153
|
+
const assetKey = hashAsset(hasher, assetFile, content);
|
|
155
154
|
validateAssetKey(assetKey);
|
|
156
155
|
|
|
157
156
|
// now put each of the files into kv
|
|
158
157
|
if (!keys.has(assetKey)) {
|
|
159
158
|
console.log(`uploading as ${assetKey}...`);
|
|
160
|
-
|
|
159
|
+
toUpload.push({
|
|
161
160
|
key: assetKey,
|
|
162
161
|
value: content,
|
|
163
162
|
base64: true,
|
|
@@ -165,9 +164,26 @@ export async function syncAssets(
|
|
|
165
164
|
} else {
|
|
166
165
|
console.log(`skipping - already uploaded`);
|
|
167
166
|
}
|
|
168
|
-
|
|
167
|
+
|
|
168
|
+
// remove the key from the set so we know what we've already uploaded
|
|
169
|
+
keys.delete(assetKey);
|
|
170
|
+
manifest[path.relative(siteAssets.assetDirectory, absAssetFile)] = assetKey;
|
|
169
171
|
}
|
|
170
|
-
|
|
172
|
+
|
|
173
|
+
// keys now contains all the files we're deleting
|
|
174
|
+
for (const key of keys) {
|
|
175
|
+
console.log(`deleting ${key} from the asset store...`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await Promise.all([
|
|
179
|
+
// upload all the new assets
|
|
180
|
+
putBulkKeyValue(accountId, namespace, toUpload, () => {}),
|
|
181
|
+
// delete all the unused assets
|
|
182
|
+
deleteBulkKeyValue(accountId, namespace, Array.from(keys), () => {}),
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
console.log("↗️ Done syncing assets");
|
|
186
|
+
|
|
171
187
|
return { manifest, namespace };
|
|
172
188
|
}
|
|
173
189
|
|
|
@@ -183,11 +199,14 @@ function createPatternMatcher(
|
|
|
183
199
|
}
|
|
184
200
|
}
|
|
185
201
|
|
|
186
|
-
async function validateAssetSize(
|
|
187
|
-
|
|
202
|
+
async function validateAssetSize(
|
|
203
|
+
absFilePath: string,
|
|
204
|
+
relativeFilePath: string
|
|
205
|
+
) {
|
|
206
|
+
const { size } = await stat(absFilePath);
|
|
188
207
|
if (size > 25 * 1024 * 1024) {
|
|
189
208
|
throw new Error(
|
|
190
|
-
`File ${
|
|
209
|
+
`File ${relativeFilePath} is too big, it should be under 25 MiB. See https://developers.cloudflare.com/workers/platform/limits#kv-limits`
|
|
191
210
|
);
|
|
192
211
|
}
|
|
193
212
|
}
|
|
@@ -213,8 +232,23 @@ function urlSafe(filePath: string): string {
|
|
|
213
232
|
* Information about the assets that should be uploaded
|
|
214
233
|
*/
|
|
215
234
|
export interface AssetPaths {
|
|
235
|
+
/**
|
|
236
|
+
* Absolute path to the root of the project.
|
|
237
|
+
*
|
|
238
|
+
* This is the directory containing wrangler.toml or cwd if no config.
|
|
239
|
+
*/
|
|
216
240
|
baseDirectory: string;
|
|
241
|
+
/**
|
|
242
|
+
* The path to the assets directory, relative to the `baseDirectory`.
|
|
243
|
+
*/
|
|
244
|
+
assetDirectory: string;
|
|
245
|
+
/**
|
|
246
|
+
* An array of patterns that match files that should be uploaded.
|
|
247
|
+
*/
|
|
217
248
|
includePatterns: string[];
|
|
249
|
+
/**
|
|
250
|
+
* An array of patterns that match files that should not be uploaded.
|
|
251
|
+
*/
|
|
218
252
|
excludePatterns: string[];
|
|
219
253
|
}
|
|
220
254
|
|
|
@@ -227,13 +261,18 @@ export interface AssetPaths {
|
|
|
227
261
|
*/
|
|
228
262
|
export function getAssetPaths(
|
|
229
263
|
config: Config,
|
|
230
|
-
|
|
264
|
+
assetDirectory = config.site?.bucket,
|
|
231
265
|
includePatterns = config.site?.include ?? [],
|
|
232
266
|
excludePatterns = config.site?.exclude ?? []
|
|
233
267
|
): undefined | AssetPaths {
|
|
234
|
-
|
|
268
|
+
const baseDirectory = path.resolve(
|
|
269
|
+
path.dirname(config.configPath ?? "wrangler.toml")
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
return assetDirectory
|
|
235
273
|
? {
|
|
236
274
|
baseDirectory,
|
|
275
|
+
assetDirectory,
|
|
237
276
|
includePatterns,
|
|
238
277
|
excludePatterns,
|
|
239
278
|
}
|
package/src/user.tsx
CHANGED
|
@@ -207,7 +207,7 @@
|
|
|
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";
|
|
@@ -222,6 +222,7 @@ import { fetch } from "undici";
|
|
|
222
222
|
import { getCloudflareApiBaseUrl } from "./cfetch";
|
|
223
223
|
import { getEnvironmentVariableFactory } from "./environment-variables";
|
|
224
224
|
import openInBrowser from "./open-in-browser";
|
|
225
|
+
import { readFileSync } from "./parse";
|
|
225
226
|
import type { Item as SelectInputItem } from "ink-select-input/build/SelectInput";
|
|
226
227
|
import type { ParsedUrlQuery } from "node:querystring";
|
|
227
228
|
import type { Response } from "undici";
|
|
@@ -251,17 +252,45 @@ interface PKCECodes {
|
|
|
251
252
|
codeVerifier: string;
|
|
252
253
|
}
|
|
253
254
|
|
|
254
|
-
|
|
255
|
-
|
|
255
|
+
/**
|
|
256
|
+
* The module level state of the authentication flow.
|
|
257
|
+
*/
|
|
258
|
+
interface State extends AuthTokens {
|
|
256
259
|
authorizationCode?: string;
|
|
257
260
|
codeChallenge?: string;
|
|
258
261
|
codeVerifier?: string;
|
|
259
262
|
hasAuthCodeBeenExchangedForAccessToken?: boolean;
|
|
260
|
-
refreshToken?: RefreshToken; // persist
|
|
261
263
|
stateQueryParam?: string;
|
|
262
264
|
scopes?: Scope[];
|
|
263
265
|
}
|
|
264
266
|
|
|
267
|
+
/**
|
|
268
|
+
* The tokens related to authentication.
|
|
269
|
+
*/
|
|
270
|
+
interface AuthTokens {
|
|
271
|
+
accessToken?: AccessToken;
|
|
272
|
+
refreshToken?: RefreshToken;
|
|
273
|
+
/** @deprecated - this field was only provided by the deprecated `wrangler1 config` command. */
|
|
274
|
+
apiToken?: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The path to the config file that holds user authentication data,
|
|
279
|
+
* relative to the user's home directory.
|
|
280
|
+
*/
|
|
281
|
+
export const USER_AUTH_CONFIG_FILE = ".wrangler/config/default.toml";
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The data that may be read from the `USER_CONFIG_FILE`.
|
|
285
|
+
*/
|
|
286
|
+
export interface UserAuthConfig {
|
|
287
|
+
oauth_token?: string;
|
|
288
|
+
refresh_token?: string;
|
|
289
|
+
expiration_time?: string;
|
|
290
|
+
/** @deprecated - this field was only provided by the deprecated `wrangler1 config` command. */
|
|
291
|
+
api_token?: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
265
294
|
interface RefreshToken {
|
|
266
295
|
value: string;
|
|
267
296
|
}
|
|
@@ -285,6 +314,8 @@ const Scopes = {
|
|
|
285
314
|
"workers_scripts:write":
|
|
286
315
|
"See and change Cloudflare Workers scripts, durable objects, subdomains, triggers, and tail data.",
|
|
287
316
|
"workers_tail:read": "See Cloudflare Workers tail and script data.",
|
|
317
|
+
"pages:write":
|
|
318
|
+
"See and change Cloudflare Pages projects, settings and deployments.",
|
|
288
319
|
"zone:read": "Grants read level access to account zone.",
|
|
289
320
|
} as const;
|
|
290
321
|
|
|
@@ -309,60 +340,63 @@ const TOKEN_URL = "https://dash.cloudflare.com/oauth2/token";
|
|
|
309
340
|
const CALLBACK_URL = "http://localhost:8976/oauth/callback";
|
|
310
341
|
const REVOKE_URL = "https://dash.cloudflare.com/oauth2/revoke";
|
|
311
342
|
|
|
312
|
-
|
|
313
|
-
let initialised = false;
|
|
343
|
+
let LocalState: State = getAuthTokens();
|
|
314
344
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Compute the current auth tokens.
|
|
347
|
+
*/
|
|
348
|
+
function getAuthTokens(): AuthTokens {
|
|
319
349
|
// get refreshToken/accessToken from fs if exists
|
|
320
350
|
try {
|
|
321
351
|
// if the environment variable is available, use that
|
|
322
352
|
const apiTokenFromEnv = getCloudflareAPITokenFromEnv();
|
|
323
353
|
if (apiTokenFromEnv) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
354
|
+
return {
|
|
355
|
+
accessToken: {
|
|
356
|
+
value: apiTokenFromEnv,
|
|
357
|
+
expiry: "3021-12-31T23:59:59+00:00",
|
|
358
|
+
},
|
|
327
359
|
};
|
|
328
|
-
initialised = true;
|
|
329
|
-
return;
|
|
330
360
|
}
|
|
331
361
|
|
|
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
|
-
};
|
|
362
|
+
// otherwise try loading from the user auth config file.
|
|
363
|
+
const { oauth_token, refresh_token, expiration_time, api_token } =
|
|
364
|
+
readAuthConfigFile();
|
|
365
|
+
|
|
342
366
|
if (oauth_token) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
367
|
+
return {
|
|
368
|
+
accessToken: {
|
|
369
|
+
value: oauth_token,
|
|
370
|
+
// If there is no `expiration_time` field then set it to an old date, to cause it to expire immediately.
|
|
371
|
+
expiry: expiration_time ?? "2000-01-01:00:00:00+00:00",
|
|
372
|
+
},
|
|
373
|
+
refreshToken: { value: refresh_token ?? "" },
|
|
374
|
+
};
|
|
375
|
+
} else if (api_token) {
|
|
376
|
+
return { apiToken: api_token };
|
|
377
|
+
} else {
|
|
378
|
+
return {};
|
|
347
379
|
}
|
|
348
|
-
} catch
|
|
349
|
-
|
|
350
|
-
// console.error(err);
|
|
380
|
+
} catch {
|
|
381
|
+
return {};
|
|
351
382
|
}
|
|
352
|
-
initialised = true;
|
|
353
383
|
}
|
|
354
384
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
);
|
|
361
|
-
}
|
|
385
|
+
/**
|
|
386
|
+
* Run the initialisation of the auth state, in the case that something changed.
|
|
387
|
+
*/
|
|
388
|
+
export function reinitialiseAuthTokens(): void {
|
|
389
|
+
LocalState = getAuthTokens();
|
|
362
390
|
}
|
|
363
391
|
|
|
364
392
|
export function getAPIToken(): string | undefined {
|
|
365
|
-
|
|
393
|
+
if (LocalState.apiToken) {
|
|
394
|
+
console.warn(
|
|
395
|
+
"It looks like you have used Wrangler 1's `config` command to login with an API token.\n" +
|
|
396
|
+
"This is no longer supported in the current version of Wrangler.\n" +
|
|
397
|
+
"If you wish to authenticate via an API token then please set the `CLOUDFLARE_API_TOKEN` environment variable."
|
|
398
|
+
);
|
|
399
|
+
}
|
|
366
400
|
return LocalState.accessToken?.value;
|
|
367
401
|
}
|
|
368
402
|
|
|
@@ -576,8 +610,8 @@ export async function getAuthURL(scopes = ScopeKeys): Promise<string> {
|
|
|
576
610
|
`?response_type=code&` +
|
|
577
611
|
`client_id=${encodeURIComponent(CLIENT_ID)}&` +
|
|
578
612
|
`redirect_uri=${encodeURIComponent(CALLBACK_URL)}&` +
|
|
579
|
-
//
|
|
580
|
-
`scope=${encodeURIComponent(scopes
|
|
613
|
+
// we add offline_access manually for every request
|
|
614
|
+
`scope=${encodeURIComponent([...scopes, "offline_access"].join(" "))}&` +
|
|
581
615
|
`state=${stateQueryParam}&` +
|
|
582
616
|
`code_challenge=${encodeURIComponent(codeChallenge)}&` +
|
|
583
617
|
`code_challenge_method=S256`
|
|
@@ -798,39 +832,55 @@ function generateRandomState(lengthOfState: number): string {
|
|
|
798
832
|
.join("");
|
|
799
833
|
}
|
|
800
834
|
|
|
801
|
-
|
|
802
|
-
|
|
835
|
+
export function writeAuthConfigFile(config: UserAuthConfig) {
|
|
836
|
+
mkdirSync(path.join(os.homedir(), ".wrangler/config/"), {
|
|
803
837
|
recursive: true,
|
|
804
838
|
});
|
|
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
|
-
`,
|
|
839
|
+
writeFileSync(
|
|
840
|
+
path.join(os.homedir(), USER_AUTH_CONFIG_FILE),
|
|
841
|
+
TOML.stringify(config as TOML.JsonMap),
|
|
812
842
|
{ encoding: "utf-8" }
|
|
813
843
|
);
|
|
814
844
|
}
|
|
815
845
|
|
|
846
|
+
function readAuthConfigFile(): UserAuthConfig {
|
|
847
|
+
const toml = TOML.parse(
|
|
848
|
+
readFileSync(path.join(os.homedir(), USER_AUTH_CONFIG_FILE))
|
|
849
|
+
);
|
|
850
|
+
return toml;
|
|
851
|
+
}
|
|
852
|
+
|
|
816
853
|
type LoginProps = {
|
|
817
854
|
scopes?: Scope[];
|
|
818
855
|
};
|
|
819
856
|
|
|
820
|
-
export async function loginOrRefreshIfRequired(
|
|
857
|
+
export async function loginOrRefreshIfRequired(
|
|
858
|
+
isInteractive = true
|
|
859
|
+
): Promise<boolean> {
|
|
821
860
|
// TODO: if there already is a token, then try refreshing
|
|
822
861
|
// TODO: ask permission before opening browser
|
|
823
|
-
if (!
|
|
824
|
-
//
|
|
825
|
-
|
|
862
|
+
if (!getAPIToken()) {
|
|
863
|
+
// Not logged in.
|
|
864
|
+
// If we are not interactive, we cannot ask the user to login
|
|
865
|
+
return isInteractive && (await login());
|
|
826
866
|
} else if (isAccessTokenExpired()) {
|
|
827
|
-
|
|
867
|
+
// We're logged in, but the refresh token seems to have expired,
|
|
868
|
+
// so let's try to refresh it
|
|
869
|
+
const didRefresh = await refreshToken();
|
|
870
|
+
if (didRefresh) {
|
|
871
|
+
// The token was refreshed, so we're done here
|
|
872
|
+
return true;
|
|
873
|
+
} else {
|
|
874
|
+
// If the refresh token isn't valid, then we ask the user to login again
|
|
875
|
+
return isInteractive && (await login());
|
|
876
|
+
}
|
|
828
877
|
} else {
|
|
829
878
|
return true;
|
|
830
879
|
}
|
|
831
880
|
}
|
|
832
881
|
|
|
833
882
|
export async function login(props?: LoginProps): Promise<boolean> {
|
|
883
|
+
console.log("Attempting to login via OAuth...");
|
|
834
884
|
const urlToOpen = await getAuthURL(props?.scopes);
|
|
835
885
|
await openInBrowser(urlToOpen);
|
|
836
886
|
let server: http.Server;
|
|
@@ -889,8 +939,12 @@ export async function login(props?: LoginProps): Promise<boolean> {
|
|
|
889
939
|
finish(false, new ErrorNoAuthCode());
|
|
890
940
|
return;
|
|
891
941
|
} else {
|
|
892
|
-
const
|
|
893
|
-
|
|
942
|
+
const exchange = await exchangeAuthCodeForAccessToken();
|
|
943
|
+
writeAuthConfigFile({
|
|
944
|
+
oauth_token: exchange.token?.value ?? "",
|
|
945
|
+
expiration_time: exchange.token?.expiry,
|
|
946
|
+
refresh_token: exchange.refreshToken?.value,
|
|
947
|
+
});
|
|
894
948
|
res.writeHead(307, {
|
|
895
949
|
Location:
|
|
896
950
|
"https://welcome.developers.workers.dev/wrangler-oauth-consent-granted",
|
|
@@ -898,9 +952,7 @@ export async function login(props?: LoginProps): Promise<boolean> {
|
|
|
898
952
|
res.end(() => {
|
|
899
953
|
finish(true);
|
|
900
954
|
});
|
|
901
|
-
console.log(
|
|
902
|
-
`Successfully configured. You can find your configuration file at: ${os.homedir()}/.wrangler/config/default.toml`
|
|
903
|
-
);
|
|
955
|
+
console.log(`Successfully logged in.`);
|
|
904
956
|
|
|
905
957
|
return;
|
|
906
958
|
}
|
|
@@ -917,18 +969,22 @@ export async function login(props?: LoginProps): Promise<boolean> {
|
|
|
917
969
|
/**
|
|
918
970
|
* Checks to see if the access token has expired.
|
|
919
971
|
*/
|
|
920
|
-
|
|
921
|
-
throwIfNotInitialised();
|
|
972
|
+
function isAccessTokenExpired(): boolean {
|
|
922
973
|
const { accessToken } = LocalState;
|
|
923
974
|
return Boolean(accessToken && new Date() >= new Date(accessToken.expiry));
|
|
924
975
|
}
|
|
925
976
|
|
|
926
|
-
|
|
927
|
-
throwIfNotInitialised();
|
|
977
|
+
async function refreshToken(): Promise<boolean> {
|
|
928
978
|
// refresh
|
|
929
979
|
try {
|
|
930
|
-
const
|
|
931
|
-
|
|
980
|
+
const {
|
|
981
|
+
token: { value: oauth_token, expiry: expiration_time } = {
|
|
982
|
+
value: "",
|
|
983
|
+
expiry: "",
|
|
984
|
+
},
|
|
985
|
+
refreshToken: { value: refresh_token } = {},
|
|
986
|
+
} = await exchangeRefreshTokenForAccessToken();
|
|
987
|
+
writeAuthConfigFile({ oauth_token, expiration_time, refresh_token });
|
|
932
988
|
return true;
|
|
933
989
|
} catch (err) {
|
|
934
990
|
console.error(err);
|
|
@@ -937,10 +993,28 @@ export async function refreshToken(): Promise<boolean> {
|
|
|
937
993
|
}
|
|
938
994
|
|
|
939
995
|
export async function logout(): Promise<void> {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
996
|
+
if (!LocalState.accessToken) {
|
|
997
|
+
if (!LocalState.refreshToken) {
|
|
998
|
+
console.log("Not logged in, exiting...");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const body =
|
|
1003
|
+
`client_id=${encodeURIComponent(CLIENT_ID)}&` +
|
|
1004
|
+
`token_type_hint=refresh_token&` +
|
|
1005
|
+
`token=${encodeURIComponent(LocalState.refreshToken?.value || "")}`;
|
|
1006
|
+
|
|
1007
|
+
const response = await fetch(REVOKE_URL, {
|
|
1008
|
+
method: "POST",
|
|
1009
|
+
body,
|
|
1010
|
+
headers: {
|
|
1011
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
await response.text(); // blank text? would be nice if it was something meaningful
|
|
1015
|
+
console.log(
|
|
1016
|
+
"💁 Wrangler is configured with an OAuth token. The token has been successfully revoked"
|
|
1017
|
+
);
|
|
944
1018
|
}
|
|
945
1019
|
const body =
|
|
946
1020
|
`client_id=${encodeURIComponent(CLIENT_ID)}&` +
|
|
@@ -955,19 +1029,12 @@ export async function logout(): Promise<void> {
|
|
|
955
1029
|
},
|
|
956
1030
|
});
|
|
957
1031
|
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
|
-
);
|
|
1032
|
+
rmSync(path.join(os.homedir(), USER_AUTH_CONFIG_FILE));
|
|
1033
|
+
console.log(`Successfully logged out.`);
|
|
966
1034
|
}
|
|
967
1035
|
|
|
968
|
-
export function listScopes(): void {
|
|
969
|
-
|
|
970
|
-
console.log("💁 Available scopes:");
|
|
1036
|
+
export function listScopes(message = "💁 Available scopes:"): void {
|
|
1037
|
+
console.log(message);
|
|
971
1038
|
const data = ScopeKeys.map((scope: Scope) => ({
|
|
972
1039
|
Scope: scope,
|
|
973
1040
|
Description: Scopes[scope],
|
|
@@ -976,7 +1043,9 @@ export function listScopes(): void {
|
|
|
976
1043
|
// TODO: maybe a good idea to show usage here
|
|
977
1044
|
}
|
|
978
1045
|
|
|
979
|
-
export async function getAccountId(
|
|
1046
|
+
export async function getAccountId(
|
|
1047
|
+
isInteractive = true
|
|
1048
|
+
): Promise<string | undefined> {
|
|
980
1049
|
const apiToken = getAPIToken();
|
|
981
1050
|
if (!apiToken) return;
|
|
982
1051
|
|
|
@@ -1007,7 +1076,7 @@ export async function getAccountId() {
|
|
|
1007
1076
|
if (responseJSON.success === true) {
|
|
1008
1077
|
if (responseJSON.result.length === 1) {
|
|
1009
1078
|
accountId = responseJSON.result[0].account.id;
|
|
1010
|
-
} else {
|
|
1079
|
+
} else if (isInteractive) {
|
|
1011
1080
|
accountId = await new Promise((resolve) => {
|
|
1012
1081
|
const accounts = responseJSON.result.map((x) => x.account);
|
|
1013
1082
|
const { unmount } = render(
|
|
@@ -1020,6 +1089,15 @@ export async function getAccountId() {
|
|
|
1020
1089
|
/>
|
|
1021
1090
|
);
|
|
1022
1091
|
});
|
|
1092
|
+
} else {
|
|
1093
|
+
throw new Error(
|
|
1094
|
+
"More than one account available but unable to select one in non-interactive mode.\n" +
|
|
1095
|
+
`Please set the appropriate \`account_id\` in your \`wrangler.toml\` file.\n` +
|
|
1096
|
+
`Available accounts are ("<name>" - "<id>"):\n` +
|
|
1097
|
+
responseJSON.result
|
|
1098
|
+
.map((x) => ` "${x.account.name}" - "${x.account.id}")`)
|
|
1099
|
+
.join("\n")
|
|
1100
|
+
);
|
|
1023
1101
|
}
|
|
1024
1102
|
}
|
|
1025
1103
|
return accountId;
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import { fetch } from "undici";
|
|
2
|
-
import { previewToken } from "./preview";
|
|
3
|
-
import type { CfPreviewToken } from "./preview";
|
|
4
|
-
|
|
5
1
|
/**
|
|
6
2
|
* A Cloudflare account.
|
|
7
3
|
*/
|
|
@@ -163,24 +159,5 @@ export interface CfWorkerInit {
|
|
|
163
159
|
export interface CfWorkerContext {
|
|
164
160
|
env: string | undefined;
|
|
165
161
|
legacyEnv: boolean | undefined;
|
|
166
|
-
zone: string | undefined;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* A stub to create a Cloudflare Worker preview.
|
|
171
|
-
*
|
|
172
|
-
* @example
|
|
173
|
-
* const {value, host} = await createWorker(init, acct);
|
|
174
|
-
*/
|
|
175
|
-
export async function createWorker(
|
|
176
|
-
init: CfWorkerInit,
|
|
177
|
-
account: CfAccount,
|
|
178
|
-
ctx: CfWorkerContext
|
|
179
|
-
): Promise<CfPreviewToken> {
|
|
180
|
-
const token = await previewToken(account, init, ctx);
|
|
181
|
-
const response = await fetch(token.prewarmUrl.href, { method: "POST" });
|
|
182
|
-
if (!response.ok) {
|
|
183
|
-
// console.error("worker failed to prewarm: ", response.statusText);
|
|
184
|
-
}
|
|
185
|
-
return token;
|
|
162
|
+
zone: { id: string; host: string } | undefined;
|
|
186
163
|
}
|