wrangler 0.0.0-fb43821 → 0.0.0-fe5dcfd

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 (41) hide show
  1. package/package.json +25 -10
  2. package/pages/functions/buildWorker.ts +62 -0
  3. package/pages/functions/filepath-routing.test.ts +39 -0
  4. package/pages/functions/filepath-routing.ts +228 -0
  5. package/pages/functions/identifiers.ts +78 -0
  6. package/pages/functions/routes.ts +158 -0
  7. package/pages/functions/template-worker.ts +136 -0
  8. package/src/__tests__/dev.test.tsx +67 -0
  9. package/src/__tests__/index.test.ts +42 -175
  10. package/src/__tests__/jest.setup.ts +22 -0
  11. package/src/__tests__/kv.test.ts +1199 -0
  12. package/src/__tests__/mock-cfetch.ts +173 -0
  13. package/src/__tests__/mock-dialogs.ts +65 -0
  14. package/src/__tests__/publish.test.ts +737 -0
  15. package/src/__tests__/run-in-tmp.ts +19 -0
  16. package/src/__tests__/run-wrangler.ts +32 -0
  17. package/src/__tests__/whoami.test.tsx +130 -0
  18. package/src/api/form_data.ts +66 -76
  19. package/src/api/preview.ts +4 -4
  20. package/src/api/worker.ts +38 -44
  21. package/src/cfetch/index.ts +110 -0
  22. package/src/cfetch/internal.ts +71 -0
  23. package/src/cli.ts +6 -7
  24. package/src/config.ts +634 -110
  25. package/src/dev.tsx +275 -272
  26. package/src/dialogs.tsx +2 -2
  27. package/src/index.tsx +504 -403
  28. package/src/inspect.ts +543 -0
  29. package/src/kv.tsx +109 -44
  30. package/src/pages.tsx +444 -222
  31. package/src/proxy.ts +302 -0
  32. package/src/publish.ts +123 -87
  33. package/src/sites.tsx +169 -41
  34. package/src/tail.tsx +5 -3
  35. package/src/user.tsx +64 -74
  36. package/src/whoami.tsx +69 -0
  37. package/wrangler-dist/cli.js +42792 -42619
  38. package/wrangler-dist/cli.js.map +3 -3
  39. package/src/__tests__/mock-cfetch.js +0 -42
  40. package/src/api/inspect.ts +0 -430
  41. package/src/cfetch.ts +0 -72
package/src/sites.tsx CHANGED
@@ -1,14 +1,32 @@
1
- import { readdir, readFile } from "node:fs/promises";
2
- import { createReadStream } from "node:fs";
3
- import cfetch from "./cfetch";
1
+ import * as path from "node:path";
2
+ import { readdir, readFile, stat } from "node:fs/promises";
3
+ import ignore from "ignore";
4
+ import { XXHash64 } from "xxhash-addon";
5
+ import { fetchResult } from "./cfetch";
6
+ import type { Config } from "./config";
4
7
  import { listNamespaceKeys, listNamespaces, putBulkKeyValue } from "./kv";
5
8
 
6
- import * as path from "path";
7
- import crypto from "node:crypto";
9
+ /** Paths to always ignore. */
10
+ const ALWAYS_IGNORE = ["node_modules"];
11
+ const HIDDEN_FILES_TO_INCLUDE = [
12
+ ".well-known", // See https://datatracker.ietf.org/doc/html/rfc8615
13
+ ];
8
14
 
9
15
  async function* getFilesInFolder(dirPath: string): AsyncIterable<string> {
10
16
  const files = await readdir(dirPath, { withFileTypes: true });
11
17
  for (const file of files) {
18
+ // Skip files that we never want to process.
19
+ if (ALWAYS_IGNORE.some((p) => file.name === p)) {
20
+ continue;
21
+ }
22
+ // Skip hidden files (starting with .) except for some special ones
23
+ if (
24
+ file.name.startsWith(".") &&
25
+ !HIDDEN_FILES_TO_INCLUDE.some((p) => file.name === p)
26
+ ) {
27
+ continue;
28
+ }
29
+ // TODO: follow symlinks??
12
30
  if (file.isDirectory()) {
13
31
  yield* await getFilesInFolder(path.join(dirPath, file.name));
14
32
  } else {
@@ -17,27 +35,32 @@ async function* getFilesInFolder(dirPath: string): AsyncIterable<string> {
17
35
  }
18
36
  }
19
37
 
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
- });
38
+ /**
39
+ * Create a hash key for the given content using the xxhash algorithm.
40
+ *
41
+ * Note we only return the first 10 characters, since we will also include the file name in the asset manifest key
42
+ * the most important thing here is to detect changes of a single file to invalidate the cache and
43
+ * it's impossible to serve two different files with the same name
44
+ */
45
+ function hashFileContent(content: string): string {
46
+ const hasher = new XXHash64();
47
+ hasher.update(Buffer.from(content));
48
+ const hash = hasher.digest();
49
+ return hash.toString("hex").substring(0, 10);
28
50
  }
29
51
 
30
- async function hashFile(filePath: string): Promise<{
31
- filePath: string;
32
- hash: string;
33
- }> {
34
- const extName = path.extname(filePath);
52
+ /**
53
+ * Create a hashed asset key for the given asset.
54
+ *
55
+ * The key will change if the file path or content of the asset changes.
56
+ * The algorithm used here matches that of Wrangler 1.
57
+ */
58
+ function hashAsset(filePath: string, content: string): string {
59
+ const extName = path.extname(filePath) || "";
35
60
  const baseName = path.basename(filePath, extName);
36
- const hash = await hashFileContent(filePath);
37
- return {
38
- filePath: `${baseName}.${hash}${extName || ""}`,
39
- hash,
40
- };
61
+ const directory = path.dirname(filePath);
62
+ const hash = hashFileContent(content);
63
+ return urlSafe(path.join(directory, `${baseName}.${hash}${extName}`));
41
64
  }
42
65
 
43
66
  async function createKVNamespaceIfNotAlreadyExisting(
@@ -54,7 +77,7 @@ async function createKVNamespaceIfNotAlreadyExisting(
54
77
 
55
78
  // else we make the namespace
56
79
  // TODO: use an export from ./kv
57
- const json = await cfetch<{ id: string }>(
80
+ const json = await fetchResult<{ id: string }>(
58
81
  `/accounts/${accountId}/storage/kv/namespaces`,
59
82
  {
60
83
  method: "POST",
@@ -71,13 +94,32 @@ async function createKVNamespaceIfNotAlreadyExisting(
71
94
  };
72
95
  }
73
96
 
97
+ /**
98
+ * Upload the assets found within the `dirPath` directory to the sites assets KV namespace for
99
+ * the worker given by `scriptName`.
100
+ *
101
+ * @param accountId the account to upload to.
102
+ * @param scriptName the name of the worker whose assets we are uploading.
103
+ * @param siteAssets an objects describing what assets to upload, or undefined if there are no assets to upload.
104
+ * @param preview if true then upload to a "preview" KV namespace.
105
+ * @param _env (not implemented).
106
+ * @returns a promise for an object mapping the relative paths of the assets to the key of that
107
+ * asset in the KV namespace.
108
+ */
74
109
  export async function syncAssets(
75
110
  accountId: string,
76
111
  scriptName: string,
77
- dirPath: string,
112
+ siteAssets: AssetPaths | undefined,
78
113
  preview: boolean,
79
114
  _env?: string
80
- ) {
115
+ ): Promise<{
116
+ manifest: { [filePath: string]: string } | undefined;
117
+ namespace: string | undefined;
118
+ }> {
119
+ if (siteAssets === undefined) {
120
+ return { manifest: undefined, namespace: undefined };
121
+ }
122
+
81
123
  const title = `__${scriptName}_sites_assets${preview ? "_preview" : ""}`;
82
124
  const { id: namespace } = await createKVNamespaceIfNotAlreadyExisting(
83
125
  title,
@@ -85,31 +127,117 @@ export async function syncAssets(
85
127
  );
86
128
 
87
129
  // let's get all the keys in this namespace
88
- const keys = new Set(
89
- (await listNamespaceKeys(accountId, namespace)).map((x) => x.name)
90
- );
130
+ const result = await listNamespaceKeys(accountId, namespace);
131
+ const keys = new Set(result.map((x) => x.name));
91
132
 
92
133
  const manifest = {};
93
- const upload = [];
134
+ const upload: {
135
+ key: string;
136
+ value: string;
137
+ base64: boolean;
138
+ }[] = [];
139
+
140
+ const include = createPatternMatcher(siteAssets.includePatterns, false);
141
+ const exclude = createPatternMatcher(siteAssets.excludePatterns, true);
94
142
  // 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);
143
+ for await (const file of getFilesInFolder(siteAssets.baseDirectory)) {
144
+ if (!include(file)) {
145
+ continue;
146
+ }
147
+ if (exclude(file)) {
148
+ continue;
149
+ }
150
+
151
+ await validateAssetSize(file);
152
+ console.log(`reading ${file}...`);
153
+ const content = await readFile(file, "base64");
154
+
155
+ const assetKey = hashAsset(file, content);
156
+ validateAssetKey(assetKey);
157
+
98
158
  // 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
- }
159
+ if (!keys.has(assetKey)) {
160
+ console.log(`uploading as ${assetKey}...`);
105
161
  upload.push({
106
- key: filePath,
162
+ key: assetKey,
107
163
  value: content,
108
164
  base64: true,
109
165
  });
166
+ } else {
167
+ console.log(`skipping - already uploaded`);
110
168
  }
111
- manifest[path.relative(dirPath, file)] = filePath;
169
+ manifest[path.relative(siteAssets.baseDirectory, file)] = assetKey;
112
170
  }
113
171
  await putBulkKeyValue(accountId, namespace, JSON.stringify(upload));
114
172
  return { manifest, namespace };
115
173
  }
174
+
175
+ function createPatternMatcher(
176
+ patterns: string[],
177
+ exclude: boolean
178
+ ): (filePath: string) => boolean {
179
+ if (patterns.length === 0) {
180
+ return (_filePath) => !exclude;
181
+ } else {
182
+ const ignorer = ignore().add(patterns);
183
+ return (filePath) => ignorer.test(filePath).ignored;
184
+ }
185
+ }
186
+
187
+ async function validateAssetSize(filePath: string) {
188
+ const { size } = await stat(filePath);
189
+ if (size > 25 * 1024 * 1024) {
190
+ throw new Error(
191
+ `File ${filePath} is too big, it should be under 25 MiB. See https://developers.cloudflare.com/workers/platform/limits#kv-limits`
192
+ );
193
+ }
194
+ }
195
+
196
+ function validateAssetKey(assetKey: string) {
197
+ if (assetKey.length > 512) {
198
+ throw new Error(
199
+ `The asset path key "${assetKey}" exceeds the maximum key size limit of 512. See https://developers.cloudflare.com/workers/platform/limits#kv-limits",`
200
+ );
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Convert a filePath to be safe to use as a relative URL.
206
+ *
207
+ * Primarily this involves converting Windows backslashes to forward slashes.
208
+ */
209
+ function urlSafe(filePath: string): string {
210
+ return filePath.replace(/\\/g, "/");
211
+ }
212
+
213
+ /**
214
+ * Information about the assets that should be uploaded
215
+ */
216
+ export interface AssetPaths {
217
+ baseDirectory: string;
218
+ includePatterns: string[];
219
+ excludePatterns: string[];
220
+ }
221
+
222
+ /**
223
+ * Get an object that describes what assets to upload, if any.
224
+ *
225
+ * Uses the args (passed from the command line) if available,
226
+ * falling back to those defined in the config.
227
+ *
228
+ * // TODO: Support for environments
229
+ */
230
+ export function getAssetPaths(
231
+ config: Config,
232
+ baseDirectory = config.site?.bucket,
233
+ includePatterns = config.site?.include ?? [],
234
+ excludePatterns = config.site?.exclude ?? []
235
+ ): undefined | AssetPaths {
236
+ return baseDirectory
237
+ ? {
238
+ baseDirectory,
239
+ includePatterns,
240
+ excludePatterns,
241
+ }
242
+ : undefined;
243
+ }
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,7 +27,9 @@ 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(
@@ -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
@@ -206,7 +206,7 @@
206
206
  */
207
207
 
208
208
  import React from "react";
209
- import { render } from "ink";
209
+ import { render, Text } from "ink";
210
210
  import Table from "ink-table";
211
211
  import SelectInput from "ink-select-input";
212
212
  import fetch from "node-fetch";
@@ -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> {
@@ -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
  }
package/src/whoami.tsx ADDED
@@ -0,0 +1,69 @@
1
+ import { Text, render } from "ink";
2
+ import Table from "ink-table";
3
+ import React from "react";
4
+ import { fetchListResult, fetchResult } from "./cfetch";
5
+ import { getAPIToken } from "./user";
6
+
7
+ export async function whoami() {
8
+ console.log("Getting User settings...");
9
+ const user = await getUserInfo();
10
+ render(<WhoAmI user={user}></WhoAmI>);
11
+ }
12
+
13
+ export function WhoAmI({ user }: { user: UserInfo | undefined }) {
14
+ return user ? (
15
+ <>
16
+ <Email tokenType={user.authType} email={user.email}></Email>
17
+ <Accounts accounts={user.accounts}></Accounts>
18
+ </>
19
+ ) : (
20
+ <Text>You are not authenticated. Please run `wrangler login`.</Text>
21
+ );
22
+ }
23
+
24
+ function Email(props: { tokenType: string; email: string }) {
25
+ return (
26
+ <Text>
27
+ 👋 You are logged in with an {props.tokenType} Token, associated with the
28
+ email &apos;{props.email}&apos;!
29
+ </Text>
30
+ );
31
+ }
32
+
33
+ function Accounts(props: { accounts: AccountInfo[] }) {
34
+ const accounts = props.accounts.map((account) => ({
35
+ "Account Name": account.name,
36
+ "Account ID": account.id,
37
+ }));
38
+ return <Table data={accounts}></Table>;
39
+ }
40
+
41
+ export interface UserInfo {
42
+ apiToken: string;
43
+ authType: string;
44
+ email: string;
45
+ accounts: AccountInfo[];
46
+ }
47
+
48
+ export async function getUserInfo(): Promise<UserInfo | undefined> {
49
+ const apiToken = getAPIToken();
50
+ return apiToken
51
+ ? {
52
+ apiToken,
53
+ authType: "OAuth",
54
+ email: await getEmail(),
55
+ accounts: await getAccounts(),
56
+ }
57
+ : undefined;
58
+ }
59
+
60
+ async function getEmail(): Promise<string> {
61
+ const { email } = await fetchResult<{ email: string }>("/user");
62
+ return email;
63
+ }
64
+
65
+ type AccountInfo = { name: string; id: string };
66
+
67
+ async function getAccounts(): Promise<AccountInfo[]> {
68
+ return await fetchListResult<AccountInfo>("/accounts");
69
+ }