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.
- package/package.json +25 -10
- package/pages/functions/buildWorker.ts +62 -0
- package/pages/functions/filepath-routing.test.ts +39 -0
- package/pages/functions/filepath-routing.ts +228 -0
- package/pages/functions/identifiers.ts +78 -0
- package/pages/functions/routes.ts +158 -0
- package/pages/functions/template-worker.ts +136 -0
- package/src/__tests__/dev.test.tsx +67 -0
- package/src/__tests__/index.test.ts +42 -175
- package/src/__tests__/jest.setup.ts +22 -0
- package/src/__tests__/kv.test.ts +1199 -0
- package/src/__tests__/mock-cfetch.ts +173 -0
- package/src/__tests__/mock-dialogs.ts +65 -0
- package/src/__tests__/publish.test.ts +737 -0
- package/src/__tests__/run-in-tmp.ts +19 -0
- package/src/__tests__/run-wrangler.ts +32 -0
- package/src/__tests__/whoami.test.tsx +130 -0
- package/src/api/form_data.ts +66 -76
- package/src/api/preview.ts +4 -4
- package/src/api/worker.ts +38 -44
- package/src/cfetch/index.ts +110 -0
- package/src/cfetch/internal.ts +71 -0
- package/src/cli.ts +6 -7
- package/src/config.ts +634 -110
- package/src/dev.tsx +275 -272
- package/src/dialogs.tsx +2 -2
- package/src/index.tsx +504 -403
- package/src/inspect.ts +543 -0
- package/src/kv.tsx +109 -44
- package/src/pages.tsx +444 -222
- package/src/proxy.ts +302 -0
- package/src/publish.ts +123 -87
- package/src/sites.tsx +169 -41
- package/src/tail.tsx +5 -3
- package/src/user.tsx +64 -74
- package/src/whoami.tsx +69 -0
- package/wrangler-dist/cli.js +42792 -42619
- package/wrangler-dist/cli.js.map +3 -3
- package/src/__tests__/mock-cfetch.js +0 -42
- package/src/api/inspect.ts +0 -430
- 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
|
+
}
|
package/src/api/form_data.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 (
|
|
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,
|
|
126
|
+
const blob = toModule(module, main.type ?? "esm");
|
|
137
127
|
formData.set(name, blob, name);
|
|
138
128
|
}
|
|
139
129
|
|
package/src/api/preview.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import
|
|
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
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
*/
|
|
72
|
-
namespaceId: string;
|
|
75
|
+
interface CfKvNamespace {
|
|
76
|
+
binding: string;
|
|
77
|
+
id: string;
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
|
|
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
|
-
|
|
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 |
|
|
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:
|
|
123
|
+
modules: undefined | CfModule[];
|
|
135
124
|
/**
|
|
136
|
-
*
|
|
125
|
+
* All the bindings
|
|
137
126
|
*/
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
}
|