wrangler 2.0.23 → 2.0.26
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 +235 -47
- package/package.json +11 -6
- package/src/__tests__/configuration.test.ts +89 -17
- package/src/__tests__/dev.test.tsx +29 -4
- package/src/__tests__/generate.test.ts +93 -0
- package/src/__tests__/helpers/mock-cfetch.ts +87 -2
- package/src/__tests__/index.test.ts +10 -27
- package/src/__tests__/init.test.ts +537 -359
- package/src/__tests__/jest.setup.ts +34 -1
- package/src/__tests__/kv.test.ts +2 -2
- package/src/__tests__/metrics.test.ts +5 -0
- package/src/__tests__/pages.test.ts +14 -0
- package/src/__tests__/publish.test.ts +497 -254
- package/src/__tests__/r2.test.ts +173 -71
- package/src/__tests__/tail.test.ts +112 -42
- 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/api/dev.ts +7 -0
- package/src/bundle.ts +279 -44
- package/src/cfetch/internal.ts +73 -2
- package/src/config/config.ts +8 -3
- package/src/config/environment.ts +40 -8
- package/src/config/index.ts +13 -0
- package/src/config/validation.ts +102 -8
- package/src/create-worker-upload-form.ts +25 -0
- package/src/dev/dev.tsx +121 -28
- package/src/dev/local.tsx +88 -14
- package/src/dev/remote.tsx +39 -8
- package/src/dev/use-esbuild.ts +28 -0
- package/src/dev/validate-dev-props.ts +31 -0
- package/src/dev-registry.tsx +160 -0
- package/src/dev.tsx +107 -80
- package/src/generate.ts +112 -14
- package/src/index.tsx +212 -4
- package/src/init.ts +111 -38
- package/src/inspect.ts +90 -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/miniflare-cli/assets.ts +27 -16
- package/src/miniflare-cli/index.ts +124 -2
- package/src/module-collection.ts +3 -3
- package/src/pages/build.tsx +75 -41
- package/src/pages/constants.ts +5 -0
- package/src/pages/deployments.tsx +10 -10
- package/src/pages/dev.tsx +177 -52
- package/src/pages/errors.ts +22 -0
- package/src/pages/functions/buildPlugin.ts +4 -0
- package/src/pages/functions/buildWorker.ts +4 -0
- package/src/pages/functions/routes-consolidation.test.ts +250 -0
- package/src/pages/functions/routes-consolidation.ts +73 -0
- package/src/pages/functions/routes-transformation.test.ts +271 -0
- package/src/pages/functions/routes-transformation.ts +122 -0
- package/src/pages/functions.tsx +96 -0
- package/src/pages/index.tsx +65 -55
- package/src/pages/projects.tsx +9 -3
- package/src/pages/publish.tsx +76 -23
- package/src/pages/types.ts +9 -0
- package/src/pages/upload.tsx +38 -21
- package/src/publish.ts +126 -112
- package/src/r2.ts +81 -0
- package/src/tail/filters.ts +3 -1
- package/src/tail/index.ts +15 -2
- package/src/tail/printing.ts +43 -3
- 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/pages-template-plugin.ts +16 -4
- package/templates/pages-template-worker.ts +16 -5
- package/templates/{static-asset-facade.js → serve-static-assets.ts} +21 -7
- package/templates/service-bindings-module-facade.js +54 -0
- package/templates/service-bindings-sw-facade.js +42 -0
- package/wrangler-dist/cli.d.ts +7 -0
- package/wrangler-dist/cli.js +40851 -15332
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,81 @@ 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
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Delete an Object
|
|
121
|
+
*/
|
|
122
|
+
export async function deleteR2Object(
|
|
123
|
+
accountId: string,
|
|
124
|
+
bucketName: string,
|
|
125
|
+
objectName: string
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
await fetchR2Objects(
|
|
128
|
+
`/accounts/${accountId}/r2/buckets/${bucketName}/objects/${objectName}`,
|
|
129
|
+
{ method: "DELETE" }
|
|
130
|
+
);
|
|
131
|
+
}
|
package/src/tail/filters.ts
CHANGED
|
@@ -58,13 +58,14 @@ type OutcomeFilter = {
|
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* There are five possible outcomes we can get, three of which
|
|
61
|
-
* (exception, exceededCpu, and unknown) are considered errors
|
|
61
|
+
* (exception, exceededCpu, exceededMemory, and unknown) are considered errors
|
|
62
62
|
*/
|
|
63
63
|
export type Outcome =
|
|
64
64
|
| "ok"
|
|
65
65
|
| "canceled"
|
|
66
66
|
| "exception"
|
|
67
67
|
| "exceededCpu"
|
|
68
|
+
| "exceededMemory"
|
|
68
69
|
| "unknown";
|
|
69
70
|
|
|
70
71
|
/**
|
|
@@ -210,6 +211,7 @@ function parseOutcome(
|
|
|
210
211
|
case "error":
|
|
211
212
|
outcomes.add("exception");
|
|
212
213
|
outcomes.add("exceededCpu");
|
|
214
|
+
outcomes.add("exceededMemory");
|
|
213
215
|
outcomes.add("unknown");
|
|
214
216
|
break;
|
|
215
217
|
|
package/src/tail/index.ts
CHANGED
|
@@ -174,12 +174,12 @@ export type TailEventMessage = {
|
|
|
174
174
|
/**
|
|
175
175
|
* The event that triggered the worker. In the case of an HTTP request,
|
|
176
176
|
* this will be a RequestEvent. If it's a cron trigger, it'll be a
|
|
177
|
-
* ScheduledEvent.
|
|
177
|
+
* ScheduledEvent. If it's a durable object alarm, it's an AlarmEvent.
|
|
178
178
|
*
|
|
179
179
|
* Until workers-types exposes individual types for export, we'll have
|
|
180
180
|
* to just re-define these types ourselves.
|
|
181
181
|
*/
|
|
182
|
-
event: RequestEvent | ScheduledEvent | undefined | null;
|
|
182
|
+
event: RequestEvent | ScheduledEvent | AlarmEvent | undefined | null;
|
|
183
183
|
};
|
|
184
184
|
|
|
185
185
|
/**
|
|
@@ -297,3 +297,16 @@ export type ScheduledEvent = {
|
|
|
297
297
|
*/
|
|
298
298
|
scheduledTime: number;
|
|
299
299
|
};
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* A event that was triggered from a durable object alarm
|
|
303
|
+
*/
|
|
304
|
+
export type AlarmEvent = {
|
|
305
|
+
/**
|
|
306
|
+
* The datetime the alarm was scheduled for.
|
|
307
|
+
*
|
|
308
|
+
* This is sent as an ISO timestamp string (different than ScheduledEvent.scheduledTime),
|
|
309
|
+
* you should parse it later on on your own.
|
|
310
|
+
*/
|
|
311
|
+
scheduledTime: string;
|
|
312
|
+
};
|
package/src/tail/printing.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { logger } from "../logger";
|
|
2
|
-
import type { RequestEvent, ScheduledEvent, TailEventMessage } from ".";
|
|
3
2
|
import type { Outcome } from "./filters";
|
|
3
|
+
import type {
|
|
4
|
+
AlarmEvent,
|
|
5
|
+
RequestEvent,
|
|
6
|
+
ScheduledEvent,
|
|
7
|
+
TailEventMessage,
|
|
8
|
+
} from "./index";
|
|
4
9
|
import type WebSocket from "ws";
|
|
5
10
|
|
|
6
11
|
export function prettyPrintLogs(data: WebSocket.RawData): void {
|
|
@@ -14,7 +19,7 @@ export function prettyPrintLogs(data: WebSocket.RawData): void {
|
|
|
14
19
|
const outcome = prettifyOutcome(eventMessage.outcome);
|
|
15
20
|
|
|
16
21
|
logger.log(`"${cronPattern}" @ ${datetime} - ${outcome}`);
|
|
17
|
-
} else {
|
|
22
|
+
} else if (isRequestEvent(eventMessage.event)) {
|
|
18
23
|
const requestMethod = eventMessage.event?.request.method.toUpperCase();
|
|
19
24
|
const url = eventMessage.event?.request.url;
|
|
20
25
|
const outcome = prettifyOutcome(eventMessage.outcome);
|
|
@@ -25,6 +30,19 @@ export function prettyPrintLogs(data: WebSocket.RawData): void {
|
|
|
25
30
|
? `${requestMethod} ${url} - ${outcome} @ ${datetime}`
|
|
26
31
|
: `[missing request] - ${outcome} @ ${datetime}`
|
|
27
32
|
);
|
|
33
|
+
} else if (isAlarmEvent(eventMessage.event)) {
|
|
34
|
+
const outcome = prettifyOutcome(eventMessage.outcome);
|
|
35
|
+
const datetime = new Date(
|
|
36
|
+
eventMessage.event.scheduledTime
|
|
37
|
+
).toLocaleString();
|
|
38
|
+
|
|
39
|
+
logger.log(`Alarm @ ${datetime} - ${outcome}`);
|
|
40
|
+
} else {
|
|
41
|
+
// Unknown event type
|
|
42
|
+
const outcome = prettifyOutcome(eventMessage.outcome);
|
|
43
|
+
const datetime = new Date(eventMessage.eventTimestamp).toLocaleString();
|
|
44
|
+
|
|
45
|
+
logger.log(`Unknown Event - ${outcome} @ ${datetime}`);
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
if (eventMessage.logs.length > 0) {
|
|
@@ -44,12 +62,32 @@ export function jsonPrintLogs(data: WebSocket.RawData): void {
|
|
|
44
62
|
console.log(JSON.stringify(JSON.parse(data.toString()), null, 2));
|
|
45
63
|
}
|
|
46
64
|
|
|
65
|
+
function isRequestEvent(
|
|
66
|
+
event: TailEventMessage["event"]
|
|
67
|
+
): event is RequestEvent {
|
|
68
|
+
return Boolean(event && "request" in event);
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
function isScheduledEvent(
|
|
48
|
-
event:
|
|
72
|
+
event: TailEventMessage["event"]
|
|
49
73
|
): event is ScheduledEvent {
|
|
50
74
|
return Boolean(event && "cron" in event);
|
|
51
75
|
}
|
|
52
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Check to see if an event sent from a worker is an AlarmEvent.
|
|
79
|
+
*
|
|
80
|
+
* Because the only property on `AlarmEvent` is "scheduledTime", which it
|
|
81
|
+
* shares with `ScheduledEvent`, `isAlarmEvent` checks if there's _not_
|
|
82
|
+
* a "cron" property in `event` to confirm it's an alarm event.
|
|
83
|
+
*
|
|
84
|
+
* @param event An event
|
|
85
|
+
* @returns true if the event is an AlarmEvent
|
|
86
|
+
*/
|
|
87
|
+
function isAlarmEvent(event: TailEventMessage["event"]): event is AlarmEvent {
|
|
88
|
+
return Boolean(event && "scheduledTime" in event && !("cron" in event));
|
|
89
|
+
}
|
|
90
|
+
|
|
53
91
|
function prettifyOutcome(outcome: Outcome): string {
|
|
54
92
|
switch (outcome) {
|
|
55
93
|
case "ok":
|
|
@@ -58,6 +96,8 @@ function prettifyOutcome(outcome: Outcome): string {
|
|
|
58
96
|
return "Canceled";
|
|
59
97
|
case "exceededCpu":
|
|
60
98
|
return "Exceeded CPU Limit";
|
|
99
|
+
case "exceededMemory":
|
|
100
|
+
return "Exceeded Memory Limit";
|
|
61
101
|
case "exception":
|
|
62
102
|
return "Exception Thrown";
|
|
63
103
|
case "unknown":
|
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
|
+
}
|