wrangler 0.0.0-e14e589 → 0.0.0-e2a26a0
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 +51 -0
- package/bin/wrangler.js +2 -2
- package/package.json +49 -30
- package/pages/functions/buildWorker.ts +62 -0
- package/pages/functions/filepath-routing.test.ts +47 -0
- package/pages/functions/filepath-routing.ts +231 -0
- package/pages/functions/identifiers.ts +78 -0
- package/pages/functions/routes.ts +160 -0
- package/pages/functions/template-worker.ts +136 -0
- package/src/__tests__/dev.test.tsx +71 -0
- package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
- package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
- package/src/__tests__/helpers/mock-account-id.ts +30 -0
- package/src/__tests__/helpers/mock-bin.ts +33 -0
- package/src/__tests__/helpers/mock-cfetch.ts +207 -0
- package/src/__tests__/helpers/mock-console.ts +56 -0
- package/src/__tests__/helpers/mock-dialogs.ts +65 -0
- package/src/__tests__/helpers/mock-kv.ts +40 -0
- package/src/__tests__/helpers/mock-user.ts +27 -0
- package/src/__tests__/helpers/run-in-tmp.ts +29 -0
- package/src/__tests__/helpers/run-wrangler.ts +14 -0
- package/src/__tests__/index.test.ts +509 -119
- package/src/__tests__/jest.setup.ts +37 -0
- package/src/__tests__/kv.test.ts +1139 -0
- package/src/__tests__/logout.test.ts +50 -0
- package/src/__tests__/package-manager.test.ts +206 -0
- package/src/__tests__/publish.test.ts +1222 -0
- package/src/__tests__/secret.test.ts +210 -0
- package/src/__tests__/whoami.test.tsx +86 -0
- package/src/api/form_data.ts +85 -76
- package/src/api/preview.ts +29 -24
- package/src/api/worker.ts +50 -35
- package/src/cfetch/index.ts +102 -0
- package/src/cfetch/internal.ts +101 -0
- package/src/cli.ts +6 -7
- package/src/config.ts +630 -85
- package/src/dev.tsx +446 -282
- package/src/dialogs.tsx +6 -14
- package/src/index.tsx +773 -426
- package/src/inspect.ts +596 -0
- package/src/kv.tsx +118 -45
- package/src/module-collection.ts +63 -0
- package/src/package-manager.ts +120 -0
- package/src/pages.tsx +917 -211
- package/src/paths.ts +26 -0
- package/src/proxy.ts +370 -0
- package/src/publish.ts +220 -88
- package/src/sites.tsx +180 -55
- package/src/tail.tsx +6 -4
- package/src/user.tsx +77 -87
- package/src/whoami.tsx +69 -0
- package/templates/new-worker.js +15 -0
- package/templates/new-worker.ts +15 -0
- package/{static-asset-facade.js → templates/static-asset-facade.js} +0 -0
- package/wrangler-dist/cli.js +125330 -557
- package/wrangler-dist/cli.js.map +3 -3
- package/src/__tests__/fixtures/init/.gitkeep +0 -0
- package/src/__tests__/mock-cfetch.js +0 -46
- package/src/api/inspect.ts +0 -441
- package/src/cfetch.ts +0 -72
- package/src/util/fetch.ts +0 -74
package/src/sites.tsx
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import ignore from "ignore";
|
|
4
|
+
import { XXHash64 } from "xxhash-addon";
|
|
5
|
+
import {
|
|
6
|
+
createNamespace,
|
|
7
|
+
listNamespaceKeys,
|
|
8
|
+
listNamespaces,
|
|
9
|
+
putBulkKeyValue,
|
|
10
|
+
} from "./kv";
|
|
11
|
+
import type { Config } from "./config";
|
|
5
12
|
|
|
6
|
-
|
|
7
|
-
|
|
13
|
+
/** Paths to always ignore. */
|
|
14
|
+
const ALWAYS_IGNORE = ["node_modules"];
|
|
15
|
+
const HIDDEN_FILES_TO_INCLUDE = [
|
|
16
|
+
".well-known", // See https://datatracker.ietf.org/doc/html/rfc8615
|
|
17
|
+
];
|
|
8
18
|
|
|
9
19
|
async function* getFilesInFolder(dirPath: string): AsyncIterable<string> {
|
|
10
20
|
const files = await readdir(dirPath, { withFileTypes: true });
|
|
11
21
|
for (const file of files) {
|
|
22
|
+
// Skip files that we never want to process.
|
|
23
|
+
if (ALWAYS_IGNORE.some((p) => file.name === p)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
// Skip hidden files (starting with .) except for some special ones
|
|
27
|
+
if (
|
|
28
|
+
file.name.startsWith(".") &&
|
|
29
|
+
!HIDDEN_FILES_TO_INCLUDE.some((p) => file.name === p)
|
|
30
|
+
) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// TODO: follow symlinks??
|
|
12
34
|
if (file.isDirectory()) {
|
|
13
35
|
yield* await getFilesInFolder(path.join(dirPath, file.name));
|
|
14
36
|
} else {
|
|
@@ -17,27 +39,32 @@ async function* getFilesInFolder(dirPath: string): AsyncIterable<string> {
|
|
|
17
39
|
}
|
|
18
40
|
}
|
|
19
41
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Create a hash key for the given content using the xxhash algorithm.
|
|
44
|
+
*
|
|
45
|
+
* Note we only return the first 10 characters, since we will also include the file name in the asset manifest key
|
|
46
|
+
* the most important thing here is to detect changes of a single file to invalidate the cache and
|
|
47
|
+
* it's impossible to serve two different files with the same name
|
|
48
|
+
*/
|
|
49
|
+
function hashFileContent(content: string): string {
|
|
50
|
+
const hasher = new XXHash64();
|
|
51
|
+
hasher.update(Buffer.from(content));
|
|
52
|
+
const hash = hasher.digest();
|
|
53
|
+
return hash.toString("hex").substring(0, 10);
|
|
28
54
|
}
|
|
29
55
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Create a hashed asset key for the given asset.
|
|
58
|
+
*
|
|
59
|
+
* The key will change if the file path or content of the asset changes.
|
|
60
|
+
* The algorithm used here matches that of Wrangler 1.
|
|
61
|
+
*/
|
|
62
|
+
function hashAsset(filePath: string, content: string): string {
|
|
63
|
+
const extName = path.extname(filePath) || "";
|
|
35
64
|
const baseName = path.basename(filePath, extName);
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
hash,
|
|
40
|
-
};
|
|
65
|
+
const directory = path.dirname(filePath);
|
|
66
|
+
const hash = hashFileContent(content);
|
|
67
|
+
return urlSafe(path.join(directory, `${baseName}.${hash}${extName}`));
|
|
41
68
|
}
|
|
42
69
|
|
|
43
70
|
async function createKVNamespaceIfNotAlreadyExisting(
|
|
@@ -53,63 +80,161 @@ async function createKVNamespaceIfNotAlreadyExisting(
|
|
|
53
80
|
}
|
|
54
81
|
|
|
55
82
|
// else we make the namespace
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
`/accounts/${accountId}/storage/kv/namespaces`,
|
|
59
|
-
{
|
|
60
|
-
method: "POST",
|
|
61
|
-
headers: {
|
|
62
|
-
"Content-Type": "application/json",
|
|
63
|
-
},
|
|
64
|
-
body: JSON.stringify({ title }),
|
|
65
|
-
}
|
|
66
|
-
);
|
|
83
|
+
const id = await createNamespace(accountId, title);
|
|
84
|
+
console.log(`🌀 Created namespace for Workers Site "${title}"`);
|
|
67
85
|
|
|
68
86
|
return {
|
|
69
87
|
created: true,
|
|
70
|
-
id
|
|
88
|
+
id,
|
|
71
89
|
};
|
|
72
90
|
}
|
|
73
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Upload the assets found within the `dirPath` directory to the sites assets KV namespace for
|
|
94
|
+
* the worker given by `scriptName`.
|
|
95
|
+
*
|
|
96
|
+
* @param accountId the account to upload to.
|
|
97
|
+
* @param scriptName the name of the worker whose assets we are uploading.
|
|
98
|
+
* @param siteAssets an objects describing what assets to upload, or undefined if there are no assets to upload.
|
|
99
|
+
* @param preview if true then upload to a "preview" KV namespace.
|
|
100
|
+
* @param _env (not implemented).
|
|
101
|
+
* @returns a promise for an object mapping the relative paths of the assets to the key of that
|
|
102
|
+
* asset in the KV namespace.
|
|
103
|
+
*/
|
|
74
104
|
export async function syncAssets(
|
|
75
105
|
accountId: string,
|
|
76
106
|
scriptName: string,
|
|
77
|
-
|
|
107
|
+
siteAssets: AssetPaths | undefined,
|
|
78
108
|
preview: boolean,
|
|
79
|
-
env
|
|
80
|
-
) {
|
|
81
|
-
|
|
109
|
+
env: string | undefined
|
|
110
|
+
): Promise<{
|
|
111
|
+
manifest: { [filePath: string]: string } | undefined;
|
|
112
|
+
namespace: string | undefined;
|
|
113
|
+
}> {
|
|
114
|
+
if (siteAssets === undefined) {
|
|
115
|
+
return { manifest: undefined, namespace: undefined };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const title = `__${scriptName}${env ? `-${env}` : ""}-workers_sites_assets${
|
|
119
|
+
preview ? "_preview" : ""
|
|
120
|
+
}`;
|
|
82
121
|
const { id: namespace } = await createKVNamespaceIfNotAlreadyExisting(
|
|
83
122
|
title,
|
|
84
123
|
accountId
|
|
85
124
|
);
|
|
86
125
|
|
|
87
126
|
// let's get all the keys in this namespace
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
);
|
|
127
|
+
const result = await listNamespaceKeys(accountId, namespace);
|
|
128
|
+
const keys = new Set(result.map((x) => x.name));
|
|
91
129
|
|
|
92
130
|
const manifest = {};
|
|
93
|
-
const upload
|
|
131
|
+
const upload: {
|
|
132
|
+
key: string;
|
|
133
|
+
value: string;
|
|
134
|
+
base64: boolean;
|
|
135
|
+
}[] = [];
|
|
136
|
+
|
|
137
|
+
const include = createPatternMatcher(siteAssets.includePatterns, false);
|
|
138
|
+
const exclude = createPatternMatcher(siteAssets.excludePatterns, true);
|
|
94
139
|
// TODO: this can be more efficient by parallelising
|
|
95
|
-
for await (const file of getFilesInFolder(
|
|
96
|
-
|
|
97
|
-
|
|
140
|
+
for await (const file of getFilesInFolder(siteAssets.baseDirectory)) {
|
|
141
|
+
if (!include(file)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (exclude(file)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await validateAssetSize(file);
|
|
149
|
+
console.log(`reading ${file}...`);
|
|
150
|
+
const content = await readFile(file, "base64");
|
|
151
|
+
|
|
152
|
+
const assetKey = hashAsset(file, content);
|
|
153
|
+
validateAssetKey(assetKey);
|
|
154
|
+
|
|
98
155
|
// now put each of the files into kv
|
|
99
|
-
if (!keys.has(
|
|
100
|
-
console.log(`uploading ${
|
|
101
|
-
const content = await readFile(file, "base64");
|
|
102
|
-
if (content.length > 25 * 1024 * 1024) {
|
|
103
|
-
throw new Error(`File ${file} is too big, it should be under 25 mb.`);
|
|
104
|
-
}
|
|
156
|
+
if (!keys.has(assetKey)) {
|
|
157
|
+
console.log(`uploading as ${assetKey}...`);
|
|
105
158
|
upload.push({
|
|
106
|
-
key:
|
|
159
|
+
key: assetKey,
|
|
107
160
|
value: content,
|
|
108
161
|
base64: true,
|
|
109
162
|
});
|
|
163
|
+
} else {
|
|
164
|
+
console.log(`skipping - already uploaded`);
|
|
110
165
|
}
|
|
111
|
-
manifest[path.relative(
|
|
166
|
+
manifest[path.relative(siteAssets.baseDirectory, file)] = assetKey;
|
|
112
167
|
}
|
|
113
168
|
await putBulkKeyValue(accountId, namespace, JSON.stringify(upload));
|
|
114
169
|
return { manifest, namespace };
|
|
115
170
|
}
|
|
171
|
+
|
|
172
|
+
function createPatternMatcher(
|
|
173
|
+
patterns: string[],
|
|
174
|
+
exclude: boolean
|
|
175
|
+
): (filePath: string) => boolean {
|
|
176
|
+
if (patterns.length === 0) {
|
|
177
|
+
return (_filePath) => !exclude;
|
|
178
|
+
} else {
|
|
179
|
+
const ignorer = ignore().add(patterns);
|
|
180
|
+
return (filePath) => ignorer.test(filePath).ignored;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function validateAssetSize(filePath: string) {
|
|
185
|
+
const { size } = await stat(filePath);
|
|
186
|
+
if (size > 25 * 1024 * 1024) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`File ${filePath} is too big, it should be under 25 MiB. See https://developers.cloudflare.com/workers/platform/limits#kv-limits`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function validateAssetKey(assetKey: string) {
|
|
194
|
+
if (assetKey.length > 512) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`The asset path key "${assetKey}" exceeds the maximum key size limit of 512. See https://developers.cloudflare.com/workers/platform/limits#kv-limits",`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Convert a filePath to be safe to use as a relative URL.
|
|
203
|
+
*
|
|
204
|
+
* Primarily this involves converting Windows backslashes to forward slashes.
|
|
205
|
+
*/
|
|
206
|
+
function urlSafe(filePath: string): string {
|
|
207
|
+
return filePath.replace(/\\/g, "/");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Information about the assets that should be uploaded
|
|
212
|
+
*/
|
|
213
|
+
export interface AssetPaths {
|
|
214
|
+
baseDirectory: string;
|
|
215
|
+
includePatterns: string[];
|
|
216
|
+
excludePatterns: string[];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get an object that describes what assets to upload, if any.
|
|
221
|
+
*
|
|
222
|
+
* Uses the args (passed from the command line) if available,
|
|
223
|
+
* falling back to those defined in the config.
|
|
224
|
+
*
|
|
225
|
+
* // TODO: Support for environments
|
|
226
|
+
*/
|
|
227
|
+
export function getAssetPaths(
|
|
228
|
+
config: Config,
|
|
229
|
+
baseDirectory = config.site?.bucket,
|
|
230
|
+
includePatterns = config.site?.include ?? [],
|
|
231
|
+
excludePatterns = config.site?.exclude ?? []
|
|
232
|
+
): undefined | AssetPaths {
|
|
233
|
+
return baseDirectory
|
|
234
|
+
? {
|
|
235
|
+
baseDirectory,
|
|
236
|
+
includePatterns,
|
|
237
|
+
excludePatterns,
|
|
238
|
+
}
|
|
239
|
+
: undefined;
|
|
240
|
+
}
|
package/src/tail.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
|
-
import cfetch from "./cfetch";
|
|
3
2
|
import { version as packageVersion } from "../package.json";
|
|
3
|
+
import { fetchResult } from "./cfetch";
|
|
4
4
|
|
|
5
5
|
export type TailApiResponse = {
|
|
6
6
|
id: string;
|
|
@@ -27,13 +27,15 @@ async function createTailButDontConnect(
|
|
|
27
27
|
): Promise<TailApiResponse> {
|
|
28
28
|
const createTailUrl = makeCreateTailUrl(accountId, workerName);
|
|
29
29
|
/// https://api.cloudflare.com/#worker-tail-logs-start-tail
|
|
30
|
-
return await
|
|
30
|
+
return await fetchResult<TailApiResponse>(createTailUrl, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
});
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export async function createTail(
|
|
34
36
|
accountId: string,
|
|
35
37
|
workerName: string,
|
|
36
|
-
|
|
38
|
+
_filters: Filters
|
|
37
39
|
): Promise<{
|
|
38
40
|
tail: WebSocket;
|
|
39
41
|
expiration: Date;
|
|
@@ -48,7 +50,7 @@ export async function createTail(
|
|
|
48
50
|
|
|
49
51
|
// deletes the tail
|
|
50
52
|
async function deleteTail() {
|
|
51
|
-
await
|
|
53
|
+
await fetchResult(deleteUrl, { method: "DELETE" });
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
const tail = new WebSocket(websocketUrl, "trace-v1", {
|
package/src/user.tsx
CHANGED
|
@@ -205,25 +205,25 @@
|
|
|
205
205
|
limitations under the License.
|
|
206
206
|
*/
|
|
207
207
|
|
|
208
|
-
import
|
|
209
|
-
import { render } from "ink";
|
|
210
|
-
import Table from "ink-table";
|
|
211
|
-
import SelectInput from "ink-select-input";
|
|
212
|
-
import fetch from "node-fetch";
|
|
208
|
+
import assert from "node:assert";
|
|
213
209
|
import { webcrypto as crypto } from "node:crypto";
|
|
214
|
-
import { TextEncoder } from "node:util";
|
|
215
|
-
import open from "open";
|
|
216
|
-
import url from "node:url";
|
|
217
|
-
import http from "node:http";
|
|
218
210
|
import { readFile, writeFile, rm, mkdir } from "node:fs/promises";
|
|
211
|
+
import http from "node:http";
|
|
212
|
+
import os from "node:os";
|
|
219
213
|
import path from "node:path";
|
|
220
214
|
import process from "node:process";
|
|
221
|
-
import
|
|
215
|
+
import url from "node:url";
|
|
216
|
+
import { TextEncoder } from "node:util";
|
|
222
217
|
import TOML from "@iarna/toml";
|
|
223
|
-
import
|
|
224
|
-
import
|
|
218
|
+
import { render, Text } from "ink";
|
|
219
|
+
import SelectInput from "ink-select-input";
|
|
220
|
+
import Table from "ink-table";
|
|
221
|
+
import open from "open";
|
|
222
|
+
import React from "react";
|
|
223
|
+
import { fetch } from "undici";
|
|
225
224
|
import { CF_API_BASE_URL } from "./cfetch";
|
|
226
|
-
import type {
|
|
225
|
+
import type { ParsedUrlQuery } from "node:querystring";
|
|
226
|
+
import type { Response } from "undici";
|
|
227
227
|
|
|
228
228
|
/**
|
|
229
229
|
* An implementation of rfc6749#section-4.1 and rfc7636.
|
|
@@ -254,38 +254,37 @@ interface AccessToken {
|
|
|
254
254
|
expiry: string;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
];
|
|
257
|
+
const Scopes = {
|
|
258
|
+
"account:read":
|
|
259
|
+
"See your account info such as account details, analytics, and memberships.",
|
|
260
|
+
"user:read":
|
|
261
|
+
"See your user info such as name, email address, and account memberships.",
|
|
262
|
+
"workers:write":
|
|
263
|
+
"See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.",
|
|
264
|
+
"workers_kv:write":
|
|
265
|
+
"See and change Cloudflare Workers KV Storage data such as keys and namespaces.",
|
|
266
|
+
"workers_routes:write":
|
|
267
|
+
"See and change Cloudflare Workers data such as filters and routes.",
|
|
268
|
+
"workers_scripts:write":
|
|
269
|
+
"See and change Cloudflare Workers scripts, durable objects, subdomains, triggers, and tail data.",
|
|
270
|
+
"workers_tail:read": "See Cloudflare Workers tail and script data.",
|
|
271
|
+
"zone:read": "Grants read level access to account zone.",
|
|
272
|
+
} as const;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The possible keys for a Scope.
|
|
276
|
+
*
|
|
277
|
+
* "offline_access" is automatically included.
|
|
278
|
+
*/
|
|
279
|
+
type Scope = keyof typeof Scopes | "offline_access";
|
|
280
|
+
|
|
281
|
+
const ScopeKeys = Object.keys(Scopes) as Scope[];
|
|
282
|
+
|
|
283
|
+
export function validateScopeKeys(
|
|
284
|
+
scopes: string[]
|
|
285
|
+
): scopes is typeof ScopeKeys {
|
|
286
|
+
return scopes.every((scope) => Scopes[scope]);
|
|
287
|
+
}
|
|
289
288
|
|
|
290
289
|
const CLIENT_ID = "54d11594-84e4-41aa-b438-e81b8fa78ee7";
|
|
291
290
|
const AUTH_URL = "https://dash.cloudflare.com/oauth2/auth";
|
|
@@ -298,9 +297,9 @@ let initialised = false;
|
|
|
298
297
|
|
|
299
298
|
// we do this because we have some async stuff
|
|
300
299
|
// TODO: this should just happen in the top level
|
|
301
|
-
//
|
|
300
|
+
// and we should figure out how to do top level await
|
|
302
301
|
export async function initialise(): Promise<void> {
|
|
303
|
-
// get
|
|
302
|
+
// get refreshToken/accessToken from fs if exists
|
|
304
303
|
try {
|
|
305
304
|
// if CF_API_TOKEN available, use that
|
|
306
305
|
if (process.env.CF_API_TOKEN) {
|
|
@@ -344,11 +343,10 @@ function throwIfNotInitialised() {
|
|
|
344
343
|
}
|
|
345
344
|
}
|
|
346
345
|
|
|
347
|
-
export function getAPIToken(): string {
|
|
346
|
+
export function getAPIToken(): string | undefined {
|
|
348
347
|
if (process.env.CF_API_TOKEN) {
|
|
349
348
|
return process.env.CF_API_TOKEN;
|
|
350
349
|
}
|
|
351
|
-
|
|
352
350
|
throwIfNotInitialised();
|
|
353
351
|
return LocalState.accessToken?.value;
|
|
354
352
|
}
|
|
@@ -548,7 +546,7 @@ function isReturningFromAuthServer(query: ParsedUrlQuery): boolean {
|
|
|
548
546
|
return true;
|
|
549
547
|
}
|
|
550
548
|
|
|
551
|
-
export async function getAuthURL(scopes
|
|
549
|
+
export async function getAuthURL(scopes = ScopeKeys): Promise<string> {
|
|
552
550
|
const { codeChallenge, codeVerifier } = await generatePKCECodes();
|
|
553
551
|
const stateQueryParam = generateRandomState(RECOMMENDED_STATE_LENGTH);
|
|
554
552
|
|
|
@@ -558,16 +556,12 @@ export async function getAuthURL(scopes?: string[]): Promise<string> {
|
|
|
558
556
|
stateQueryParam,
|
|
559
557
|
});
|
|
560
558
|
|
|
561
|
-
// TODO: verify that the scopes passed are legit
|
|
562
|
-
|
|
563
559
|
return (
|
|
564
560
|
AUTH_URL +
|
|
565
561
|
`?response_type=code&` +
|
|
566
562
|
`client_id=${encodeURIComponent(CLIENT_ID)}&` +
|
|
567
563
|
`redirect_uri=${encodeURIComponent(CALLBACK_URL)}&` +
|
|
568
|
-
`scope=${encodeURIComponent(
|
|
569
|
-
(scopes || Scopes).concat("offline_access").join(" ")
|
|
570
|
-
)}&` +
|
|
564
|
+
`scope=${encodeURIComponent(scopes.concat("offline_access").join(" "))}&` +
|
|
571
565
|
`state=${stateQueryParam}&` +
|
|
572
566
|
`code_challenge=${encodeURIComponent(codeChallenge)}&` +
|
|
573
567
|
`code_challenge_method=S256`
|
|
@@ -578,19 +572,16 @@ export async function getAuthURL(scopes?: string[]): Promise<string> {
|
|
|
578
572
|
* Refresh an access token from the remote service.
|
|
579
573
|
*/
|
|
580
574
|
async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (!refreshToken) {
|
|
575
|
+
if (!LocalState.refreshToken) {
|
|
584
576
|
console.warn("No refresh token is present.");
|
|
585
577
|
}
|
|
586
578
|
|
|
587
|
-
const url = TOKEN_URL;
|
|
588
579
|
const body =
|
|
589
580
|
`grant_type=refresh_token&` +
|
|
590
|
-
`refresh_token=${refreshToken?.value}&` +
|
|
581
|
+
`refresh_token=${LocalState.refreshToken?.value}&` +
|
|
591
582
|
`client_id=${CLIENT_ID}`;
|
|
592
583
|
|
|
593
|
-
const response = await fetch(
|
|
584
|
+
const response = await fetch(TOKEN_URL, {
|
|
594
585
|
method: "POST",
|
|
595
586
|
body,
|
|
596
587
|
headers: {
|
|
@@ -617,10 +608,9 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
|
|
|
617
608
|
LocalState.accessToken = accessToken;
|
|
618
609
|
|
|
619
610
|
if (refresh_token) {
|
|
620
|
-
|
|
611
|
+
LocalState.refreshToken = {
|
|
621
612
|
value: refresh_token,
|
|
622
613
|
};
|
|
623
|
-
LocalState.refreshToken = refreshToken;
|
|
624
614
|
}
|
|
625
615
|
|
|
626
616
|
if (scope) {
|
|
@@ -666,7 +656,6 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
|
|
|
666
656
|
console.warn("No authorization grant code is being passed.");
|
|
667
657
|
}
|
|
668
658
|
|
|
669
|
-
const url = TOKEN_URL;
|
|
670
659
|
const body =
|
|
671
660
|
`grant_type=authorization_code&` +
|
|
672
661
|
`code=${encodeURIComponent(authorizationCode || "")}&` +
|
|
@@ -674,7 +663,7 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
|
|
|
674
663
|
`client_id=${encodeURIComponent(CLIENT_ID)}&` +
|
|
675
664
|
`code_verifier=${codeVerifier}`;
|
|
676
665
|
|
|
677
|
-
const response = await fetch(
|
|
666
|
+
const response = await fetch(TOKEN_URL, {
|
|
678
667
|
method: "POST",
|
|
679
668
|
body,
|
|
680
669
|
headers: {
|
|
@@ -709,10 +698,9 @@ async function exchangeAuthCodeForAccessToken(): Promise<AccessContext> {
|
|
|
709
698
|
LocalState.accessToken = accessToken;
|
|
710
699
|
|
|
711
700
|
if (refresh_token) {
|
|
712
|
-
|
|
701
|
+
LocalState.refreshToken = {
|
|
713
702
|
value: refresh_token,
|
|
714
703
|
};
|
|
715
|
-
LocalState.refreshToken = refreshToken;
|
|
716
704
|
}
|
|
717
705
|
|
|
718
706
|
if (scope) {
|
|
@@ -798,7 +786,7 @@ expiration_time = "${tokenData.token?.expiry}"
|
|
|
798
786
|
}
|
|
799
787
|
|
|
800
788
|
type LoginProps = {
|
|
801
|
-
scopes?:
|
|
789
|
+
scopes?: Scope[];
|
|
802
790
|
};
|
|
803
791
|
|
|
804
792
|
export async function loginOrRefreshIfRequired(): Promise<boolean> {
|
|
@@ -816,7 +804,7 @@ export async function loginOrRefreshIfRequired(): Promise<boolean> {
|
|
|
816
804
|
|
|
817
805
|
export async function login(props?: LoginProps): Promise<boolean> {
|
|
818
806
|
const urlToOpen = await getAuthURL(props?.scopes);
|
|
819
|
-
open(urlToOpen);
|
|
807
|
+
await open(urlToOpen);
|
|
820
808
|
// TODO: log url only if on system where it's unreliable/unavailable
|
|
821
809
|
// console.log(`💁 Opened ${urlToOpen}`);
|
|
822
810
|
let server;
|
|
@@ -921,15 +909,14 @@ export async function refreshToken(): Promise<boolean> {
|
|
|
921
909
|
|
|
922
910
|
export async function logout(): Promise<void> {
|
|
923
911
|
throwIfNotInitialised();
|
|
924
|
-
|
|
925
|
-
if (!refreshToken) {
|
|
912
|
+
if (!LocalState.refreshToken) {
|
|
926
913
|
console.log("Not logged in, exiting...");
|
|
927
914
|
return;
|
|
928
915
|
}
|
|
929
916
|
const body =
|
|
930
917
|
`client_id=${encodeURIComponent(CLIENT_ID)}&` +
|
|
931
918
|
`token_type_hint=refresh_token&` +
|
|
932
|
-
`token=${encodeURIComponent(refreshToken?.value || "")}`;
|
|
919
|
+
`token=${encodeURIComponent(LocalState.refreshToken?.value || "")}`;
|
|
933
920
|
|
|
934
921
|
const response = await fetch(REVOKE_URL, {
|
|
935
922
|
method: "POST",
|
|
@@ -952,9 +939,9 @@ export async function logout(): Promise<void> {
|
|
|
952
939
|
export function listScopes(): void {
|
|
953
940
|
throwIfNotInitialised();
|
|
954
941
|
console.log("💁 Available scopes:");
|
|
955
|
-
const data =
|
|
942
|
+
const data = ScopeKeys.map((scope) => ({
|
|
956
943
|
Scope: scope,
|
|
957
|
-
Description:
|
|
944
|
+
Description: Scopes[scope],
|
|
958
945
|
}));
|
|
959
946
|
render(<Table data={data} />);
|
|
960
947
|
// TODO: maybe a good idea to show usage here
|
|
@@ -978,14 +965,14 @@ export async function getAccountId() {
|
|
|
978
965
|
});
|
|
979
966
|
} catch (err) {
|
|
980
967
|
// probably offline
|
|
968
|
+
return;
|
|
981
969
|
}
|
|
982
|
-
|
|
983
|
-
let accountId: string;
|
|
984
|
-
|
|
985
|
-
const responseJSON: {
|
|
970
|
+
|
|
971
|
+
let accountId: string | undefined;
|
|
972
|
+
const responseJSON = (await response.json()) as {
|
|
986
973
|
success: boolean;
|
|
987
974
|
result: { id: string; account: { id: string; name: string } }[];
|
|
988
|
-
}
|
|
975
|
+
};
|
|
989
976
|
|
|
990
977
|
if (responseJSON.success === true) {
|
|
991
978
|
if (responseJSON.result.length === 1) {
|
|
@@ -1017,13 +1004,16 @@ export function ChooseAccount(props: {
|
|
|
1017
1004
|
onSelect: (item) => void;
|
|
1018
1005
|
}) {
|
|
1019
1006
|
return (
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1007
|
+
<>
|
|
1008
|
+
<Text bold>Select an account from below:</Text>
|
|
1009
|
+
<SelectInput
|
|
1010
|
+
items={props.accounts.map((item) => ({
|
|
1011
|
+
key: item.id,
|
|
1012
|
+
label: item.name,
|
|
1013
|
+
value: item,
|
|
1014
|
+
}))}
|
|
1015
|
+
onSelect={props.onSelect}
|
|
1016
|
+
/>
|
|
1017
|
+
</>
|
|
1028
1018
|
);
|
|
1029
1019
|
}
|