wrangler 2.0.8 → 2.0.9
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 +1 -1
- package/src/__tests__/dev.test.tsx +61 -58
- package/src/__tests__/init.test.ts +3 -0
- package/src/__tests__/pages.test.ts +98 -1
- package/src/__tests__/publish.test.ts +123 -3
- package/src/__tests__/whoami.test.tsx +34 -0
- package/src/cfetch/internal.ts +6 -9
- package/src/config/config.ts +1 -1
- package/src/create-worker-preview.ts +15 -15
- package/src/dev/dev.tsx +4 -12
- package/src/dev/remote.tsx +26 -16
- package/src/index.tsx +22 -83
- package/src/pages.tsx +14 -3
- package/src/publish.ts +148 -15
- package/src/user.tsx +12 -1
- package/src/whoami.tsx +3 -2
- package/src/worker.ts +2 -1
- package/src/zones.ts +73 -0
- package/wrangler-dist/cli.js +188 -80
package/src/dev/dev.tsx
CHANGED
|
@@ -13,7 +13,6 @@ import { runCustomBuild } from "../entry";
|
|
|
13
13
|
import { openInspector } from "../inspect";
|
|
14
14
|
import { logger } from "../logger";
|
|
15
15
|
import openInBrowser from "../open-in-browser";
|
|
16
|
-
import { getAPIToken } from "../user";
|
|
17
16
|
import { Local } from "./local";
|
|
18
17
|
import { Remote } from "./remote";
|
|
19
18
|
import { useEsbuild } from "./use-esbuild";
|
|
@@ -54,16 +53,11 @@ export type DevProps = {
|
|
|
54
53
|
};
|
|
55
54
|
env: string | undefined;
|
|
56
55
|
legacyEnv: boolean;
|
|
57
|
-
zone:
|
|
58
|
-
|
|
59
|
-
id: string;
|
|
60
|
-
host: string;
|
|
61
|
-
}
|
|
62
|
-
| undefined;
|
|
56
|
+
zone: string | undefined;
|
|
57
|
+
host: string | undefined;
|
|
63
58
|
};
|
|
64
59
|
|
|
65
60
|
export function DevImplementation(props: DevProps): JSX.Element {
|
|
66
|
-
const apiToken = props.initialMode === "remote" ? getAPIToken() : undefined;
|
|
67
61
|
const directory = useTmpDir();
|
|
68
62
|
|
|
69
63
|
useCustomBuild(props.entry, props.build);
|
|
@@ -108,19 +102,17 @@ export function DevImplementation(props: DevProps): JSX.Element {
|
|
|
108
102
|
// only load the UI if we're running in a supported environment
|
|
109
103
|
const { isRawModeSupported } = useStdin();
|
|
110
104
|
return isRawModeSupported ? (
|
|
111
|
-
<InteractiveDevSession {...props} bundle={bundle}
|
|
105
|
+
<InteractiveDevSession {...props} bundle={bundle} />
|
|
112
106
|
) : (
|
|
113
107
|
<DevSession
|
|
114
108
|
{...props}
|
|
115
109
|
bundle={bundle}
|
|
116
|
-
apiToken={apiToken}
|
|
117
110
|
local={props.initialMode === "local"}
|
|
118
111
|
/>
|
|
119
112
|
);
|
|
120
113
|
}
|
|
121
114
|
|
|
122
115
|
type InteractiveDevSessionProps = DevProps & {
|
|
123
|
-
apiToken: string | undefined;
|
|
124
116
|
bundle: EsbuildBundle | undefined;
|
|
125
117
|
};
|
|
126
118
|
|
|
@@ -183,7 +175,6 @@ function DevSession(props: DevSessionProps) {
|
|
|
183
175
|
bundle={props.bundle}
|
|
184
176
|
format={props.entry.format}
|
|
185
177
|
accountId={props.accountId}
|
|
186
|
-
apiToken={props.apiToken}
|
|
187
178
|
bindings={props.bindings}
|
|
188
179
|
assetPaths={props.assetPaths}
|
|
189
180
|
public={props.public}
|
|
@@ -197,6 +188,7 @@ function DevSession(props: DevSessionProps) {
|
|
|
197
188
|
env={props.env}
|
|
198
189
|
legacyEnv={props.legacyEnv}
|
|
199
190
|
zone={props.zone}
|
|
191
|
+
host={props.host}
|
|
200
192
|
/>
|
|
201
193
|
);
|
|
202
194
|
}
|
package/src/dev/remote.tsx
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
1
|
import { readFile } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { useState, useEffect, useRef } from "react";
|
|
@@ -7,6 +6,7 @@ import useInspector from "../inspect";
|
|
|
7
6
|
import { logger } from "../logger";
|
|
8
7
|
import { usePreviewServer } from "../proxy";
|
|
9
8
|
import { syncAssets } from "../sites";
|
|
9
|
+
import { requireApiToken, requireAuth } from "../user";
|
|
10
10
|
import type { CfPreviewToken } from "../create-worker-preview";
|
|
11
11
|
import type { AssetPaths } from "../sites";
|
|
12
12
|
import type { CfModule, CfWorkerInit, CfScriptFormat } from "../worker";
|
|
@@ -23,24 +23,21 @@ export function Remote(props: {
|
|
|
23
23
|
localProtocol: "https" | "http";
|
|
24
24
|
inspectorPort: number;
|
|
25
25
|
accountId: string | undefined;
|
|
26
|
-
apiToken: string | undefined;
|
|
27
26
|
bindings: CfWorkerInit["bindings"];
|
|
28
27
|
compatibilityDate: string;
|
|
29
28
|
compatibilityFlags: string[] | undefined;
|
|
30
29
|
usageModel: "bundled" | "unbound" | undefined;
|
|
31
30
|
env: string | undefined;
|
|
32
31
|
legacyEnv: boolean | undefined;
|
|
33
|
-
zone:
|
|
32
|
+
zone: string | undefined;
|
|
33
|
+
host: string | undefined;
|
|
34
34
|
}) {
|
|
35
|
-
assert(props.accountId, "accountId is required");
|
|
36
|
-
assert(props.apiToken, "apiToken is required");
|
|
37
35
|
const previewToken = useWorker({
|
|
38
36
|
name: props.name,
|
|
39
37
|
bundle: props.bundle,
|
|
40
38
|
format: props.format,
|
|
41
39
|
modules: props.bundle ? props.bundle.modules : [],
|
|
42
40
|
accountId: props.accountId,
|
|
43
|
-
apiToken: props.apiToken,
|
|
44
41
|
bindings: props.bindings,
|
|
45
42
|
assetPaths: props.assetPaths,
|
|
46
43
|
port: props.port,
|
|
@@ -50,6 +47,7 @@ export function Remote(props: {
|
|
|
50
47
|
env: props.env,
|
|
51
48
|
legacyEnv: props.legacyEnv,
|
|
52
49
|
zone: props.zone,
|
|
50
|
+
host: props.host,
|
|
53
51
|
});
|
|
54
52
|
|
|
55
53
|
usePreviewServer({
|
|
@@ -73,8 +71,7 @@ export function useWorker(props: {
|
|
|
73
71
|
bundle: EsbuildBundle | undefined;
|
|
74
72
|
format: CfScriptFormat | undefined;
|
|
75
73
|
modules: CfModule[];
|
|
76
|
-
accountId: string;
|
|
77
|
-
apiToken: string;
|
|
74
|
+
accountId: string | undefined;
|
|
78
75
|
bindings: CfWorkerInit["bindings"];
|
|
79
76
|
assetPaths: AssetPaths | undefined;
|
|
80
77
|
port: number;
|
|
@@ -83,7 +80,8 @@ export function useWorker(props: {
|
|
|
83
80
|
usageModel: "bundled" | "unbound" | undefined;
|
|
84
81
|
env: string | undefined;
|
|
85
82
|
legacyEnv: boolean | undefined;
|
|
86
|
-
zone:
|
|
83
|
+
zone: string | undefined;
|
|
84
|
+
host: string | undefined;
|
|
87
85
|
}): CfPreviewToken | undefined {
|
|
88
86
|
const {
|
|
89
87
|
name,
|
|
@@ -91,7 +89,6 @@ export function useWorker(props: {
|
|
|
91
89
|
format,
|
|
92
90
|
modules,
|
|
93
91
|
accountId,
|
|
94
|
-
apiToken,
|
|
95
92
|
bindings,
|
|
96
93
|
assetPaths,
|
|
97
94
|
compatibilityDate,
|
|
@@ -105,6 +102,9 @@ export function useWorker(props: {
|
|
|
105
102
|
// something's "happened" in our system; We make a ref and
|
|
106
103
|
// mark it once we log our initial message. Refs are vars!
|
|
107
104
|
const startedRef = useRef(false);
|
|
105
|
+
// This ref holds the actual accountId being used, the `accountId` prop could be undefined
|
|
106
|
+
// as it is only what is retrieved from the wrangler.toml config.
|
|
107
|
+
const accountIdRef = useRef(accountId);
|
|
108
108
|
|
|
109
109
|
useEffect(() => {
|
|
110
110
|
const abortController = new AbortController();
|
|
@@ -119,8 +119,13 @@ export function useWorker(props: {
|
|
|
119
119
|
logger.log("⎔ Detected changes, restarted server.");
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// Ensure we have an account id, even if it means logging in here.
|
|
123
|
+
accountIdRef.current = await requireAuth({
|
|
124
|
+
account_id: accountIdRef.current,
|
|
125
|
+
});
|
|
126
|
+
|
|
122
127
|
const assets = await syncAssets(
|
|
123
|
-
|
|
128
|
+
accountIdRef.current,
|
|
124
129
|
// When we're using the newer service environments, we wouldn't
|
|
125
130
|
// have added the env name on to the script name. However, we must
|
|
126
131
|
// include it in the kv namespace name regardless (since there's no
|
|
@@ -173,10 +178,15 @@ export function useWorker(props: {
|
|
|
173
178
|
await createWorkerPreview(
|
|
174
179
|
init,
|
|
175
180
|
{
|
|
176
|
-
accountId,
|
|
177
|
-
apiToken,
|
|
181
|
+
accountId: accountIdRef.current,
|
|
182
|
+
apiToken: requireApiToken(),
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
env: props.env,
|
|
186
|
+
legacyEnv: props.legacyEnv,
|
|
187
|
+
zone: props.zone,
|
|
188
|
+
host: props.host,
|
|
178
189
|
},
|
|
179
|
-
{ env: props.env, legacyEnv: props.legacyEnv, zone: props.zone },
|
|
180
190
|
abortController.signal
|
|
181
191
|
)
|
|
182
192
|
);
|
|
@@ -193,7 +203,7 @@ export function useWorker(props: {
|
|
|
193
203
|
"Error: You need to register a workers.dev subdomain before running the dev command in remote mode";
|
|
194
204
|
const solutionMessage =
|
|
195
205
|
"You can either enable local mode by pressing l, or register a workers.dev subdomain here:";
|
|
196
|
-
const onboardingLink = `https://dash.cloudflare.com/${
|
|
206
|
+
const onboardingLink = `https://dash.cloudflare.com/${accountIdRef.current}/workers/onboarding`;
|
|
197
207
|
logger.error(
|
|
198
208
|
`${errorMessage}\n${solutionMessage}\n${onboardingLink}`
|
|
199
209
|
);
|
|
@@ -211,7 +221,6 @@ export function useWorker(props: {
|
|
|
211
221
|
bundle,
|
|
212
222
|
format,
|
|
213
223
|
accountId,
|
|
214
|
-
apiToken,
|
|
215
224
|
port,
|
|
216
225
|
assetPaths,
|
|
217
226
|
compatibilityDate,
|
|
@@ -222,6 +231,7 @@ export function useWorker(props: {
|
|
|
222
231
|
props.env,
|
|
223
232
|
props.legacyEnv,
|
|
224
233
|
props.zone,
|
|
234
|
+
props.host,
|
|
225
235
|
]);
|
|
226
236
|
return token;
|
|
227
237
|
}
|
package/src/index.tsx
CHANGED
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
requireAuth,
|
|
67
67
|
} from "./user";
|
|
68
68
|
import { whoami } from "./whoami";
|
|
69
|
+
import { getZoneIdFromHost, getZoneForRoute } from "./zones";
|
|
69
70
|
|
|
70
71
|
import type { Config } from "./config";
|
|
71
72
|
import type { TailCLIFilters } from "./tail";
|
|
@@ -444,7 +445,10 @@ function createCLIParser(argv: string[]) {
|
|
|
444
445
|
yesFlag ||
|
|
445
446
|
(await confirm("Would you like to use git to manage this Worker?"));
|
|
446
447
|
if (shouldInitGit) {
|
|
447
|
-
await execa("git", ["init"
|
|
448
|
+
await execa("git", ["init"], {
|
|
449
|
+
cwd: creationDirectory,
|
|
450
|
+
});
|
|
451
|
+
await execa("git", ["branch", "-m", "main"], {
|
|
448
452
|
cwd: creationDirectory,
|
|
449
453
|
});
|
|
450
454
|
await writeFile(
|
|
@@ -1079,92 +1083,25 @@ function createCLIParser(argv: string[]) {
|
|
|
1079
1083
|
);
|
|
1080
1084
|
}
|
|
1081
1085
|
|
|
1082
|
-
const accountId = !args.local ? await requireAuth(config) : undefined;
|
|
1083
|
-
|
|
1084
1086
|
// TODO: if worker_dev = false and no routes, then error (only for dev)
|
|
1085
1087
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
*/
|
|
1090
|
-
function getHost(urlLike: string): string | undefined {
|
|
1091
|
-
// strip leading * / *.
|
|
1092
|
-
urlLike = urlLike.replace(/^\*(\.)?/g, "");
|
|
1093
|
-
|
|
1094
|
-
if (
|
|
1095
|
-
!(urlLike.startsWith("http://") || urlLike.startsWith("https://"))
|
|
1096
|
-
) {
|
|
1097
|
-
urlLike = "http://" + urlLike;
|
|
1098
|
-
}
|
|
1099
|
-
return new URL(urlLike).host;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
/**
|
|
1103
|
-
* Given something that resembles a host,
|
|
1104
|
-
* try to infer a zone id from it
|
|
1105
|
-
*/
|
|
1106
|
-
async function getZoneId(host: string): Promise<string | undefined> {
|
|
1107
|
-
const zones = await fetchResult<{ id: string }[]>(
|
|
1108
|
-
`/zones`,
|
|
1109
|
-
{},
|
|
1110
|
-
new URLSearchParams({ name: host })
|
|
1111
|
-
);
|
|
1112
|
-
return zones[0]?.id;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
// When we're given a host (in one of the above ways), we do 2 things:
|
|
1116
|
-
// - We try to extract a host from it
|
|
1117
|
-
// - We try to get a zone id from the host
|
|
1118
|
-
//
|
|
1119
|
-
// So it turns out it's particularly hard to get a 'valid' domain
|
|
1120
|
-
// from a string, so we don't even try to validate TLDs, etc.
|
|
1121
|
-
// Once we get something that looks like w.x.y.z-ish, we then try to
|
|
1122
|
-
// get a zone id for it, by lopping off subdomains until we get a hit
|
|
1123
|
-
// from the API. That's it!
|
|
1124
|
-
|
|
1125
|
-
let zone: { host: string; id: string } | undefined;
|
|
1088
|
+
// Compute zone info from the `host` and `route` args and config;
|
|
1089
|
+
let host = args.host || config.dev.host;
|
|
1090
|
+
let zoneId: string | undefined;
|
|
1126
1091
|
|
|
1127
1092
|
if (!args.local) {
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
config.dev.host ||
|
|
1131
|
-
(args.routes && args.routes[0]) ||
|
|
1132
|
-
config.route ||
|
|
1133
|
-
(config.routes && config.routes[0]);
|
|
1134
|
-
|
|
1135
|
-
let zoneId: string | undefined =
|
|
1136
|
-
typeof hostLike === "object" && "zone_id" in hostLike
|
|
1137
|
-
? hostLike.zone_id
|
|
1138
|
-
: undefined;
|
|
1139
|
-
|
|
1140
|
-
const host =
|
|
1141
|
-
typeof hostLike === "string"
|
|
1142
|
-
? getHost(hostLike)
|
|
1143
|
-
: typeof hostLike === "object"
|
|
1144
|
-
? "zone_name" in hostLike
|
|
1145
|
-
? getHost(hostLike.zone_name)
|
|
1146
|
-
: getHost(hostLike.pattern)
|
|
1147
|
-
: undefined;
|
|
1148
|
-
|
|
1149
|
-
const hostPieces =
|
|
1150
|
-
typeof host === "string" ? host.split(".") : undefined;
|
|
1151
|
-
|
|
1152
|
-
while (!zoneId && hostPieces && hostPieces.length > 1) {
|
|
1153
|
-
zoneId = await getZoneId(hostPieces.join("."));
|
|
1154
|
-
hostPieces.shift();
|
|
1093
|
+
if (host) {
|
|
1094
|
+
zoneId = await getZoneIdFromHost(host);
|
|
1155
1095
|
}
|
|
1156
|
-
|
|
1157
|
-
if (
|
|
1158
|
-
|
|
1096
|
+
const routes = args.routes || config.route || config.routes;
|
|
1097
|
+
if (!zoneId && routes) {
|
|
1098
|
+
const firstRoute = Array.isArray(routes) ? routes[0] : routes;
|
|
1099
|
+
const zone = await getZoneForRoute(firstRoute);
|
|
1100
|
+
if (zone) {
|
|
1101
|
+
zoneId = zone.id;
|
|
1102
|
+
host = zone.host;
|
|
1103
|
+
}
|
|
1159
1104
|
}
|
|
1160
|
-
|
|
1161
|
-
zone =
|
|
1162
|
-
typeof zoneId === "string" && typeof host === "string"
|
|
1163
|
-
? {
|
|
1164
|
-
host,
|
|
1165
|
-
id: zoneId,
|
|
1166
|
-
}
|
|
1167
|
-
: undefined;
|
|
1168
1105
|
}
|
|
1169
1106
|
|
|
1170
1107
|
const nodeCompat = args.nodeCompat ?? config.node_compat;
|
|
@@ -1243,7 +1180,8 @@ function createCLIParser(argv: string[]) {
|
|
|
1243
1180
|
name={getScriptName(args, config)}
|
|
1244
1181
|
entry={entry}
|
|
1245
1182
|
env={args.env}
|
|
1246
|
-
zone={
|
|
1183
|
+
zone={zoneId}
|
|
1184
|
+
host={host}
|
|
1247
1185
|
rules={getRules(config)}
|
|
1248
1186
|
legacyEnv={isLegacyEnv(config)}
|
|
1249
1187
|
minify={args.minify ?? config.minify}
|
|
@@ -1258,7 +1196,7 @@ function createCLIParser(argv: string[]) {
|
|
|
1258
1196
|
enableLocalPersistence={
|
|
1259
1197
|
args["experimental-enable-local-persistence"] || false
|
|
1260
1198
|
}
|
|
1261
|
-
accountId={
|
|
1199
|
+
accountId={config.account_id}
|
|
1262
1200
|
assetPaths={getAssetPaths(
|
|
1263
1201
|
config,
|
|
1264
1202
|
args.site,
|
|
@@ -1672,6 +1610,7 @@ function createCLIParser(argv: string[]) {
|
|
|
1672
1610
|
rules={getRules(config)}
|
|
1673
1611
|
env={args.env}
|
|
1674
1612
|
zone={undefined}
|
|
1613
|
+
host={undefined}
|
|
1675
1614
|
legacyEnv={isLegacyEnv(config)}
|
|
1676
1615
|
build={config.build || {}}
|
|
1677
1616
|
minify={undefined}
|
package/src/pages.tsx
CHANGED
|
@@ -1139,9 +1139,15 @@ const createDeployment: CommandModule<
|
|
|
1139
1139
|
|
|
1140
1140
|
const files = [...fileMap.values()];
|
|
1141
1141
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1142
|
+
async function fetchJwt(): Promise<string> {
|
|
1143
|
+
return (
|
|
1144
|
+
await fetchResult<{ jwt: string }>(
|
|
1145
|
+
`/accounts/${accountId}/pages/projects/${projectName}/upload-token`
|
|
1146
|
+
)
|
|
1147
|
+
).jwt;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
let jwt = await fetchJwt();
|
|
1145
1151
|
|
|
1146
1152
|
const start = Date.now();
|
|
1147
1153
|
|
|
@@ -1237,6 +1243,11 @@ const createDeployment: CommandModule<
|
|
|
1237
1243
|
await new Promise((resolve) =>
|
|
1238
1244
|
setTimeout(resolve, attempts++ * 1000)
|
|
1239
1245
|
);
|
|
1246
|
+
|
|
1247
|
+
if ((e as { code: number }).code === 8000013) {
|
|
1248
|
+
// Looks like the JWT expired, fetch another one
|
|
1249
|
+
jwt = await fetchJwt();
|
|
1250
|
+
}
|
|
1240
1251
|
return doUpload();
|
|
1241
1252
|
} else {
|
|
1242
1253
|
throw e;
|
package/src/publish.ts
CHANGED
|
@@ -4,13 +4,15 @@ import path from "node:path";
|
|
|
4
4
|
import { URLSearchParams } from "node:url";
|
|
5
5
|
import tmp from "tmp-promise";
|
|
6
6
|
import { bundleWorker } from "./bundle";
|
|
7
|
-
import { fetchResult } from "./cfetch";
|
|
7
|
+
import { fetchListResult, fetchResult } from "./cfetch";
|
|
8
8
|
import { printBindings } from "./config";
|
|
9
9
|
import { createWorkerUploadForm } from "./create-worker-upload-form";
|
|
10
10
|
import { confirm } from "./dialogs";
|
|
11
11
|
import { getMigrationsToUpload } from "./durable";
|
|
12
12
|
import { logger } from "./logger";
|
|
13
|
+
import { ParseError } from "./parse";
|
|
13
14
|
import { syncAssets } from "./sites";
|
|
15
|
+
import { getZoneForRoute } from "./zones";
|
|
14
16
|
import type { Config } from "./config";
|
|
15
17
|
import type {
|
|
16
18
|
Route,
|
|
@@ -260,7 +262,7 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
260
262
|
const nodeCompat = props.nodeCompat ?? config.node_compat;
|
|
261
263
|
if (nodeCompat) {
|
|
262
264
|
logger.warn(
|
|
263
|
-
"Enabling node.js compatibility mode for
|
|
265
|
+
"Enabling node.js compatibility mode for built-ins and globals. This is experimental and has serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
|
|
264
266
|
);
|
|
265
267
|
}
|
|
266
268
|
|
|
@@ -291,7 +293,7 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
291
293
|
const envName = props.env ?? "production";
|
|
292
294
|
|
|
293
295
|
const start = Date.now();
|
|
294
|
-
const notProd = !props.legacyEnv && props.env;
|
|
296
|
+
const notProd = Boolean(!props.legacyEnv && props.env);
|
|
295
297
|
const workerName = notProd ? `${scriptName} (${envName})` : scriptName;
|
|
296
298
|
const workerUrl = notProd
|
|
297
299
|
? `/accounts/${accountId}/workers/services/${scriptName}/environments/${envName}`
|
|
@@ -497,18 +499,7 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
497
499
|
// Update routing table for the script.
|
|
498
500
|
if (routesOnly.length > 0) {
|
|
499
501
|
deployments.push(
|
|
500
|
-
|
|
501
|
-
// Note: PUT will delete previous routes on this script.
|
|
502
|
-
method: "PUT",
|
|
503
|
-
body: JSON.stringify(
|
|
504
|
-
routesOnly.map((route) =>
|
|
505
|
-
typeof route !== "object" ? { pattern: route } : route
|
|
506
|
-
)
|
|
507
|
-
),
|
|
508
|
-
headers: {
|
|
509
|
-
"Content-Type": "application/json",
|
|
510
|
-
},
|
|
511
|
-
}).then(() => {
|
|
502
|
+
publishRoutes(routesOnly, { workerUrl, scriptName, notProd }).then(() => {
|
|
512
503
|
if (routesOnly.length > 10) {
|
|
513
504
|
return routesOnly
|
|
514
505
|
.slice(0, 9)
|
|
@@ -581,3 +572,145 @@ async function getSubdomain(accountId: string): Promise<string> {
|
|
|
581
572
|
}
|
|
582
573
|
}
|
|
583
574
|
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Associate the newly deployed Worker with the given routes.
|
|
578
|
+
*/
|
|
579
|
+
async function publishRoutes(
|
|
580
|
+
routes: Route[],
|
|
581
|
+
{
|
|
582
|
+
workerUrl,
|
|
583
|
+
scriptName,
|
|
584
|
+
notProd,
|
|
585
|
+
}: { workerUrl: string; scriptName: string; notProd: boolean }
|
|
586
|
+
): Promise<string[]> {
|
|
587
|
+
try {
|
|
588
|
+
return await fetchResult(`${workerUrl}/routes`, {
|
|
589
|
+
// Note: PUT will delete previous routes on this script.
|
|
590
|
+
method: "PUT",
|
|
591
|
+
body: JSON.stringify(
|
|
592
|
+
routes.map((route) =>
|
|
593
|
+
typeof route !== "object" ? { pattern: route } : route
|
|
594
|
+
)
|
|
595
|
+
),
|
|
596
|
+
headers: {
|
|
597
|
+
"Content-Type": "application/json",
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
} catch (e) {
|
|
601
|
+
if (isAuthenticationError(e)) {
|
|
602
|
+
// An authentication error is probably due to a known issue,
|
|
603
|
+
// where the user is logged in via an API token that does not have "All Zones".
|
|
604
|
+
return await publishRoutesFallback(routes, { scriptName, notProd });
|
|
605
|
+
} else {
|
|
606
|
+
throw e;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Try updating routes for the Worker using a less optimal zone-based API.
|
|
613
|
+
*
|
|
614
|
+
* Compute match zones to the routes, then for each route attempt to connect it to the Worker via the zone.
|
|
615
|
+
*/
|
|
616
|
+
async function publishRoutesFallback(
|
|
617
|
+
routes: Route[],
|
|
618
|
+
{ scriptName, notProd }: { scriptName: string; notProd: boolean }
|
|
619
|
+
) {
|
|
620
|
+
if (notProd) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
"Service environments combined with an API token that doesn't have 'All Zones' permissions is not supported.\n" +
|
|
623
|
+
"Either turn off service environments by setting `legacy_env = true`, creating an API token with 'All Zones' permissions, or logging in via OAuth"
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
logger.warn(
|
|
627
|
+
"The current authentication token does not have 'All Zones' permissions.\n" +
|
|
628
|
+
"Falling back to using the zone-based API endpoint to update each route individually.\n" +
|
|
629
|
+
"Note that there is no access to routes associated with zones that the API token does not have permission for.\n" +
|
|
630
|
+
"Existing routes for this Worker in such zones will not be deleted."
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
const deployedRoutes: string[] = [];
|
|
634
|
+
|
|
635
|
+
// Collect the routes (and their zones) that will be deployed.
|
|
636
|
+
const activeZones = new Map<string, string>();
|
|
637
|
+
const routesToDeploy = new Map<string, string>();
|
|
638
|
+
for (const route of routes) {
|
|
639
|
+
const zone = await getZoneForRoute(route);
|
|
640
|
+
if (zone) {
|
|
641
|
+
activeZones.set(zone.id, zone.host);
|
|
642
|
+
routesToDeploy.set(
|
|
643
|
+
typeof route === "string" ? route : route.pattern,
|
|
644
|
+
zone.id
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Collect the routes that are already deployed.
|
|
650
|
+
const allRoutes = new Map<string, string>();
|
|
651
|
+
const alreadyDeployedRoutes = new Set<string>();
|
|
652
|
+
for (const [zone, host] of activeZones) {
|
|
653
|
+
try {
|
|
654
|
+
for (const { pattern, script } of await fetchListResult<{
|
|
655
|
+
pattern: string;
|
|
656
|
+
script: string;
|
|
657
|
+
}>(`/zones/${zone}/workers/routes`)) {
|
|
658
|
+
allRoutes.set(pattern, script);
|
|
659
|
+
if (script === scriptName) {
|
|
660
|
+
alreadyDeployedRoutes.add(pattern);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
} catch (e) {
|
|
664
|
+
if (isAuthenticationError(e)) {
|
|
665
|
+
e.notes.push({
|
|
666
|
+
text: `This could be because the API token being used does not have permission to access the zone "${host}" (${zone}).`,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
throw e;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Deploy each route that is not already deployed.
|
|
674
|
+
for (const [routePattern, zoneId] of routesToDeploy.entries()) {
|
|
675
|
+
if (allRoutes.has(routePattern)) {
|
|
676
|
+
const knownScript = allRoutes.get(routePattern);
|
|
677
|
+
if (knownScript === scriptName) {
|
|
678
|
+
// This route is already associated with this worker, so no need to hit the API.
|
|
679
|
+
alreadyDeployedRoutes.delete(routePattern);
|
|
680
|
+
continue;
|
|
681
|
+
} else {
|
|
682
|
+
throw new Error(
|
|
683
|
+
`The route with pattern "${routePattern}" is already associated with another worker called "${knownScript}".`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const { pattern } = await fetchResult(`/zones/${zoneId}/workers/routes`, {
|
|
689
|
+
method: "POST",
|
|
690
|
+
body: JSON.stringify({
|
|
691
|
+
pattern: routePattern,
|
|
692
|
+
script: scriptName,
|
|
693
|
+
}),
|
|
694
|
+
headers: {
|
|
695
|
+
"Content-Type": "application/json",
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
deployedRoutes.push(pattern);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (alreadyDeployedRoutes.size) {
|
|
703
|
+
logger.warn(
|
|
704
|
+
"Previously deployed routes:\n" +
|
|
705
|
+
"The following routes were already associated with this worker, and have not been deleted:\n" +
|
|
706
|
+
[...alreadyDeployedRoutes.values()].map((route) => ` - "${route}"\n`) +
|
|
707
|
+
"If these routes are not wanted then you can remove them in the dashboard."
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return deployedRoutes;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function isAuthenticationError(e: unknown): e is ParseError {
|
|
715
|
+
return e instanceof ParseError && (e as { code?: number }).code === 10000;
|
|
716
|
+
}
|
package/src/user.tsx
CHANGED
|
@@ -233,7 +233,7 @@ import type { Response } from "undici";
|
|
|
233
233
|
/**
|
|
234
234
|
* Try to read the API token from the environment.
|
|
235
235
|
*/
|
|
236
|
-
const getCloudflareAPITokenFromEnv = getEnvironmentVariableFactory({
|
|
236
|
+
export const getCloudflareAPITokenFromEnv = getEnvironmentVariableFactory({
|
|
237
237
|
variableName: "CLOUDFLARE_API_TOKEN",
|
|
238
238
|
deprecatedName: "CF_API_TOKEN",
|
|
239
239
|
});
|
|
@@ -1201,3 +1201,14 @@ export async function requireAuth(config: {
|
|
|
1201
1201
|
|
|
1202
1202
|
return accountId;
|
|
1203
1203
|
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Throw an error if there is no API token available.
|
|
1207
|
+
*/
|
|
1208
|
+
export function requireApiToken(): string {
|
|
1209
|
+
const authToken = getAPIToken();
|
|
1210
|
+
if (!authToken) {
|
|
1211
|
+
throw new Error("No API token found.");
|
|
1212
|
+
}
|
|
1213
|
+
return authToken;
|
|
1214
|
+
}
|
package/src/whoami.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import Table from "ink-table";
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { fetchListResult, fetchResult } from "./cfetch";
|
|
5
5
|
import { logger } from "./logger";
|
|
6
|
-
import { getAPIToken } from "./user";
|
|
6
|
+
import { getAPIToken, getCloudflareAPITokenFromEnv } from "./user";
|
|
7
7
|
|
|
8
8
|
export async function whoami() {
|
|
9
9
|
logger.log("Getting User settings...");
|
|
@@ -48,10 +48,11 @@ export interface UserInfo {
|
|
|
48
48
|
|
|
49
49
|
export async function getUserInfo(): Promise<UserInfo | undefined> {
|
|
50
50
|
const apiToken = getAPIToken();
|
|
51
|
+
const apiTokenFromEnv = getCloudflareAPITokenFromEnv();
|
|
51
52
|
return apiToken
|
|
52
53
|
? {
|
|
53
54
|
apiToken,
|
|
54
|
-
authType: "OAuth",
|
|
55
|
+
authType: apiTokenFromEnv ? "API" : "OAuth",
|
|
55
56
|
email: await getEmail(),
|
|
56
57
|
accounts: await getAccounts(),
|
|
57
58
|
}
|
package/src/worker.ts
CHANGED