wrangler 0.0.0-fb43821 → 0.0.0-fe5dcfd

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 (41) hide show
  1. package/package.json +25 -10
  2. package/pages/functions/buildWorker.ts +62 -0
  3. package/pages/functions/filepath-routing.test.ts +39 -0
  4. package/pages/functions/filepath-routing.ts +228 -0
  5. package/pages/functions/identifiers.ts +78 -0
  6. package/pages/functions/routes.ts +158 -0
  7. package/pages/functions/template-worker.ts +136 -0
  8. package/src/__tests__/dev.test.tsx +67 -0
  9. package/src/__tests__/index.test.ts +42 -175
  10. package/src/__tests__/jest.setup.ts +22 -0
  11. package/src/__tests__/kv.test.ts +1199 -0
  12. package/src/__tests__/mock-cfetch.ts +173 -0
  13. package/src/__tests__/mock-dialogs.ts +65 -0
  14. package/src/__tests__/publish.test.ts +737 -0
  15. package/src/__tests__/run-in-tmp.ts +19 -0
  16. package/src/__tests__/run-wrangler.ts +32 -0
  17. package/src/__tests__/whoami.test.tsx +130 -0
  18. package/src/api/form_data.ts +66 -76
  19. package/src/api/preview.ts +4 -4
  20. package/src/api/worker.ts +38 -44
  21. package/src/cfetch/index.ts +110 -0
  22. package/src/cfetch/internal.ts +71 -0
  23. package/src/cli.ts +6 -7
  24. package/src/config.ts +634 -110
  25. package/src/dev.tsx +275 -272
  26. package/src/dialogs.tsx +2 -2
  27. package/src/index.tsx +504 -403
  28. package/src/inspect.ts +543 -0
  29. package/src/kv.tsx +109 -44
  30. package/src/pages.tsx +444 -222
  31. package/src/proxy.ts +302 -0
  32. package/src/publish.ts +123 -87
  33. package/src/sites.tsx +169 -41
  34. package/src/tail.tsx +5 -3
  35. package/src/user.tsx +64 -74
  36. package/src/whoami.tsx +69 -0
  37. package/wrangler-dist/cli.js +42792 -42619
  38. package/wrangler-dist/cli.js.map +3 -3
  39. package/src/__tests__/mock-cfetch.js +0 -42
  40. package/src/api/inspect.ts +0 -430
  41. package/src/cfetch.ts +0 -72
@@ -0,0 +1,19 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+ import * as fs from "fs";
4
+
5
+ const originalCwd = process.cwd();
6
+
7
+ export function runInTempDir() {
8
+ let tmpDir: string;
9
+
10
+ beforeEach(() => {
11
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wrangler-tests"));
12
+ process.chdir(tmpDir);
13
+ });
14
+
15
+ afterEach(() => {
16
+ process.chdir(originalCwd);
17
+ fs.rmSync(tmpDir, { recursive: true });
18
+ });
19
+ }
@@ -0,0 +1,32 @@
1
+ import { main } from "../index";
2
+
3
+ /**
4
+ * Run the wrangler CLI tool in tests, capturing the logging output.
5
+ */
6
+ export async function runWrangler(cmd?: string) {
7
+ const logSpy = jest.spyOn(console, "log").mockImplementation();
8
+ const errorSpy = jest.spyOn(console, "error").mockImplementation();
9
+ const warnSpy = jest.spyOn(console, "warn").mockImplementation();
10
+ try {
11
+ let error: unknown = undefined;
12
+ try {
13
+ await main(cmd?.split(" ") ?? []);
14
+ } catch (e) {
15
+ error = e;
16
+ }
17
+ return {
18
+ error,
19
+ stdout: captureCalls(logSpy),
20
+ stderr: captureCalls(errorSpy),
21
+ warnings: captureCalls(warnSpy),
22
+ };
23
+ } finally {
24
+ logSpy.mockRestore();
25
+ errorSpy.mockRestore();
26
+ warnSpy.mockRestore();
27
+ }
28
+ }
29
+
30
+ function captureCalls(spy: jest.SpyInstance): string {
31
+ return spy.mock.calls.flat(2).join("\n");
32
+ }
@@ -0,0 +1,130 @@
1
+ import React from "react";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { render } from "ink-testing-library";
5
+ import type { UserInfo } from "../whoami";
6
+ import { getUserInfo, WhoAmI } from "../whoami";
7
+ import { runInTempDir } from "./run-in-tmp";
8
+ import { mkdirSync, writeFileSync } from "node:fs";
9
+ import { setMockResponse } from "./mock-cfetch";
10
+ import { initialise } from "../user";
11
+
12
+ const ORIGINAL_CF_API_TOKEN = process.env.CF_API_TOKEN;
13
+ const ORIGINAL_CF_ACCOUNT_ID = process.env.CF_ACCOUNT_ID;
14
+
15
+ describe("getUserInfo()", () => {
16
+ runInTempDir();
17
+
18
+ beforeEach(() => {
19
+ // Clear the environment variables, so we can control them in the tests
20
+ delete process.env.CF_API_TOKEN;
21
+ delete process.env.CF_ACCOUNT_ID;
22
+ // Override where the home directory is so that we can specify a user config
23
+ mkdirSync("./home");
24
+ jest.spyOn(os, "homedir").mockReturnValue("./home");
25
+ });
26
+
27
+ afterEach(() => {
28
+ // Reset any changes to the environment variables
29
+ process.env.CF_API_TOKEN = ORIGINAL_CF_API_TOKEN;
30
+ process.env.CF_ACCOUNT_ID = ORIGINAL_CF_ACCOUNT_ID;
31
+ });
32
+
33
+ it("should return undefined if there is no config file", async () => {
34
+ await initialise();
35
+ const userInfo = await getUserInfo();
36
+ expect(userInfo).toBeUndefined();
37
+ });
38
+
39
+ it("should return undefined if there is an empty config file", async () => {
40
+ writeUserConfig();
41
+ await initialise();
42
+ const userInfo = await getUserInfo();
43
+ expect(userInfo).toBeUndefined();
44
+ });
45
+
46
+ it("should return the user's email and accounts if authenticated via config token", async () => {
47
+ writeUserConfig("some-oauth-token");
48
+ setMockResponse("/user", () => {
49
+ return { email: "user@example.com" };
50
+ });
51
+ setMockResponse("/accounts", () => {
52
+ return [
53
+ { name: "Account One", id: "account-1" },
54
+ { name: "Account Two", id: "account-2" },
55
+ { name: "Account Three", id: "account-3" },
56
+ ];
57
+ });
58
+
59
+ await initialise();
60
+ const userInfo = await getUserInfo();
61
+
62
+ expect(userInfo).toEqual({
63
+ authType: "OAuth",
64
+ apiToken: "some-oauth-token",
65
+ email: "user@example.com",
66
+ accounts: [
67
+ { name: "Account One", id: "account-1" },
68
+ { name: "Account Two", id: "account-2" },
69
+ { name: "Account Three", id: "account-3" },
70
+ ],
71
+ });
72
+ });
73
+ });
74
+
75
+ describe("WhoAmI component", () => {
76
+ it("should return undefined if there is no user", async () => {
77
+ const { lastFrame } = render(<WhoAmI user={undefined}></WhoAmI>);
78
+
79
+ expect(lastFrame()).toMatchInlineSnapshot(
80
+ `"You are not authenticated. Please run \`wrangler login\`."`
81
+ );
82
+ });
83
+
84
+ it("should display the user's email and accounts", async () => {
85
+ const user: UserInfo = {
86
+ authType: "OAuth",
87
+ apiToken: "some-oauth-token",
88
+ email: "user@example.com",
89
+ accounts: [
90
+ { name: "Account One", id: "account-1" },
91
+ { name: "Account Two", id: "account-2" },
92
+ { name: "Account Three", id: "account-3" },
93
+ ],
94
+ };
95
+
96
+ const { lastFrame } = render(<WhoAmI user={user}></WhoAmI>);
97
+
98
+ expect(lastFrame()).toContain(
99
+ "You are logged in with an OAuth Token, associated with the email 'user@example.com'!"
100
+ );
101
+ expect(lastFrame()).toMatch(/Account Name .+ Account ID/);
102
+ expect(lastFrame()).toMatch(/Account One .+ account-1/);
103
+ expect(lastFrame()).toMatch(/Account Two .+ account-2/);
104
+ expect(lastFrame()).toMatch(/Account Three .+ account-3/);
105
+ });
106
+ });
107
+
108
+ function writeUserConfig(
109
+ oauth_token?: string,
110
+ refresh_token?: string,
111
+ expiration_time?: string
112
+ ) {
113
+ const lines: string[] = [];
114
+ if (oauth_token) {
115
+ lines.push(`oauth_token = "${oauth_token}"`);
116
+ }
117
+ if (refresh_token) {
118
+ lines.push(`refresh_token = "${refresh_token}"`);
119
+ }
120
+ if (expiration_time) {
121
+ lines.push(`expiration_time = "${expiration_time}"`);
122
+ }
123
+ const configPath = path.join(os.homedir(), ".wrangler/config");
124
+ mkdirSync(configPath, { recursive: true });
125
+ writeFileSync(
126
+ path.join(configPath, "default.toml"),
127
+ lines.join("\n"),
128
+ "utf-8"
129
+ );
130
+ }
@@ -1,72 +1,11 @@
1
1
  import type {
2
2
  CfWorkerInit,
3
3
  CfModuleType,
4
- CfVariable,
5
4
  CfModule,
5
+ CfDurableObjectMigrations,
6
6
  } from "./worker.js";
7
7
  import { FormData, Blob } from "formdata-node";
8
8
 
9
- // Credit: https://stackoverflow.com/a/9458996
10
- function toBase64(source: BufferSource): string {
11
- let result = "";
12
- const buffer = source instanceof ArrayBuffer ? source : source.buffer;
13
- const bytes = new Uint8Array(buffer);
14
- for (let i = 0; i < bytes.byteLength; i++) {
15
- result += String.fromCharCode(bytes[i]);
16
- }
17
- return btoa(result);
18
- }
19
-
20
- function toBinding(
21
- name: string,
22
- variable: CfVariable
23
- ): Record<string, unknown> {
24
- if (typeof variable === "string") {
25
- return { name, type: "plain_text", text: variable };
26
- }
27
-
28
- if ("namespaceId" in variable) {
29
- return {
30
- name,
31
- type: "kv_namespace",
32
- namespace_id: variable.namespaceId,
33
- };
34
- }
35
-
36
- if ("class_name" in variable) {
37
- return {
38
- name,
39
- type: "durable_object_namespace",
40
- class_name: variable.class_name,
41
- ...(variable.script_name && {
42
- script_name: variable.script_name,
43
- }),
44
- };
45
- }
46
-
47
- const { format, algorithm, usages, data } = variable;
48
- if (format) {
49
- let key_base64;
50
- let key_jwk;
51
- if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
52
- key_base64 = toBase64(data);
53
- } else {
54
- key_jwk = data;
55
- }
56
- return {
57
- name,
58
- type: "secret_key",
59
- format,
60
- algorithm,
61
- usages,
62
- key_base64,
63
- key_jwk,
64
- };
65
- }
66
-
67
- throw new TypeError("Unsupported variable: " + variable);
68
- }
69
-
70
9
  export function toMimeType(type: CfModuleType): string {
71
10
  switch (type) {
72
11
  case "esm":
@@ -84,13 +23,36 @@ export function toMimeType(type: CfModuleType): string {
84
23
  }
85
24
  }
86
25
 
87
- function toModule(module: CfModule, entryType?: CfModuleType): Blob {
26
+ function toModule(module: CfModule, entryType: CfModuleType): Blob {
88
27
  const { type: moduleType, content } = module;
89
28
  const type = toMimeType(moduleType ?? entryType);
90
29
 
91
30
  return new Blob([content], { type });
92
31
  }
93
32
 
33
+ interface WorkerMetadata {
34
+ compatibility_date?: string;
35
+ compatibility_flags?: string[];
36
+ usage_model?: "bundled" | "unbound";
37
+ migrations?: CfDurableObjectMigrations;
38
+ bindings: (
39
+ | { type: "kv_namespace"; name: string; namespace_id: string }
40
+ | { type: "plain_text"; name: string; text: string }
41
+ | {
42
+ type: "durable_object_namespace";
43
+ name: string;
44
+ class_name: string;
45
+ script_name?: string;
46
+ }
47
+ | {
48
+ type: "service";
49
+ name: string;
50
+ service: string;
51
+ environment: string;
52
+ }
53
+ )[];
54
+ }
55
+
94
56
  /**
95
57
  * Creates a `FormData` upload from a `CfWorkerInit`.
96
58
  */
@@ -99,24 +61,52 @@ export function toFormData(worker: CfWorkerInit): FormData {
99
61
  const {
100
62
  main,
101
63
  modules,
102
- variables,
64
+ bindings,
103
65
  migrations,
104
66
  usage_model,
105
67
  compatibility_date,
106
68
  compatibility_flags,
107
69
  } = worker;
108
- const { name, type: mainType } = main;
109
70
 
110
- const bindings = [];
111
- for (const [name, variable] of Object.entries(variables ?? {})) {
112
- const binding = toBinding(name, variable);
113
- bindings.push(binding);
114
- }
71
+ const metadataBindings: WorkerMetadata["bindings"] = [];
115
72
 
116
- // TODO: this object should be typed
117
- const metadata = {
118
- ...(mainType !== "commonjs" ? { main_module: name } : { body_part: name }),
119
- bindings,
73
+ bindings.kv_namespaces?.forEach(({ id, binding }) => {
74
+ metadataBindings.push({
75
+ name: binding,
76
+ type: "kv_namespace",
77
+ namespace_id: id,
78
+ });
79
+ });
80
+
81
+ bindings.durable_objects?.bindings.forEach(
82
+ ({ name, class_name, script_name }) => {
83
+ metadataBindings.push({
84
+ name,
85
+ type: "durable_object_namespace",
86
+ class_name: class_name,
87
+ ...(script_name && { script_name }),
88
+ });
89
+ }
90
+ );
91
+
92
+ Object.entries(bindings.vars || {})?.forEach(([key, value]) => {
93
+ metadataBindings.push({ name: key, type: "plain_text", text: value });
94
+ });
95
+
96
+ bindings.services?.forEach(({ name, service, environment }) => {
97
+ metadataBindings.push({
98
+ name,
99
+ type: "service",
100
+ service,
101
+ environment,
102
+ });
103
+ });
104
+
105
+ const metadata: WorkerMetadata = {
106
+ ...(main.type !== "commonjs"
107
+ ? { main_module: main.name }
108
+ : { body_part: main.name }),
109
+ bindings: metadataBindings,
120
110
  ...(compatibility_date && { compatibility_date }),
121
111
  ...(compatibility_flags && { compatibility_flags }),
122
112
  ...(usage_model && { usage_model }),
@@ -125,7 +115,7 @@ export function toFormData(worker: CfWorkerInit): FormData {
125
115
 
126
116
  formData.set("metadata", JSON.stringify(metadata));
127
117
 
128
- if (mainType === "commonjs" && modules && modules.length > 0) {
118
+ if (main.type === "commonjs" && modules && modules.length > 0) {
129
119
  throw new TypeError(
130
120
  "More than one module can only be specified when type = 'esm'"
131
121
  );
@@ -133,7 +123,7 @@ export function toFormData(worker: CfWorkerInit): FormData {
133
123
 
134
124
  for (const module of [main].concat(modules || [])) {
135
125
  const { name } = module;
136
- const blob = toModule(module, mainType ?? "esm");
126
+ const blob = toModule(module, main.type ?? "esm");
137
127
  formData.set(name, blob, name);
138
128
  }
139
129
 
@@ -1,7 +1,7 @@
1
- import cfetch from "../cfetch";
1
+ import fetch from "node-fetch";
2
+ import { fetchResult } from "../cfetch";
2
3
  import { toFormData } from "./form_data";
3
4
  import type { CfAccount, CfWorkerInit } from "./worker";
4
- import fetch from "node-fetch";
5
5
 
6
6
  /**
7
7
  * A preview mode.
@@ -60,7 +60,7 @@ async function sessionToken(account: CfAccount): Promise<CfPreviewToken> {
60
60
  ? `/zones/${zoneId}/workers/edge-preview`
61
61
  : `/accounts/${accountId}/workers/subdomain/edge-preview`;
62
62
 
63
- const { exchange_url } = await cfetch<{ exchange_url: string }>(initUrl);
63
+ const { exchange_url } = await fetchResult<{ exchange_url: string }>(initUrl);
64
64
  const { inspector_websocket, token } = (await (
65
65
  await fetch(exchange_url)
66
66
  ).json()) as { inspector_websocket: string; token: string };
@@ -106,7 +106,7 @@ export async function previewToken(
106
106
  const formData = toFormData(worker);
107
107
  formData.set("wrangler-session-config", JSON.stringify(mode));
108
108
 
109
- const { preview_token } = await cfetch<{ preview_token: string }>(url, {
109
+ const { preview_token } = await fetchResult<{ preview_token: string }>(url, {
110
110
  method: "POST",
111
111
  // @ts-expect-error TODO: fix this
112
112
  body: formData,
package/src/api/worker.ts CHANGED
@@ -62,22 +62,40 @@ export interface CfModule {
62
62
  type?: CfModuleType;
63
63
  }
64
64
 
65
+ /**
66
+ * A map of variable names to values.
67
+ */
68
+ interface CfVars {
69
+ [key: string]: string;
70
+ }
71
+
65
72
  /**
66
73
  * A KV namespace.
67
74
  */
68
- export interface CfKvNamespace {
69
- /**
70
- * The namespace ID.
71
- */
72
- namespaceId: string;
75
+ interface CfKvNamespace {
76
+ binding: string;
77
+ id: string;
73
78
  }
74
79
 
75
- export interface CfDurableObject {
80
+ /**
81
+ * A Durable Object.
82
+ */
83
+ interface CfDurableObject {
84
+ name: string;
76
85
  class_name: string;
77
86
  script_name?: string;
78
87
  }
79
88
 
80
- interface CfDOMigrations {
89
+ /**
90
+ * A Service.
91
+ */
92
+ interface CfService {
93
+ name: string;
94
+ service: string;
95
+ environment: string;
96
+ }
97
+
98
+ export interface CfDurableObjectMigrations {
81
99
  old_tag?: string;
82
100
  new_tag: string;
83
101
  steps: {
@@ -87,35 +105,6 @@ interface CfDOMigrations {
87
105
  }[];
88
106
  }
89
107
 
90
- /**
91
- * A `WebCrypto` key.
92
- *
93
- * @link https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
94
- */
95
- export interface CfCryptoKey {
96
- /**
97
- * The format.
98
- */
99
- format: string;
100
- /**
101
- * The algorithm.
102
- */
103
- algorithm: string;
104
- /**
105
- * The usages.
106
- */
107
- usages: string[];
108
- /**
109
- * The data.
110
- */
111
- data: BufferSource | JsonWebKey;
112
- }
113
-
114
- /**
115
- * A variable (aka. environment variable).
116
- */
117
- export type CfVariable = string | CfKvNamespace | CfCryptoKey | CfDurableObject;
118
-
119
108
  /**
120
109
  * Options for creating a `CfWorker`.
121
110
  */
@@ -123,7 +112,7 @@ export interface CfWorkerInit {
123
112
  /**
124
113
  * The name of the worker.
125
114
  */
126
- name: string | void;
115
+ name: string | undefined;
127
116
  /**
128
117
  * The entrypoint module.
129
118
  */
@@ -131,15 +120,20 @@ export interface CfWorkerInit {
131
120
  /**
132
121
  * The list of additional modules.
133
122
  */
134
- modules: void | CfModule[];
123
+ modules: undefined | CfModule[];
135
124
  /**
136
- * The map of names to variables. (aka. environment variables)
125
+ * All the bindings
137
126
  */
138
- variables?: { [name: string]: CfVariable };
139
- migrations: void | CfDOMigrations;
140
- compatibility_date: string | void;
141
- compatibility_flags: void | string[];
142
- usage_model: void | "bundled" | "unbound";
127
+ bindings: {
128
+ kv_namespaces?: CfKvNamespace[];
129
+ durable_objects?: { bindings: CfDurableObject[] };
130
+ vars?: CfVars;
131
+ services?: CfService[];
132
+ };
133
+ migrations: undefined | CfDurableObjectMigrations;
134
+ compatibility_date: string | undefined;
135
+ compatibility_flags: undefined | string[];
136
+ usage_model: undefined | "bundled" | "unbound";
143
137
  }
144
138
 
145
139
  /**
@@ -0,0 +1,110 @@
1
+ import type { RequestInit } from "node-fetch";
2
+ import { URLSearchParams } from "url";
3
+ import { fetchInternal } from "./internal";
4
+
5
+ // Check out https://api.cloudflare.com/ for API docs.
6
+
7
+ export { CF_API_BASE_URL } from "./internal";
8
+
9
+ export interface FetchError {
10
+ code: number;
11
+ message: string;
12
+ }
13
+ export interface FetchResult<ResponseType = unknown> {
14
+ success: boolean;
15
+ result: ResponseType;
16
+ errors: FetchError[];
17
+ messages: string[];
18
+ result_info?: unknown;
19
+ }
20
+
21
+ /**
22
+ * Make a fetch request for a raw JSON value.
23
+ */
24
+ export async function fetchRaw<ResponseType>(
25
+ resource: string,
26
+ init: RequestInit = {},
27
+ queryParams?: URLSearchParams
28
+ ): Promise<ResponseType> {
29
+ return fetchInternal<ResponseType>(resource, init, queryParams);
30
+ }
31
+
32
+ /**
33
+ * Make a fetch request, and extract the `result` from the JSON response.
34
+ */
35
+ export async function fetchResult<ResponseType>(
36
+ resource: string,
37
+ init: RequestInit = {},
38
+ queryParams?: URLSearchParams
39
+ ): Promise<ResponseType> {
40
+ const json = await fetchInternal<FetchResult<ResponseType>>(
41
+ resource,
42
+ init,
43
+ queryParams
44
+ );
45
+ if (json.success) {
46
+ return json.result;
47
+ } else {
48
+ throwFetchError(resource, json);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Make a fetch request for a list of values,
54
+ * extracting the `result` from the JSON response,
55
+ * and repeating the request if the results are paginated.
56
+ */
57
+ export async function fetchListResult<ResponseType>(
58
+ resource: string,
59
+ init: RequestInit = {},
60
+ queryParams?: URLSearchParams
61
+ ): Promise<ResponseType[]> {
62
+ const results: ResponseType[] = [];
63
+ let getMoreResults = true;
64
+ let cursor: string | undefined;
65
+ while (getMoreResults) {
66
+ if (cursor) {
67
+ queryParams = new URLSearchParams(queryParams);
68
+ queryParams.set("cursor", cursor);
69
+ }
70
+ const json = await fetchInternal<FetchResult<ResponseType[]>>(
71
+ resource,
72
+ init,
73
+ queryParams
74
+ );
75
+ if (json.success) {
76
+ results.push(...json.result);
77
+ if (hasCursor(json.result_info)) {
78
+ cursor = json.result_info?.cursor;
79
+ } else {
80
+ getMoreResults = false;
81
+ }
82
+ } else {
83
+ throwFetchError(resource, json);
84
+ }
85
+ }
86
+ return results;
87
+ }
88
+
89
+ function throwFetchError(
90
+ resource: string,
91
+ response: FetchResult<unknown>
92
+ ): never {
93
+ response.messages.forEach((message) => console.warn(message));
94
+ const errors = response.errors
95
+ .map((error) => `${error.code}: ${error.message}`)
96
+ .join("\n");
97
+ const error = new Error(`Failed to fetch ${resource} - \n${errors}`);
98
+ // add the first error code directly to this error
99
+ // so consumers can use it for specific behaviour
100
+ const code = response.errors[0]?.code;
101
+ if (code) {
102
+ //@ts-expect-error non-standard property on Error
103
+ error.code = code;
104
+ }
105
+ throw error;
106
+ }
107
+
108
+ function hasCursor(result_info: unknown): result_info is { cursor: string } {
109
+ return (result_info as { cursor: string } | undefined)?.cursor !== undefined;
110
+ }
@@ -0,0 +1,71 @@
1
+ import fetch from "node-fetch";
2
+ import type { RequestInit, HeadersInit } from "node-fetch";
3
+ import { getAPIToken, loginOrRefreshIfRequired } from "../user";
4
+
5
+ export const CF_API_BASE_URL =
6
+ process.env.CF_API_BASE_URL || "https://api.cloudflare.com/client/v4";
7
+
8
+ /**
9
+ * Make a fetch request to the Cloudflare API.
10
+ *
11
+ * This function handles acquiring the API token and logging the caller in, as necessary.
12
+ *
13
+ * Check out https://api.cloudflare.com/ for API docs.
14
+ *
15
+ * This function should not be used directly, instead use the functions in `cfetch/index.ts`.
16
+ */
17
+ export async function fetchInternal<ResponseType>(
18
+ resource: string,
19
+ init: RequestInit = {},
20
+ queryParams?: URLSearchParams
21
+ ): Promise<ResponseType> {
22
+ await requireLoggedIn();
23
+ const apiToken = requireApiToken();
24
+ const headers = cloneHeaders(init.headers);
25
+ addAuthorizationHeader(headers, apiToken);
26
+
27
+ const queryString = queryParams ? `?${queryParams.toString()}` : "";
28
+ const response = await fetch(`${CF_API_BASE_URL}${resource}${queryString}`, {
29
+ method: "GET",
30
+ ...init,
31
+ headers,
32
+ });
33
+
34
+ const jsonText = await response.text();
35
+ try {
36
+ const json = JSON.parse(jsonText);
37
+ return json as ResponseType;
38
+ } catch (e) {
39
+ throw new Error(
40
+ `Failed to fetch ${resource} - ${response.status}: ${response.statusText}\nInvalid JSON response:\n${jsonText}`
41
+ );
42
+ }
43
+ }
44
+
45
+ function cloneHeaders(headers: HeadersInit | undefined): HeadersInit {
46
+ return { ...headers };
47
+ }
48
+
49
+ async function requireLoggedIn(): Promise<void> {
50
+ const loggedIn = await loginOrRefreshIfRequired();
51
+ if (!loggedIn) {
52
+ throw new Error("Not logged in.");
53
+ }
54
+ }
55
+
56
+ function requireApiToken(): string {
57
+ const apiToken = getAPIToken();
58
+ if (!apiToken) {
59
+ throw new Error("No API token found.");
60
+ }
61
+ return apiToken;
62
+ }
63
+
64
+ function addAuthorizationHeader(headers: HeadersInit, apiToken: string): void {
65
+ if (headers["Authorization"]) {
66
+ throw new Error(
67
+ "The request already specifies an authorisation header - cannot add a new one."
68
+ );
69
+ }
70
+ headers["Authorization"] = `Bearer ${apiToken}`;
71
+ }