@feastalytics/cli 0.1.2 → 0.1.3

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 (3) hide show
  1. package/README.md +47 -0
  2. package/dist/cli.js +274 -162
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -93,6 +93,53 @@ To refresh from a local checkout instead of GitHub, run `npx skills add ./feast
93
93
  - `FEAST_API_URL` — override the API base URL (e.g. a local dev server)
94
94
  - `FEAST_API_KEY` — override the static API key
95
95
 
96
+ ### Non-interactive credentials
97
+
98
+ `feast login` stores tokens under `~/.config/feast-cli/` and refreshes them on
99
+ demand. That does not work where there is no interactive login and no writable
100
+ home directory — most importantly inside a sandboxed agent, where the token is
101
+ supplied by the host and must never be readable by the agent itself.
102
+
103
+ Set `FEAST_ACCESS_TOKEN` for that case:
104
+
105
+ ```bash
106
+ export FEAST_ACCESS_TOKEN="$TOKEN"
107
+ export FEAST_ORGANIZATION_ID=4fafb31e-dd46-4081-9778-c298e577a1d1
108
+ export FEAST_PREFERRED_ROLE=OWNER
109
+ feast call listCampaigns
110
+ ```
111
+
112
+ - `FEAST_ACCESS_TOKEN` — access token sent verbatim as `x-access-token`
113
+ - `FEAST_ORGANIZATION_ID` — **pins** the organization
114
+ - `FEAST_PREFERRED_ROLE` — **pins** the role; a role name (`OWNER`) or a full role ARN
115
+
116
+ When `FEAST_ACCESS_TOKEN` is set the CLI reads no token from disk, refreshes
117
+ nothing, and **never decodes the token**. Normally the organization and role are
118
+ read from the token's `cognito:groups` claim, so both must be supplied instead.
119
+
120
+ Not decoding the token is the point, not a limitation: it lets the token be an
121
+ opaque placeholder that a secret store substitutes for the real value after the
122
+ request leaves the machine, so a compromised sandbox yields nothing.
123
+
124
+ `FEAST_ORGANIZATION_ID` and `FEAST_PREFERRED_ROLE` pin the session rather than
125
+ just defaulting it. `--org` and `--role` may repeat the pinned value but cannot
126
+ change it:
127
+
128
+ ```
129
+ $ FEAST_ORGANIZATION_ID=org-A feast call listCampaigns --org org-B
130
+ FEAST_ORGANIZATION_ID pins this session to org-A, so --org org-B is not allowed.
131
+ ```
132
+
133
+ Pinning the role is how you run an agent at **reduced** privilege. The API
134
+ validates a requested role against the user's real Cognito groups, so it will
135
+ happily accept `OWNER` from a user who is an owner — only the pin can hold that
136
+ user's agent down to `VIEWER`.
137
+
138
+ Neither pin is a security boundary on its own: both are ordinary environment
139
+ variables, so anything that can set env vars in the same process tree can change
140
+ them. They stop a tool call from wandering, not a determined process. Scope the
141
+ token itself if you need a hard guarantee.
142
+
96
143
  ## Development
97
144
 
98
145
  ```bash
package/dist/cli.js CHANGED
@@ -8656,146 +8656,6 @@ function errorMessage(error) {
8656
8656
  // src/main.ts
8657
8657
  import fs2 from "fs";
8658
8658
 
8659
- // src/browserLogin.ts
8660
- import { spawn } from "child_process";
8661
- import { createHash, randomBytes } from "crypto";
8662
- import http from "http";
8663
- var CALLBACK_TIMEOUT_MS = 5 * 60 * 1e3;
8664
- function base64url(buffer) {
8665
- return buffer.toString("base64url");
8666
- }
8667
- function tabHtml(title, body) {
8668
- return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title>
8669
- <style>body{font-family:-apple-system,system-ui,sans-serif;background:#0f0f0f;color:#f5f5f5;
8670
- display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0}
8671
- .card{text-align:center;padding:2rem 3rem}.t{font-size:1.4rem;font-weight:600;margin-bottom:.5rem}
8672
- .b{color:#a1a1a1}</style></head><body><div class="card">
8673
- <div class="t">${title}</div><div class="b">${body}</div></div></body></html>`;
8674
- }
8675
- function openBrowser(url) {
8676
- let command;
8677
- let args;
8678
- if (process.platform === "darwin") {
8679
- command = "open";
8680
- args = [url];
8681
- } else if (process.platform === "win32") {
8682
- command = "cmd";
8683
- args = ["/c", "start", "", url];
8684
- } else {
8685
- command = "xdg-open";
8686
- args = [url];
8687
- }
8688
- try {
8689
- const child = spawn(command, args, { stdio: "ignore", detached: true });
8690
- child.on("error", () => {
8691
- });
8692
- child.unref();
8693
- } catch {
8694
- }
8695
- }
8696
- function startLoopback(expectedState) {
8697
- return new Promise((resolveOuter, rejectOuter) => {
8698
- let resolveCode = () => {
8699
- };
8700
- let rejectCode = () => {
8701
- };
8702
- const waitForCode = new Promise((resolve, reject) => {
8703
- resolveCode = resolve;
8704
- rejectCode = reject;
8705
- });
8706
- waitForCode.catch(() => {
8707
- });
8708
- const server = http.createServer((req, res) => {
8709
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
8710
- if (url.pathname !== "/callback") {
8711
- res.writeHead(404);
8712
- res.end();
8713
- return;
8714
- }
8715
- const error = url.searchParams.get("error");
8716
- if (error != null) {
8717
- res.writeHead(200, { "Content-Type": "text/html" });
8718
- res.end(tabHtml("Authorization declined", "You can close this tab."));
8719
- rejectCode(new Error(`Authorization declined: ${error}`));
8720
- return;
8721
- }
8722
- if (url.searchParams.get("state") !== expectedState) {
8723
- res.writeHead(400, { "Content-Type": "text/html" });
8724
- res.end(
8725
- tabHtml("Invalid request", "State mismatch \u2014 retry from the CLI.")
8726
- );
8727
- rejectCode(new Error("State mismatch \u2014 aborting (possible CSRF)"));
8728
- return;
8729
- }
8730
- const code = url.searchParams.get("code");
8731
- if (code == null || code.length === 0) {
8732
- res.writeHead(400);
8733
- res.end();
8734
- rejectCode(new Error("Authorization callback did not include a code"));
8735
- return;
8736
- }
8737
- res.writeHead(200, {
8738
- "Content-Type": "text/html",
8739
- Connection: "close"
8740
- });
8741
- res.end(
8742
- tabHtml(
8743
- "You're all set \u2713",
8744
- "The feast CLI is authorized. Close this tab and return to your terminal."
8745
- )
8746
- );
8747
- resolveCode(code);
8748
- });
8749
- const timeout = setTimeout(() => {
8750
- rejectCode(
8751
- new Error("Timed out waiting for browser authorization (5 minutes)")
8752
- );
8753
- }, CALLBACK_TIMEOUT_MS);
8754
- const close = () => {
8755
- clearTimeout(timeout);
8756
- server.closeAllConnections?.();
8757
- server.close();
8758
- };
8759
- server.on("error", rejectOuter);
8760
- server.listen(0, "127.0.0.1", () => {
8761
- server.unref();
8762
- const address = server.address();
8763
- const port = typeof address === "object" && address != null ? address.port : 0;
8764
- resolveOuter({ port, waitForCode, close });
8765
- });
8766
- });
8767
- }
8768
- async function browserLogin() {
8769
- const codeVerifier = base64url(randomBytes(32));
8770
- const codeChallenge = base64url(
8771
- createHash("sha256").update(codeVerifier).digest()
8772
- );
8773
- const state = base64url(randomBytes(16));
8774
- const { port, waitForCode, close } = await startLoopback(state);
8775
- try {
8776
- const authUrl = `${getWebBaseUrl()}/oauth?port=${port}&state=${encodeURIComponent(state)}&challenge=${encodeURIComponent(codeChallenge)}`;
8777
- console.error("Opening your browser to authorize the feast CLI...");
8778
- console.error(`If it doesn't open, visit:
8779
- ${authUrl}
8780
- `);
8781
- openBrowser(authUrl);
8782
- const code = await waitForCode;
8783
- const client = createHttpCaller({});
8784
- const response = await client.api.auth.exchangeAuthCode.mutate({
8785
- code,
8786
- codeVerifier
8787
- });
8788
- const tokens = response?.tokens;
8789
- if (tokens?.accessToken?.jwtToken == null) {
8790
- throw new Error("Authorization failed: empty token response");
8791
- }
8792
- saveTokens(tokens);
8793
- return tokens;
8794
- } finally {
8795
- close();
8796
- }
8797
- }
8798
-
8799
8659
  // src/generated/manifest.ts
8800
8660
  var CLI_MANIFEST = {
8801
8661
  "awsAccountId": "891817334910",
@@ -10879,7 +10739,8 @@ var CLI_MANIFEST = {
10879
10739
  "funnel",
10880
10740
  "organizationFilePublic",
10881
10741
  "organizationFilePrivate",
10882
- "creativeLibrary"
10742
+ "creativeLibrary",
10743
+ "creativeLibraryBroll"
10883
10744
  ]
10884
10745
  },
10885
10746
  "key": {
@@ -11360,7 +11221,7 @@ var CLI_MANIFEST = {
11360
11221
  {
11361
11222
  "id": "getMediaUploadUrl",
11362
11223
  "domain": "core",
11363
- "description": "Get a presigned S3 upload URL for a media file in a given scope (passBuilder, funnel, organizationFilePublic, organizationFilePrivate, creativeLibrary). Set cropped to store a cropped variant under the scope's cropped/ subfolder. PUT the file bytes to the returned presignedUrl, then reference the returned url/key.",
11224
+ "description": "Get a presigned S3 upload URL for a media file in a given scope (passBuilder, funnel, organizationFilePublic, organizationFilePrivate, creativeLibrary, creativeLibraryBroll). Set cropped to store a cropped variant under the scope's cropped/ subfolder. PUT the file bytes to the returned presignedUrl, then reference the returned url/key.",
11364
11225
  "needsApproval": false,
11365
11226
  "type": "mutation",
11366
11227
  "path": [
@@ -11378,7 +11239,8 @@ var CLI_MANIFEST = {
11378
11239
  "funnel",
11379
11240
  "organizationFilePublic",
11380
11241
  "organizationFilePrivate",
11381
- "creativeLibrary"
11242
+ "creativeLibrary",
11243
+ "creativeLibraryBroll"
11382
11244
  ]
11383
11245
  },
11384
11246
  "fileName": {
@@ -11581,7 +11443,7 @@ var CLI_MANIFEST = {
11581
11443
  {
11582
11444
  "id": "listMedia",
11583
11445
  "domain": "core",
11584
- "description": "List uploaded media files for the organization across one or more scopes (passBuilder, funnel, organizationFilePublic, organizationFilePrivate, creativeLibrary). Each file is tagged with its scope and a canDelete flag. Set mediaOnly to return only image/video files. Set cropped to list the cropped variants (each scope's cropped/ subfolder) instead of the base files.",
11446
+ "description": "List uploaded media files for the organization across one or more scopes (passBuilder, funnel, organizationFilePublic, organizationFilePrivate, creativeLibrary, creativeLibraryBroll). Each file is tagged with its scope and a canDelete flag. Set mediaOnly to return only image/video files. Set cropped to list the cropped variants (each scope's cropped/ subfolder) instead of the base files.",
11585
11447
  "needsApproval": false,
11586
11448
  "type": "query",
11587
11449
  "path": [
@@ -11601,7 +11463,8 @@ var CLI_MANIFEST = {
11601
11463
  "funnel",
11602
11464
  "organizationFilePublic",
11603
11465
  "organizationFilePrivate",
11604
- "creativeLibrary"
11466
+ "creativeLibrary",
11467
+ "creativeLibraryBroll"
11605
11468
  ]
11606
11469
  },
11607
11470
  "minItems": 1
@@ -18265,6 +18128,234 @@ ${available}`
18265
18128
  };
18266
18129
  }
18267
18130
 
18131
+ // src/actingContext.ts
18132
+ function environmentAccessToken() {
18133
+ const token = process.env.FEAST_ACCESS_TOKEN;
18134
+ return token != null && token.length > 0 ? token : void 0;
18135
+ }
18136
+ function firstNonEmpty(...values) {
18137
+ for (const value of values) {
18138
+ if (value != null && value.length > 0) {
18139
+ return value;
18140
+ }
18141
+ }
18142
+ return void 0;
18143
+ }
18144
+ function roleLabelFromArn(arn) {
18145
+ return arn.split("/").pop()?.split(".").pop() ?? arn;
18146
+ }
18147
+ function buildPreferredRole(organizationId, role) {
18148
+ if (role.startsWith("arn:")) {
18149
+ return role;
18150
+ }
18151
+ if (!CLI_MANIFEST.organizationRoles.includes(role)) {
18152
+ throw new Error(
18153
+ `Unknown role "${role}". Expected one of ${CLI_MANIFEST.organizationRoles.join(", ")}, or a full role ARN.`
18154
+ );
18155
+ }
18156
+ return formRoleArn(organizationId, role);
18157
+ }
18158
+ function resolvePinned(pinned, flag, variableName, flagName) {
18159
+ if (pinned == null) {
18160
+ return flag;
18161
+ }
18162
+ if (flag != null && flag !== pinned) {
18163
+ throw new Error(
18164
+ `${variableName} pins this session to ${pinned}, so ${flagName} ${flag} is not allowed.`
18165
+ );
18166
+ }
18167
+ return pinned;
18168
+ }
18169
+ function resolveFromEnvironment(accessToken, orgFlag, roleFlag) {
18170
+ const organizationId = resolvePinned(
18171
+ firstNonEmpty(process.env.FEAST_ORGANIZATION_ID),
18172
+ orgFlag,
18173
+ "FEAST_ORGANIZATION_ID",
18174
+ "--org"
18175
+ );
18176
+ if (organizationId == null) {
18177
+ throw new Error(
18178
+ "FEAST_ACCESS_TOKEN is set, so the organization cannot be read from the token. Pass --org <organizationId> or set FEAST_ORGANIZATION_ID."
18179
+ );
18180
+ }
18181
+ const role = resolvePinned(
18182
+ firstNonEmpty(process.env.FEAST_PREFERRED_ROLE),
18183
+ roleFlag,
18184
+ "FEAST_PREFERRED_ROLE",
18185
+ "--role"
18186
+ );
18187
+ if (role == null) {
18188
+ throw new Error(
18189
+ "FEAST_ACCESS_TOKEN is set, so the role cannot be read from the token. Pass --role <role> or set FEAST_PREFERRED_ROLE."
18190
+ );
18191
+ }
18192
+ const preferredRole = buildPreferredRole(organizationId, role);
18193
+ return {
18194
+ accessToken,
18195
+ organizationId,
18196
+ preferredRole,
18197
+ roleLabel: roleLabelFromArn(preferredRole),
18198
+ source: "environment"
18199
+ };
18200
+ }
18201
+ async function resolveActingContext(orgFlag, roleFlag) {
18202
+ const environmentToken = environmentAccessToken();
18203
+ if (environmentToken != null) {
18204
+ return resolveFromEnvironment(environmentToken, orgFlag, roleFlag);
18205
+ }
18206
+ const tokens = await ensureFreshTokens();
18207
+ const accessToken = tokens.accessToken.jwtToken;
18208
+ const organizationId = resolveOrganizationId(accessToken, orgFlag);
18209
+ const resolved = resolvePreferredRole(accessToken, organizationId);
18210
+ return {
18211
+ accessToken,
18212
+ organizationId,
18213
+ preferredRole: resolved.arn,
18214
+ roleLabel: resolved.orgRole.role,
18215
+ source: "credentials"
18216
+ };
18217
+ }
18218
+
18219
+ // src/browserLogin.ts
18220
+ import { spawn } from "child_process";
18221
+ import { createHash, randomBytes } from "crypto";
18222
+ import http from "http";
18223
+ var CALLBACK_TIMEOUT_MS = 5 * 60 * 1e3;
18224
+ function base64url(buffer) {
18225
+ return buffer.toString("base64url");
18226
+ }
18227
+ function tabHtml(title, body) {
18228
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title>
18229
+ <style>body{font-family:-apple-system,system-ui,sans-serif;background:#0f0f0f;color:#f5f5f5;
18230
+ display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0}
18231
+ .card{text-align:center;padding:2rem 3rem}.t{font-size:1.4rem;font-weight:600;margin-bottom:.5rem}
18232
+ .b{color:#a1a1a1}</style></head><body><div class="card">
18233
+ <div class="t">${title}</div><div class="b">${body}</div></div></body></html>`;
18234
+ }
18235
+ function openBrowser(url) {
18236
+ let command;
18237
+ let args;
18238
+ if (process.platform === "darwin") {
18239
+ command = "open";
18240
+ args = [url];
18241
+ } else if (process.platform === "win32") {
18242
+ command = "cmd";
18243
+ args = ["/c", "start", "", url];
18244
+ } else {
18245
+ command = "xdg-open";
18246
+ args = [url];
18247
+ }
18248
+ try {
18249
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
18250
+ child.on("error", () => {
18251
+ });
18252
+ child.unref();
18253
+ } catch {
18254
+ }
18255
+ }
18256
+ function startLoopback(expectedState) {
18257
+ return new Promise((resolveOuter, rejectOuter) => {
18258
+ let resolveCode = () => {
18259
+ };
18260
+ let rejectCode = () => {
18261
+ };
18262
+ const waitForCode = new Promise((resolve, reject) => {
18263
+ resolveCode = resolve;
18264
+ rejectCode = reject;
18265
+ });
18266
+ waitForCode.catch(() => {
18267
+ });
18268
+ const server = http.createServer((req, res) => {
18269
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
18270
+ if (url.pathname !== "/callback") {
18271
+ res.writeHead(404);
18272
+ res.end();
18273
+ return;
18274
+ }
18275
+ const error = url.searchParams.get("error");
18276
+ if (error != null) {
18277
+ res.writeHead(200, { "Content-Type": "text/html" });
18278
+ res.end(tabHtml("Authorization declined", "You can close this tab."));
18279
+ rejectCode(new Error(`Authorization declined: ${error}`));
18280
+ return;
18281
+ }
18282
+ if (url.searchParams.get("state") !== expectedState) {
18283
+ res.writeHead(400, { "Content-Type": "text/html" });
18284
+ res.end(
18285
+ tabHtml("Invalid request", "State mismatch \u2014 retry from the CLI.")
18286
+ );
18287
+ rejectCode(new Error("State mismatch \u2014 aborting (possible CSRF)"));
18288
+ return;
18289
+ }
18290
+ const code = url.searchParams.get("code");
18291
+ if (code == null || code.length === 0) {
18292
+ res.writeHead(400);
18293
+ res.end();
18294
+ rejectCode(new Error("Authorization callback did not include a code"));
18295
+ return;
18296
+ }
18297
+ res.writeHead(200, {
18298
+ "Content-Type": "text/html",
18299
+ Connection: "close"
18300
+ });
18301
+ res.end(
18302
+ tabHtml(
18303
+ "You're all set \u2713",
18304
+ "The feast CLI is authorized. Close this tab and return to your terminal."
18305
+ )
18306
+ );
18307
+ resolveCode(code);
18308
+ });
18309
+ const timeout = setTimeout(() => {
18310
+ rejectCode(
18311
+ new Error("Timed out waiting for browser authorization (5 minutes)")
18312
+ );
18313
+ }, CALLBACK_TIMEOUT_MS);
18314
+ const close = () => {
18315
+ clearTimeout(timeout);
18316
+ server.closeAllConnections?.();
18317
+ server.close();
18318
+ };
18319
+ server.on("error", rejectOuter);
18320
+ server.listen(0, "127.0.0.1", () => {
18321
+ server.unref();
18322
+ const address = server.address();
18323
+ const port = typeof address === "object" && address != null ? address.port : 0;
18324
+ resolveOuter({ port, waitForCode, close });
18325
+ });
18326
+ });
18327
+ }
18328
+ async function browserLogin() {
18329
+ const codeVerifier = base64url(randomBytes(32));
18330
+ const codeChallenge = base64url(
18331
+ createHash("sha256").update(codeVerifier).digest()
18332
+ );
18333
+ const state = base64url(randomBytes(16));
18334
+ const { port, waitForCode, close } = await startLoopback(state);
18335
+ try {
18336
+ const authUrl = `${getWebBaseUrl()}/oauth?port=${port}&state=${encodeURIComponent(state)}&challenge=${encodeURIComponent(codeChallenge)}`;
18337
+ console.error("Opening your browser to authorize the feast CLI...");
18338
+ console.error(`If it doesn't open, visit:
18339
+ ${authUrl}
18340
+ `);
18341
+ openBrowser(authUrl);
18342
+ const code = await waitForCode;
18343
+ const client = createHttpCaller({});
18344
+ const response = await client.api.auth.exchangeAuthCode.mutate({
18345
+ code,
18346
+ codeVerifier
18347
+ });
18348
+ const tokens = response?.tokens;
18349
+ if (tokens?.accessToken?.jwtToken == null) {
18350
+ throw new Error("Authorization failed: empty token response");
18351
+ }
18352
+ saveTokens(tokens);
18353
+ return tokens;
18354
+ } finally {
18355
+ close();
18356
+ }
18357
+ }
18358
+
18268
18359
  // src/prompt.ts
18269
18360
  import readline from "readline";
18270
18361
  function promptText(question) {
@@ -18349,6 +18440,7 @@ Commands:
18349
18440
 
18350
18441
  Call options:
18351
18442
  --org <organizationId> Organization to act on (required unless you belong to exactly one)
18443
+ --role <role|roleArn> Role to act as, e.g. OWNER (required when FEAST_ACCESS_TOKEN is set)
18352
18444
  --input <json> Tool input as a JSON string (default: {})
18353
18445
  --input-file <path> Tool input from a JSON file
18354
18446
  --yes Skip the confirmation prompt for mutations
@@ -18356,11 +18448,18 @@ Call options:
18356
18448
  Environment:
18357
18449
  FEAST_API_URL Override the API base URL (default: production)
18358
18450
  FEAST_WEB_URL Override the web app base URL for browser login (default: production)
18359
- FEAST_NO_UPDATE_CHECK Disable the daily check for a newer published version`;
18451
+ FEAST_NO_UPDATE_CHECK Disable the daily check for a newer published version
18452
+
18453
+ Non-interactive credentials (for sandboxed agents):
18454
+ FEAST_ACCESS_TOKEN Access token to send verbatim. When set, no token is read
18455
+ from disk and none is refreshed, and the token is never
18456
+ decoded \u2014 so the organization and role must be supplied too.
18457
+ FEAST_ORGANIZATION_ID Pins the organization. --org must match it or be omitted.
18458
+ FEAST_PREFERRED_ROLE Pins the role. --role must match it or be omitted.`;
18360
18459
  function parseArgs(argv) {
18361
18460
  const positional = [];
18362
18461
  const flags = {};
18363
- const valueFlags = /* @__PURE__ */ new Set(["org", "input", "input-file", "domain"]);
18462
+ const valueFlags = /* @__PURE__ */ new Set(["org", "role", "input", "input-file", "domain"]);
18364
18463
  for (let i = 0; i < argv.length; i++) {
18365
18464
  const arg = argv[i];
18366
18465
  if (!arg.startsWith("--")) {
@@ -18382,6 +18481,11 @@ function parseArgs(argv) {
18382
18481
  return { positional, flags };
18383
18482
  }
18384
18483
  async function commandLogin(args) {
18484
+ if (environmentAccessToken() != null) {
18485
+ throw new Error(
18486
+ "FEAST_ACCESS_TOKEN is set, so this CLI is using credentials from the environment. Unset it to log in and store tokens on disk."
18487
+ );
18488
+ }
18385
18489
  if (args.flags["password"] === true) {
18386
18490
  const username = args.positional[0] ?? await promptText("Username: ");
18387
18491
  if (username.length === 0) {
@@ -18402,7 +18506,20 @@ async function commandLogin(args) {
18402
18506
  console.info(`Logged in as ${payload.username ?? payload.sub}`);
18403
18507
  console.info(`Tokens stored in ${getCredentialsPath()}`);
18404
18508
  }
18405
- async function commandWhoami() {
18509
+ async function commandWhoami(args) {
18510
+ if (environmentAccessToken() != null) {
18511
+ const acting = await resolveActingContext(
18512
+ args.flags["org"],
18513
+ args.flags["role"]
18514
+ );
18515
+ console.info("Credentials: FEAST_ACCESS_TOKEN (environment)");
18516
+ console.info(`Organization: ${acting.organizationId}`);
18517
+ console.info(`Role: ${acting.roleLabel}`);
18518
+ console.info(
18519
+ "The token is opaque to this CLI, so the user and their other organizations cannot be listed."
18520
+ );
18521
+ return;
18522
+ }
18406
18523
  const tokens = await ensureFreshTokens();
18407
18524
  const payload = decodeJwtPayload(tokens.accessToken.jwtToken);
18408
18525
  const orgRoles = listOrgRoles(tokens.accessToken.jwtToken);
@@ -18544,25 +18661,20 @@ ${issues.join("\n")}
18544
18661
  Run: feast describe ${tool.id}`
18545
18662
  );
18546
18663
  }
18547
- const tokens = await ensureFreshTokens();
18548
- const isWrite = tool.type === "mutation" || tool.needsApproval;
18549
- const organizationId = resolveOrganizationId(
18550
- tokens.accessToken.jwtToken,
18551
- args.flags["org"]
18552
- );
18553
- const resolved = resolvePreferredRole(
18554
- tokens.accessToken.jwtToken,
18555
- organizationId
18664
+ const acting = await resolveActingContext(
18665
+ args.flags["org"],
18666
+ args.flags["role"]
18556
18667
  );
18668
+ const isWrite = tool.type === "mutation" || tool.needsApproval;
18557
18669
  console.error(
18558
- `Acting on organization ${organizationId} as ${resolved.orgRole.role}`
18670
+ `Acting on organization ${acting.organizationId} as ${acting.roleLabel}`
18559
18671
  );
18560
18672
  const client = createHttpCaller({
18561
- accessToken: tokens.accessToken.jwtToken,
18562
- preferredRole: resolved.arn
18673
+ accessToken: acting.accessToken,
18674
+ preferredRole: acting.preferredRole
18563
18675
  });
18564
18676
  if (isWrite) {
18565
- const organization = await verifyOrganization(client, organizationId);
18677
+ const organization = await verifyOrganization(client, acting.organizationId);
18566
18678
  if (args.flags["yes"] !== true) {
18567
18679
  const orgLabel = organization.name != null ? `"${organization.name}" (${organization.id})` : organization.id;
18568
18680
  const approved = await promptConfirm(
@@ -18590,7 +18702,7 @@ async function runCli(argv) {
18590
18702
  console.info("Logged out");
18591
18703
  break;
18592
18704
  case "whoami":
18593
- await commandWhoami();
18705
+ await commandWhoami(args);
18594
18706
  break;
18595
18707
  case "tools":
18596
18708
  commandTools(args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feastalytics/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Command-line client for the Feastalytics platform — list, create, and update campaigns, automations, offers, and members-program rewards from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {