wrangler 0.0.0-e043479 → 0.0.0-e0b9366

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 (51) hide show
  1. package/package.json +10 -4
  2. package/pages/functions/filepath-routing.test.ts +125 -25
  3. package/pages/functions/filepath-routing.ts +6 -3
  4. package/src/__tests__/configuration.test.ts +479 -114
  5. package/src/__tests__/dev.test.tsx +162 -8
  6. package/src/__tests__/dev2.test.tsx +152 -0
  7. package/src/__tests__/guess-worker-format.test.ts +9 -17
  8. package/src/__tests__/helpers/mock-account-id.ts +4 -0
  9. package/src/__tests__/helpers/mock-cfetch.ts +3 -3
  10. package/src/__tests__/helpers/mock-kv.ts +9 -16
  11. package/src/__tests__/helpers/run-in-tmp.ts +3 -0
  12. package/src/__tests__/https-options.test.ts +127 -0
  13. package/src/__tests__/index.test.ts +125 -0
  14. package/src/__tests__/jest.setup.ts +1 -1
  15. package/src/__tests__/kv.test.ts +27 -53
  16. package/src/__tests__/logout.test.ts +15 -13
  17. package/src/__tests__/pages.test.ts +44 -0
  18. package/src/__tests__/publish.test.ts +504 -32
  19. package/src/__tests__/route.test.ts +45 -0
  20. package/src/__tests__/whoami.test.tsx +21 -7
  21. package/src/bundle.ts +20 -11
  22. package/src/cfetch/internal.ts +11 -5
  23. package/src/config/config.ts +53 -44
  24. package/src/config/validation-helpers.ts +110 -12
  25. package/src/config/validation.ts +59 -41
  26. package/src/{api/preview.ts → create-worker-preview.ts} +23 -4
  27. package/src/{api/form_data.ts → create-worker-upload-form.ts} +1 -1
  28. package/src/dev/dev.tsx +357 -0
  29. package/src/dev/local.tsx +241 -0
  30. package/src/dev/remote.tsx +201 -0
  31. package/src/dev/use-esbuild.ts +100 -0
  32. package/src/entry.ts +111 -9
  33. package/src/https-options.ts +122 -0
  34. package/src/index.tsx +171 -109
  35. package/src/inspect.ts +9 -1
  36. package/src/{kv.tsx → kv.ts} +0 -0
  37. package/src/module-collection.ts +85 -2
  38. package/src/open-in-browser.ts +14 -2
  39. package/src/pages.tsx +1 -1
  40. package/src/proxy.ts +102 -40
  41. package/src/publish.ts +41 -50
  42. package/src/reporting.ts +25 -9
  43. package/src/selfsigned.d.ts +29 -0
  44. package/src/sites.tsx +51 -16
  45. package/src/user.tsx +132 -77
  46. package/src/{api/worker.ts → worker.ts} +0 -23
  47. package/wrangler-dist/cli.js +5012 -3107
  48. package/wrangler-dist/cli.js.map +3 -3
  49. package/src/__tests__/helpers/mock-user.ts +0 -27
  50. package/src/dev.tsx +0 -960
  51. 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
@@ -4,32 +4,31 @@ import ignore from "ignore";
4
4
  import xxhash from "xxhash-wasm";
5
5
  import {
6
6
  createNamespace,
7
+ getKeyValue,
7
8
  listNamespaceKeys,
8
9
  listNamespaces,
9
10
  putBulkKeyValue,
10
11
  } from "./kv";
11
12
  import type { Config } from "./config";
12
- import type { KeyValue } from "./kv";
13
+ import type { KeyValue, NamespaceKeyInfo } from "./kv";
13
14
  import type { XXHashAPI } from "xxhash-wasm";
14
15
 
15
16
  /** Paths to always ignore. */
16
- const ALWAYS_IGNORE = ["node_modules"];
17
- const HIDDEN_FILES_TO_INCLUDE = [
17
+ const ALWAYS_IGNORE = new Set(["node_modules"]);
18
+ const HIDDEN_FILES_TO_INCLUDE = new Set([
18
19
  ".well-known", // See https://datatracker.ietf.org/doc/html/rfc8615
19
- ];
20
+ ]);
21
+ const FIVE_MINUTES = 60 * 5; // expressed in seconds, since that's what the api expects
20
22
 
21
23
  async function* getFilesInFolder(dirPath: string): AsyncIterable<string> {
22
24
  const files = await readdir(dirPath, { withFileTypes: true });
23
25
  for (const file of files) {
24
26
  // Skip files that we never want to process.
25
- if (ALWAYS_IGNORE.some((p) => file.name === p)) {
27
+ if (ALWAYS_IGNORE.has(file.name)) {
26
28
  continue;
27
29
  }
28
30
  // Skip hidden files (starting with .) except for some special ones
29
- if (
30
- file.name.startsWith(".") &&
31
- !HIDDEN_FILES_TO_INCLUDE.some((p) => file.name === p)
32
- ) {
31
+ if (file.name.startsWith(".") && !HIDDEN_FILES_TO_INCLUDE.has(file.name)) {
33
32
  continue;
34
33
  }
35
34
  // TODO: follow symlinks??
@@ -126,16 +125,18 @@ export async function syncAssets(
126
125
 
127
126
  // let's get all the keys in this namespace
128
127
  const result = await listNamespaceKeys(accountId, namespace);
129
- const keys = new Set(result.map((x) => x.name));
128
+ const keyMap = result.reduce<Record<string, NamespaceKeyInfo>>(
129
+ (km, key) => Object.assign(km, { [key.name]: key }),
130
+ {}
131
+ );
130
132
 
131
133
  const manifest: Record<string, string> = {};
132
- const upload: KeyValue[] = [];
134
+ const toUpload: KeyValue[] = [];
133
135
 
134
136
  const include = createPatternMatcher(siteAssets.includePatterns, false);
135
137
  const exclude = createPatternMatcher(siteAssets.excludePatterns, true);
136
138
  const hasher = await xxhash();
137
139
 
138
- // TODO: this can be more efficient by parallelising
139
140
  for await (const file of getFilesInFolder(siteAssets.baseDirectory)) {
140
141
  if (!include(file)) {
141
142
  continue;
@@ -148,13 +149,13 @@ export async function syncAssets(
148
149
  console.log(`reading ${file}...`);
149
150
  const content = await readFile(file, "base64");
150
151
 
151
- const assetKey = await hashAsset(hasher, file, content);
152
+ const assetKey = hashAsset(hasher, file, content);
152
153
  validateAssetKey(assetKey);
153
154
 
154
155
  // now put each of the files into kv
155
- if (!keys.has(assetKey)) {
156
+ if (!(assetKey in keyMap) || keyMap[assetKey].expiration) {
156
157
  console.log(`uploading as ${assetKey}...`);
157
- upload.push({
158
+ toUpload.push({
158
159
  key: assetKey,
159
160
  value: content,
160
161
  base64: true,
@@ -162,9 +163,43 @@ export async function syncAssets(
162
163
  } else {
163
164
  console.log(`skipping - already uploaded`);
164
165
  }
166
+ // remove the key from the set so we know we've seen it
167
+ delete keyMap[assetKey];
165
168
  manifest[path.relative(siteAssets.baseDirectory, file)] = assetKey;
166
169
  }
167
- await putBulkKeyValue(accountId, namespace, upload, () => {});
170
+
171
+ // `keyMap` now contains the assets that we need to expire
172
+
173
+ let toExpire: KeyValue[] = [];
174
+ for (const asset of Object.values(keyMap)) {
175
+ if (!asset.expiration) {
176
+ console.log(`expiring unused ${asset.name}...`);
177
+ toExpire.push({
178
+ key: asset.name,
179
+ value: "", // we'll fill all the values in one go
180
+ // we use expiration_ttl, since we can't trust the time
181
+ // that a deploy source might provide (eg - https://github.com/cloudflare/wrangler/issues/2224)
182
+ expiration_ttl: FIVE_MINUTES,
183
+ base64: true,
184
+ });
185
+ }
186
+ }
187
+
188
+ // TODO: batch these in groups if it causes problems
189
+ toExpire = await Promise.all(
190
+ toExpire.map(async (asset) => ({
191
+ ...asset,
192
+ // it would be great if we didn't have to do this fetch at all
193
+ value: await getKeyValue(accountId, namespace, asset.key),
194
+ }))
195
+ );
196
+
197
+ await putBulkKeyValue(
198
+ accountId,
199
+ namespace,
200
+ toUpload.concat(toExpire),
201
+ () => {}
202
+ );
168
203
  return { manifest, namespace };
169
204
  }
170
205
 
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 { readFile, writeFile, rm, mkdir } from "node:fs/promises";
210
+ import { writeFileSync, mkdirSync, rmSync } from "node:fs";
211
211
  import http from "node:http";
212
212
  import os from "node:os";
213
213
  import path from "node:path";
@@ -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
- interface State {
255
- accessToken?: AccessToken; // persist
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
- const LocalState: State = {};
313
- let initialised = false;
343
+ let LocalState: State = getAuthTokens();
314
344
 
315
- // we do this because we have some async stuff
316
- // TODO: this should just happen in the top level
317
- // and we should figure out how to do top level await
318
- export async function initialise(): Promise<void> {
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
- LocalState.accessToken = {
325
- value: apiTokenFromEnv,
326
- expiry: "3021-12-31T23:59:59+00:00",
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
- const toml = TOML.parse(
333
- await readFile(path.join(os.homedir(), ".wrangler/config/default.toml"), {
334
- encoding: "utf-8",
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
- LocalState.accessToken = { value: oauth_token, expiry: expiration_time };
344
- }
345
- if (refresh_token) {
346
- LocalState.refreshToken = { value: refresh_token };
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 (err) {
349
- // no config yet, let's chill
350
- // console.error(err);
380
+ } catch {
381
+ return {};
351
382
  }
352
- initialised = true;
353
383
  }
354
384
 
355
- // ugh. TODO: see fix from above.
356
- function throwIfNotInitialised() {
357
- if (initialised === false) {
358
- throw new Error(
359
- "did you forget to call initialise() from the user module?"
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
- throwIfNotInitialised();
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
 
@@ -798,21 +832,24 @@ function generateRandomState(lengthOfState: number): string {
798
832
  .join("");
799
833
  }
800
834
 
801
- async function writeToConfigFile(tokenData: AccessContext) {
802
- await mkdir(path.join(os.homedir(), ".wrangler/config/"), {
835
+ export function writeAuthConfigFile(config: UserAuthConfig) {
836
+ mkdirSync(path.join(os.homedir(), ".wrangler/config/"), {
803
837
  recursive: true,
804
838
  });
805
- await writeFile(
806
- path.join(os.homedir(), ".wrangler/config/default.toml"),
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
  };
@@ -822,7 +859,7 @@ export async function loginOrRefreshIfRequired(
822
859
  ): Promise<boolean> {
823
860
  // TODO: if there already is a token, then try refreshing
824
861
  // TODO: ask permission before opening browser
825
- if (!LocalState.accessToken) {
862
+ if (!getAPIToken()) {
826
863
  // Not logged in.
827
864
  // If we are not interactive, we cannot ask the user to login
828
865
  return isInteractive && (await login());
@@ -834,6 +871,7 @@ export async function loginOrRefreshIfRequired(
834
871
  }
835
872
 
836
873
  export async function login(props?: LoginProps): Promise<boolean> {
874
+ console.log("Attempting to login via OAuth...");
837
875
  const urlToOpen = await getAuthURL(props?.scopes);
838
876
  await openInBrowser(urlToOpen);
839
877
  let server: http.Server;
@@ -892,8 +930,12 @@ export async function login(props?: LoginProps): Promise<boolean> {
892
930
  finish(false, new ErrorNoAuthCode());
893
931
  return;
894
932
  } else {
895
- const tokenData = await exchangeAuthCodeForAccessToken();
896
- await writeToConfigFile(tokenData);
933
+ const exchange = await exchangeAuthCodeForAccessToken();
934
+ writeAuthConfigFile({
935
+ oauth_token: exchange.token?.value ?? "",
936
+ expiration_time: exchange.token?.expiry,
937
+ refresh_token: exchange.refreshToken?.value,
938
+ });
897
939
  res.writeHead(307, {
898
940
  Location:
899
941
  "https://welcome.developers.workers.dev/wrangler-oauth-consent-granted",
@@ -901,9 +943,7 @@ export async function login(props?: LoginProps): Promise<boolean> {
901
943
  res.end(() => {
902
944
  finish(true);
903
945
  });
904
- console.log(
905
- `Successfully configured. You can find your configuration file at: ${os.homedir()}/.wrangler/config/default.toml`
906
- );
946
+ console.log(`Successfully logged in.`);
907
947
 
908
948
  return;
909
949
  }
@@ -920,18 +960,22 @@ export async function login(props?: LoginProps): Promise<boolean> {
920
960
  /**
921
961
  * Checks to see if the access token has expired.
922
962
  */
923
- export function isAccessTokenExpired(): boolean {
924
- throwIfNotInitialised();
963
+ function isAccessTokenExpired(): boolean {
925
964
  const { accessToken } = LocalState;
926
965
  return Boolean(accessToken && new Date() >= new Date(accessToken.expiry));
927
966
  }
928
967
 
929
- export async function refreshToken(): Promise<boolean> {
930
- throwIfNotInitialised();
968
+ async function refreshToken(): Promise<boolean> {
931
969
  // refresh
932
970
  try {
933
- const refreshed = await exchangeRefreshTokenForAccessToken();
934
- await writeToConfigFile(refreshed);
971
+ const {
972
+ token: { value: oauth_token, expiry: expiration_time } = {
973
+ value: "",
974
+ expiry: "",
975
+ },
976
+ refreshToken: { value: refresh_token } = {},
977
+ } = await exchangeRefreshTokenForAccessToken();
978
+ writeAuthConfigFile({ oauth_token, expiration_time, refresh_token });
935
979
  return true;
936
980
  } catch (err) {
937
981
  console.error(err);
@@ -940,10 +984,28 @@ export async function refreshToken(): Promise<boolean> {
940
984
  }
941
985
 
942
986
  export async function logout(): Promise<void> {
943
- throwIfNotInitialised();
944
- if (!LocalState.refreshToken) {
945
- console.log("Not logged in, exiting...");
946
- return;
987
+ if (!LocalState.accessToken) {
988
+ if (!LocalState.refreshToken) {
989
+ console.log("Not logged in, exiting...");
990
+ return;
991
+ }
992
+
993
+ const body =
994
+ `client_id=${encodeURIComponent(CLIENT_ID)}&` +
995
+ `token_type_hint=refresh_token&` +
996
+ `token=${encodeURIComponent(LocalState.refreshToken?.value || "")}`;
997
+
998
+ const response = await fetch(REVOKE_URL, {
999
+ method: "POST",
1000
+ body,
1001
+ headers: {
1002
+ "Content-Type": "application/x-www-form-urlencoded",
1003
+ },
1004
+ });
1005
+ await response.text(); // blank text? would be nice if it was something meaningful
1006
+ console.log(
1007
+ "💁 Wrangler is configured with an OAuth token. The token has been successfully revoked"
1008
+ );
947
1009
  }
948
1010
  const body =
949
1011
  `client_id=${encodeURIComponent(CLIENT_ID)}&` +
@@ -958,19 +1020,12 @@ export async function logout(): Promise<void> {
958
1020
  },
959
1021
  });
960
1022
  await response.text(); // blank text? would be nice if it was something meaningful
961
- console.log(
962
- "💁 Wrangler is configured with an OAuth token. The token has been successfully revoked"
963
- );
964
- // delete the file
965
- await rm(path.join(os.homedir(), ".wrangler/config/default.toml"));
966
- console.log(
967
- `Removing ${os.homedir()}/.wrangler/config/default.toml.. success!`
968
- );
1023
+ rmSync(path.join(os.homedir(), USER_AUTH_CONFIG_FILE));
1024
+ console.log(`Successfully logged out.`);
969
1025
  }
970
1026
 
971
- export function listScopes(): void {
972
- throwIfNotInitialised();
973
- console.log("💁 Available scopes:");
1027
+ export function listScopes(message = "💁 Available scopes:"): void {
1028
+ console.log(message);
974
1029
  const data = ScopeKeys.map((scope: Scope) => ({
975
1030
  Scope: scope,
976
1031
  Description: Scopes[scope],
@@ -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
  */
@@ -165,22 +161,3 @@ export interface CfWorkerContext {
165
161
  legacyEnv: boolean | undefined;
166
162
  zone: { id: string; host: string } | undefined;
167
163
  }
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;
186
- }