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.
Files changed (61) hide show
  1. package/README.md +51 -0
  2. package/bin/wrangler.js +2 -2
  3. package/package.json +49 -30
  4. package/pages/functions/buildWorker.ts +62 -0
  5. package/pages/functions/filepath-routing.test.ts +47 -0
  6. package/pages/functions/filepath-routing.ts +231 -0
  7. package/pages/functions/identifiers.ts +78 -0
  8. package/pages/functions/routes.ts +160 -0
  9. package/pages/functions/template-worker.ts +136 -0
  10. package/src/__tests__/dev.test.tsx +71 -0
  11. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  12. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  13. package/src/__tests__/helpers/mock-account-id.ts +30 -0
  14. package/src/__tests__/helpers/mock-bin.ts +33 -0
  15. package/src/__tests__/helpers/mock-cfetch.ts +207 -0
  16. package/src/__tests__/helpers/mock-console.ts +56 -0
  17. package/src/__tests__/helpers/mock-dialogs.ts +65 -0
  18. package/src/__tests__/helpers/mock-kv.ts +40 -0
  19. package/src/__tests__/helpers/mock-user.ts +27 -0
  20. package/src/__tests__/helpers/run-in-tmp.ts +29 -0
  21. package/src/__tests__/helpers/run-wrangler.ts +14 -0
  22. package/src/__tests__/index.test.ts +509 -119
  23. package/src/__tests__/jest.setup.ts +37 -0
  24. package/src/__tests__/kv.test.ts +1139 -0
  25. package/src/__tests__/logout.test.ts +50 -0
  26. package/src/__tests__/package-manager.test.ts +206 -0
  27. package/src/__tests__/publish.test.ts +1222 -0
  28. package/src/__tests__/secret.test.ts +210 -0
  29. package/src/__tests__/whoami.test.tsx +86 -0
  30. package/src/api/form_data.ts +85 -76
  31. package/src/api/preview.ts +29 -24
  32. package/src/api/worker.ts +50 -35
  33. package/src/cfetch/index.ts +102 -0
  34. package/src/cfetch/internal.ts +101 -0
  35. package/src/cli.ts +6 -7
  36. package/src/config.ts +630 -85
  37. package/src/dev.tsx +446 -282
  38. package/src/dialogs.tsx +6 -14
  39. package/src/index.tsx +773 -426
  40. package/src/inspect.ts +596 -0
  41. package/src/kv.tsx +118 -45
  42. package/src/module-collection.ts +63 -0
  43. package/src/package-manager.ts +120 -0
  44. package/src/pages.tsx +917 -211
  45. package/src/paths.ts +26 -0
  46. package/src/proxy.ts +370 -0
  47. package/src/publish.ts +220 -88
  48. package/src/sites.tsx +180 -55
  49. package/src/tail.tsx +6 -4
  50. package/src/user.tsx +77 -87
  51. package/src/whoami.tsx +69 -0
  52. package/templates/new-worker.js +15 -0
  53. package/templates/new-worker.ts +15 -0
  54. package/{static-asset-facade.js → templates/static-asset-facade.js} +0 -0
  55. package/wrangler-dist/cli.js +125330 -557
  56. package/wrangler-dist/cli.js.map +3 -3
  57. package/src/__tests__/fixtures/init/.gitkeep +0 -0
  58. package/src/__tests__/mock-cfetch.js +0 -46
  59. package/src/api/inspect.ts +0 -441
  60. package/src/cfetch.ts +0 -72
  61. 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 { createReadStream } from "node:fs";
3
- import cfetch from "./cfetch";
4
- import { listNamespaceKeys, listNamespaces, putBulkKeyValue } from "./kv";
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
- import * as path from "path";
7
- import crypto from "node:crypto";
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
- async function hashFileContent(filePath: string): Promise<string> {
21
- return new Promise((resolve, reject) => {
22
- const hash = crypto.createHash("sha1");
23
- const rs = createReadStream(filePath);
24
- rs.on("error", reject);
25
- rs.on("data", (chunk) => hash.update(chunk));
26
- rs.on("end", () => resolve(hash.digest("hex")));
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
- async function hashFile(filePath: string): Promise<{
31
- filePath: string;
32
- hash: string;
33
- }> {
34
- const extName = path.extname(filePath);
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 hash = await hashFileContent(filePath);
37
- return {
38
- filePath: `${baseName}.${hash}${extName || ""}`,
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
- // TODO: use an export from ./kv
57
- const json = await cfetch<{ id: string }>(
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: json.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
- dirPath: string,
107
+ siteAssets: AssetPaths | undefined,
78
108
  preview: boolean,
79
- env?: string
80
- ) {
81
- const title = `__${scriptName}_sites_assets${preview ? "_preview" : ""}`;
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 keys = new Set(
89
- (await listNamespaceKeys(accountId, namespace)).map((x) => x.name)
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(dirPath)) {
96
- // TODO: "exclude:" config
97
- const { filePath } = await hashFile(file);
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(filePath)) {
100
- console.log(`uploading ${file}...`);
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: filePath,
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(dirPath, file)] = filePath;
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 cfetch<TailApiResponse>(createTailUrl, { method: "POST" });
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
- filters: Filters
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 cfetch(deleteUrl, { method: "DELETE" });
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 React from "react";
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 os from "node:os";
215
+ import url from "node:url";
216
+ import { TextEncoder } from "node:util";
222
217
  import TOML from "@iarna/toml";
223
- import assert from "node:assert";
224
- import type { ParsedUrlQuery } from "node:querystring";
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 { Response } from "node-fetch";
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
- type Scope =
258
- | "account:read"
259
- | "user:read"
260
- | "workers:write"
261
- | "workers_kv:write"
262
- | "workers_routes:write"
263
- | "workers_scripts:write"
264
- | "workers_tail:read"
265
- | "zone:read"
266
- | "offline_access"; // this should be included by default
267
-
268
- const Scopes: Scope[] = [
269
- "account:read",
270
- "user:read",
271
- "workers:write",
272
- "workers_kv:write",
273
- "workers_routes:write",
274
- "workers_scripts:write",
275
- "workers_tail:read",
276
- "zone:read",
277
- ];
278
-
279
- const ScopeDescriptions = [
280
- "See your account info such as account details, analytics, and memberships.",
281
- "See your user info such as name, email address, and account memberships.",
282
- "See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.",
283
- "See and change Cloudflare Workers KV Storage data such as keys and namespaces.",
284
- "See and change Cloudflare Workers data such as filters and routes.",
285
- "See and change Cloudflare Workers scripts, durable objects, subdomains, triggers, and tail data.",
286
- "See Cloudflare Workers tail and script data.",
287
- "Grants read level access to account zone.",
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
- // abd we should fiure out how to do top level await
300
+ // and we should figure out how to do top level await
302
301
  export async function initialise(): Promise<void> {
303
- // get refreshtoken/accesstoken from fs if exists
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?: string[]): Promise<string> {
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
- const { refreshToken } = LocalState;
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(url, {
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
- const refreshToken: RefreshToken = {
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(url, {
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
- const refreshToken: RefreshToken = {
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?: string[];
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
- const { refreshToken } = LocalState;
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 = Scopes.map((scope, index) => ({
942
+ const data = ScopeKeys.map((scope) => ({
956
943
  Scope: scope,
957
- Description: ScopeDescriptions[index],
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
- if (!response) return;
983
- let accountId: string;
984
- // @ts-expect-error need to type this response
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
- } = await response.json();
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
- <SelectInput
1021
- items={props.accounts.map((item) => ({
1022
- key: item.id,
1023
- label: item.name,
1024
- value: item,
1025
- }))}
1026
- onSelect={props.onSelect}
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
  }