wrangler 2.0.23 → 2.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/bin/wrangler.js +1 -1
- package/miniflare-dist/index.mjs +96 -34
- package/package.json +9 -4
- package/src/__tests__/configuration.test.ts +88 -16
- package/src/__tests__/dev.test.tsx +3 -0
- package/src/__tests__/generate.test.ts +93 -0
- package/src/__tests__/helpers/mock-cfetch.ts +54 -2
- package/src/__tests__/index.test.ts +10 -27
- package/src/__tests__/jest.setup.ts +31 -1
- package/src/__tests__/kv.test.ts +2 -2
- package/src/__tests__/metrics.test.ts +5 -0
- package/src/__tests__/publish.test.ts +497 -254
- package/src/__tests__/r2.test.ts +155 -71
- package/src/__tests__/user.test.ts +1 -0
- package/src/__tests__/validate-dev-props.test.ts +56 -0
- package/src/__tests__/whoami.test.tsx +60 -1
- package/src/bundle.ts +278 -44
- package/src/cfetch/internal.ts +34 -2
- package/src/config/config.ts +7 -2
- package/src/config/environment.ts +40 -8
- package/src/config/index.ts +13 -0
- package/src/config/validation.ts +101 -7
- package/src/create-worker-upload-form.ts +25 -0
- package/src/dev/dev.tsx +107 -28
- package/src/dev/local.tsx +20 -10
- package/src/dev/remote.tsx +39 -8
- package/src/dev/use-esbuild.ts +25 -0
- package/src/dev/validate-dev-props.ts +31 -0
- package/src/dev-registry.tsx +157 -0
- package/src/dev.tsx +93 -75
- package/src/generate.ts +112 -14
- package/src/index.tsx +182 -4
- package/src/inspect.ts +93 -5
- package/src/metrics/index.ts +1 -0
- package/src/metrics/metrics-dispatcher.ts +1 -0
- package/src/metrics/metrics-usage-headers.ts +24 -0
- package/src/metrics/send-event.ts +2 -2
- package/src/module-collection.ts +3 -3
- package/src/pages/constants.ts +1 -0
- package/src/pages/deployments.tsx +1 -1
- package/src/pages/dev.tsx +6 -1
- package/src/pages/publish.tsx +1 -1
- package/src/pages/upload.tsx +32 -13
- package/src/publish.ts +126 -112
- package/src/r2.ts +68 -0
- package/src/user/user.tsx +20 -2
- package/src/whoami.tsx +79 -1
- package/src/worker.ts +12 -0
- package/templates/first-party-worker-module-facade.ts +18 -0
- package/templates/format-dev-errors.ts +32 -0
- package/templates/{static-asset-facade.js → serve-static-assets.ts} +21 -7
- package/templates/service-bindings-module-facade.js +51 -0
- package/templates/service-bindings-sw-facade.js +39 -0
- package/wrangler-dist/cli.js +40192 -15265
package/src/publish.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { createWorkerUploadForm } from "./create-worker-upload-form";
|
|
|
11
11
|
import { confirm } from "./dialogs";
|
|
12
12
|
import { getMigrationsToUpload } from "./durable";
|
|
13
13
|
import { logger } from "./logger";
|
|
14
|
+
import { getMetricsUsageHeaders } from "./metrics";
|
|
14
15
|
import { ParseError } from "./parse";
|
|
15
16
|
import { syncAssets } from "./sites";
|
|
16
17
|
import { getZoneForRoute } from "./zones";
|
|
@@ -51,6 +52,27 @@ type Props = {
|
|
|
51
52
|
|
|
52
53
|
type RouteObject = ZoneIdRoute | ZoneNameRoute | CustomDomainRoute;
|
|
53
54
|
|
|
55
|
+
export type CustomDomain = {
|
|
56
|
+
id: string;
|
|
57
|
+
zone_id: string;
|
|
58
|
+
zone_name: string;
|
|
59
|
+
hostname: string;
|
|
60
|
+
service: string;
|
|
61
|
+
environment: string;
|
|
62
|
+
};
|
|
63
|
+
type UpdatedCustomDomain = CustomDomain & { modified: boolean };
|
|
64
|
+
type ConflictingCustomDomain = CustomDomain & {
|
|
65
|
+
external_dns_record_id?: string;
|
|
66
|
+
external_cert_id?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type CustomDomainChangeset = {
|
|
70
|
+
added: CustomDomain[];
|
|
71
|
+
removed: CustomDomain[];
|
|
72
|
+
updated: UpdatedCustomDomain[];
|
|
73
|
+
conflicting: ConflictingCustomDomain[];
|
|
74
|
+
};
|
|
75
|
+
|
|
54
76
|
function sleep(ms: number) {
|
|
55
77
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
56
78
|
}
|
|
@@ -79,58 +101,27 @@ function renderRoute(route: Route): string {
|
|
|
79
101
|
return result;
|
|
80
102
|
}
|
|
81
103
|
|
|
82
|
-
// this function takes a string with quotes in it
|
|
83
|
-
// (i.e. `hello "world", if that really is your name`)
|
|
84
|
-
// and peels out the first instance of a substring
|
|
85
|
-
// bounded by quotes (so, in the example above, `world`)
|
|
86
|
-
//
|
|
87
|
-
// this is useful because the /domains api will return
|
|
88
|
-
// which domains conflicted in an error message, bounded
|
|
89
|
-
// by a string, which we can use to provide helpful
|
|
90
|
-
// messages to a user
|
|
91
|
-
function getQuoteBoundedSubstring(content: string) {
|
|
92
|
-
const matches = content.split('"');
|
|
93
|
-
return matches[1] ?? "";
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function isOriginConflictError(
|
|
97
|
-
e: unknown
|
|
98
|
-
): e is { code: 100116; message: string; notes: Array<{ text: string }> } {
|
|
99
|
-
return (
|
|
100
|
-
typeof e === "object" &&
|
|
101
|
-
e !== null &&
|
|
102
|
-
(e as { code: number }).code === 100116
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function isDNSConflictError(
|
|
107
|
-
e: unknown
|
|
108
|
-
): e is { code: 100117; message: string; notes: Array<{ text: string }> } {
|
|
109
|
-
return (
|
|
110
|
-
typeof e === "object" &&
|
|
111
|
-
e !== null &&
|
|
112
|
-
(e as { code: number }).code === 100117
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// empty error class to throw and then explicitly catch via `instanceof`
|
|
117
|
-
class CustomDomainOverrideRejected extends Error {}
|
|
118
|
-
|
|
119
104
|
// publishing to custom domains involves a few more steps than just updating
|
|
120
105
|
// the routing table, and thus the api implementing it is fairly defensive -
|
|
121
106
|
// it will error eagerly on conflicts against existing domains or existing
|
|
122
107
|
// managed DNS records
|
|
123
|
-
|
|
124
|
-
// however, you can pass params to override the errors.
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
108
|
+
|
|
109
|
+
// however, you can pass params to override the errors. to know if we should
|
|
110
|
+
// override the current state, we generate a "changeset" of required actions
|
|
111
|
+
// to get to the state we want (specified by the list of custom domains). the
|
|
112
|
+
// changeset returns an "updated" collection (existing custom domains
|
|
113
|
+
// connected to other scripts) and a "conflicting" collection (the requested
|
|
114
|
+
// custom domains that have a managed, conflicting DNS record preventing the
|
|
115
|
+
// host's use as a custom domain). with this information, we can prompt to
|
|
116
|
+
// the user what will occur if we create the custom domains requested, and
|
|
117
|
+
// add the override param if they confirm the action
|
|
128
118
|
//
|
|
129
119
|
// if a user does not confirm that they want to override, we skip publishing
|
|
130
120
|
// to these custom domains, but continue on through the rest of the
|
|
131
121
|
// publish stage
|
|
132
|
-
function publishCustomDomains(
|
|
122
|
+
async function publishCustomDomains(
|
|
133
123
|
workerUrl: string,
|
|
124
|
+
accountId: string,
|
|
134
125
|
domains: Array<RouteObject>
|
|
135
126
|
): Promise<string[]> {
|
|
136
127
|
const config = {
|
|
@@ -146,79 +137,81 @@ function publishCustomDomains(
|
|
|
146
137
|
};
|
|
147
138
|
});
|
|
148
139
|
|
|
140
|
+
const fail = () => {
|
|
141
|
+
return [
|
|
142
|
+
domains.length > 1
|
|
143
|
+
? `Publishing to ${domains.length} Custom Domains was skipped, fix conflicts and try again`
|
|
144
|
+
: `Publishing to Custom Domain "${domains[0].pattern}" was skipped, fix conflict and try again`,
|
|
145
|
+
];
|
|
146
|
+
};
|
|
147
|
+
|
|
149
148
|
if (!process.stdout.isTTY) {
|
|
150
149
|
// running in non-interactive mode.
|
|
151
150
|
// existing origins / dns records are not indicative of errors,
|
|
152
151
|
// so we aggressively update rather than aggressively fail
|
|
153
152
|
config.override_existing_origin = true;
|
|
154
153
|
config.override_existing_dns_record = true;
|
|
154
|
+
} else {
|
|
155
|
+
// get a changeset for operations required to achieve a state with the requested domains
|
|
156
|
+
const changeset = await fetchResult<CustomDomainChangeset>(
|
|
157
|
+
`${workerUrl}/domains/changeset?replace_state=true`,
|
|
158
|
+
{
|
|
159
|
+
method: "POST",
|
|
160
|
+
body: JSON.stringify(origins),
|
|
161
|
+
headers: {
|
|
162
|
+
"Content-Type": "application/json",
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const updatesRequired = changeset.updated.filter(
|
|
168
|
+
(domain) => domain.modified
|
|
169
|
+
);
|
|
170
|
+
if (updatesRequired.length > 0) {
|
|
171
|
+
// find out which scripts the conflict domains are already attached to
|
|
172
|
+
// so we can provide that in the confirmation prompt
|
|
173
|
+
const existing = await Promise.all(
|
|
174
|
+
updatesRequired.map((domain) =>
|
|
175
|
+
fetchResult<CustomDomain>(
|
|
176
|
+
`/accounts/${accountId}/workers/domains/records/${domain.id}`
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
const existingRendered = existing
|
|
181
|
+
.map(
|
|
182
|
+
(domain) =>
|
|
183
|
+
`\t• ${domain.hostname} (used as a domain for "${domain.service}")`
|
|
184
|
+
)
|
|
185
|
+
.join("\n");
|
|
186
|
+
const message = `Custom Domains already exist for these domains:
|
|
187
|
+
${existingRendered}
|
|
188
|
+
Update them to point to this script instead?`;
|
|
189
|
+
if (!(await confirm(message))) return fail();
|
|
190
|
+
config.override_existing_origin = true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (changeset.conflicting.length > 0) {
|
|
194
|
+
const conflicitingRendered = changeset.conflicting
|
|
195
|
+
.map((domain) => `\t• ${domain.hostname}`)
|
|
196
|
+
.join("\n");
|
|
197
|
+
const message = `You already have DNS records that conflict for these Custom Domains:
|
|
198
|
+
${conflicitingRendered}
|
|
199
|
+
Update them to point to this script instead?`;
|
|
200
|
+
if (!(await confirm(message))) return fail();
|
|
201
|
+
config.override_existing_dns_record = true;
|
|
202
|
+
}
|
|
155
203
|
}
|
|
156
204
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
// while retaining the flexibility of promise chain fall-throughs. We can group error
|
|
160
|
-
// handling logic in dedicated catch calls, and all we have to do is re-throw an
|
|
161
|
-
// error and it will pass down to the next catch call
|
|
162
|
-
return fetchResult(`${workerUrl}/domains`, {
|
|
205
|
+
// publish to domains
|
|
206
|
+
await fetchResult(`${workerUrl}/domains/records`, {
|
|
163
207
|
method: "PUT",
|
|
164
208
|
body: JSON.stringify({ ...config, origins }),
|
|
165
209
|
headers: {
|
|
166
210
|
"Content-Type": "application/json",
|
|
167
211
|
},
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const conflictingOrigins = getQuoteBoundedSubstring(err.notes[0].text);
|
|
172
|
-
const shouldContinue = await confirm(
|
|
173
|
-
`Custom Domains already exist for these domains: "${conflictingOrigins}"\nUpdate them to point to this script instead?`
|
|
174
|
-
);
|
|
175
|
-
if (!shouldContinue) {
|
|
176
|
-
throw new CustomDomainOverrideRejected();
|
|
177
|
-
}
|
|
178
|
-
config.override_existing_origin = true;
|
|
179
|
-
await fetchResult(`${workerUrl}/domains`, {
|
|
180
|
-
method: "PUT",
|
|
181
|
-
body: JSON.stringify({ ...config, origins }),
|
|
182
|
-
headers: {
|
|
183
|
-
"Content-Type": "application/json",
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
} else {
|
|
187
|
-
throw err;
|
|
188
|
-
}
|
|
189
|
-
})
|
|
190
|
-
.catch(async (err) => {
|
|
191
|
-
if (isDNSConflictError(err)) {
|
|
192
|
-
const conflictingOrigins = getQuoteBoundedSubstring(err.notes[0].text);
|
|
193
|
-
const shouldContinue = await confirm(
|
|
194
|
-
`You already have conflicting DNS records for these domains: "${conflictingOrigins}"\nUpdate them to point to this script instead?`
|
|
195
|
-
);
|
|
196
|
-
if (!shouldContinue) {
|
|
197
|
-
throw new CustomDomainOverrideRejected();
|
|
198
|
-
}
|
|
199
|
-
config.override_existing_dns_record = true;
|
|
200
|
-
await fetchResult(`${workerUrl}/domains`, {
|
|
201
|
-
method: "PUT",
|
|
202
|
-
body: JSON.stringify({ ...config, origins }),
|
|
203
|
-
headers: {
|
|
204
|
-
"Content-Type": "application/json",
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
} else {
|
|
208
|
-
throw err;
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
.then(() => domains.map((domain) => renderRoute(domain)))
|
|
212
|
-
.catch((err) => {
|
|
213
|
-
if (err instanceof CustomDomainOverrideRejected) {
|
|
214
|
-
return [
|
|
215
|
-
domains.length > 1
|
|
216
|
-
? `Publishing to ${domains.length} Custom Domains was skipped, fix conflicts and try again`
|
|
217
|
-
: `Publishing to Custom Domain "${domains[0].pattern}" was skipped, fix conflict and try again`,
|
|
218
|
-
];
|
|
219
|
-
}
|
|
220
|
-
throw err;
|
|
221
|
-
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return domains.map((domain) => renderRoute(domain));
|
|
222
215
|
}
|
|
223
216
|
|
|
224
217
|
export default async function publish(props: Props): Promise<void> {
|
|
@@ -391,6 +384,17 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
|
|
|
391
384
|
nodeCompat,
|
|
392
385
|
define: config.define,
|
|
393
386
|
checkFetch: false,
|
|
387
|
+
assets: config.assets && {
|
|
388
|
+
...config.assets,
|
|
389
|
+
// enable the cache when publishing
|
|
390
|
+
bypassCache: false,
|
|
391
|
+
},
|
|
392
|
+
services: config.services,
|
|
393
|
+
// We don't set workerDefinitions here,
|
|
394
|
+
// because we don't want to apply the dev-time
|
|
395
|
+
// facades on top of it
|
|
396
|
+
workerDefinitions: undefined,
|
|
397
|
+
firstPartyWorkerDevFacade: false,
|
|
394
398
|
}
|
|
395
399
|
);
|
|
396
400
|
|
|
@@ -440,6 +444,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
|
|
|
440
444
|
r2_buckets: config.r2_buckets,
|
|
441
445
|
services: config.services,
|
|
442
446
|
worker_namespaces: config.worker_namespaces,
|
|
447
|
+
logfwdr: config.logfwdr,
|
|
443
448
|
unsafe: config.unsafe?.bindings,
|
|
444
449
|
};
|
|
445
450
|
|
|
@@ -492,6 +497,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
|
|
|
492
497
|
{
|
|
493
498
|
method: "PUT",
|
|
494
499
|
body: createWorkerUploadForm(worker),
|
|
500
|
+
headers: await getMetricsUsageHeaders(config.send_metrics),
|
|
495
501
|
},
|
|
496
502
|
new URLSearchParams({
|
|
497
503
|
include_subdomain_availability: "true",
|
|
@@ -503,13 +509,15 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
|
|
|
503
509
|
|
|
504
510
|
available_on_subdomain = result.available_on_subdomain;
|
|
505
511
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
512
|
+
if (config.first_party_worker) {
|
|
513
|
+
// Print some useful information returned after publishing
|
|
514
|
+
// Not all fields will be populated for every worker
|
|
515
|
+
// These fields are likely to be scraped by tools, so do not rename
|
|
516
|
+
if (result.id) logger.log("Worker ID: ", result.id);
|
|
517
|
+
if (result.etag) logger.log("Worker ETag: ", result.etag);
|
|
518
|
+
if (result.pipeline_hash)
|
|
519
|
+
logger.log("Worker PipelineHash: ", result.pipeline_hash);
|
|
520
|
+
}
|
|
513
521
|
}
|
|
514
522
|
} finally {
|
|
515
523
|
if (typeof destination !== "string") {
|
|
@@ -590,7 +598,9 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
|
|
|
590
598
|
|
|
591
599
|
// Update custom domains for the script
|
|
592
600
|
if (customDomainsOnly.length > 0) {
|
|
593
|
-
deployments.push(
|
|
601
|
+
deployments.push(
|
|
602
|
+
publishCustomDomains(workerUrl, accountId, customDomainsOnly)
|
|
603
|
+
);
|
|
594
604
|
}
|
|
595
605
|
|
|
596
606
|
// Configure any schedules for the script.
|
|
@@ -614,7 +624,11 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
|
|
|
614
624
|
if (deployments.length > 0) {
|
|
615
625
|
logger.log("Published", workerName, formatTime(deployMs));
|
|
616
626
|
for (const target of targets.flat()) {
|
|
617
|
-
|
|
627
|
+
// Append protocol only on workers.dev domains
|
|
628
|
+
logger.log(
|
|
629
|
+
" ",
|
|
630
|
+
(target.endsWith("workers.dev") ? "https://" : "") + target
|
|
631
|
+
);
|
|
618
632
|
}
|
|
619
633
|
} else {
|
|
620
634
|
logger.log("No publish targets for", workerName, formatTime(deployMs));
|
package/src/r2.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
1
2
|
import { fetchResult } from "./cfetch";
|
|
3
|
+
import { fetchR2Objects } from "./cfetch/internal";
|
|
4
|
+
import type { HeadersInit } from "undici";
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Information about a bucket, returned from `listR2Buckets()`.
|
|
@@ -48,3 +51,68 @@ export async function deleteR2Bucket(
|
|
|
48
51
|
{ method: "DELETE" }
|
|
49
52
|
);
|
|
50
53
|
}
|
|
54
|
+
|
|
55
|
+
export function bucketAndKeyFromObjectPath(objectPath = ""): {
|
|
56
|
+
bucket: string;
|
|
57
|
+
key: string;
|
|
58
|
+
} {
|
|
59
|
+
const match = /^([^/]+)\/(.*)/.exec(objectPath);
|
|
60
|
+
if (match === null) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`The object path must be in the form of {bucket}/{key} you provided ${objectPath}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { bucket: match[1], key: match[2] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Downloads an object
|
|
71
|
+
*/
|
|
72
|
+
export async function getR2Object(
|
|
73
|
+
accountId: string,
|
|
74
|
+
bucketName: string,
|
|
75
|
+
objectName: string
|
|
76
|
+
): Promise<Readable> {
|
|
77
|
+
const response = await fetchR2Objects(
|
|
78
|
+
`/accounts/${accountId}/r2/buckets/${bucketName}/objects/${objectName}`,
|
|
79
|
+
{ method: "GET" }
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return Readable.from(response.body);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Uploads an object
|
|
87
|
+
*/
|
|
88
|
+
export async function putR2Object(
|
|
89
|
+
accountId: string,
|
|
90
|
+
bucketName: string,
|
|
91
|
+
objectName: string,
|
|
92
|
+
object: Readable | Buffer,
|
|
93
|
+
options: Record<string, unknown>
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
const headerKeys = [
|
|
96
|
+
"content-length",
|
|
97
|
+
"content-type",
|
|
98
|
+
"content-disposition",
|
|
99
|
+
"content-encoding",
|
|
100
|
+
"content-language",
|
|
101
|
+
"cache-control",
|
|
102
|
+
"expires",
|
|
103
|
+
];
|
|
104
|
+
const headers: HeadersInit = {};
|
|
105
|
+
for (const key of headerKeys) {
|
|
106
|
+
const value = options[key] || "";
|
|
107
|
+
if (value && typeof value === "string") headers[key] = value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await fetchR2Objects(
|
|
111
|
+
`/accounts/${accountId}/r2/buckets/${bucketName}/objects/${objectName}`,
|
|
112
|
+
{
|
|
113
|
+
body: object,
|
|
114
|
+
headers,
|
|
115
|
+
method: "PUT",
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
}
|
package/src/user/user.tsx
CHANGED
|
@@ -262,6 +262,7 @@ interface State extends AuthTokens {
|
|
|
262
262
|
interface AuthTokens {
|
|
263
263
|
accessToken?: AccessToken;
|
|
264
264
|
refreshToken?: RefreshToken;
|
|
265
|
+
scopes?: Scope[];
|
|
265
266
|
/** @deprecated - this field was only provided by the deprecated `wrangler1 config` command. */
|
|
266
267
|
apiToken?: string;
|
|
267
268
|
}
|
|
@@ -279,6 +280,7 @@ export interface UserAuthConfig {
|
|
|
279
280
|
oauth_token?: string;
|
|
280
281
|
refresh_token?: string;
|
|
281
282
|
expiration_time?: string;
|
|
283
|
+
scopes?: string[];
|
|
282
284
|
/** @deprecated - this field was only provided by the deprecated `wrangler1 config` command. */
|
|
283
285
|
api_token?: string;
|
|
284
286
|
}
|
|
@@ -355,7 +357,7 @@ function getAuthTokens(config?: UserAuthConfig): AuthTokens | undefined {
|
|
|
355
357
|
if (getAuthFromEnv()) return;
|
|
356
358
|
|
|
357
359
|
// otherwise try loading from the user auth config file.
|
|
358
|
-
const { oauth_token, refresh_token, expiration_time, api_token } =
|
|
360
|
+
const { oauth_token, refresh_token, expiration_time, scopes, api_token } =
|
|
359
361
|
config || readAuthConfigFile();
|
|
360
362
|
|
|
361
363
|
if (oauth_token) {
|
|
@@ -366,6 +368,7 @@ function getAuthTokens(config?: UserAuthConfig): AuthTokens | undefined {
|
|
|
366
368
|
expiry: expiration_time ?? "2000-01-01:00:00:00+00:00",
|
|
367
369
|
},
|
|
368
370
|
refreshToken: { value: refresh_token ?? "" },
|
|
371
|
+
scopes: scopes as Scope[],
|
|
369
372
|
};
|
|
370
373
|
} else if (api_token) {
|
|
371
374
|
logger.warn(
|
|
@@ -959,6 +962,7 @@ export async function login(props?: LoginProps): Promise<boolean> {
|
|
|
959
962
|
oauth_token: exchange.token?.value ?? "",
|
|
960
963
|
expiration_time: exchange.token?.expiry,
|
|
961
964
|
refresh_token: exchange.refreshToken?.value,
|
|
965
|
+
scopes: exchange.scopes,
|
|
962
966
|
});
|
|
963
967
|
res.writeHead(307, {
|
|
964
968
|
Location:
|
|
@@ -1003,8 +1007,14 @@ async function refreshToken(): Promise<boolean> {
|
|
|
1003
1007
|
expiry: "",
|
|
1004
1008
|
},
|
|
1005
1009
|
refreshToken: { value: refresh_token } = {},
|
|
1010
|
+
scopes,
|
|
1006
1011
|
} = await exchangeRefreshTokenForAccessToken();
|
|
1007
|
-
writeAuthConfigFile({
|
|
1012
|
+
writeAuthConfigFile({
|
|
1013
|
+
oauth_token,
|
|
1014
|
+
expiration_time,
|
|
1015
|
+
refresh_token,
|
|
1016
|
+
scopes,
|
|
1017
|
+
});
|
|
1008
1018
|
return true;
|
|
1009
1019
|
} catch (err) {
|
|
1010
1020
|
return false;
|
|
@@ -1169,3 +1179,11 @@ export function getAccountFromCache():
|
|
|
1169
1179
|
"wrangler-account.json"
|
|
1170
1180
|
).account;
|
|
1171
1181
|
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Get the scopes of the following token, will only return scopes
|
|
1185
|
+
* if the token is an OAuth token.
|
|
1186
|
+
*/
|
|
1187
|
+
export function getScopes(): Scope[] | undefined {
|
|
1188
|
+
return LocalState.scopes;
|
|
1189
|
+
}
|
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, getAuthFromEnv } from "./user";
|
|
6
|
+
import { getAPIToken, getAuthFromEnv, getScopes } from "./user";
|
|
7
7
|
|
|
8
8
|
export async function whoami() {
|
|
9
9
|
logger.log("Getting User settings...");
|
|
@@ -17,6 +17,10 @@ export function WhoAmI({ user }: { user: UserInfo | undefined }) {
|
|
|
17
17
|
<>
|
|
18
18
|
<Email tokenType={user.authType} email={user.email}></Email>
|
|
19
19
|
<Accounts accounts={user.accounts}></Accounts>
|
|
20
|
+
<Permissions
|
|
21
|
+
tokenType={user.authType}
|
|
22
|
+
tokenPermissions={user.tokenPermissions}
|
|
23
|
+
/>
|
|
20
24
|
</>
|
|
21
25
|
) : (
|
|
22
26
|
<Text>You are not authenticated. Please run `wrangler login`.</Text>
|
|
@@ -46,17 +50,51 @@ function Accounts(props: { accounts: AccountInfo[] }) {
|
|
|
46
50
|
return <Table data={accounts}></Table>;
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
function Permissions(props: {
|
|
54
|
+
tokenPermissions: string[] | undefined;
|
|
55
|
+
tokenType: string;
|
|
56
|
+
}) {
|
|
57
|
+
const permissions =
|
|
58
|
+
props.tokenPermissions?.map((scope) => scope.split(":")) || [];
|
|
59
|
+
return props.tokenType === "OAuth Token" ? (
|
|
60
|
+
props.tokenPermissions ? (
|
|
61
|
+
<>
|
|
62
|
+
<Text>
|
|
63
|
+
🔓 Token Permissions: If scopes are missing, you may need to logout
|
|
64
|
+
and re-login.
|
|
65
|
+
</Text>
|
|
66
|
+
<Text>Scope (Access)</Text>
|
|
67
|
+
{permissions.map(([type, name]) => (
|
|
68
|
+
<>
|
|
69
|
+
<Text>
|
|
70
|
+
- {type} {name && `(${name})`}
|
|
71
|
+
</Text>
|
|
72
|
+
</>
|
|
73
|
+
))}
|
|
74
|
+
</>
|
|
75
|
+
) : null
|
|
76
|
+
) : (
|
|
77
|
+
<Text>
|
|
78
|
+
🔓 To see token permissions visit
|
|
79
|
+
https://dash.cloudflare.com/profile/api-tokens
|
|
80
|
+
</Text>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
49
84
|
export interface UserInfo {
|
|
50
85
|
apiToken: string;
|
|
51
86
|
authType: string;
|
|
52
87
|
email: string | undefined;
|
|
53
88
|
accounts: AccountInfo[];
|
|
89
|
+
tokenPermissions: string[] | undefined;
|
|
54
90
|
}
|
|
55
91
|
|
|
56
92
|
export async function getUserInfo(): Promise<UserInfo | undefined> {
|
|
57
93
|
const apiToken = getAPIToken();
|
|
58
94
|
if (!apiToken) return;
|
|
59
95
|
|
|
96
|
+
const tokenPermissions = await getTokenPermissions();
|
|
97
|
+
|
|
60
98
|
const usingEnvAuth = !!getAuthFromEnv();
|
|
61
99
|
const usingGlobalAuthKey = "authKey" in apiToken;
|
|
62
100
|
return {
|
|
@@ -68,6 +106,7 @@ export async function getUserInfo(): Promise<UserInfo | undefined> {
|
|
|
68
106
|
: "OAuth Token",
|
|
69
107
|
email: "authEmail" in apiToken ? apiToken.authEmail : await getEmail(),
|
|
70
108
|
accounts: await getAccounts(),
|
|
109
|
+
tokenPermissions,
|
|
71
110
|
};
|
|
72
111
|
}
|
|
73
112
|
|
|
@@ -89,3 +128,42 @@ type AccountInfo = { name: string; id: string };
|
|
|
89
128
|
async function getAccounts(): Promise<AccountInfo[]> {
|
|
90
129
|
return await fetchListResult<AccountInfo>("/accounts");
|
|
91
130
|
}
|
|
131
|
+
|
|
132
|
+
async function getTokenPermissions(): Promise<string[] | undefined> {
|
|
133
|
+
// Tokens can either be API tokens or Oauth tokens.
|
|
134
|
+
// Here we only extract permissions from OAuth tokens.
|
|
135
|
+
|
|
136
|
+
return getScopes() as string[];
|
|
137
|
+
|
|
138
|
+
// In future we may be able to get the token permissions on an API token,
|
|
139
|
+
// but currently we cannot as that permission is not able to be added to
|
|
140
|
+
// an API token.
|
|
141
|
+
|
|
142
|
+
// try {
|
|
143
|
+
// // First we get the token identifier (only returned on API tokens)
|
|
144
|
+
// const { id } = await fetchResult<{ id: string }>("/user/tokens/verify");
|
|
145
|
+
|
|
146
|
+
// // Get the token permissions for the current token
|
|
147
|
+
// const { policies } = await fetchResult<{
|
|
148
|
+
// policies: { id: string; name: string }[];
|
|
149
|
+
// }>(`/user/tokens/${id}`);
|
|
150
|
+
|
|
151
|
+
// return policies.map((p) => p.name);
|
|
152
|
+
// } catch (e) {
|
|
153
|
+
// if ((e as { code?: number }).code === 1000) {
|
|
154
|
+
// // Invalid token - Oauth token
|
|
155
|
+
|
|
156
|
+
// // Get scopes
|
|
157
|
+
// const scopes = getScopes() as string[];
|
|
158
|
+
// // We may not get scopes back if they are not currently cached,
|
|
159
|
+
// // however next time an access token is requested,
|
|
160
|
+
// // the scopes will be added to the cache.
|
|
161
|
+
// return scopes;
|
|
162
|
+
// } else if ((e as { code?: number }).code === 9109) {
|
|
163
|
+
// // Token cannot view its permissions
|
|
164
|
+
// return undefined;
|
|
165
|
+
// } else {
|
|
166
|
+
// throw e;
|
|
167
|
+
// }
|
|
168
|
+
// }
|
|
169
|
+
}
|
package/src/worker.ts
CHANGED
|
@@ -127,6 +127,16 @@ interface CfWorkerNamespace {
|
|
|
127
127
|
namespace: string;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
interface CfLogfwdr {
|
|
131
|
+
schema: string | undefined;
|
|
132
|
+
bindings: CfLogfwdrBinding[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface CfLogfwdrBinding {
|
|
136
|
+
name: string;
|
|
137
|
+
destination: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
130
140
|
interface CfUnsafeBinding {
|
|
131
141
|
name: string;
|
|
132
142
|
type: string;
|
|
@@ -174,6 +184,7 @@ export interface CfWorkerInit {
|
|
|
174
184
|
r2_buckets: CfR2Bucket[] | undefined;
|
|
175
185
|
services: CfService[] | undefined;
|
|
176
186
|
worker_namespaces: CfWorkerNamespace[] | undefined;
|
|
187
|
+
logfwdr: CfLogfwdr | undefined;
|
|
177
188
|
unsafe: CfUnsafeBinding[] | undefined;
|
|
178
189
|
};
|
|
179
190
|
migrations: CfDurableObjectMigrations | undefined;
|
|
@@ -188,4 +199,5 @@ export interface CfWorkerContext {
|
|
|
188
199
|
zone: string | undefined;
|
|
189
200
|
host: string | undefined;
|
|
190
201
|
routes: Route[] | undefined;
|
|
202
|
+
sendMetrics: boolean | undefined;
|
|
191
203
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @ts-ignore entry point will get replaced
|
|
2
|
+
import worker from "__ENTRY_POINT__";
|
|
3
|
+
// @ts-ignore entry point will get replaced
|
|
4
|
+
export * from "__ENTRY_POINT__";
|
|
5
|
+
|
|
6
|
+
type Env = {
|
|
7
|
+
// TODO: type this
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Setup globals/vars as required
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
16
|
+
return worker.fetch(request, env, ctx);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @ts-expect-error We'll swap in the entry point during build
|
|
2
|
+
import Worker from "__ENTRY_POINT__";
|
|
3
|
+
|
|
4
|
+
// @ts-expect-error
|
|
5
|
+
export * from "__ENTRY_POINT__";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
async fetch(req: Request, env: unknown, ctx: ExecutionContext) {
|
|
9
|
+
try {
|
|
10
|
+
return await Worker.fetch(req, env, ctx);
|
|
11
|
+
} catch (err) {
|
|
12
|
+
return new Response(
|
|
13
|
+
`<!DOCTYPE html>
|
|
14
|
+
<html>
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="utf-8" />
|
|
17
|
+
<title>Error</title>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<pre>${(err as Error).stack}</pre>
|
|
21
|
+
</body>
|
|
22
|
+
</html>`,
|
|
23
|
+
{
|
|
24
|
+
status: 500,
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "text/html",
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|